react-msaview 8.0.0 → 8.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 (86) 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/model.d.ts +31 -32
  51. package/dist/model.js +184 -141
  52. package/dist/model.js.map +1 -1
  53. package/dist/svgTestUtil.d.ts +18 -22
  54. package/dist/types.d.ts +2 -0
  55. package/dist/useWheelScroll.js +0 -4
  56. package/dist/useWheelScroll.js.map +1 -1
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/package.json +3 -3
  60. package/src/calculateBlocks.ts +5 -3
  61. package/src/components/Loading.tsx +2 -30
  62. package/src/components/SequenceTextArea.tsx +0 -1
  63. package/src/components/Track.tsx +2 -1
  64. package/src/components/dialogs/AboutDialog.tsx +35 -6
  65. package/src/components/dialogs/AnnotationFileDialog.tsx +10 -1
  66. package/src/components/dialogs/ExportSVGDialog.tsx +1 -1
  67. package/src/components/dialogs/FeatureDialog.tsx +11 -3
  68. package/src/components/header/Header.tsx +1 -1
  69. package/src/components/header/LoadWarnings.tsx +8 -21
  70. package/src/components/header/settingsMenuItems.ts +3 -3
  71. package/src/components/import/ImportForm.tsx +3 -5
  72. package/src/components/msa/MSACanvas.tsx +0 -3
  73. package/src/components/msa/drawFeatureSpans.ts +80 -25
  74. package/src/components/msa/renderBoxFeatureCanvasBlock.ts +7 -9
  75. package/src/components/msa/renderMSABlock.ts +9 -8
  76. package/src/components/tracks/drawTracks.ts +8 -16
  77. package/src/components/tree/TreeBranchMenu.tsx +1 -2
  78. package/src/components/tree/TreeCanvas.tsx +0 -3
  79. package/src/components/tree/TreeCanvasBlock.tsx +2 -2
  80. package/src/components/tree/TreeNodeMenu.tsx +1 -2
  81. package/src/components/tree/renderTreeCanvas.ts +2 -1
  82. package/src/constants.ts +5 -0
  83. package/src/model.ts +232 -161
  84. package/src/types.ts +2 -0
  85. package/src/useWheelScroll.ts +0 -4
  86. 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
@@ -788,10 +815,12 @@ function stateModelFactory() {
788
815
  /**
789
816
  * #action
790
817
  * record a non-fatal load problem: a layer that failed to load, a file
791
- * that failed to parse
818
+ * that failed to parse. A message already on the list is not added again
792
819
  */
793
820
  addWarning(warning) {
794
- self.warnings = [...self.warnings, warning];
821
+ if (!self.warnings.includes(warning)) {
822
+ self.warnings = [...self.warnings, warning];
823
+ }
795
824
  },
796
825
  /**
797
826
  * #action
@@ -1440,7 +1469,7 @@ function stateModelFactory() {
1440
1469
  */
1441
1470
  get insertionPositions() {
1442
1471
  const { blanks, rows } = this;
1443
- if (blanks.length === 0 || !self.hideGapsEffective) {
1472
+ if (blanks.length === 0) {
1444
1473
  return new Map();
1445
1474
  }
1446
1475
  const result = new Map();
@@ -1682,7 +1711,7 @@ function stateModelFactory() {
1682
1711
  * cladogram mode. The tree's scale bar uses it.
1683
1712
  */
1684
1713
  get pxPerBranchLength() {
1685
- const max = maxLength(this.root);
1714
+ const max = this.rootToTipLength;
1686
1715
  return this.showBranchLenEffective && max ? self.treeWidth / max : 0;
1687
1716
  },
1688
1717
  /**
@@ -1731,6 +1760,125 @@ function stateModelFactory() {
1731
1760
  return self.drawLabels && self.rowHeight >= minLetterRowHeight;
1732
1761
  },
1733
1762
  }))
1763
+ .views(self => {
1764
+ // a spec is a frozen value, so it keys its computed until setColumnTracks
1765
+ // replaces it
1766
+ const columnTrackModels = new WeakMap();
1767
+ return {
1768
+ /**
1769
+ * #getter
1770
+ * a data track's values or string, projected from its row's residues
1771
+ * onto alignment columns when it names a row
1772
+ */
1773
+ get columnTrackContent() {
1774
+ const { MSA, blanks, hideGapsEffective } = self;
1775
+ const width = MSA?.getWidth() ?? 0;
1776
+ const project = (track, items, fill) => {
1777
+ if (!track.row) {
1778
+ return items;
1779
+ }
1780
+ const out = Array.from({ length: width }, () => fill);
1781
+ const index = self.seqPosIndex(track.row);
1782
+ items.forEach((item, seqPos) => {
1783
+ const col = index?.[seqPos];
1784
+ if (col !== undefined) {
1785
+ out[col] = item;
1786
+ }
1787
+ });
1788
+ return out;
1789
+ };
1790
+ const skip = (items) => hideGapsEffective ? dropBlanks(blanks, items) : items;
1791
+ // an arc endpoint maps row residue -> column -> visible column.
1792
+ // visibleColsBefore, not globalColToVisibleCol, so an endpoint in a
1793
+ // hidden column moves to the neighboring visible one and the arc stays
1794
+ const resolve = (track, pos) => {
1795
+ const col = track.row
1796
+ ? self.seqPosIndex(track.row)?.[pos - 1]
1797
+ : pos - 1;
1798
+ if (col === undefined || col < 0 || col >= width) {
1799
+ return undefined;
1800
+ }
1801
+ return hideGapsEffective ? visibleColsBefore(blanks, col) : col;
1802
+ };
1803
+ return new Map(self.columnTracks.map(track => {
1804
+ if (track.kind === 'arc') {
1805
+ const arcs = (track.arcs ?? [])
1806
+ .map(arc => {
1807
+ const start = resolve(track, Math.min(arc.start, arc.end));
1808
+ const end = resolve(track, Math.max(arc.start, arc.end));
1809
+ return start !== undefined &&
1810
+ end !== undefined &&
1811
+ start < end
1812
+ ? { start, end, color: arc.color }
1813
+ : undefined;
1814
+ })
1815
+ .filter(notEmpty);
1816
+ return [track.id, { arcs }];
1817
+ }
1818
+ if (track.kind === 'bar') {
1819
+ const max = track.max ?? 1;
1820
+ const values = skip(project(track, track.values ?? [], 0)).map(v => Math.min(1, Math.max(0, v / max)));
1821
+ return [track.id, { values }];
1822
+ }
1823
+ const data = skip(project(track, (track.data ?? '').split(''), ' '));
1824
+ return [track.id, { data: data.join('') }];
1825
+ }));
1826
+ },
1827
+ /**
1828
+ * #method
1829
+ * the height a track draws at: what the user dragged its divider to,
1830
+ * then the height its snapshot asked for, then its kind's default. Only
1831
+ * a text track falls through to rowHeight
1832
+ */
1833
+ trackHeight(kind, heightKey = kind, given) {
1834
+ return (self.trackHeights.get(heightKey) ??
1835
+ given ??
1836
+ defaultTrackHeights[kind] ??
1837
+ self.rowHeight);
1838
+ },
1839
+ /**
1840
+ * #method
1841
+ * the track a column track spec draws as, computed once per spec. A
1842
+ * text track's height falls through to rowHeight, and its own computed
1843
+ * confines the vertical zoom to it, so a sibling keeps its object and
1844
+ * its canvas skips the redraw
1845
+ */
1846
+ columnTrackModel(track) {
1847
+ let computedModel = columnTrackModels.get(track);
1848
+ if (!computedModel) {
1849
+ computedModel = computed(() => {
1850
+ const heightKey = resizableKinds.has(track.kind)
1851
+ ? ownHeightKey(track.id)
1852
+ : undefined;
1853
+ const content = this.columnTrackContent.get(track.id);
1854
+ return {
1855
+ model: {
1856
+ id: track.id,
1857
+ name: track.name,
1858
+ kind: track.kind,
1859
+ heightKey,
1860
+ height: this.trackHeight(track.kind, heightKey, track.height),
1861
+ barColor: track.color,
1862
+ arcColor: track.color,
1863
+ customColorScheme: track.colors,
1864
+ data: content?.data,
1865
+ arcs: content?.arcs,
1866
+ },
1867
+ ReactComponent: TrackBlocks,
1868
+ };
1869
+ });
1870
+ columnTrackModels.set(track, computedModel);
1871
+ }
1872
+ return computedModel.get();
1873
+ },
1874
+ /**
1875
+ * #getter
1876
+ */
1877
+ get columnTrackModels() {
1878
+ return self.columnTracks.map(track => this.columnTrackModel(track));
1879
+ },
1880
+ };
1881
+ })
1734
1882
  .views(self => ({
1735
1883
  /**
1736
1884
  * #getter
@@ -1825,101 +1973,6 @@ function stateModelFactory() {
1825
1973
  ReactComponent: TrackBlocks,
1826
1974
  }));
1827
1975
  },
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
1976
  /**
1924
1977
  * #getter
1925
1978
  * the consensus secondary structure as a track, when there is one. A
@@ -1936,7 +1989,7 @@ function stateModelFactory() {
1936
1989
  name: 'Base pairs',
1937
1990
  kind: 'arc',
1938
1991
  heightKey: 'arc',
1939
- height: this.trackHeight('arc'),
1992
+ height: self.trackHeight('arc'),
1940
1993
  arcs,
1941
1994
  },
1942
1995
  ReactComponent: TrackBlocks,
@@ -1984,7 +2037,7 @@ function stateModelFactory() {
1984
2037
  model: {
1985
2038
  ...model,
1986
2039
  heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
1987
- height: this.trackHeight(model.kind),
2040
+ height: self.trackHeight(model.kind),
1988
2041
  },
1989
2042
  ReactComponent: TrackBlocks,
1990
2043
  }));
@@ -1993,7 +2046,7 @@ function stateModelFactory() {
1993
2046
  return [
1994
2047
  ...this.adapterTrackModels,
1995
2048
  ...this.basePairTrackModels,
1996
- ...this.columnTrackModels,
2049
+ ...self.columnTrackModels,
1997
2050
  // every computed track reads the alignment's columns, and a tree, a
1998
2051
  // GFF and a features panel draw a figure with none
1999
2052
  ...(self.numColumns > 0 ? this.computedTrackModels : []),
@@ -2778,12 +2831,6 @@ function stateModelFactory() {
2778
2831
  const categorical = createPaletteMap(this.categoricalDomainTypes.map(d => d.accession));
2779
2832
  return { ...segments, ...categorical };
2780
2833
  },
2781
- get strokePalette() {
2782
- return transform(this.fillPalette, ([key, val]) => [
2783
- key,
2784
- outlineColor(val),
2785
- ]);
2786
- },
2787
2834
  /**
2788
2835
  * #getter
2789
2836
  * the encoding coloring the overlay's spans, undefined when none does,
@@ -3062,22 +3109,6 @@ function stateModelFactory() {
3062
3109
  propertyConservation,
3063
3110
  });
3064
3111
  },
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
3112
  /**
3082
3113
  * #method
3083
3114
  */
@@ -3139,7 +3170,6 @@ function stateModelFactory() {
3139
3170
  spans: featurePanelSpans({
3140
3171
  panel,
3141
3172
  width,
3142
- rowHeight: self.rowHeight,
3143
3173
  colWidth: self.colWidth,
3144
3174
  domainBands: this.domainBands,
3145
3175
  annotationsByRow: self.annotationsByRow,
@@ -3406,11 +3436,14 @@ function stateModelFactory() {
3406
3436
  // alignment all read. The tree arrives with the model for inline data
3407
3437
  // and later for a filehandle, so the seeding waits for it and then runs
3408
3438
  // once: expanding a seeded clade sticks, and the record collapses it
3409
- // again only on reload.
3439
+ // again only on reload. `dataInitialized` is true once the MSA alone
3440
+ // has loaded, when the tree is still the flat stub, so a tree
3441
+ // filehandle holds the seeding until its text lands.
3410
3442
  let cladesSeeded = false;
3411
3443
  addDisposer(self, autorun(() => {
3412
3444
  if (cladesSeeded ||
3413
3445
  !self.dataInitialized ||
3446
+ (self.treeFilehandle && !self.data.tree) ||
3414
3447
  self.clades.length === 0) {
3415
3448
  return;
3416
3449
  }
@@ -3566,6 +3599,16 @@ function stateModelFactory() {
3566
3599
  self.setAnnotations([]);
3567
3600
  }
3568
3601
  }));
3602
+ // a GFF whose first column names rows of some other alignment parses
3603
+ // without error and draws nothing
3604
+ addDisposer(self, autorun(() => {
3605
+ const { annotations, rowNamesSet } = self;
3606
+ if (annotations.length > 0 &&
3607
+ rowNamesSet.size > 0 &&
3608
+ !annotations.some(a => rowNamesSet.has(a.id))) {
3609
+ self.addWarning(`0 of ${annotations.length} annotations name a row in this alignment`);
3610
+ }
3611
+ }));
3569
3612
  // gffFilehandle loads into data.gff, which the autorun above parses
3570
3613
  loadOnFilehandleChange({
3571
3614
  what: 'annotations',