react-msaview 9.1.0 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/bundle/index.js +111 -115
  2. package/bundle/index.js.map +4 -4
  3. package/dist/blosum62.d.ts +3 -0
  4. package/dist/blosum62.js +43 -0
  5. package/dist/blosum62.js.map +1 -0
  6. package/dist/colorSchemes.js +1 -0
  7. package/dist/colorSchemes.js.map +1 -1
  8. package/dist/components/Loading.d.ts +3 -2
  9. package/dist/components/Loading.js +2 -2
  10. package/dist/components/Loading.js.map +1 -1
  11. package/dist/components/MSAView.d.ts +3 -2
  12. package/dist/components/MSAView.js +7 -4
  13. package/dist/components/MSAView.js.map +1 -1
  14. package/dist/components/MSAViewer.d.ts +64 -12
  15. package/dist/components/MetadataTable.d.ts +4 -1
  16. package/dist/components/MetadataTable.js +55 -16
  17. package/dist/components/MetadataTable.js.map +1 -1
  18. package/dist/components/Track.js +6 -0
  19. package/dist/components/Track.js.map +1 -1
  20. package/dist/components/controls.js +2 -1
  21. package/dist/components/controls.js.map +1 -1
  22. package/dist/components/dialogs/AnnotationFileDialog.js +7 -2
  23. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
  24. package/dist/components/dialogs/FeatureDialog.js +2 -2
  25. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  26. package/dist/components/{header/MSASettingsMenu.d.ts → dialogs/GapThresholdDialog.d.ts} +3 -2
  27. package/dist/components/dialogs/GapThresholdDialog.js +15 -0
  28. package/dist/components/dialogs/GapThresholdDialog.js.map +1 -0
  29. package/dist/components/{header → dialogs}/GappynessSlider.d.ts +1 -1
  30. package/dist/components/{header → dialogs}/GappynessSlider.js +3 -10
  31. package/dist/components/dialogs/GappynessSlider.js.map +1 -0
  32. package/dist/components/{header → dialogs}/GoToBox.d.ts +4 -2
  33. package/dist/components/dialogs/GoToBox.js +48 -0
  34. package/dist/components/dialogs/GoToBox.js.map +1 -0
  35. package/dist/components/dialogs/GoToDialog.d.ts +6 -0
  36. package/dist/components/dialogs/GoToDialog.js +10 -0
  37. package/dist/components/dialogs/GoToDialog.js.map +1 -0
  38. package/dist/components/dialogs/MetadataDialog.js +11 -3
  39. package/dist/components/dialogs/MetadataDialog.js.map +1 -1
  40. package/dist/components/dialogs/goTo.d.ts +5 -0
  41. package/dist/components/{header → dialogs}/goTo.js +1 -1
  42. package/dist/components/dialogs/goTo.js.map +1 -0
  43. package/dist/components/dialogs/metadataLabels.d.ts +4 -0
  44. package/dist/components/dialogs/metadataLabels.js +52 -0
  45. package/dist/components/dialogs/metadataLabels.js.map +1 -0
  46. package/dist/components/dialogs/metadataRecord.d.ts +1 -0
  47. package/dist/components/dialogs/metadataRecord.js +38 -0
  48. package/dist/components/dialogs/metadataRecord.js.map +1 -0
  49. package/dist/components/header/Header.d.ts +6 -2
  50. package/dist/components/header/Header.js +30 -32
  51. package/dist/components/header/Header.js.map +1 -1
  52. package/dist/components/header/{TreeSettingsMenu.d.ts → HiddenColumnsChip.d.ts} +2 -2
  53. package/dist/components/header/HiddenColumnsChip.js +15 -0
  54. package/dist/components/header/HiddenColumnsChip.js.map +1 -0
  55. package/dist/components/header/MenuBar.d.ts +6 -0
  56. package/dist/components/header/MenuBar.js +33 -0
  57. package/dist/components/header/MenuBar.js.map +1 -0
  58. package/dist/components/header/ScrollZoomCheckbox.d.ts +7 -0
  59. package/dist/components/header/ScrollZoomCheckbox.js +19 -0
  60. package/dist/components/header/ScrollZoomCheckbox.js.map +1 -0
  61. package/dist/components/header/ZoomControls.js +10 -30
  62. package/dist/components/header/ZoomControls.js.map +1 -1
  63. package/dist/components/header/colorSchemeMenuItems.d.ts +4 -0
  64. package/dist/components/header/colorSchemeMenuItems.js +106 -0
  65. package/dist/components/header/colorSchemeMenuItems.js.map +1 -0
  66. package/dist/components/header/getAnnotationsMenu.d.ts +3 -30
  67. package/dist/components/header/getAnnotationsMenu.js +48 -54
  68. package/dist/components/header/getAnnotationsMenu.js.map +1 -1
  69. package/dist/components/header/menuBarItems.d.ts +11 -0
  70. package/dist/components/header/menuBarItems.js +277 -0
  71. package/dist/components/header/menuBarItems.js.map +1 -0
  72. package/dist/components/import/ImportForm.js +12 -6
  73. package/dist/components/import/ImportForm.js.map +1 -1
  74. package/dist/components/minimap/Minimap.js +7 -3
  75. package/dist/components/minimap/Minimap.js.map +1 -1
  76. package/dist/components/minimap/MinimapSVG.js +2 -1
  77. package/dist/components/minimap/MinimapSVG.js.map +1 -1
  78. package/dist/components/minimap/minimapLayout.d.ts +12 -0
  79. package/dist/components/minimap/minimapLayout.js +20 -0
  80. package/dist/components/minimap/minimapLayout.js.map +1 -1
  81. package/dist/components/msa/MSACanvas.js +26 -9
  82. package/dist/components/msa/MSACanvas.js.map +1 -1
  83. package/dist/components/msa/MSACanvasBlock.js +15 -11
  84. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  85. package/dist/components/msa/MSAMouseoverCanvas.js +2 -2
  86. package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
  87. package/dist/components/msa/WheelZoomHint.d.ts +7 -0
  88. package/dist/components/msa/WheelZoomHint.js +38 -0
  89. package/dist/components/msa/WheelZoomHint.js.map +1 -0
  90. package/dist/components/msa/msaKeys.js +3 -0
  91. package/dist/components/msa/msaKeys.js.map +1 -1
  92. package/dist/components/msa/msaRaster.d.ts +4 -5
  93. package/dist/components/msa/msaRaster.js +5 -5
  94. package/dist/components/msa/msaRaster.js.map +1 -1
  95. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +22 -2
  96. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  97. package/dist/components/msa/renderHighlights.d.ts +0 -3
  98. package/dist/components/msa/renderHighlights.js +3 -3
  99. package/dist/components/msa/renderHighlights.js.map +1 -1
  100. package/dist/components/msa/renderMSABlock.d.ts +6 -1
  101. package/dist/components/msa/renderMSABlock.js +44 -17
  102. package/dist/components/msa/renderMSABlock.js.map +1 -1
  103. package/dist/components/msa/tileColor.js +7 -0
  104. package/dist/components/msa/tileColor.js.map +1 -1
  105. package/dist/components/rowpanels/RowPanelBlock.js +2 -2
  106. package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
  107. package/dist/components/settings/MenuItemsForm.d.ts +10 -0
  108. package/dist/components/settings/MenuItemsForm.js +80 -0
  109. package/dist/components/settings/MenuItemsForm.js.map +1 -0
  110. package/dist/components/settings/SettingsPanel.d.ts +12 -0
  111. package/dist/components/settings/SettingsPanel.js +92 -0
  112. package/dist/components/settings/SettingsPanel.js.map +1 -0
  113. package/dist/components/{header/FileMenu.d.ts → settings/TrackList.d.ts} +2 -2
  114. package/dist/components/settings/TrackList.js +58 -0
  115. package/dist/components/settings/TrackList.js.map +1 -0
  116. package/dist/components/tracks/TrackBlocks.js +2 -2
  117. package/dist/components/tracks/TrackBlocks.js.map +1 -1
  118. package/dist/components/tracks/TrackTooltipContent.js +2 -1
  119. package/dist/components/tracks/TrackTooltipContent.js.map +1 -1
  120. package/dist/components/tracks/drawTracks.js +13 -6
  121. package/dist/components/tracks/drawTracks.js.map +1 -1
  122. package/dist/components/tree/TreeCanvasBlock.js +2 -2
  123. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  124. package/dist/components/tree/TreeMenu.js +4 -6
  125. package/dist/components/tree/TreeMenu.js.map +1 -1
  126. package/dist/components/tree/TreeOverview.js +2 -2
  127. package/dist/components/tree/TreeOverview.js.map +1 -1
  128. package/dist/constants.d.ts +4 -1
  129. package/dist/constants.js +7 -1
  130. package/dist/constants.js.map +1 -1
  131. package/dist/darkReader.d.ts +7 -0
  132. package/dist/darkReader.js +42 -0
  133. package/dist/darkReader.js.map +1 -0
  134. package/dist/element.js +22 -47
  135. package/dist/element.js.map +1 -1
  136. package/dist/featurePanels.d.ts +37 -0
  137. package/dist/featurePanels.js +187 -0
  138. package/dist/featurePanels.js.map +1 -0
  139. package/dist/filehandleLoader.d.ts +41 -0
  140. package/dist/filehandleLoader.js +99 -0
  141. package/dist/filehandleLoader.js.map +1 -0
  142. package/dist/index.d.ts +3 -0
  143. package/dist/index.js +2 -0
  144. package/dist/index.js.map +1 -1
  145. package/dist/model/treeModel.d.ts +9 -0
  146. package/dist/model/treeModel.js +12 -1
  147. package/dist/model/treeModel.js.map +1 -1
  148. package/dist/model.d.ts +148 -32
  149. package/dist/model.js +291 -569
  150. package/dist/model.js.map +1 -1
  151. package/dist/neighborJoining.js +1 -40
  152. package/dist/neighborJoining.js.map +1 -1
  153. package/dist/renderToSvg.js +1 -13
  154. package/dist/renderToSvg.js.map +1 -1
  155. package/dist/rerootTree.d.ts +1 -0
  156. package/dist/rerootTree.js +7 -6
  157. package/dist/rerootTree.js.map +1 -1
  158. package/dist/schemeResidueColor.d.ts +12 -0
  159. package/dist/schemeResidueColor.js +33 -0
  160. package/dist/schemeResidueColor.js.map +1 -0
  161. package/dist/similarityColors.d.ts +11 -0
  162. package/dist/similarityColors.js +90 -0
  163. package/dist/similarityColors.js.map +1 -0
  164. package/dist/svgTestUtil.d.ts +64 -12
  165. package/dist/treeDisplay.d.ts +37 -0
  166. package/dist/treeDisplay.js +209 -0
  167. package/dist/treeDisplay.js.map +1 -0
  168. package/dist/types.d.ts +7 -3
  169. package/dist/useWheelScroll.d.ts +3 -1
  170. package/dist/useWheelScroll.js +36 -17
  171. package/dist/useWheelScroll.js.map +1 -1
  172. package/dist/util.d.ts +2 -0
  173. package/dist/util.js +2 -0
  174. package/dist/util.js.map +1 -1
  175. package/dist/version.d.ts +1 -1
  176. package/dist/version.js +1 -1
  177. package/dist/wheelZoom.d.ts +8 -3
  178. package/dist/wheelZoom.js +44 -32
  179. package/dist/wheelZoom.js.map +1 -1
  180. package/dist/zoomSnapshot.d.ts +29 -0
  181. package/dist/zoomSnapshot.js +47 -0
  182. package/dist/zoomSnapshot.js.map +1 -0
  183. package/package.json +11 -11
  184. package/src/blosum62.ts +45 -0
  185. package/src/colorSchemes.ts +1 -0
  186. package/src/components/Loading.tsx +6 -2
  187. package/src/components/MSAView.tsx +22 -5
  188. package/src/components/MetadataTable.tsx +83 -26
  189. package/src/components/Track.tsx +13 -0
  190. package/src/components/controls.ts +2 -1
  191. package/src/components/dialogs/AnnotationFileDialog.tsx +12 -1
  192. package/src/components/dialogs/FeatureDialog.tsx +2 -5
  193. package/src/components/dialogs/GapThresholdDialog.tsx +36 -0
  194. package/src/components/{header → dialogs}/GappynessSlider.tsx +3 -10
  195. package/src/components/{header → dialogs}/GoToBox.tsx +36 -10
  196. package/src/components/dialogs/GoToDialog.tsx +30 -0
  197. package/src/components/dialogs/MetadataDialog.tsx +32 -4
  198. package/src/components/{header → dialogs}/goTo.ts +1 -1
  199. package/src/components/dialogs/metadataLabels.ts +54 -0
  200. package/src/components/dialogs/metadataRecord.tsx +61 -0
  201. package/src/components/header/Header.tsx +75 -42
  202. package/src/components/header/HiddenColumnsChip.tsx +39 -0
  203. package/src/components/header/MenuBar.tsx +64 -0
  204. package/src/components/header/ScrollZoomCheckbox.tsx +46 -0
  205. package/src/components/header/ZoomControls.tsx +20 -35
  206. package/src/components/header/colorSchemeMenuItems.ts +121 -0
  207. package/src/components/header/getAnnotationsMenu.ts +52 -56
  208. package/src/components/header/menuBarItems.ts +357 -0
  209. package/src/components/import/ImportForm.tsx +19 -7
  210. package/src/components/minimap/Minimap.tsx +11 -3
  211. package/src/components/minimap/MinimapSVG.tsx +17 -1
  212. package/src/components/minimap/minimapLayout.ts +27 -0
  213. package/src/components/msa/MSACanvas.tsx +22 -3
  214. package/src/components/msa/MSACanvasBlock.tsx +18 -11
  215. package/src/components/msa/MSAMouseoverCanvas.tsx +2 -2
  216. package/src/components/msa/WheelZoomHint.tsx +56 -0
  217. package/src/components/msa/msaKeys.ts +3 -0
  218. package/src/components/msa/msaRaster.ts +6 -8
  219. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +28 -5
  220. package/src/components/msa/renderHighlights.ts +3 -3
  221. package/src/components/msa/renderMSABlock.ts +51 -15
  222. package/src/components/msa/tileColor.ts +7 -0
  223. package/src/components/rowpanels/RowPanelBlock.tsx +2 -2
  224. package/src/components/settings/MenuItemsForm.tsx +225 -0
  225. package/src/components/settings/SettingsPanel.tsx +156 -0
  226. package/src/components/settings/TrackList.tsx +124 -0
  227. package/src/components/tracks/TrackBlocks.tsx +2 -2
  228. package/src/components/tracks/TrackTooltipContent.tsx +2 -1
  229. package/src/components/tracks/drawTracks.ts +15 -7
  230. package/src/components/tree/TreeCanvasBlock.tsx +2 -2
  231. package/src/components/tree/TreeMenu.tsx +4 -6
  232. package/src/components/tree/TreeOverview.tsx +2 -2
  233. package/src/constants.ts +9 -1
  234. package/src/darkReader.ts +57 -0
  235. package/src/element.ts +34 -57
  236. package/src/featurePanels.ts +269 -0
  237. package/src/filehandleLoader.ts +136 -0
  238. package/src/index.ts +4 -0
  239. package/src/model/treeModel.ts +13 -0
  240. package/src/model.ts +370 -748
  241. package/src/neighborJoining.ts +1 -42
  242. package/src/renderToSvg.tsx +1 -13
  243. package/src/rerootTree.ts +7 -6
  244. package/src/schemeResidueColor.ts +37 -0
  245. package/src/similarityColors.ts +112 -0
  246. package/src/treeDisplay.ts +278 -0
  247. package/src/types.ts +7 -4
  248. package/src/useWheelScroll.ts +43 -22
  249. package/src/util.ts +3 -0
  250. package/src/version.ts +1 -1
  251. package/src/wheelZoom.ts +55 -33
  252. package/src/zoomSnapshot.ts +80 -0
  253. package/dist/components/header/ColorSchemeMenu.d.ts +0 -12
  254. package/dist/components/header/ColorSchemeMenu.js +0 -37
  255. package/dist/components/header/ColorSchemeMenu.js.map +0 -1
  256. package/dist/components/header/FileMenu.js +0 -92
  257. package/dist/components/header/FileMenu.js.map +0 -1
  258. package/dist/components/header/GappynessSlider.js.map +0 -1
  259. package/dist/components/header/GoToBox.js +0 -31
  260. package/dist/components/header/GoToBox.js.map +0 -1
  261. package/dist/components/header/MSASettingsMenu.js +0 -11
  262. package/dist/components/header/MSASettingsMenu.js.map +0 -1
  263. package/dist/components/header/ScrollZoomIcons.d.ts +0 -6
  264. package/dist/components/header/ScrollZoomIcons.js +0 -35
  265. package/dist/components/header/ScrollZoomIcons.js.map +0 -1
  266. package/dist/components/header/TreeSettingsMenu.js +0 -11
  267. package/dist/components/header/TreeSettingsMenu.js.map +0 -1
  268. package/dist/components/header/goTo.d.ts +0 -20
  269. package/dist/components/header/goTo.js.map +0 -1
  270. package/dist/components/header/settingsMenuItems.d.ts +0 -5
  271. package/dist/components/header/settingsMenuItems.js +0 -171
  272. package/dist/components/header/settingsMenuItems.js.map +0 -1
  273. package/src/components/header/ColorSchemeMenu.tsx +0 -56
  274. package/src/components/header/FileMenu.tsx +0 -107
  275. package/src/components/header/MSASettingsMenu.tsx +0 -26
  276. package/src/components/header/ScrollZoomIcons.tsx +0 -52
  277. package/src/components/header/TreeSettingsMenu.tsx +0 -30
  278. package/src/components/header/settingsMenuItems.ts +0 -216
package/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, 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';
@@ -14,38 +13,34 @@ import { packDomainLanes } from './components/msa/packDomainLanes.js';
14
13
  import { visibleColRange } from './components/msa/visibleColRange.js';
15
14
  import { cladeGutterWidth } from './components/tree/cladeBrackets.js';
16
15
  import { scaleBarLength } from './components/tree/scaleBar.js';
17
- 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';
18
17
  import { createPaletteMap } from './createPaletteMap.js';
19
18
  import { exportFileName } from './exportFileName.js';
20
- import { featureField, featureName } from './featureFields.js';
19
+ import { featureName } from './featureFields.js';
20
+ import { colorByRow, featureColorMap, featureLabelMap, featurePanelSpans, resolveFeatureScale, rowPanelWidth, stringFields, } from './featurePanels.js';
21
21
  import { featuresToAnnotations } from './featuresLayer.js';
22
- import { fetchTextWithProgress, isAbortError } from './fetchUtils.js';
23
- import { flatToTree } from './flatToTree.js';
24
- import { collapse, find, forEachDescendant, hierarchy, layoutTree, leafIndex, leaves, mrca, nodeCoveringRows, sort, sum as hierarchySum, } from './hierarchy.js';
22
+ import { installFilehandleLoader } from './filehandleLoader.js';
23
+ import { find, hierarchy, layoutTree, leaves, nodeCoveringRows, } from './hierarchy.js';
25
24
  import { DataModelF } from './model/DataModel.js';
26
25
  import { DialogQueueSessionMixin } from './model/DialogQueue.js';
27
26
  import { MSAModelF } from './model/msaModel.js';
28
27
  import { TreeModelF } from './model/treeModel.js';
29
28
  import { calculateNeighborJoiningTree } from './neighborJoining.js';
30
- import { parseAsn1 } from './parseAsn1.js';
31
29
  import { calculatePropertyConservation } from './propertyConservation.js';
32
- import { midpointRoot, rerootTree } from './rerootTree.js';
30
+ import { preorder } from './rerootTree.js';
33
31
  import { globalColToVisibleCol, seqPosOfGlobalCol, visibleColToGlobalCol, visibleColsBefore, } from './rowCoordinateCalculations.js';
34
32
  import { resolveScale } from './scales.js';
35
33
  import { buildSeqPosIndex } from './seqPosToGlobalCol.js';
36
34
  import { maxBitsFor } from './sequenceLogo.js';
35
+ import { blosum62ColumnColors, identityColumnColors, } from './similarityColors.js';
37
36
  import { stripDefault } from './stripDefault.js';
38
37
  import { treeTips, widestLabel } from './tipLabels.js';
39
- 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';
40
40
  import { saveAs } from './vendor/fileSaver.js';
41
41
  import { writeNewick } from './writeNewick.js';
42
42
  import { parseWuss } from './wuss.js';
43
- function parseTreeText(text) {
44
- if (text.startsWith('BioTreeContainer')) {
45
- return flatToTree(parseAsn1(text));
46
- }
47
- return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text);
48
- }
43
+ import { takeZoomSnapshot, zoomSnapshotCovers } from './zoomSnapshot.js';
49
44
  // The height each kind draws at before the user drags a divider. A text track
50
45
  // is absent: it is one alignment row tall and follows rowHeight, so the zoom
51
46
  // controls already size it.
@@ -69,186 +64,6 @@ const ownHeightKey = (id) => `own:${id}`;
69
64
  // them
70
65
  const HELIX_ARC = '#4e79a7';
71
66
  const PSEUDOKNOT_ARC = '#e15759';
72
- // the fill of a span under a scale that gives its value no color, such as a
73
- // gene a `map` over gene names leaves out
74
- const UNSCALED_FEATURE = '#d9d9d9';
75
- // the fraction of a span a following one may cover before it takes a lane of
76
- // its own in a `features` panel
77
- const laneOverlap = 0.1;
78
- // a row panel's own width, or a default: the row height, which makes a strip
79
- // cell square, and a gene neighborhood's width for a features panel. The row
80
- // height is read only when used, so a panel with a width of its own does not
81
- // recompute on a vertical zoom
82
- function rowPanelWidth(panel, view) {
83
- return (panel.width ??
84
- (panel.kind === 'features' ? defaultFeaturePanelWidth : view.rowHeight));
85
- }
86
- // one row of the row table as the channels read it. A number or a boolean
87
- // reads as its string, the way a GFF value does, and null or an object drops
88
- function stringFields(row) {
89
- if (typeof row !== 'object' || row === null) {
90
- return undefined;
91
- }
92
- const fields = {};
93
- for (const [field, value] of Object.entries(row)) {
94
- if (typeof value === 'string') {
95
- fields[field] = value;
96
- }
97
- else if (typeof value === 'number' || typeof value === 'boolean') {
98
- fields[field] = String(value);
99
- }
100
- }
101
- return fields;
102
- }
103
- // the color each row takes from a field of the row table under a scale
104
- function colorByRow(rowData, field, colorOf) {
105
- const colors = new Map();
106
- for (const [name, row] of Object.entries(rowData)) {
107
- const value = row?.[field];
108
- const color = value === undefined ? undefined : colorOf(value);
109
- if (color) {
110
- colors.set(name, color);
111
- }
112
- }
113
- return colors;
114
- }
115
- // the fill and outline of every feature in a list: its own GFF `color=` first,
116
- // then the scale an encoding resolves over one of its fields, then the
117
- // accession palette
118
- function featureColorMap(annotations, encoding, fillPalette) {
119
- const strokes = new Map();
120
- const strokeOf = (fill) => {
121
- const hit = strokes.get(fill);
122
- if (hit !== undefined) {
123
- return hit;
124
- }
125
- const stroke = outlineColor(fill);
126
- strokes.set(fill, stroke);
127
- return stroke;
128
- };
129
- return new Map(annotations.map(annotation => {
130
- const value = encoding
131
- ? featureField(annotation, encoding.field)
132
- : undefined;
133
- const fill = annotation.color ??
134
- (value === undefined ? undefined : encoding.colorOf(value)) ??
135
- (encoding ? UNSCALED_FEATURE : fillPalette[annotation.accession]);
136
- return [annotation, { fill, stroke: strokeOf(fill) }];
137
- }));
138
- }
139
- // the text a label channel draws inside each span, for the features carrying
140
- // the field it names
141
- function featureLabelMap(annotations, field) {
142
- const labels = new Map();
143
- for (const annotation of annotations) {
144
- const value = featureField(annotation, field);
145
- if (value !== undefined) {
146
- labels.set(annotation, value);
147
- }
148
- }
149
- return labels;
150
- }
151
- /**
152
- * Lanes for `position: "strandpile"`, gggenomes' `position_strandpile`: the
153
- * forward features stack above the line and the reverse below it, each strand
154
- * packing on its own. The deepest row on each side sets the grid every row lays
155
- * out on, so the line between the strands sits at one height down the panel and
156
- * a reader can scan it. A feature with no strand piles with the forward ones.
157
- */
158
- function strandpileLanes(rows) {
159
- const piled = rows.map(([name, bands]) => {
160
- const reverse = packDomainLanes(bands.filter(b => b.annotation.strand === -1));
161
- const forward = packDomainLanes(bands.filter(b => b.annotation.strand !== -1));
162
- return { name, forward, reverse };
163
- });
164
- const up = Math.max(0, ...piled.map(p => p.forward[0]?.laneCount ?? 0));
165
- const down = Math.max(0, ...piled.map(p => p.reverse[0]?.laneCount ?? 0));
166
- const laneCount = up + down;
167
- return new Map(piled.map(({ name, forward, reverse }) => [
168
- name,
169
- [
170
- // forward level 0 is the lane just above the line, stacking upward
171
- ...forward.map(b => ({ ...b, lane: up - 1 - b.lane, laneCount })),
172
- ...reverse.map(b => ({ ...b, lane: up + b.lane, laneCount })),
173
- ],
174
- ]));
175
- }
176
- /** one row's spans laid out by the panel's `position` */
177
- function panelLanes(panel, rows) {
178
- return panel.position === 'strandpile'
179
- ? strandpileLanes(rows)
180
- : new Map(rows.map(([name, bands]) => [name, packDomainLanes(bands)]));
181
- }
182
- /**
183
- * The spans a `features` panel draws, keyed by row name and measured in the
184
- * panel's own pixels. `column` takes the bands the overlay draws, at the
185
- * alignment's column width. `position` packs each row's features in its own
186
- * residue positions, shifted by an `align` transform, and maps the extent they
187
- * cover across every row onto the panel width, so a row with no alignment
188
- * still has an x.
189
- */
190
- function featurePanelSpans({ panel, width, colWidth, domainBands, annotationsByRow, shifts, }) {
191
- if (panel.x === 'column') {
192
- return panelLanes(panel, [...domainBands].map(([name, bands]) => [
193
- name,
194
- bands.map(band => ({
195
- ...band,
196
- xStart: band.startCol * colWidth,
197
- xEnd: band.endCol * colWidth,
198
- })),
199
- ]));
200
- }
201
- const shifted = Object.entries(annotationsByRow).map(([name, annotations]) => {
202
- const shift = shifts?.get(name) ?? 0;
203
- return [
204
- name,
205
- annotations.map(annotation => ({
206
- annotation,
207
- start: annotation.start - 1 + shift,
208
- end: annotation.end + shift,
209
- })),
210
- ];
211
- });
212
- let min = Infinity;
213
- let max = -Infinity;
214
- for (const [, features] of shifted) {
215
- for (const { start, end } of features) {
216
- min = Math.min(min, start);
217
- max = Math.max(max, end);
218
- }
219
- }
220
- // a gene arrow ends where its feature does, so the extent maps onto the whole
221
- // panel less the pixel the rightmost stroke needs
222
- const drawable = Math.max(1, width - 1);
223
- const scale = max > min ? drawable / (max - min) : 0;
224
- // a lane opens where a span covers more than a tenth of the one before it,
225
- // which keeps the genes of an operon on one lane: adjacent genes commonly
226
- // share a few bases, and a stop codon overlapping the next start reads as a
227
- // second lane over the whole row
228
- return panelLanes(panel, shifted.map(([name, features]) => [
229
- name,
230
- features.map(({ annotation, start, end }) => ({
231
- annotation,
232
- xStart: (start - min) * scale,
233
- xEnd: (end - min) * scale,
234
- startCol: start,
235
- endCol: end - (end - start) * laneOverlap,
236
- })),
237
- ]));
238
- }
239
- // a scale over the values a field takes across every feature loaded, so hiding
240
- // a type in the filter dialog leaves the rest in their colors, and a legend of
241
- // the values the features drawn carry
242
- function resolveFeatureScale(field, scale, annotations, drawn) {
243
- const valuesOf = (list) => list.map(a => featureField(a, field)).filter(notEmpty);
244
- const resolved = resolveScale(scale, valuesOf(annotations));
245
- const shown = new Set(valuesOf(drawn));
246
- return {
247
- field,
248
- ...resolved,
249
- legend: resolved.legend.filter(entry => shown.has(entry.id)),
250
- };
251
- }
252
67
  const columnTrackBytes = new WeakMap();
253
68
  function columnTrackSize(track) {
254
69
  let bytes = columnTrackBytes.get(track);
@@ -301,6 +116,7 @@ export const preservedOnReset = new Set([
301
116
  'drawNodeBubbles',
302
117
  'drawNodeLabels',
303
118
  'showTreeOverview',
119
+ 'showTreeScaleBar',
304
120
  'overviewHeight',
305
121
  'turnedOffTracks',
306
122
  'trackHeights',
@@ -308,212 +124,17 @@ export const preservedOnReset = new Set([
308
124
  'allowedGappyness',
309
125
  'subFeatureRows',
310
126
  'showDomainLegend',
127
+ 'domainFill',
128
+ 'embeddedMode',
311
129
  ]);
312
130
  // `turnedOffTracks` holds only the user's explicit choices, so a hidden-by-default
313
- // track writes nothing into the snapshot. A file-supplied track passes its own
314
- // `defaultOff`, since the count varies by file: a Pfam seed has a couple of #=GR
315
- // lines, an Rfam family one per row.
316
- function trackIsOff(turnedOffTracks, id, defaultOff) {
317
- 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);
318
134
  }
319
- // the marks that set tree state from a node rather than drawing
320
- const seedingMarks = new Set(['collapse', 'focus', 'rotate']);
321
135
  // shared empty results, so observers don't see a fresh [] as a change
322
136
  const noDomains = [];
323
137
  const noClades = [];
324
- /**
325
- * Each tip of the full tree in the order the tree draws it, with the on-screen
326
- * row that shows it: its own, or the row of the collapsed node standing for it
327
- */
328
- function tipRows(root) {
329
- const rows = new Map();
330
- let row = 0;
331
- const pushReversed = (stack, nodes) => {
332
- for (let i = nodes.length - 1; i >= 0; i--) {
333
- stack.push(nodes[i]);
334
- }
335
- };
336
- const stack = [root];
337
- while (stack.length > 0) {
338
- const node = stack.pop();
339
- if (node.children?.length) {
340
- pushReversed(stack, node.children);
341
- }
342
- else {
343
- const hidden = [node];
344
- while (hidden.length > 0) {
345
- const tip = hidden.pop();
346
- const kids = tip.children ?? tip._children;
347
- if (kids?.length) {
348
- pushReversed(hidden, kids);
349
- }
350
- else {
351
- rows.set(tip.data.name, { order: rows.size, row });
352
- }
353
- }
354
- row++;
355
- }
356
- }
357
- return rows;
358
- }
359
- /**
360
- * The rows a clade covers and the node an `mrca` names, or undefined when the
361
- * clade does not resolve: a tip name the tree does not have or has twice, or a
362
- * leaf count `tips` disagrees with. A `range` counts its tips in the order
363
- * `tips` gives them, so a collapsed clade inside it counts every leaf it
364
- * stands for. `index` is the memoized leaf pass over the tree.
365
- */
366
- function cladeRows(clade, root, index, tips) {
367
- if (clade.range) {
368
- const [a, b] = clade.range.map(name => index.get(name) ? tips.get(name) : undefined);
369
- if (a === undefined || b === undefined) {
370
- return undefined;
371
- }
372
- const [first, last] = a.order <= b.order ? [a, b] : [b, a];
373
- return last.order - first.order + 1 === clade.tips
374
- ? { rows: [first.row, last.row] }
375
- : undefined;
376
- }
377
- const node = clade.mrca ? mrca(root, clade.mrca, index) : undefined;
378
- if (!node) {
379
- return undefined;
380
- }
381
- const nodeTips = leaves(node);
382
- if (nodeTips.length !== clade.tips) {
383
- return undefined;
384
- }
385
- let first = Infinity;
386
- let last = -Infinity;
387
- for (const tip of nodeTips) {
388
- const row = tips.get(tip.data.name)?.row;
389
- if (row !== undefined) {
390
- first = Math.min(first, row);
391
- last = Math.max(last, row);
392
- }
393
- }
394
- return first <= last
395
- ? { rows: [first, last], nodeId: node.data.id }
396
- : undefined;
397
- }
398
- /**
399
- * Every clade that resolves, with its fill color settled, against the rows of
400
- * `displayed`, the tree as drawn. The tree panel resolves against the
401
- * displayed rows and the overview against its own, so both take the tree they
402
- * draw. A `range` record names no node, so the seeding marks, which need one,
403
- * drop it.
404
- */
405
- function resolveClades(clades, root, displayed) {
406
- const index = leafIndex(root);
407
- const tips = tipRows(displayed);
408
- return clades.flatMap(clade => {
409
- const resolved = cladeRows(clade, root, index, tips);
410
- const seeding = seedingMarks.has(clade.mark);
411
- return resolved && !(seeding && resolved.nodeId === undefined)
412
- ? [
413
- {
414
- ...resolved,
415
- mark: clade.mark,
416
- color: withAlpha(clade.color ?? cladeHighlightColor, cladeHighlightAlpha),
417
- markColor: clade.color,
418
- label: clade.label,
419
- },
420
- ]
421
- : [];
422
- });
423
- }
424
- const childOrders = {
425
- input: undefined,
426
- branchLength: (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1),
427
- ladderize: (a, b) => a.value - b.value,
428
- ladderizeReverse: (a, b) => b.value - a.value,
429
- };
430
- /**
431
- * The tree with the display transforms applied: leaf counts summed, children
432
- * put in `order` and each `rotated` node's reversed, `showOnly` taken as the
433
- * new root, and each `collapsed` clade folded. The overview builds the same
434
- * tree without the focus, so it shows the focused subtree inside the whole.
435
- */
436
- function buildTreeRoot(tree, { collapsed, rotated, order, showOnly, }) {
437
- let hier = hierarchy(tree, d => d.children);
438
- hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1));
439
- const compare = childOrders[order];
440
- if (compare) {
441
- sort(hier, compare);
442
- }
443
- if (rotated.length > 0) {
444
- const flip = new Set(rotated);
445
- forEachDescendant(hier, n => {
446
- if (flip.has(n.data.id)) {
447
- n.children?.reverse();
448
- }
449
- });
450
- }
451
- if (showOnly) {
452
- const res = find(hier, n => n.data.id === showOnly);
453
- if (res) {
454
- hier = res;
455
- }
456
- }
457
- for (const collapsedId of collapsed) {
458
- const node = find(hier, n => n.data.id === collapsedId);
459
- if (node) {
460
- if (node.children) {
461
- collapse(node);
462
- }
463
- else if (node.parent?.children) {
464
- node.parent.children = node.parent.children.filter(c => c.data.id !== collapsedId);
465
- }
466
- }
467
- }
468
- return hier;
469
- }
470
- /**
471
- * The node whose branch an outgroup roots the tree on: the outgroup's most
472
- * recent common ancestor, or the rest of the tips' where that ancestor is the
473
- * root, since the outgroup then straddles the root and the branch above the
474
- * other side is the same branch of the unrooted tree.
475
- */
476
- function outgroupNode(root, outgroup) {
477
- const index = leafIndex(root);
478
- const node = mrca(root, outgroup, index);
479
- if (node !== root) {
480
- return node;
481
- }
482
- const out = new Set(outgroup);
483
- const rest = leaves(root)
484
- .map(n => n.data.name)
485
- .filter(name => !out.has(name));
486
- const other = mrca(root, rest, index);
487
- return other === root ? undefined : other;
488
- }
489
- /**
490
- * `tree` rerooted as `treeRoot` asks, with fresh path ids. The ids of the tree
491
- * as written become names on unnamed nodes, and a rerooting would carry them
492
- * along as labels, so they come off first.
493
- */
494
- function rootTree(tree, treeRoot) {
495
- const plain = new Map();
496
- for (const node of preorder(tree).reverse()) {
497
- plain.set(node, {
498
- name: node.name === node.id ? undefined : node.name,
499
- length: node.length,
500
- children: node.children.map(c => plain.get(c)),
501
- });
502
- }
503
- const input = plain.get(tree);
504
- if (treeRoot === 'midpoint') {
505
- return generateNodeIds(midpointRoot(input));
506
- }
507
- const target = outgroupNode(hierarchy(tree, d => d.children), treeRoot.outgroup);
508
- return target
509
- ? generateNodeIds(rerootTree(input, plain.get(target.data)))
510
- : tree;
511
- }
512
- // the inclusive tip indices a node covers, from its xMin/xMax in a layout one
513
- // row per tip
514
- function tipRange(node) {
515
- return [Math.round(node.xMin - 0.5), Math.round(node.xMax - 0.5)];
516
- }
517
138
  // the channels reading the feature table; every other channel reads rowData
518
139
  const featureChannels = new Set([
519
140
  'featureFill',
@@ -538,6 +159,22 @@ function sameLength(segment) {
538
159
  return (segment.rowEnd - segment.rowStart ===
539
160
  segment.structEnd - segment.structStart);
540
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
+ }
541
178
  // fit() divides the viewport by the row or column count and multiplies back, so
542
179
  // an exact fit can land a fraction of a pixel over. A plain `>` would then show
543
180
  // a scrollbar that shrinks the viewport and leaves a gap.
@@ -569,19 +206,6 @@ function sharedValue(values) {
569
206
  ? first
570
207
  : undefined;
571
208
  }
572
- // preorder, so reversing the list puts every node after its descendants
573
- function preorder(tree) {
574
- const order = [];
575
- const stack = [tree];
576
- while (stack.length > 0) {
577
- const node = stack.pop();
578
- order.push(node);
579
- for (const child of node.children) {
580
- stack.push(child);
581
- }
582
- }
583
- return order;
584
- }
585
209
  /**
586
210
  * #stateModel MsaView
587
211
  *
@@ -619,6 +243,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
619
243
  * #property
620
244
  */
621
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),
622
253
  /**
623
254
  * #property
624
255
  * whether the domain legend is expanded. The legend floats over the
@@ -647,6 +278,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
647
278
  * #property
648
279
  */
649
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),
650
287
  /**
651
288
  * #property
652
289
  * zoom in/out on plain mouse-wheel without holding ctrl
@@ -771,10 +408,10 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
771
408
  data: types.optional(DataModelF(maxInlineSnapshotBytes), {}),
772
409
  /**
773
410
  * #property
774
- * the user's explicit hide choices per annotation accession, keyed by
775
- * accession with the value meaning "off", like `turnedOffTracks`. An
776
- * untouched accession is absent and drawn, so the shared URL grows
777
- * 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
778
415
  */
779
416
  turnedOffFeatures: stripDefault(types.map(types.boolean), {}),
780
417
  /**
@@ -854,6 +491,18 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
854
491
  * #volatile
855
492
  */
856
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 },
857
506
  /**
858
507
  * #volatile
859
508
  * leaves the toolbar out, for a host drawing its own controls. Kept out
@@ -875,6 +524,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
875
524
  * canvas scale factor, from the device pixel ratio
876
525
  */
877
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,
878
534
  /**
879
535
  * #volatile
880
536
  */
@@ -1019,6 +675,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1019
675
  setWidth(arg) {
1020
676
  self.volatileWidth = arg;
1021
677
  },
678
+ /**
679
+ * #action
680
+ */
681
+ setSettingsOpen(arg) {
682
+ self.settingsOpen = arg;
683
+ },
1022
684
  /**
1023
685
  * #action
1024
686
  * update the canvas scale factor when the device pixel ratio changes
@@ -1156,6 +818,23 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1156
818
  setShowDomains(arg) {
1157
819
  self.showDomains = arg;
1158
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
+ },
1159
838
  /**
1160
839
  * #action
1161
840
  * expand or collapse the domain legend that floats over the alignment
@@ -1183,6 +862,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1183
862
  */
1184
863
  setRowHeight(n) {
1185
864
  self.rowHeight = n;
865
+ self.zoomHeld = { ...self.zoomHeld, row: 0 };
1186
866
  },
1187
867
  /**
1188
868
  * #action
@@ -1190,6 +870,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1190
870
  */
1191
871
  setColWidth(n) {
1192
872
  self.colWidth = n;
873
+ self.zoomHeld = { ...self.zoomHeld, col: 0 };
1193
874
  },
1194
875
  /**
1195
876
  * #action
@@ -1340,22 +1021,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1340
1021
  self.showOnly !== undefined ||
1341
1022
  self.allowedGappyness < 100));
1342
1023
  },
1343
- /**
1344
- * #getter
1345
- * the annotations the overlay, the legend and the encodings draw: the
1346
- * GFF's, then the `features` layer's
1347
- */
1348
- get allAnnotations() {
1349
- return self.features.length > 0
1350
- ? [...self.annotations, ...featuresToAnnotations(self.features)]
1351
- : self.annotations;
1352
- },
1353
- /**
1354
- * #getter
1355
- */
1356
- get actuallyShowDomains() {
1357
- return self.showDomains && this.allAnnotations.length > 0;
1358
- },
1359
1024
  /**
1360
1025
  * #getter
1361
1026
  * whether the host restores the loaded documents outside the snapshot.
@@ -1380,7 +1045,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1380
1045
  if (self.volatileWidth === undefined) {
1381
1046
  throw new Error('not initialized');
1382
1047
  }
1383
- return self.volatileWidth;
1048
+ return self.volatileWidth - (self.settingsOpen ? settingsPanelWidth : 0);
1384
1049
  },
1385
1050
  }))
1386
1051
  .views(self => ({
@@ -1427,7 +1092,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1427
1092
  return !!this.tree.noTree;
1428
1093
  },
1429
1094
  get noDomains() {
1430
- return self.allAnnotations.length === 0;
1095
+ return this.allAnnotations.length === 0;
1431
1096
  },
1432
1097
  /**
1433
1098
  * #getter
@@ -1528,6 +1193,27 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1528
1193
  ? parseMSA(text, self.currentAlignment, self.msaFormat)
1529
1194
  : null;
1530
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
+ },
1531
1217
  /**
1532
1218
  * #getter
1533
1219
  */
@@ -2056,6 +1742,18 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2056
1742
  const { colStats } = this;
2057
1743
  return Array.from({ length: colStats.numColumns }, (_, col) => clustalXColumnColors(colStats, col));
2058
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
+ },
2059
1757
  /**
2060
1758
  * #getter
2061
1759
  * Conservation score per column using Shannon entropy (biojs-msa style).
@@ -2419,6 +2117,28 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2419
2117
  },
2420
2118
  }));
2421
2119
  },
2120
+ /**
2121
+ * #getter
2122
+ * the reference row (`relativeTo`) repeated above the alignment, so it
2123
+ * stays in view while the rows compared against it scroll
2124
+ */
2125
+ get referenceTrackModels() {
2126
+ const { relativeTo } = self;
2127
+ const data = relativeTo === undefined ? undefined : self.columns.get(relativeTo);
2128
+ return relativeTo === undefined || data === undefined
2129
+ ? []
2130
+ : [
2131
+ {
2132
+ model: {
2133
+ id: 'reference-row',
2134
+ name: relativeTo,
2135
+ kind: 'reference',
2136
+ data,
2137
+ height: self.rowHeight,
2138
+ },
2139
+ },
2140
+ ];
2141
+ },
2422
2142
  get tracks() {
2423
2143
  return [
2424
2144
  ...this.adapterTrackModels,
@@ -2427,13 +2147,14 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2427
2147
  // every computed track reads the alignment's columns, and a tree, a
2428
2148
  // GFF and a features panel draw a figure with none
2429
2149
  ...(self.numColumns > 0 ? this.computedTrackModels : []),
2150
+ ...this.referenceTrackModels,
2430
2151
  ];
2431
2152
  },
2432
2153
  /**
2433
2154
  * #getter
2434
2155
  */
2435
2156
  get turnedOnTracks() {
2436
- return this.tracks.filter(f => !trackIsOff(self.turnedOffTracks, f.model.id, f.model.defaultOff));
2157
+ return this.tracks.filter(f => !trackIsOff(self.turnedOffTracks, f.model.id));
2437
2158
  },
2438
2159
  /**
2439
2160
  * #getter
@@ -2722,7 +2443,9 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2722
2443
  if (mapping.structure.id !== structureId) {
2723
2444
  continue;
2724
2445
  }
2725
- if (asymId !== undefined && mapping.structure.asymId !== asymId) {
2446
+ if (asymId !== undefined &&
2447
+ mapping.structure.asymId !== undefined &&
2448
+ mapping.structure.asymId !== asymId) {
2726
2449
  continue;
2727
2450
  }
2728
2451
  const segment = mapping.segments.find(seg => position >= seg.structStart && position <= seg.structEnd);
@@ -2739,10 +2462,14 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2739
2462
  .views(self => ({
2740
2463
  /**
2741
2464
  * #getter
2742
- * the branch-length scale bar over the tree, undefined in cladogram mode
2743
- * or when the tree area is too narrow for one
2465
+ * the branch-length scale bar over the tree, undefined while
2466
+ * `showTreeScaleBar` is off, in cladogram mode, or when the tree area is
2467
+ * too narrow for one
2744
2468
  */
2745
2469
  get treeScaleBar() {
2470
+ if (!self.showTreeScaleBar) {
2471
+ return undefined;
2472
+ }
2746
2473
  return scaleBarLength(self.pxPerBranchLength, self.effectiveTreeAreaWidth - self.marginLeft * 2);
2747
2474
  },
2748
2475
  /**
@@ -2789,8 +2516,38 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2789
2516
  }
2790
2517
  return types;
2791
2518
  },
2519
+ /**
2520
+ * accessions hidden until the user turns them on, such as a Stockholm
2521
+ * file's #=GR lines
2522
+ */
2523
+ get defaultOffAccessions() {
2524
+ return new Set(self.allAnnotations.filter(a => a.defaultOff).map(a => a.accession));
2525
+ },
2526
+ /**
2527
+ * #method
2528
+ */
2529
+ featureOff(accession) {
2530
+ return (self.turnedOffFeatures.get(accession) ??
2531
+ this.defaultOffAccessions.has(accession));
2532
+ },
2792
2533
  get filteredAnnotations() {
2793
- return self.allAnnotations.filter(r => !self.turnedOffFeatures.get(r.accession));
2534
+ return self.allAnnotations.filter(r => !this.featureOff(r.accession));
2535
+ },
2536
+ /**
2537
+ * #getter
2538
+ * what colors the alignment's cells: the residue scheme, the domains
2539
+ * covering them, or nothing, which leaves the scheme to the letters.
2540
+ * Domains own the fill only while some are drawn, so a file whose
2541
+ * annotations all start hidden keeps its residue colors
2542
+ */
2543
+ get cellFill() {
2544
+ return !self.bgColor
2545
+ ? 'none'
2546
+ : self.actuallyShowDomains &&
2547
+ self.domainFill &&
2548
+ this.filteredAnnotations.length > 0
2549
+ ? 'domain'
2550
+ : 'residue';
2794
2551
  },
2795
2552
  get annotationsByRow() {
2796
2553
  const { rowNamesSet } = self;
@@ -2866,6 +2623,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2866
2623
  get blocks2d() {
2867
2624
  return self.blocksY.flatMap(by => self.blocksX.map(bx => [bx, by]));
2868
2625
  },
2626
+ /**
2627
+ * #getter
2628
+ */
2629
+ get wheelZooming() {
2630
+ return self.zoomSnapshot !== undefined;
2631
+ },
2869
2632
  /**
2870
2633
  * #getter
2871
2634
  */
@@ -3062,11 +2825,20 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3062
2825
  transaction(() => {
3063
2826
  const colInView = (-self.scrollX + offsetX) / self.colWidth;
3064
2827
  const rowInView = (-self.scrollY + offsetY) / self.rowHeight;
2828
+ // zooming both axes, an axis holds at the default only while the
2829
+ // other sits there too, or one would hold while the other grows
2830
+ const canHold = axis !== 'both' ||
2831
+ (self.colWidth === defaultColWidth &&
2832
+ self.rowHeight === defaultRowHeight);
3065
2833
  if (axis !== 'vertical') {
3066
- self.colWidth = clamp(self.colWidth * scaleFactor, minColWidth, maxCellSize);
2834
+ const { value, held } = detentZoom(self.colWidth, scaleFactor, defaultColWidth, self.zoomHeld.col, minColWidth, canHold);
2835
+ self.colWidth = value;
2836
+ self.zoomHeld = { ...self.zoomHeld, col: held };
3067
2837
  }
3068
2838
  if (axis !== 'horizontal') {
3069
- self.rowHeight = clamp(self.rowHeight * scaleFactor, minRowHeight, maxCellSize);
2839
+ const { value, held } = detentZoom(self.rowHeight, scaleFactor, defaultRowHeight, self.zoomHeld.row, minRowHeight, canHold);
2840
+ self.rowHeight = value;
2841
+ self.zoomHeld = { ...self.zoomHeld, row: held };
3070
2842
  }
3071
2843
  self.scrollX = clamp(offsetX - colInView * self.colWidth, self.maxScrollX, 0);
3072
2844
  const anchoredScrollY = offsetY - rowInView * self.rowHeight;
@@ -3077,6 +2849,25 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3077
2849
  self.scrollY = clamp(anchoredScrollY * (1 - topBias), self.maxScrollY, 0);
3078
2850
  });
3079
2851
  },
2852
+ /**
2853
+ * #action
2854
+ * zoomToPos for one wheel or pinch frame. The alignment keeps the blocks
2855
+ * it drew at the start of the gesture, scaled, and redraws them only once
2856
+ * they stop covering the viewport; endWheelZoom redraws them sharp
2857
+ */
2858
+ wheelZoomToPos(scaleFactor, offsetX, offsetY) {
2859
+ self.zoomSnapshot ??= takeZoomSnapshot(self, false);
2860
+ this.zoomToPos(scaleFactor, offsetX, offsetY, self.wheelZoomAxis);
2861
+ if (!zoomSnapshotCovers(self, self.zoomSnapshot)) {
2862
+ self.zoomSnapshot = takeZoomSnapshot(self, true);
2863
+ }
2864
+ },
2865
+ /**
2866
+ * #action
2867
+ */
2868
+ endWheelZoom() {
2869
+ self.zoomSnapshot = undefined;
2870
+ },
3080
2871
  /**
3081
2872
  * #action
3082
2873
  */
@@ -3155,8 +2946,23 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3155
2946
  * #action
3156
2947
  */
3157
2948
  toggleTrack(id) {
3158
- const defaultOff = self.MSA?.tracks.find(t => t.id === id)?.defaultOff;
3159
- self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id, defaultOff));
2949
+ self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id));
2950
+ },
2951
+ /**
2952
+ * #action
2953
+ * show or hide several tracks at once, as the track selector's Show all
2954
+ * and Hide all do. A choice matching the track's default is dropped, so
2955
+ * it stays out of the shared URL
2956
+ */
2957
+ setTracksShown(ids, shown) {
2958
+ for (const id of ids) {
2959
+ if (defaultOffTracks.has(id) === !shown) {
2960
+ self.turnedOffTracks.delete(id);
2961
+ }
2962
+ else {
2963
+ self.turnedOffTracks.set(id, !shown);
2964
+ }
2965
+ }
3160
2966
  },
3161
2967
  /**
3162
2968
  * #action
@@ -3302,7 +3108,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3302
3108
  */
3303
3109
  get visibleDomainTypes() {
3304
3110
  return this.categoricalDomainTypes
3305
- .filter(d => !self.turnedOffFeatures.get(d.accession))
3111
+ .filter(d => !self.featureOff(d.accession))
3306
3112
  .toSorted((a, b) => a.start - b.start);
3307
3113
  },
3308
3114
  /**
@@ -3362,15 +3168,15 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3362
3168
  /**
3363
3169
  * #getter
3364
3170
  * whether the overlay marks each domain with a bar under its row instead
3365
- * of filling the row behind the letters. Letter-color mode hands the
3366
- * background to the color scheme, so a filled box would paint over it and
3367
- * leave the setting with nothing to show. Sub-row layout already stacks
3171
+ * of filling the row behind the letters: whenever the domains do not own
3172
+ * the cell fill (`cellFill`), a filled box would paint over the residue
3173
+ * colors the user picked. Sub-row layout already stacks
3368
3174
  * the boxes clear of the letters, and with the letters too small to draw
3369
3175
  * the filled box is the only thing left to read.
3370
3176
  */
3371
3177
  get domainUnderline() {
3372
3178
  return (self.actuallyShowDomains &&
3373
- !self.bgColor &&
3179
+ self.cellFill !== 'domain' &&
3374
3180
  !self.subFeatureRows &&
3375
3181
  self.showMsaLetters);
3376
3182
  },
@@ -3819,6 +3625,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3819
3625
  setHideHeader(arg) {
3820
3626
  self.hideHeader = arg;
3821
3627
  },
3628
+ /**
3629
+ * #action
3630
+ */
3631
+ setEmbeddedMode(arg) {
3632
+ self.embeddedMode = arg;
3633
+ },
3822
3634
  /**
3823
3635
  * #action
3824
3636
  */
@@ -3883,15 +3695,16 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3883
3695
  },
3884
3696
  /**
3885
3697
  * #action
3886
- * show or hide an annotation type. Only hidden types are recorded; see
3887
- * `turnedOffFeatures`
3698
+ * show or hide an annotation type. Only a choice that differs from the
3699
+ * type's default is recorded; see `turnedOffFeatures`
3888
3700
  */
3889
3701
  setFilter(accession, shown) {
3890
- if (shown) {
3891
- self.turnedOffFeatures.delete(accession);
3702
+ // only a departure from the accession's default is recorded
3703
+ if (shown === self.defaultOffAccessions.has(accession)) {
3704
+ self.turnedOffFeatures.set(accession, !shown);
3892
3705
  }
3893
3706
  else {
3894
- self.turnedOffFeatures.set(accession, true);
3707
+ self.turnedOffFeatures.delete(accession);
3895
3708
  }
3896
3709
  },
3897
3710
  /**
@@ -3899,20 +3712,23 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3899
3712
  */
3900
3713
  fit() {
3901
3714
  // two passes: each direction's viewport depends on whether the other
3902
- // overflows (the minimap takes height, the vertical scrollbar width)
3715
+ // overflows (the minimap takes height, the vertical scrollbar width).
3716
+ // Fit shrinks an alignment into the window but never grows one past
3717
+ // the default cell size, which would stretch a small alignment's rows
3718
+ // and columns out of proportion
3903
3719
  transaction(() => {
3904
3720
  for (let pass = 0; pass < 2; pass++) {
3905
- this.fitHorizontally();
3906
- this.fitVertically();
3721
+ this.fitHorizontally(defaultColWidth);
3722
+ this.fitVertically(defaultRowHeight);
3907
3723
  }
3908
3724
  });
3909
3725
  },
3910
3726
  /**
3911
3727
  * #action
3912
3728
  */
3913
- fitVertically() {
3729
+ fitVertically(maxRowHeight = maxCellSize) {
3914
3730
  if (self.numRows > 0) {
3915
- self.rowHeight = clamp(self.msaAreaHeight / self.numRows, minRowHeight, maxCellSize);
3731
+ self.setRowHeight(clamp(self.msaAreaHeight / self.numRows, minRowHeight, maxRowHeight));
3916
3732
  }
3917
3733
  self.scrollY = 0;
3918
3734
  },
@@ -3928,7 +3744,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3928
3744
  return;
3929
3745
  }
3930
3746
  transaction(() => {
3931
- self.colWidth = clamp(self.msaCanvasWidth / (span.endCol - span.startCol + 1), minColWidth, maxCellSize);
3747
+ self.setColWidth(clamp(self.msaCanvasWidth / (span.endCol - span.startCol + 1), minColWidth, maxCellSize));
3932
3748
  self.scrollX = clamp(-span.startCol * self.colWidth, self.maxScrollX, 0);
3933
3749
  });
3934
3750
  },
@@ -3966,7 +3782,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3966
3782
  const { rowRuns } = resolvedSelection;
3967
3783
  const first = rowRuns[0][0];
3968
3784
  const last = rowRuns.at(-1)[1];
3969
- self.rowHeight = clamp(self.msaAreaHeight / (last - first + 1), minRowHeight, maxCellSize);
3785
+ self.setRowHeight(clamp(self.msaAreaHeight / (last - first + 1), minRowHeight, maxCellSize));
3970
3786
  self.scrollY = clamp(-first * self.rowHeight, self.maxScrollY, 0);
3971
3787
  }
3972
3788
  });
@@ -3998,10 +3814,10 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3998
3814
  /**
3999
3815
  * #action
4000
3816
  */
4001
- fitHorizontally() {
3817
+ fitHorizontally(maxColWidth = maxCellSize) {
4002
3818
  if (self.numColumns > 0) {
4003
3819
  // msaCanvasWidth excludes the vertical scrollbar's 20px
4004
- self.colWidth = clamp(self.msaCanvasWidth / self.numColumns, minColWidth, maxCellSize);
3820
+ self.setColWidth(clamp(self.msaCanvasWidth / self.numColumns, minColWidth, maxColWidth));
4005
3821
  }
4006
3822
  self.scrollX = 0;
4007
3823
  },
@@ -4009,17 +3825,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4009
3825
  if (self.highlightColumns?.length) {
4010
3826
  self.setHighlightedColumns(self.highlightColumns);
4011
3827
  }
4012
- // The `collapse`, `focus` and `rotate` clade marks seed the collapsed
4013
- // list, the subtree in focus and the rotated nodes, which the tree,
4014
- // `hideGapsEffective` and the alignment all read. The tree arrives with
4015
- // the model for inline data and later for a filehandle, so the seeding
4016
- // waits for it and then runs once per data load, per change of
4017
- // `clades` and per reroot, which clears the node ids the seeding
4018
- // wrote: expanding a seeded clade sticks until one of those changes.
4019
- // `dataInitialized` is true once the MSA alone has loaded, when the
4020
- // tree is still the flat stub, so a tree filehandle holds the seeding
4021
- // until its text lands.
4022
- addDisposer(self, reaction(() => self.dataInitialized && !(self.treeFilehandle && !self.data.tree)
3828
+ const awaitingTree = () => !!self.treeFilehandle && !self.data.tree;
3829
+ // `collapse`, `focus` and `rotate` marks seed `collapsed`, `showOnly`
3830
+ // and `rotated`. Seeding runs once per data load, `clades` change and
3831
+ // reroot, so expanding a seeded clade sticks until one of those. A tree
3832
+ // filehandle holds it back, since `dataInitialized` is true while the
3833
+ // tree is still the flat stub.
3834
+ addDisposer(self, reaction(() => self.dataInitialized && !awaitingTree()
4023
3835
  ? { clades: getSnapshot(self.clades), treeRoot: self.treeRoot }
4024
3836
  : undefined, seed => {
4025
3837
  if (!seed?.clades.length) {
@@ -4049,7 +3861,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4049
3861
  if (region &&
4050
3862
  self.viewInitialized &&
4051
3863
  self.numColumns > 0 &&
4052
- !(self.treeFilehandle && !self.data.tree)) {
3864
+ !awaitingTree()) {
4053
3865
  this.revealRegion(region);
4054
3866
  self.setRegion(undefined);
4055
3867
  }
@@ -4071,89 +3883,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4071
3883
  mql.removeEventListener('change', onChange);
4072
3884
  });
4073
3885
  }
4074
- /**
4075
- * Fetch a filehandle whenever it changes, and hand the text to
4076
- * `onLoad`.
4077
- *
4078
- * A generation guard keeps a slower superseded request from overwriting
4079
- * the newer one's data, status or loading flag. A superseded or cleared
4080
- * request is aborted and clears its status line.
4081
- *
4082
- * `clearFilehandle` runs after a local file loads, since a blob has no
4083
- * URL to refetch, and when the user cancels, which returns the view to
4084
- * the import form.
4085
- *
4086
- * `what` names the layer in a failure message. An `optional` layer
4087
- * (annotations, row metadata) that fails adds a warning instead of an
4088
- * error.
4089
- */
4090
- const loadOnFilehandleChange = ({ what, getFilehandle, onLoad, setLoading, clearFilehandle, optional = false, }) => {
4091
- let generation = 0;
4092
- let cancelInFlight;
4093
- addDisposer(self, () => {
4094
- cancelInFlight?.();
4095
- });
4096
- addDisposer(self, autorun(async () => {
4097
- const filehandle = getFilehandle();
4098
- const current = ++generation;
4099
- cancelInFlight?.();
4100
- cancelInFlight = undefined;
4101
- if (!filehandle) {
4102
- setLoading?.(false);
4103
- return;
4104
- }
4105
- const isCurrent = () => current === generation && isAlive(self);
4106
- const controller = new AbortController();
4107
- cancelInFlight = () => {
4108
- controller.abort();
4109
- if (isAlive(self)) {
4110
- self.setStatus(undefined);
4111
- }
4112
- };
4113
- try {
4114
- setLoading?.(true);
4115
- if (!optional) {
4116
- self.setError(undefined);
4117
- }
4118
- const text = await fetchTextWithProgress(openLocation(filehandle), status => {
4119
- if (isCurrent()) {
4120
- self.setStatus(status);
4121
- }
4122
- }, { controller });
4123
- if (isCurrent()) {
4124
- transaction(() => {
4125
- onLoad(text);
4126
- if (filehandle.locationType === 'BlobLocation') {
4127
- clearFilehandle?.();
4128
- }
4129
- });
4130
- }
4131
- }
4132
- catch (e) {
4133
- if (isCurrent()) {
4134
- if (isAbortError(e)) {
4135
- clearFilehandle?.();
4136
- }
4137
- else {
4138
- console.error(e);
4139
- if (optional) {
4140
- self.addWarning(`The ${what} did not load: ${e}`);
4141
- }
4142
- else {
4143
- self.setError(e);
4144
- }
4145
- }
4146
- }
4147
- }
4148
- finally {
4149
- if (isCurrent()) {
4150
- cancelInFlight = undefined;
4151
- setLoading?.(false);
4152
- }
4153
- }
4154
- }));
4155
- };
4156
- loadOnFilehandleChange({
3886
+ installFilehandleLoader(self, {
4157
3887
  what: 'tree',
4158
3888
  getFilehandle: () => self.treeFilehandle,
4159
3889
  onLoad: text => {
@@ -4167,7 +3897,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4167
3897
  },
4168
3898
  });
4169
3899
  // treeMetadata has no import-form step, so no loading flag or cancel
4170
- loadOnFilehandleChange({
3900
+ installFilehandleLoader(self, {
4171
3901
  what: 'row metadata',
4172
3902
  optional: true,
4173
3903
  getFilehandle: () => self.treeMetadataFilehandle,
@@ -4196,7 +3926,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4196
3926
  }
4197
3927
  }));
4198
3928
  // gffFilehandle loads into data.gff, which the autorun above parses
4199
- loadOnFilehandleChange({
3929
+ installFilehandleLoader(self, {
4200
3930
  what: 'annotations',
4201
3931
  optional: true,
4202
3932
  getFilehandle: () => self.gffFilehandle,
@@ -4207,7 +3937,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4207
3937
  self.setGFFFilehandle(undefined);
4208
3938
  },
4209
3939
  });
4210
- loadOnFilehandleChange({
3940
+ installFilehandleLoader(self, {
4211
3941
  what: 'alignment',
4212
3942
  getFilehandle: () => self.msaFilehandle,
4213
3943
  onLoad: text => {
@@ -4228,20 +3958,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4228
3958
  // xref problem https://github.com/GMOD/react-msaview/issues/75
4229
3959
  addDisposer(self, autorun(() => {
4230
3960
  if (self.dynamicColorSchemeName) {
4231
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4232
- self.colStats;
3961
+ readComputed(self.colStats);
4233
3962
  }
4234
3963
  if (self.showColumnStats) {
4235
- // everything the hover tooltip reads per column
4236
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4237
- self.colStats;
4238
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4239
- self.conservation;
4240
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4241
- self.propertyConservation;
3964
+ readComputed(self.colStats, self.conservation, self.propertyConservation);
4242
3965
  }
4243
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4244
- self.columns;
3966
+ readComputed(self.columns);
4245
3967
  }));
4246
3968
  },
4247
3969
  }))