react-msaview 9.1.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 (272) 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 +62 -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 +55 -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/tree/TreeCanvasBlock.js +2 -2
  119. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  120. package/dist/components/tree/TreeMenu.js +4 -6
  121. package/dist/components/tree/TreeMenu.js.map +1 -1
  122. package/dist/components/tree/TreeOverview.js +2 -2
  123. package/dist/components/tree/TreeOverview.js.map +1 -1
  124. package/dist/constants.d.ts +4 -1
  125. package/dist/constants.js +7 -1
  126. package/dist/constants.js.map +1 -1
  127. package/dist/darkReader.d.ts +7 -0
  128. package/dist/darkReader.js +42 -0
  129. package/dist/darkReader.js.map +1 -0
  130. package/dist/element.js +22 -47
  131. package/dist/element.js.map +1 -1
  132. package/dist/featurePanels.d.ts +37 -0
  133. package/dist/featurePanels.js +187 -0
  134. package/dist/featurePanels.js.map +1 -0
  135. package/dist/filehandleLoader.d.ts +41 -0
  136. package/dist/filehandleLoader.js +99 -0
  137. package/dist/filehandleLoader.js.map +1 -0
  138. package/dist/index.d.ts +3 -0
  139. package/dist/index.js +2 -0
  140. package/dist/index.js.map +1 -1
  141. package/dist/model/treeModel.d.ts +9 -0
  142. package/dist/model/treeModel.js +12 -1
  143. package/dist/model/treeModel.js.map +1 -1
  144. package/dist/model.d.ts +142 -32
  145. package/dist/model.js +268 -569
  146. package/dist/model.js.map +1 -1
  147. package/dist/neighborJoining.js +1 -40
  148. package/dist/neighborJoining.js.map +1 -1
  149. package/dist/renderToSvg.js +1 -13
  150. package/dist/renderToSvg.js.map +1 -1
  151. package/dist/rerootTree.d.ts +1 -0
  152. package/dist/rerootTree.js +7 -6
  153. package/dist/rerootTree.js.map +1 -1
  154. package/dist/schemeResidueColor.d.ts +12 -0
  155. package/dist/schemeResidueColor.js +33 -0
  156. package/dist/schemeResidueColor.js.map +1 -0
  157. package/dist/similarityColors.d.ts +11 -0
  158. package/dist/similarityColors.js +90 -0
  159. package/dist/similarityColors.js.map +1 -0
  160. package/dist/svgTestUtil.d.ts +62 -12
  161. package/dist/treeDisplay.d.ts +37 -0
  162. package/dist/treeDisplay.js +209 -0
  163. package/dist/treeDisplay.js.map +1 -0
  164. package/dist/types.d.ts +6 -2
  165. package/dist/useWheelScroll.d.ts +3 -1
  166. package/dist/useWheelScroll.js +36 -17
  167. package/dist/useWheelScroll.js.map +1 -1
  168. package/dist/util.d.ts +2 -0
  169. package/dist/util.js +2 -0
  170. package/dist/util.js.map +1 -1
  171. package/dist/version.d.ts +1 -1
  172. package/dist/version.js +1 -1
  173. package/dist/wheelZoom.d.ts +8 -3
  174. package/dist/wheelZoom.js +44 -32
  175. package/dist/wheelZoom.js.map +1 -1
  176. package/dist/zoomSnapshot.d.ts +29 -0
  177. package/dist/zoomSnapshot.js +47 -0
  178. package/dist/zoomSnapshot.js.map +1 -0
  179. package/package.json +11 -11
  180. package/src/blosum62.ts +45 -0
  181. package/src/colorSchemes.ts +1 -0
  182. package/src/components/Loading.tsx +6 -2
  183. package/src/components/MSAView.tsx +22 -5
  184. package/src/components/MetadataTable.tsx +83 -26
  185. package/src/components/Track.tsx +13 -0
  186. package/src/components/controls.ts +2 -1
  187. package/src/components/dialogs/AnnotationFileDialog.tsx +12 -1
  188. package/src/components/dialogs/FeatureDialog.tsx +2 -5
  189. package/src/components/dialogs/GapThresholdDialog.tsx +36 -0
  190. package/src/components/{header → dialogs}/GappynessSlider.tsx +3 -10
  191. package/src/components/{header → dialogs}/GoToBox.tsx +36 -10
  192. package/src/components/dialogs/GoToDialog.tsx +30 -0
  193. package/src/components/dialogs/MetadataDialog.tsx +32 -4
  194. package/src/components/{header → dialogs}/goTo.ts +1 -1
  195. package/src/components/dialogs/metadataLabels.ts +54 -0
  196. package/src/components/dialogs/metadataRecord.tsx +61 -0
  197. package/src/components/header/Header.tsx +75 -42
  198. package/src/components/header/HiddenColumnsChip.tsx +39 -0
  199. package/src/components/header/MenuBar.tsx +64 -0
  200. package/src/components/header/ScrollZoomCheckbox.tsx +46 -0
  201. package/src/components/header/ZoomControls.tsx +20 -35
  202. package/src/components/header/colorSchemeMenuItems.ts +121 -0
  203. package/src/components/header/getAnnotationsMenu.ts +52 -56
  204. package/src/components/header/menuBarItems.ts +357 -0
  205. package/src/components/import/ImportForm.tsx +19 -7
  206. package/src/components/minimap/Minimap.tsx +11 -3
  207. package/src/components/minimap/MinimapSVG.tsx +17 -1
  208. package/src/components/minimap/minimapLayout.ts +27 -0
  209. package/src/components/msa/MSACanvas.tsx +22 -3
  210. package/src/components/msa/MSACanvasBlock.tsx +18 -11
  211. package/src/components/msa/MSAMouseoverCanvas.tsx +2 -2
  212. package/src/components/msa/WheelZoomHint.tsx +56 -0
  213. package/src/components/msa/msaKeys.ts +3 -0
  214. package/src/components/msa/msaRaster.ts +6 -8
  215. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +28 -5
  216. package/src/components/msa/renderHighlights.ts +3 -3
  217. package/src/components/msa/renderMSABlock.ts +51 -15
  218. package/src/components/msa/tileColor.ts +7 -0
  219. package/src/components/rowpanels/RowPanelBlock.tsx +2 -2
  220. package/src/components/settings/MenuItemsForm.tsx +225 -0
  221. package/src/components/settings/SettingsPanel.tsx +156 -0
  222. package/src/components/settings/TrackList.tsx +119 -0
  223. package/src/components/tracks/TrackBlocks.tsx +2 -2
  224. package/src/components/tree/TreeCanvasBlock.tsx +2 -2
  225. package/src/components/tree/TreeMenu.tsx +4 -6
  226. package/src/components/tree/TreeOverview.tsx +2 -2
  227. package/src/constants.ts +9 -1
  228. package/src/darkReader.ts +57 -0
  229. package/src/element.ts +34 -57
  230. package/src/featurePanels.ts +269 -0
  231. package/src/filehandleLoader.ts +136 -0
  232. package/src/index.ts +4 -0
  233. package/src/model/treeModel.ts +13 -0
  234. package/src/model.ts +345 -748
  235. package/src/neighborJoining.ts +1 -42
  236. package/src/renderToSvg.tsx +1 -13
  237. package/src/rerootTree.ts +7 -6
  238. package/src/schemeResidueColor.ts +37 -0
  239. package/src/similarityColors.ts +112 -0
  240. package/src/treeDisplay.ts +278 -0
  241. package/src/types.ts +6 -3
  242. package/src/useWheelScroll.ts +43 -22
  243. package/src/util.ts +3 -0
  244. package/src/version.ts +1 -1
  245. package/src/wheelZoom.ts +55 -33
  246. package/src/zoomSnapshot.ts +80 -0
  247. package/dist/components/header/ColorSchemeMenu.d.ts +0 -12
  248. package/dist/components/header/ColorSchemeMenu.js +0 -37
  249. package/dist/components/header/ColorSchemeMenu.js.map +0 -1
  250. package/dist/components/header/FileMenu.js +0 -92
  251. package/dist/components/header/FileMenu.js.map +0 -1
  252. package/dist/components/header/GappynessSlider.js.map +0 -1
  253. package/dist/components/header/GoToBox.js +0 -31
  254. package/dist/components/header/GoToBox.js.map +0 -1
  255. package/dist/components/header/MSASettingsMenu.js +0 -11
  256. package/dist/components/header/MSASettingsMenu.js.map +0 -1
  257. package/dist/components/header/ScrollZoomIcons.d.ts +0 -6
  258. package/dist/components/header/ScrollZoomIcons.js +0 -35
  259. package/dist/components/header/ScrollZoomIcons.js.map +0 -1
  260. package/dist/components/header/TreeSettingsMenu.js +0 -11
  261. package/dist/components/header/TreeSettingsMenu.js.map +0 -1
  262. package/dist/components/header/goTo.d.ts +0 -20
  263. package/dist/components/header/goTo.js.map +0 -1
  264. package/dist/components/header/settingsMenuItems.d.ts +0 -5
  265. package/dist/components/header/settingsMenuItems.js +0 -171
  266. package/dist/components/header/settingsMenuItems.js.map +0 -1
  267. package/src/components/header/ColorSchemeMenu.tsx +0 -56
  268. package/src/components/header/FileMenu.tsx +0 -107
  269. package/src/components/header/MSASettingsMenu.tsx +0 -26
  270. package/src/components/header/ScrollZoomIcons.tsx +0 -52
  271. package/src/components/header/TreeSettingsMenu.tsx +0 -30
  272. 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).
@@ -2433,7 +2131,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2433
2131
  * #getter
2434
2132
  */
2435
2133
  get turnedOnTracks() {
2436
- 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));
2437
2135
  },
2438
2136
  /**
2439
2137
  * #getter
@@ -2722,7 +2420,9 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2722
2420
  if (mapping.structure.id !== structureId) {
2723
2421
  continue;
2724
2422
  }
2725
- if (asymId !== undefined && mapping.structure.asymId !== asymId) {
2423
+ if (asymId !== undefined &&
2424
+ mapping.structure.asymId !== undefined &&
2425
+ mapping.structure.asymId !== asymId) {
2726
2426
  continue;
2727
2427
  }
2728
2428
  const segment = mapping.segments.find(seg => position >= seg.structStart && position <= seg.structEnd);
@@ -2739,10 +2439,14 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2739
2439
  .views(self => ({
2740
2440
  /**
2741
2441
  * #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
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
2744
2445
  */
2745
2446
  get treeScaleBar() {
2447
+ if (!self.showTreeScaleBar) {
2448
+ return undefined;
2449
+ }
2746
2450
  return scaleBarLength(self.pxPerBranchLength, self.effectiveTreeAreaWidth - self.marginLeft * 2);
2747
2451
  },
2748
2452
  /**
@@ -2789,8 +2493,38 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2789
2493
  }
2790
2494
  return types;
2791
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
+ },
2792
2510
  get filteredAnnotations() {
2793
- 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';
2794
2528
  },
2795
2529
  get annotationsByRow() {
2796
2530
  const { rowNamesSet } = self;
@@ -2866,6 +2600,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2866
2600
  get blocks2d() {
2867
2601
  return self.blocksY.flatMap(by => self.blocksX.map(bx => [bx, by]));
2868
2602
  },
2603
+ /**
2604
+ * #getter
2605
+ */
2606
+ get wheelZooming() {
2607
+ return self.zoomSnapshot !== undefined;
2608
+ },
2869
2609
  /**
2870
2610
  * #getter
2871
2611
  */
@@ -3062,11 +2802,20 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3062
2802
  transaction(() => {
3063
2803
  const colInView = (-self.scrollX + offsetX) / self.colWidth;
3064
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);
3065
2810
  if (axis !== 'vertical') {
3066
- 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 };
3067
2814
  }
3068
2815
  if (axis !== 'horizontal') {
3069
- 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 };
3070
2819
  }
3071
2820
  self.scrollX = clamp(offsetX - colInView * self.colWidth, self.maxScrollX, 0);
3072
2821
  const anchoredScrollY = offsetY - rowInView * self.rowHeight;
@@ -3077,6 +2826,25 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3077
2826
  self.scrollY = clamp(anchoredScrollY * (1 - topBias), self.maxScrollY, 0);
3078
2827
  });
3079
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
+ },
3080
2848
  /**
3081
2849
  * #action
3082
2850
  */
@@ -3155,8 +2923,23 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3155
2923
  * #action
3156
2924
  */
3157
2925
  toggleTrack(id) {
3158
- const defaultOff = self.MSA?.tracks.find(t => t.id === id)?.defaultOff;
3159
- 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
+ }
3160
2943
  },
3161
2944
  /**
3162
2945
  * #action
@@ -3302,7 +3085,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3302
3085
  */
3303
3086
  get visibleDomainTypes() {
3304
3087
  return this.categoricalDomainTypes
3305
- .filter(d => !self.turnedOffFeatures.get(d.accession))
3088
+ .filter(d => !self.featureOff(d.accession))
3306
3089
  .toSorted((a, b) => a.start - b.start);
3307
3090
  },
3308
3091
  /**
@@ -3362,15 +3145,15 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3362
3145
  /**
3363
3146
  * #getter
3364
3147
  * 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
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
3368
3151
  * the boxes clear of the letters, and with the letters too small to draw
3369
3152
  * the filled box is the only thing left to read.
3370
3153
  */
3371
3154
  get domainUnderline() {
3372
3155
  return (self.actuallyShowDomains &&
3373
- !self.bgColor &&
3156
+ self.cellFill !== 'domain' &&
3374
3157
  !self.subFeatureRows &&
3375
3158
  self.showMsaLetters);
3376
3159
  },
@@ -3819,6 +3602,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3819
3602
  setHideHeader(arg) {
3820
3603
  self.hideHeader = arg;
3821
3604
  },
3605
+ /**
3606
+ * #action
3607
+ */
3608
+ setEmbeddedMode(arg) {
3609
+ self.embeddedMode = arg;
3610
+ },
3822
3611
  /**
3823
3612
  * #action
3824
3613
  */
@@ -3883,15 +3672,16 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3883
3672
  },
3884
3673
  /**
3885
3674
  * #action
3886
- * show or hide an annotation type. Only hidden types are recorded; see
3887
- * `turnedOffFeatures`
3675
+ * show or hide an annotation type. Only a choice that differs from the
3676
+ * type's default is recorded; see `turnedOffFeatures`
3888
3677
  */
3889
3678
  setFilter(accession, shown) {
3890
- if (shown) {
3891
- 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);
3892
3682
  }
3893
3683
  else {
3894
- self.turnedOffFeatures.set(accession, true);
3684
+ self.turnedOffFeatures.delete(accession);
3895
3685
  }
3896
3686
  },
3897
3687
  /**
@@ -3899,20 +3689,23 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3899
3689
  */
3900
3690
  fit() {
3901
3691
  // two passes: each direction's viewport depends on whether the other
3902
- // 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
3903
3696
  transaction(() => {
3904
3697
  for (let pass = 0; pass < 2; pass++) {
3905
- this.fitHorizontally();
3906
- this.fitVertically();
3698
+ this.fitHorizontally(defaultColWidth);
3699
+ this.fitVertically(defaultRowHeight);
3907
3700
  }
3908
3701
  });
3909
3702
  },
3910
3703
  /**
3911
3704
  * #action
3912
3705
  */
3913
- fitVertically() {
3706
+ fitVertically(maxRowHeight = maxCellSize) {
3914
3707
  if (self.numRows > 0) {
3915
- self.rowHeight = clamp(self.msaAreaHeight / self.numRows, minRowHeight, maxCellSize);
3708
+ self.setRowHeight(clamp(self.msaAreaHeight / self.numRows, minRowHeight, maxRowHeight));
3916
3709
  }
3917
3710
  self.scrollY = 0;
3918
3711
  },
@@ -3928,7 +3721,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3928
3721
  return;
3929
3722
  }
3930
3723
  transaction(() => {
3931
- 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));
3932
3725
  self.scrollX = clamp(-span.startCol * self.colWidth, self.maxScrollX, 0);
3933
3726
  });
3934
3727
  },
@@ -3966,7 +3759,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3966
3759
  const { rowRuns } = resolvedSelection;
3967
3760
  const first = rowRuns[0][0];
3968
3761
  const last = rowRuns.at(-1)[1];
3969
- self.rowHeight = clamp(self.msaAreaHeight / (last - first + 1), minRowHeight, maxCellSize);
3762
+ self.setRowHeight(clamp(self.msaAreaHeight / (last - first + 1), minRowHeight, maxCellSize));
3970
3763
  self.scrollY = clamp(-first * self.rowHeight, self.maxScrollY, 0);
3971
3764
  }
3972
3765
  });
@@ -3998,10 +3791,10 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3998
3791
  /**
3999
3792
  * #action
4000
3793
  */
4001
- fitHorizontally() {
3794
+ fitHorizontally(maxColWidth = maxCellSize) {
4002
3795
  if (self.numColumns > 0) {
4003
3796
  // msaCanvasWidth excludes the vertical scrollbar's 20px
4004
- self.colWidth = clamp(self.msaCanvasWidth / self.numColumns, minColWidth, maxCellSize);
3797
+ self.setColWidth(clamp(self.msaCanvasWidth / self.numColumns, minColWidth, maxColWidth));
4005
3798
  }
4006
3799
  self.scrollX = 0;
4007
3800
  },
@@ -4009,17 +3802,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4009
3802
  if (self.highlightColumns?.length) {
4010
3803
  self.setHighlightedColumns(self.highlightColumns);
4011
3804
  }
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)
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()
4023
3812
  ? { clades: getSnapshot(self.clades), treeRoot: self.treeRoot }
4024
3813
  : undefined, seed => {
4025
3814
  if (!seed?.clades.length) {
@@ -4049,7 +3838,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4049
3838
  if (region &&
4050
3839
  self.viewInitialized &&
4051
3840
  self.numColumns > 0 &&
4052
- !(self.treeFilehandle && !self.data.tree)) {
3841
+ !awaitingTree()) {
4053
3842
  this.revealRegion(region);
4054
3843
  self.setRegion(undefined);
4055
3844
  }
@@ -4071,89 +3860,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4071
3860
  mql.removeEventListener('change', onChange);
4072
3861
  });
4073
3862
  }
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({
3863
+ installFilehandleLoader(self, {
4157
3864
  what: 'tree',
4158
3865
  getFilehandle: () => self.treeFilehandle,
4159
3866
  onLoad: text => {
@@ -4167,7 +3874,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4167
3874
  },
4168
3875
  });
4169
3876
  // treeMetadata has no import-form step, so no loading flag or cancel
4170
- loadOnFilehandleChange({
3877
+ installFilehandleLoader(self, {
4171
3878
  what: 'row metadata',
4172
3879
  optional: true,
4173
3880
  getFilehandle: () => self.treeMetadataFilehandle,
@@ -4196,7 +3903,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4196
3903
  }
4197
3904
  }));
4198
3905
  // gffFilehandle loads into data.gff, which the autorun above parses
4199
- loadOnFilehandleChange({
3906
+ installFilehandleLoader(self, {
4200
3907
  what: 'annotations',
4201
3908
  optional: true,
4202
3909
  getFilehandle: () => self.gffFilehandle,
@@ -4207,7 +3914,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4207
3914
  self.setGFFFilehandle(undefined);
4208
3915
  },
4209
3916
  });
4210
- loadOnFilehandleChange({
3917
+ installFilehandleLoader(self, {
4211
3918
  what: 'alignment',
4212
3919
  getFilehandle: () => self.msaFilehandle,
4213
3920
  onLoad: text => {
@@ -4228,20 +3935,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4228
3935
  // xref problem https://github.com/GMOD/react-msaview/issues/75
4229
3936
  addDisposer(self, autorun(() => {
4230
3937
  if (self.dynamicColorSchemeName) {
4231
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4232
- self.colStats;
3938
+ readComputed(self.colStats);
4233
3939
  }
4234
3940
  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;
3941
+ readComputed(self.colStats, self.conservation, self.propertyConservation);
4242
3942
  }
4243
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4244
- self.columns;
3943
+ readComputed(self.columns);
4245
3944
  }));
4246
3945
  },
4247
3946
  }))