react-msaview 8.0.0 → 8.2.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 (94) hide show
  1. package/bundle/index.js +99 -99
  2. package/bundle/index.js.map +3 -3
  3. package/dist/calculateBlocks.js +5 -3
  4. package/dist/calculateBlocks.js.map +1 -1
  5. package/dist/components/Loading.js +2 -8
  6. package/dist/components/Loading.js.map +1 -1
  7. package/dist/components/SequenceTextArea.js +0 -1
  8. package/dist/components/SequenceTextArea.js.map +1 -1
  9. package/dist/components/Track.js +2 -2
  10. package/dist/components/Track.js.map +1 -1
  11. package/dist/components/dialogs/AboutDialog.js +6 -6
  12. package/dist/components/dialogs/AboutDialog.js.map +1 -1
  13. package/dist/components/dialogs/AnnotationFileDialog.js +5 -2
  14. package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
  15. package/dist/components/dialogs/ExportSVGDialog.js +1 -1
  16. package/dist/components/dialogs/FeatureDialog.js +8 -3
  17. package/dist/components/dialogs/FeatureDialog.js.map +1 -1
  18. package/dist/components/header/Header.js +1 -1
  19. package/dist/components/header/Header.js.map +1 -1
  20. package/dist/components/header/LoadWarnings.js +3 -19
  21. package/dist/components/header/LoadWarnings.js.map +1 -1
  22. package/dist/components/header/settingsMenuItems.js +3 -3
  23. package/dist/components/header/settingsMenuItems.js.map +1 -1
  24. package/dist/components/import/ImportForm.js +1 -1
  25. package/dist/components/import/ImportForm.js.map +1 -1
  26. package/dist/components/msa/MSACanvas.js +1 -3
  27. package/dist/components/msa/MSACanvas.js.map +1 -1
  28. package/dist/components/msa/drawFeatureSpans.d.ts +7 -1
  29. package/dist/components/msa/drawFeatureSpans.js +58 -23
  30. package/dist/components/msa/drawFeatureSpans.js.map +1 -1
  31. package/dist/components/msa/renderBoxFeatureCanvasBlock.js +7 -8
  32. package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
  33. package/dist/components/msa/renderMSABlock.js +8 -9
  34. package/dist/components/msa/renderMSABlock.js.map +1 -1
  35. package/dist/components/tracks/drawTracks.js +8 -5
  36. package/dist/components/tracks/drawTracks.js.map +1 -1
  37. package/dist/components/tree/TreeBranchMenu.js +1 -1
  38. package/dist/components/tree/TreeBranchMenu.js.map +1 -1
  39. package/dist/components/tree/TreeCanvas.js +1 -3
  40. package/dist/components/tree/TreeCanvas.js.map +1 -1
  41. package/dist/components/tree/TreeCanvasBlock.js +2 -2
  42. package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
  43. package/dist/components/tree/TreeNodeMenu.js +1 -1
  44. package/dist/components/tree/TreeNodeMenu.js.map +1 -1
  45. package/dist/components/tree/renderTreeCanvas.js +2 -1
  46. package/dist/components/tree/renderTreeCanvas.js.map +1 -1
  47. package/dist/constants.d.ts +1 -0
  48. package/dist/constants.js +4 -0
  49. package/dist/constants.js.map +1 -1
  50. package/dist/expandSpec.d.ts +46 -0
  51. package/dist/expandSpec.js +127 -0
  52. package/dist/expandSpec.js.map +1 -0
  53. package/dist/index.d.ts +2 -0
  54. package/dist/index.js +2 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/model.d.ts +52 -33
  57. package/dist/model.js +232 -142
  58. package/dist/model.js.map +1 -1
  59. package/dist/svgTestUtil.d.ts +28 -24
  60. package/dist/types.d.ts +2 -0
  61. package/dist/useWheelScroll.js +0 -4
  62. package/dist/useWheelScroll.js.map +1 -1
  63. package/dist/version.d.ts +1 -1
  64. package/dist/version.js +1 -1
  65. package/package.json +3 -3
  66. package/src/calculateBlocks.ts +5 -3
  67. package/src/components/Loading.tsx +2 -30
  68. package/src/components/SequenceTextArea.tsx +0 -1
  69. package/src/components/Track.tsx +2 -1
  70. package/src/components/dialogs/AboutDialog.tsx +35 -6
  71. package/src/components/dialogs/AnnotationFileDialog.tsx +10 -1
  72. package/src/components/dialogs/ExportSVGDialog.tsx +1 -1
  73. package/src/components/dialogs/FeatureDialog.tsx +11 -3
  74. package/src/components/header/Header.tsx +1 -1
  75. package/src/components/header/LoadWarnings.tsx +8 -21
  76. package/src/components/header/settingsMenuItems.ts +3 -3
  77. package/src/components/import/ImportForm.tsx +3 -5
  78. package/src/components/msa/MSACanvas.tsx +0 -3
  79. package/src/components/msa/drawFeatureSpans.ts +80 -25
  80. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +7 -9
  81. package/src/components/msa/renderMSABlock.ts +9 -8
  82. package/src/components/tracks/drawTracks.ts +8 -16
  83. package/src/components/tree/TreeBranchMenu.tsx +1 -2
  84. package/src/components/tree/TreeCanvas.tsx +0 -3
  85. package/src/components/tree/TreeCanvasBlock.tsx +2 -2
  86. package/src/components/tree/TreeNodeMenu.tsx +1 -2
  87. package/src/components/tree/renderTreeCanvas.ts +2 -1
  88. package/src/constants.ts +5 -0
  89. package/src/expandSpec.ts +196 -0
  90. package/src/index.ts +8 -0
  91. package/src/model.ts +289 -162
  92. package/src/types.ts +2 -0
  93. package/src/useWheelScroll.ts +0 -4
  94. package/src/version.ts +1 -1
package/dist/model.js CHANGED
@@ -2,7 +2,7 @@ import { clamp, groupBy, notEmpty, sum } from '@jbrowse/core/util';
2
2
  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
- import { autorun, transaction } from 'mobx';
5
+ import { autorun, computed, transaction } from 'mobx';
6
6
  import { generateNodeIds, gffToAnnotations, interProScanToAnnotations, parseEmfTree, parseGFF, parseMSA, parseNewick, } from 'msa-parsers';
7
7
  import { calculateBlocks } from './calculateBlocks.js';
8
8
  import { clustalXColumnColors } from './clustalX.js';
@@ -34,7 +34,7 @@ import { resolveScale } from './scales.js';
34
34
  import { buildSeqPosIndex } from './seqPosToGlobalCol.js';
35
35
  import { maxBitsFor } from './sequenceLogo.js';
36
36
  import { stripDefault } from './stripDefault.js';
37
- import { computeRowInsertions, dropBlanks, len, outlineColor, skipBlanks, transform, withAlpha, } from './util.js';
37
+ import { computeRowInsertions, dropBlanks, len, outlineColor, skipBlanks, withAlpha, } from './util.js';
38
38
  import { saveAs } from './vendor/fileSaver.js';
39
39
  import { parseWuss } from './wuss.js';
40
40
  function parseTreeText(text) {
@@ -114,6 +114,37 @@ function featureLabelMap(annotations, field) {
114
114
  }
115
115
  return labels;
116
116
  }
117
+ /**
118
+ * Lanes for `position: "strandpile"`, gggenomes' `position_strandpile`: the
119
+ * forward features stack above the line and the reverse below it, each strand
120
+ * packing on its own. The deepest row on each side sets the grid every row lays
121
+ * out on, so the line between the strands sits at one height down the panel and
122
+ * a reader can scan it. A feature with no strand piles with the forward ones.
123
+ */
124
+ function strandpileLanes(rows) {
125
+ const piled = rows.map(([name, bands]) => {
126
+ const reverse = packDomainLanes(bands.filter(b => b.annotation.strand === -1));
127
+ const forward = packDomainLanes(bands.filter(b => b.annotation.strand !== -1));
128
+ return { name, forward, reverse };
129
+ });
130
+ const up = Math.max(0, ...piled.map(p => p.forward[0]?.laneCount ?? 0));
131
+ const down = Math.max(0, ...piled.map(p => p.reverse[0]?.laneCount ?? 0));
132
+ const laneCount = up + down;
133
+ return new Map(piled.map(({ name, forward, reverse }) => [
134
+ name,
135
+ [
136
+ // forward level 0 is the lane just above the line, stacking upward
137
+ ...forward.map(b => ({ ...b, lane: up - 1 - b.lane, laneCount })),
138
+ ...reverse.map(b => ({ ...b, lane: up + b.lane, laneCount })),
139
+ ],
140
+ ]));
141
+ }
142
+ /** one row's spans laid out by the panel's `position` */
143
+ function panelLanes(panel, rows) {
144
+ return panel.position === 'strandpile'
145
+ ? strandpileLanes(rows)
146
+ : new Map(rows.map(([name, bands]) => [name, packDomainLanes(bands)]));
147
+ }
117
148
  /**
118
149
  * The spans a `features` panel draws, keyed by row name and measured in the
119
150
  * panel's own pixels. `column` takes the bands the overlay draws, at the
@@ -122,9 +153,9 @@ function featureLabelMap(annotations, field) {
122
153
  * cover across every row onto the panel width, so a row with no alignment
123
154
  * still has an x.
124
155
  */
125
- function featurePanelSpans({ panel, width, rowHeight, colWidth, domainBands, annotationsByRow, shifts, }) {
156
+ function featurePanelSpans({ panel, width, colWidth, domainBands, annotationsByRow, shifts, }) {
126
157
  if (panel.x === 'column') {
127
- return new Map([...domainBands].map(([name, bands]) => [
158
+ return panelLanes(panel, [...domainBands].map(([name, bands]) => [
128
159
  name,
129
160
  bands.map(band => ({
130
161
  ...band,
@@ -152,23 +183,23 @@ function featurePanelSpans({ panel, width, rowHeight, colWidth, domainBands, ann
152
183
  max = Math.max(max, end);
153
184
  }
154
185
  }
155
- // the arrowhead on the rightmost feature reaches a row height past its end,
156
- // so the extent maps onto the panel less that much
157
- const drawable = Math.max(1, width - rowHeight);
186
+ // a gene arrow ends where its feature does, so the extent maps onto the whole
187
+ // panel less the pixel the rightmost stroke needs
188
+ const drawable = Math.max(1, width - 1);
158
189
  const scale = max > min ? drawable / (max - min) : 0;
159
190
  // a lane opens where a span covers more than a tenth of the one before it,
160
191
  // which keeps the genes of an operon on one lane: adjacent genes commonly
161
192
  // share a few bases, and a stop codon overlapping the next start reads as a
162
193
  // second lane over the whole row
163
- return new Map(shifted.map(([name, features]) => [
194
+ return panelLanes(panel, shifted.map(([name, features]) => [
164
195
  name,
165
- packDomainLanes(features.map(({ annotation, start, end }) => ({
196
+ features.map(({ annotation, start, end }) => ({
166
197
  annotation,
167
198
  xStart: (start - min) * scale,
168
199
  xEnd: (end - min) * scale,
169
200
  startCol: start,
170
201
  endCol: end - (end - start) * laneOverlap,
171
- }))),
202
+ })),
172
203
  ]));
173
204
  }
174
205
  // a scale over the values a field takes across the features drawn
@@ -545,11 +576,7 @@ function stateModelFactory() {
545
576
  * data from the loaded tree/msa/treeMetadata, generally loaded by
546
577
  * autorun
547
578
  */
548
- data: types.optional(DataModelF(), {
549
- tree: '',
550
- msa: '',
551
- treeMetadata: '',
552
- }),
579
+ data: types.optional(DataModelF(), {}),
553
580
  /**
554
581
  * #property
555
582
  * the user's explicit hide choices per annotation accession, keyed by
@@ -579,6 +606,14 @@ function stateModelFactory() {
579
606
  * Persists in the snapshot and the URL.
580
607
  */
581
608
  highlights: stripDefault(types.array(types.frozen()), []),
609
+ /**
610
+ * #property
611
+ * where the view opens, in `highlights` coordinates: `{row, start,
612
+ * end}` zooms onto residues of that row, `{start, end}` onto alignment
613
+ * columns. Applied once the alignment and any tree file have loaded,
614
+ * then cleared, so a reloaded session keeps the reader's own scroll.
615
+ */
616
+ region: types.frozen(),
582
617
  /**
583
618
  * #property
584
619
  * clades of the tree with a mark drawn over them. `mrca` names tips
@@ -788,10 +823,12 @@ function stateModelFactory() {
788
823
  /**
789
824
  * #action
790
825
  * record a non-fatal load problem: a layer that failed to load, a file
791
- * that failed to parse
826
+ * that failed to parse. A message already on the list is not added again
792
827
  */
793
828
  addWarning(warning) {
794
- self.warnings = [...self.warnings, warning];
829
+ if (!self.warnings.includes(warning)) {
830
+ self.warnings = [...self.warnings, warning];
831
+ }
795
832
  },
796
833
  /**
797
834
  * #action
@@ -830,6 +867,12 @@ function stateModelFactory() {
830
867
  setHighlightedColumns(columns) {
831
868
  self.highlightedColumns = columns;
832
869
  },
870
+ /**
871
+ * #action
872
+ */
873
+ setRegion(region) {
874
+ self.region = region;
875
+ },
833
876
  /**
834
877
  * #action
835
878
  */
@@ -1440,7 +1483,7 @@ function stateModelFactory() {
1440
1483
  */
1441
1484
  get insertionPositions() {
1442
1485
  const { blanks, rows } = this;
1443
- if (blanks.length === 0 || !self.hideGapsEffective) {
1486
+ if (blanks.length === 0) {
1444
1487
  return new Map();
1445
1488
  }
1446
1489
  const result = new Map();
@@ -1682,7 +1725,7 @@ function stateModelFactory() {
1682
1725
  * cladogram mode. The tree's scale bar uses it.
1683
1726
  */
1684
1727
  get pxPerBranchLength() {
1685
- const max = maxLength(this.root);
1728
+ const max = this.rootToTipLength;
1686
1729
  return this.showBranchLenEffective && max ? self.treeWidth / max : 0;
1687
1730
  },
1688
1731
  /**
@@ -1731,6 +1774,125 @@ function stateModelFactory() {
1731
1774
  return self.drawLabels && self.rowHeight >= minLetterRowHeight;
1732
1775
  },
1733
1776
  }))
1777
+ .views(self => {
1778
+ // a spec is a frozen value, so it keys its computed until setColumnTracks
1779
+ // replaces it
1780
+ const columnTrackModels = new WeakMap();
1781
+ return {
1782
+ /**
1783
+ * #getter
1784
+ * a data track's values or string, projected from its row's residues
1785
+ * onto alignment columns when it names a row
1786
+ */
1787
+ get columnTrackContent() {
1788
+ const { MSA, blanks, hideGapsEffective } = self;
1789
+ const width = MSA?.getWidth() ?? 0;
1790
+ const project = (track, items, fill) => {
1791
+ if (!track.row) {
1792
+ return items;
1793
+ }
1794
+ const out = Array.from({ length: width }, () => fill);
1795
+ const index = self.seqPosIndex(track.row);
1796
+ items.forEach((item, seqPos) => {
1797
+ const col = index?.[seqPos];
1798
+ if (col !== undefined) {
1799
+ out[col] = item;
1800
+ }
1801
+ });
1802
+ return out;
1803
+ };
1804
+ const skip = (items) => hideGapsEffective ? dropBlanks(blanks, items) : items;
1805
+ // an arc endpoint maps row residue -> column -> visible column.
1806
+ // visibleColsBefore, not globalColToVisibleCol, so an endpoint in a
1807
+ // hidden column moves to the neighboring visible one and the arc stays
1808
+ const resolve = (track, pos) => {
1809
+ const col = track.row
1810
+ ? self.seqPosIndex(track.row)?.[pos - 1]
1811
+ : pos - 1;
1812
+ if (col === undefined || col < 0 || col >= width) {
1813
+ return undefined;
1814
+ }
1815
+ return hideGapsEffective ? visibleColsBefore(blanks, col) : col;
1816
+ };
1817
+ return new Map(self.columnTracks.map(track => {
1818
+ if (track.kind === 'arc') {
1819
+ const arcs = (track.arcs ?? [])
1820
+ .map(arc => {
1821
+ const start = resolve(track, Math.min(arc.start, arc.end));
1822
+ const end = resolve(track, Math.max(arc.start, arc.end));
1823
+ return start !== undefined &&
1824
+ end !== undefined &&
1825
+ start < end
1826
+ ? { start, end, color: arc.color }
1827
+ : undefined;
1828
+ })
1829
+ .filter(notEmpty);
1830
+ return [track.id, { arcs }];
1831
+ }
1832
+ if (track.kind === 'bar') {
1833
+ const max = track.max ?? 1;
1834
+ const values = skip(project(track, track.values ?? [], 0)).map(v => Math.min(1, Math.max(0, v / max)));
1835
+ return [track.id, { values }];
1836
+ }
1837
+ const data = skip(project(track, (track.data ?? '').split(''), ' '));
1838
+ return [track.id, { data: data.join('') }];
1839
+ }));
1840
+ },
1841
+ /**
1842
+ * #method
1843
+ * the height a track draws at: what the user dragged its divider to,
1844
+ * then the height its snapshot asked for, then its kind's default. Only
1845
+ * a text track falls through to rowHeight
1846
+ */
1847
+ trackHeight(kind, heightKey = kind, given) {
1848
+ return (self.trackHeights.get(heightKey) ??
1849
+ given ??
1850
+ defaultTrackHeights[kind] ??
1851
+ self.rowHeight);
1852
+ },
1853
+ /**
1854
+ * #method
1855
+ * the track a column track spec draws as, computed once per spec. A
1856
+ * text track's height falls through to rowHeight, and its own computed
1857
+ * confines the vertical zoom to it, so a sibling keeps its object and
1858
+ * its canvas skips the redraw
1859
+ */
1860
+ columnTrackModel(track) {
1861
+ let computedModel = columnTrackModels.get(track);
1862
+ if (!computedModel) {
1863
+ computedModel = computed(() => {
1864
+ const heightKey = resizableKinds.has(track.kind)
1865
+ ? ownHeightKey(track.id)
1866
+ : undefined;
1867
+ const content = this.columnTrackContent.get(track.id);
1868
+ return {
1869
+ model: {
1870
+ id: track.id,
1871
+ name: track.name,
1872
+ kind: track.kind,
1873
+ heightKey,
1874
+ height: this.trackHeight(track.kind, heightKey, track.height),
1875
+ barColor: track.color,
1876
+ arcColor: track.color,
1877
+ customColorScheme: track.colors,
1878
+ data: content?.data,
1879
+ arcs: content?.arcs,
1880
+ },
1881
+ ReactComponent: TrackBlocks,
1882
+ };
1883
+ });
1884
+ columnTrackModels.set(track, computedModel);
1885
+ }
1886
+ return computedModel.get();
1887
+ },
1888
+ /**
1889
+ * #getter
1890
+ */
1891
+ get columnTrackModels() {
1892
+ return self.columnTracks.map(track => this.columnTrackModel(track));
1893
+ },
1894
+ };
1895
+ })
1734
1896
  .views(self => ({
1735
1897
  /**
1736
1898
  * #getter
@@ -1825,101 +1987,6 @@ function stateModelFactory() {
1825
1987
  ReactComponent: TrackBlocks,
1826
1988
  }));
1827
1989
  },
1828
- /**
1829
- * #getter
1830
- * a data track's values or string, projected from its row's residues
1831
- * onto alignment columns when it names a row
1832
- */
1833
- get columnTrackContent() {
1834
- const { MSA, blanks, hideGapsEffective } = self;
1835
- const width = MSA?.getWidth() ?? 0;
1836
- const project = (track, items, fill) => {
1837
- if (!track.row) {
1838
- return items;
1839
- }
1840
- const out = Array.from({ length: width }, () => fill);
1841
- const index = self.seqPosIndex(track.row);
1842
- items.forEach((item, seqPos) => {
1843
- const col = index?.[seqPos];
1844
- if (col !== undefined) {
1845
- out[col] = item;
1846
- }
1847
- });
1848
- return out;
1849
- };
1850
- const skip = (items) => hideGapsEffective ? dropBlanks(blanks, items) : items;
1851
- // an arc endpoint maps row residue -> column -> visible column.
1852
- // visibleColsBefore, not globalColToVisibleCol, so an endpoint in a
1853
- // hidden column moves to the neighboring visible one and the arc stays
1854
- const resolve = (track, pos) => {
1855
- const col = track.row
1856
- ? self.seqPosIndex(track.row)?.[pos - 1]
1857
- : pos - 1;
1858
- if (col === undefined || col < 0 || col >= width) {
1859
- return undefined;
1860
- }
1861
- return hideGapsEffective ? visibleColsBefore(blanks, col) : col;
1862
- };
1863
- return new Map(self.columnTracks.map(track => {
1864
- if (track.kind === 'arc') {
1865
- const arcs = (track.arcs ?? [])
1866
- .map(arc => {
1867
- const start = resolve(track, Math.min(arc.start, arc.end));
1868
- const end = resolve(track, Math.max(arc.start, arc.end));
1869
- return start !== undefined && end !== undefined && start < end
1870
- ? { start, end, color: arc.color }
1871
- : undefined;
1872
- })
1873
- .filter(notEmpty);
1874
- return [track.id, { arcs }];
1875
- }
1876
- if (track.kind === 'bar') {
1877
- const max = track.max ?? 1;
1878
- const values = skip(project(track, track.values ?? [], 0)).map(v => Math.min(1, Math.max(0, v / max)));
1879
- return [track.id, { values }];
1880
- }
1881
- const data = skip(project(track, (track.data ?? '').split(''), ' '));
1882
- return [track.id, { data: data.join('') }];
1883
- }));
1884
- },
1885
- /**
1886
- * #method
1887
- * the height a track draws at: what the user dragged its divider to,
1888
- * then the height its snapshot asked for, then its kind's default. Only
1889
- * a text track falls through to rowHeight, and `??` short-circuits
1890
- * before reading it, so vertical zoom does not rebuild the other tracks
1891
- */
1892
- trackHeight(kind, heightKey = kind, given) {
1893
- return (self.trackHeights.get(heightKey) ??
1894
- given ??
1895
- defaultTrackHeights[kind] ??
1896
- self.rowHeight);
1897
- },
1898
- /**
1899
- * #getter
1900
- */
1901
- get columnTrackModels() {
1902
- return self.columnTracks.map(track => {
1903
- const heightKey = resizableKinds.has(track.kind)
1904
- ? ownHeightKey(track.id)
1905
- : undefined;
1906
- return {
1907
- model: {
1908
- id: track.id,
1909
- name: track.name,
1910
- kind: track.kind,
1911
- heightKey,
1912
- height: this.trackHeight(track.kind, heightKey, track.height),
1913
- barColor: track.color,
1914
- arcColor: track.color,
1915
- customColorScheme: track.colors,
1916
- data: this.columnTrackContent.get(track.id)?.data,
1917
- arcs: this.columnTrackContent.get(track.id)?.arcs,
1918
- },
1919
- ReactComponent: TrackBlocks,
1920
- };
1921
- });
1922
- },
1923
1990
  /**
1924
1991
  * #getter
1925
1992
  * the consensus secondary structure as a track, when there is one. A
@@ -1936,7 +2003,7 @@ function stateModelFactory() {
1936
2003
  name: 'Base pairs',
1937
2004
  kind: 'arc',
1938
2005
  heightKey: 'arc',
1939
- height: this.trackHeight('arc'),
2006
+ height: self.trackHeight('arc'),
1940
2007
  arcs,
1941
2008
  },
1942
2009
  ReactComponent: TrackBlocks,
@@ -1984,7 +2051,7 @@ function stateModelFactory() {
1984
2051
  model: {
1985
2052
  ...model,
1986
2053
  heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
1987
- height: this.trackHeight(model.kind),
2054
+ height: self.trackHeight(model.kind),
1988
2055
  },
1989
2056
  ReactComponent: TrackBlocks,
1990
2057
  }));
@@ -1993,7 +2060,7 @@ function stateModelFactory() {
1993
2060
  return [
1994
2061
  ...this.adapterTrackModels,
1995
2062
  ...this.basePairTrackModels,
1996
- ...this.columnTrackModels,
2063
+ ...self.columnTrackModels,
1997
2064
  // every computed track reads the alignment's columns, and a tree, a
1998
2065
  // GFF and a features panel draw a figure with none
1999
2066
  ...(self.numColumns > 0 ? this.computedTrackModels : []),
@@ -2778,12 +2845,6 @@ function stateModelFactory() {
2778
2845
  const categorical = createPaletteMap(this.categoricalDomainTypes.map(d => d.accession));
2779
2846
  return { ...segments, ...categorical };
2780
2847
  },
2781
- get strokePalette() {
2782
- return transform(this.fillPalette, ([key, val]) => [
2783
- key,
2784
- outlineColor(val),
2785
- ]);
2786
- },
2787
2848
  /**
2788
2849
  * #getter
2789
2850
  * the encoding coloring the overlay's spans, undefined when none does,
@@ -3017,6 +3078,23 @@ function stateModelFactory() {
3017
3078
  }
3018
3079
  return runs;
3019
3080
  },
3081
+ /**
3082
+ * #method
3083
+ * a highlight label with `{residue}` and `{position}` filled in from the
3084
+ * row's letter at `start`, which is how the `175` shorthand draws "R175"
3085
+ */
3086
+ fillHighlightLabel(label, row, start) {
3087
+ if (!label.includes('{')) {
3088
+ return label;
3089
+ }
3090
+ const col = row && start !== undefined
3091
+ ? self.seqPosIndex(row)?.[start - 1]
3092
+ : undefined;
3093
+ const residue = row && col !== undefined ? (self.MSA?.getRow(row)[col] ?? '') : '';
3094
+ return label
3095
+ .replaceAll('{residue}', residue)
3096
+ .replaceAll('{position}', start === undefined ? '' : String(start));
3097
+ },
3020
3098
  /**
3021
3099
  * #getter
3022
3100
  * `highlights` projected onto what is on screen: residue spans go
@@ -3032,7 +3110,10 @@ function stateModelFactory() {
3032
3110
  ...Object.values(transientHighlights).flat(),
3033
3111
  ];
3034
3112
  return all.flatMap(({ row, rows, start, end, label, color }) => {
3035
- const base = { label, color };
3113
+ const base = {
3114
+ label: label && this.fillHighlightLabel(label, row, start),
3115
+ color,
3116
+ };
3036
3117
  if (rows) {
3037
3118
  const rowIndices = rows
3038
3119
  .map(name => rowNamesSet.get(name))
@@ -3062,22 +3143,6 @@ function stateModelFactory() {
3062
3143
  propertyConservation,
3063
3144
  });
3064
3145
  },
3065
- /**
3066
- * #getter
3067
- * `columnStatsAt` for the hovered column, undefined when nothing is
3068
- * hovered
3069
- */
3070
- get mouseOverColumnStats() {
3071
- const { mouseCol, colStats, conservation, propertyConservation } = self;
3072
- return mouseCol === undefined
3073
- ? undefined
3074
- : columnStats({
3075
- col: mouseCol,
3076
- colStats,
3077
- conservation,
3078
- propertyConservation,
3079
- });
3080
- },
3081
3146
  /**
3082
3147
  * #method
3083
3148
  */
@@ -3139,7 +3204,6 @@ function stateModelFactory() {
3139
3204
  spans: featurePanelSpans({
3140
3205
  panel,
3141
3206
  width,
3142
- rowHeight: self.rowHeight,
3143
3207
  colWidth: self.colWidth,
3144
3208
  domainBands: this.domainBands,
3145
3209
  annotationsByRow: self.annotationsByRow,
@@ -3406,11 +3470,14 @@ function stateModelFactory() {
3406
3470
  // alignment all read. The tree arrives with the model for inline data
3407
3471
  // and later for a filehandle, so the seeding waits for it and then runs
3408
3472
  // once: expanding a seeded clade sticks, and the record collapses it
3409
- // again only on reload.
3473
+ // again only on reload. `dataInitialized` is true once the MSA alone
3474
+ // has loaded, when the tree is still the flat stub, so a tree
3475
+ // filehandle holds the seeding until its text lands.
3410
3476
  let cladesSeeded = false;
3411
3477
  addDisposer(self, autorun(() => {
3412
3478
  if (cladesSeeded ||
3413
3479
  !self.dataInitialized ||
3480
+ (self.treeFilehandle && !self.data.tree) ||
3414
3481
  self.clades.length === 0) {
3415
3482
  return;
3416
3483
  }
@@ -3427,6 +3494,19 @@ function stateModelFactory() {
3427
3494
  }
3428
3495
  }
3429
3496
  }));
3497
+ // zoomToRegion needs a width and resolves residues through the visible
3498
+ // columns, so this waits for both, and for a tree file whose collapsed
3499
+ // clades would hide columns and move the target after the zoom
3500
+ addDisposer(self, autorun(() => {
3501
+ const { region } = self;
3502
+ if (region &&
3503
+ self.viewInitialized &&
3504
+ self.numColumns > 0 &&
3505
+ !(self.treeFilehandle && !self.data.tree)) {
3506
+ this.zoomToRegion(region);
3507
+ self.setRegion(undefined);
3508
+ }
3509
+ }));
3430
3510
  // the matchMedia query is pinned to the current device pixel ratio, so
3431
3511
  // each change re-registers against the new one
3432
3512
  if (typeof window !== 'undefined' &&
@@ -3566,6 +3646,16 @@ function stateModelFactory() {
3566
3646
  self.setAnnotations([]);
3567
3647
  }
3568
3648
  }));
3649
+ // a GFF whose first column names rows of some other alignment parses
3650
+ // without error and draws nothing
3651
+ addDisposer(self, autorun(() => {
3652
+ const { annotations, rowNamesSet } = self;
3653
+ if (annotations.length > 0 &&
3654
+ rowNamesSet.size > 0 &&
3655
+ !annotations.some(a => rowNamesSet.has(a.id))) {
3656
+ self.addWarning(`0 of ${annotations.length} annotations name a row in this alignment`);
3657
+ }
3658
+ }));
3569
3659
  // gffFilehandle loads into data.gff, which the autorun above parses
3570
3660
  loadOnFilehandleChange({
3571
3661
  what: 'annotations',