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/dist/model.js CHANGED
@@ -1,9 +1,8 @@
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
- import { addDisposer, applySnapshot, cast, getSnapshot, isAlive, types, } from '@jbrowse/mobx-state-tree';
3
+ import { addDisposer, applySnapshot, cast, getSnapshot, types, } from '@jbrowse/mobx-state-tree';
5
4
  import { autorun, compareStructural, computed, reaction, transaction, } from 'mobx';
6
- import { generateNodeIds, gffToAnnotations, interProScanToAnnotations, parseEmfTree, parseGFF, parseMSA, parseNewick, } from 'msa-parsers';
5
+ import { generateNodeIds, gffToAnnotations, parseGFF, parseMSA, } from 'msa-parsers';
7
6
  import { blockFasta } from './blockFasta.js';
8
7
  import { calculateBlocks } from './calculateBlocks.js';
9
8
  import { clustalXColumnColors } from './clustalX.js';
@@ -12,41 +11,36 @@ import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.js';
12
11
  import { columnStats } from './columnStats.js';
13
12
  import { packDomainLanes } from './components/msa/packDomainLanes.js';
14
13
  import { visibleColRange } from './components/msa/visibleColRange.js';
15
- import TrackBlocks from './components/tracks/TrackBlocks.js';
16
14
  import { cladeGutterWidth } from './components/tree/cladeBrackets.js';
17
15
  import { scaleBarLength } from './components/tree/scaleBar.js';
18
- import { cladeHighlightAlpha, cladeHighlightColor, defaultAllowedGappyness, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawMsaLetters, defaultFeaturePanelWidth, defaultHeight, defaultHideGaps, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultScrollZoomAxis, defaultShowDomainLegend, defaultShowDomains, defaultSubFeatureRows, fontSizeFor, labelFitFontSize, maxCellSize, maxFitTreeWidth, maxTreeAreaShare, maxInlineSnapshotBytes as defaultMaxInlineSnapshotBytes, maxNeighborJoiningRows, minColWidth, minLetterColWidth, minLetterRowHeight, minRowHeight, minTreeWidth, pxPerTreeDepth, rowPanelHeaderHeight, rowTintAlpha, scrollZoomAxes, segmentFeatureTypes, segmentShades, treeScaleBarHeight, } from './constants.js';
16
+ import { defaultAllowedGappyness, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawMsaLetters, defaultHeight, defaultHideGaps, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultScrollZoomAxis, defaultShowDomainLegend, defaultShowDomains, defaultSubFeatureRows, fontSizeFor, labelFitFontSize, maxCellSize, maxFitTreeWidth, maxTreeAreaShare, maxInlineSnapshotBytes as defaultMaxInlineSnapshotBytes, maxNeighborJoiningRows, minColWidth, minLetterColWidth, minLetterRowHeight, minRowHeight, minTreeWidth, pxPerTreeDepth, rowPanelHeaderHeight, rowTintAlpha, scrollZoomAxes, segmentFeatureTypes, segmentShades, settingsPanelWidth, treeScaleBarHeight, } from './constants.js';
19
17
  import { createPaletteMap } from './createPaletteMap.js';
20
18
  import { exportFileName } from './exportFileName.js';
21
- import { featureField, featureName } from './featureFields.js';
19
+ import { featureName } from './featureFields.js';
20
+ import { colorByRow, featureColorMap, featureLabelMap, featurePanelSpans, resolveFeatureScale, rowPanelWidth, stringFields, } from './featurePanels.js';
22
21
  import { featuresToAnnotations } from './featuresLayer.js';
23
- import { fetchTextWithProgress, isAbortError } from './fetchUtils.js';
24
- import { flatToTree } from './flatToTree.js';
25
- import { calcDepthToLeaf, clusterLayout, collapse, find, forEachDescendant, hierarchy, layoutTree, leafIndex, leaves, maxLength, mrca, nodeCoveringRows, setBrLength, sort, sum as hierarchySum, } from './hierarchy.js';
22
+ import { installFilehandleLoader } from './filehandleLoader.js';
23
+ import { find, hierarchy, layoutTree, leaves, nodeCoveringRows, } from './hierarchy.js';
26
24
  import { DataModelF } from './model/DataModel.js';
27
25
  import { DialogQueueSessionMixin } from './model/DialogQueue.js';
28
26
  import { MSAModelF } from './model/msaModel.js';
29
27
  import { TreeModelF } from './model/treeModel.js';
30
28
  import { calculateNeighborJoiningTree } from './neighborJoining.js';
31
- import { parseAsn1 } from './parseAsn1.js';
32
29
  import { calculatePropertyConservation } from './propertyConservation.js';
33
- import { midpointRoot, rerootTree } from './rerootTree.js';
30
+ import { preorder } from './rerootTree.js';
34
31
  import { globalColToVisibleCol, seqPosOfGlobalCol, visibleColToGlobalCol, visibleColsBefore, } from './rowCoordinateCalculations.js';
35
32
  import { resolveScale } from './scales.js';
36
33
  import { buildSeqPosIndex } from './seqPosToGlobalCol.js';
37
34
  import { maxBitsFor } from './sequenceLogo.js';
35
+ import { blosum62ColumnColors, identityColumnColors, } from './similarityColors.js';
38
36
  import { stripDefault } from './stripDefault.js';
39
37
  import { treeTips, widestLabel } from './tipLabels.js';
40
- import { computeRowInsertions, dropBlanks, len, outlineColor, skipBlanks, withAlpha, } from './util.js';
38
+ import { buildTreeRoot, outgroupNode, parseTreeText, resolveClades, rootTree, tipRange, } from './treeDisplay.js';
39
+ import { computeRowInsertions, dropBlanks, len, readComputed, skipBlanks, withAlpha, } from './util.js';
41
40
  import { saveAs } from './vendor/fileSaver.js';
42
41
  import { writeNewick } from './writeNewick.js';
43
42
  import { parseWuss } from './wuss.js';
44
- function parseTreeText(text) {
45
- if (text.startsWith('BioTreeContainer')) {
46
- return flatToTree(parseAsn1(text));
47
- }
48
- return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text);
49
- }
43
+ import { takeZoomSnapshot, zoomSnapshotCovers } from './zoomSnapshot.js';
50
44
  // The height each kind draws at before the user drags a divider. A text track
51
45
  // is absent: it is one alignment row tall and follows rowHeight, so the zoom
52
46
  // controls already size it.
@@ -70,186 +64,6 @@ const ownHeightKey = (id) => `own:${id}`;
70
64
  // them
71
65
  const HELIX_ARC = '#4e79a7';
72
66
  const PSEUDOKNOT_ARC = '#e15759';
73
- // the fill of a span under a scale that gives its value no color, such as a
74
- // gene a `map` over gene names leaves out
75
- const UNSCALED_FEATURE = '#d9d9d9';
76
- // the fraction of a span a following one may cover before it takes a lane of
77
- // its own in a `features` panel
78
- const laneOverlap = 0.1;
79
- // a row panel's own width, or a default: the row height, which makes a strip
80
- // cell square, and a gene neighborhood's width for a features panel. The row
81
- // height is read only when used, so a panel with a width of its own does not
82
- // recompute on a vertical zoom
83
- function rowPanelWidth(panel, view) {
84
- return (panel.width ??
85
- (panel.kind === 'features' ? defaultFeaturePanelWidth : view.rowHeight));
86
- }
87
- // one row of the row table as the channels read it. A number or a boolean
88
- // reads as its string, the way a GFF value does, and null or an object drops
89
- function stringFields(row) {
90
- if (typeof row !== 'object' || row === null) {
91
- return undefined;
92
- }
93
- const fields = {};
94
- for (const [field, value] of Object.entries(row)) {
95
- if (typeof value === 'string') {
96
- fields[field] = value;
97
- }
98
- else if (typeof value === 'number' || typeof value === 'boolean') {
99
- fields[field] = String(value);
100
- }
101
- }
102
- return fields;
103
- }
104
- // the color each row takes from a field of the row table under a scale
105
- function colorByRow(rowData, field, colorOf) {
106
- const colors = new Map();
107
- for (const [name, row] of Object.entries(rowData)) {
108
- const value = row?.[field];
109
- const color = value === undefined ? undefined : colorOf(value);
110
- if (color) {
111
- colors.set(name, color);
112
- }
113
- }
114
- return colors;
115
- }
116
- // the fill and outline of every feature in a list: its own GFF `color=` first,
117
- // then the scale an encoding resolves over one of its fields, then the
118
- // accession palette
119
- function featureColorMap(annotations, encoding, fillPalette) {
120
- const strokes = new Map();
121
- const strokeOf = (fill) => {
122
- const hit = strokes.get(fill);
123
- if (hit !== undefined) {
124
- return hit;
125
- }
126
- const stroke = outlineColor(fill);
127
- strokes.set(fill, stroke);
128
- return stroke;
129
- };
130
- return new Map(annotations.map(annotation => {
131
- const value = encoding
132
- ? featureField(annotation, encoding.field)
133
- : undefined;
134
- const fill = annotation.color ??
135
- (value === undefined ? undefined : encoding.colorOf(value)) ??
136
- (encoding ? UNSCALED_FEATURE : fillPalette[annotation.accession]);
137
- return [annotation, { fill, stroke: strokeOf(fill) }];
138
- }));
139
- }
140
- // the text a label channel draws inside each span, for the features carrying
141
- // the field it names
142
- function featureLabelMap(annotations, field) {
143
- const labels = new Map();
144
- for (const annotation of annotations) {
145
- const value = featureField(annotation, field);
146
- if (value !== undefined) {
147
- labels.set(annotation, value);
148
- }
149
- }
150
- return labels;
151
- }
152
- /**
153
- * Lanes for `position: "strandpile"`, gggenomes' `position_strandpile`: the
154
- * forward features stack above the line and the reverse below it, each strand
155
- * packing on its own. The deepest row on each side sets the grid every row lays
156
- * out on, so the line between the strands sits at one height down the panel and
157
- * a reader can scan it. A feature with no strand piles with the forward ones.
158
- */
159
- function strandpileLanes(rows) {
160
- const piled = rows.map(([name, bands]) => {
161
- const reverse = packDomainLanes(bands.filter(b => b.annotation.strand === -1));
162
- const forward = packDomainLanes(bands.filter(b => b.annotation.strand !== -1));
163
- return { name, forward, reverse };
164
- });
165
- const up = Math.max(0, ...piled.map(p => p.forward[0]?.laneCount ?? 0));
166
- const down = Math.max(0, ...piled.map(p => p.reverse[0]?.laneCount ?? 0));
167
- const laneCount = up + down;
168
- return new Map(piled.map(({ name, forward, reverse }) => [
169
- name,
170
- [
171
- // forward level 0 is the lane just above the line, stacking upward
172
- ...forward.map(b => ({ ...b, lane: up - 1 - b.lane, laneCount })),
173
- ...reverse.map(b => ({ ...b, lane: up + b.lane, laneCount })),
174
- ],
175
- ]));
176
- }
177
- /** one row's spans laid out by the panel's `position` */
178
- function panelLanes(panel, rows) {
179
- return panel.position === 'strandpile'
180
- ? strandpileLanes(rows)
181
- : new Map(rows.map(([name, bands]) => [name, packDomainLanes(bands)]));
182
- }
183
- /**
184
- * The spans a `features` panel draws, keyed by row name and measured in the
185
- * panel's own pixels. `column` takes the bands the overlay draws, at the
186
- * alignment's column width. `position` packs each row's features in its own
187
- * residue positions, shifted by an `align` transform, and maps the extent they
188
- * cover across every row onto the panel width, so a row with no alignment
189
- * still has an x.
190
- */
191
- function featurePanelSpans({ panel, width, colWidth, domainBands, annotationsByRow, shifts, }) {
192
- if (panel.x === 'column') {
193
- return panelLanes(panel, [...domainBands].map(([name, bands]) => [
194
- name,
195
- bands.map(band => ({
196
- ...band,
197
- xStart: band.startCol * colWidth,
198
- xEnd: band.endCol * colWidth,
199
- })),
200
- ]));
201
- }
202
- const shifted = Object.entries(annotationsByRow).map(([name, annotations]) => {
203
- const shift = shifts?.get(name) ?? 0;
204
- return [
205
- name,
206
- annotations.map(annotation => ({
207
- annotation,
208
- start: annotation.start - 1 + shift,
209
- end: annotation.end + shift,
210
- })),
211
- ];
212
- });
213
- let min = Infinity;
214
- let max = -Infinity;
215
- for (const [, features] of shifted) {
216
- for (const { start, end } of features) {
217
- min = Math.min(min, start);
218
- max = Math.max(max, end);
219
- }
220
- }
221
- // a gene arrow ends where its feature does, so the extent maps onto the whole
222
- // panel less the pixel the rightmost stroke needs
223
- const drawable = Math.max(1, width - 1);
224
- const scale = max > min ? drawable / (max - min) : 0;
225
- // a lane opens where a span covers more than a tenth of the one before it,
226
- // which keeps the genes of an operon on one lane: adjacent genes commonly
227
- // share a few bases, and a stop codon overlapping the next start reads as a
228
- // second lane over the whole row
229
- return panelLanes(panel, shifted.map(([name, features]) => [
230
- name,
231
- features.map(({ annotation, start, end }) => ({
232
- annotation,
233
- xStart: (start - min) * scale,
234
- xEnd: (end - min) * scale,
235
- startCol: start,
236
- endCol: end - (end - start) * laneOverlap,
237
- })),
238
- ]));
239
- }
240
- // a scale over the values a field takes across every feature loaded, so hiding
241
- // a type in the filter dialog leaves the rest in their colors, and a legend of
242
- // the values the features drawn carry
243
- function resolveFeatureScale(field, scale, annotations, drawn) {
244
- const valuesOf = (list) => list.map(a => featureField(a, field)).filter(notEmpty);
245
- const resolved = resolveScale(scale, valuesOf(annotations));
246
- const shown = new Set(valuesOf(drawn));
247
- return {
248
- field,
249
- ...resolved,
250
- legend: resolved.legend.filter(entry => shown.has(entry.id)),
251
- };
252
- }
253
67
  const columnTrackBytes = new WeakMap();
254
68
  function columnTrackSize(track) {
255
69
  let bytes = columnTrackBytes.get(track);
@@ -302,6 +116,7 @@ export const preservedOnReset = new Set([
302
116
  'drawNodeBubbles',
303
117
  'drawNodeLabels',
304
118
  'showTreeOverview',
119
+ 'showTreeScaleBar',
305
120
  'overviewHeight',
306
121
  'turnedOffTracks',
307
122
  'trackHeights',
@@ -309,172 +124,17 @@ export const preservedOnReset = new Set([
309
124
  'allowedGappyness',
310
125
  'subFeatureRows',
311
126
  'showDomainLegend',
127
+ 'domainFill',
128
+ 'embeddedMode',
312
129
  ]);
313
130
  // `turnedOffTracks` holds only the user's explicit choices, so a hidden-by-default
314
- // track writes nothing into the snapshot. A file-supplied track passes its own
315
- // `defaultOff`, since the count varies by file: a Pfam seed has a couple of #=GR
316
- // lines, an Rfam family one per row.
317
- function trackIsOff(turnedOffTracks, id, defaultOff) {
318
- return turnedOffTracks.get(id) ?? (defaultOff || defaultOffTracks.has(id));
131
+ // track writes nothing into the snapshot
132
+ function trackIsOff(turnedOffTracks, id) {
133
+ return turnedOffTracks.get(id) ?? defaultOffTracks.has(id);
319
134
  }
320
- // the marks that set tree state from a node rather than drawing
321
- const seedingMarks = new Set(['collapse', 'focus', 'rotate']);
322
135
  // shared empty results, so observers don't see a fresh [] as a change
323
136
  const noDomains = [];
324
137
  const noClades = [];
325
- /**
326
- * The rows a clade covers and the node an `mrca` names, or undefined when the
327
- * clade does not resolve: a tip name the tree does not have or has twice, or a
328
- * leaf count `tips` disagrees with. `index` and `rowNamesSet` are the memoized
329
- * passes over the tree.
330
- */
331
- function cladeRows(clade, root, index, rowNamesSet) {
332
- if (clade.range) {
333
- const [a, b] = clade.range.map(name => index.get(name) ? rowNamesSet.get(name) : undefined);
334
- if (a === undefined || b === undefined) {
335
- return undefined;
336
- }
337
- const rows = a <= b ? [a, b] : [b, a];
338
- return rows[1] - rows[0] + 1 === clade.tips ? { rows } : undefined;
339
- }
340
- const node = clade.mrca ? mrca(root, clade.mrca, index) : undefined;
341
- if (!node) {
342
- return undefined;
343
- }
344
- const tips = leaves(node);
345
- if (tips.length !== clade.tips) {
346
- return undefined;
347
- }
348
- let first = Infinity;
349
- let last = -Infinity;
350
- for (const tip of tips) {
351
- const row = rowNamesSet.get(tip.data.name);
352
- if (row !== undefined) {
353
- first = Math.min(first, row);
354
- last = Math.max(last, row);
355
- }
356
- }
357
- return first <= last
358
- ? { rows: [first, last], nodeId: node.data.id }
359
- : undefined;
360
- }
361
- /**
362
- * Every clade that resolves, with its fill color settled, against the leaf
363
- * order `rowNamesSet` gives. The tree panel resolves against the displayed
364
- * rows and the overview against its own, so both take the tree they draw. A
365
- * `range` record names no node, so the seeding marks, which need one, drop it.
366
- */
367
- function resolveClades(clades, root, rowNamesSet) {
368
- const index = leafIndex(root);
369
- return clades.flatMap(clade => {
370
- const resolved = cladeRows(clade, root, index, rowNamesSet);
371
- const seeding = seedingMarks.has(clade.mark);
372
- return resolved && !(seeding && resolved.nodeId === undefined)
373
- ? [
374
- {
375
- ...resolved,
376
- mark: clade.mark,
377
- color: withAlpha(clade.color ?? cladeHighlightColor, cladeHighlightAlpha),
378
- markColor: clade.color,
379
- label: clade.label,
380
- },
381
- ]
382
- : [];
383
- });
384
- }
385
- const childOrders = {
386
- input: undefined,
387
- branchLength: (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1),
388
- ladderize: (a, b) => a.value - b.value,
389
- ladderizeReverse: (a, b) => b.value - a.value,
390
- };
391
- /**
392
- * The tree with the display transforms applied: leaf counts summed, children
393
- * put in `order` and each `rotated` node's reversed, `showOnly` taken as the
394
- * new root, and each `collapsed` clade folded. The overview builds the same
395
- * tree without the focus, so it shows the focused subtree inside the whole.
396
- */
397
- function buildTreeRoot(tree, { collapsed, rotated, order, showOnly, }) {
398
- let hier = hierarchy(tree, d => d.children);
399
- hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1));
400
- const compare = childOrders[order];
401
- if (compare) {
402
- sort(hier, compare);
403
- }
404
- if (rotated.length > 0) {
405
- const flip = new Set(rotated);
406
- forEachDescendant(hier, n => {
407
- if (flip.has(n.data.id)) {
408
- n.children?.reverse();
409
- }
410
- });
411
- }
412
- if (showOnly) {
413
- const res = find(hier, n => n.data.id === showOnly);
414
- if (res) {
415
- hier = res;
416
- }
417
- }
418
- for (const collapsedId of collapsed) {
419
- const node = find(hier, n => n.data.id === collapsedId);
420
- if (node) {
421
- if (node.children) {
422
- collapse(node);
423
- }
424
- else if (node.parent?.children) {
425
- node.parent.children = node.parent.children.filter(c => c.data.id !== collapsedId);
426
- }
427
- }
428
- }
429
- return hier;
430
- }
431
- /**
432
- * The node whose branch an outgroup roots the tree on: the outgroup's most
433
- * recent common ancestor, or the rest of the tips' where that ancestor is the
434
- * root, since the outgroup then straddles the root and the branch above the
435
- * other side is the same branch of the unrooted tree.
436
- */
437
- function outgroupNode(root, outgroup) {
438
- const index = leafIndex(root);
439
- const node = mrca(root, outgroup, index);
440
- if (node !== root) {
441
- return node;
442
- }
443
- const out = new Set(outgroup);
444
- const rest = leaves(root)
445
- .map(n => n.data.name)
446
- .filter(name => !out.has(name));
447
- const other = mrca(root, rest, index);
448
- return other === root ? undefined : other;
449
- }
450
- /**
451
- * `tree` rerooted as `treeRoot` asks, with fresh path ids. The ids of the tree
452
- * as written become names on unnamed nodes, and a rerooting would carry them
453
- * along as labels, so they come off first.
454
- */
455
- function rootTree(tree, treeRoot) {
456
- const plain = new Map();
457
- for (const node of preorder(tree).reverse()) {
458
- plain.set(node, {
459
- name: node.name === node.id ? undefined : node.name,
460
- length: node.length,
461
- children: node.children.map(c => plain.get(c)),
462
- });
463
- }
464
- const input = plain.get(tree);
465
- if (treeRoot === 'midpoint') {
466
- return generateNodeIds(midpointRoot(input));
467
- }
468
- const target = outgroupNode(hierarchy(tree, d => d.children), treeRoot.outgroup);
469
- return target
470
- ? generateNodeIds(rerootTree(input, plain.get(target.data)))
471
- : tree;
472
- }
473
- // the inclusive tip indices a node covers, from the row-space extent
474
- // clusterLayout writes to xMin/xMax as tip centers
475
- function tipRange(node) {
476
- return [Math.round(node.xMin - 0.5), Math.round(node.xMax - 0.5)];
477
- }
478
138
  // the channels reading the feature table; every other channel reads rowData
479
139
  const featureChannels = new Set([
480
140
  'featureFill',
@@ -483,6 +143,15 @@ const featureChannels = new Set([
483
143
  // seqPos -> column indexes per row, keyed on the parse so they are garbage
484
144
  // collected with it. A computed would rebuild every row's index when read
485
145
  // outside a reactive context.
146
+ // Newick tools write a colon in a name as an underscore, so a tree's row
147
+ // `chr1_100-200` is the GFF's `chr1:100-200`, as FastaMSA.getRow resolves it
148
+ function annotationRowName(id, rowNames) {
149
+ if (rowNames.has(id)) {
150
+ return id;
151
+ }
152
+ const underscored = id.replaceAll(':', '_');
153
+ return rowNames.has(underscored) ? underscored : id;
154
+ }
486
155
  const seqPosIndexCache = new WeakMap();
487
156
  // A segment is a 1:1 run. A segment whose sides differ in length is malformed,
488
157
  // and the lookups treat it like an uncovered position.
@@ -490,6 +159,22 @@ function sameLength(segment) {
490
159
  return (segment.rowEnd - segment.rowStart ===
491
160
  segment.structEnd - segment.structStart);
492
161
  }
162
+ // A zoom that passes the default cell size stops on it, and one that starts on
163
+ // it holds there until the wheel has gathered `zoomDetent` worth of scale, so
164
+ // a zoom in or out finds its way back to the default
165
+ const zoomDetent = Math.log(1.3);
166
+ function detentZoom(value, scale, home, held, min, canHold) {
167
+ if (value === home && canHold) {
168
+ const total = held + Math.log(scale);
169
+ return Math.abs(total) < zoomDetent
170
+ ? { value, held: total }
171
+ : { value: clamp(home * Math.exp(total), min, maxCellSize), held: 0 };
172
+ }
173
+ const next = clamp(value * scale, min, maxCellSize);
174
+ return (value - home) * (next - home) < 0
175
+ ? { value: home, held: 0 }
176
+ : { value: next, held: 0 };
177
+ }
493
178
  // fit() divides the viewport by the row or column count and multiplies back, so
494
179
  // an exact fit can land a fraction of a pixel over. A plain `>` would then show
495
180
  // a scrollbar that shrinks the viewport and leaves a gap.
@@ -521,19 +206,6 @@ function sharedValue(values) {
521
206
  ? first
522
207
  : undefined;
523
208
  }
524
- // preorder, so reversing the list puts every node after its descendants
525
- function preorder(tree) {
526
- const order = [];
527
- const stack = [tree];
528
- while (stack.length > 0) {
529
- const node = stack.pop();
530
- order.push(node);
531
- for (const child of node.children) {
532
- stack.push(child);
533
- }
534
- }
535
- return order;
536
- }
537
209
  /**
538
210
  * #stateModel MsaView
539
211
  *
@@ -571,6 +243,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
571
243
  * #property
572
244
  */
573
245
  showDomains: stripDefault(types.boolean, defaultShowDomains),
246
+ /**
247
+ * #property
248
+ * whether shown domains fill the cells they cover. Off, the residue
249
+ * scheme keeps the cells and each domain draws as a bar under its
250
+ * row. See `cellFill`.
251
+ */
252
+ domainFill: stripDefault(types.boolean, true),
574
253
  /**
575
254
  * #property
576
255
  * whether the domain legend is expanded. The legend floats over the
@@ -599,6 +278,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
599
278
  * #property
600
279
  */
601
280
  drawMsaLetters: stripDefault(types.boolean, defaultDrawMsaLetters),
281
+ /**
282
+ * #property
283
+ * draws the toolbar's menus and Settings as icons without text, for a
284
+ * host with little room, such as a JBrowse view
285
+ */
286
+ embeddedMode: stripDefault(types.boolean, false),
602
287
  /**
603
288
  * #property
604
289
  * zoom in/out on plain mouse-wheel without holding ctrl
@@ -723,10 +408,10 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
723
408
  data: types.optional(DataModelF(maxInlineSnapshotBytes), {}),
724
409
  /**
725
410
  * #property
726
- * the user's explicit hide choices per annotation accession, keyed by
727
- * accession with the value meaning "off", like `turnedOffTracks`. An
728
- * untouched accession is absent and drawn, so the shared URL grows
729
- * only with the user's filters
411
+ * the user's departures from each annotation accession's default,
412
+ * keyed by accession with the value meaning "off", like
413
+ * `turnedOffTracks`. An untouched accession is absent and takes its
414
+ * default, so the shared URL grows only with the user's filters
730
415
  */
731
416
  turnedOffFeatures: stripDefault(types.map(types.boolean), {}),
732
417
  /**
@@ -768,9 +453,10 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
768
453
  /**
769
454
  * #property
770
455
  * where the view opens, in `highlights` coordinates: `{row, start,
771
- * end}` zooms onto residues of that row, `{start, end}` onto alignment
772
- * columns. Applied once the alignment and any tree file have loaded,
773
- * then cleared, so a reloaded session keeps the reader's own scroll.
456
+ * end}` zooms onto residues of that row and scrolls the row to the
457
+ * middle, `{start, end}` zooms onto alignment columns. Applied once the
458
+ * alignment and any tree file have loaded, then cleared, so a reloaded
459
+ * session keeps the reader's own scroll.
774
460
  */
775
461
  region: types.frozen(),
776
462
  /**
@@ -805,12 +491,30 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
805
491
  * #volatile
806
492
  */
807
493
  headerHeight: 0,
494
+ /**
495
+ * #volatile
496
+ * whether the settings panel docks beside the alignment, taking
497
+ * `settingsPanelWidth` out of the width the view lays out in
498
+ */
499
+ settingsOpen: false,
500
+ /**
501
+ * #volatile
502
+ * the log scale a wheel zoom has gathered against the detent at the
503
+ * default column width and row height, per axis
504
+ */
505
+ zoomHeld: { col: 0, row: 0 },
808
506
  /**
809
507
  * #volatile
810
508
  * leaves the toolbar out, for a host drawing its own controls. Kept out
811
509
  * of the snapshot so a link opened in the full app shows the toolbar.
812
510
  */
813
511
  hideHeader: false,
512
+ /**
513
+ * #volatile
514
+ * shows the import form while the view has no data. MSAViewer turns it
515
+ * off, since its host supplies the data and the form is the demo app's
516
+ */
517
+ importForm: true,
814
518
  /**
815
519
  * #volatile
816
520
  */
@@ -820,6 +524,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
820
524
  * canvas scale factor, from the device pixel ratio
821
525
  */
822
526
  highResScaleFactor: typeof window === 'undefined' ? 1 : window.devicePixelRatio,
527
+ /**
528
+ * #volatile
529
+ * the cell size and block grid the alignment last drew at, held from a
530
+ * wheel-zoom frame until the gesture settles. MSACanvas stretches those
531
+ * blocks to the current cell size with one CSS transform
532
+ */
533
+ zoomSnapshot: undefined,
823
534
  /**
824
535
  * #volatile
825
536
  */
@@ -964,6 +675,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
964
675
  setWidth(arg) {
965
676
  self.volatileWidth = arg;
966
677
  },
678
+ /**
679
+ * #action
680
+ */
681
+ setSettingsOpen(arg) {
682
+ self.settingsOpen = arg;
683
+ },
967
684
  /**
968
685
  * #action
969
686
  * update the canvas scale factor when the device pixel ratio changes
@@ -1101,6 +818,23 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1101
818
  setShowDomains(arg) {
1102
819
  self.showDomains = arg;
1103
820
  },
821
+ /**
822
+ * #action
823
+ */
824
+ setDomainFill(arg) {
825
+ self.domainFill = arg;
826
+ },
827
+ /**
828
+ * #action
829
+ * hand the cell fill to the residue scheme, the domains, or neither,
830
+ * which colors the letters instead
831
+ */
832
+ setCellFill(mode) {
833
+ self.setBgColor(mode !== 'none');
834
+ if (mode !== 'none') {
835
+ self.domainFill = mode === 'domain';
836
+ }
837
+ },
1104
838
  /**
1105
839
  * #action
1106
840
  * expand or collapse the domain legend that floats over the alignment
@@ -1128,6 +862,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1128
862
  */
1129
863
  setRowHeight(n) {
1130
864
  self.rowHeight = n;
865
+ self.zoomHeld = { ...self.zoomHeld, row: 0 };
1131
866
  },
1132
867
  /**
1133
868
  * #action
@@ -1135,6 +870,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1135
870
  */
1136
871
  setColWidth(n) {
1137
872
  self.colWidth = n;
873
+ self.zoomHeld = { ...self.zoomHeld, col: 0 };
1138
874
  },
1139
875
  /**
1140
876
  * #action
@@ -1275,33 +1011,15 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1275
1011
  .views(self => ({
1276
1012
  /**
1277
1013
  * #getter
1278
- * hideGaps takes effect when there are collapsed rows or allowedGappyness < 100
1014
+ * hideGaps takes effect when a collapse or a focus hides rows, whose
1015
+ * residues leave columns all gaps among the rest, or when
1016
+ * allowedGappyness is under 100
1279
1017
  */
1280
1018
  get hideGapsEffective() {
1281
1019
  return (self.hideGaps &&
1282
- (self.collapsed.length > 0 || self.allowedGappyness < 100));
1283
- },
1284
- /**
1285
- * #getter
1286
- */
1287
- get realAllowedGappyness() {
1288
- return this.hideGapsEffective ? self.allowedGappyness : 100;
1289
- },
1290
- /**
1291
- * #getter
1292
- * the annotations the overlay, the legend and the encodings draw: the
1293
- * GFF's, then the `features` layer's
1294
- */
1295
- get allAnnotations() {
1296
- return self.features.length > 0
1297
- ? [...self.annotations, ...featuresToAnnotations(self.features)]
1298
- : self.annotations;
1299
- },
1300
- /**
1301
- * #getter
1302
- */
1303
- get actuallyShowDomains() {
1304
- return self.showDomains && this.allAnnotations.length > 0;
1020
+ (self.collapsed.length > 0 ||
1021
+ self.showOnly !== undefined ||
1022
+ self.allowedGappyness < 100));
1305
1023
  },
1306
1024
  /**
1307
1025
  * #getter
@@ -1327,7 +1045,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1327
1045
  if (self.volatileWidth === undefined) {
1328
1046
  throw new Error('not initialized');
1329
1047
  }
1330
- return self.volatileWidth;
1048
+ return self.volatileWidth - (self.settingsOpen ? settingsPanelWidth : 0);
1331
1049
  },
1332
1050
  }))
1333
1051
  .views(self => ({
@@ -1374,7 +1092,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1374
1092
  return !!this.tree.noTree;
1375
1093
  },
1376
1094
  get noDomains() {
1377
- return self.allAnnotations.length === 0;
1095
+ return this.allAnnotations.length === 0;
1378
1096
  },
1379
1097
  /**
1380
1098
  * #getter
@@ -1475,6 +1193,27 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1475
1193
  ? parseMSA(text, self.currentAlignment, self.msaFormat)
1476
1194
  : null;
1477
1195
  },
1196
+ /**
1197
+ * #getter
1198
+ * the annotations the overlay, the legend and the encodings draw: the
1199
+ * GFF's, the `features` layer's, then the file's own per-row lines
1200
+ */
1201
+ get allAnnotations() {
1202
+ const inline = this.MSA?.annotations ?? [];
1203
+ return self.features.length > 0 || inline.length > 0
1204
+ ? [
1205
+ ...self.annotations,
1206
+ ...featuresToAnnotations(self.features),
1207
+ ...inline,
1208
+ ]
1209
+ : self.annotations;
1210
+ },
1211
+ /**
1212
+ * #getter
1213
+ */
1214
+ get actuallyShowDomains() {
1215
+ return self.showDomains && this.allAnnotations.length > 0;
1216
+ },
1478
1217
  /**
1479
1218
  * #getter
1480
1219
  */
@@ -1534,7 +1273,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1534
1273
  if (self.clades.length === 0) {
1535
1274
  return noClades;
1536
1275
  }
1537
- return resolveClades(self.clades, hierarchy(this.tree, d => d.children), this.rowNamesSet);
1276
+ return resolveClades(self.clades, hierarchy(this.tree, d => d.children), this.root);
1538
1277
  },
1539
1278
  /**
1540
1279
  * #getter
@@ -1702,21 +1441,16 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1702
1441
  if (!self.showTreeOverview) {
1703
1442
  return undefined;
1704
1443
  }
1705
- const root = buildTreeRoot(this.tree, {
1444
+ const layout = layoutTree(buildTreeRoot(this.tree, {
1706
1445
  collapsed: self.collapsed,
1707
1446
  rotated: self.rotated,
1708
1447
  order: self.treeOrder,
1709
- });
1710
- const numTips = leaves(root).length;
1711
- clusterLayout(root, numTips, 1);
1712
- const rootLen = Math.max(root.data.length || 0, 0);
1713
- const extent = maxLength(root) - rootLen;
1714
- setBrLength(root, -rootLen, extent ? 1 / extent : 0);
1448
+ }), { rowHeight: 1, treeWidth: 1 });
1715
1449
  return {
1716
- root,
1717
- numTips,
1718
- maxDepthToLeaf: calcDepthToLeaf(root),
1719
- showBranchLen: self.showBranchLen && extent > 0,
1450
+ root: layout.root,
1451
+ numTips: layout.leaves.length,
1452
+ maxDepthToLeaf: layout.root.depthToLeaf,
1453
+ showBranchLen: self.showBranchLen && layout.rootToTipLength > 0,
1720
1454
  };
1721
1455
  },
1722
1456
  /**
@@ -1729,8 +1463,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1729
1463
  if (!layout || self.clades.length === 0) {
1730
1464
  return noClades;
1731
1465
  }
1732
- const rowNames = new Map(leaves(layout.root).map((leaf, index) => [leaf.data.name, index]));
1733
- return resolveClades(self.clades, layout.root, rowNames);
1466
+ return resolveClades(self.clades, hierarchy(this.tree, d => d.children), layout.root);
1734
1467
  },
1735
1468
  /**
1736
1469
  * #getter
@@ -1811,7 +1544,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1811
1544
  * #getter
1812
1545
  */
1813
1546
  get blanks() {
1814
- const { hideGapsEffective, realAllowedGappyness } = self;
1547
+ const { hideGapsEffective, allowedGappyness } = self;
1815
1548
  if (!hideGapsEffective) {
1816
1549
  return [];
1817
1550
  }
@@ -1827,7 +1560,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1827
1560
  // row that stops early counts as gapped for the remainder
1828
1561
  const numCols = this.MSA?.getWidth() ?? 0;
1829
1562
  const numRows = strs.length;
1830
- const threshold = Math.ceil((realAllowedGappyness / 100) * numRows);
1563
+ const threshold = Math.ceil((allowedGappyness / 100) * numRows);
1831
1564
  const blankCounts = new Uint32Array(numCols);
1832
1565
  for (let j = 0; j < numRows; j++) {
1833
1566
  const str = strs[j];
@@ -1909,11 +1642,19 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1909
1642
  }
1910
1643
  return index;
1911
1644
  },
1645
+ /**
1646
+ * #method
1647
+ * the row's aligned sequence by the name the tree gives it, including
1648
+ * rows a collapsed clade hides. `MSA.getRow` resolves a FASTA name whose
1649
+ * colon a Newick tool wrote as an underscore, which `rowMap` does not
1650
+ */
1651
+ rowSequence(name) {
1652
+ return this.MSA?.getRow(name) || undefined;
1653
+ },
1912
1654
  /**
1913
1655
  * #getter
1914
- * every sequence in the alignment, keyed by row name, including rows a
1915
- * collapsed clade hides. `rows` holds only the rows on screen; lookups
1916
- * by row name use this
1656
+ * every sequence in the alignment, keyed by the alignment's own row
1657
+ * names, including rows a collapsed clade hides
1917
1658
  */
1918
1659
  get rowMap() {
1919
1660
  const MSA = this.MSA;
@@ -1956,10 +1697,11 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1956
1697
  */
1957
1698
  get sequenceType() {
1958
1699
  const letters = this.colStats.lettersPresent;
1959
- // isDna already excludes U (not in the DNA set) and isRna excludes T,
1960
- // so the set membership alone disambiguates the two
1961
- const dna = new Set(['A', 'C', 'G', 'T', 'N']);
1962
- const rna = new Set(['A', 'C', 'G', 'U', 'N']);
1700
+ // the two-base IUPAC codes keep an ambiguous read nucleotide; the
1701
+ // three-base ones (B, D, H, V) are common amino acids, so they don't
1702
+ const ambiguous = ['N', 'R', 'Y', 'K', 'M', 'S', 'W'];
1703
+ const dna = new Set(['A', 'C', 'G', 'T', ...ambiguous]);
1704
+ const rna = new Set(['A', 'C', 'G', 'U', ...ambiguous]);
1963
1705
  const isDna = letters.size > 0 && [...letters].every(l => dna.has(l));
1964
1706
  const isRna = letters.size > 0 && [...letters].every(l => rna.has(l));
1965
1707
  return isDna ? 'dna' : isRna ? 'rna' : 'amino';
@@ -2000,6 +1742,18 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2000
1742
  const { colStats } = this;
2001
1743
  return Array.from({ length: colStats.numColumns }, (_, col) => clustalXColumnColors(colStats, col));
2002
1744
  },
1745
+ /**
1746
+ * #getter
1747
+ * Pre-computed similarity shades per column, for blosum62_dynamic.
1748
+ * Returns a map of letter -> color for each column. Amino acids shade by
1749
+ * BLOSUM62 similarity to the rest of the column, and an alignment of
1750
+ * nucleotides by identity, since the matrix names amino acids.
1751
+ */
1752
+ get colSimilarity() {
1753
+ const { colStats, sequenceType } = this;
1754
+ const columnColors = sequenceType === 'amino' ? blosum62ColumnColors : identityColumnColors;
1755
+ return Array.from({ length: colStats.numColumns }, (_, col) => columnColors(colStats, col));
1756
+ },
2003
1757
  /**
2004
1758
  * #getter
2005
1759
  * Conservation score per column using Shannon entropy (biojs-msa style).
@@ -2082,14 +1836,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2082
1836
  get rootToTipLength() {
2083
1837
  return this.treeLayout.rootToTipLength;
2084
1838
  },
2085
- /**
2086
- * #getter
2087
- * x-position of the farthest tip in a phylogram, px: effectiveTreeWidth,
2088
- * or 0 for a tree with no branch lengths (drawn as a cladogram)
2089
- */
2090
- get maxBranchLength() {
2091
- return this.rootToTipLength ? this.effectiveTreeWidth : 0;
2092
- },
2093
1839
  /**
2094
1840
  * #getter
2095
1841
  * pixels per unit of branch length in the phylogram layout, 0 in
@@ -2245,7 +1991,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2245
1991
  data: content?.data,
2246
1992
  arcs: content?.arcs,
2247
1993
  },
2248
- ReactComponent: TrackBlocks,
2249
1994
  };
2250
1995
  });
2251
1996
  columnTrackModels.set(track, computedModel);
@@ -2303,7 +2048,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2303
2048
  data: hideGapsEffective ? skipBlanks(blanks, t.data) : t.data,
2304
2049
  height: rowHeight,
2305
2050
  },
2306
- ReactComponent: TrackBlocks,
2307
2051
  }));
2308
2052
  },
2309
2053
  /**
@@ -2325,7 +2069,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2325
2069
  height: self.trackHeight('arc'),
2326
2070
  arcs,
2327
2071
  },
2328
- ReactComponent: TrackBlocks,
2329
2072
  },
2330
2073
  ]
2331
2074
  : [];
@@ -2372,7 +2115,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2372
2115
  heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
2373
2116
  height: self.trackHeight(model.kind),
2374
2117
  },
2375
- ReactComponent: TrackBlocks,
2376
2118
  }));
2377
2119
  },
2378
2120
  get tracks() {
@@ -2389,7 +2131,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2389
2131
  * #getter
2390
2132
  */
2391
2133
  get turnedOnTracks() {
2392
- return this.tracks.filter(f => !trackIsOff(self.turnedOffTracks, f.model.id, f.model.defaultOff));
2134
+ return this.tracks.filter(f => !trackIsOff(self.turnedOffTracks, f.model.id));
2393
2135
  },
2394
2136
  /**
2395
2137
  * #getter
@@ -2408,7 +2150,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2408
2150
  * @returns The character at that position
2409
2151
  */
2410
2152
  visibleColToRowLetter(rowName, visibleCol) {
2411
- return self.rowMap.get(rowName)?.[this.visibleColToGlobalCol(visibleCol)];
2153
+ return self.rowSequence(rowName)?.[this.visibleColToGlobalCol(visibleCol)];
2412
2154
  },
2413
2155
  /**
2414
2156
  * #method
@@ -2483,7 +2225,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2483
2225
  * @returns The global column index in the full MSA, or undefined
2484
2226
  */
2485
2227
  seqPosToGlobalCol(rowName, seqPos) {
2486
- const seq = self.rowMap.get(rowName);
2228
+ const seq = self.rowSequence(rowName);
2487
2229
  if (seq === undefined) {
2488
2230
  return undefined;
2489
2231
  }
@@ -2678,7 +2420,9 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2678
2420
  if (mapping.structure.id !== structureId) {
2679
2421
  continue;
2680
2422
  }
2681
- if (asymId !== undefined && mapping.structure.asymId !== asymId) {
2423
+ if (asymId !== undefined &&
2424
+ mapping.structure.asymId !== undefined &&
2425
+ mapping.structure.asymId !== asymId) {
2682
2426
  continue;
2683
2427
  }
2684
2428
  const segment = mapping.segments.find(seg => position >= seg.structStart && position <= seg.structEnd);
@@ -2695,10 +2439,14 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2695
2439
  .views(self => ({
2696
2440
  /**
2697
2441
  * #getter
2698
- * the branch-length scale bar over the tree, undefined in cladogram mode
2699
- * or when the tree area is too narrow for one
2442
+ * the branch-length scale bar over the tree, undefined while
2443
+ * `showTreeScaleBar` is off, in cladogram mode, or when the tree area is
2444
+ * too narrow for one
2700
2445
  */
2701
2446
  get treeScaleBar() {
2447
+ if (!self.showTreeScaleBar) {
2448
+ return undefined;
2449
+ }
2702
2450
  return scaleBarLength(self.pxPerBranchLength, self.effectiveTreeAreaWidth - self.marginLeft * 2);
2703
2451
  },
2704
2452
  /**
@@ -2745,11 +2493,42 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2745
2493
  }
2746
2494
  return types;
2747
2495
  },
2496
+ /**
2497
+ * accessions hidden until the user turns them on, such as a Stockholm
2498
+ * file's #=GR lines
2499
+ */
2500
+ get defaultOffAccessions() {
2501
+ return new Set(self.allAnnotations.filter(a => a.defaultOff).map(a => a.accession));
2502
+ },
2503
+ /**
2504
+ * #method
2505
+ */
2506
+ featureOff(accession) {
2507
+ return (self.turnedOffFeatures.get(accession) ??
2508
+ this.defaultOffAccessions.has(accession));
2509
+ },
2748
2510
  get filteredAnnotations() {
2749
- return self.allAnnotations.filter(r => !self.turnedOffFeatures.get(r.accession));
2511
+ return self.allAnnotations.filter(r => !this.featureOff(r.accession));
2512
+ },
2513
+ /**
2514
+ * #getter
2515
+ * what colors the alignment's cells: the residue scheme, the domains
2516
+ * covering them, or nothing, which leaves the scheme to the letters.
2517
+ * Domains own the fill only while some are drawn, so a file whose
2518
+ * annotations all start hidden keeps its residue colors
2519
+ */
2520
+ get cellFill() {
2521
+ return !self.bgColor
2522
+ ? 'none'
2523
+ : self.actuallyShowDomains &&
2524
+ self.domainFill &&
2525
+ this.filteredAnnotations.length > 0
2526
+ ? 'domain'
2527
+ : 'residue';
2750
2528
  },
2751
2529
  get annotationsByRow() {
2752
- return groupBy(this.filteredAnnotations, r => r.id);
2530
+ const { rowNamesSet } = self;
2531
+ return groupBy(this.filteredAnnotations, r => annotationRowName(r.id, rowNamesSet));
2753
2532
  },
2754
2533
  }))
2755
2534
  .views(self => ({
@@ -2821,6 +2600,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2821
2600
  get blocks2d() {
2822
2601
  return self.blocksY.flatMap(by => self.blocksX.map(bx => [bx, by]));
2823
2602
  },
2603
+ /**
2604
+ * #getter
2605
+ */
2606
+ get wheelZooming() {
2607
+ return self.zoomSnapshot !== undefined;
2608
+ },
2824
2609
  /**
2825
2610
  * #getter
2826
2611
  */
@@ -2837,7 +2622,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2837
2622
  const { annotations, rowNamesSet } = self;
2838
2623
  return self.dataInitialized &&
2839
2624
  annotations.length > 0 &&
2840
- !annotations.some(a => rowNamesSet.has(a.id))
2625
+ !annotations.some(a => rowNamesSet.has(annotationRowName(a.id, rowNamesSet)))
2841
2626
  ? [
2842
2627
  `0 of ${annotations.length} annotations name a row in this alignment`,
2843
2628
  ]
@@ -2962,10 +2747,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2962
2747
  * #action
2963
2748
  * swap in a different tree over the same alignment. Clears `collapsed`,
2964
2749
  * `rotated` and `showOnly`, since path-derived node ids (node-0-0-1) from the old
2965
- * tree would match unrelated nodes in the new one.
2750
+ * tree would match unrelated nodes in the new one, and the tree's
2751
+ * filehandle, which the snapshot would otherwise reopen in place of
2752
+ * the new tree
2966
2753
  */
2967
2754
  replaceTree(newick) {
2968
2755
  transaction(() => {
2756
+ self.setTreeFilehandle(undefined);
2969
2757
  self.collapsed.clear();
2970
2758
  self.rotated.clear();
2971
2759
  self.setShowOnly(undefined);
@@ -2987,30 +2775,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2987
2775
  zoomAtCenter(scaleFactor, axis = 'both') {
2988
2776
  this.zoomToPos(scaleFactor, self.msaCanvasWidth / 2, self.msaAreaHeight / 2, axis);
2989
2777
  },
2990
- /**
2991
- * #action
2992
- */
2993
- zoomOutHorizontal() {
2994
- this.zoomAtCenter(0.75, 'horizontal');
2995
- },
2996
- /**
2997
- * #action
2998
- */
2999
- zoomInHorizontal() {
3000
- this.zoomAtCenter(1.5, 'horizontal');
3001
- },
3002
- /**
3003
- * #action
3004
- */
3005
- zoomInVertical() {
3006
- this.zoomAtCenter(1.5, 'vertical');
3007
- },
3008
- /**
3009
- * #action
3010
- */
3011
- zoomOutVertical() {
3012
- this.zoomAtCenter(0.75, 'vertical');
3013
- },
3014
2778
  /**
3015
2779
  * #action
3016
2780
  */
@@ -3038,11 +2802,20 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3038
2802
  transaction(() => {
3039
2803
  const colInView = (-self.scrollX + offsetX) / self.colWidth;
3040
2804
  const rowInView = (-self.scrollY + offsetY) / self.rowHeight;
2805
+ // zooming both axes, an axis holds at the default only while the
2806
+ // other sits there too, or one would hold while the other grows
2807
+ const canHold = axis !== 'both' ||
2808
+ (self.colWidth === defaultColWidth &&
2809
+ self.rowHeight === defaultRowHeight);
3041
2810
  if (axis !== 'vertical') {
3042
- self.colWidth = clamp(self.colWidth * scaleFactor, minColWidth, maxCellSize);
2811
+ const { value, held } = detentZoom(self.colWidth, scaleFactor, defaultColWidth, self.zoomHeld.col, minColWidth, canHold);
2812
+ self.colWidth = value;
2813
+ self.zoomHeld = { ...self.zoomHeld, col: held };
3043
2814
  }
3044
2815
  if (axis !== 'horizontal') {
3045
- self.rowHeight = clamp(self.rowHeight * scaleFactor, minRowHeight, maxCellSize);
2816
+ const { value, held } = detentZoom(self.rowHeight, scaleFactor, defaultRowHeight, self.zoomHeld.row, minRowHeight, canHold);
2817
+ self.rowHeight = value;
2818
+ self.zoomHeld = { ...self.zoomHeld, row: held };
3046
2819
  }
3047
2820
  self.scrollX = clamp(offsetX - colInView * self.colWidth, self.maxScrollX, 0);
3048
2821
  const anchoredScrollY = offsetY - rowInView * self.rowHeight;
@@ -3053,6 +2826,25 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3053
2826
  self.scrollY = clamp(anchoredScrollY * (1 - topBias), self.maxScrollY, 0);
3054
2827
  });
3055
2828
  },
2829
+ /**
2830
+ * #action
2831
+ * zoomToPos for one wheel or pinch frame. The alignment keeps the blocks
2832
+ * it drew at the start of the gesture, scaled, and redraws them only once
2833
+ * they stop covering the viewport; endWheelZoom redraws them sharp
2834
+ */
2835
+ wheelZoomToPos(scaleFactor, offsetX, offsetY) {
2836
+ self.zoomSnapshot ??= takeZoomSnapshot(self, false);
2837
+ this.zoomToPos(scaleFactor, offsetX, offsetY, self.wheelZoomAxis);
2838
+ if (!zoomSnapshotCovers(self, self.zoomSnapshot)) {
2839
+ self.zoomSnapshot = takeZoomSnapshot(self, true);
2840
+ }
2841
+ },
2842
+ /**
2843
+ * #action
2844
+ */
2845
+ endWheelZoom() {
2846
+ self.zoomSnapshot = undefined;
2847
+ },
3056
2848
  /**
3057
2849
  * #action
3058
2850
  */
@@ -3077,15 +2869,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3077
2869
  setAnnotations(annotations) {
3078
2870
  self.annotations = annotations;
3079
2871
  },
3080
- /**
3081
- * #action
3082
- * set the overlay from raw InterProScan results keyed by row name. Kept
3083
- * for downstream plugins that hold the EBI wire format; new code should
3084
- * adapt to Annotation[] and call setAnnotations.
3085
- */
3086
- setDomains(data) {
3087
- this.setAnnotations(data ? interProScanToAnnotations(data) : []);
3088
- },
3089
2872
  applyGFFText(gffText) {
3090
2873
  this.setAnnotations(gffToAnnotations(parseGFF(gffText)));
3091
2874
  },
@@ -3140,8 +2923,23 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3140
2923
  * #action
3141
2924
  */
3142
2925
  toggleTrack(id) {
3143
- const defaultOff = self.MSA?.tracks.find(t => t.id === id)?.defaultOff;
3144
- self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id, defaultOff));
2926
+ self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id));
2927
+ },
2928
+ /**
2929
+ * #action
2930
+ * show or hide several tracks at once, as the track selector's Show all
2931
+ * and Hide all do. A choice matching the track's default is dropped, so
2932
+ * it stays out of the shared URL
2933
+ */
2934
+ setTracksShown(ids, shown) {
2935
+ for (const id of ids) {
2936
+ if (defaultOffTracks.has(id) === !shown) {
2937
+ self.turnedOffTracks.delete(id);
2938
+ }
2939
+ else {
2940
+ self.turnedOffTracks.set(id, !shown);
2941
+ }
2942
+ }
3145
2943
  },
3146
2944
  /**
3147
2945
  * #action
@@ -3166,7 +2964,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3166
2964
  column: column + 1,
3167
2965
  row,
3168
2966
  residue: self.visibleColToSeqPosOneBased(row, visibleCol),
3169
- letter: self.rowMap.get(row)?.[column],
2967
+ letter: self.rowSequence(row)?.[column],
3170
2968
  };
3171
2969
  },
3172
2970
  /**
@@ -3287,7 +3085,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3287
3085
  */
3288
3086
  get visibleDomainTypes() {
3289
3087
  return this.categoricalDomainTypes
3290
- .filter(d => !self.turnedOffFeatures.get(d.accession))
3088
+ .filter(d => !self.featureOff(d.accession))
3291
3089
  .toSorted((a, b) => a.start - b.start);
3292
3090
  },
3293
3091
  /**
@@ -3331,7 +3129,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3331
3129
  : visibleDomainTypes.map(d => ({
3332
3130
  id: d.accession,
3333
3131
  label: d.name,
3334
- color: fillPalette[d.accession],
3132
+ color: d.color ?? fillPalette[d.accession],
3335
3133
  })), 'domains');
3336
3134
  }
3337
3135
  for (const { channel, field, legend } of this.resolvedEncodings) {
@@ -3347,15 +3145,15 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3347
3145
  /**
3348
3146
  * #getter
3349
3147
  * whether the overlay marks each domain with a bar under its row instead
3350
- * of filling the row behind the letters. Letter-color mode hands the
3351
- * background to the color scheme, so a filled box would paint over it and
3352
- * leave the setting with nothing to show. Sub-row layout already stacks
3148
+ * of filling the row behind the letters: whenever the domains do not own
3149
+ * the cell fill (`cellFill`), a filled box would paint over the residue
3150
+ * colors the user picked. Sub-row layout already stacks
3353
3151
  * the boxes clear of the letters, and with the letters too small to draw
3354
3152
  * the filled box is the only thing left to read.
3355
3153
  */
3356
3154
  get domainUnderline() {
3357
3155
  return (self.actuallyShowDomains &&
3358
- !self.bgColor &&
3156
+ self.cellFill !== 'domain' &&
3359
3157
  !self.subFeatureRows &&
3360
3158
  self.showMsaLetters);
3361
3159
  },
@@ -3567,7 +3365,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3567
3365
  * to `end`, gaps included
3568
3366
  */
3569
3367
  get selectionFasta() {
3570
- const { rowNames, rowMap } = self;
3368
+ const { rowNames } = self;
3571
3369
  const resolved = this.resolvedSelection;
3572
3370
  const span = this.selectionFileSpan;
3573
3371
  if (!resolved || !span) {
@@ -3576,7 +3374,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3576
3374
  const rows = resolved.rowRuns
3577
3375
  .flatMap(([first, last]) => rowNames.slice(first, last + 1))
3578
3376
  .flatMap(name => {
3579
- const seq = rowMap.get(name);
3377
+ const seq = self.rowSequence(name);
3580
3378
  return seq === undefined ? [] : [[name, seq]];
3581
3379
  });
3582
3380
  return blockFasta(rows, span.start, span.end);
@@ -3655,7 +3453,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3655
3453
  this.visibleDomainTypes.map(d => ({
3656
3454
  id: d.accession,
3657
3455
  label: d.name,
3658
- color: fillPalette[d.accession],
3456
+ color: d.color ?? fillPalette[d.accession],
3659
3457
  })),
3660
3458
  };
3661
3459
  }
@@ -3804,6 +3602,18 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3804
3602
  setHideHeader(arg) {
3805
3603
  self.hideHeader = arg;
3806
3604
  },
3605
+ /**
3606
+ * #action
3607
+ */
3608
+ setEmbeddedMode(arg) {
3609
+ self.embeddedMode = arg;
3610
+ },
3611
+ /**
3612
+ * #action
3613
+ */
3614
+ setImportForm(arg) {
3615
+ self.importForm = arg;
3616
+ },
3807
3617
  /**
3808
3618
  * #action
3809
3619
  * focus the subtree a click `y` pixels down the tree overview lands on.
@@ -3862,15 +3672,16 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3862
3672
  },
3863
3673
  /**
3864
3674
  * #action
3865
- * show or hide an annotation type. Only hidden types are recorded; see
3866
- * `turnedOffFeatures`
3675
+ * show or hide an annotation type. Only a choice that differs from the
3676
+ * type's default is recorded; see `turnedOffFeatures`
3867
3677
  */
3868
3678
  setFilter(accession, shown) {
3869
- if (shown) {
3870
- self.turnedOffFeatures.delete(accession);
3679
+ // only a departure from the accession's default is recorded
3680
+ if (shown === self.defaultOffAccessions.has(accession)) {
3681
+ self.turnedOffFeatures.set(accession, !shown);
3871
3682
  }
3872
3683
  else {
3873
- self.turnedOffFeatures.set(accession, true);
3684
+ self.turnedOffFeatures.delete(accession);
3874
3685
  }
3875
3686
  },
3876
3687
  /**
@@ -3878,20 +3689,23 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3878
3689
  */
3879
3690
  fit() {
3880
3691
  // two passes: each direction's viewport depends on whether the other
3881
- // overflows (the minimap takes height, the vertical scrollbar width)
3692
+ // overflows (the minimap takes height, the vertical scrollbar width).
3693
+ // Fit shrinks an alignment into the window but never grows one past
3694
+ // the default cell size, which would stretch a small alignment's rows
3695
+ // and columns out of proportion
3882
3696
  transaction(() => {
3883
3697
  for (let pass = 0; pass < 2; pass++) {
3884
- this.fitHorizontally();
3885
- this.fitVertically();
3698
+ this.fitHorizontally(defaultColWidth);
3699
+ this.fitVertically(defaultRowHeight);
3886
3700
  }
3887
3701
  });
3888
3702
  },
3889
3703
  /**
3890
3704
  * #action
3891
3705
  */
3892
- fitVertically() {
3706
+ fitVertically(maxRowHeight = maxCellSize) {
3893
3707
  if (self.numRows > 0) {
3894
- self.rowHeight = clamp(self.msaAreaHeight / self.numRows, minRowHeight, maxCellSize);
3708
+ self.setRowHeight(clamp(self.msaAreaHeight / self.numRows, minRowHeight, maxRowHeight));
3895
3709
  }
3896
3710
  self.scrollY = 0;
3897
3711
  },
@@ -3907,10 +3721,28 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3907
3721
  return;
3908
3722
  }
3909
3723
  transaction(() => {
3910
- self.colWidth = clamp(self.msaCanvasWidth / (span.endCol - span.startCol + 1), minColWidth, maxCellSize);
3724
+ self.setColWidth(clamp(self.msaCanvasWidth / (span.endCol - span.startCol + 1), minColWidth, maxCellSize));
3911
3725
  self.scrollX = clamp(-span.startCol * self.colWidth, self.maxScrollX, 0);
3912
3726
  });
3913
3727
  },
3728
+ /**
3729
+ * #action
3730
+ * zoomToRegion, and for a region naming a row, also scroll that row to
3731
+ * the middle of the alignment, as Go to does. What `region` opens on; a
3732
+ * caller following a range as it moves wants zoomToRegion alone, which
3733
+ * leaves the reader's vertical scroll where they put it.
3734
+ */
3735
+ revealRegion(region) {
3736
+ transaction(() => {
3737
+ this.zoomToRegion(region);
3738
+ const row = region.row === undefined
3739
+ ? undefined
3740
+ : self.rowNamesSet.get(region.row);
3741
+ if (row !== undefined && self.viewInitialized) {
3742
+ self.setScrollY(-(row + 0.5) * self.rowHeight + self.msaAreaHeight / 2);
3743
+ }
3744
+ });
3745
+ },
3914
3746
  /**
3915
3747
  * #action
3916
3748
  * zoom and scroll so the selected columns fill the alignment's width,
@@ -3927,7 +3759,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3927
3759
  const { rowRuns } = resolvedSelection;
3928
3760
  const first = rowRuns[0][0];
3929
3761
  const last = rowRuns.at(-1)[1];
3930
- self.rowHeight = clamp(self.msaAreaHeight / (last - first + 1), minRowHeight, maxCellSize);
3762
+ self.setRowHeight(clamp(self.msaAreaHeight / (last - first + 1), minRowHeight, maxCellSize));
3931
3763
  self.scrollY = clamp(-first * self.rowHeight, self.maxScrollY, 0);
3932
3764
  }
3933
3765
  });
@@ -3959,10 +3791,10 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3959
3791
  /**
3960
3792
  * #action
3961
3793
  */
3962
- fitHorizontally() {
3794
+ fitHorizontally(maxColWidth = maxCellSize) {
3963
3795
  if (self.numColumns > 0) {
3964
3796
  // msaCanvasWidth excludes the vertical scrollbar's 20px
3965
- self.colWidth = clamp(self.msaCanvasWidth / self.numColumns, minColWidth, maxCellSize);
3797
+ self.setColWidth(clamp(self.msaCanvasWidth / self.numColumns, minColWidth, maxColWidth));
3966
3798
  }
3967
3799
  self.scrollX = 0;
3968
3800
  },
@@ -3970,17 +3802,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3970
3802
  if (self.highlightColumns?.length) {
3971
3803
  self.setHighlightedColumns(self.highlightColumns);
3972
3804
  }
3973
- // The `collapse`, `focus` and `rotate` clade marks seed the collapsed
3974
- // list, the subtree in focus and the rotated nodes, which the tree,
3975
- // `hideGapsEffective` and the alignment all read. The tree arrives with
3976
- // the model for inline data and later for a filehandle, so the seeding
3977
- // waits for it and then runs once per data load, per change of
3978
- // `clades` and per reroot, which clears the node ids the seeding
3979
- // wrote: expanding a seeded clade sticks until one of those changes.
3980
- // `dataInitialized` is true once the MSA alone has loaded, when the
3981
- // tree is still the flat stub, so a tree filehandle holds the seeding
3982
- // until its text lands.
3983
- addDisposer(self, reaction(() => self.dataInitialized && !(self.treeFilehandle && !self.data.tree)
3805
+ const awaitingTree = () => !!self.treeFilehandle && !self.data.tree;
3806
+ // `collapse`, `focus` and `rotate` marks seed `collapsed`, `showOnly`
3807
+ // and `rotated`. Seeding runs once per data load, `clades` change and
3808
+ // reroot, so expanding a seeded clade sticks until one of those. A tree
3809
+ // filehandle holds it back, since `dataInitialized` is true while the
3810
+ // tree is still the flat stub.
3811
+ addDisposer(self, reaction(() => self.dataInitialized && !awaitingTree()
3984
3812
  ? { clades: getSnapshot(self.clades), treeRoot: self.treeRoot }
3985
3813
  : undefined, seed => {
3986
3814
  if (!seed?.clades.length) {
@@ -4010,8 +3838,8 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4010
3838
  if (region &&
4011
3839
  self.viewInitialized &&
4012
3840
  self.numColumns > 0 &&
4013
- !(self.treeFilehandle && !self.data.tree)) {
4014
- this.zoomToRegion(region);
3841
+ !awaitingTree()) {
3842
+ this.revealRegion(region);
4015
3843
  self.setRegion(undefined);
4016
3844
  }
4017
3845
  }));
@@ -4032,87 +3860,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4032
3860
  mql.removeEventListener('change', onChange);
4033
3861
  });
4034
3862
  }
4035
- /**
4036
- * Fetch a filehandle whenever it changes, and hand the text to
4037
- * `onLoad`.
4038
- *
4039
- * A generation guard keeps a slower superseded request from overwriting
4040
- * the newer one's data, status or loading flag. A superseded or cleared
4041
- * request is aborted and clears its status line.
4042
- *
4043
- * `clearFilehandle` runs after a local file loads, since a blob has no
4044
- * URL to refetch, and when the user cancels, which returns the view to
4045
- * the import form.
4046
- *
4047
- * `what` names the layer in a failure message. An `optional` layer
4048
- * (annotations, row metadata) that fails adds a warning instead of an
4049
- * error.
4050
- */
4051
- const loadOnFilehandleChange = ({ what, getFilehandle, onLoad, setLoading, clearFilehandle, optional = false, }) => {
4052
- let generation = 0;
4053
- let cancelInFlight;
4054
- addDisposer(self, () => {
4055
- cancelInFlight?.();
4056
- });
4057
- addDisposer(self, autorun(async () => {
4058
- const filehandle = getFilehandle();
4059
- const current = ++generation;
4060
- cancelInFlight?.();
4061
- cancelInFlight = undefined;
4062
- if (!filehandle) {
4063
- setLoading?.(false);
4064
- return;
4065
- }
4066
- const isCurrent = () => current === generation && isAlive(self);
4067
- const controller = new AbortController();
4068
- cancelInFlight = () => {
4069
- controller.abort();
4070
- if (isAlive(self)) {
4071
- self.setStatus(undefined);
4072
- }
4073
- };
4074
- try {
4075
- setLoading?.(true);
4076
- self.setError(undefined);
4077
- const text = await fetchTextWithProgress(openLocation(filehandle), status => {
4078
- if (isCurrent()) {
4079
- self.setStatus(status);
4080
- }
4081
- }, { controller });
4082
- if (isCurrent()) {
4083
- transaction(() => {
4084
- onLoad(text);
4085
- if (filehandle.locationType === 'BlobLocation') {
4086
- clearFilehandle?.();
4087
- }
4088
- });
4089
- }
4090
- }
4091
- catch (e) {
4092
- if (isCurrent()) {
4093
- if (isAbortError(e)) {
4094
- clearFilehandle?.();
4095
- }
4096
- else {
4097
- console.error(e);
4098
- if (optional) {
4099
- self.addWarning(`The ${what} did not load: ${e}`);
4100
- }
4101
- else {
4102
- self.setError(e);
4103
- }
4104
- }
4105
- }
4106
- }
4107
- finally {
4108
- if (isCurrent()) {
4109
- cancelInFlight = undefined;
4110
- setLoading?.(false);
4111
- }
4112
- }
4113
- }));
4114
- };
4115
- loadOnFilehandleChange({
3863
+ installFilehandleLoader(self, {
4116
3864
  what: 'tree',
4117
3865
  getFilehandle: () => self.treeFilehandle,
4118
3866
  onLoad: text => {
@@ -4126,7 +3874,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4126
3874
  },
4127
3875
  });
4128
3876
  // treeMetadata has no import-form step, so no loading flag or cancel
4129
- loadOnFilehandleChange({
3877
+ installFilehandleLoader(self, {
4130
3878
  what: 'row metadata',
4131
3879
  optional: true,
4132
3880
  getFilehandle: () => self.treeMetadataFilehandle,
@@ -4155,7 +3903,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4155
3903
  }
4156
3904
  }));
4157
3905
  // gffFilehandle loads into data.gff, which the autorun above parses
4158
- loadOnFilehandleChange({
3906
+ installFilehandleLoader(self, {
4159
3907
  what: 'annotations',
4160
3908
  optional: true,
4161
3909
  getFilehandle: () => self.gffFilehandle,
@@ -4166,7 +3914,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4166
3914
  self.setGFFFilehandle(undefined);
4167
3915
  },
4168
3916
  });
4169
- loadOnFilehandleChange({
3917
+ installFilehandleLoader(self, {
4170
3918
  what: 'alignment',
4171
3919
  getFilehandle: () => self.msaFilehandle,
4172
3920
  onLoad: text => {
@@ -4187,20 +3935,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4187
3935
  // xref problem https://github.com/GMOD/react-msaview/issues/75
4188
3936
  addDisposer(self, autorun(() => {
4189
3937
  if (self.dynamicColorSchemeName) {
4190
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4191
- self.colStats;
3938
+ readComputed(self.colStats);
4192
3939
  }
4193
3940
  if (self.showColumnStats) {
4194
- // everything the hover tooltip reads per column
4195
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4196
- self.colStats;
4197
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4198
- self.conservation;
4199
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4200
- self.propertyConservation;
3941
+ readComputed(self.colStats, self.conservation, self.propertyConservation);
4201
3942
  }
4202
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4203
- self.columns;
3943
+ readComputed(self.columns);
4204
3944
  }));
4205
3945
  },
4206
3946
  }))