react-msaview 8.4.1 → 9.1.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 (193) hide show
  1. package/README.md +19 -10
  2. package/bundle/index.js +74 -74
  3. package/bundle/index.js.map +4 -4
  4. package/dist/colorSchemes.js +1 -3
  5. package/dist/colorSchemes.js.map +1 -1
  6. package/dist/columnCounts.d.ts +0 -1
  7. package/dist/columnCounts.js +1 -1
  8. package/dist/columnCounts.js.map +1 -1
  9. package/dist/components/Loading.js +5 -5
  10. package/dist/components/Loading.js.map +1 -1
  11. package/dist/components/MSAView.js +2 -2
  12. package/dist/components/MSAView.js.map +1 -1
  13. package/dist/components/MSAViewer.d.ts +1892 -7
  14. package/dist/components/MSAViewer.js +144 -202
  15. package/dist/components/MSAViewer.js.map +1 -1
  16. package/dist/components/ResizeHandles.js +4 -3
  17. package/dist/components/ResizeHandles.js.map +1 -1
  18. package/dist/components/Track.js +3 -2
  19. package/dist/components/Track.js.map +1 -1
  20. package/dist/components/controls.d.ts +11 -0
  21. package/dist/components/controls.js +48 -0
  22. package/dist/components/controls.js.map +1 -0
  23. package/dist/components/dialogs/AnnotationFileDialog.js +5 -16
  24. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
  25. package/dist/components/dialogs/ExportSVGDialog.js +5 -7
  26. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  27. package/dist/components/dialogs/HelpDialog.d.ts +6 -0
  28. package/dist/components/dialogs/HelpDialog.js +30 -0
  29. package/dist/components/dialogs/HelpDialog.js.map +1 -0
  30. package/dist/components/header/FileMenu.js +33 -7
  31. package/dist/components/header/FileMenu.js.map +1 -1
  32. package/dist/components/header/Header.js +4 -6
  33. package/dist/components/header/Header.js.map +1 -1
  34. package/dist/components/header/TreeSettingsMenu.d.ts +1 -1
  35. package/dist/components/header/TreeSettingsMenu.js +1 -32
  36. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  37. package/dist/components/header/ZoomControls.js +28 -44
  38. package/dist/components/header/ZoomControls.js.map +1 -1
  39. package/dist/components/header/settingsMenuItems.d.ts +1 -0
  40. package/dist/components/header/settingsMenuItems.js +48 -1
  41. package/dist/components/header/settingsMenuItems.js.map +1 -1
  42. package/dist/components/import/data/seq2.d.ts +0 -1
  43. package/dist/components/import/data/seq2.js +0 -17
  44. package/dist/components/import/data/seq2.js.map +1 -1
  45. package/dist/components/msa/MSACanvas.js +2 -1
  46. package/dist/components/msa/MSACanvas.js.map +1 -1
  47. package/dist/components/msa/MSACanvasBlock.js +2 -5
  48. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  49. package/dist/components/msa/drawFeatureSpans.d.ts +50 -11
  50. package/dist/components/msa/drawFeatureSpans.js +99 -56
  51. package/dist/components/msa/drawFeatureSpans.js.map +1 -1
  52. package/dist/components/msa/msaRaster.d.ts +14 -1
  53. package/dist/components/msa/msaRaster.js +19 -9
  54. package/dist/components/msa/msaRaster.js.map +1 -1
  55. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +9 -0
  56. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +19 -8
  57. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  58. package/dist/components/msa/renderHighlights.d.ts +2 -2
  59. package/dist/components/msa/renderHighlights.js +2 -3
  60. package/dist/components/msa/renderHighlights.js.map +1 -1
  61. package/dist/components/msa/renderMSAMouseover.js +2 -1
  62. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  63. package/dist/components/renderCtx.d.ts +1 -1
  64. package/dist/components/rowpanels/renderFeaturePanel.d.ts +5 -1
  65. package/dist/components/rowpanels/renderFeaturePanel.js +12 -5
  66. package/dist/components/rowpanels/renderFeaturePanel.js.map +1 -1
  67. package/dist/components/tracks/drawTracks.d.ts +1 -53
  68. package/dist/components/tracks/drawTracks.js +5 -5
  69. package/dist/components/tracks/drawTracks.js.map +1 -1
  70. package/dist/components/tree/CladeLabels.d.ts +1 -1
  71. package/dist/components/tree/CladeLabels.js +22 -34
  72. package/dist/components/tree/CladeLabels.js.map +1 -1
  73. package/dist/components/tree/TreeCanvas.js +4 -4
  74. package/dist/components/tree/TreeCanvas.js.map +1 -1
  75. package/dist/components/tree/TreeCanvasBlock.js +4 -4
  76. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  77. package/dist/components/tree/TreeOverview.js +10 -5
  78. package/dist/components/tree/TreeOverview.js.map +1 -1
  79. package/dist/components/tree/TreePanel.js +6 -2
  80. package/dist/components/tree/TreePanel.js.map +1 -1
  81. package/dist/components/tree/TreeRuler.js +2 -2
  82. package/dist/components/tree/TreeRuler.js.map +1 -1
  83. package/dist/components/tree/cladeBrackets.d.ts +22 -17
  84. package/dist/components/tree/cladeBrackets.js +40 -9
  85. package/dist/components/tree/cladeBrackets.js.map +1 -1
  86. package/dist/components/tree/clickMap.d.ts +2 -0
  87. package/dist/components/tree/clickMap.js.map +1 -1
  88. package/dist/components/tree/ellipsize.d.ts +8 -0
  89. package/dist/components/tree/ellipsize.js +20 -0
  90. package/dist/components/tree/ellipsize.js.map +1 -0
  91. package/dist/components/tree/renderTreeCanvas.js +30 -32
  92. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  93. package/dist/components/tree/renderTreeMouseover.js +1 -1
  94. package/dist/components/tree/renderTreeMouseover.js.map +1 -1
  95. package/dist/components/tree/renderTreeOverview.js +4 -11
  96. package/dist/components/tree/renderTreeOverview.js.map +1 -1
  97. package/dist/constants.d.ts +6 -3
  98. package/dist/constants.js +16 -6
  99. package/dist/constants.js.map +1 -1
  100. package/dist/element.js +3 -6
  101. package/dist/element.js.map +1 -1
  102. package/dist/exportLayout.d.ts +39 -0
  103. package/dist/exportLayout.js +78 -0
  104. package/dist/exportLayout.js.map +1 -0
  105. package/dist/headlessRenderEnv.d.ts +12 -39
  106. package/dist/headlessRenderEnv.js +18 -12
  107. package/dist/headlessRenderEnv.js.map +1 -1
  108. package/dist/hierarchy.d.ts +5 -8
  109. package/dist/hierarchy.js +5 -79
  110. package/dist/hierarchy.js.map +1 -1
  111. package/dist/index.d.ts +3 -1
  112. package/dist/index.js +4 -1
  113. package/dist/index.js.map +1 -1
  114. package/dist/measureTextCanvas.d.ts +7 -0
  115. package/dist/measureTextCanvas.js +10 -8
  116. package/dist/measureTextCanvas.js.map +1 -1
  117. package/dist/model/treeModel.d.ts +14 -25
  118. package/dist/model/treeModel.js +16 -30
  119. package/dist/model/treeModel.js.map +1 -1
  120. package/dist/model.d.ts +108 -79
  121. package/dist/model.js +258 -209
  122. package/dist/model.js.map +1 -1
  123. package/dist/renderToSvg.d.ts +2 -7
  124. package/dist/renderToSvg.js +49 -135
  125. package/dist/renderToSvg.js.map +1 -1
  126. package/dist/svgTestUtil.d.ts +48 -48
  127. package/dist/tipLabels.d.ts +8 -0
  128. package/dist/tipLabels.js +46 -0
  129. package/dist/tipLabels.js.map +1 -0
  130. package/dist/types.d.ts +0 -1
  131. package/dist/version.d.ts +1 -1
  132. package/dist/version.js +1 -1
  133. package/dist/viewerSnapshot.d.ts +169 -0
  134. package/dist/viewerSnapshot.js +100 -0
  135. package/dist/viewerSnapshot.js.map +1 -0
  136. package/package.json +3 -3
  137. package/src/colorSchemes.ts +1 -4
  138. package/src/columnCounts.ts +1 -1
  139. package/src/components/Loading.tsx +19 -12
  140. package/src/components/MSAView.tsx +2 -2
  141. package/src/components/MSAViewer.tsx +276 -256
  142. package/src/components/ResizeHandles.tsx +10 -3
  143. package/src/components/Track.tsx +3 -2
  144. package/src/components/controls.ts +61 -0
  145. package/src/components/dialogs/AnnotationFileDialog.tsx +5 -21
  146. package/src/components/dialogs/ExportSVGDialog.tsx +8 -19
  147. package/src/components/dialogs/HelpDialog.tsx +79 -0
  148. package/src/components/header/FileMenu.tsx +33 -7
  149. package/src/components/header/Header.tsx +4 -6
  150. package/src/components/header/TreeSettingsMenu.tsx +2 -32
  151. package/src/components/header/ZoomControls.tsx +30 -66
  152. package/src/components/header/settingsMenuItems.ts +67 -0
  153. package/src/components/import/data/seq2.ts +0 -18
  154. package/src/components/msa/MSACanvas.tsx +2 -1
  155. package/src/components/msa/MSACanvasBlock.tsx +2 -6
  156. package/src/components/msa/drawFeatureSpans.ts +160 -79
  157. package/src/components/msa/msaRaster.ts +27 -9
  158. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +39 -24
  159. package/src/components/msa/renderHighlights.ts +2 -3
  160. package/src/components/msa/renderMSAMouseover.ts +5 -1
  161. package/src/components/renderCtx.ts +0 -1
  162. package/src/components/rowpanels/renderFeaturePanel.ts +26 -7
  163. package/src/components/tracks/drawTracks.ts +5 -5
  164. package/src/components/tree/CladeLabels.tsx +30 -54
  165. package/src/components/tree/TreeCanvas.tsx +10 -4
  166. package/src/components/tree/TreeCanvasBlock.tsx +4 -4
  167. package/src/components/tree/TreeOverview.tsx +10 -5
  168. package/src/components/tree/TreePanel.tsx +8 -2
  169. package/src/components/tree/TreeRuler.tsx +7 -3
  170. package/src/components/tree/cladeBrackets.ts +59 -19
  171. package/src/components/tree/clickMap.ts +2 -0
  172. package/src/components/tree/ellipsize.ts +24 -0
  173. package/src/components/tree/renderTreeCanvas.ts +37 -36
  174. package/src/components/tree/renderTreeMouseover.ts +1 -1
  175. package/src/components/tree/renderTreeOverview.ts +7 -14
  176. package/src/constants.ts +17 -6
  177. package/src/element.ts +3 -7
  178. package/src/exportLayout.ts +134 -0
  179. package/src/headlessRenderEnv.ts +24 -11
  180. package/src/hierarchy.ts +5 -88
  181. package/src/index.ts +5 -5
  182. package/src/measureTextCanvas.ts +10 -8
  183. package/src/model/treeModel.ts +17 -34
  184. package/src/model.ts +296 -243
  185. package/src/renderToSvg.tsx +72 -209
  186. package/src/tipLabels.ts +51 -0
  187. package/src/types.ts +0 -1
  188. package/src/version.ts +1 -1
  189. package/src/viewerSnapshot.ts +147 -0
  190. package/dist/components/header/ZoomMenu.d.ts +0 -6
  191. package/dist/components/header/ZoomMenu.js +0 -37
  192. package/dist/components/header/ZoomMenu.js.map +0 -1
  193. package/src/components/header/ZoomMenu.tsx +0 -48
package/src/model.ts CHANGED
@@ -19,7 +19,6 @@ import {
19
19
  import {
20
20
  generateNodeIds,
21
21
  gffToAnnotations,
22
- interProScanToAnnotations,
23
22
  parseEmfTree,
24
23
  parseGFF,
25
24
  parseMSA,
@@ -34,7 +33,6 @@ import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.ts'
34
33
  import { columnStats } from './columnStats.ts'
35
34
  import { packDomainLanes } from './components/msa/packDomainLanes.ts'
36
35
  import { visibleColRange } from './components/msa/visibleColRange.ts'
37
- import TrackBlocks from './components/tracks/TrackBlocks.tsx'
38
36
  import { cladeGutterWidth } from './components/tree/cladeBrackets.ts'
39
37
  import { scaleBarLength } from './components/tree/scaleBar.ts'
40
38
  import {
@@ -56,15 +54,19 @@ import {
56
54
  defaultShowDomainLegend,
57
55
  defaultShowDomains,
58
56
  defaultSubFeatureRows,
59
- defaultTreeWidth,
60
- labelReferenceFontSize,
57
+ fontSizeFor,
58
+ labelFitFontSize,
61
59
  maxCellSize,
60
+ maxFitTreeWidth,
61
+ maxTreeAreaShare,
62
62
  maxInlineSnapshotBytes as defaultMaxInlineSnapshotBytes,
63
63
  maxNeighborJoiningRows,
64
64
  minColWidth,
65
65
  minLetterColWidth,
66
66
  minLetterRowHeight,
67
67
  minRowHeight,
68
+ minTreeWidth,
69
+ pxPerTreeDepth,
68
70
  rowPanelHeaderHeight,
69
71
  rowTintAlpha,
70
72
  scrollZoomAxes,
@@ -79,8 +81,6 @@ import { featuresToAnnotations } from './featuresLayer.ts'
79
81
  import { fetchTextWithProgress, isAbortError } from './fetchUtils.ts'
80
82
  import { flatToTree } from './flatToTree.ts'
81
83
  import {
82
- calcDepthToLeaf,
83
- clusterLayout,
84
84
  collapse,
85
85
  find,
86
86
  forEachDescendant,
@@ -88,14 +88,11 @@ import {
88
88
  layoutTree,
89
89
  leafIndex,
90
90
  leaves,
91
- maxLength,
92
91
  mrca,
93
92
  nodeCoveringRows,
94
- setBrLength,
95
93
  sort,
96
94
  sum as hierarchySum,
97
95
  } from './hierarchy.ts'
98
- import { measureTextCanvas } from './measureTextCanvas.ts'
99
96
  import { DataModelF } from './model/DataModel.ts'
100
97
  import { DialogQueueSessionMixin } from './model/DialogQueue.ts'
101
98
  import { MSAModelF } from './model/msaModel.ts'
@@ -114,6 +111,7 @@ import { resolveScale } from './scales.ts'
114
111
  import { buildSeqPosIndex } from './seqPosToGlobalCol.ts'
115
112
  import { maxBitsFor } from './sequenceLogo.ts'
116
113
  import { stripDefault } from './stripDefault.ts'
114
+ import { treeTips, widestLabel } from './tipLabels.ts'
117
115
  import {
118
116
  computeRowInsertions,
119
117
  dropBlanks,
@@ -175,7 +173,6 @@ import type {
175
173
  import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
176
174
  import type { Instance } from '@jbrowse/mobx-state-tree'
177
175
  import type { IComputedValue } from 'mobx'
178
- import type { InterProScanResults } from 'msa-parsers'
179
176
 
180
177
  function parseTreeText(text: string) {
181
178
  if (text.startsWith('BioTreeContainer')) {
@@ -528,7 +525,6 @@ export const preservedOnReset = new Set([
528
525
  'drawNodeLabels',
529
526
  'showTreeOverview',
530
527
  'overviewHeight',
531
- 'autoTreeAreaWidth',
532
528
  'turnedOffTracks',
533
529
  'trackHeights',
534
530
  'hideGaps',
@@ -556,40 +552,80 @@ const seedingMarks = new Set<CladeMark>(['collapse', 'focus', 'rotate'])
556
552
  const noDomains: Annotation[] = []
557
553
  const noClades: ResolvedClade[] = []
558
554
 
555
+ /**
556
+ * Each tip of the full tree in the order the tree draws it, with the on-screen
557
+ * row that shows it: its own, or the row of the collapsed node standing for it
558
+ */
559
+ function tipRows(root: HierarchyNode<NodeWithIds>) {
560
+ const rows = new Map<string, { order: number; row: number }>()
561
+ let row = 0
562
+ const pushReversed = (
563
+ stack: HierarchyNode<NodeWithIds>[],
564
+ nodes: HierarchyNode<NodeWithIds>[],
565
+ ) => {
566
+ for (let i = nodes.length - 1; i >= 0; i--) {
567
+ stack.push(nodes[i]!)
568
+ }
569
+ }
570
+ const stack = [root]
571
+ while (stack.length > 0) {
572
+ const node = stack.pop()!
573
+ if (node.children?.length) {
574
+ pushReversed(stack, node.children)
575
+ } else {
576
+ const hidden = [node]
577
+ while (hidden.length > 0) {
578
+ const tip = hidden.pop()!
579
+ const kids = tip.children ?? tip._children
580
+ if (kids?.length) {
581
+ pushReversed(hidden, kids)
582
+ } else {
583
+ rows.set(tip.data.name, { order: rows.size, row })
584
+ }
585
+ }
586
+ row++
587
+ }
588
+ }
589
+ return rows
590
+ }
591
+
559
592
  /**
560
593
  * The rows a clade covers and the node an `mrca` names, or undefined when the
561
594
  * clade does not resolve: a tip name the tree does not have or has twice, or a
562
- * leaf count `tips` disagrees with. `index` and `rowNamesSet` are the memoized
563
- * passes over the tree.
595
+ * leaf count `tips` disagrees with. A `range` counts its tips in the order
596
+ * `tips` gives them, so a collapsed clade inside it counts every leaf it
597
+ * stands for. `index` is the memoized leaf pass over the tree.
564
598
  */
565
599
  function cladeRows(
566
600
  clade: Clade,
567
601
  root: HierarchyNode<NodeWithIds>,
568
602
  index: Map<string, HierarchyNode<NodeWithIds> | undefined>,
569
- rowNamesSet: Map<string, number>,
603
+ tips: Map<string, { order: number; row: number }>,
570
604
  ): { rows: [number, number]; nodeId?: string } | undefined {
571
605
  if (clade.range) {
572
606
  const [a, b] = clade.range.map(name =>
573
- index.get(name) ? rowNamesSet.get(name) : undefined,
607
+ index.get(name) ? tips.get(name) : undefined,
574
608
  )
575
609
  if (a === undefined || b === undefined) {
576
610
  return undefined
577
611
  }
578
- const rows: [number, number] = a <= b ? [a, b] : [b, a]
579
- return rows[1] - rows[0] + 1 === clade.tips ? { rows } : undefined
612
+ const [first, last] = a.order <= b.order ? [a, b] : [b, a]
613
+ return last.order - first.order + 1 === clade.tips
614
+ ? { rows: [first.row, last.row] }
615
+ : undefined
580
616
  }
581
617
  const node = clade.mrca ? mrca(root, clade.mrca, index) : undefined
582
618
  if (!node) {
583
619
  return undefined
584
620
  }
585
- const tips = leaves(node)
586
- if (tips.length !== clade.tips) {
621
+ const nodeTips = leaves(node)
622
+ if (nodeTips.length !== clade.tips) {
587
623
  return undefined
588
624
  }
589
625
  let first = Infinity
590
626
  let last = -Infinity
591
- for (const tip of tips) {
592
- const row = rowNamesSet.get(tip.data.name)
627
+ for (const tip of nodeTips) {
628
+ const row = tips.get(tip.data.name)?.row
593
629
  if (row !== undefined) {
594
630
  first = Math.min(first, row)
595
631
  last = Math.max(last, row)
@@ -601,19 +637,21 @@ function cladeRows(
601
637
  }
602
638
 
603
639
  /**
604
- * Every clade that resolves, with its fill color settled, against the leaf
605
- * order `rowNamesSet` gives. The tree panel resolves against the displayed
606
- * rows and the overview against its own, so both take the tree they draw. A
607
- * `range` record names no node, so the seeding marks, which need one, drop it.
640
+ * Every clade that resolves, with its fill color settled, against the rows of
641
+ * `displayed`, the tree as drawn. The tree panel resolves against the
642
+ * displayed rows and the overview against its own, so both take the tree they
643
+ * draw. A `range` record names no node, so the seeding marks, which need one,
644
+ * drop it.
608
645
  */
609
646
  function resolveClades(
610
647
  clades: Clade[],
611
648
  root: HierarchyNode<NodeWithIds>,
612
- rowNamesSet: Map<string, number>,
649
+ displayed: HierarchyNode<NodeWithIds>,
613
650
  ): ResolvedClade[] {
614
651
  const index = leafIndex(root)
652
+ const tips = tipRows(displayed)
615
653
  return clades.flatMap(clade => {
616
- const resolved = cladeRows(clade, root, index, rowNamesSet)
654
+ const resolved = cladeRows(clade, root, index, tips)
617
655
  const seeding = seedingMarks.has(clade.mark)
618
656
  return resolved && !(seeding && resolved.nodeId === undefined)
619
657
  ? [
@@ -747,8 +785,8 @@ function rootTree(tree: NodeWithIds, treeRoot: TreeRoot) {
747
785
  : tree
748
786
  }
749
787
 
750
- // the inclusive tip indices a node covers, from the row-space extent
751
- // clusterLayout writes to xMin/xMax as tip centers
788
+ // the inclusive tip indices a node covers, from its xMin/xMax in a layout one
789
+ // row per tip
752
790
  function tipRange(node: HierarchyNode): [number, number] {
753
791
  return [Math.round(node.xMin! - 0.5), Math.round(node.xMax! - 0.5)]
754
792
  }
@@ -762,6 +800,16 @@ const featureChannels = new Set<EncodingChannel>([
762
800
  // seqPos -> column indexes per row, keyed on the parse so they are garbage
763
801
  // collected with it. A computed would rebuild every row's index when read
764
802
  // outside a reactive context.
803
+ // Newick tools write a colon in a name as an underscore, so a tree's row
804
+ // `chr1_100-200` is the GFF's `chr1:100-200`, as FastaMSA.getRow resolves it
805
+ function annotationRowName(id: string, rowNames: Map<string, number>) {
806
+ if (rowNames.has(id)) {
807
+ return id
808
+ }
809
+ const underscored = id.replaceAll(':', '_')
810
+ return rowNames.has(underscored) ? underscored : id
811
+ }
812
+
765
813
  const seqPosIndexCache = new WeakMap<object, Map<string, Int32Array>>()
766
814
 
767
815
  // A segment is a 1:1 run. A segment whose sides differ in length is malformed,
@@ -1093,9 +1141,10 @@ function stateModelFactory({
1093
1141
  /**
1094
1142
  * #property
1095
1143
  * where the view opens, in `highlights` coordinates: `{row, start,
1096
- * end}` zooms onto residues of that row, `{start, end}` onto alignment
1097
- * columns. Applied once the alignment and any tree file have loaded,
1098
- * then cleared, so a reloaded session keeps the reader's own scroll.
1144
+ * end}` zooms onto residues of that row and scrolls the row to the
1145
+ * middle, `{start, end}` zooms onto alignment columns. Applied once the
1146
+ * alignment and any tree file have loaded, then cleared, so a reloaded
1147
+ * session keeps the reader's own scroll.
1099
1148
  */
1100
1149
  region: types.frozen<Region | undefined>(),
1101
1150
  /**
@@ -1137,6 +1186,12 @@ function stateModelFactory({
1137
1186
  * of the snapshot so a link opened in the full app shows the toolbar.
1138
1187
  */
1139
1188
  hideHeader: false,
1189
+ /**
1190
+ * #volatile
1191
+ * shows the import form while the view has no data. MSAViewer turns it
1192
+ * off, since its host supplies the data and the form is the demo app's
1193
+ */
1194
+ importForm: true,
1140
1195
  /**
1141
1196
  * #volatile
1142
1197
  */
@@ -1648,20 +1703,18 @@ function stateModelFactory({
1648
1703
  .views(self => ({
1649
1704
  /**
1650
1705
  * #getter
1651
- * hideGaps takes effect when there are collapsed rows or allowedGappyness < 100
1706
+ * hideGaps takes effect when a collapse or a focus hides rows, whose
1707
+ * residues leave columns all gaps among the rest, or when
1708
+ * allowedGappyness is under 100
1652
1709
  */
1653
1710
  get hideGapsEffective() {
1654
1711
  return (
1655
1712
  self.hideGaps &&
1656
- (self.collapsed.length > 0 || self.allowedGappyness < 100)
1713
+ (self.collapsed.length > 0 ||
1714
+ self.showOnly !== undefined ||
1715
+ self.allowedGappyness < 100)
1657
1716
  )
1658
1717
  },
1659
- /**
1660
- * #getter
1661
- */
1662
- get realAllowedGappyness() {
1663
- return this.hideGapsEffective ? self.allowedGappyness : 100
1664
- },
1665
1718
  /**
1666
1719
  * #getter
1667
1720
  * the annotations the overlay, the legend and the encodings draw: the
@@ -1804,10 +1857,10 @@ function stateModelFactory({
1804
1857
  * the row table: extra fields per row, keyed by row name, which the
1805
1858
  * `encodings` channels read. It is stored as the JSON string
1806
1859
  * `data.treeMetadata`, the name that travels in existing links, so the
1807
- * inline size limit and `treeMetadataFilehandle` cover it. labelWidthMap
1808
- * reads it on every layout, so a malformed user-supplied file returns {}
1809
- * instead of throwing out of rendering, and every field is a string by
1810
- * the time a scale sorts it.
1860
+ * inline size limit and `treeMetadataFilehandle` cover it. The tree area
1861
+ * fit reads it on every layout, so a malformed user-supplied file
1862
+ * returns {} instead of throwing out of rendering, and every field is a
1863
+ * string by the time a scale sorts it.
1811
1864
  */
1812
1865
  get rowData(): Record<string, Record<string, string> | undefined> {
1813
1866
  const text = self.data.treeMetadata
@@ -1928,7 +1981,7 @@ function stateModelFactory({
1928
1981
  return resolveClades(
1929
1982
  self.clades,
1930
1983
  hierarchy(this.tree, d => d.children),
1931
- this.rowNamesSet,
1984
+ this.root,
1932
1985
  )
1933
1986
  },
1934
1987
  /**
@@ -1938,11 +1991,107 @@ function stateModelFactory({
1938
1991
  * where no clade draws a bar or a label.
1939
1992
  */
1940
1993
  get cladeGutterWidth() {
1941
- return cladeGutterWidth({
1942
- clades: this.resolvedClades,
1943
- rowHeight: self.rowHeight,
1944
- fontSize: this.fontSize,
1945
- })
1994
+ return cladeGutterWidth(self.clades)
1995
+ },
1996
+ /**
1997
+ * #getter
1998
+ * whether a tree draws beside the labels: false for a file with no
1999
+ * tree, and with drawTree off
2000
+ */
2001
+ get treeDrawn() {
2002
+ return self.drawTree && !this.noTree
2003
+ },
2004
+ /**
2005
+ * #getter
2006
+ * the leaf names of the whole tree and its depth in levels. The fit
2007
+ * reads these, so a collapse or a focus leaves the tree area as it is
2008
+ */
2009
+ get treeTips() {
2010
+ return treeTips(this.tree)
2011
+ },
2012
+ /**
2013
+ * #getter
2014
+ * the widest tip label at labelFitFontSize, measured whether or not a
2015
+ * zoom hides the labels
2016
+ */
2017
+ get maxTipLabelWidth() {
2018
+ return widestLabel(
2019
+ this.treeTips.names.map(name => this.rowDataOf(name)?.genome || name),
2020
+ labelFitFontSize,
2021
+ )
2022
+ },
2023
+ /**
2024
+ * #getter
2025
+ * the px the tip labels take across the tree area: the widest label at
2026
+ * labelFitFontSize and the gaps either side of it, or 0 with drawLabels
2027
+ * off
2028
+ */
2029
+ get labelColumnWidth() {
2030
+ return self.drawLabels
2031
+ ? this.maxTipLabelWidth + (this.treeDrawn ? 10 : 6)
2032
+ : 0
2033
+ },
2034
+ /**
2035
+ * #getter
2036
+ * the tree area width while treeAreaWidth is undefined: the label column
2037
+ * beside a tree that widens with its depth, up to maxTreeAreaShare of the
2038
+ * view, or the room the row panels leave where there is no alignment.
2039
+ * Where that binds, the tree keeps minTreeWidth and the renderer
2040
+ * ellipsizes the labels
2041
+ */
2042
+ get fittedTreeAreaWidth() {
2043
+ const chrome = self.marginLeft + this.cladeGutterWidth
2044
+ const tree = this.treeDrawn
2045
+ ? (self.treeWidth ??
2046
+ clamp(
2047
+ this.treeTips.depth * pxPerTreeDepth,
2048
+ minTreeWidth,
2049
+ maxFitTreeWidth,
2050
+ ))
2051
+ : 0
2052
+ const fit = chrome + tree + this.labelColumnWidth
2053
+ const room = self.viewInitialized
2054
+ ? this.numColumns > 0
2055
+ ? self.width * maxTreeAreaShare
2056
+ : self.width - this.rowPanelsWidth - self.resizeHandleWidth
2057
+ : Infinity
2058
+ const cap = Math.max(room, chrome + Math.min(tree, minTreeWidth))
2059
+ return Math.ceil(Math.min(fit, cap))
2060
+ },
2061
+ /**
2062
+ * #getter
2063
+ * the width of the area the tree and its labels draw in, px:
2064
+ * treeAreaWidth where set, the fit otherwise
2065
+ */
2066
+ get effectiveTreeAreaWidth() {
2067
+ return self.treeAreaWidth ?? this.fittedTreeAreaWidth
2068
+ },
2069
+ /**
2070
+ * #getter
2071
+ * the width of the tree, px: treeWidth where set, otherwise the tree
2072
+ * area less the label column, which a zoom hiding the labels gives to
2073
+ * the tree
2074
+ */
2075
+ get effectiveTreeWidth() {
2076
+ const labels = this.showTreeText ? this.labelColumnWidth : 0
2077
+ return (
2078
+ self.treeWidth ??
2079
+ Math.max(
2080
+ minTreeWidth,
2081
+ Math.floor(
2082
+ this.effectiveTreeAreaWidth -
2083
+ self.marginLeft -
2084
+ this.cladeGutterWidth -
2085
+ labels,
2086
+ ),
2087
+ )
2088
+ )
2089
+ },
2090
+ /**
2091
+ * #getter
2092
+ */
2093
+ get showTreeText() {
2094
+ return self.drawLabels && self.rowHeight >= minLetterRowHeight
1946
2095
  },
1947
2096
  /**
1948
2097
  * #getter
@@ -2021,21 +2170,19 @@ function stateModelFactory({
2021
2170
  if (!self.showTreeOverview) {
2022
2171
  return undefined
2023
2172
  }
2024
- const root = buildTreeRoot(this.tree, {
2025
- collapsed: self.collapsed,
2026
- rotated: self.rotated,
2027
- order: self.treeOrder,
2028
- })
2029
- const numTips = leaves(root).length
2030
- clusterLayout(root, numTips, 1)
2031
- const rootLen = Math.max(root.data.length || 0, 0)
2032
- const extent = maxLength(root) - rootLen
2033
- setBrLength(root, -rootLen, extent ? 1 / extent : 0)
2173
+ const layout = layoutTree(
2174
+ buildTreeRoot(this.tree, {
2175
+ collapsed: self.collapsed,
2176
+ rotated: self.rotated,
2177
+ order: self.treeOrder,
2178
+ }),
2179
+ { rowHeight: 1, treeWidth: 1 },
2180
+ )
2034
2181
  return {
2035
- root,
2036
- numTips,
2037
- maxDepthToLeaf: calcDepthToLeaf(root),
2038
- showBranchLen: self.showBranchLen && extent > 0,
2182
+ root: layout.root,
2183
+ numTips: layout.leaves.length,
2184
+ maxDepthToLeaf: layout.root.depthToLeaf,
2185
+ showBranchLen: self.showBranchLen && layout.rootToTipLength > 0,
2039
2186
  }
2040
2187
  },
2041
2188
 
@@ -2049,10 +2196,11 @@ function stateModelFactory({
2049
2196
  if (!layout || self.clades.length === 0) {
2050
2197
  return noClades
2051
2198
  }
2052
- const rowNames = new Map(
2053
- leaves(layout.root).map((leaf, index) => [leaf.data.name, index]),
2199
+ return resolveClades(
2200
+ self.clades,
2201
+ hierarchy(this.tree, d => d.children),
2202
+ layout.root,
2054
2203
  )
2055
- return resolveClades(self.clades, layout.root, rowNames)
2056
2204
  },
2057
2205
 
2058
2206
  /**
@@ -2125,7 +2273,7 @@ function stateModelFactory({
2125
2273
  get msaAreaWidth() {
2126
2274
  return (
2127
2275
  self.width -
2128
- self.treeAreaWidth -
2276
+ this.effectiveTreeAreaWidth -
2129
2277
  this.rowPanelsWidth -
2130
2278
  self.resizeHandleWidth
2131
2279
  )
@@ -2137,13 +2285,15 @@ function stateModelFactory({
2137
2285
  * margin and the bracket gutter
2138
2286
  */
2139
2287
  get treeAreaWidthMinusMargin() {
2140
- return self.treeAreaWidth - self.marginLeft - this.cladeGutterWidth
2288
+ return (
2289
+ this.effectiveTreeAreaWidth - self.marginLeft - this.cladeGutterWidth
2290
+ )
2141
2291
  },
2142
2292
  /**
2143
2293
  * #getter
2144
2294
  */
2145
2295
  get blanks() {
2146
- const { hideGapsEffective, realAllowedGappyness } = self
2296
+ const { hideGapsEffective, allowedGappyness } = self
2147
2297
  if (!hideGapsEffective) {
2148
2298
  return []
2149
2299
  }
@@ -2159,7 +2309,7 @@ function stateModelFactory({
2159
2309
  // row that stops early counts as gapped for the remainder
2160
2310
  const numCols = this.MSA?.getWidth() ?? 0
2161
2311
  const numRows = strs.length
2162
- const threshold = Math.ceil((realAllowedGappyness / 100) * numRows)
2312
+ const threshold = Math.ceil((allowedGappyness / 100) * numRows)
2163
2313
  const blankCounts = new Uint32Array(numCols)
2164
2314
  for (let j = 0; j < numRows; j++) {
2165
2315
  const str = strs[j]!
@@ -2243,11 +2393,19 @@ function stateModelFactory({
2243
2393
  return index
2244
2394
  },
2245
2395
 
2396
+ /**
2397
+ * #method
2398
+ * the row's aligned sequence by the name the tree gives it, including
2399
+ * rows a collapsed clade hides. `MSA.getRow` resolves a FASTA name whose
2400
+ * colon a Newick tool wrote as an underscore, which `rowMap` does not
2401
+ */
2402
+ rowSequence(name: string) {
2403
+ return this.MSA?.getRow(name) || undefined
2404
+ },
2246
2405
  /**
2247
2406
  * #getter
2248
- * every sequence in the alignment, keyed by row name, including rows a
2249
- * collapsed clade hides. `rows` holds only the rows on screen; lookups
2250
- * by row name use this
2407
+ * every sequence in the alignment, keyed by the alignment's own row
2408
+ * names, including rows a collapsed clade hides
2251
2409
  */
2252
2410
  get rowMap() {
2253
2411
  const MSA = this.MSA
@@ -2282,7 +2440,7 @@ function stateModelFactory({
2282
2440
  * #getter
2283
2441
  */
2284
2442
  get fontSize() {
2285
- return Math.min(Math.max(6, self.rowHeight - 3), 18)
2443
+ return fontSizeFor(self.rowHeight)
2286
2444
  },
2287
2445
  /**
2288
2446
  * #getter
@@ -2298,10 +2456,11 @@ function stateModelFactory({
2298
2456
  */
2299
2457
  get sequenceType(): 'dna' | 'rna' | 'amino' {
2300
2458
  const letters = this.colStats.lettersPresent
2301
- // isDna already excludes U (not in the DNA set) and isRna excludes T,
2302
- // so the set membership alone disambiguates the two
2303
- const dna = new Set(['A', 'C', 'G', 'T', 'N'])
2304
- const rna = new Set(['A', 'C', 'G', 'U', 'N'])
2459
+ // the two-base IUPAC codes keep an ambiguous read nucleotide; the
2460
+ // three-base ones (B, D, H, V) are common amino acids, so they don't
2461
+ const ambiguous = ['N', 'R', 'Y', 'K', 'M', 'S', 'W']
2462
+ const dna = new Set(['A', 'C', 'G', 'T', ...ambiguous])
2463
+ const rna = new Set(['A', 'C', 'G', 'U', ...ambiguous])
2305
2464
  const isDna = letters.size > 0 && [...letters].every(l => dna.has(l))
2306
2465
  const isRna = letters.size > 0 && [...letters].every(l => rna.has(l))
2307
2466
  return isDna ? 'dna' : isRna ? 'rna' : 'amino'
@@ -2388,17 +2547,18 @@ function stateModelFactory({
2388
2547
  /**
2389
2548
  * #getter
2390
2549
  * `root` laid out on new nodes in row and branch-length units, read in
2391
- * pixels through `rowHeight` and `treeWidth`. A zoom leaves it and every
2392
- * node in it as they are, and an observer reading a node's `x` or `len`
2393
- * observes the size it scales by.
2550
+ * pixels through `rowHeight` and `effectiveTreeWidth`. A zoom leaves it
2551
+ * and every node in it as they are, and an observer reading a node's `x`
2552
+ * or `len` observes the size it scales by.
2394
2553
  */
2395
2554
  get treeLayout() {
2555
+ const treeWidth = () => this.effectiveTreeWidth
2396
2556
  return layoutTree(this.root, {
2397
2557
  get rowHeight() {
2398
2558
  return self.rowHeight
2399
2559
  },
2400
2560
  get treeWidth() {
2401
- return self.treeWidth
2561
+ return treeWidth()
2402
2562
  },
2403
2563
  })
2404
2564
  },
@@ -2434,15 +2594,6 @@ function stateModelFactory({
2434
2594
  return this.treeLayout.rootToTipLength
2435
2595
  },
2436
2596
 
2437
- /**
2438
- * #getter
2439
- * x-position of the farthest tip in a phylogram, px: treeWidth, or 0 for
2440
- * a tree with no branch lengths (drawn as a cladogram)
2441
- */
2442
- get maxBranchLength() {
2443
- return this.rootToTipLength ? self.treeWidth : 0
2444
- },
2445
-
2446
2597
  /**
2447
2598
  * #getter
2448
2599
  * pixels per unit of branch length in the phylogram layout, 0 in
@@ -2450,7 +2601,9 @@ function stateModelFactory({
2450
2601
  */
2451
2602
  get pxPerBranchLength() {
2452
2603
  const max = this.rootToTipLength
2453
- return this.showBranchLenEffective && max ? self.treeWidth / max : 0
2604
+ return this.showBranchLenEffective && max
2605
+ ? this.effectiveTreeWidth / max
2606
+ : 0
2454
2607
  },
2455
2608
 
2456
2609
  /**
@@ -2497,12 +2650,6 @@ function stateModelFactory({
2497
2650
  self.colWidth > self.rowHeight / 2
2498
2651
  )
2499
2652
  },
2500
- /**
2501
- * #getter
2502
- */
2503
- get showTreeText() {
2504
- return self.drawLabels && self.rowHeight >= minLetterRowHeight
2505
- },
2506
2653
  }))
2507
2654
  .views(self => {
2508
2655
  // a spec is a frozen value, so it keys its computed until setColumnTracks
@@ -2623,7 +2770,6 @@ function stateModelFactory({
2623
2770
  data: content?.data,
2624
2771
  arcs: content?.arcs,
2625
2772
  },
2626
- ReactComponent: TrackBlocks,
2627
2773
  }
2628
2774
  })
2629
2775
  columnTrackModels.set(track, computedModel)
@@ -2639,53 +2785,6 @@ function stateModelFactory({
2639
2785
  }
2640
2786
  })
2641
2787
  .views(self => ({
2642
- /**
2643
- * #getter
2644
- */
2645
- get labelWidthMap() {
2646
- const { showTreeText, leaves } = self
2647
- // gated on the renderer's label condition, so hidden labels reserve no
2648
- // gutter. Measured once at a reference size and scaled by
2649
- // labelWidthScale: re-measuring per vertical-zoom frame cost ~200ms on a
2650
- // 50k-leaf tree
2651
- return showTreeText
2652
- ? new Map(
2653
- leaves.map(node => {
2654
- const { name } = node.data
2655
- // `||`, matching renderTreeLabels: an empty genome falls back
2656
- // to the row name
2657
- const displayName = self.rowDataOf(name)?.genome || name
2658
- return [
2659
- name,
2660
- measureTextCanvas(displayName, labelReferenceFontSize),
2661
- ] as const
2662
- }),
2663
- )
2664
- : new Map<string, number>()
2665
- },
2666
-
2667
- /**
2668
- * #getter
2669
- * factor turning a labelWidthMap entry into its width at the current
2670
- * font size
2671
- */
2672
- get labelWidthScale() {
2673
- return self.fontSize / labelReferenceFontSize
2674
- },
2675
-
2676
- get labelsWidth() {
2677
- // a loop, not Math.max(...): spreading passes one argument per leaf,
2678
- // and the ~125k argument limit throws RangeError on the 230k-tip COVID
2679
- // tree
2680
- let max = 0
2681
- for (const width of this.labelWidthMap.values()) {
2682
- if (width > max) {
2683
- max = width
2684
- }
2685
- }
2686
- return max * this.labelWidthScale
2687
- },
2688
-
2689
2788
  /**
2690
2789
  * #getter
2691
2790
  */
@@ -2732,7 +2831,6 @@ function stateModelFactory({
2732
2831
  data: hideGapsEffective ? skipBlanks(blanks, t.data!) : t.data,
2733
2832
  height: rowHeight,
2734
2833
  },
2735
- ReactComponent: TrackBlocks,
2736
2834
  }))
2737
2835
  },
2738
2836
 
@@ -2755,7 +2853,6 @@ function stateModelFactory({
2755
2853
  height: self.trackHeight('arc'),
2756
2854
  arcs,
2757
2855
  },
2758
- ReactComponent: TrackBlocks,
2759
2856
  },
2760
2857
  ]
2761
2858
  : []
@@ -2803,7 +2900,6 @@ function stateModelFactory({
2803
2900
  heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
2804
2901
  height: self.trackHeight(model.kind),
2805
2902
  },
2806
- ReactComponent: TrackBlocks,
2807
2903
  }))
2808
2904
  },
2809
2905
 
@@ -2846,7 +2942,7 @@ function stateModelFactory({
2846
2942
  * @returns The character at that position
2847
2943
  */
2848
2944
  visibleColToRowLetter(rowName: string, visibleCol: number) {
2849
- return self.rowMap.get(rowName)?.[
2945
+ return self.rowSequence(rowName)?.[
2850
2946
  this.visibleColToGlobalCol(visibleCol)
2851
2947
  ]
2852
2948
  },
@@ -2931,7 +3027,7 @@ function stateModelFactory({
2931
3027
  * @returns The global column index in the full MSA, or undefined
2932
3028
  */
2933
3029
  seqPosToGlobalCol(rowName: string, seqPos: number) {
2934
- const seq = self.rowMap.get(rowName)
3030
+ const seq = self.rowSequence(rowName)
2935
3031
  if (seq === undefined) {
2936
3032
  return undefined
2937
3033
  }
@@ -3180,7 +3276,7 @@ function stateModelFactory({
3180
3276
  get treeScaleBar() {
3181
3277
  return scaleBarLength(
3182
3278
  self.pxPerBranchLength,
3183
- self.treeAreaWidth - self.marginLeft * 2,
3279
+ self.effectiveTreeAreaWidth - self.marginLeft * 2,
3184
3280
  )
3185
3281
  },
3186
3282
  /**
@@ -3239,7 +3335,10 @@ function stateModelFactory({
3239
3335
  )
3240
3336
  },
3241
3337
  get annotationsByRow() {
3242
- return groupBy(this.filteredAnnotations, r => r.id)
3338
+ const { rowNamesSet } = self
3339
+ return groupBy(this.filteredAnnotations, r =>
3340
+ annotationRowName(r.id, rowNamesSet),
3341
+ )
3243
3342
  },
3244
3343
  }))
3245
3344
  .views(self => ({
@@ -3330,7 +3429,9 @@ function stateModelFactory({
3330
3429
  const { annotations, rowNamesSet } = self
3331
3430
  return self.dataInitialized &&
3332
3431
  annotations.length > 0 &&
3333
- !annotations.some(a => rowNamesSet.has(a.id))
3432
+ !annotations.some(a =>
3433
+ rowNamesSet.has(annotationRowName(a.id, rowNamesSet)),
3434
+ )
3334
3435
  ? [
3335
3436
  `0 of ${annotations.length} annotations name a row in this alignment`,
3336
3437
  ]
@@ -3470,10 +3571,13 @@ function stateModelFactory({
3470
3571
  * #action
3471
3572
  * swap in a different tree over the same alignment. Clears `collapsed`,
3472
3573
  * `rotated` and `showOnly`, since path-derived node ids (node-0-0-1) from the old
3473
- * tree would match unrelated nodes in the new one.
3574
+ * tree would match unrelated nodes in the new one, and the tree's
3575
+ * filehandle, which the snapshot would otherwise reopen in place of
3576
+ * the new tree
3474
3577
  */
3475
3578
  replaceTree(newick: string) {
3476
3579
  transaction(() => {
3580
+ self.setTreeFilehandle(undefined)
3477
3581
  self.collapsed.clear()
3478
3582
  self.rotated.clear()
3479
3583
  self.setShowOnly(undefined)
@@ -3501,30 +3605,6 @@ function stateModelFactory({
3501
3605
  axis,
3502
3606
  )
3503
3607
  },
3504
- /**
3505
- * #action
3506
- */
3507
- zoomOutHorizontal() {
3508
- this.zoomAtCenter(0.75, 'horizontal')
3509
- },
3510
- /**
3511
- * #action
3512
- */
3513
- zoomInHorizontal() {
3514
- this.zoomAtCenter(1.5, 'horizontal')
3515
- },
3516
- /**
3517
- * #action
3518
- */
3519
- zoomInVertical() {
3520
- this.zoomAtCenter(1.5, 'vertical')
3521
- },
3522
- /**
3523
- * #action
3524
- */
3525
- zoomOutVertical() {
3526
- this.zoomAtCenter(0.75, 'vertical')
3527
- },
3528
3608
  /**
3529
3609
  * #action
3530
3610
  */
@@ -3617,16 +3697,6 @@ function stateModelFactory({
3617
3697
  self.annotations = annotations
3618
3698
  },
3619
3699
 
3620
- /**
3621
- * #action
3622
- * set the overlay from raw InterProScan results keyed by row name. Kept
3623
- * for downstream plugins that hold the EBI wire format; new code should
3624
- * adapt to Annotation[] and call setAnnotations.
3625
- */
3626
- setDomains(data?: Record<string, InterProScanResults>) {
3627
- this.setAnnotations(data ? interProScanToAnnotations(data) : [])
3628
- },
3629
-
3630
3700
  applyGFFText(gffText: string) {
3631
3701
  this.setAnnotations(gffToAnnotations(parseGFF(gffText)))
3632
3702
  },
@@ -3714,7 +3784,7 @@ function stateModelFactory({
3714
3784
  column: column + 1,
3715
3785
  row,
3716
3786
  residue: self.visibleColToSeqPosOneBased(row, visibleCol),
3717
- letter: self.rowMap.get(row)?.[column],
3787
+ letter: self.rowSequence(row)?.[column],
3718
3788
  }
3719
3789
  },
3720
3790
  /**
@@ -3903,7 +3973,7 @@ function stateModelFactory({
3903
3973
  : visibleDomainTypes.map(d => ({
3904
3974
  id: d.accession,
3905
3975
  label: d.name,
3906
- color: fillPalette[d.accession]!,
3976
+ color: d.color ?? fillPalette[d.accession]!,
3907
3977
  })),
3908
3978
  'domains',
3909
3979
  )
@@ -4170,7 +4240,7 @@ function stateModelFactory({
4170
4240
  * to `end`, gaps included
4171
4241
  */
4172
4242
  get selectionFasta() {
4173
- const { rowNames, rowMap } = self
4243
+ const { rowNames } = self
4174
4244
  const resolved = this.resolvedSelection
4175
4245
  const span = this.selectionFileSpan
4176
4246
  if (!resolved || !span) {
@@ -4179,7 +4249,7 @@ function stateModelFactory({
4179
4249
  const rows = resolved.rowRuns
4180
4250
  .flatMap(([first, last]) => rowNames.slice(first, last + 1))
4181
4251
  .flatMap(name => {
4182
- const seq = rowMap.get(name)
4252
+ const seq = self.rowSequence(name)
4183
4253
  return seq === undefined ? [] : [[name, seq] as const]
4184
4254
  })
4185
4255
  return blockFasta(rows, span.start, span.end)
@@ -4276,7 +4346,7 @@ function stateModelFactory({
4276
4346
  this.visibleDomainTypes.map(d => ({
4277
4347
  id: d.accession,
4278
4348
  label: d.name,
4279
- color: fillPalette[d.accession]!,
4349
+ color: d.color ?? fillPalette[d.accession]!,
4280
4350
  })),
4281
4351
  }
4282
4352
  }
@@ -4453,6 +4523,12 @@ function stateModelFactory({
4453
4523
  setHideHeader(arg: boolean) {
4454
4524
  self.hideHeader = arg
4455
4525
  },
4526
+ /**
4527
+ * #action
4528
+ */
4529
+ setImportForm(arg: boolean) {
4530
+ self.importForm = arg
4531
+ },
4456
4532
  /**
4457
4533
  * #action
4458
4534
  * focus the subtree a click `y` pixels down the tree overview lands on.
@@ -4587,6 +4663,27 @@ function stateModelFactory({
4587
4663
  )
4588
4664
  })
4589
4665
  },
4666
+ /**
4667
+ * #action
4668
+ * zoomToRegion, and for a region naming a row, also scroll that row to
4669
+ * the middle of the alignment, as Go to does. What `region` opens on; a
4670
+ * caller following a range as it moves wants zoomToRegion alone, which
4671
+ * leaves the reader's vertical scroll where they put it.
4672
+ */
4673
+ revealRegion(region: Region) {
4674
+ transaction(() => {
4675
+ this.zoomToRegion(region)
4676
+ const row =
4677
+ region.row === undefined
4678
+ ? undefined
4679
+ : self.rowNamesSet.get(region.row)
4680
+ if (row !== undefined && self.viewInitialized) {
4681
+ self.setScrollY(
4682
+ -(row + 0.5) * self.rowHeight + self.msaAreaHeight / 2,
4683
+ )
4684
+ }
4685
+ })
4686
+ },
4590
4687
  /**
4591
4688
  * #action
4592
4689
  * zoom and scroll so the selected columns fill the alignment's width,
@@ -4718,7 +4815,7 @@ function stateModelFactory({
4718
4815
  self.numColumns > 0 &&
4719
4816
  !(self.treeFilehandle && !self.data.tree)
4720
4817
  ) {
4721
- this.zoomToRegion(region)
4818
+ this.revealRegion(region)
4722
4819
  self.setRegion(undefined)
4723
4820
  }
4724
4821
  }),
@@ -4802,7 +4899,9 @@ function stateModelFactory({
4802
4899
  }
4803
4900
  try {
4804
4901
  setLoading?.(true)
4805
- self.setError(undefined)
4902
+ if (!optional) {
4903
+ self.setError(undefined)
4904
+ }
4806
4905
  const text = await fetchTextWithProgress(
4807
4906
  openLocation(filehandle),
4808
4907
  status => {
@@ -4942,52 +5041,6 @@ function stateModelFactory({
4942
5041
  self.columns
4943
5042
  }),
4944
5043
  )
4945
-
4946
- // with autoTreeAreaWidth and no tree drawn, size the tree area to the
4947
- // labels. Gated on noTree/!drawTree so it does not loop with the
4948
- // treeWidth autorun below
4949
- addDisposer(
4950
- self,
4951
- autorun(() => {
4952
- if (
4953
- self.autoTreeAreaWidth &&
4954
- (self.noTree || !self.drawTree) &&
4955
- self.labelsWidth
4956
- ) {
4957
- self.setTreeAreaWidth(
4958
- self.labelsWidth + self.marginLeft + 12 + self.cladeGutterWidth,
4959
- )
4960
- }
4961
- }),
4962
- )
4963
-
4964
- // treeWidth follows the tree area less the labels. A non-default
4965
- // snapshot treeWidth (jbrowse-plugin-msaview opens at 100 in a 200px
4966
- // area) is kept until the tree area changes. Not a getter, because
4967
- // labelsWidth is measured off leaves laid out against treeWidth.
4968
- let pinnedAreaWidth =
4969
- self.treeWidth === defaultTreeWidth ? undefined : self.treeAreaWidth
4970
- addDisposer(
4971
- self,
4972
- autorun(() => {
4973
- const areaWidth = self.treeAreaWidth
4974
- const labelsWidth = self.labelsWidth
4975
- if (pinnedAreaWidth === areaWidth) {
4976
- return
4977
- }
4978
- pinnedAreaWidth = undefined
4979
- self.setTreeWidth(
4980
- Math.max(
4981
- 50,
4982
- areaWidth -
4983
- labelsWidth -
4984
- 10 -
4985
- self.marginLeft -
4986
- self.cladeGutterWidth,
4987
- ),
4988
- )
4989
- }),
4990
- )
4991
5044
  },
4992
5045
  }))
4993
5046
  .postProcessSnapshot(({ data, columnTracks, ...rest }) => ({