react-msaview 8.1.0 → 8.3.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 +104 -117
- package/bundle/index.js.map +4 -4
- package/dist/columnCounts.d.ts +4 -3
- package/dist/columnCounts.js +11 -6
- package/dist/columnCounts.js.map +1 -1
- package/dist/components/CopyButton.d.ts +1 -1
- package/dist/components/CopyButton.js +1 -1
- package/dist/components/CopyButton.js.map +1 -1
- package/dist/components/DragHandle.js +14 -2
- package/dist/components/DragHandle.js.map +1 -1
- package/dist/components/Loading.js +10 -11
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +12 -1
- package/dist/components/MSAViewer.js +12 -1
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/SequenceTextArea.d.ts +11 -0
- package/dist/components/SequenceTextArea.js +40 -8
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/StatusMessage.d.ts +12 -0
- package/dist/components/StatusMessage.js +43 -0
- package/dist/components/StatusMessage.js.map +1 -0
- package/dist/components/Track.js +3 -3
- package/dist/components/Track.js.map +1 -1
- package/dist/components/VerticalScrollbar.js +6 -1
- package/dist/components/VerticalScrollbar.js.map +1 -1
- package/dist/components/dialogs/ExportSVGDialog.d.ts +3 -2
- package/dist/components/dialogs/ExportSVGDialog.js +4 -2
- package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
- package/dist/components/dialogs/FeatureDialog.js +15 -10
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/getVisibleLeaves.d.ts +1 -1
- package/dist/components/header/FileMenu.js +9 -0
- package/dist/components/header/FileMenu.js.map +1 -1
- package/dist/components/header/GappynessSlider.js +15 -3
- package/dist/components/header/GappynessSlider.js.map +1 -1
- package/dist/components/header/Header.js +6 -7
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/{HeaderStatusArea.d.ts → RowSearch.d.ts} +2 -2
- package/dist/components/header/RowSearch.js +15 -0
- package/dist/components/header/RowSearch.js.map +1 -0
- package/dist/components/header/revealRow.d.ts +3 -0
- package/dist/components/header/revealRow.js +15 -0
- package/dist/components/header/revealRow.js.map +1 -0
- package/dist/components/header/settingsMenuItems.js +57 -1
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/import/ImportForm.js +10 -4
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/import/ImportFormExamples.js +19 -27
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/minimap/Minimap.js +6 -1
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/msa/AnnotationLegend.js +11 -5
- package/dist/components/msa/AnnotationLegend.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +6 -1
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +3 -7
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/msaKeys.d.ts +5 -0
- package/dist/components/msa/msaKeys.js +35 -0
- package/dist/components/msa/msaKeys.js.map +1 -0
- package/dist/components/msa/useMsaBlockMouse.d.ts +2 -4
- package/dist/components/msa/useMsaBlockMouse.js +18 -5
- package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
- package/dist/components/rowpanels/RowPanelBlock.js +10 -14
- package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
- package/dist/components/tracks/drawTracks.d.ts +1 -0
- package/dist/components/tracks/drawTracks.js +14 -2
- package/dist/components/tracks/drawTracks.js.map +1 -1
- package/dist/components/tracks/useTrackHover.d.ts +1 -5
- package/dist/components/tracks/useTrackHover.js +5 -5
- package/dist/components/tracks/useTrackHover.js.map +1 -1
- package/dist/components/tree/ScaleBarSVG.d.ts +14 -0
- package/dist/components/tree/ScaleBarSVG.js +11 -0
- package/dist/components/tree/ScaleBarSVG.js.map +1 -0
- package/dist/components/tree/TreeCanvas.js +8 -26
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +11 -20
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/{TreeBranchMenu.d.ts → TreeMenu.d.ts} +5 -4
- package/dist/components/tree/TreeMenu.js +48 -0
- package/dist/components/tree/TreeMenu.js.map +1 -0
- package/dist/components/tree/TreeRuler.js +3 -3
- package/dist/components/tree/TreeRuler.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +19 -9
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/renderTreeMouseover.d.ts +5 -0
- package/dist/components/tree/renderTreeMouseover.js +20 -0
- package/dist/components/tree/renderTreeMouseover.js.map +1 -0
- package/dist/components/tree/useTreeHover.d.ts +3 -8
- package/dist/components/tree/useTreeHover.js +8 -10
- package/dist/components/tree/useTreeHover.js.map +1 -1
- package/dist/components/useHoverAnchor.d.ts +19 -0
- package/dist/components/useHoverAnchor.js +18 -0
- package/dist/components/useHoverAnchor.js.map +1 -0
- package/dist/constants.d.ts +4 -1
- package/dist/constants.js +12 -3
- 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/exportFileName.d.ts +3 -3
- package/dist/exportFileName.js +4 -4
- package/dist/exportFileName.js.map +1 -1
- package/dist/hierarchy.d.ts +51 -0
- package/dist/hierarchy.js +125 -0
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/model/treeModel.d.ts +12 -0
- package/dist/model/treeModel.js +14 -1
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +188 -47
- package/dist/model.js +482 -192
- package/dist/model.js.map +1 -1
- package/dist/renderToSvg.js +2 -4
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rerootTree.d.ts +33 -0
- package/dist/rerootTree.js +144 -0
- package/dist/rerootTree.js.map +1 -0
- package/dist/svgTestUtil.d.ts +114 -20
- package/dist/types.d.ts +25 -12
- package/dist/useMsaSvgFigure.d.ts +2 -1
- package/dist/useMsaSvgFigure.js +11 -4
- package/dist/useMsaSvgFigure.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/writeNewick.d.ts +8 -0
- package/dist/writeNewick.js +34 -0
- package/dist/writeNewick.js.map +1 -0
- package/package.json +3 -3
- package/src/columnCounts.ts +14 -6
- package/src/components/CopyButton.tsx +2 -2
- package/src/components/DragHandle.tsx +21 -1
- package/src/components/Loading.tsx +14 -30
- package/src/components/MSAViewer.tsx +26 -0
- package/src/components/SequenceTextArea.tsx +54 -8
- package/src/components/StatusMessage.tsx +70 -0
- package/src/components/Track.tsx +8 -4
- package/src/components/VerticalScrollbar.tsx +7 -0
- package/src/components/dialogs/ExportSVGDialog.tsx +5 -2
- package/src/components/dialogs/FeatureDialog.tsx +59 -38
- package/src/components/header/FileMenu.tsx +9 -0
- package/src/components/header/GappynessSlider.tsx +17 -4
- package/src/components/header/Header.tsx +7 -8
- package/src/components/header/RowSearch.tsx +34 -0
- package/src/components/header/revealRow.ts +17 -0
- package/src/components/header/settingsMenuItems.ts +66 -0
- package/src/components/import/ImportForm.tsx +14 -7
- package/src/components/import/ImportFormExamples.tsx +20 -28
- package/src/components/minimap/Minimap.tsx +7 -0
- package/src/components/msa/AnnotationLegend.tsx +12 -6
- package/src/components/msa/MSACanvas.tsx +8 -0
- package/src/components/msa/MSACanvasBlock.tsx +5 -11
- package/src/components/msa/msaKeys.ts +39 -0
- package/src/components/msa/useMsaBlockMouse.ts +23 -7
- package/src/components/rowpanels/RowPanelBlock.tsx +16 -23
- package/src/components/tracks/drawTracks.ts +15 -2
- package/src/components/tracks/useTrackHover.ts +5 -5
- package/src/components/tree/ScaleBarSVG.tsx +30 -0
- package/src/components/tree/TreeCanvas.tsx +9 -40
- package/src/components/tree/TreeCanvasBlock.tsx +19 -39
- package/src/components/tree/TreeMenu.tsx +129 -0
- package/src/components/tree/TreeRuler.tsx +7 -13
- package/src/components/tree/renderTreeCanvas.ts +20 -8
- package/src/components/tree/renderTreeMouseover.ts +41 -0
- package/src/components/tree/useTreeHover.ts +8 -17
- package/src/components/useHoverAnchor.ts +31 -0
- package/src/constants.ts +14 -3
- package/src/expandSpec.ts +196 -0
- package/src/exportFileName.ts +4 -3
- package/src/hierarchy.ts +151 -4
- package/src/index.ts +11 -1
- package/src/model/treeModel.ts +22 -0
- package/src/model.ts +588 -221
- package/src/renderToSvg.tsx +6 -9
- package/src/rerootTree.ts +172 -0
- package/src/types.ts +34 -12
- package/src/useMsaSvgFigure.ts +11 -4
- package/src/version.ts +1 -1
- package/src/writeNewick.ts +38 -0
- package/dist/components/PortalTooltip.d.ts +0 -11
- package/dist/components/PortalTooltip.js +0 -29
- package/dist/components/PortalTooltip.js.map +0 -1
- package/dist/components/header/HeaderStatusArea.js +0 -21
- package/dist/components/header/HeaderStatusArea.js.map +0 -1
- package/dist/components/tree/TreeBranchMenu.js +0 -29
- package/dist/components/tree/TreeBranchMenu.js.map +0 -1
- package/dist/components/tree/TreeNodeMenu.d.ts +0 -13
- package/dist/components/tree/TreeNodeMenu.js +0 -41
- package/dist/components/tree/TreeNodeMenu.js.map +0 -1
- package/src/components/PortalTooltip.tsx +0 -47
- package/src/components/header/HeaderStatusArea.tsx +0 -32
- package/src/components/tree/TreeBranchMenu.tsx +0 -68
- package/src/components/tree/TreeNodeMenu.tsx +0 -95
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, computed, transaction } from 'mobx';
|
|
5
|
+
import { autorun, compareStructural, computed, reaction, 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';
|
|
@@ -20,7 +20,7 @@ import { exportFileName } from './exportFileName.js';
|
|
|
20
20
|
import { featureField, featureName } from './featureFields.js';
|
|
21
21
|
import { fetchTextWithProgress, isAbortError } from './fetchUtils.js';
|
|
22
22
|
import { flatToTree } from './flatToTree.js';
|
|
23
|
-
import { calcDepthToLeaf, clusterLayout, collapse,
|
|
23
|
+
import { calcDepthToLeaf, clusterLayout, collapse, find, forEachDescendant, hierarchy, layoutTree, leafIndex, leaves, maxLength, mrca, nodeCoveringRows, setBrLength, sort, sum as hierarchySum, } from './hierarchy.js';
|
|
24
24
|
import { measureTextCanvas } from './measureTextCanvas.js';
|
|
25
25
|
import { DataModelF } from './model/DataModel.js';
|
|
26
26
|
import { DialogQueueSessionMixin } from './model/DialogQueue.js';
|
|
@@ -29,6 +29,7 @@ import { TreeModelF } from './model/treeModel.js';
|
|
|
29
29
|
import { calculateNeighborJoiningTree } from './neighborJoining.js';
|
|
30
30
|
import { parseAsn1 } from './parseAsn1.js';
|
|
31
31
|
import { calculatePropertyConservation } from './propertyConservation.js';
|
|
32
|
+
import { midpointRoot, rerootTree } from './rerootTree.js';
|
|
32
33
|
import { globalColToVisibleCol, seqPosOfGlobalCol, visibleColToGlobalCol, visibleColsBefore, } from './rowCoordinateCalculations.js';
|
|
33
34
|
import { resolveScale } from './scales.js';
|
|
34
35
|
import { buildSeqPosIndex } from './seqPosToGlobalCol.js';
|
|
@@ -36,6 +37,7 @@ import { maxBitsFor } from './sequenceLogo.js';
|
|
|
36
37
|
import { stripDefault } from './stripDefault.js';
|
|
37
38
|
import { computeRowInsertions, dropBlanks, len, outlineColor, skipBlanks, withAlpha, } from './util.js';
|
|
38
39
|
import { saveAs } from './vendor/fileSaver.js';
|
|
40
|
+
import { writeNewick } from './writeNewick.js';
|
|
39
41
|
import { parseWuss } from './wuss.js';
|
|
40
42
|
function parseTreeText(text) {
|
|
41
43
|
if (text.startsWith('BioTreeContainer')) {
|
|
@@ -73,10 +75,24 @@ const UNSCALED_FEATURE = '#d9d9d9';
|
|
|
73
75
|
// its own in a `features` panel
|
|
74
76
|
const laneOverlap = 0.1;
|
|
75
77
|
// a row panel's own width, or a default: the row height, which makes a strip
|
|
76
|
-
// cell square, and a gene neighborhood's width for a features panel
|
|
77
|
-
|
|
78
|
+
// cell square, and a gene neighborhood's width for a features panel. The row
|
|
79
|
+
// height is read only when used, so a panel with a width of its own does not
|
|
80
|
+
// recompute on a vertical zoom
|
|
81
|
+
function rowPanelWidth(panel, view) {
|
|
78
82
|
return (panel.width ??
|
|
79
|
-
(panel.kind === 'features' ? defaultFeaturePanelWidth : rowHeight));
|
|
83
|
+
(panel.kind === 'features' ? defaultFeaturePanelWidth : view.rowHeight));
|
|
84
|
+
}
|
|
85
|
+
// the color each row takes from a field of the row table under a scale
|
|
86
|
+
function colorByRow(rowData, field, colorOf) {
|
|
87
|
+
const colors = new Map();
|
|
88
|
+
for (const [name, row] of Object.entries(rowData)) {
|
|
89
|
+
const value = row?.[field];
|
|
90
|
+
const color = value === undefined ? undefined : colorOf(value);
|
|
91
|
+
if (color) {
|
|
92
|
+
colors.set(name, color);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return colors;
|
|
80
96
|
}
|
|
81
97
|
// the fill and outline of every feature in a list: its own GFF `color=` first,
|
|
82
98
|
// then the scale an encoding resolves over one of its fields, then the
|
|
@@ -209,10 +225,19 @@ function resolveFeatureScale(field, scale, annotations) {
|
|
|
209
225
|
...resolveScale(scale, annotations.map(a => featureField(a, field)).filter(notEmpty)),
|
|
210
226
|
};
|
|
211
227
|
}
|
|
228
|
+
const columnTrackBytes = new WeakMap();
|
|
229
|
+
function columnTrackSize(track) {
|
|
230
|
+
let bytes = columnTrackBytes.get(track);
|
|
231
|
+
if (bytes === undefined) {
|
|
232
|
+
bytes = JSON.stringify(track).length;
|
|
233
|
+
columnTrackBytes.set(track, bytes);
|
|
234
|
+
}
|
|
235
|
+
return bytes;
|
|
236
|
+
}
|
|
212
237
|
// a data track over this size stays in the live model but leaves the snapshot,
|
|
213
238
|
// the same rule DataModel applies to an inline document
|
|
214
239
|
function columnTrackSizes(tracks) {
|
|
215
|
-
return (tracks ?? []).map(
|
|
240
|
+
return (tracks ?? []).map(columnTrackSize);
|
|
216
241
|
}
|
|
217
242
|
function smallColumnTracks(tracks) {
|
|
218
243
|
const sizes = columnTrackSizes(tracks);
|
|
@@ -246,6 +271,7 @@ export const preservedOnReset = new Set([
|
|
|
246
271
|
'treeAreaWidth',
|
|
247
272
|
'treeWidth',
|
|
248
273
|
'showBranchLen',
|
|
274
|
+
'treeOrder',
|
|
249
275
|
'drawTree',
|
|
250
276
|
'drawNodeBubbles',
|
|
251
277
|
'drawNodeLabels',
|
|
@@ -266,6 +292,8 @@ export const preservedOnReset = new Set([
|
|
|
266
292
|
function trackIsOff(turnedOffTracks, id, defaultOff) {
|
|
267
293
|
return turnedOffTracks.get(id) ?? (defaultOff || defaultOffTracks.has(id));
|
|
268
294
|
}
|
|
295
|
+
// the marks that set tree state from a node rather than drawing
|
|
296
|
+
const seedingMarks = new Set(['collapse', 'focus', 'rotate']);
|
|
269
297
|
// shared empty results, so observers don't see a fresh [] as a change
|
|
270
298
|
const noDomains = [];
|
|
271
299
|
const noClades = [];
|
|
@@ -309,14 +337,13 @@ function cladeRows(clade, root, index, rowNamesSet) {
|
|
|
309
337
|
* Every clade that resolves, with its fill color settled, against the leaf
|
|
310
338
|
* order `rowNamesSet` gives. The tree panel resolves against the displayed
|
|
311
339
|
* rows and the overview against its own, so both take the tree they draw. A
|
|
312
|
-
* `range` record names no node, so
|
|
313
|
-
* drop it.
|
|
340
|
+
* `range` record names no node, so the seeding marks, which need one, drop it.
|
|
314
341
|
*/
|
|
315
342
|
function resolveClades(clades, root, rowNamesSet) {
|
|
316
343
|
const index = leafIndex(root);
|
|
317
344
|
return clades.flatMap(clade => {
|
|
318
345
|
const resolved = cladeRows(clade, root, index, rowNamesSet);
|
|
319
|
-
const seeding =
|
|
346
|
+
const seeding = seedingMarks.has(clade.mark);
|
|
320
347
|
return resolved && !(seeding && resolved.nodeId === undefined)
|
|
321
348
|
? [
|
|
322
349
|
{
|
|
@@ -330,16 +357,33 @@ function resolveClades(clades, root, rowNamesSet) {
|
|
|
330
357
|
: [];
|
|
331
358
|
});
|
|
332
359
|
}
|
|
360
|
+
const childOrders = {
|
|
361
|
+
input: undefined,
|
|
362
|
+
branchLength: (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1),
|
|
363
|
+
ladderize: (a, b) => a.value - b.value,
|
|
364
|
+
ladderizeReverse: (a, b) => b.value - a.value,
|
|
365
|
+
};
|
|
333
366
|
/**
|
|
334
367
|
* The tree with the display transforms applied: leaf counts summed, children
|
|
335
|
-
*
|
|
336
|
-
* `collapsed` clade folded. The overview builds the same
|
|
337
|
-
* focus, so it shows the focused subtree inside the whole.
|
|
368
|
+
* put in `order` and each `rotated` node's reversed, `showOnly` taken as the
|
|
369
|
+
* new root, and each `collapsed` clade folded. The overview builds the same
|
|
370
|
+
* tree without the focus, so it shows the focused subtree inside the whole.
|
|
338
371
|
*/
|
|
339
|
-
function buildTreeRoot(tree, collapsed, showOnly) {
|
|
372
|
+
function buildTreeRoot(tree, { collapsed, rotated, order, showOnly, }) {
|
|
340
373
|
let hier = hierarchy(tree, d => d.children);
|
|
341
374
|
hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1));
|
|
342
|
-
|
|
375
|
+
const compare = childOrders[order];
|
|
376
|
+
if (compare) {
|
|
377
|
+
sort(hier, compare);
|
|
378
|
+
}
|
|
379
|
+
if (rotated.length > 0) {
|
|
380
|
+
const flip = new Set(rotated);
|
|
381
|
+
forEachDescendant(hier, n => {
|
|
382
|
+
if (flip.has(n.data.id)) {
|
|
383
|
+
n.children?.reverse();
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
343
387
|
if (showOnly) {
|
|
344
388
|
const res = find(hier, n => n.data.id === showOnly);
|
|
345
389
|
if (res) {
|
|
@@ -359,6 +403,48 @@ function buildTreeRoot(tree, collapsed, showOnly) {
|
|
|
359
403
|
}
|
|
360
404
|
return hier;
|
|
361
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* The node whose branch an outgroup roots the tree on: the outgroup's most
|
|
408
|
+
* recent common ancestor, or the rest of the tips' where that ancestor is the
|
|
409
|
+
* root, since the outgroup then straddles the root and the branch above the
|
|
410
|
+
* other side is the same branch of the unrooted tree.
|
|
411
|
+
*/
|
|
412
|
+
function outgroupNode(root, outgroup) {
|
|
413
|
+
const index = leafIndex(root);
|
|
414
|
+
const node = mrca(root, outgroup, index);
|
|
415
|
+
if (node !== root) {
|
|
416
|
+
return node;
|
|
417
|
+
}
|
|
418
|
+
const out = new Set(outgroup);
|
|
419
|
+
const rest = leaves(root)
|
|
420
|
+
.map(n => n.data.name)
|
|
421
|
+
.filter(name => !out.has(name));
|
|
422
|
+
const other = mrca(root, rest, index);
|
|
423
|
+
return other === root ? undefined : other;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* `tree` rerooted as `treeRoot` asks, with fresh path ids. The ids of the tree
|
|
427
|
+
* as written become names on unnamed nodes, and a rerooting would carry them
|
|
428
|
+
* along as labels, so they come off first.
|
|
429
|
+
*/
|
|
430
|
+
function rootTree(tree, treeRoot) {
|
|
431
|
+
const plain = new Map();
|
|
432
|
+
for (const node of preorder(tree).reverse()) {
|
|
433
|
+
plain.set(node, {
|
|
434
|
+
name: node.name === node.id ? undefined : node.name,
|
|
435
|
+
length: node.length,
|
|
436
|
+
children: node.children.map(c => plain.get(c)),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
const input = plain.get(tree);
|
|
440
|
+
if (treeRoot === 'midpoint') {
|
|
441
|
+
return generateNodeIds(midpointRoot(input));
|
|
442
|
+
}
|
|
443
|
+
const target = outgroupNode(hierarchy(tree, d => d.children), treeRoot.outgroup);
|
|
444
|
+
return target
|
|
445
|
+
? generateNodeIds(rerootTree(input, plain.get(target.data)))
|
|
446
|
+
: tree;
|
|
447
|
+
}
|
|
362
448
|
// the inclusive tip indices a node covers, from the row-space extent
|
|
363
449
|
// clusterLayout writes to xMin/xMax as tip centers
|
|
364
450
|
function tipRange(node) {
|
|
@@ -535,6 +621,20 @@ function stateModelFactory() {
|
|
|
535
621
|
* hidden)
|
|
536
622
|
*/
|
|
537
623
|
collapsed: stripDefault(types.array(types.string), []),
|
|
624
|
+
/**
|
|
625
|
+
* #property
|
|
626
|
+
* tree nodes whose children draw in reverse order, which is ggtree's
|
|
627
|
+
* `rotate`: the same tree, with the clades on either side of the node
|
|
628
|
+
* swapped
|
|
629
|
+
*/
|
|
630
|
+
rotated: stripDefault(types.array(types.string), []),
|
|
631
|
+
/**
|
|
632
|
+
* #property
|
|
633
|
+
* where the tree is rooted: `midpoint` roots it halfway along the
|
|
634
|
+
* longest path between two tips, `{outgroup}` on the branch above
|
|
635
|
+
* the tips named, and undefined keeps the file's root
|
|
636
|
+
*/
|
|
637
|
+
treeRoot: types.frozen(),
|
|
538
638
|
/**
|
|
539
639
|
* #property
|
|
540
640
|
* focus on particular subtree
|
|
@@ -606,12 +706,21 @@ function stateModelFactory() {
|
|
|
606
706
|
* Persists in the snapshot and the URL.
|
|
607
707
|
*/
|
|
608
708
|
highlights: stripDefault(types.array(types.frozen()), []),
|
|
709
|
+
/**
|
|
710
|
+
* #property
|
|
711
|
+
* where the view opens, in `highlights` coordinates: `{row, start,
|
|
712
|
+
* end}` zooms onto residues of that row, `{start, end}` onto alignment
|
|
713
|
+
* columns. Applied once the alignment and any tree file have loaded,
|
|
714
|
+
* then cleared, so a reloaded session keeps the reader's own scroll.
|
|
715
|
+
*/
|
|
716
|
+
region: types.frozen(),
|
|
609
717
|
/**
|
|
610
718
|
* #property
|
|
611
719
|
* clades of the tree with a mark drawn over them. `mrca` names tips
|
|
612
720
|
* whose common ancestor is the clade, or `range` its first and last
|
|
613
721
|
* tip in display order, and `tips` is the leaf count the producer
|
|
614
|
-
* measured. `mark` is `highlight`, `bracket`, `collapse` or
|
|
722
|
+
* measured. `mark` is `highlight`, `bracket`, `collapse`, `focus` or
|
|
723
|
+
* `rotate`.
|
|
615
724
|
* See docs/layers.md
|
|
616
725
|
*/
|
|
617
726
|
clades: stripDefault(types.array(types.frozen()), []),
|
|
@@ -727,9 +836,15 @@ function stateModelFactory() {
|
|
|
727
836
|
* #volatile
|
|
728
837
|
* non-fatal load problems: an optional layer that failed to load, an
|
|
729
838
|
* overlay that failed to parse. `error` replaces the view and is for the
|
|
730
|
-
* alignment itself
|
|
839
|
+
* alignment itself. `warnings` adds the ones derived from the data
|
|
840
|
+
*/
|
|
841
|
+
loadWarnings: [],
|
|
842
|
+
/**
|
|
843
|
+
* #volatile
|
|
844
|
+
* derived warnings the user dismissed, which stay hidden while the data
|
|
845
|
+
* behind them holds
|
|
731
846
|
*/
|
|
732
|
-
|
|
847
|
+
dismissedWarnings: [],
|
|
733
848
|
/**
|
|
734
849
|
* #volatile
|
|
735
850
|
* bumped by reset(). The error boundary above the view uses it as its key,
|
|
@@ -818,16 +933,10 @@ function stateModelFactory() {
|
|
|
818
933
|
* that failed to parse. A message already on the list is not added again
|
|
819
934
|
*/
|
|
820
935
|
addWarning(warning) {
|
|
821
|
-
if (!self.
|
|
822
|
-
self.
|
|
936
|
+
if (!self.loadWarnings.includes(warning)) {
|
|
937
|
+
self.loadWarnings = [...self.loadWarnings, warning];
|
|
823
938
|
}
|
|
824
939
|
},
|
|
825
|
-
/**
|
|
826
|
-
* #action
|
|
827
|
-
*/
|
|
828
|
-
clearWarnings() {
|
|
829
|
-
self.warnings = [];
|
|
830
|
-
},
|
|
831
940
|
/**
|
|
832
941
|
* #action
|
|
833
942
|
* declare that this host restores the loaded documents itself, which
|
|
@@ -859,6 +968,12 @@ function stateModelFactory() {
|
|
|
859
968
|
setHighlightedColumns(columns) {
|
|
860
969
|
self.highlightedColumns = columns;
|
|
861
970
|
},
|
|
971
|
+
/**
|
|
972
|
+
* #action
|
|
973
|
+
*/
|
|
974
|
+
setRegion(region) {
|
|
975
|
+
self.region = region;
|
|
976
|
+
},
|
|
862
977
|
/**
|
|
863
978
|
* #action
|
|
864
979
|
*/
|
|
@@ -938,8 +1053,9 @@ function stateModelFactory() {
|
|
|
938
1053
|
/**
|
|
939
1054
|
* #action
|
|
940
1055
|
* switch to another alignment of a multi-alignment file (Stockholm).
|
|
941
|
-
* Clears the collapsed node ids, the subtree in focus, the
|
|
942
|
-
* and the scroll position, which all refer to the
|
|
1056
|
+
* Clears the collapsed and rotated node ids, the subtree in focus, the
|
|
1057
|
+
* reference row and the scroll position, which all refer to the
|
|
1058
|
+
* previous alignment
|
|
943
1059
|
*/
|
|
944
1060
|
setCurrentAlignment(n) {
|
|
945
1061
|
if (n === self.currentAlignment) {
|
|
@@ -948,6 +1064,7 @@ function stateModelFactory() {
|
|
|
948
1064
|
transaction(() => {
|
|
949
1065
|
self.currentAlignment = n;
|
|
950
1066
|
self.collapsed.clear();
|
|
1067
|
+
self.rotated.clear();
|
|
951
1068
|
this.setShowOnly(undefined);
|
|
952
1069
|
this.drawRelativeTo(undefined);
|
|
953
1070
|
self.scrollX = 0;
|
|
@@ -966,6 +1083,40 @@ function stateModelFactory() {
|
|
|
966
1083
|
self.collapsed.push(node);
|
|
967
1084
|
}
|
|
968
1085
|
},
|
|
1086
|
+
/**
|
|
1087
|
+
* #action
|
|
1088
|
+
* reverse the order of the given node's children, or restore it
|
|
1089
|
+
*/
|
|
1090
|
+
toggleRotated(node) {
|
|
1091
|
+
if (self.rotated.includes(node)) {
|
|
1092
|
+
self.rotated.remove(node);
|
|
1093
|
+
}
|
|
1094
|
+
else {
|
|
1095
|
+
self.rotated.push(node);
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
/**
|
|
1099
|
+
* #action
|
|
1100
|
+
*/
|
|
1101
|
+
clearRotated() {
|
|
1102
|
+
self.rotated.clear();
|
|
1103
|
+
},
|
|
1104
|
+
/**
|
|
1105
|
+
* #action
|
|
1106
|
+
* reroot the tree. Clears `collapsed`, `rotated` and `showOnly`, whose
|
|
1107
|
+
* path-derived node ids name other nodes in the rerooted tree
|
|
1108
|
+
*/
|
|
1109
|
+
setTreeRoot(treeRoot) {
|
|
1110
|
+
if (compareStructural(self.treeRoot, treeRoot)) {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
transaction(() => {
|
|
1114
|
+
self.treeRoot = treeRoot;
|
|
1115
|
+
self.collapsed.clear();
|
|
1116
|
+
self.rotated.clear();
|
|
1117
|
+
this.setShowOnly(undefined);
|
|
1118
|
+
});
|
|
1119
|
+
},
|
|
969
1120
|
/**
|
|
970
1121
|
* #action
|
|
971
1122
|
* show only the subtree rooted at the given node id (pass undefined to
|
|
@@ -1120,12 +1271,13 @@ function stateModelFactory() {
|
|
|
1120
1271
|
* from disk or pasted in becomes inline text, and DataModel drops an
|
|
1121
1272
|
* inline document past `maxInlineSnapshotBytes`.
|
|
1122
1273
|
*
|
|
1123
|
-
* The header lists these, and the standalone app
|
|
1124
|
-
* address bar while the list is non-empty
|
|
1125
|
-
*
|
|
1126
|
-
*
|
|
1274
|
+
* The header lists these, and the standalone app drops `?data=` from
|
|
1275
|
+
* the address bar while the list is non-empty or the encoded link would
|
|
1276
|
+
* pass 8,000 characters, so a copied link never opens an empty viewer
|
|
1277
|
+
* unannounced. A document fetched from a URL never appears here, since
|
|
1278
|
+
* the snapshot keeps its filehandle.
|
|
1127
1279
|
*
|
|
1128
|
-
* Empty when `
|
|
1280
|
+
* Empty when `hostCarriesData` is true.
|
|
1129
1281
|
*/
|
|
1130
1282
|
get unshareableData() {
|
|
1131
1283
|
if (self.hostRestoresData) {
|
|
@@ -1216,8 +1368,9 @@ function stateModelFactory() {
|
|
|
1216
1368
|
},
|
|
1217
1369
|
/**
|
|
1218
1370
|
* #getter
|
|
1371
|
+
* the tree the file or the alignment gives, before `treeRoot`
|
|
1219
1372
|
*/
|
|
1220
|
-
get
|
|
1373
|
+
get inputTree() {
|
|
1221
1374
|
const text = self.data.tree;
|
|
1222
1375
|
return text
|
|
1223
1376
|
? generateNodeIds(parseTreeText(text))
|
|
@@ -1228,6 +1381,18 @@ function stateModelFactory() {
|
|
|
1228
1381
|
name: 'empty',
|
|
1229
1382
|
});
|
|
1230
1383
|
},
|
|
1384
|
+
/**
|
|
1385
|
+
* #getter
|
|
1386
|
+
* the tree as the file or the alignment gives it, rerooted where
|
|
1387
|
+
* `treeRoot` asks. Every node id, and so `collapsed`, `rotated` and
|
|
1388
|
+
* `showOnly`, refers to this tree.
|
|
1389
|
+
*/
|
|
1390
|
+
get tree() {
|
|
1391
|
+
const tree = this.inputTree;
|
|
1392
|
+
return self.treeRoot && !tree.noTree
|
|
1393
|
+
? rootTree(tree, self.treeRoot)
|
|
1394
|
+
: tree;
|
|
1395
|
+
},
|
|
1231
1396
|
/**
|
|
1232
1397
|
* #getter
|
|
1233
1398
|
* Returns the list of row (sequence) names in display order.
|
|
@@ -1247,8 +1412,8 @@ function stateModelFactory() {
|
|
|
1247
1412
|
* `clades` resolved to the rows each one covers. The tip names resolve
|
|
1248
1413
|
* against `tree` rather than `root`, so a clade whose ancestor the user
|
|
1249
1414
|
* collapsed keeps its rows. One leaf pass over the tree serves every
|
|
1250
|
-
* clade. A `range` record names no node, so `collapse
|
|
1251
|
-
* need one, drop it.
|
|
1415
|
+
* clade. A `range` record names no node, so `collapse`, `focus` and
|
|
1416
|
+
* `rotate`, which need one, drop it.
|
|
1252
1417
|
*/
|
|
1253
1418
|
get resolvedClades() {
|
|
1254
1419
|
if (self.clades.length === 0) {
|
|
@@ -1301,7 +1466,24 @@ function stateModelFactory() {
|
|
|
1301
1466
|
* #getter
|
|
1302
1467
|
*/
|
|
1303
1468
|
get root() {
|
|
1304
|
-
return buildTreeRoot(this.tree,
|
|
1469
|
+
return buildTreeRoot(this.tree, {
|
|
1470
|
+
collapsed: self.collapsed,
|
|
1471
|
+
rotated: self.rotated,
|
|
1472
|
+
order: self.treeOrder,
|
|
1473
|
+
showOnly: self.showOnly,
|
|
1474
|
+
});
|
|
1475
|
+
},
|
|
1476
|
+
/**
|
|
1477
|
+
* #getter
|
|
1478
|
+
* the tree as Newick in the order and with the root it is drawn, with
|
|
1479
|
+
* every clade, collapsed or out of focus
|
|
1480
|
+
*/
|
|
1481
|
+
get treeNewick() {
|
|
1482
|
+
return writeNewick(buildTreeRoot(this.tree, {
|
|
1483
|
+
collapsed: [],
|
|
1484
|
+
rotated: self.rotated,
|
|
1485
|
+
order: self.treeOrder,
|
|
1486
|
+
}));
|
|
1305
1487
|
},
|
|
1306
1488
|
/**
|
|
1307
1489
|
* #getter
|
|
@@ -1323,7 +1505,11 @@ function stateModelFactory() {
|
|
|
1323
1505
|
if (!self.showTreeOverview) {
|
|
1324
1506
|
return undefined;
|
|
1325
1507
|
}
|
|
1326
|
-
const root = buildTreeRoot(this.tree,
|
|
1508
|
+
const root = buildTreeRoot(this.tree, {
|
|
1509
|
+
collapsed: self.collapsed,
|
|
1510
|
+
rotated: self.rotated,
|
|
1511
|
+
order: self.treeOrder,
|
|
1512
|
+
});
|
|
1327
1513
|
const numTips = leaves(root).length;
|
|
1328
1514
|
clusterLayout(root, numTips, 1);
|
|
1329
1515
|
const rootLen = Math.max(root.data.length || 0, 0);
|
|
@@ -1395,7 +1581,7 @@ function stateModelFactory() {
|
|
|
1395
1581
|
* alignment, the sum of each record's width
|
|
1396
1582
|
*/
|
|
1397
1583
|
get rowPanelsWidth() {
|
|
1398
|
-
return sum(self.rowPanels.map(p => rowPanelWidth(p, self
|
|
1584
|
+
return sum(self.rowPanels.map(p => rowPanelWidth(p, self)));
|
|
1399
1585
|
},
|
|
1400
1586
|
/**
|
|
1401
1587
|
* #getter
|
|
@@ -1439,9 +1625,10 @@ function stateModelFactory() {
|
|
|
1439
1625
|
return [];
|
|
1440
1626
|
}
|
|
1441
1627
|
// ragged input (a3m, hand-edited fasta) can have rows shorter than the
|
|
1442
|
-
// alignment; the widest row
|
|
1443
|
-
//
|
|
1444
|
-
|
|
1628
|
+
// alignment; the widest row of the whole alignment, collapsed rows
|
|
1629
|
+
// included, defines the column count as it does for numColumns, and a
|
|
1630
|
+
// row that stops early counts as gapped for the remainder
|
|
1631
|
+
const numCols = this.MSA?.getWidth() ?? 0;
|
|
1445
1632
|
const numRows = strs.length;
|
|
1446
1633
|
const threshold = Math.ceil((realAllowedGappyness / 100) * numRows);
|
|
1447
1634
|
const blankCounts = new Uint32Array(numCols);
|
|
@@ -1563,7 +1750,7 @@ function stateModelFactory() {
|
|
|
1563
1750
|
* #getter
|
|
1564
1751
|
*/
|
|
1565
1752
|
get colStats() {
|
|
1566
|
-
return columnCountsFromRows(this.columns2d);
|
|
1753
|
+
return columnCountsFromRows(this.columns2d, this.numColumns);
|
|
1567
1754
|
},
|
|
1568
1755
|
/**
|
|
1569
1756
|
* #getter
|
|
@@ -1655,38 +1842,39 @@ function stateModelFactory() {
|
|
|
1655
1842
|
},
|
|
1656
1843
|
/**
|
|
1657
1844
|
* #getter
|
|
1658
|
-
*
|
|
1845
|
+
* `root` laid out on new nodes in row and branch-length units, read in
|
|
1846
|
+
* pixels through `rowHeight` and `treeWidth`. A zoom leaves it and every
|
|
1847
|
+
* node in it as they are, and an observer reading a node's `x` or `len`
|
|
1848
|
+
* observes the size it scales by.
|
|
1659
1849
|
*/
|
|
1660
|
-
get
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
// for each collapsed clade, record the pixel x-position of its farthest
|
|
1669
|
-
// tip so the renderer can draw a triangle spanning the branch-length
|
|
1670
|
-
// extent of the hidden subtree
|
|
1671
|
-
forEachDescendant(r, node => {
|
|
1672
|
-
if (node._children) {
|
|
1673
|
-
node.collapsedTipXFar =
|
|
1674
|
-
(node.len ?? 0) + collapsedSubtreeMaxLength(node) * k;
|
|
1675
|
-
}
|
|
1850
|
+
get treeLayout() {
|
|
1851
|
+
return layoutTree(this.root, {
|
|
1852
|
+
get rowHeight() {
|
|
1853
|
+
return self.rowHeight;
|
|
1854
|
+
},
|
|
1855
|
+
get treeWidth() {
|
|
1856
|
+
return self.treeWidth;
|
|
1857
|
+
},
|
|
1676
1858
|
});
|
|
1677
|
-
|
|
1859
|
+
},
|
|
1860
|
+
/**
|
|
1861
|
+
* #getter
|
|
1862
|
+
* the laid-out tree, with pixel positions on every node
|
|
1863
|
+
*/
|
|
1864
|
+
get hierarchy() {
|
|
1865
|
+
return this.treeLayout.root;
|
|
1678
1866
|
},
|
|
1679
1867
|
/**
|
|
1680
1868
|
* #getter
|
|
1681
1869
|
*/
|
|
1682
1870
|
get totalHeight() {
|
|
1683
|
-
return
|
|
1871
|
+
return this.leaves.length * self.rowHeight;
|
|
1684
1872
|
},
|
|
1685
1873
|
/**
|
|
1686
1874
|
* #getter
|
|
1687
1875
|
*/
|
|
1688
1876
|
get leaves() {
|
|
1689
|
-
return
|
|
1877
|
+
return this.treeLayout.leaves;
|
|
1690
1878
|
},
|
|
1691
1879
|
/**
|
|
1692
1880
|
* #getter
|
|
@@ -1694,8 +1882,7 @@ function stateModelFactory() {
|
|
|
1694
1882
|
* the tree's own units
|
|
1695
1883
|
*/
|
|
1696
1884
|
get rootToTipLength() {
|
|
1697
|
-
|
|
1698
|
-
return maxLength(r) - Math.max(r.data.length || 0, 0);
|
|
1885
|
+
return this.treeLayout.rootToTipLength;
|
|
1699
1886
|
},
|
|
1700
1887
|
/**
|
|
1701
1888
|
* #getter
|
|
@@ -1719,7 +1906,7 @@ function stateModelFactory() {
|
|
|
1719
1906
|
* max topological depth to a tip, used to scale cladogram x-positions
|
|
1720
1907
|
*/
|
|
1721
1908
|
get maxDepthToLeaf() {
|
|
1722
|
-
return
|
|
1909
|
+
return this.treeLayout.root.depthToLeaf;
|
|
1723
1910
|
},
|
|
1724
1911
|
/**
|
|
1725
1912
|
* #getter
|
|
@@ -1928,12 +2115,6 @@ function stateModelFactory() {
|
|
|
1928
2115
|
get secondaryStructureConsensus() {
|
|
1929
2116
|
return self.MSA?.secondaryStructureConsensus;
|
|
1930
2117
|
},
|
|
1931
|
-
/**
|
|
1932
|
-
* #getter
|
|
1933
|
-
*/
|
|
1934
|
-
get seqConsensus() {
|
|
1935
|
-
return self.MSA?.seqConsensus;
|
|
1936
|
-
},
|
|
1937
2118
|
/**
|
|
1938
2119
|
* #getter
|
|
1939
2120
|
* the base pairs of the consensus secondary structure, as arcs, in
|
|
@@ -2066,11 +2247,13 @@ function stateModelFactory() {
|
|
|
2066
2247
|
},
|
|
2067
2248
|
/**
|
|
2068
2249
|
* #method
|
|
2069
|
-
* Return a row
|
|
2250
|
+
* Return the character a row holds at a visible column: a residue, or
|
|
2251
|
+
* the gap character (`-` or `.`) on a gap. Undefined for a row name the
|
|
2252
|
+
* alignment does not have or a column past the row's end.
|
|
2070
2253
|
*
|
|
2071
2254
|
* @param rowName - The name of the row
|
|
2072
2255
|
* @param visibleCol - The visible column index (what the user sees on screen)
|
|
2073
|
-
* @returns The
|
|
2256
|
+
* @returns The character at that position
|
|
2074
2257
|
*/
|
|
2075
2258
|
visibleColToRowLetter(rowName, visibleCol) {
|
|
2076
2259
|
return self.rowMap.get(rowName)?.[this.visibleColToGlobalCol(visibleCol)];
|
|
@@ -2157,6 +2340,21 @@ function stateModelFactory() {
|
|
|
2157
2340
|
// row, otherwise one past the last column
|
|
2158
2341
|
return col ?? (seqPos === 0 ? 0 : seq.length);
|
|
2159
2342
|
},
|
|
2343
|
+
/**
|
|
2344
|
+
* #method
|
|
2345
|
+
* the global column of a span's last residue, clamping a position past
|
|
2346
|
+
* the end of the row to its last residue. Undefined for a row name the
|
|
2347
|
+
* alignment does not have, or a row with no residues.
|
|
2348
|
+
*
|
|
2349
|
+
* @param rowName - The name of the row
|
|
2350
|
+
* @param seqPos - The sequence position (0-based, ungapped)
|
|
2351
|
+
*/
|
|
2352
|
+
seqEndToGlobalCol(rowName, seqPos) {
|
|
2353
|
+
const index = self.seqPosIndex(rowName);
|
|
2354
|
+
return index && index.length > 0
|
|
2355
|
+
? index[Math.min(seqPos, index.length - 1)]
|
|
2356
|
+
: undefined;
|
|
2357
|
+
},
|
|
2160
2358
|
/**
|
|
2161
2359
|
* #method
|
|
2162
2360
|
* Convert a sequence position (ungapped) directly to a visible column index.
|
|
@@ -2185,10 +2383,10 @@ function stateModelFactory() {
|
|
|
2185
2383
|
const start = Math.max(1, Math.floor(rawStart));
|
|
2186
2384
|
const end = Math.ceil(rawEnd);
|
|
2187
2385
|
let startGlobal = start - 1;
|
|
2188
|
-
let endGlobal = end - 1;
|
|
2386
|
+
let endGlobal = Math.min(end, self.MSA?.getWidth() ?? end) - 1;
|
|
2189
2387
|
if (row !== undefined) {
|
|
2190
2388
|
const rowStart = this.seqPosToGlobalCol(row, start - 1);
|
|
2191
|
-
const rowEnd = this.
|
|
2389
|
+
const rowEnd = this.seqEndToGlobalCol(row, end - 1);
|
|
2192
2390
|
if (rowStart === undefined || rowEnd === undefined) {
|
|
2193
2391
|
return undefined;
|
|
2194
2392
|
}
|
|
@@ -2443,6 +2641,28 @@ function stateModelFactory() {
|
|
|
2443
2641
|
},
|
|
2444
2642
|
}))
|
|
2445
2643
|
.views(self => ({
|
|
2644
|
+
/**
|
|
2645
|
+
* #getter
|
|
2646
|
+
*/
|
|
2647
|
+
get verticalScrollbarWidth() {
|
|
2648
|
+
return self.showVerticalScrollbar ? 20 : 0;
|
|
2649
|
+
},
|
|
2650
|
+
/**
|
|
2651
|
+
* #getter
|
|
2652
|
+
* width of the alignment canvas: the msa area less the vertical
|
|
2653
|
+
* scrollbar. showHorizontalScrollbar must not read it, since that feeds
|
|
2654
|
+
* msaAreaHeight -> showVerticalScrollbar and would form a cycle
|
|
2655
|
+
*/
|
|
2656
|
+
get msaCanvasWidth() {
|
|
2657
|
+
return self.msaAreaWidth - this.verticalScrollbarWidth;
|
|
2658
|
+
},
|
|
2659
|
+
/**
|
|
2660
|
+
* #getter
|
|
2661
|
+
* most-negative allowed scrollX, which keeps the last column in view
|
|
2662
|
+
*/
|
|
2663
|
+
get maxScrollX() {
|
|
2664
|
+
return Math.min(-self.totalWidth + this.msaCanvasWidth, 0);
|
|
2665
|
+
},
|
|
2446
2666
|
/**
|
|
2447
2667
|
* #getter
|
|
2448
2668
|
*/
|
|
@@ -2457,9 +2677,29 @@ function stateModelFactory() {
|
|
|
2457
2677
|
},
|
|
2458
2678
|
/**
|
|
2459
2679
|
* #getter
|
|
2680
|
+
* warnings that follow from the loaded data and clear when it changes. A
|
|
2681
|
+
* GFF whose first column names rows of some other alignment parses
|
|
2682
|
+
* without error and draws nothing
|
|
2460
2683
|
*/
|
|
2461
|
-
get
|
|
2462
|
-
|
|
2684
|
+
get dataWarnings() {
|
|
2685
|
+
const { annotations, rowNamesSet } = self;
|
|
2686
|
+
return self.dataInitialized &&
|
|
2687
|
+
annotations.length > 0 &&
|
|
2688
|
+
!annotations.some(a => rowNamesSet.has(a.id))
|
|
2689
|
+
? [
|
|
2690
|
+
`0 of ${annotations.length} annotations name a row in this alignment`,
|
|
2691
|
+
]
|
|
2692
|
+
: [];
|
|
2693
|
+
},
|
|
2694
|
+
/**
|
|
2695
|
+
* #getter
|
|
2696
|
+
* the load warnings and the undismissed data warnings, for the header
|
|
2697
|
+
*/
|
|
2698
|
+
get warnings() {
|
|
2699
|
+
return [
|
|
2700
|
+
...self.loadWarnings,
|
|
2701
|
+
...this.dataWarnings.filter(w => !self.dismissedWarnings.includes(w)),
|
|
2702
|
+
];
|
|
2463
2703
|
},
|
|
2464
2704
|
/**
|
|
2465
2705
|
* #getter
|
|
@@ -2485,6 +2725,13 @@ function stateModelFactory() {
|
|
|
2485
2725
|
setDrawMsaLetters(arg) {
|
|
2486
2726
|
self.drawMsaLetters = arg;
|
|
2487
2727
|
},
|
|
2728
|
+
/**
|
|
2729
|
+
* #action
|
|
2730
|
+
*/
|
|
2731
|
+
clearWarnings() {
|
|
2732
|
+
self.loadWarnings = [];
|
|
2733
|
+
self.dismissedWarnings = self.dataWarnings;
|
|
2734
|
+
},
|
|
2488
2735
|
/**
|
|
2489
2736
|
* #action
|
|
2490
2737
|
*/
|
|
@@ -2539,13 +2786,36 @@ function stateModelFactory() {
|
|
|
2539
2786
|
},
|
|
2540
2787
|
/**
|
|
2541
2788
|
* #action
|
|
2542
|
-
*
|
|
2543
|
-
* and
|
|
2789
|
+
* root the tree on the branch above the given node. The node's tips are
|
|
2790
|
+
* a split of the unrooted tree, and the side of it that is a clade in
|
|
2791
|
+
* the tree as written stands for it as an outgroup, by its first and
|
|
2792
|
+
* last tip, so the root survives the path ids changing under it
|
|
2793
|
+
*/
|
|
2794
|
+
rerootAt(nodeId) {
|
|
2795
|
+
const shown = find(hierarchy(self.tree, d => d.children), n => n.data.id === nodeId);
|
|
2796
|
+
if (!shown?.parent) {
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
const node = outgroupNode(hierarchy(self.inputTree, d => d.children), leaves(shown).map(n => n.data.name));
|
|
2800
|
+
if (node) {
|
|
2801
|
+
const tips = leaves(node);
|
|
2802
|
+
self.setTreeRoot({
|
|
2803
|
+
outgroup: [
|
|
2804
|
+
...new Set([tips[0].data.name, tips.at(-1).data.name]),
|
|
2805
|
+
],
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
},
|
|
2809
|
+
/**
|
|
2810
|
+
* #action
|
|
2811
|
+
* swap in a different tree over the same alignment. Clears `collapsed`,
|
|
2812
|
+
* `rotated` and `showOnly`, since path-derived node ids (node-0-0-1) from the old
|
|
2544
2813
|
* tree would match unrelated nodes in the new one.
|
|
2545
2814
|
*/
|
|
2546
2815
|
replaceTree(newick) {
|
|
2547
2816
|
transaction(() => {
|
|
2548
2817
|
self.collapsed.clear();
|
|
2818
|
+
self.rotated.clear();
|
|
2549
2819
|
self.setShowOnly(undefined);
|
|
2550
2820
|
self.setTree(newick);
|
|
2551
2821
|
});
|
|
@@ -2558,49 +2828,48 @@ function stateModelFactory() {
|
|
|
2558
2828
|
self.setColWidth(defaultColWidth);
|
|
2559
2829
|
self.setRowHeight(defaultRowHeight);
|
|
2560
2830
|
},
|
|
2831
|
+
/**
|
|
2832
|
+
* #action
|
|
2833
|
+
* zoom by `scaleFactor` about the center of the alignment area
|
|
2834
|
+
*/
|
|
2835
|
+
zoomAtCenter(scaleFactor, axis = 'both') {
|
|
2836
|
+
this.zoomToPos(scaleFactor, self.msaCanvasWidth / 2, self.msaAreaHeight / 2, axis);
|
|
2837
|
+
},
|
|
2561
2838
|
/**
|
|
2562
2839
|
* #action
|
|
2563
2840
|
*/
|
|
2564
2841
|
zoomOutHorizontal() {
|
|
2565
|
-
|
|
2566
|
-
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0);
|
|
2842
|
+
this.zoomAtCenter(0.75, 'horizontal');
|
|
2567
2843
|
},
|
|
2568
2844
|
/**
|
|
2569
2845
|
* #action
|
|
2570
2846
|
*/
|
|
2571
2847
|
zoomInHorizontal() {
|
|
2572
|
-
|
|
2573
|
-
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0);
|
|
2848
|
+
this.zoomAtCenter(1.5, 'horizontal');
|
|
2574
2849
|
},
|
|
2575
2850
|
/**
|
|
2576
2851
|
* #action
|
|
2577
2852
|
*/
|
|
2578
2853
|
zoomInVertical() {
|
|
2579
|
-
|
|
2854
|
+
this.zoomAtCenter(1.5, 'vertical');
|
|
2580
2855
|
},
|
|
2581
2856
|
/**
|
|
2582
2857
|
* #action
|
|
2583
2858
|
*/
|
|
2584
2859
|
zoomOutVertical() {
|
|
2585
|
-
|
|
2860
|
+
this.zoomAtCenter(0.75, 'vertical');
|
|
2586
2861
|
},
|
|
2587
2862
|
/**
|
|
2588
2863
|
* #action
|
|
2589
2864
|
*/
|
|
2590
2865
|
zoomIn() {
|
|
2591
|
-
|
|
2592
|
-
this.zoomInHorizontal();
|
|
2593
|
-
this.zoomInVertical();
|
|
2594
|
-
});
|
|
2866
|
+
this.zoomAtCenter(1.5);
|
|
2595
2867
|
},
|
|
2596
2868
|
/**
|
|
2597
2869
|
* #action
|
|
2598
2870
|
*/
|
|
2599
2871
|
zoomOut() {
|
|
2600
|
-
|
|
2601
|
-
this.zoomOutHorizontal();
|
|
2602
|
-
this.zoomOutVertical();
|
|
2603
|
-
});
|
|
2872
|
+
this.zoomAtCenter(0.75);
|
|
2604
2873
|
},
|
|
2605
2874
|
/**
|
|
2606
2875
|
* #action
|
|
@@ -2730,25 +2999,11 @@ function stateModelFactory() {
|
|
|
2730
2999
|
},
|
|
2731
3000
|
}))
|
|
2732
3001
|
.views(self => ({
|
|
2733
|
-
/**
|
|
2734
|
-
* #getter
|
|
2735
|
-
*/
|
|
2736
|
-
get verticalScrollbarWidth() {
|
|
2737
|
-
return self.showVerticalScrollbar ? 20 : 0;
|
|
2738
|
-
},
|
|
2739
|
-
/**
|
|
2740
|
-
* #getter
|
|
2741
|
-
* width of the alignment canvas: the msa area less the vertical
|
|
2742
|
-
* scrollbar. showHorizontalScrollbar must not read it, since that feeds
|
|
2743
|
-
* msaAreaHeight -> showVerticalScrollbar and would form a cycle
|
|
2744
|
-
*/
|
|
2745
|
-
get msaCanvasWidth() {
|
|
2746
|
-
return self.msaAreaWidth - this.verticalScrollbarWidth;
|
|
2747
|
-
},
|
|
2748
3002
|
/**
|
|
2749
3003
|
* #method
|
|
2750
3004
|
* the cell at a visible column and row index, in the coordinates a host
|
|
2751
|
-
* writes highlights in
|
|
3005
|
+
* writes highlights in. On a gap `residue` is undefined and `letter` is
|
|
3006
|
+
* the gap character
|
|
2752
3007
|
*/
|
|
2753
3008
|
cellAt(visibleCol, rowIndex) {
|
|
2754
3009
|
const column = self.visibleColToGlobalCol(visibleCol);
|
|
@@ -2795,7 +3050,7 @@ function stateModelFactory() {
|
|
|
2795
3050
|
}
|
|
2796
3051
|
const { xStart, xEnd } = visibleColRange({
|
|
2797
3052
|
offsetX: -scrollX,
|
|
2798
|
-
blockWidth:
|
|
3053
|
+
blockWidth: self.msaCanvasWidth,
|
|
2799
3054
|
colWidth,
|
|
2800
3055
|
});
|
|
2801
3056
|
const last = Math.max(0, Math.min(xEnd, numColumns) - 1);
|
|
@@ -2932,7 +3187,7 @@ function stateModelFactory() {
|
|
|
2932
3187
|
add(field, legend);
|
|
2933
3188
|
}
|
|
2934
3189
|
}
|
|
2935
|
-
for (const { legendTitle, legend } of this.
|
|
3190
|
+
for (const { legendTitle, legend } of this.rowPanelScales) {
|
|
2936
3191
|
add(legendTitle, legend);
|
|
2937
3192
|
}
|
|
2938
3193
|
return [...byKey.values()];
|
|
@@ -2974,7 +3229,7 @@ function stateModelFactory() {
|
|
|
2974
3229
|
// column moves to the neighboring boundary. Bands with no visible
|
|
2975
3230
|
// columns, or naming a missing row, are dropped.
|
|
2976
3231
|
const start = self.seqPosToGlobalCol(name, annotation.start - 1);
|
|
2977
|
-
const end = self.
|
|
3232
|
+
const end = self.seqEndToGlobalCol(name, annotation.end - 1);
|
|
2978
3233
|
if (start === undefined || end === undefined) {
|
|
2979
3234
|
return undefined;
|
|
2980
3235
|
}
|
|
@@ -3064,6 +3319,23 @@ function stateModelFactory() {
|
|
|
3064
3319
|
}
|
|
3065
3320
|
return runs;
|
|
3066
3321
|
},
|
|
3322
|
+
/**
|
|
3323
|
+
* #method
|
|
3324
|
+
* a highlight label with `{residue}` and `{position}` filled in from the
|
|
3325
|
+
* row's letter at `start`, which is how the `175` shorthand draws "R175"
|
|
3326
|
+
*/
|
|
3327
|
+
fillHighlightLabel(label, row, start) {
|
|
3328
|
+
if (!label.includes('{')) {
|
|
3329
|
+
return label;
|
|
3330
|
+
}
|
|
3331
|
+
const col = row && start !== undefined
|
|
3332
|
+
? self.seqPosIndex(row)?.[start - 1]
|
|
3333
|
+
: undefined;
|
|
3334
|
+
const residue = row && col !== undefined ? (self.MSA?.getRow(row)[col] ?? '') : '';
|
|
3335
|
+
return label
|
|
3336
|
+
.replaceAll('{residue}', residue)
|
|
3337
|
+
.replaceAll('{position}', start === undefined ? '' : String(start));
|
|
3338
|
+
},
|
|
3067
3339
|
/**
|
|
3068
3340
|
* #getter
|
|
3069
3341
|
* `highlights` projected onto what is on screen: residue spans go
|
|
@@ -3079,7 +3351,10 @@ function stateModelFactory() {
|
|
|
3079
3351
|
...Object.values(transientHighlights).flat(),
|
|
3080
3352
|
];
|
|
3081
3353
|
return all.flatMap(({ row, rows, start, end, label, color }) => {
|
|
3082
|
-
const base = {
|
|
3354
|
+
const base = {
|
|
3355
|
+
label: label && this.fillHighlightLabel(label, row, start),
|
|
3356
|
+
color,
|
|
3357
|
+
};
|
|
3083
3358
|
if (rows) {
|
|
3084
3359
|
const rowIndices = rows
|
|
3085
3360
|
.map(name => rowNamesSet.get(name))
|
|
@@ -3139,45 +3414,30 @@ function stateModelFactory() {
|
|
|
3139
3414
|
},
|
|
3140
3415
|
/**
|
|
3141
3416
|
* #getter
|
|
3142
|
-
* each row panel
|
|
3143
|
-
*
|
|
3144
|
-
*
|
|
3145
|
-
* per change of that table or the panels, never per block per frame.
|
|
3417
|
+
* each row panel's scale resolved against the values its field takes,
|
|
3418
|
+
* giving the colors, labels and legend entries. Reads no cell size, so
|
|
3419
|
+
* a zoom reuses it and the legends built from it.
|
|
3146
3420
|
*/
|
|
3147
|
-
get
|
|
3148
|
-
const
|
|
3421
|
+
get rowPanelScales() {
|
|
3422
|
+
const { rowData } = self;
|
|
3149
3423
|
const { fillPalette, featureFillEncoding, featureLabels } = this;
|
|
3150
3424
|
const annotations = self.filteredAnnotations;
|
|
3151
|
-
|
|
3152
|
-
return self.rowPanels.map((panel, index) => {
|
|
3153
|
-
const width = rowPanelWidth(panel, self.rowHeight);
|
|
3154
|
-
const base = { id: `rowpanel-${index}`, width, offsetX };
|
|
3155
|
-
offsetX += width;
|
|
3425
|
+
return self.rowPanels.map(panel => {
|
|
3156
3426
|
if (panel.kind === 'features') {
|
|
3157
3427
|
const color = panel.encoding?.color;
|
|
3158
3428
|
const encoding = color
|
|
3159
3429
|
? resolveFeatureScale(color.field, color.scale, annotations)
|
|
3160
3430
|
: featureFillEncoding;
|
|
3161
3431
|
const label = panel.encoding?.label;
|
|
3162
|
-
const align = panel.transform?.find(t => t.type === 'align');
|
|
3163
3432
|
return {
|
|
3164
|
-
...base,
|
|
3165
3433
|
kind: panel.kind,
|
|
3166
3434
|
x: panel.x,
|
|
3167
3435
|
header: panel.header ?? '',
|
|
3168
3436
|
field: encoding?.field,
|
|
3169
3437
|
legendTitle: encoding?.field,
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
colWidth: self.colWidth,
|
|
3174
|
-
domainBands: this.domainBands,
|
|
3175
|
-
annotationsByRow: self.annotationsByRow,
|
|
3176
|
-
shifts: align
|
|
3177
|
-
? this.featureAlignShifts.get(align.on)
|
|
3178
|
-
: undefined,
|
|
3179
|
-
}),
|
|
3180
|
-
colors: featureColorMap(annotations, encoding, fillPalette),
|
|
3438
|
+
colors: color
|
|
3439
|
+
? featureColorMap(annotations, encoding, fillPalette)
|
|
3440
|
+
: this.featureColors,
|
|
3181
3441
|
labels: label
|
|
3182
3442
|
? featureLabelMap(annotations, label)
|
|
3183
3443
|
: featureLabels,
|
|
@@ -3189,26 +3449,53 @@ function stateModelFactory() {
|
|
|
3189
3449
|
})),
|
|
3190
3450
|
};
|
|
3191
3451
|
}
|
|
3192
|
-
const { colorOf, legend } = resolveScale(panel.scale,
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
const value = row?.[panel.field];
|
|
3196
|
-
const color = value === undefined ? undefined : colorOf(value);
|
|
3197
|
-
if (color) {
|
|
3198
|
-
colors.set(name, color);
|
|
3199
|
-
}
|
|
3200
|
-
}
|
|
3452
|
+
const { colorOf, legend } = resolveScale(panel.scale, Object.values(rowData)
|
|
3453
|
+
.map(row => row?.[panel.field])
|
|
3454
|
+
.filter(notEmpty));
|
|
3201
3455
|
return {
|
|
3202
|
-
...base,
|
|
3203
3456
|
kind: panel.kind,
|
|
3204
3457
|
field: panel.field,
|
|
3205
3458
|
header: panel.header ?? panel.field,
|
|
3206
3459
|
legendTitle: panel.legend ?? panel.field,
|
|
3207
|
-
colors,
|
|
3460
|
+
colors: colorByRow(rowData, panel.field, colorOf),
|
|
3208
3461
|
legend,
|
|
3209
3462
|
};
|
|
3210
3463
|
});
|
|
3211
3464
|
},
|
|
3465
|
+
/**
|
|
3466
|
+
* #getter
|
|
3467
|
+
* each row panel with its scale from `rowPanelScales` and its geometry:
|
|
3468
|
+
* the pixel column it draws in and, for a features panel, the spans per
|
|
3469
|
+
* row. Resolved once per change of those inputs, never per block per
|
|
3470
|
+
* frame.
|
|
3471
|
+
*/
|
|
3472
|
+
get resolvedRowPanels() {
|
|
3473
|
+
const scales = this.rowPanelScales;
|
|
3474
|
+
let offsetX = 0;
|
|
3475
|
+
return self.rowPanels.map((panel, index) => {
|
|
3476
|
+
const width = rowPanelWidth(panel, self);
|
|
3477
|
+
const base = { id: `rowpanel-${index}`, width, offsetX };
|
|
3478
|
+
offsetX += width;
|
|
3479
|
+
if (panel.kind === 'features') {
|
|
3480
|
+
const align = panel.transform?.find(t => t.type === 'align');
|
|
3481
|
+
return {
|
|
3482
|
+
...base,
|
|
3483
|
+
...scales[index],
|
|
3484
|
+
spans: featurePanelSpans({
|
|
3485
|
+
panel,
|
|
3486
|
+
width,
|
|
3487
|
+
colWidth: self.colWidth,
|
|
3488
|
+
domainBands: this.domainBands,
|
|
3489
|
+
annotationsByRow: self.annotationsByRow,
|
|
3490
|
+
shifts: align
|
|
3491
|
+
? this.featureAlignShifts.get(align.on)
|
|
3492
|
+
: undefined,
|
|
3493
|
+
}),
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
return { ...base, ...scales[index] };
|
|
3497
|
+
});
|
|
3498
|
+
},
|
|
3212
3499
|
/**
|
|
3213
3500
|
* #getter
|
|
3214
3501
|
* the shift an `align` transform gives each row, keyed by the feature
|
|
@@ -3240,18 +3527,9 @@ function stateModelFactory() {
|
|
|
3240
3527
|
*/
|
|
3241
3528
|
get tipLabelColors() {
|
|
3242
3529
|
const encoding = this.resolvedEncodings.find(e => e.channel === 'tipLabel');
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
const colors = new Map();
|
|
3247
|
-
for (const [name, row] of Object.entries(self.rowData)) {
|
|
3248
|
-
const value = row?.[encoding.field];
|
|
3249
|
-
const color = value === undefined ? undefined : encoding.colorOf(value);
|
|
3250
|
-
if (color) {
|
|
3251
|
-
colors.set(name, color);
|
|
3252
|
-
}
|
|
3253
|
-
}
|
|
3254
|
-
return colors;
|
|
3530
|
+
return encoding
|
|
3531
|
+
? colorByRow(self.rowData, encoding.field, encoding.colorOf)
|
|
3532
|
+
: undefined;
|
|
3255
3533
|
},
|
|
3256
3534
|
/**
|
|
3257
3535
|
* #getter
|
|
@@ -3264,13 +3542,10 @@ function stateModelFactory() {
|
|
|
3264
3542
|
if (!encoding) {
|
|
3265
3543
|
return undefined;
|
|
3266
3544
|
}
|
|
3545
|
+
const colors = colorByRow(self.rowData, encoding.field, encoding.colorOf);
|
|
3267
3546
|
return self.rowNames.map(name => {
|
|
3268
|
-
const
|
|
3269
|
-
|
|
3270
|
-
if (!color) {
|
|
3271
|
-
return undefined;
|
|
3272
|
-
}
|
|
3273
|
-
return withAlpha(color, rowTintAlpha);
|
|
3547
|
+
const color = colors.get(name);
|
|
3548
|
+
return color && withAlpha(color, rowTintAlpha);
|
|
3274
3549
|
});
|
|
3275
3550
|
},
|
|
3276
3551
|
/**
|
|
@@ -3346,7 +3621,8 @@ function stateModelFactory() {
|
|
|
3346
3621
|
*/
|
|
3347
3622
|
reset() {
|
|
3348
3623
|
self.resetCount++;
|
|
3349
|
-
self.
|
|
3624
|
+
self.loadWarnings = [];
|
|
3625
|
+
self.dismissedWarnings = [];
|
|
3350
3626
|
self.setStatus(undefined);
|
|
3351
3627
|
applySnapshot(self, Object.fromEntries(Object.entries(getSnapshot(self)).filter(([key]) => preservedOnReset.has(key))));
|
|
3352
3628
|
self.setError(undefined);
|
|
@@ -3366,6 +3642,14 @@ function stateModelFactory() {
|
|
|
3366
3642
|
const blob = new Blob([html], { type: 'image/svg+xml' });
|
|
3367
3643
|
saveAs(blob, exportFileName(self.msaFilehandle, 'svg'));
|
|
3368
3644
|
},
|
|
3645
|
+
/**
|
|
3646
|
+
* #action
|
|
3647
|
+
* save `treeNewick` as a file
|
|
3648
|
+
*/
|
|
3649
|
+
exportNewick() {
|
|
3650
|
+
const blob = new Blob([self.treeNewick], { type: 'text/plain' });
|
|
3651
|
+
saveAs(blob, exportFileName(self.treeFilehandle ?? self.msaFilehandle, 'nwk', 'tree'));
|
|
3652
|
+
},
|
|
3369
3653
|
/**
|
|
3370
3654
|
* #action
|
|
3371
3655
|
* show or hide an annotation type. Only hidden types are recorded; see
|
|
@@ -3431,23 +3715,22 @@ function stateModelFactory() {
|
|
|
3431
3715
|
if (self.highlightColumns?.length) {
|
|
3432
3716
|
self.setHighlightedColumns(self.highlightColumns);
|
|
3433
3717
|
}
|
|
3434
|
-
// The `collapse` and `
|
|
3435
|
-
// the subtree in focus, which the tree,
|
|
3436
|
-
// alignment all read. The tree arrives with
|
|
3437
|
-
// and later for a filehandle, so the seeding
|
|
3438
|
-
//
|
|
3439
|
-
//
|
|
3440
|
-
//
|
|
3441
|
-
//
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3718
|
+
// The `collapse`, `focus` and `rotate` clade marks seed the collapsed
|
|
3719
|
+
// list, the subtree in focus and the rotated nodes, which the tree,
|
|
3720
|
+
// `hideGapsEffective` and the alignment all read. The tree arrives with
|
|
3721
|
+
// the model for inline data and later for a filehandle, so the seeding
|
|
3722
|
+
// waits for it and then runs once per data load, per change of
|
|
3723
|
+
// `clades` and per reroot, which clears the node ids the seeding
|
|
3724
|
+
// wrote: expanding a seeded clade sticks until one of those changes.
|
|
3725
|
+
// `dataInitialized` is true once the MSA alone has loaded, when the
|
|
3726
|
+
// tree is still the flat stub, so a tree filehandle holds the seeding
|
|
3727
|
+
// until its text lands.
|
|
3728
|
+
addDisposer(self, reaction(() => self.dataInitialized && !(self.treeFilehandle && !self.data.tree)
|
|
3729
|
+
? { clades: getSnapshot(self.clades), treeRoot: self.treeRoot }
|
|
3730
|
+
: undefined, seed => {
|
|
3731
|
+
if (!seed?.clades.length) {
|
|
3448
3732
|
return;
|
|
3449
3733
|
}
|
|
3450
|
-
cladesSeeded = true;
|
|
3451
3734
|
for (const { mark, nodeId } of self.resolvedClades) {
|
|
3452
3735
|
if (nodeId === undefined) {
|
|
3453
3736
|
continue;
|
|
@@ -3458,6 +3741,23 @@ function stateModelFactory() {
|
|
|
3458
3741
|
else if (mark === 'focus') {
|
|
3459
3742
|
self.setShowOnly(nodeId);
|
|
3460
3743
|
}
|
|
3744
|
+
else if (mark === 'rotate' &&
|
|
3745
|
+
!self.rotated.includes(nodeId)) {
|
|
3746
|
+
self.toggleRotated(nodeId);
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
}, { fireImmediately: true, equals: compareStructural }));
|
|
3750
|
+
// zoomToRegion needs a width and resolves residues through the visible
|
|
3751
|
+
// columns, so this waits for both, and for a tree file whose collapsed
|
|
3752
|
+
// clades would hide columns and move the target after the zoom
|
|
3753
|
+
addDisposer(self, autorun(() => {
|
|
3754
|
+
const { region } = self;
|
|
3755
|
+
if (region &&
|
|
3756
|
+
self.viewInitialized &&
|
|
3757
|
+
self.numColumns > 0 &&
|
|
3758
|
+
!(self.treeFilehandle && !self.data.tree)) {
|
|
3759
|
+
this.zoomToRegion(region);
|
|
3760
|
+
self.setRegion(undefined);
|
|
3461
3761
|
}
|
|
3462
3762
|
}));
|
|
3463
3763
|
// the matchMedia query is pinned to the current device pixel ratio, so
|
|
@@ -3599,16 +3899,6 @@ function stateModelFactory() {
|
|
|
3599
3899
|
self.setAnnotations([]);
|
|
3600
3900
|
}
|
|
3601
3901
|
}));
|
|
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
|
-
}));
|
|
3612
3902
|
// gffFilehandle loads into data.gff, which the autorun above parses
|
|
3613
3903
|
loadOnFilehandleChange({
|
|
3614
3904
|
what: 'annotations',
|