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/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,18 +12,16 @@ 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
- import { cladeHighlightAlpha, cladeHighlightColor, defaultAllowedGappyness, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawMsaLetters, defaultFeaturePanelWidth, defaultHeight, defaultHideGaps, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultScrollZoomAxis, defaultShowDomainLegend, defaultShowDomains, defaultSubFeatureRows, defaultTreeWidth, labelReferenceFontSize, maxCellSize, maxInlineSnapshotBytes as defaultMaxInlineSnapshotBytes, maxNeighborJoiningRows, minColWidth, minLetterColWidth, minLetterRowHeight, minRowHeight, rowPanelHeaderHeight, rowTintAlpha, scrollZoomAxes, segmentFeatureTypes, segmentShades, treeScaleBarHeight, } from './constants.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';
19
18
  import { createPaletteMap } from './createPaletteMap.js';
20
19
  import { exportFileName } from './exportFileName.js';
21
20
  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';
26
- import { measureTextCanvas } from './measureTextCanvas.js';
24
+ import { collapse, find, forEachDescendant, hierarchy, layoutTree, leafIndex, leaves, mrca, nodeCoveringRows, sort, sum as hierarchySum, } from './hierarchy.js';
27
25
  import { DataModelF } from './model/DataModel.js';
28
26
  import { DialogQueueSessionMixin } from './model/DialogQueue.js';
29
27
  import { MSAModelF } from './model/msaModel.js';
@@ -37,6 +35,7 @@ import { resolveScale } from './scales.js';
37
35
  import { buildSeqPosIndex } from './seqPosToGlobalCol.js';
38
36
  import { maxBitsFor } from './sequenceLogo.js';
39
37
  import { stripDefault } from './stripDefault.js';
38
+ import { treeTips, widestLabel } from './tipLabels.js';
40
39
  import { computeRowInsertions, dropBlanks, len, outlineColor, skipBlanks, withAlpha, } from './util.js';
41
40
  import { saveAs } from './vendor/fileSaver.js';
42
41
  import { writeNewick } from './writeNewick.js';
@@ -303,7 +302,6 @@ export const preservedOnReset = new Set([
303
302
  'drawNodeLabels',
304
303
  'showTreeOverview',
305
304
  'overviewHeight',
306
- 'autoTreeAreaWidth',
307
305
  'turnedOffTracks',
308
306
  'trackHeights',
309
307
  'hideGaps',
@@ -323,33 +321,71 @@ const seedingMarks = new Set(['collapse', 'focus', 'rotate']);
323
321
  // shared empty results, so observers don't see a fresh [] as a change
324
322
  const noDomains = [];
325
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
+ }
326
359
  /**
327
360
  * The rows a clade covers and the node an `mrca` names, or undefined when the
328
361
  * clade does not resolve: a tip name the tree does not have or has twice, or a
329
- * leaf count `tips` disagrees with. `index` and `rowNamesSet` are the memoized
330
- * 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.
331
365
  */
332
- function cladeRows(clade, root, index, rowNamesSet) {
366
+ function cladeRows(clade, root, index, tips) {
333
367
  if (clade.range) {
334
- 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);
335
369
  if (a === undefined || b === undefined) {
336
370
  return undefined;
337
371
  }
338
- const rows = a <= b ? [a, b] : [b, a];
339
- 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;
340
376
  }
341
377
  const node = clade.mrca ? mrca(root, clade.mrca, index) : undefined;
342
378
  if (!node) {
343
379
  return undefined;
344
380
  }
345
- const tips = leaves(node);
346
- if (tips.length !== clade.tips) {
381
+ const nodeTips = leaves(node);
382
+ if (nodeTips.length !== clade.tips) {
347
383
  return undefined;
348
384
  }
349
385
  let first = Infinity;
350
386
  let last = -Infinity;
351
- for (const tip of tips) {
352
- const row = rowNamesSet.get(tip.data.name);
387
+ for (const tip of nodeTips) {
388
+ const row = tips.get(tip.data.name)?.row;
353
389
  if (row !== undefined) {
354
390
  first = Math.min(first, row);
355
391
  last = Math.max(last, row);
@@ -360,15 +396,17 @@ function cladeRows(clade, root, index, rowNamesSet) {
360
396
  : undefined;
361
397
  }
362
398
  /**
363
- * Every clade that resolves, with its fill color settled, against the leaf
364
- * order `rowNamesSet` gives. The tree panel resolves against the displayed
365
- * rows and the overview against its own, so both take the tree they draw. A
366
- * `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.
367
404
  */
368
- function resolveClades(clades, root, rowNamesSet) {
405
+ function resolveClades(clades, root, displayed) {
369
406
  const index = leafIndex(root);
407
+ const tips = tipRows(displayed);
370
408
  return clades.flatMap(clade => {
371
- const resolved = cladeRows(clade, root, index, rowNamesSet);
409
+ const resolved = cladeRows(clade, root, index, tips);
372
410
  const seeding = seedingMarks.has(clade.mark);
373
411
  return resolved && !(seeding && resolved.nodeId === undefined)
374
412
  ? [
@@ -471,8 +509,8 @@ function rootTree(tree, treeRoot) {
471
509
  ? generateNodeIds(rerootTree(input, plain.get(target.data)))
472
510
  : tree;
473
511
  }
474
- // the inclusive tip indices a node covers, from the row-space extent
475
- // 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
476
514
  function tipRange(node) {
477
515
  return [Math.round(node.xMin - 0.5), Math.round(node.xMax - 0.5)];
478
516
  }
@@ -484,6 +522,15 @@ const featureChannels = new Set([
484
522
  // seqPos -> column indexes per row, keyed on the parse so they are garbage
485
523
  // collected with it. A computed would rebuild every row's index when read
486
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
+ }
487
534
  const seqPosIndexCache = new WeakMap();
488
535
  // A segment is a 1:1 run. A segment whose sides differ in length is malformed,
489
536
  // and the lookups treat it like an uncovered position.
@@ -769,9 +816,10 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
769
816
  /**
770
817
  * #property
771
818
  * where the view opens, in `highlights` coordinates: `{row, start,
772
- * end}` zooms onto residues of that row, `{start, end}` onto alignment
773
- * columns. Applied once the alignment and any tree file have loaded,
774
- * 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.
775
823
  */
776
824
  region: types.frozen(),
777
825
  /**
@@ -812,6 +860,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
812
860
  * of the snapshot so a link opened in the full app shows the toolbar.
813
861
  */
814
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,
815
869
  /**
816
870
  * #volatile
817
871
  */
@@ -1276,17 +1330,15 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1276
1330
  .views(self => ({
1277
1331
  /**
1278
1332
  * #getter
1279
- * 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
1280
1336
  */
1281
1337
  get hideGapsEffective() {
1282
1338
  return (self.hideGaps &&
1283
- (self.collapsed.length > 0 || self.allowedGappyness < 100));
1284
- },
1285
- /**
1286
- * #getter
1287
- */
1288
- get realAllowedGappyness() {
1289
- return this.hideGapsEffective ? self.allowedGappyness : 100;
1339
+ (self.collapsed.length > 0 ||
1340
+ self.showOnly !== undefined ||
1341
+ self.allowedGappyness < 100));
1290
1342
  },
1291
1343
  /**
1292
1344
  * #getter
@@ -1420,10 +1472,10 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1420
1472
  * the row table: extra fields per row, keyed by row name, which the
1421
1473
  * `encodings` channels read. It is stored as the JSON string
1422
1474
  * `data.treeMetadata`, the name that travels in existing links, so the
1423
- * inline size limit and `treeMetadataFilehandle` cover it. labelWidthMap
1424
- * reads it on every layout, so a malformed user-supplied file returns {}
1425
- * instead of throwing out of rendering, and every field is a string by
1426
- * the time a scale sorts it.
1475
+ * inline size limit and `treeMetadataFilehandle` cover it. The tree area
1476
+ * fit reads it on every layout, so a malformed user-supplied file
1477
+ * returns {} instead of throwing out of rendering, and every field is a
1478
+ * string by the time a scale sorts it.
1427
1479
  */
1428
1480
  get rowData() {
1429
1481
  const text = self.data.treeMetadata;
@@ -1535,7 +1587,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1535
1587
  if (self.clades.length === 0) {
1536
1588
  return noClades;
1537
1589
  }
1538
- 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);
1539
1591
  },
1540
1592
  /**
1541
1593
  * #getter
@@ -1544,11 +1596,93 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1544
1596
  * where no clade draws a bar or a label.
1545
1597
  */
1546
1598
  get cladeGutterWidth() {
1547
- return cladeGutterWidth({
1548
- clades: this.resolvedClades,
1549
- rowHeight: self.rowHeight,
1550
- fontSize: this.fontSize,
1551
- });
1599
+ return cladeGutterWidth(self.clades);
1600
+ },
1601
+ /**
1602
+ * #getter
1603
+ * whether a tree draws beside the labels: false for a file with no
1604
+ * tree, and with drawTree off
1605
+ */
1606
+ get treeDrawn() {
1607
+ return self.drawTree && !this.noTree;
1608
+ },
1609
+ /**
1610
+ * #getter
1611
+ * the leaf names of the whole tree and its depth in levels. The fit
1612
+ * reads these, so a collapse or a focus leaves the tree area as it is
1613
+ */
1614
+ get treeTips() {
1615
+ return treeTips(this.tree);
1616
+ },
1617
+ /**
1618
+ * #getter
1619
+ * the widest tip label at labelFitFontSize, measured whether or not a
1620
+ * zoom hides the labels
1621
+ */
1622
+ get maxTipLabelWidth() {
1623
+ return widestLabel(this.treeTips.names.map(name => this.rowDataOf(name)?.genome || name), labelFitFontSize);
1624
+ },
1625
+ /**
1626
+ * #getter
1627
+ * the px the tip labels take across the tree area: the widest label at
1628
+ * labelFitFontSize and the gaps either side of it, or 0 with drawLabels
1629
+ * off
1630
+ */
1631
+ get labelColumnWidth() {
1632
+ return self.drawLabels
1633
+ ? this.maxTipLabelWidth + (this.treeDrawn ? 10 : 6)
1634
+ : 0;
1635
+ },
1636
+ /**
1637
+ * #getter
1638
+ * the tree area width while treeAreaWidth is undefined: the label column
1639
+ * beside a tree that widens with its depth, up to maxTreeAreaShare of the
1640
+ * view, or the room the row panels leave where there is no alignment.
1641
+ * Where that binds, the tree keeps minTreeWidth and the renderer
1642
+ * ellipsizes the labels
1643
+ */
1644
+ get fittedTreeAreaWidth() {
1645
+ const chrome = self.marginLeft + this.cladeGutterWidth;
1646
+ const tree = this.treeDrawn
1647
+ ? (self.treeWidth ??
1648
+ clamp(this.treeTips.depth * pxPerTreeDepth, minTreeWidth, maxFitTreeWidth))
1649
+ : 0;
1650
+ const fit = chrome + tree + this.labelColumnWidth;
1651
+ const room = self.viewInitialized
1652
+ ? this.numColumns > 0
1653
+ ? self.width * maxTreeAreaShare
1654
+ : self.width - this.rowPanelsWidth - self.resizeHandleWidth
1655
+ : Infinity;
1656
+ const cap = Math.max(room, chrome + Math.min(tree, minTreeWidth));
1657
+ return Math.ceil(Math.min(fit, cap));
1658
+ },
1659
+ /**
1660
+ * #getter
1661
+ * the width of the area the tree and its labels draw in, px:
1662
+ * treeAreaWidth where set, the fit otherwise
1663
+ */
1664
+ get effectiveTreeAreaWidth() {
1665
+ return self.treeAreaWidth ?? this.fittedTreeAreaWidth;
1666
+ },
1667
+ /**
1668
+ * #getter
1669
+ * the width of the tree, px: treeWidth where set, otherwise the tree
1670
+ * area less the label column, which a zoom hiding the labels gives to
1671
+ * the tree
1672
+ */
1673
+ get effectiveTreeWidth() {
1674
+ const labels = this.showTreeText ? this.labelColumnWidth : 0;
1675
+ return (self.treeWidth ??
1676
+ Math.max(minTreeWidth, Math.floor(this.effectiveTreeAreaWidth -
1677
+ self.marginLeft -
1678
+ this.cladeGutterWidth -
1679
+ labels)));
1680
+ },
1681
+ /**
1682
+ * #getter
1683
+ */
1684
+ get showTreeText() {
1685
+ return self.drawLabels && self.rowHeight >= minLetterRowHeight;
1552
1686
  },
1553
1687
  /**
1554
1688
  * #getter
@@ -1621,21 +1755,16 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1621
1755
  if (!self.showTreeOverview) {
1622
1756
  return undefined;
1623
1757
  }
1624
- const root = buildTreeRoot(this.tree, {
1758
+ const layout = layoutTree(buildTreeRoot(this.tree, {
1625
1759
  collapsed: self.collapsed,
1626
1760
  rotated: self.rotated,
1627
1761
  order: self.treeOrder,
1628
- });
1629
- const numTips = leaves(root).length;
1630
- clusterLayout(root, numTips, 1);
1631
- const rootLen = Math.max(root.data.length || 0, 0);
1632
- const extent = maxLength(root) - rootLen;
1633
- setBrLength(root, -rootLen, extent ? 1 / extent : 0);
1762
+ }), { rowHeight: 1, treeWidth: 1 });
1634
1763
  return {
1635
- root,
1636
- numTips,
1637
- maxDepthToLeaf: calcDepthToLeaf(root),
1638
- 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,
1639
1768
  };
1640
1769
  },
1641
1770
  /**
@@ -1648,8 +1777,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1648
1777
  if (!layout || self.clades.length === 0) {
1649
1778
  return noClades;
1650
1779
  }
1651
- const rowNames = new Map(leaves(layout.root).map((leaf, index) => [leaf.data.name, index]));
1652
- return resolveClades(self.clades, layout.root, rowNames);
1780
+ return resolveClades(self.clades, hierarchy(this.tree, d => d.children), layout.root);
1653
1781
  },
1654
1782
  /**
1655
1783
  * #getter
@@ -1714,7 +1842,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1714
1842
  */
1715
1843
  get msaAreaWidth() {
1716
1844
  return (self.width -
1717
- self.treeAreaWidth -
1845
+ this.effectiveTreeAreaWidth -
1718
1846
  this.rowPanelsWidth -
1719
1847
  self.resizeHandleWidth);
1720
1848
  },
@@ -1724,13 +1852,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1724
1852
  * margin and the bracket gutter
1725
1853
  */
1726
1854
  get treeAreaWidthMinusMargin() {
1727
- return self.treeAreaWidth - self.marginLeft - this.cladeGutterWidth;
1855
+ return (this.effectiveTreeAreaWidth - self.marginLeft - this.cladeGutterWidth);
1728
1856
  },
1729
1857
  /**
1730
1858
  * #getter
1731
1859
  */
1732
1860
  get blanks() {
1733
- const { hideGapsEffective, realAllowedGappyness } = self;
1861
+ const { hideGapsEffective, allowedGappyness } = self;
1734
1862
  if (!hideGapsEffective) {
1735
1863
  return [];
1736
1864
  }
@@ -1746,7 +1874,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1746
1874
  // row that stops early counts as gapped for the remainder
1747
1875
  const numCols = this.MSA?.getWidth() ?? 0;
1748
1876
  const numRows = strs.length;
1749
- const threshold = Math.ceil((realAllowedGappyness / 100) * numRows);
1877
+ const threshold = Math.ceil((allowedGappyness / 100) * numRows);
1750
1878
  const blankCounts = new Uint32Array(numCols);
1751
1879
  for (let j = 0; j < numRows; j++) {
1752
1880
  const str = strs[j];
@@ -1828,11 +1956,19 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1828
1956
  }
1829
1957
  return index;
1830
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
+ },
1831
1968
  /**
1832
1969
  * #getter
1833
- * every sequence in the alignment, keyed by row name, including rows a
1834
- * collapsed clade hides. `rows` holds only the rows on screen; lookups
1835
- * 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
1836
1972
  */
1837
1973
  get rowMap() {
1838
1974
  const MSA = this.MSA;
@@ -1860,7 +1996,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1860
1996
  * #getter
1861
1997
  */
1862
1998
  get fontSize() {
1863
- return Math.min(Math.max(6, self.rowHeight - 3), 18);
1999
+ return fontSizeFor(self.rowHeight);
1864
2000
  },
1865
2001
  /**
1866
2002
  * #getter
@@ -1875,10 +2011,11 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1875
2011
  */
1876
2012
  get sequenceType() {
1877
2013
  const letters = this.colStats.lettersPresent;
1878
- // isDna already excludes U (not in the DNA set) and isRna excludes T,
1879
- // so the set membership alone disambiguates the two
1880
- const dna = new Set(['A', 'C', 'G', 'T', 'N']);
1881
- 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]);
1882
2019
  const isDna = letters.size > 0 && [...letters].every(l => dna.has(l));
1883
2020
  const isRna = letters.size > 0 && [...letters].every(l => rna.has(l));
1884
2021
  return isDna ? 'dna' : isRna ? 'rna' : 'amino';
@@ -1959,17 +2096,18 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
1959
2096
  /**
1960
2097
  * #getter
1961
2098
  * `root` laid out on new nodes in row and branch-length units, read in
1962
- * pixels through `rowHeight` and `treeWidth`. A zoom leaves it and every
1963
- * node in it as they are, and an observer reading a node's `x` or `len`
1964
- * observes the size it scales by.
2099
+ * pixels through `rowHeight` and `effectiveTreeWidth`. A zoom leaves it
2100
+ * and every node in it as they are, and an observer reading a node's `x`
2101
+ * or `len` observes the size it scales by.
1965
2102
  */
1966
2103
  get treeLayout() {
2104
+ const treeWidth = () => this.effectiveTreeWidth;
1967
2105
  return layoutTree(this.root, {
1968
2106
  get rowHeight() {
1969
2107
  return self.rowHeight;
1970
2108
  },
1971
2109
  get treeWidth() {
1972
- return self.treeWidth;
2110
+ return treeWidth();
1973
2111
  },
1974
2112
  });
1975
2113
  },
@@ -2000,14 +2138,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2000
2138
  get rootToTipLength() {
2001
2139
  return this.treeLayout.rootToTipLength;
2002
2140
  },
2003
- /**
2004
- * #getter
2005
- * x-position of the farthest tip in a phylogram, px: treeWidth, or 0 for
2006
- * a tree with no branch lengths (drawn as a cladogram)
2007
- */
2008
- get maxBranchLength() {
2009
- return this.rootToTipLength ? self.treeWidth : 0;
2010
- },
2011
2141
  /**
2012
2142
  * #getter
2013
2143
  * pixels per unit of branch length in the phylogram layout, 0 in
@@ -2015,7 +2145,9 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2015
2145
  */
2016
2146
  get pxPerBranchLength() {
2017
2147
  const max = this.rootToTipLength;
2018
- return this.showBranchLenEffective && max ? self.treeWidth / max : 0;
2148
+ return this.showBranchLenEffective && max
2149
+ ? this.effectiveTreeWidth / max
2150
+ : 0;
2019
2151
  },
2020
2152
  /**
2021
2153
  * #getter
@@ -2056,12 +2188,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2056
2188
  self.colWidth >= minLetterColWidth &&
2057
2189
  self.colWidth > self.rowHeight / 2);
2058
2190
  },
2059
- /**
2060
- * #getter
2061
- */
2062
- get showTreeText() {
2063
- return self.drawLabels && self.rowHeight >= minLetterRowHeight;
2064
- },
2065
2191
  }))
2066
2192
  .views(self => {
2067
2193
  // a spec is a frozen value, so it keys its computed until setColumnTracks
@@ -2167,7 +2293,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2167
2293
  data: content?.data,
2168
2294
  arcs: content?.arcs,
2169
2295
  },
2170
- ReactComponent: TrackBlocks,
2171
2296
  };
2172
2297
  });
2173
2298
  columnTrackModels.set(track, computedModel);
@@ -2183,48 +2308,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2183
2308
  };
2184
2309
  })
2185
2310
  .views(self => ({
2186
- /**
2187
- * #getter
2188
- */
2189
- get labelWidthMap() {
2190
- const { showTreeText, leaves } = self;
2191
- // gated on the renderer's label condition, so hidden labels reserve no
2192
- // gutter. Measured once at a reference size and scaled by
2193
- // labelWidthScale: re-measuring per vertical-zoom frame cost ~200ms on a
2194
- // 50k-leaf tree
2195
- return showTreeText
2196
- ? new Map(leaves.map(node => {
2197
- const { name } = node.data;
2198
- // `||`, matching renderTreeLabels: an empty genome falls back
2199
- // to the row name
2200
- const displayName = self.rowDataOf(name)?.genome || name;
2201
- return [
2202
- name,
2203
- measureTextCanvas(displayName, labelReferenceFontSize),
2204
- ];
2205
- }))
2206
- : new Map();
2207
- },
2208
- /**
2209
- * #getter
2210
- * factor turning a labelWidthMap entry into its width at the current
2211
- * font size
2212
- */
2213
- get labelWidthScale() {
2214
- return self.fontSize / labelReferenceFontSize;
2215
- },
2216
- get labelsWidth() {
2217
- // a loop, not Math.max(...): spreading passes one argument per leaf,
2218
- // and the ~125k argument limit throws RangeError on the 230k-tip COVID
2219
- // tree
2220
- let max = 0;
2221
- for (const width of this.labelWidthMap.values()) {
2222
- if (width > max) {
2223
- max = width;
2224
- }
2225
- }
2226
- return max * this.labelWidthScale;
2227
- },
2228
2311
  /**
2229
2312
  * #getter
2230
2313
  */
@@ -2267,7 +2350,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2267
2350
  data: hideGapsEffective ? skipBlanks(blanks, t.data) : t.data,
2268
2351
  height: rowHeight,
2269
2352
  },
2270
- ReactComponent: TrackBlocks,
2271
2353
  }));
2272
2354
  },
2273
2355
  /**
@@ -2289,7 +2371,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2289
2371
  height: self.trackHeight('arc'),
2290
2372
  arcs,
2291
2373
  },
2292
- ReactComponent: TrackBlocks,
2293
2374
  },
2294
2375
  ]
2295
2376
  : [];
@@ -2336,7 +2417,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2336
2417
  heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
2337
2418
  height: self.trackHeight(model.kind),
2338
2419
  },
2339
- ReactComponent: TrackBlocks,
2340
2420
  }));
2341
2421
  },
2342
2422
  get tracks() {
@@ -2372,7 +2452,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2372
2452
  * @returns The character at that position
2373
2453
  */
2374
2454
  visibleColToRowLetter(rowName, visibleCol) {
2375
- return self.rowMap.get(rowName)?.[this.visibleColToGlobalCol(visibleCol)];
2455
+ return self.rowSequence(rowName)?.[this.visibleColToGlobalCol(visibleCol)];
2376
2456
  },
2377
2457
  /**
2378
2458
  * #method
@@ -2447,7 +2527,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2447
2527
  * @returns The global column index in the full MSA, or undefined
2448
2528
  */
2449
2529
  seqPosToGlobalCol(rowName, seqPos) {
2450
- const seq = self.rowMap.get(rowName);
2530
+ const seq = self.rowSequence(rowName);
2451
2531
  if (seq === undefined) {
2452
2532
  return undefined;
2453
2533
  }
@@ -2663,7 +2743,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2663
2743
  * or when the tree area is too narrow for one
2664
2744
  */
2665
2745
  get treeScaleBar() {
2666
- return scaleBarLength(self.pxPerBranchLength, self.treeAreaWidth - self.marginLeft * 2);
2746
+ return scaleBarLength(self.pxPerBranchLength, self.effectiveTreeAreaWidth - self.marginLeft * 2);
2667
2747
  },
2668
2748
  /**
2669
2749
  * #getter
@@ -2713,7 +2793,8 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2713
2793
  return self.allAnnotations.filter(r => !self.turnedOffFeatures.get(r.accession));
2714
2794
  },
2715
2795
  get annotationsByRow() {
2716
- return groupBy(this.filteredAnnotations, r => r.id);
2796
+ const { rowNamesSet } = self;
2797
+ return groupBy(this.filteredAnnotations, r => annotationRowName(r.id, rowNamesSet));
2717
2798
  },
2718
2799
  }))
2719
2800
  .views(self => ({
@@ -2801,7 +2882,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2801
2882
  const { annotations, rowNamesSet } = self;
2802
2883
  return self.dataInitialized &&
2803
2884
  annotations.length > 0 &&
2804
- !annotations.some(a => rowNamesSet.has(a.id))
2885
+ !annotations.some(a => rowNamesSet.has(annotationRowName(a.id, rowNamesSet)))
2805
2886
  ? [
2806
2887
  `0 of ${annotations.length} annotations name a row in this alignment`,
2807
2888
  ]
@@ -2926,10 +3007,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2926
3007
  * #action
2927
3008
  * swap in a different tree over the same alignment. Clears `collapsed`,
2928
3009
  * `rotated` and `showOnly`, since path-derived node ids (node-0-0-1) from the old
2929
- * 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
2930
3013
  */
2931
3014
  replaceTree(newick) {
2932
3015
  transaction(() => {
3016
+ self.setTreeFilehandle(undefined);
2933
3017
  self.collapsed.clear();
2934
3018
  self.rotated.clear();
2935
3019
  self.setShowOnly(undefined);
@@ -2951,30 +3035,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
2951
3035
  zoomAtCenter(scaleFactor, axis = 'both') {
2952
3036
  this.zoomToPos(scaleFactor, self.msaCanvasWidth / 2, self.msaAreaHeight / 2, axis);
2953
3037
  },
2954
- /**
2955
- * #action
2956
- */
2957
- zoomOutHorizontal() {
2958
- this.zoomAtCenter(0.75, 'horizontal');
2959
- },
2960
- /**
2961
- * #action
2962
- */
2963
- zoomInHorizontal() {
2964
- this.zoomAtCenter(1.5, 'horizontal');
2965
- },
2966
- /**
2967
- * #action
2968
- */
2969
- zoomInVertical() {
2970
- this.zoomAtCenter(1.5, 'vertical');
2971
- },
2972
- /**
2973
- * #action
2974
- */
2975
- zoomOutVertical() {
2976
- this.zoomAtCenter(0.75, 'vertical');
2977
- },
2978
3038
  /**
2979
3039
  * #action
2980
3040
  */
@@ -3041,15 +3101,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3041
3101
  setAnnotations(annotations) {
3042
3102
  self.annotations = annotations;
3043
3103
  },
3044
- /**
3045
- * #action
3046
- * set the overlay from raw InterProScan results keyed by row name. Kept
3047
- * for downstream plugins that hold the EBI wire format; new code should
3048
- * adapt to Annotation[] and call setAnnotations.
3049
- */
3050
- setDomains(data) {
3051
- this.setAnnotations(data ? interProScanToAnnotations(data) : []);
3052
- },
3053
3104
  applyGFFText(gffText) {
3054
3105
  this.setAnnotations(gffToAnnotations(parseGFF(gffText)));
3055
3106
  },
@@ -3130,7 +3181,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3130
3181
  column: column + 1,
3131
3182
  row,
3132
3183
  residue: self.visibleColToSeqPosOneBased(row, visibleCol),
3133
- letter: self.rowMap.get(row)?.[column],
3184
+ letter: self.rowSequence(row)?.[column],
3134
3185
  };
3135
3186
  },
3136
3187
  /**
@@ -3295,7 +3346,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3295
3346
  : visibleDomainTypes.map(d => ({
3296
3347
  id: d.accession,
3297
3348
  label: d.name,
3298
- color: fillPalette[d.accession],
3349
+ color: d.color ?? fillPalette[d.accession],
3299
3350
  })), 'domains');
3300
3351
  }
3301
3352
  for (const { channel, field, legend } of this.resolvedEncodings) {
@@ -3531,7 +3582,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3531
3582
  * to `end`, gaps included
3532
3583
  */
3533
3584
  get selectionFasta() {
3534
- const { rowNames, rowMap } = self;
3585
+ const { rowNames } = self;
3535
3586
  const resolved = this.resolvedSelection;
3536
3587
  const span = this.selectionFileSpan;
3537
3588
  if (!resolved || !span) {
@@ -3540,7 +3591,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3540
3591
  const rows = resolved.rowRuns
3541
3592
  .flatMap(([first, last]) => rowNames.slice(first, last + 1))
3542
3593
  .flatMap(name => {
3543
- const seq = rowMap.get(name);
3594
+ const seq = self.rowSequence(name);
3544
3595
  return seq === undefined ? [] : [[name, seq]];
3545
3596
  });
3546
3597
  return blockFasta(rows, span.start, span.end);
@@ -3619,7 +3670,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3619
3670
  this.visibleDomainTypes.map(d => ({
3620
3671
  id: d.accession,
3621
3672
  label: d.name,
3622
- color: fillPalette[d.accession],
3673
+ color: d.color ?? fillPalette[d.accession],
3623
3674
  })),
3624
3675
  };
3625
3676
  }
@@ -3768,6 +3819,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3768
3819
  setHideHeader(arg) {
3769
3820
  self.hideHeader = arg;
3770
3821
  },
3822
+ /**
3823
+ * #action
3824
+ */
3825
+ setImportForm(arg) {
3826
+ self.importForm = arg;
3827
+ },
3771
3828
  /**
3772
3829
  * #action
3773
3830
  * focus the subtree a click `y` pixels down the tree overview lands on.
@@ -3875,6 +3932,24 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3875
3932
  self.scrollX = clamp(-span.startCol * self.colWidth, self.maxScrollX, 0);
3876
3933
  });
3877
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
+ },
3878
3953
  /**
3879
3954
  * #action
3880
3955
  * zoom and scroll so the selected columns fill the alignment's width,
@@ -3975,7 +4050,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
3975
4050
  self.viewInitialized &&
3976
4051
  self.numColumns > 0 &&
3977
4052
  !(self.treeFilehandle && !self.data.tree)) {
3978
- this.zoomToRegion(region);
4053
+ this.revealRegion(region);
3979
4054
  self.setRegion(undefined);
3980
4055
  }
3981
4056
  }));
@@ -4037,7 +4112,9 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4037
4112
  };
4038
4113
  try {
4039
4114
  setLoading?.(true);
4040
- self.setError(undefined);
4115
+ if (!optional) {
4116
+ self.setError(undefined);
4117
+ }
4041
4118
  const text = await fetchTextWithProgress(openLocation(filehandle), status => {
4042
4119
  if (isCurrent()) {
4043
4120
  self.setStatus(status);
@@ -4166,34 +4243,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
4166
4243
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4167
4244
  self.columns;
4168
4245
  }));
4169
- // with autoTreeAreaWidth and no tree drawn, size the tree area to the
4170
- // labels. Gated on noTree/!drawTree so it does not loop with the
4171
- // treeWidth autorun below
4172
- addDisposer(self, autorun(() => {
4173
- if (self.autoTreeAreaWidth &&
4174
- (self.noTree || !self.drawTree) &&
4175
- self.labelsWidth) {
4176
- self.setTreeAreaWidth(self.labelsWidth + self.marginLeft + 12 + self.cladeGutterWidth);
4177
- }
4178
- }));
4179
- // treeWidth follows the tree area less the labels. A non-default
4180
- // snapshot treeWidth (jbrowse-plugin-msaview opens at 100 in a 200px
4181
- // area) is kept until the tree area changes. Not a getter, because
4182
- // labelsWidth is measured off leaves laid out against treeWidth.
4183
- let pinnedAreaWidth = self.treeWidth === defaultTreeWidth ? undefined : self.treeAreaWidth;
4184
- addDisposer(self, autorun(() => {
4185
- const areaWidth = self.treeAreaWidth;
4186
- const labelsWidth = self.labelsWidth;
4187
- if (pinnedAreaWidth === areaWidth) {
4188
- return;
4189
- }
4190
- pinnedAreaWidth = undefined;
4191
- self.setTreeWidth(Math.max(50, areaWidth -
4192
- labelsWidth -
4193
- 10 -
4194
- self.marginLeft -
4195
- self.cladeGutterWidth));
4196
- }));
4197
4246
  },
4198
4247
  }))
4199
4248
  .postProcessSnapshot(({ data, columnTracks, ...rest }) => ({