react-msaview 8.3.0 → 8.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/bundle/index.js +499 -295
  2. package/bundle/index.js.map +4 -4
  3. package/dist/blockFasta.d.ts +6 -0
  4. package/dist/blockFasta.js +12 -0
  5. package/dist/blockFasta.js.map +1 -0
  6. package/dist/colorSchemes.d.ts +7 -0
  7. package/dist/colorSchemes.js +15 -0
  8. package/dist/colorSchemes.js.map +1 -1
  9. package/dist/components/MSAView.js +15 -9
  10. package/dist/components/MSAView.js.map +1 -1
  11. package/dist/components/MSAViewer.d.ts +28 -7
  12. package/dist/components/MSAViewer.js +61 -22
  13. package/dist/components/MSAViewer.js.map +1 -1
  14. package/dist/components/MetadataTable.d.ts +5 -0
  15. package/dist/components/MetadataTable.js +29 -0
  16. package/dist/components/MetadataTable.js.map +1 -0
  17. package/dist/components/Track.js +11 -2
  18. package/dist/components/Track.js.map +1 -1
  19. package/dist/components/dialogs/MetadataDialog.js +5 -6
  20. package/dist/components/dialogs/MetadataDialog.js.map +1 -1
  21. package/dist/components/header/ColorSchemeMenu.d.ts +6 -0
  22. package/dist/components/header/ColorSchemeMenu.js +23 -5
  23. package/dist/components/header/ColorSchemeMenu.js.map +1 -1
  24. package/dist/components/header/GoToBox.d.ts +338 -0
  25. package/dist/components/header/GoToBox.js +31 -0
  26. package/dist/components/header/GoToBox.js.map +1 -0
  27. package/dist/components/header/Header.js +4 -2
  28. package/dist/components/header/Header.js.map +1 -1
  29. package/dist/components/header/SelectionMenu.d.ts +12 -0
  30. package/dist/components/header/SelectionMenu.js +72 -0
  31. package/dist/components/header/SelectionMenu.js.map +1 -0
  32. package/dist/components/header/goTo.d.ts +20 -0
  33. package/dist/components/header/goTo.js +69 -0
  34. package/dist/components/header/goTo.js.map +1 -0
  35. package/dist/components/msa/MSACanvas.js +13 -2
  36. package/dist/components/msa/MSACanvas.js.map +1 -1
  37. package/dist/components/msa/MSACanvasBlock.js +5 -1
  38. package/dist/components/msa/MSACanvasBlock.js.map +1 -1
  39. package/dist/components/msa/msaKeys.js +6 -0
  40. package/dist/components/msa/msaKeys.js.map +1 -1
  41. package/dist/components/msa/msaRaster.js +1 -0
  42. package/dist/components/msa/msaRaster.js.map +1 -1
  43. package/dist/components/msa/renderHighlights.d.ts +14 -3
  44. package/dist/components/msa/renderHighlights.js +29 -4
  45. package/dist/components/msa/renderHighlights.js.map +1 -1
  46. package/dist/components/msa/tileColor.js +9 -8
  47. package/dist/components/msa/tileColor.js.map +1 -1
  48. package/dist/components/msa/useMsaBlockMouse.d.ts +1 -1
  49. package/dist/components/msa/useMsaBlockMouse.js +18 -11
  50. package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
  51. package/dist/components/overlayColors.d.ts +2 -0
  52. package/dist/components/overlayColors.js +3 -0
  53. package/dist/components/overlayColors.js.map +1 -1
  54. package/dist/components/selectionDrag.d.ts +17 -0
  55. package/dist/components/selectionDrag.js +30 -0
  56. package/dist/components/selectionDrag.js.map +1 -0
  57. package/dist/components/tracks/useTrackHover.d.ts +3 -1
  58. package/dist/components/tracks/useTrackHover.js +26 -6
  59. package/dist/components/tracks/useTrackHover.js.map +1 -1
  60. package/dist/components/tree/dialogs/TreeNodeInfoDialog.js +9 -8
  61. package/dist/components/tree/dialogs/TreeNodeInfoDialog.js.map +1 -1
  62. package/dist/constants.d.ts +1 -1
  63. package/dist/constants.js +6 -6
  64. package/dist/constants.js.map +1 -1
  65. package/dist/element.d.ts +7 -3
  66. package/dist/element.js +32 -4
  67. package/dist/element.js.map +1 -1
  68. package/dist/featuresLayer.d.ts +8 -0
  69. package/dist/featuresLayer.js +42 -0
  70. package/dist/featuresLayer.js.map +1 -0
  71. package/dist/index.d.ts +1 -1
  72. package/dist/model/DataModel.d.ts +1 -1
  73. package/dist/model/DataModel.js +3 -3
  74. package/dist/model/DataModel.js.map +1 -1
  75. package/dist/model/msaModel.d.ts +14 -1
  76. package/dist/model/msaModel.js +17 -1
  77. package/dist/model/msaModel.js.map +1 -1
  78. package/dist/model.d.ts +242 -187
  79. package/dist/model.js +264 -45
  80. package/dist/model.js.map +1 -1
  81. package/dist/svgTestUtil.d.ts +284 -336
  82. package/dist/types.d.ts +50 -0
  83. package/dist/version.d.ts +1 -1
  84. package/dist/version.js +1 -1
  85. package/package.json +6 -6
  86. package/src/blockFasta.ts +18 -0
  87. package/src/colorSchemes.ts +20 -0
  88. package/src/components/MSAView.tsx +16 -9
  89. package/src/components/MSAViewer.tsx +95 -26
  90. package/src/components/MetadataTable.tsx +55 -0
  91. package/src/components/Track.tsx +14 -1
  92. package/src/components/dialogs/MetadataDialog.tsx +9 -8
  93. package/src/components/header/ColorSchemeMenu.tsx +31 -12
  94. package/src/components/header/GoToBox.tsx +64 -0
  95. package/src/components/header/Header.tsx +6 -2
  96. package/src/components/header/SelectionMenu.tsx +117 -0
  97. package/src/components/header/goTo.ts +75 -0
  98. package/src/components/msa/MSACanvas.tsx +12 -2
  99. package/src/components/msa/MSACanvasBlock.tsx +5 -1
  100. package/src/components/msa/msaKeys.ts +6 -0
  101. package/src/components/msa/msaRaster.ts +1 -0
  102. package/src/components/msa/renderHighlights.ts +46 -4
  103. package/src/components/msa/tileColor.ts +9 -8
  104. package/src/components/msa/useMsaBlockMouse.ts +24 -27
  105. package/src/components/overlayColors.ts +4 -0
  106. package/src/components/selectionDrag.ts +42 -0
  107. package/src/components/tracks/useTrackHover.ts +28 -6
  108. package/src/components/tree/dialogs/TreeNodeInfoDialog.tsx +14 -16
  109. package/src/constants.ts +6 -6
  110. package/src/element.ts +45 -5
  111. package/src/featuresLayer.ts +48 -0
  112. package/src/index.ts +5 -0
  113. package/src/model/DataModel.ts +3 -3
  114. package/src/model/msaModel.ts +19 -1
  115. package/src/model.ts +313 -55
  116. package/src/types.ts +58 -0
  117. package/src/version.ts +1 -1
  118. package/dist/components/header/RowSearch.d.ts +0 -6
  119. package/dist/components/header/RowSearch.js +0 -15
  120. package/dist/components/header/RowSearch.js.map +0 -1
  121. package/dist/components/header/revealRow.d.ts +0 -3
  122. package/dist/components/header/revealRow.js +0 -15
  123. package/dist/components/header/revealRow.js.map +0 -1
  124. package/src/components/header/RowSearch.tsx +0 -34
  125. package/src/components/header/revealRow.ts +0 -17
package/src/model.ts CHANGED
@@ -26,9 +26,10 @@ import {
26
26
  parseNewick,
27
27
  } from 'msa-parsers'
28
28
 
29
+ import { blockFasta } from './blockFasta.ts'
29
30
  import { calculateBlocks } from './calculateBlocks.ts'
30
31
  import { clustalXColumnColors } from './clustalX.ts'
31
- import colorSchemes from './colorSchemes.ts'
32
+ import colorSchemes, { letterColorTable } from './colorSchemes.ts'
32
33
  import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.ts'
33
34
  import { columnStats } from './columnStats.ts'
34
35
  import { packDomainLanes } from './components/msa/packDomainLanes.ts'
@@ -58,7 +59,7 @@ import {
58
59
  defaultTreeWidth,
59
60
  labelReferenceFontSize,
60
61
  maxCellSize,
61
- maxInlineSnapshotBytes,
62
+ maxInlineSnapshotBytes as defaultMaxInlineSnapshotBytes,
62
63
  maxNeighborJoiningRows,
63
64
  minColWidth,
64
65
  minLetterColWidth,
@@ -74,6 +75,7 @@ import {
74
75
  import { createPaletteMap } from './createPaletteMap.ts'
75
76
  import { exportFileName } from './exportFileName.ts'
76
77
  import { featureField, featureName } from './featureFields.ts'
78
+ import { featuresToAnnotations } from './featuresLayer.ts'
77
79
  import { fetchTextWithProgress, isAbortError } from './fetchUtils.ts'
78
80
  import { flatToTree } from './flatToTree.ts'
79
81
  import {
@@ -142,9 +144,11 @@ import type {
142
144
  DomainBand,
143
145
  Encoding,
144
146
  EncodingChannel,
147
+ Feature,
145
148
  Highlight,
146
149
  Legend,
147
150
  LegendEntry,
151
+ MsaSelection,
148
152
  NodeWithIds,
149
153
  Region,
150
154
  ResidueMapping,
@@ -153,12 +157,14 @@ import type {
153
157
  ResolvedEncoding,
154
158
  ResolvedFeaturePanel,
155
159
  ResolvedHighlight,
160
+ ResolvedSelection,
156
161
  TreeOrder,
157
162
  TreeRoot,
158
163
  ResolvedRowPanel,
159
164
  ResolvedStripPanel,
160
165
  RowFeaturesSpec,
161
166
  RowPanelSpan,
167
+ RowDataInput,
162
168
  RowPanelSpec,
163
169
  RowResidue,
164
170
  StructureResidue,
@@ -225,6 +231,23 @@ function rowPanelWidth(panel: RowPanelSpec, view: { rowHeight: number }) {
225
231
  )
226
232
  }
227
233
 
234
+ // one row of the row table as the channels read it. A number or a boolean
235
+ // reads as its string, the way a GFF value does, and null or an object drops
236
+ function stringFields(row: unknown) {
237
+ if (typeof row !== 'object' || row === null) {
238
+ return undefined
239
+ }
240
+ const fields: Record<string, string> = {}
241
+ for (const [field, value] of Object.entries(row)) {
242
+ if (typeof value === 'string') {
243
+ fields[field] = value
244
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
245
+ fields[field] = String(value)
246
+ }
247
+ }
248
+ return fields
249
+ }
250
+
228
251
  // the color each row takes from a field of the row table under a scale
229
252
  function colorByRow(
230
253
  rowData: Record<string, Record<string, string> | undefined>,
@@ -425,18 +448,23 @@ function featurePanelSpans({
425
448
  )
426
449
  }
427
450
 
428
- // a scale over the values a field takes across the features drawn
451
+ // a scale over the values a field takes across every feature loaded, so hiding
452
+ // a type in the filter dialog leaves the rest in their colors, and a legend of
453
+ // the values the features drawn carry
429
454
  function resolveFeatureScale(
430
455
  field: string,
431
456
  scale: ScaleSpec | undefined,
432
457
  annotations: Annotation[],
458
+ drawn: Annotation[],
433
459
  ) {
460
+ const valuesOf = (list: Annotation[]) =>
461
+ list.map(a => featureField(a, field)).filter(notEmpty)
462
+ const resolved = resolveScale(scale, valuesOf(annotations))
463
+ const shown = new Set(valuesOf(drawn))
434
464
  return {
435
465
  field,
436
- ...resolveScale(
437
- scale,
438
- annotations.map(a => featureField(a, field)).filter(notEmpty),
439
- ),
466
+ ...resolved,
467
+ legend: resolved.legend.filter(entry => shown.has(entry.id)),
440
468
  }
441
469
  }
442
470
 
@@ -457,7 +485,10 @@ function columnTrackSizes(tracks?: readonly ColumnTrackSpec[]) {
457
485
  return (tracks ?? []).map(columnTrackSize)
458
486
  }
459
487
 
460
- function smallColumnTracks(tracks?: ColumnTrackSpec[]) {
488
+ function smallColumnTracks(
489
+ tracks: ColumnTrackSpec[] | undefined,
490
+ maxInlineSnapshotBytes: number,
491
+ ) {
461
492
  const sizes = columnTrackSizes(tracks)
462
493
  const kept = tracks?.filter((_t, i) => sizes[i]! <= maxInlineSnapshotBytes)
463
494
  return kept?.length ? { columnTracks: kept } : {}
@@ -484,6 +515,7 @@ export const preservedOnReset = new Set([
484
515
  'scrollZoomAxis',
485
516
  'bgColor',
486
517
  'colorSchemeName',
518
+ 'customColorScheme',
487
519
  'showColumnStats',
488
520
  'drawLabels',
489
521
  'labelsAlignRight',
@@ -752,6 +784,20 @@ function inRanges(ranges: [number, number][] | undefined, position: number) {
752
784
  return !!ranges?.some(([start, end]) => position >= start && position <= end)
753
785
  }
754
786
 
787
+ // sorted integers as inclusive [first, last] runs of consecutive values
788
+ function consecutiveRuns(sorted: number[]) {
789
+ const runs: [number, number][] = []
790
+ for (const n of sorted) {
791
+ const last = runs.at(-1)
792
+ if (last && n <= last[1] + 1) {
793
+ last[1] = Math.max(last[1], n)
794
+ } else {
795
+ runs.push([n, n])
796
+ }
797
+ }
798
+ return runs
799
+ }
800
+
755
801
  // the value every one of a node's children reports, or undefined where one of
756
802
  // them has no value or they disagree
757
803
  function sharedValue(values: (string | undefined)[]) {
@@ -794,8 +840,15 @@ function preorder(tree: NodeWithIds) {
794
840
  * const root = RootModel.create({})
795
841
  * root.view.setData({ msa: '>seq1\nACGT\n>seq2\nACGT' })
796
842
  * ```
843
+ *
844
+ * `maxInlineSnapshotBytes` is the largest pasted or local document, and the
845
+ * largest data track, the snapshot keeps; a larger one stays in the live model
846
+ * and `unshareableData` names it. The standalone app, whose links carry the
847
+ * snapshot in the URL fragment, raises it.
797
848
  */
798
- function stateModelFactory() {
849
+ function stateModelFactory({
850
+ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBytes,
851
+ }: { maxInlineSnapshotBytes?: number } = {}) {
799
852
  return types
800
853
  .compose(
801
854
  DialogQueueSessionMixin(),
@@ -991,7 +1044,7 @@ function stateModelFactory() {
991
1044
  * data from the loaded tree/msa/treeMetadata, generally loaded by
992
1045
  * autorun
993
1046
  */
994
- data: types.optional(DataModelF(), {}),
1047
+ data: types.optional(DataModelF(maxInlineSnapshotBytes), {}),
995
1048
 
996
1049
  /**
997
1050
  * #property
@@ -1022,6 +1075,21 @@ function stateModelFactory() {
1022
1075
  * Persists in the snapshot and the URL.
1023
1076
  */
1024
1077
  highlights: stripDefault(types.array(types.frozen<Highlight>()), []),
1078
+ /**
1079
+ * #property
1080
+ * features on the rows as data, `{row, start, end, name, ...}` in
1081
+ * residues of each row, drawn with the GFF's annotations. Persists in
1082
+ * the snapshot and the URL.
1083
+ */
1084
+ features: stripDefault(types.array(types.frozen<Feature>()), []),
1085
+ /**
1086
+ * #property
1087
+ * the block the reader selected: `{start, end}` columns of the file,
1088
+ * 1-based inclusive like a column highlight, and `rows` by name, every
1089
+ * row where absent. Undefined until something is selected, so a view
1090
+ * without one adds nothing to the shared URL.
1091
+ */
1092
+ selection: types.frozen<MsaSelection | undefined>(),
1025
1093
  /**
1026
1094
  * #property
1027
1095
  * where the view opens, in `highlights` coordinates: `{row, start,
@@ -1322,6 +1390,33 @@ function stateModelFactory() {
1322
1390
  setHighlights(highlights: Highlight[]) {
1323
1391
  self.highlights.replace(highlights)
1324
1392
  },
1393
+ /**
1394
+ * #action
1395
+ */
1396
+ setFeatures(features: Feature[]) {
1397
+ self.features.replace(features)
1398
+ },
1399
+ /**
1400
+ * #action
1401
+ * select a block, in `selection` coordinates. `start` and `end` may come
1402
+ * in either order.
1403
+ */
1404
+ setSelection(selection?: MsaSelection) {
1405
+ const next = selection && {
1406
+ start: Math.min(selection.start, selection.end),
1407
+ end: Math.max(selection.start, selection.end),
1408
+ ...(selection.rows ? { rows: selection.rows } : {}),
1409
+ }
1410
+ if (!compareStructural(next, self.selection)) {
1411
+ self.selection = next
1412
+ }
1413
+ },
1414
+ /**
1415
+ * #action
1416
+ */
1417
+ clearSelection() {
1418
+ this.setSelection(undefined)
1419
+ },
1325
1420
 
1326
1421
  /**
1327
1422
  * #action
@@ -1493,28 +1588,28 @@ function stateModelFactory() {
1493
1588
  * #action
1494
1589
  */
1495
1590
  setMSAFilehandle(msaFilehandle?: FileLocationType) {
1496
- self.msaFilehandle = msaFilehandle
1591
+ self.msaFilehandle = cast(msaFilehandle)
1497
1592
  },
1498
1593
 
1499
1594
  /**
1500
1595
  * #action
1501
1596
  */
1502
1597
  setTreeFilehandle(treeFilehandle?: FileLocationType) {
1503
- self.treeFilehandle = treeFilehandle
1598
+ self.treeFilehandle = cast(treeFilehandle)
1504
1599
  },
1505
1600
 
1506
1601
  /**
1507
1602
  * #action
1508
1603
  */
1509
1604
  setTreeMetadataFilehandle(treeMetadataFilehandle?: FileLocationType) {
1510
- self.treeMetadataFilehandle = treeMetadataFilehandle
1605
+ self.treeMetadataFilehandle = cast(treeMetadataFilehandle)
1511
1606
  },
1512
1607
 
1513
1608
  /**
1514
1609
  * #action
1515
1610
  */
1516
1611
  setGFFFilehandle(gffFilehandle?: FileLocationType) {
1517
- self.gffFilehandle = gffFilehandle
1612
+ self.gffFilehandle = cast(gffFilehandle)
1518
1613
  },
1519
1614
 
1520
1615
  /**
@@ -1543,9 +1638,9 @@ function stateModelFactory() {
1543
1638
  * store the GFF text in the snapshot like the alignment and tree. The
1544
1639
  * parsed annotations are volatile and a blob filehandle is cleared once
1545
1640
  * read, so the text is the only persisted copy. An autorun parses it
1546
- * into annotations.
1641
+ * into annotations, and clears the ones it drew when the text goes.
1547
1642
  */
1548
- setGFF(result: string) {
1643
+ setGFF(result?: string) {
1549
1644
  self.data.setGFF(result)
1550
1645
  },
1551
1646
  }))
@@ -1567,11 +1662,21 @@ function stateModelFactory() {
1567
1662
  get realAllowedGappyness() {
1568
1663
  return this.hideGapsEffective ? self.allowedGappyness : 100
1569
1664
  },
1665
+ /**
1666
+ * #getter
1667
+ * the annotations the overlay, the legend and the encodings draw: the
1668
+ * GFF's, then the `features` layer's
1669
+ */
1670
+ get allAnnotations(): Annotation[] {
1671
+ return self.features.length > 0
1672
+ ? [...self.annotations, ...featuresToAnnotations(self.features)]
1673
+ : self.annotations
1674
+ },
1570
1675
  /**
1571
1676
  * #getter
1572
1677
  */
1573
1678
  get actuallyShowDomains() {
1574
- return self.showDomains && self.annotations.length > 0
1679
+ return self.showDomains && this.allAnnotations.length > 0
1575
1680
  },
1576
1681
  /**
1577
1682
  * #getter
@@ -1605,12 +1710,26 @@ function stateModelFactory() {
1605
1710
  * #getter
1606
1711
  */
1607
1712
  get colorScheme() {
1713
+ const { customColorScheme, colorSchemeName } = self
1608
1714
  // colorSchemeName is a free string (menus, snapshots, URL params), so a
1609
1715
  // stale name falls back to the default
1610
- return (
1611
- colorSchemes[self.colorSchemeName] ??
1612
- colorSchemes[defaultColorSchemeName]!
1613
- )
1716
+ return customColorScheme
1717
+ ? letterColorTable(customColorScheme)
1718
+ : (colorSchemes[colorSchemeName] ??
1719
+ colorSchemes[defaultColorSchemeName]!)
1720
+ },
1721
+
1722
+ /**
1723
+ * #getter
1724
+ * the scheme coloring each cell from its column's statistics, or
1725
+ * undefined while the cells take a fixed color per letter, from the
1726
+ * built-in table or from `customColorScheme`
1727
+ */
1728
+ get dynamicColorSchemeName() {
1729
+ const { customColorScheme, colorSchemeName } = self
1730
+ return !customColorScheme && colorSchemeName.includes('dynamic')
1731
+ ? colorSchemeName
1732
+ : undefined
1614
1733
  },
1615
1734
 
1616
1735
  /**
@@ -1633,7 +1752,7 @@ function stateModelFactory() {
1633
1752
  return !!this.tree.noTree
1634
1753
  },
1635
1754
  get noDomains() {
1636
- return self.annotations.length === 0
1755
+ return self.allAnnotations.length === 0
1637
1756
  },
1638
1757
 
1639
1758
  /**
@@ -1642,9 +1761,9 @@ function stateModelFactory() {
1642
1761
  * from disk or pasted in becomes inline text, and DataModel drops an
1643
1762
  * inline document past `maxInlineSnapshotBytes`.
1644
1763
  *
1645
- * The header lists these, and the standalone app drops `?data=` from
1764
+ * The header lists these, and the standalone app drops `#data=` from
1646
1765
  * the address bar while the list is non-empty or the encoded link would
1647
- * pass 8,000 characters, so a copied link never opens an empty viewer
1766
+ * pass its `maxLinkLength`, so a copied link never opens an empty viewer
1648
1767
  * unannounced. A document fetched from a URL never appears here, since
1649
1768
  * the snapshot keeps its filehandle.
1650
1769
  *
@@ -1687,7 +1806,8 @@ function stateModelFactory() {
1687
1806
  * `data.treeMetadata`, the name that travels in existing links, so the
1688
1807
  * inline size limit and `treeMetadataFilehandle` cover it. labelWidthMap
1689
1808
  * reads it on every layout, so a malformed user-supplied file returns {}
1690
- * instead of throwing out of rendering.
1809
+ * instead of throwing out of rendering, and every field is a string by
1810
+ * the time a scale sorts it.
1691
1811
  */
1692
1812
  get rowData(): Record<string, Record<string, string> | undefined> {
1693
1813
  const text = self.data.treeMetadata
@@ -1697,7 +1817,12 @@ function stateModelFactory() {
1697
1817
  try {
1698
1818
  const parsed: unknown = JSON.parse(text)
1699
1819
  return typeof parsed === 'object' && parsed !== null
1700
- ? (parsed as Record<string, Record<string, string> | undefined>)
1820
+ ? Object.fromEntries(
1821
+ Object.entries(parsed).map(([name, row]) => [
1822
+ name,
1823
+ stringFields(row),
1824
+ ]),
1825
+ )
1701
1826
  : {}
1702
1827
  } catch (e) {
1703
1828
  console.error('failed to parse rowData', e)
@@ -3101,7 +3226,7 @@ function stateModelFactory() {
3101
3226
  // first occurrence wins; only name, description and segment start are
3102
3227
  // read from it
3103
3228
  const types = new Map<string, Annotation>()
3104
- for (const annot of self.annotations) {
3229
+ for (const annot of self.allAnnotations) {
3105
3230
  if (!types.has(annot.accession)) {
3106
3231
  types.set(annot.accession, annot)
3107
3232
  }
@@ -3109,7 +3234,7 @@ function stateModelFactory() {
3109
3234
  return types
3110
3235
  },
3111
3236
  get filteredAnnotations() {
3112
- return self.annotations.filter(
3237
+ return self.allAnnotations.filter(
3113
3238
  r => !self.turnedOffFeatures.get(r.accession),
3114
3239
  )
3115
3240
  },
@@ -3531,7 +3656,7 @@ function stateModelFactory() {
3531
3656
  * replace the row table, which the model keeps as the JSON string
3532
3657
  * `data.treeMetadata` (see docs/layers.md)
3533
3658
  */
3534
- setRowData(rowData: Record<string, Record<string, string>>) {
3659
+ setRowData(rowData: RowDataInput) {
3535
3660
  self.data.setTreeMetadata(JSON.stringify(rowData))
3536
3661
  },
3537
3662
  /**
@@ -3923,19 +4048,9 @@ function stateModelFactory() {
3923
4048
  * on every mouse move.
3924
4049
  */
3925
4050
  get highlightedColumnRuns() {
3926
- const { highlightedColumns } = self
3927
- const runs: { start: number; end: number }[] = []
3928
- for (const col of [...(highlightedColumns ?? [])].sort(
3929
- (a, b) => a - b,
3930
- )) {
3931
- const last = runs.at(-1)
3932
- if (last && col === last.end + 1) {
3933
- last.end = col
3934
- } else {
3935
- runs.push({ start: col, end: col })
3936
- }
3937
- }
3938
- return runs
4051
+ return consecutiveRuns(
4052
+ [...(self.highlightedColumns ?? [])].sort((a, b) => a - b),
4053
+ ).map(([start, end]) => ({ start, end }))
3939
4054
  },
3940
4055
 
3941
4056
  /**
@@ -3991,6 +4106,85 @@ function stateModelFactory() {
3991
4106
  })
3992
4107
  },
3993
4108
 
4109
+ /**
4110
+ * #getter
4111
+ * `selection` projected onto what is on screen, the way
4112
+ * `resolvedHighlights` projects a column span and a row set. A
4113
+ * selection whose columns are all hidden, or whose rows are all
4114
+ * collapsed away or unknown, resolves to undefined.
4115
+ */
4116
+ get resolvedSelection(): ResolvedSelection | undefined {
4117
+ const { selection, rowNamesSet, numRows } = self
4118
+ const span =
4119
+ selection &&
4120
+ self.visibleSpan({ start: selection.start, end: selection.end })
4121
+ if (!selection || !span) {
4122
+ return undefined
4123
+ }
4124
+ const rowRuns = selection.rows
4125
+ ? consecutiveRuns(
4126
+ selection.rows
4127
+ .map(name => rowNamesSet.get(name))
4128
+ .filter(notEmpty)
4129
+ .sort((a, b) => a - b),
4130
+ )
4131
+ : numRows > 0
4132
+ ? [[0, numRows - 1] as [number, number]]
4133
+ : []
4134
+ return rowRuns.length > 0 ? { ...span, rowRuns } : undefined
4135
+ },
4136
+
4137
+ /**
4138
+ * #getter
4139
+ * the columns and rows the selection covers, for the header's readout.
4140
+ * Columns count the file's columns from `start` to `end`, hidden ones
4141
+ * included, as `selectionFasta` copies them.
4142
+ */
4143
+ get selectionSize() {
4144
+ const resolved = this.resolvedSelection
4145
+ const span = this.selectionFileSpan
4146
+ return resolved && span
4147
+ ? {
4148
+ columns: span.end - span.start + 1,
4149
+ rows: sum(resolved.rowRuns.map(([a, b]) => b - a + 1)),
4150
+ }
4151
+ : undefined
4152
+ },
4153
+
4154
+ /**
4155
+ * #getter
4156
+ * `selection`'s columns clamped to the file's
4157
+ */
4158
+ get selectionFileSpan() {
4159
+ const { selection } = self
4160
+ const width = self.MSA?.getWidth() ?? 0
4161
+ const start = Math.max(1, Math.floor(selection?.start ?? 0))
4162
+ const end = Math.min(width, Math.ceil(selection?.end ?? 0))
4163
+ return selection && start <= end ? { start, end } : undefined
4164
+ },
4165
+
4166
+ /**
4167
+ * #getter
4168
+ * the selected block as FASTA: the selected rows on screen, top to
4169
+ * bottom, each with its letters across the file's columns from `start`
4170
+ * to `end`, gaps included
4171
+ */
4172
+ get selectionFasta() {
4173
+ const { rowNames, rowMap } = self
4174
+ const resolved = this.resolvedSelection
4175
+ const span = this.selectionFileSpan
4176
+ if (!resolved || !span) {
4177
+ return ''
4178
+ }
4179
+ const rows = resolved.rowRuns
4180
+ .flatMap(([first, last]) => rowNames.slice(first, last + 1))
4181
+ .flatMap(name => {
4182
+ const seq = rowMap.get(name)
4183
+ return seq === undefined ? [] : [[name, seq] as const]
4184
+ })
4185
+ return blockFasta(rows, span.start, span.end)
4186
+ },
4187
+
3994
4188
  /**
3995
4189
  * #method
3996
4190
  * per-column summary statistics: consensus residue and its identity
@@ -4021,23 +4215,25 @@ function stateModelFactory() {
4021
4215
  /**
4022
4216
  * #getter
4023
4217
  * each encoding with its scale resolved against the values its field
4024
- * takes: a feature channel reads them across the features drawn, every
4218
+ * takes: a feature channel reads them across every feature loaded, every
4025
4219
  * other channel across the row table. Resolved once per change of that
4026
4220
  * table or the encodings, never per row per frame.
4027
4221
  */
4028
4222
  get resolvedEncodings(): ResolvedEncoding[] {
4029
4223
  const rows = Object.values(self.rowData)
4030
- const features = self.filteredAnnotations
4031
4224
  return self.encodings.map(encoding => ({
4032
4225
  ...encoding,
4033
- ...resolveScale(
4034
- encoding.scale,
4035
- featureChannels.has(encoding.channel)
4036
- ? features
4037
- .map(a => featureField(a, encoding.field))
4038
- .filter(notEmpty)
4039
- : rows.map(row => row?.[encoding.field]).filter(notEmpty),
4040
- ),
4226
+ ...(featureChannels.has(encoding.channel)
4227
+ ? resolveFeatureScale(
4228
+ encoding.field,
4229
+ encoding.scale,
4230
+ self.allAnnotations,
4231
+ self.filteredAnnotations,
4232
+ )
4233
+ : resolveScale(
4234
+ encoding.scale,
4235
+ rows.map(row => row?.[encoding.field]).filter(notEmpty),
4236
+ )),
4041
4237
  }))
4042
4238
  },
4043
4239
 
@@ -4055,7 +4251,12 @@ function stateModelFactory() {
4055
4251
  if (panel.kind === 'features') {
4056
4252
  const color = panel.encoding?.color
4057
4253
  const encoding = color
4058
- ? resolveFeatureScale(color.field, color.scale, annotations)
4254
+ ? resolveFeatureScale(
4255
+ color.field,
4256
+ color.scale,
4257
+ self.allAnnotations,
4258
+ annotations,
4259
+ )
4059
4260
  : featureFillEncoding
4060
4261
  const label = panel.encoding?.label
4061
4262
  return {
@@ -4386,6 +4587,63 @@ function stateModelFactory() {
4386
4587
  )
4387
4588
  })
4388
4589
  },
4590
+ /**
4591
+ * #action
4592
+ * zoom and scroll so the selected columns fill the alignment's width,
4593
+ * and, where the selection names its rows, so those rows fill its height
4594
+ */
4595
+ zoomToSelection() {
4596
+ const { selection, resolvedSelection } = self
4597
+ if (!selection || !resolvedSelection) {
4598
+ return
4599
+ }
4600
+ transaction(() => {
4601
+ this.zoomToRegion({ start: selection.start, end: selection.end })
4602
+ if (selection.rows && self.viewInitialized) {
4603
+ const { rowRuns } = resolvedSelection
4604
+ const first = rowRuns[0]![0]
4605
+ const last = rowRuns.at(-1)![1]
4606
+ self.rowHeight = clamp(
4607
+ self.msaAreaHeight / (last - first + 1),
4608
+ minRowHeight,
4609
+ maxCellSize,
4610
+ )
4611
+ self.scrollY = clamp(-first * self.rowHeight, self.maxScrollY, 0)
4612
+ }
4613
+ })
4614
+ },
4615
+ /**
4616
+ * #action
4617
+ * select the block between two cells given as visible column and row
4618
+ * indices, the coordinates a drag reports, clamped to the alignment. A
4619
+ * cell with no `row` selects the columns across every row, and so does a
4620
+ * block spanning every row on screen.
4621
+ */
4622
+ selectBlock(
4623
+ anchor: { col: number; row?: number },
4624
+ head: { col: number; row?: number },
4625
+ ) {
4626
+ const { numColumns, numRows, rowNames } = self
4627
+ if (numColumns === 0 || numRows === 0) {
4628
+ return
4629
+ }
4630
+ const ordered = (a: number, b: number, last: number) =>
4631
+ [clamp(a, 0, last), clamp(b, 0, last)].sort((x, y) => x - y) as [
4632
+ number,
4633
+ number,
4634
+ ]
4635
+ const [startCol, endCol] = ordered(anchor.col, head.col, numColumns - 1)
4636
+ const [firstRow, lastRow] =
4637
+ anchor.row === undefined || head.row === undefined
4638
+ ? [0, numRows - 1]
4639
+ : ordered(anchor.row, head.row, numRows - 1)
4640
+ const everyRow = firstRow === 0 && lastRow === numRows - 1
4641
+ self.setSelection({
4642
+ start: self.visibleColToGlobalCol(startCol) + 1,
4643
+ end: self.visibleColToGlobalCol(endCol) + 1,
4644
+ ...(everyRow ? {} : { rows: rowNames.slice(firstRow, lastRow + 1) }),
4645
+ })
4646
+ },
4389
4647
  /**
4390
4648
  * #action
4391
4649
  */
@@ -4667,7 +4925,7 @@ function stateModelFactory() {
4667
4925
  addDisposer(
4668
4926
  self,
4669
4927
  autorun(() => {
4670
- if (self.colorSchemeName.includes('dynamic')) {
4928
+ if (self.dynamicColorSchemeName) {
4671
4929
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
4672
4930
  self.colStats
4673
4931
  }
@@ -4736,7 +4994,7 @@ function stateModelFactory() {
4736
4994
  // stripDefault handles per-property defaults; this drops inline documents
4737
4995
  // whose filehandle can refetch them
4738
4996
  ...rest,
4739
- ...smallColumnTracks(columnTracks),
4997
+ ...smallColumnTracks(columnTracks, maxInlineSnapshotBytes),
4740
4998
  data: {
4741
4999
  ...(rest.treeFilehandle ? {} : { tree: data.tree }),
4742
5000
  ...(rest.msaFilehandle ? {} : { msa: data.msa }),