react-msaview 9.0.0 → 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 (157) 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/MSAViewer.d.ts +1891 -4
  12. package/dist/components/MSAViewer.js +144 -201
  13. package/dist/components/MSAViewer.js.map +1 -1
  14. package/dist/components/ResizeHandles.js +3 -2
  15. package/dist/components/ResizeHandles.js.map +1 -1
  16. package/dist/components/Track.js +2 -1
  17. package/dist/components/Track.js.map +1 -1
  18. package/dist/components/controls.d.ts +11 -0
  19. package/dist/components/controls.js +48 -0
  20. package/dist/components/controls.js.map +1 -0
  21. package/dist/components/dialogs/AnnotationFileDialog.js +5 -16
  22. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
  23. package/dist/components/dialogs/ExportSVGDialog.js +5 -7
  24. package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
  25. package/dist/components/dialogs/HelpDialog.d.ts +6 -0
  26. package/dist/components/dialogs/HelpDialog.js +30 -0
  27. package/dist/components/dialogs/HelpDialog.js.map +1 -0
  28. package/dist/components/header/FileMenu.js +33 -7
  29. package/dist/components/header/FileMenu.js.map +1 -1
  30. package/dist/components/header/Header.js +4 -6
  31. package/dist/components/header/Header.js.map +1 -1
  32. package/dist/components/header/TreeSettingsMenu.d.ts +1 -1
  33. package/dist/components/header/TreeSettingsMenu.js +1 -32
  34. package/dist/components/header/TreeSettingsMenu.js.map +1 -1
  35. package/dist/components/header/ZoomControls.js +28 -44
  36. package/dist/components/header/ZoomControls.js.map +1 -1
  37. package/dist/components/header/settingsMenuItems.d.ts +1 -0
  38. package/dist/components/header/settingsMenuItems.js +44 -0
  39. package/dist/components/header/settingsMenuItems.js.map +1 -1
  40. package/dist/components/import/data/seq2.d.ts +0 -1
  41. package/dist/components/import/data/seq2.js +0 -17
  42. package/dist/components/import/data/seq2.js.map +1 -1
  43. package/dist/components/msa/MSACanvas.js +2 -1
  44. package/dist/components/msa/MSACanvas.js.map +1 -1
  45. package/dist/components/msa/MSACanvasBlock.js +2 -5
  46. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  47. package/dist/components/msa/drawFeatureSpans.d.ts +50 -11
  48. package/dist/components/msa/drawFeatureSpans.js +99 -56
  49. package/dist/components/msa/drawFeatureSpans.js.map +1 -1
  50. package/dist/components/msa/msaRaster.d.ts +14 -1
  51. package/dist/components/msa/msaRaster.js +19 -9
  52. package/dist/components/msa/msaRaster.js.map +1 -1
  53. package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +9 -0
  54. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +19 -8
  55. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  56. package/dist/components/msa/renderHighlights.d.ts +2 -2
  57. package/dist/components/msa/renderHighlights.js +2 -3
  58. package/dist/components/msa/renderHighlights.js.map +1 -1
  59. package/dist/components/msa/renderMSAMouseover.js +2 -1
  60. package/dist/components/msa/renderMSAMouseover.js.map +1 -1
  61. package/dist/components/renderCtx.d.ts +1 -1
  62. package/dist/components/rowpanels/renderFeaturePanel.d.ts +5 -1
  63. package/dist/components/rowpanels/renderFeaturePanel.js +12 -5
  64. package/dist/components/rowpanels/renderFeaturePanel.js.map +1 -1
  65. package/dist/components/tracks/drawTracks.d.ts +1 -53
  66. package/dist/components/tracks/drawTracks.js +5 -5
  67. package/dist/components/tracks/drawTracks.js.map +1 -1
  68. package/dist/components/tree/CladeLabels.d.ts +1 -1
  69. package/dist/components/tree/CladeLabels.js +22 -34
  70. package/dist/components/tree/CladeLabels.js.map +1 -1
  71. package/dist/components/tree/cladeBrackets.d.ts +17 -10
  72. package/dist/components/tree/cladeBrackets.js +33 -3
  73. package/dist/components/tree/cladeBrackets.js.map +1 -1
  74. package/dist/components/tree/renderTreeCanvas.js +2 -8
  75. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  76. package/dist/components/tree/renderTreeOverview.js +4 -11
  77. package/dist/components/tree/renderTreeOverview.js.map +1 -1
  78. package/dist/element.js +3 -6
  79. package/dist/element.js.map +1 -1
  80. package/dist/exportLayout.d.ts +39 -0
  81. package/dist/exportLayout.js +78 -0
  82. package/dist/exportLayout.js.map +1 -0
  83. package/dist/headlessRenderEnv.d.ts +12 -39
  84. package/dist/headlessRenderEnv.js +18 -12
  85. package/dist/headlessRenderEnv.js.map +1 -1
  86. package/dist/hierarchy.d.ts +5 -8
  87. package/dist/hierarchy.js +5 -79
  88. package/dist/hierarchy.js.map +1 -1
  89. package/dist/index.d.ts +3 -1
  90. package/dist/index.js +4 -1
  91. package/dist/index.js.map +1 -1
  92. package/dist/measureTextCanvas.d.ts +7 -0
  93. package/dist/measureTextCanvas.js +8 -6
  94. package/dist/measureTextCanvas.js.map +1 -1
  95. package/dist/model/treeModel.d.ts +0 -5
  96. package/dist/model/treeModel.js +0 -7
  97. package/dist/model/treeModel.js.map +1 -1
  98. package/dist/model.d.ts +38 -44
  99. package/dist/model.js +153 -112
  100. package/dist/model.js.map +1 -1
  101. package/dist/renderToSvg.d.ts +2 -7
  102. package/dist/renderToSvg.js +32 -120
  103. package/dist/renderToSvg.js.map +1 -1
  104. package/dist/svgTestUtil.d.ts +12 -20
  105. package/dist/types.d.ts +0 -1
  106. package/dist/version.d.ts +1 -1
  107. package/dist/version.js +1 -1
  108. package/dist/viewerSnapshot.d.ts +169 -0
  109. package/dist/viewerSnapshot.js +100 -0
  110. package/dist/viewerSnapshot.js.map +1 -0
  111. package/package.json +3 -3
  112. package/src/colorSchemes.ts +1 -4
  113. package/src/columnCounts.ts +1 -1
  114. package/src/components/Loading.tsx +19 -12
  115. package/src/components/MSAViewer.tsx +275 -251
  116. package/src/components/ResizeHandles.tsx +9 -2
  117. package/src/components/Track.tsx +2 -1
  118. package/src/components/controls.ts +61 -0
  119. package/src/components/dialogs/AnnotationFileDialog.tsx +5 -21
  120. package/src/components/dialogs/ExportSVGDialog.tsx +8 -19
  121. package/src/components/dialogs/HelpDialog.tsx +79 -0
  122. package/src/components/header/FileMenu.tsx +33 -7
  123. package/src/components/header/Header.tsx +4 -6
  124. package/src/components/header/TreeSettingsMenu.tsx +2 -32
  125. package/src/components/header/ZoomControls.tsx +30 -66
  126. package/src/components/header/settingsMenuItems.ts +59 -0
  127. package/src/components/import/data/seq2.ts +0 -18
  128. package/src/components/msa/MSACanvas.tsx +2 -1
  129. package/src/components/msa/MSACanvasBlock.tsx +2 -6
  130. package/src/components/msa/drawFeatureSpans.ts +160 -79
  131. package/src/components/msa/msaRaster.ts +27 -9
  132. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +39 -24
  133. package/src/components/msa/renderHighlights.ts +2 -3
  134. package/src/components/msa/renderMSAMouseover.ts +5 -1
  135. package/src/components/renderCtx.ts +0 -1
  136. package/src/components/rowpanels/renderFeaturePanel.ts +26 -7
  137. package/src/components/tracks/drawTracks.ts +5 -5
  138. package/src/components/tree/CladeLabels.tsx +30 -54
  139. package/src/components/tree/cladeBrackets.ts +50 -3
  140. package/src/components/tree/renderTreeCanvas.ts +2 -13
  141. package/src/components/tree/renderTreeOverview.ts +7 -14
  142. package/src/element.ts +3 -7
  143. package/src/exportLayout.ts +134 -0
  144. package/src/headlessRenderEnv.ts +24 -11
  145. package/src/hierarchy.ts +5 -88
  146. package/src/index.ts +5 -5
  147. package/src/measureTextCanvas.ts +8 -6
  148. package/src/model/treeModel.ts +0 -7
  149. package/src/model.ts +170 -122
  150. package/src/renderToSvg.tsx +55 -194
  151. package/src/types.ts +0 -1
  152. package/src/version.ts +1 -1
  153. package/src/viewerSnapshot.ts +147 -0
  154. package/dist/components/header/ZoomMenu.d.ts +0 -6
  155. package/dist/components/header/ZoomMenu.js +0 -37
  156. package/dist/components/header/ZoomMenu.js.map +0 -1
  157. package/src/components/header/ZoomMenu.tsx +0 -48
package/dist/model.js CHANGED
@@ -3,7 +3,7 @@ import { openLocation } from '@jbrowse/core/util/io';
3
3
  import { ElementId, FileLocation } from '@jbrowse/core/util/types/mst';
4
4
  import { addDisposer, applySnapshot, cast, getSnapshot, isAlive, types, } from '@jbrowse/mobx-state-tree';
5
5
  import { autorun, compareStructural, computed, reaction, transaction, } from 'mobx';
6
- import { generateNodeIds, gffToAnnotations, interProScanToAnnotations, parseEmfTree, parseGFF, parseMSA, parseNewick, } from 'msa-parsers';
6
+ import { generateNodeIds, gffToAnnotations, parseEmfTree, parseGFF, parseMSA, parseNewick, } from 'msa-parsers';
7
7
  import { blockFasta } from './blockFasta.js';
8
8
  import { calculateBlocks } from './calculateBlocks.js';
9
9
  import { clustalXColumnColors } from './clustalX.js';
@@ -12,7 +12,6 @@ import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.js';
12
12
  import { columnStats } from './columnStats.js';
13
13
  import { packDomainLanes } from './components/msa/packDomainLanes.js';
14
14
  import { visibleColRange } from './components/msa/visibleColRange.js';
15
- import TrackBlocks from './components/tracks/TrackBlocks.js';
16
15
  import { cladeGutterWidth } from './components/tree/cladeBrackets.js';
17
16
  import { scaleBarLength } from './components/tree/scaleBar.js';
18
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';
@@ -22,7 +21,7 @@ import { featureField, featureName } from './featureFields.js';
22
21
  import { featuresToAnnotations } from './featuresLayer.js';
23
22
  import { fetchTextWithProgress, isAbortError } from './fetchUtils.js';
24
23
  import { flatToTree } from './flatToTree.js';
25
- import { calcDepthToLeaf, clusterLayout, collapse, find, forEachDescendant, hierarchy, layoutTree, leafIndex, leaves, maxLength, mrca, nodeCoveringRows, setBrLength, sort, sum as hierarchySum, } from './hierarchy.js';
24
+ import { collapse, find, forEachDescendant, hierarchy, layoutTree, leafIndex, leaves, mrca, nodeCoveringRows, sort, sum as hierarchySum, } from './hierarchy.js';
26
25
  import { DataModelF } from './model/DataModel.js';
27
26
  import { DialogQueueSessionMixin } from './model/DialogQueue.js';
28
27
  import { MSAModelF } from './model/msaModel.js';
@@ -322,33 +321,71 @@ const seedingMarks = new Set(['collapse', 'focus', 'rotate']);
322
321
  // shared empty results, so observers don't see a fresh [] as a change
323
322
  const noDomains = [];
324
323
  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
+ }
325
359
  /**
326
360
  * The rows a clade covers and the node an `mrca` names, or undefined when the
327
361
  * clade does not resolve: a tip name the tree does not have or has twice, or a
328
- * leaf count `tips` disagrees with. `index` and `rowNamesSet` are the memoized
329
- * passes over the tree.
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.
330
365
  */
331
- function cladeRows(clade, root, index, rowNamesSet) {
366
+ function cladeRows(clade, root, index, tips) {
332
367
  if (clade.range) {
333
- const [a, b] = clade.range.map(name => index.get(name) ? rowNamesSet.get(name) : undefined);
368
+ const [a, b] = clade.range.map(name => index.get(name) ? tips.get(name) : undefined);
334
369
  if (a === undefined || b === undefined) {
335
370
  return undefined;
336
371
  }
337
- const rows = a <= b ? [a, b] : [b, a];
338
- return rows[1] - rows[0] + 1 === clade.tips ? { rows } : undefined;
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;
339
376
  }
340
377
  const node = clade.mrca ? mrca(root, clade.mrca, index) : undefined;
341
378
  if (!node) {
342
379
  return undefined;
343
380
  }
344
- const tips = leaves(node);
345
- if (tips.length !== clade.tips) {
381
+ const nodeTips = leaves(node);
382
+ if (nodeTips.length !== clade.tips) {
346
383
  return undefined;
347
384
  }
348
385
  let first = Infinity;
349
386
  let last = -Infinity;
350
- for (const tip of tips) {
351
- const row = rowNamesSet.get(tip.data.name);
387
+ for (const tip of nodeTips) {
388
+ const row = tips.get(tip.data.name)?.row;
352
389
  if (row !== undefined) {
353
390
  first = Math.min(first, row);
354
391
  last = Math.max(last, row);
@@ -359,15 +396,17 @@ function cladeRows(clade, root, index, rowNamesSet) {
359
396
  : undefined;
360
397
  }
361
398
  /**
362
- * Every clade that resolves, with its fill color settled, against the leaf
363
- * order `rowNamesSet` gives. The tree panel resolves against the displayed
364
- * rows and the overview against its own, so both take the tree they draw. A
365
- * `range` record names no node, so the seeding marks, which need one, drop it.
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.
366
404
  */
367
- function resolveClades(clades, root, rowNamesSet) {
405
+ function resolveClades(clades, root, displayed) {
368
406
  const index = leafIndex(root);
407
+ const tips = tipRows(displayed);
369
408
  return clades.flatMap(clade => {
370
- const resolved = cladeRows(clade, root, index, rowNamesSet);
409
+ const resolved = cladeRows(clade, root, index, tips);
371
410
  const seeding = seedingMarks.has(clade.mark);
372
411
  return resolved && !(seeding && resolved.nodeId === undefined)
373
412
  ? [
@@ -470,8 +509,8 @@ function rootTree(tree, treeRoot) {
470
509
  ? generateNodeIds(rerootTree(input, plain.get(target.data)))
471
510
  : tree;
472
511
  }
473
- // the inclusive tip indices a node covers, from the row-space extent
474
- // clusterLayout writes to xMin/xMax as tip centers
512
+ // the inclusive tip indices a node covers, from its xMin/xMax in a layout one
513
+ // row per tip
475
514
  function tipRange(node) {
476
515
  return [Math.round(node.xMin - 0.5), Math.round(node.xMax - 0.5)];
477
516
  }
@@ -483,6 +522,15 @@ const featureChannels = new Set([
483
522
  // seqPos -> column indexes per row, keyed on the parse so they are garbage
484
523
  // collected with it. A computed would rebuild every row's index when read
485
524
  // outside a reactive context.
525
+ // Newick tools write a colon in a name as an underscore, so a tree's row
526
+ // `chr1_100-200` is the GFF's `chr1:100-200`, as FastaMSA.getRow resolves it
527
+ function annotationRowName(id, rowNames) {
528
+ if (rowNames.has(id)) {
529
+ return id;
530
+ }
531
+ const underscored = id.replaceAll(':', '_');
532
+ return rowNames.has(underscored) ? underscored : id;
533
+ }
486
534
  const seqPosIndexCache = new WeakMap();
487
535
  // A segment is a 1:1 run. A segment whose sides differ in length is malformed,
488
536
  // and the lookups treat it like an uncovered position.
@@ -768,9 +816,10 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
768
816
  /**
769
817
  * #property
770
818
  * where the view opens, in `highlights` coordinates: `{row, start,
771
- * end}` zooms onto residues of that row, `{start, end}` onto alignment
772
- * columns. Applied once the alignment and any tree file have loaded,
773
- * then cleared, so a reloaded session keeps the reader's own scroll.
819
+ * end}` zooms onto residues of that row and scrolls the row to the
820
+ * middle, `{start, end}` zooms onto alignment columns. Applied once the
821
+ * alignment and any tree file have loaded, then cleared, so a reloaded
822
+ * session keeps the reader's own scroll.
774
823
  */
775
824
  region: types.frozen(),
776
825
  /**
@@ -811,6 +860,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
811
860
  * of the snapshot so a link opened in the full app shows the toolbar.
812
861
  */
813
862
  hideHeader: false,
863
+ /**
864
+ * #volatile
865
+ * shows the import form while the view has no data. MSAViewer turns it
866
+ * off, since its host supplies the data and the form is the demo app's
867
+ */
868
+ importForm: true,
814
869
  /**
815
870
  * #volatile
816
871
  */
@@ -1275,17 +1330,15 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1275
1330
  .views(self => ({
1276
1331
  /**
1277
1332
  * #getter
1278
- * hideGaps takes effect when there are collapsed rows or allowedGappyness < 100
1333
+ * hideGaps takes effect when a collapse or a focus hides rows, whose
1334
+ * residues leave columns all gaps among the rest, or when
1335
+ * allowedGappyness is under 100
1279
1336
  */
1280
1337
  get hideGapsEffective() {
1281
1338
  return (self.hideGaps &&
1282
- (self.collapsed.length > 0 || self.allowedGappyness < 100));
1283
- },
1284
- /**
1285
- * #getter
1286
- */
1287
- get realAllowedGappyness() {
1288
- return this.hideGapsEffective ? self.allowedGappyness : 100;
1339
+ (self.collapsed.length > 0 ||
1340
+ self.showOnly !== undefined ||
1341
+ self.allowedGappyness < 100));
1289
1342
  },
1290
1343
  /**
1291
1344
  * #getter
@@ -1534,7 +1587,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1534
1587
  if (self.clades.length === 0) {
1535
1588
  return noClades;
1536
1589
  }
1537
- return resolveClades(self.clades, hierarchy(this.tree, d => d.children), this.rowNamesSet);
1590
+ return resolveClades(self.clades, hierarchy(this.tree, d => d.children), this.root);
1538
1591
  },
1539
1592
  /**
1540
1593
  * #getter
@@ -1702,21 +1755,16 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1702
1755
  if (!self.showTreeOverview) {
1703
1756
  return undefined;
1704
1757
  }
1705
- const root = buildTreeRoot(this.tree, {
1758
+ const layout = layoutTree(buildTreeRoot(this.tree, {
1706
1759
  collapsed: self.collapsed,
1707
1760
  rotated: self.rotated,
1708
1761
  order: self.treeOrder,
1709
- });
1710
- const numTips = leaves(root).length;
1711
- clusterLayout(root, numTips, 1);
1712
- const rootLen = Math.max(root.data.length || 0, 0);
1713
- const extent = maxLength(root) - rootLen;
1714
- setBrLength(root, -rootLen, extent ? 1 / extent : 0);
1762
+ }), { rowHeight: 1, treeWidth: 1 });
1715
1763
  return {
1716
- root,
1717
- numTips,
1718
- maxDepthToLeaf: calcDepthToLeaf(root),
1719
- showBranchLen: self.showBranchLen && extent > 0,
1764
+ root: layout.root,
1765
+ numTips: layout.leaves.length,
1766
+ maxDepthToLeaf: layout.root.depthToLeaf,
1767
+ showBranchLen: self.showBranchLen && layout.rootToTipLength > 0,
1720
1768
  };
1721
1769
  },
1722
1770
  /**
@@ -1729,8 +1777,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1729
1777
  if (!layout || self.clades.length === 0) {
1730
1778
  return noClades;
1731
1779
  }
1732
- const rowNames = new Map(leaves(layout.root).map((leaf, index) => [leaf.data.name, index]));
1733
- return resolveClades(self.clades, layout.root, rowNames);
1780
+ return resolveClades(self.clades, hierarchy(this.tree, d => d.children), layout.root);
1734
1781
  },
1735
1782
  /**
1736
1783
  * #getter
@@ -1811,7 +1858,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1811
1858
  * #getter
1812
1859
  */
1813
1860
  get blanks() {
1814
- const { hideGapsEffective, realAllowedGappyness } = self;
1861
+ const { hideGapsEffective, allowedGappyness } = self;
1815
1862
  if (!hideGapsEffective) {
1816
1863
  return [];
1817
1864
  }
@@ -1827,7 +1874,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1827
1874
  // row that stops early counts as gapped for the remainder
1828
1875
  const numCols = this.MSA?.getWidth() ?? 0;
1829
1876
  const numRows = strs.length;
1830
- const threshold = Math.ceil((realAllowedGappyness / 100) * numRows);
1877
+ const threshold = Math.ceil((allowedGappyness / 100) * numRows);
1831
1878
  const blankCounts = new Uint32Array(numCols);
1832
1879
  for (let j = 0; j < numRows; j++) {
1833
1880
  const str = strs[j];
@@ -1909,11 +1956,19 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1909
1956
  }
1910
1957
  return index;
1911
1958
  },
1959
+ /**
1960
+ * #method
1961
+ * the row's aligned sequence by the name the tree gives it, including
1962
+ * rows a collapsed clade hides. `MSA.getRow` resolves a FASTA name whose
1963
+ * colon a Newick tool wrote as an underscore, which `rowMap` does not
1964
+ */
1965
+ rowSequence(name) {
1966
+ return this.MSA?.getRow(name) || undefined;
1967
+ },
1912
1968
  /**
1913
1969
  * #getter
1914
- * every sequence in the alignment, keyed by row name, including rows a
1915
- * collapsed clade hides. `rows` holds only the rows on screen; lookups
1916
- * by row name use this
1970
+ * every sequence in the alignment, keyed by the alignment's own row
1971
+ * names, including rows a collapsed clade hides
1917
1972
  */
1918
1973
  get rowMap() {
1919
1974
  const MSA = this.MSA;
@@ -1956,10 +2011,11 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1956
2011
  */
1957
2012
  get sequenceType() {
1958
2013
  const letters = this.colStats.lettersPresent;
1959
- // isDna already excludes U (not in the DNA set) and isRna excludes T,
1960
- // so the set membership alone disambiguates the two
1961
- const dna = new Set(['A', 'C', 'G', 'T', 'N']);
1962
- const rna = new Set(['A', 'C', 'G', 'U', 'N']);
2014
+ // the two-base IUPAC codes keep an ambiguous read nucleotide; the
2015
+ // three-base ones (B, D, H, V) are common amino acids, so they don't
2016
+ const ambiguous = ['N', 'R', 'Y', 'K', 'M', 'S', 'W'];
2017
+ const dna = new Set(['A', 'C', 'G', 'T', ...ambiguous]);
2018
+ const rna = new Set(['A', 'C', 'G', 'U', ...ambiguous]);
1963
2019
  const isDna = letters.size > 0 && [...letters].every(l => dna.has(l));
1964
2020
  const isRna = letters.size > 0 && [...letters].every(l => rna.has(l));
1965
2021
  return isDna ? 'dna' : isRna ? 'rna' : 'amino';
@@ -2082,14 +2138,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2082
2138
  get rootToTipLength() {
2083
2139
  return this.treeLayout.rootToTipLength;
2084
2140
  },
2085
- /**
2086
- * #getter
2087
- * x-position of the farthest tip in a phylogram, px: effectiveTreeWidth,
2088
- * or 0 for a tree with no branch lengths (drawn as a cladogram)
2089
- */
2090
- get maxBranchLength() {
2091
- return this.rootToTipLength ? this.effectiveTreeWidth : 0;
2092
- },
2093
2141
  /**
2094
2142
  * #getter
2095
2143
  * pixels per unit of branch length in the phylogram layout, 0 in
@@ -2245,7 +2293,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2245
2293
  data: content?.data,
2246
2294
  arcs: content?.arcs,
2247
2295
  },
2248
- ReactComponent: TrackBlocks,
2249
2296
  };
2250
2297
  });
2251
2298
  columnTrackModels.set(track, computedModel);
@@ -2303,7 +2350,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2303
2350
  data: hideGapsEffective ? skipBlanks(blanks, t.data) : t.data,
2304
2351
  height: rowHeight,
2305
2352
  },
2306
- ReactComponent: TrackBlocks,
2307
2353
  }));
2308
2354
  },
2309
2355
  /**
@@ -2325,7 +2371,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2325
2371
  height: self.trackHeight('arc'),
2326
2372
  arcs,
2327
2373
  },
2328
- ReactComponent: TrackBlocks,
2329
2374
  },
2330
2375
  ]
2331
2376
  : [];
@@ -2372,7 +2417,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2372
2417
  heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
2373
2418
  height: self.trackHeight(model.kind),
2374
2419
  },
2375
- ReactComponent: TrackBlocks,
2376
2420
  }));
2377
2421
  },
2378
2422
  get tracks() {
@@ -2408,7 +2452,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2408
2452
  * @returns The character at that position
2409
2453
  */
2410
2454
  visibleColToRowLetter(rowName, visibleCol) {
2411
- return self.rowMap.get(rowName)?.[this.visibleColToGlobalCol(visibleCol)];
2455
+ return self.rowSequence(rowName)?.[this.visibleColToGlobalCol(visibleCol)];
2412
2456
  },
2413
2457
  /**
2414
2458
  * #method
@@ -2483,7 +2527,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2483
2527
  * @returns The global column index in the full MSA, or undefined
2484
2528
  */
2485
2529
  seqPosToGlobalCol(rowName, seqPos) {
2486
- const seq = self.rowMap.get(rowName);
2530
+ const seq = self.rowSequence(rowName);
2487
2531
  if (seq === undefined) {
2488
2532
  return undefined;
2489
2533
  }
@@ -2749,7 +2793,8 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2749
2793
  return self.allAnnotations.filter(r => !self.turnedOffFeatures.get(r.accession));
2750
2794
  },
2751
2795
  get annotationsByRow() {
2752
- return groupBy(this.filteredAnnotations, r => r.id);
2796
+ const { rowNamesSet } = self;
2797
+ return groupBy(this.filteredAnnotations, r => annotationRowName(r.id, rowNamesSet));
2753
2798
  },
2754
2799
  }))
2755
2800
  .views(self => ({
@@ -2837,7 +2882,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2837
2882
  const { annotations, rowNamesSet } = self;
2838
2883
  return self.dataInitialized &&
2839
2884
  annotations.length > 0 &&
2840
- !annotations.some(a => rowNamesSet.has(a.id))
2885
+ !annotations.some(a => rowNamesSet.has(annotationRowName(a.id, rowNamesSet)))
2841
2886
  ? [
2842
2887
  `0 of ${annotations.length} annotations name a row in this alignment`,
2843
2888
  ]
@@ -2962,10 +3007,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2962
3007
  * #action
2963
3008
  * swap in a different tree over the same alignment. Clears `collapsed`,
2964
3009
  * `rotated` and `showOnly`, since path-derived node ids (node-0-0-1) from the old
2965
- * tree would match unrelated nodes in the new one.
3010
+ * tree would match unrelated nodes in the new one, and the tree's
3011
+ * filehandle, which the snapshot would otherwise reopen in place of
3012
+ * the new tree
2966
3013
  */
2967
3014
  replaceTree(newick) {
2968
3015
  transaction(() => {
3016
+ self.setTreeFilehandle(undefined);
2969
3017
  self.collapsed.clear();
2970
3018
  self.rotated.clear();
2971
3019
  self.setShowOnly(undefined);
@@ -2987,30 +3035,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2987
3035
  zoomAtCenter(scaleFactor, axis = 'both') {
2988
3036
  this.zoomToPos(scaleFactor, self.msaCanvasWidth / 2, self.msaAreaHeight / 2, axis);
2989
3037
  },
2990
- /**
2991
- * #action
2992
- */
2993
- zoomOutHorizontal() {
2994
- this.zoomAtCenter(0.75, 'horizontal');
2995
- },
2996
- /**
2997
- * #action
2998
- */
2999
- zoomInHorizontal() {
3000
- this.zoomAtCenter(1.5, 'horizontal');
3001
- },
3002
- /**
3003
- * #action
3004
- */
3005
- zoomInVertical() {
3006
- this.zoomAtCenter(1.5, 'vertical');
3007
- },
3008
- /**
3009
- * #action
3010
- */
3011
- zoomOutVertical() {
3012
- this.zoomAtCenter(0.75, 'vertical');
3013
- },
3014
3038
  /**
3015
3039
  * #action
3016
3040
  */
@@ -3077,15 +3101,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3077
3101
  setAnnotations(annotations) {
3078
3102
  self.annotations = annotations;
3079
3103
  },
3080
- /**
3081
- * #action
3082
- * set the overlay from raw InterProScan results keyed by row name. Kept
3083
- * for downstream plugins that hold the EBI wire format; new code should
3084
- * adapt to Annotation[] and call setAnnotations.
3085
- */
3086
- setDomains(data) {
3087
- this.setAnnotations(data ? interProScanToAnnotations(data) : []);
3088
- },
3089
3104
  applyGFFText(gffText) {
3090
3105
  this.setAnnotations(gffToAnnotations(parseGFF(gffText)));
3091
3106
  },
@@ -3166,7 +3181,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3166
3181
  column: column + 1,
3167
3182
  row,
3168
3183
  residue: self.visibleColToSeqPosOneBased(row, visibleCol),
3169
- letter: self.rowMap.get(row)?.[column],
3184
+ letter: self.rowSequence(row)?.[column],
3170
3185
  };
3171
3186
  },
3172
3187
  /**
@@ -3331,7 +3346,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3331
3346
  : visibleDomainTypes.map(d => ({
3332
3347
  id: d.accession,
3333
3348
  label: d.name,
3334
- color: fillPalette[d.accession],
3349
+ color: d.color ?? fillPalette[d.accession],
3335
3350
  })), 'domains');
3336
3351
  }
3337
3352
  for (const { channel, field, legend } of this.resolvedEncodings) {
@@ -3567,7 +3582,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3567
3582
  * to `end`, gaps included
3568
3583
  */
3569
3584
  get selectionFasta() {
3570
- const { rowNames, rowMap } = self;
3585
+ const { rowNames } = self;
3571
3586
  const resolved = this.resolvedSelection;
3572
3587
  const span = this.selectionFileSpan;
3573
3588
  if (!resolved || !span) {
@@ -3576,7 +3591,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3576
3591
  const rows = resolved.rowRuns
3577
3592
  .flatMap(([first, last]) => rowNames.slice(first, last + 1))
3578
3593
  .flatMap(name => {
3579
- const seq = rowMap.get(name);
3594
+ const seq = self.rowSequence(name);
3580
3595
  return seq === undefined ? [] : [[name, seq]];
3581
3596
  });
3582
3597
  return blockFasta(rows, span.start, span.end);
@@ -3655,7 +3670,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3655
3670
  this.visibleDomainTypes.map(d => ({
3656
3671
  id: d.accession,
3657
3672
  label: d.name,
3658
- color: fillPalette[d.accession],
3673
+ color: d.color ?? fillPalette[d.accession],
3659
3674
  })),
3660
3675
  };
3661
3676
  }
@@ -3804,6 +3819,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3804
3819
  setHideHeader(arg) {
3805
3820
  self.hideHeader = arg;
3806
3821
  },
3822
+ /**
3823
+ * #action
3824
+ */
3825
+ setImportForm(arg) {
3826
+ self.importForm = arg;
3827
+ },
3807
3828
  /**
3808
3829
  * #action
3809
3830
  * focus the subtree a click `y` pixels down the tree overview lands on.
@@ -3911,6 +3932,24 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3911
3932
  self.scrollX = clamp(-span.startCol * self.colWidth, self.maxScrollX, 0);
3912
3933
  });
3913
3934
  },
3935
+ /**
3936
+ * #action
3937
+ * zoomToRegion, and for a region naming a row, also scroll that row to
3938
+ * the middle of the alignment, as Go to does. What `region` opens on; a
3939
+ * caller following a range as it moves wants zoomToRegion alone, which
3940
+ * leaves the reader's vertical scroll where they put it.
3941
+ */
3942
+ revealRegion(region) {
3943
+ transaction(() => {
3944
+ this.zoomToRegion(region);
3945
+ const row = region.row === undefined
3946
+ ? undefined
3947
+ : self.rowNamesSet.get(region.row);
3948
+ if (row !== undefined && self.viewInitialized) {
3949
+ self.setScrollY(-(row + 0.5) * self.rowHeight + self.msaAreaHeight / 2);
3950
+ }
3951
+ });
3952
+ },
3914
3953
  /**
3915
3954
  * #action
3916
3955
  * zoom and scroll so the selected columns fill the alignment's width,
@@ -4011,7 +4050,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4011
4050
  self.viewInitialized &&
4012
4051
  self.numColumns > 0 &&
4013
4052
  !(self.treeFilehandle && !self.data.tree)) {
4014
- this.zoomToRegion(region);
4053
+ this.revealRegion(region);
4015
4054
  self.setRegion(undefined);
4016
4055
  }
4017
4056
  }));
@@ -4073,7 +4112,9 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4073
4112
  };
4074
4113
  try {
4075
4114
  setLoading?.(true);
4076
- self.setError(undefined);
4115
+ if (!optional) {
4116
+ self.setError(undefined);
4117
+ }
4077
4118
  const text = await fetchTextWithProgress(openLocation(filehandle), status => {
4078
4119
  if (isCurrent()) {
4079
4120
  self.setStatus(status);