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.
- package/bundle/index.js +99 -99
- package/bundle/index.js.map +3 -3
- package/dist/calculateBlocks.js +5 -3
- package/dist/calculateBlocks.js.map +1 -1
- package/dist/components/Loading.js +2 -8
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/SequenceTextArea.js +0 -1
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/Track.js +2 -2
- package/dist/components/Track.js.map +1 -1
- package/dist/components/dialogs/AboutDialog.js +6 -6
- package/dist/components/dialogs/AboutDialog.js.map +1 -1
- package/dist/components/dialogs/AnnotationFileDialog.js +5 -2
- package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
- package/dist/components/dialogs/ExportSVGDialog.js +1 -1
- package/dist/components/dialogs/FeatureDialog.js +8 -3
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/header/Header.js +1 -1
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/LoadWarnings.js +3 -19
- package/dist/components/header/LoadWarnings.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +3 -3
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/import/ImportForm.js +1 -1
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +1 -3
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/drawFeatureSpans.d.ts +7 -1
- package/dist/components/msa/drawFeatureSpans.js +58 -23
- package/dist/components/msa/drawFeatureSpans.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +7 -8
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +8 -9
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/tracks/drawTracks.js +8 -5
- package/dist/components/tracks/drawTracks.js.map +1 -1
- package/dist/components/tree/TreeBranchMenu.js +1 -1
- package/dist/components/tree/TreeBranchMenu.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +1 -3
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +2 -2
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeNodeMenu.js +1 -1
- package/dist/components/tree/TreeNodeMenu.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +2 -1
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +4 -0
- package/dist/constants.js.map +1 -1
- package/dist/expandSpec.d.ts +46 -0
- package/dist/expandSpec.js +127 -0
- package/dist/expandSpec.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/model.d.ts +52 -33
- package/dist/model.js +232 -142
- package/dist/model.js.map +1 -1
- package/dist/svgTestUtil.d.ts +28 -24
- package/dist/types.d.ts +2 -0
- package/dist/useWheelScroll.js +0 -4
- package/dist/useWheelScroll.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/src/calculateBlocks.ts +5 -3
- package/src/components/Loading.tsx +2 -30
- package/src/components/SequenceTextArea.tsx +0 -1
- package/src/components/Track.tsx +2 -1
- package/src/components/dialogs/AboutDialog.tsx +35 -6
- package/src/components/dialogs/AnnotationFileDialog.tsx +10 -1
- package/src/components/dialogs/ExportSVGDialog.tsx +1 -1
- package/src/components/dialogs/FeatureDialog.tsx +11 -3
- package/src/components/header/Header.tsx +1 -1
- package/src/components/header/LoadWarnings.tsx +8 -21
- package/src/components/header/settingsMenuItems.ts +3 -3
- package/src/components/import/ImportForm.tsx +3 -5
- package/src/components/msa/MSACanvas.tsx +0 -3
- package/src/components/msa/drawFeatureSpans.ts +80 -25
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +7 -9
- package/src/components/msa/renderMSABlock.ts +9 -8
- package/src/components/tracks/drawTracks.ts +8 -16
- package/src/components/tree/TreeBranchMenu.tsx +1 -2
- package/src/components/tree/TreeCanvas.tsx +0 -3
- package/src/components/tree/TreeCanvasBlock.tsx +2 -2
- package/src/components/tree/TreeNodeMenu.tsx +1 -2
- package/src/components/tree/renderTreeCanvas.ts +2 -1
- package/src/constants.ts +5 -0
- package/src/expandSpec.ts +196 -0
- package/src/index.ts +8 -0
- package/src/model.ts +289 -162
- package/src/types.ts +2 -0
- package/src/useWheelScroll.ts +0 -4
- 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,
|
|
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,
|
|
156
|
+
function featurePanelSpans({ panel, width, colWidth, domainBands, annotationsByRow, shifts, }) {
|
|
126
157
|
if (panel.x === 'column') {
|
|
127
|
-
return
|
|
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
|
-
//
|
|
156
|
-
//
|
|
157
|
-
const drawable = Math.max(1, width -
|
|
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
|
|
194
|
+
return panelLanes(panel, shifted.map(([name, features]) => [
|
|
164
195
|
name,
|
|
165
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
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:
|
|
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
|
-
...
|
|
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 = {
|
|
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',
|