react-msaview 8.2.0 → 8.4.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 +77 -300
- package/bundle/index.js.map +4 -4
- package/dist/blockFasta.d.ts +6 -0
- package/dist/blockFasta.js +12 -0
- package/dist/blockFasta.js.map +1 -0
- package/dist/colorSchemes.d.ts +7 -0
- package/dist/colorSchemes.js +15 -0
- package/dist/colorSchemes.js.map +1 -1
- 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/MSAView.js +15 -9
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +39 -7
- package/dist/components/MSAViewer.js +72 -22
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/MetadataTable.d.ts +5 -0
- package/dist/components/MetadataTable.js +29 -0
- package/dist/components/MetadataTable.js.map +1 -0
- 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 +13 -4
- 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/dialogs/MetadataDialog.js +5 -6
- package/dist/components/dialogs/MetadataDialog.js.map +1 -1
- package/dist/components/getVisibleLeaves.d.ts +1 -1
- package/dist/components/header/ColorSchemeMenu.d.ts +6 -0
- package/dist/components/header/ColorSchemeMenu.js +23 -5
- package/dist/components/header/ColorSchemeMenu.js.map +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/{HeaderStatusArea.d.ts → GoToBox.d.ts} +2 -2
- package/dist/components/header/GoToBox.js +21 -0
- package/dist/components/header/GoToBox.js.map +1 -0
- package/dist/components/header/Header.js +8 -7
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/SelectionMenu.d.ts +12 -0
- package/dist/components/header/SelectionMenu.js +72 -0
- package/dist/components/header/SelectionMenu.js.map +1 -0
- package/dist/components/header/goTo.d.ts +20 -0
- package/dist/components/header/goTo.js +69 -0
- package/dist/components/header/goTo.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 +17 -1
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +6 -6
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/msaKeys.d.ts +5 -0
- package/dist/components/msa/msaKeys.js +41 -0
- package/dist/components/msa/msaKeys.js.map +1 -0
- package/dist/components/msa/msaRaster.js +1 -0
- package/dist/components/msa/msaRaster.js.map +1 -1
- package/dist/components/msa/renderHighlights.d.ts +14 -3
- package/dist/components/msa/renderHighlights.js +29 -4
- package/dist/components/msa/renderHighlights.js.map +1 -1
- package/dist/components/msa/tileColor.js +9 -8
- package/dist/components/msa/tileColor.js.map +1 -1
- package/dist/components/msa/useMsaBlockMouse.d.ts +2 -4
- package/dist/components/msa/useMsaBlockMouse.js +34 -14
- package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
- package/dist/components/overlayColors.d.ts +2 -0
- package/dist/components/overlayColors.js +3 -0
- package/dist/components/overlayColors.js.map +1 -1
- package/dist/components/rowpanels/RowPanelBlock.js +10 -14
- package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
- package/dist/components/selectionDrag.d.ts +17 -0
- package/dist/components/selectionDrag.js +30 -0
- package/dist/components/selectionDrag.js.map +1 -0
- 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 +4 -6
- package/dist/components/tracks/useTrackHover.js +30 -10
- 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/dialogs/TreeNodeInfoDialog.js +9 -8
- package/dist/components/tree/dialogs/TreeNodeInfoDialog.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 +3 -0
- package/dist/constants.js +12 -3
- package/dist/constants.js.map +1 -1
- package/dist/element.d.ts +7 -3
- package/dist/element.js +32 -4
- package/dist/element.js.map +1 -1
- package/dist/expandSpec.js +3 -3
- package/dist/expandSpec.js.map +1 -1
- package/dist/exportFileName.d.ts +3 -3
- package/dist/exportFileName.js +4 -4
- package/dist/exportFileName.js.map +1 -1
- package/dist/featuresLayer.d.ts +8 -0
- package/dist/featuresLayer.js +42 -0
- package/dist/featuresLayer.js.map +1 -0
- 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 +2 -2
- package/dist/model/DataModel.d.ts +1 -1
- package/dist/model/DataModel.js +3 -3
- package/dist/model/DataModel.js.map +1 -1
- package/dist/model/msaModel.d.ts +14 -1
- package/dist/model/msaModel.js +17 -1
- package/dist/model/msaModel.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 +285 -53
- package/dist/model.js +693 -231
- 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 +172 -26
- package/dist/types.d.ts +75 -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/blockFasta.ts +18 -0
- package/src/colorSchemes.ts +20 -0
- 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/MSAView.tsx +16 -9
- package/src/components/MSAViewer.tsx +121 -26
- package/src/components/MetadataTable.tsx +55 -0
- package/src/components/SequenceTextArea.tsx +54 -8
- package/src/components/StatusMessage.tsx +70 -0
- package/src/components/Track.tsx +21 -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/dialogs/MetadataDialog.tsx +9 -8
- package/src/components/header/ColorSchemeMenu.tsx +31 -12
- package/src/components/header/FileMenu.tsx +9 -0
- package/src/components/header/GappynessSlider.tsx +17 -4
- package/src/components/header/GoToBox.tsx +50 -0
- package/src/components/header/Header.tsx +11 -8
- package/src/components/header/SelectionMenu.tsx +117 -0
- package/src/components/header/goTo.ts +75 -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 +19 -1
- package/src/components/msa/MSACanvasBlock.tsx +9 -11
- package/src/components/msa/msaKeys.ts +45 -0
- package/src/components/msa/msaRaster.ts +1 -0
- package/src/components/msa/renderHighlights.ts +46 -4
- package/src/components/msa/tileColor.ts +9 -8
- package/src/components/msa/useMsaBlockMouse.ts +44 -31
- package/src/components/overlayColors.ts +4 -0
- package/src/components/rowpanels/RowPanelBlock.tsx +16 -23
- package/src/components/selectionDrag.ts +42 -0
- package/src/components/tracks/drawTracks.ts +15 -2
- package/src/components/tracks/useTrackHover.ts +32 -10
- 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/dialogs/TreeNodeInfoDialog.tsx +14 -16
- 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/element.ts +45 -5
- package/src/expandSpec.ts +3 -3
- package/src/exportFileName.ts +4 -3
- package/src/featuresLayer.ts +48 -0
- package/src/hierarchy.ts +151 -4
- package/src/index.ts +8 -1
- package/src/model/DataModel.ts +3 -3
- package/src/model/msaModel.ts +19 -1
- package/src/model/treeModel.ts +22 -0
- package/src/model.ts +841 -272
- package/src/renderToSvg.tsx +6 -9
- package/src/rerootTree.ts +172 -0
- package/src/types.ts +92 -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,11 +2,12 @@ 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
|
+
import { blockFasta } from './blockFasta.js';
|
|
7
8
|
import { calculateBlocks } from './calculateBlocks.js';
|
|
8
9
|
import { clustalXColumnColors } from './clustalX.js';
|
|
9
|
-
import colorSchemes from './colorSchemes.js';
|
|
10
|
+
import colorSchemes, { letterColorTable } from './colorSchemes.js';
|
|
10
11
|
import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.js';
|
|
11
12
|
import { columnStats } from './columnStats.js';
|
|
12
13
|
import { packDomainLanes } from './components/msa/packDomainLanes.js';
|
|
@@ -14,13 +15,14 @@ import { visibleColRange } from './components/msa/visibleColRange.js';
|
|
|
14
15
|
import TrackBlocks from './components/tracks/TrackBlocks.js';
|
|
15
16
|
import { cladeGutterWidth } from './components/tree/cladeBrackets.js';
|
|
16
17
|
import { scaleBarLength } from './components/tree/scaleBar.js';
|
|
17
|
-
import { cladeHighlightAlpha, cladeHighlightColor, defaultAllowedGappyness, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawMsaLetters, defaultFeaturePanelWidth, defaultHeight, defaultHideGaps, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultScrollZoomAxis, defaultShowDomainLegend, defaultShowDomains, defaultSubFeatureRows, defaultTreeWidth, labelReferenceFontSize, maxCellSize, maxInlineSnapshotBytes, maxNeighborJoiningRows, minColWidth, minLetterColWidth, minLetterRowHeight, minRowHeight, rowPanelHeaderHeight, rowTintAlpha, scrollZoomAxes, segmentFeatureTypes, segmentShades, treeScaleBarHeight, } from './constants.js';
|
|
18
|
+
import { cladeHighlightAlpha, cladeHighlightColor, defaultAllowedGappyness, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawMsaLetters, defaultFeaturePanelWidth, defaultHeight, defaultHideGaps, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultScrollZoomAxis, defaultShowDomainLegend, defaultShowDomains, defaultSubFeatureRows, defaultTreeWidth, labelReferenceFontSize, maxCellSize, maxInlineSnapshotBytes as defaultMaxInlineSnapshotBytes, maxNeighborJoiningRows, minColWidth, minLetterColWidth, minLetterRowHeight, minRowHeight, rowPanelHeaderHeight, rowTintAlpha, scrollZoomAxes, segmentFeatureTypes, segmentShades, treeScaleBarHeight, } from './constants.js';
|
|
18
19
|
import { createPaletteMap } from './createPaletteMap.js';
|
|
19
20
|
import { exportFileName } from './exportFileName.js';
|
|
20
21
|
import { featureField, featureName } from './featureFields.js';
|
|
22
|
+
import { featuresToAnnotations } from './featuresLayer.js';
|
|
21
23
|
import { fetchTextWithProgress, isAbortError } from './fetchUtils.js';
|
|
22
24
|
import { flatToTree } from './flatToTree.js';
|
|
23
|
-
import { calcDepthToLeaf, clusterLayout, collapse,
|
|
25
|
+
import { calcDepthToLeaf, clusterLayout, collapse, find, forEachDescendant, hierarchy, layoutTree, leafIndex, leaves, maxLength, mrca, nodeCoveringRows, setBrLength, sort, sum as hierarchySum, } from './hierarchy.js';
|
|
24
26
|
import { measureTextCanvas } from './measureTextCanvas.js';
|
|
25
27
|
import { DataModelF } from './model/DataModel.js';
|
|
26
28
|
import { DialogQueueSessionMixin } from './model/DialogQueue.js';
|
|
@@ -29,6 +31,7 @@ import { TreeModelF } from './model/treeModel.js';
|
|
|
29
31
|
import { calculateNeighborJoiningTree } from './neighborJoining.js';
|
|
30
32
|
import { parseAsn1 } from './parseAsn1.js';
|
|
31
33
|
import { calculatePropertyConservation } from './propertyConservation.js';
|
|
34
|
+
import { midpointRoot, rerootTree } from './rerootTree.js';
|
|
32
35
|
import { globalColToVisibleCol, seqPosOfGlobalCol, visibleColToGlobalCol, visibleColsBefore, } from './rowCoordinateCalculations.js';
|
|
33
36
|
import { resolveScale } from './scales.js';
|
|
34
37
|
import { buildSeqPosIndex } from './seqPosToGlobalCol.js';
|
|
@@ -36,6 +39,7 @@ import { maxBitsFor } from './sequenceLogo.js';
|
|
|
36
39
|
import { stripDefault } from './stripDefault.js';
|
|
37
40
|
import { computeRowInsertions, dropBlanks, len, outlineColor, skipBlanks, withAlpha, } from './util.js';
|
|
38
41
|
import { saveAs } from './vendor/fileSaver.js';
|
|
42
|
+
import { writeNewick } from './writeNewick.js';
|
|
39
43
|
import { parseWuss } from './wuss.js';
|
|
40
44
|
function parseTreeText(text) {
|
|
41
45
|
if (text.startsWith('BioTreeContainer')) {
|
|
@@ -73,10 +77,41 @@ const UNSCALED_FEATURE = '#d9d9d9';
|
|
|
73
77
|
// its own in a `features` panel
|
|
74
78
|
const laneOverlap = 0.1;
|
|
75
79
|
// 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
|
-
|
|
80
|
+
// cell square, and a gene neighborhood's width for a features panel. The row
|
|
81
|
+
// height is read only when used, so a panel with a width of its own does not
|
|
82
|
+
// recompute on a vertical zoom
|
|
83
|
+
function rowPanelWidth(panel, view) {
|
|
78
84
|
return (panel.width ??
|
|
79
|
-
(panel.kind === 'features' ? defaultFeaturePanelWidth : rowHeight));
|
|
85
|
+
(panel.kind === 'features' ? defaultFeaturePanelWidth : view.rowHeight));
|
|
86
|
+
}
|
|
87
|
+
// one row of the row table as the channels read it. A number or a boolean
|
|
88
|
+
// reads as its string, the way a GFF value does, and null or an object drops
|
|
89
|
+
function stringFields(row) {
|
|
90
|
+
if (typeof row !== 'object' || row === null) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
const fields = {};
|
|
94
|
+
for (const [field, value] of Object.entries(row)) {
|
|
95
|
+
if (typeof value === 'string') {
|
|
96
|
+
fields[field] = value;
|
|
97
|
+
}
|
|
98
|
+
else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
99
|
+
fields[field] = String(value);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return fields;
|
|
103
|
+
}
|
|
104
|
+
// the color each row takes from a field of the row table under a scale
|
|
105
|
+
function colorByRow(rowData, field, colorOf) {
|
|
106
|
+
const colors = new Map();
|
|
107
|
+
for (const [name, row] of Object.entries(rowData)) {
|
|
108
|
+
const value = row?.[field];
|
|
109
|
+
const color = value === undefined ? undefined : colorOf(value);
|
|
110
|
+
if (color) {
|
|
111
|
+
colors.set(name, color);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return colors;
|
|
80
115
|
}
|
|
81
116
|
// the fill and outline of every feature in a list: its own GFF `color=` first,
|
|
82
117
|
// then the scale an encoding resolves over one of its fields, then the
|
|
@@ -202,19 +237,34 @@ function featurePanelSpans({ panel, width, colWidth, domainBands, annotationsByR
|
|
|
202
237
|
})),
|
|
203
238
|
]));
|
|
204
239
|
}
|
|
205
|
-
// a scale over the values a field takes across
|
|
206
|
-
|
|
240
|
+
// a scale over the values a field takes across every feature loaded, so hiding
|
|
241
|
+
// a type in the filter dialog leaves the rest in their colors, and a legend of
|
|
242
|
+
// the values the features drawn carry
|
|
243
|
+
function resolveFeatureScale(field, scale, annotations, drawn) {
|
|
244
|
+
const valuesOf = (list) => list.map(a => featureField(a, field)).filter(notEmpty);
|
|
245
|
+
const resolved = resolveScale(scale, valuesOf(annotations));
|
|
246
|
+
const shown = new Set(valuesOf(drawn));
|
|
207
247
|
return {
|
|
208
248
|
field,
|
|
209
|
-
...
|
|
249
|
+
...resolved,
|
|
250
|
+
legend: resolved.legend.filter(entry => shown.has(entry.id)),
|
|
210
251
|
};
|
|
211
252
|
}
|
|
253
|
+
const columnTrackBytes = new WeakMap();
|
|
254
|
+
function columnTrackSize(track) {
|
|
255
|
+
let bytes = columnTrackBytes.get(track);
|
|
256
|
+
if (bytes === undefined) {
|
|
257
|
+
bytes = JSON.stringify(track).length;
|
|
258
|
+
columnTrackBytes.set(track, bytes);
|
|
259
|
+
}
|
|
260
|
+
return bytes;
|
|
261
|
+
}
|
|
212
262
|
// a data track over this size stays in the live model but leaves the snapshot,
|
|
213
263
|
// the same rule DataModel applies to an inline document
|
|
214
264
|
function columnTrackSizes(tracks) {
|
|
215
|
-
return (tracks ?? []).map(
|
|
265
|
+
return (tracks ?? []).map(columnTrackSize);
|
|
216
266
|
}
|
|
217
|
-
function smallColumnTracks(tracks) {
|
|
267
|
+
function smallColumnTracks(tracks, maxInlineSnapshotBytes) {
|
|
218
268
|
const sizes = columnTrackSizes(tracks);
|
|
219
269
|
const kept = tracks?.filter((_t, i) => sizes[i] <= maxInlineSnapshotBytes);
|
|
220
270
|
return kept?.length ? { columnTracks: kept } : {};
|
|
@@ -240,12 +290,14 @@ export const preservedOnReset = new Set([
|
|
|
240
290
|
'scrollZoomAxis',
|
|
241
291
|
'bgColor',
|
|
242
292
|
'colorSchemeName',
|
|
293
|
+
'customColorScheme',
|
|
243
294
|
'showColumnStats',
|
|
244
295
|
'drawLabels',
|
|
245
296
|
'labelsAlignRight',
|
|
246
297
|
'treeAreaWidth',
|
|
247
298
|
'treeWidth',
|
|
248
299
|
'showBranchLen',
|
|
300
|
+
'treeOrder',
|
|
249
301
|
'drawTree',
|
|
250
302
|
'drawNodeBubbles',
|
|
251
303
|
'drawNodeLabels',
|
|
@@ -266,6 +318,8 @@ export const preservedOnReset = new Set([
|
|
|
266
318
|
function trackIsOff(turnedOffTracks, id, defaultOff) {
|
|
267
319
|
return turnedOffTracks.get(id) ?? (defaultOff || defaultOffTracks.has(id));
|
|
268
320
|
}
|
|
321
|
+
// the marks that set tree state from a node rather than drawing
|
|
322
|
+
const seedingMarks = new Set(['collapse', 'focus', 'rotate']);
|
|
269
323
|
// shared empty results, so observers don't see a fresh [] as a change
|
|
270
324
|
const noDomains = [];
|
|
271
325
|
const noClades = [];
|
|
@@ -309,14 +363,13 @@ function cladeRows(clade, root, index, rowNamesSet) {
|
|
|
309
363
|
* Every clade that resolves, with its fill color settled, against the leaf
|
|
310
364
|
* order `rowNamesSet` gives. The tree panel resolves against the displayed
|
|
311
365
|
* 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.
|
|
366
|
+
* `range` record names no node, so the seeding marks, which need one, drop it.
|
|
314
367
|
*/
|
|
315
368
|
function resolveClades(clades, root, rowNamesSet) {
|
|
316
369
|
const index = leafIndex(root);
|
|
317
370
|
return clades.flatMap(clade => {
|
|
318
371
|
const resolved = cladeRows(clade, root, index, rowNamesSet);
|
|
319
|
-
const seeding =
|
|
372
|
+
const seeding = seedingMarks.has(clade.mark);
|
|
320
373
|
return resolved && !(seeding && resolved.nodeId === undefined)
|
|
321
374
|
? [
|
|
322
375
|
{
|
|
@@ -330,16 +383,33 @@ function resolveClades(clades, root, rowNamesSet) {
|
|
|
330
383
|
: [];
|
|
331
384
|
});
|
|
332
385
|
}
|
|
386
|
+
const childOrders = {
|
|
387
|
+
input: undefined,
|
|
388
|
+
branchLength: (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1),
|
|
389
|
+
ladderize: (a, b) => a.value - b.value,
|
|
390
|
+
ladderizeReverse: (a, b) => b.value - a.value,
|
|
391
|
+
};
|
|
333
392
|
/**
|
|
334
393
|
* 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.
|
|
394
|
+
* put in `order` and each `rotated` node's reversed, `showOnly` taken as the
|
|
395
|
+
* new root, and each `collapsed` clade folded. The overview builds the same
|
|
396
|
+
* tree without the focus, so it shows the focused subtree inside the whole.
|
|
338
397
|
*/
|
|
339
|
-
function buildTreeRoot(tree, collapsed, showOnly) {
|
|
398
|
+
function buildTreeRoot(tree, { collapsed, rotated, order, showOnly, }) {
|
|
340
399
|
let hier = hierarchy(tree, d => d.children);
|
|
341
400
|
hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1));
|
|
342
|
-
|
|
401
|
+
const compare = childOrders[order];
|
|
402
|
+
if (compare) {
|
|
403
|
+
sort(hier, compare);
|
|
404
|
+
}
|
|
405
|
+
if (rotated.length > 0) {
|
|
406
|
+
const flip = new Set(rotated);
|
|
407
|
+
forEachDescendant(hier, n => {
|
|
408
|
+
if (flip.has(n.data.id)) {
|
|
409
|
+
n.children?.reverse();
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
}
|
|
343
413
|
if (showOnly) {
|
|
344
414
|
const res = find(hier, n => n.data.id === showOnly);
|
|
345
415
|
if (res) {
|
|
@@ -359,6 +429,48 @@ function buildTreeRoot(tree, collapsed, showOnly) {
|
|
|
359
429
|
}
|
|
360
430
|
return hier;
|
|
361
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* The node whose branch an outgroup roots the tree on: the outgroup's most
|
|
434
|
+
* recent common ancestor, or the rest of the tips' where that ancestor is the
|
|
435
|
+
* root, since the outgroup then straddles the root and the branch above the
|
|
436
|
+
* other side is the same branch of the unrooted tree.
|
|
437
|
+
*/
|
|
438
|
+
function outgroupNode(root, outgroup) {
|
|
439
|
+
const index = leafIndex(root);
|
|
440
|
+
const node = mrca(root, outgroup, index);
|
|
441
|
+
if (node !== root) {
|
|
442
|
+
return node;
|
|
443
|
+
}
|
|
444
|
+
const out = new Set(outgroup);
|
|
445
|
+
const rest = leaves(root)
|
|
446
|
+
.map(n => n.data.name)
|
|
447
|
+
.filter(name => !out.has(name));
|
|
448
|
+
const other = mrca(root, rest, index);
|
|
449
|
+
return other === root ? undefined : other;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* `tree` rerooted as `treeRoot` asks, with fresh path ids. The ids of the tree
|
|
453
|
+
* as written become names on unnamed nodes, and a rerooting would carry them
|
|
454
|
+
* along as labels, so they come off first.
|
|
455
|
+
*/
|
|
456
|
+
function rootTree(tree, treeRoot) {
|
|
457
|
+
const plain = new Map();
|
|
458
|
+
for (const node of preorder(tree).reverse()) {
|
|
459
|
+
plain.set(node, {
|
|
460
|
+
name: node.name === node.id ? undefined : node.name,
|
|
461
|
+
length: node.length,
|
|
462
|
+
children: node.children.map(c => plain.get(c)),
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
const input = plain.get(tree);
|
|
466
|
+
if (treeRoot === 'midpoint') {
|
|
467
|
+
return generateNodeIds(midpointRoot(input));
|
|
468
|
+
}
|
|
469
|
+
const target = outgroupNode(hierarchy(tree, d => d.children), treeRoot.outgroup);
|
|
470
|
+
return target
|
|
471
|
+
? generateNodeIds(rerootTree(input, plain.get(target.data)))
|
|
472
|
+
: tree;
|
|
473
|
+
}
|
|
362
474
|
// the inclusive tip indices a node covers, from the row-space extent
|
|
363
475
|
// clusterLayout writes to xMin/xMax as tip centers
|
|
364
476
|
function tipRange(node) {
|
|
@@ -388,6 +500,20 @@ function overflows(content, viewport) {
|
|
|
388
500
|
function inRanges(ranges, position) {
|
|
389
501
|
return !!ranges?.some(([start, end]) => position >= start && position <= end);
|
|
390
502
|
}
|
|
503
|
+
// sorted integers as inclusive [first, last] runs of consecutive values
|
|
504
|
+
function consecutiveRuns(sorted) {
|
|
505
|
+
const runs = [];
|
|
506
|
+
for (const n of sorted) {
|
|
507
|
+
const last = runs.at(-1);
|
|
508
|
+
if (last && n <= last[1] + 1) {
|
|
509
|
+
last[1] = Math.max(last[1], n);
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
runs.push([n, n]);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return runs;
|
|
516
|
+
}
|
|
391
517
|
// the value every one of a node's children reports, or undefined where one of
|
|
392
518
|
// them has no value or they disagree
|
|
393
519
|
function sharedValue(values) {
|
|
@@ -428,8 +554,13 @@ function preorder(tree) {
|
|
|
428
554
|
* const root = RootModel.create({})
|
|
429
555
|
* root.view.setData({ msa: '>seq1\nACGT\n>seq2\nACGT' })
|
|
430
556
|
* ```
|
|
557
|
+
*
|
|
558
|
+
* `maxInlineSnapshotBytes` is the largest pasted or local document, and the
|
|
559
|
+
* largest data track, the snapshot keeps; a larger one stays in the live model
|
|
560
|
+
* and `unshareableData` names it. The standalone app, whose links carry the
|
|
561
|
+
* snapshot in the URL fragment, raises it.
|
|
431
562
|
*/
|
|
432
|
-
function stateModelFactory() {
|
|
563
|
+
function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBytes, } = {}) {
|
|
433
564
|
return types
|
|
434
565
|
.compose(DialogQueueSessionMixin(), TreeModelF(), MSAModelF(), types.model('MsaView', {
|
|
435
566
|
/**
|
|
@@ -535,6 +666,20 @@ function stateModelFactory() {
|
|
|
535
666
|
* hidden)
|
|
536
667
|
*/
|
|
537
668
|
collapsed: stripDefault(types.array(types.string), []),
|
|
669
|
+
/**
|
|
670
|
+
* #property
|
|
671
|
+
* tree nodes whose children draw in reverse order, which is ggtree's
|
|
672
|
+
* `rotate`: the same tree, with the clades on either side of the node
|
|
673
|
+
* swapped
|
|
674
|
+
*/
|
|
675
|
+
rotated: stripDefault(types.array(types.string), []),
|
|
676
|
+
/**
|
|
677
|
+
* #property
|
|
678
|
+
* where the tree is rooted: `midpoint` roots it halfway along the
|
|
679
|
+
* longest path between two tips, `{outgroup}` on the branch above
|
|
680
|
+
* the tips named, and undefined keeps the file's root
|
|
681
|
+
*/
|
|
682
|
+
treeRoot: types.frozen(),
|
|
538
683
|
/**
|
|
539
684
|
* #property
|
|
540
685
|
* focus on particular subtree
|
|
@@ -576,7 +721,7 @@ function stateModelFactory() {
|
|
|
576
721
|
* data from the loaded tree/msa/treeMetadata, generally loaded by
|
|
577
722
|
* autorun
|
|
578
723
|
*/
|
|
579
|
-
data: types.optional(DataModelF(), {}),
|
|
724
|
+
data: types.optional(DataModelF(maxInlineSnapshotBytes), {}),
|
|
580
725
|
/**
|
|
581
726
|
* #property
|
|
582
727
|
* the user's explicit hide choices per annotation accession, keyed by
|
|
@@ -606,6 +751,21 @@ function stateModelFactory() {
|
|
|
606
751
|
* Persists in the snapshot and the URL.
|
|
607
752
|
*/
|
|
608
753
|
highlights: stripDefault(types.array(types.frozen()), []),
|
|
754
|
+
/**
|
|
755
|
+
* #property
|
|
756
|
+
* features on the rows as data, `{row, start, end, name, ...}` in
|
|
757
|
+
* residues of each row, drawn with the GFF's annotations. Persists in
|
|
758
|
+
* the snapshot and the URL.
|
|
759
|
+
*/
|
|
760
|
+
features: stripDefault(types.array(types.frozen()), []),
|
|
761
|
+
/**
|
|
762
|
+
* #property
|
|
763
|
+
* the block the reader selected: `{start, end}` columns of the file,
|
|
764
|
+
* 1-based inclusive like a column highlight, and `rows` by name, every
|
|
765
|
+
* row where absent. Undefined until something is selected, so a view
|
|
766
|
+
* without one adds nothing to the shared URL.
|
|
767
|
+
*/
|
|
768
|
+
selection: types.frozen(),
|
|
609
769
|
/**
|
|
610
770
|
* #property
|
|
611
771
|
* where the view opens, in `highlights` coordinates: `{row, start,
|
|
@@ -619,7 +779,8 @@ function stateModelFactory() {
|
|
|
619
779
|
* clades of the tree with a mark drawn over them. `mrca` names tips
|
|
620
780
|
* whose common ancestor is the clade, or `range` its first and last
|
|
621
781
|
* tip in display order, and `tips` is the leaf count the producer
|
|
622
|
-
* measured. `mark` is `highlight`, `bracket`, `collapse` or
|
|
782
|
+
* measured. `mark` is `highlight`, `bracket`, `collapse`, `focus` or
|
|
783
|
+
* `rotate`.
|
|
623
784
|
* See docs/layers.md
|
|
624
785
|
*/
|
|
625
786
|
clades: stripDefault(types.array(types.frozen()), []),
|
|
@@ -735,9 +896,15 @@ function stateModelFactory() {
|
|
|
735
896
|
* #volatile
|
|
736
897
|
* non-fatal load problems: an optional layer that failed to load, an
|
|
737
898
|
* overlay that failed to parse. `error` replaces the view and is for the
|
|
738
|
-
* alignment itself
|
|
899
|
+
* alignment itself. `warnings` adds the ones derived from the data
|
|
900
|
+
*/
|
|
901
|
+
loadWarnings: [],
|
|
902
|
+
/**
|
|
903
|
+
* #volatile
|
|
904
|
+
* derived warnings the user dismissed, which stay hidden while the data
|
|
905
|
+
* behind them holds
|
|
739
906
|
*/
|
|
740
|
-
|
|
907
|
+
dismissedWarnings: [],
|
|
741
908
|
/**
|
|
742
909
|
* #volatile
|
|
743
910
|
* bumped by reset(). The error boundary above the view uses it as its key,
|
|
@@ -826,16 +993,10 @@ function stateModelFactory() {
|
|
|
826
993
|
* that failed to parse. A message already on the list is not added again
|
|
827
994
|
*/
|
|
828
995
|
addWarning(warning) {
|
|
829
|
-
if (!self.
|
|
830
|
-
self.
|
|
996
|
+
if (!self.loadWarnings.includes(warning)) {
|
|
997
|
+
self.loadWarnings = [...self.loadWarnings, warning];
|
|
831
998
|
}
|
|
832
999
|
},
|
|
833
|
-
/**
|
|
834
|
-
* #action
|
|
835
|
-
*/
|
|
836
|
-
clearWarnings() {
|
|
837
|
-
self.warnings = [];
|
|
838
|
-
},
|
|
839
1000
|
/**
|
|
840
1001
|
* #action
|
|
841
1002
|
* declare that this host restores the loaded documents itself, which
|
|
@@ -879,6 +1040,33 @@ function stateModelFactory() {
|
|
|
879
1040
|
setHighlights(highlights) {
|
|
880
1041
|
self.highlights.replace(highlights);
|
|
881
1042
|
},
|
|
1043
|
+
/**
|
|
1044
|
+
* #action
|
|
1045
|
+
*/
|
|
1046
|
+
setFeatures(features) {
|
|
1047
|
+
self.features.replace(features);
|
|
1048
|
+
},
|
|
1049
|
+
/**
|
|
1050
|
+
* #action
|
|
1051
|
+
* select a block, in `selection` coordinates. `start` and `end` may come
|
|
1052
|
+
* in either order.
|
|
1053
|
+
*/
|
|
1054
|
+
setSelection(selection) {
|
|
1055
|
+
const next = selection && {
|
|
1056
|
+
start: Math.min(selection.start, selection.end),
|
|
1057
|
+
end: Math.max(selection.start, selection.end),
|
|
1058
|
+
...(selection.rows ? { rows: selection.rows } : {}),
|
|
1059
|
+
};
|
|
1060
|
+
if (!compareStructural(next, self.selection)) {
|
|
1061
|
+
self.selection = next;
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1064
|
+
/**
|
|
1065
|
+
* #action
|
|
1066
|
+
*/
|
|
1067
|
+
clearSelection() {
|
|
1068
|
+
this.setSelection(undefined);
|
|
1069
|
+
},
|
|
882
1070
|
/**
|
|
883
1071
|
* #action
|
|
884
1072
|
* replace the clades the viewer marks (see docs/layers.md)
|
|
@@ -952,8 +1140,9 @@ function stateModelFactory() {
|
|
|
952
1140
|
/**
|
|
953
1141
|
* #action
|
|
954
1142
|
* switch to another alignment of a multi-alignment file (Stockholm).
|
|
955
|
-
* Clears the collapsed node ids, the subtree in focus, the
|
|
956
|
-
* and the scroll position, which all refer to the
|
|
1143
|
+
* Clears the collapsed and rotated node ids, the subtree in focus, the
|
|
1144
|
+
* reference row and the scroll position, which all refer to the
|
|
1145
|
+
* previous alignment
|
|
957
1146
|
*/
|
|
958
1147
|
setCurrentAlignment(n) {
|
|
959
1148
|
if (n === self.currentAlignment) {
|
|
@@ -962,6 +1151,7 @@ function stateModelFactory() {
|
|
|
962
1151
|
transaction(() => {
|
|
963
1152
|
self.currentAlignment = n;
|
|
964
1153
|
self.collapsed.clear();
|
|
1154
|
+
self.rotated.clear();
|
|
965
1155
|
this.setShowOnly(undefined);
|
|
966
1156
|
this.drawRelativeTo(undefined);
|
|
967
1157
|
self.scrollX = 0;
|
|
@@ -980,6 +1170,40 @@ function stateModelFactory() {
|
|
|
980
1170
|
self.collapsed.push(node);
|
|
981
1171
|
}
|
|
982
1172
|
},
|
|
1173
|
+
/**
|
|
1174
|
+
* #action
|
|
1175
|
+
* reverse the order of the given node's children, or restore it
|
|
1176
|
+
*/
|
|
1177
|
+
toggleRotated(node) {
|
|
1178
|
+
if (self.rotated.includes(node)) {
|
|
1179
|
+
self.rotated.remove(node);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
self.rotated.push(node);
|
|
1183
|
+
}
|
|
1184
|
+
},
|
|
1185
|
+
/**
|
|
1186
|
+
* #action
|
|
1187
|
+
*/
|
|
1188
|
+
clearRotated() {
|
|
1189
|
+
self.rotated.clear();
|
|
1190
|
+
},
|
|
1191
|
+
/**
|
|
1192
|
+
* #action
|
|
1193
|
+
* reroot the tree. Clears `collapsed`, `rotated` and `showOnly`, whose
|
|
1194
|
+
* path-derived node ids name other nodes in the rerooted tree
|
|
1195
|
+
*/
|
|
1196
|
+
setTreeRoot(treeRoot) {
|
|
1197
|
+
if (compareStructural(self.treeRoot, treeRoot)) {
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
transaction(() => {
|
|
1201
|
+
self.treeRoot = treeRoot;
|
|
1202
|
+
self.collapsed.clear();
|
|
1203
|
+
self.rotated.clear();
|
|
1204
|
+
this.setShowOnly(undefined);
|
|
1205
|
+
});
|
|
1206
|
+
},
|
|
983
1207
|
/**
|
|
984
1208
|
* #action
|
|
985
1209
|
* show only the subtree rooted at the given node id (pass undefined to
|
|
@@ -1043,7 +1267,7 @@ function stateModelFactory() {
|
|
|
1043
1267
|
* store the GFF text in the snapshot like the alignment and tree. The
|
|
1044
1268
|
* parsed annotations are volatile and a blob filehandle is cleared once
|
|
1045
1269
|
* read, so the text is the only persisted copy. An autorun parses it
|
|
1046
|
-
* into annotations.
|
|
1270
|
+
* into annotations, and clears the ones it drew when the text goes.
|
|
1047
1271
|
*/
|
|
1048
1272
|
setGFF(result) {
|
|
1049
1273
|
self.data.setGFF(result);
|
|
@@ -1064,11 +1288,21 @@ function stateModelFactory() {
|
|
|
1064
1288
|
get realAllowedGappyness() {
|
|
1065
1289
|
return this.hideGapsEffective ? self.allowedGappyness : 100;
|
|
1066
1290
|
},
|
|
1291
|
+
/**
|
|
1292
|
+
* #getter
|
|
1293
|
+
* the annotations the overlay, the legend and the encodings draw: the
|
|
1294
|
+
* GFF's, then the `features` layer's
|
|
1295
|
+
*/
|
|
1296
|
+
get allAnnotations() {
|
|
1297
|
+
return self.features.length > 0
|
|
1298
|
+
? [...self.annotations, ...featuresToAnnotations(self.features)]
|
|
1299
|
+
: self.annotations;
|
|
1300
|
+
},
|
|
1067
1301
|
/**
|
|
1068
1302
|
* #getter
|
|
1069
1303
|
*/
|
|
1070
1304
|
get actuallyShowDomains() {
|
|
1071
|
-
return self.showDomains &&
|
|
1305
|
+
return self.showDomains && this.allAnnotations.length > 0;
|
|
1072
1306
|
},
|
|
1073
1307
|
/**
|
|
1074
1308
|
* #getter
|
|
@@ -1102,10 +1336,25 @@ function stateModelFactory() {
|
|
|
1102
1336
|
* #getter
|
|
1103
1337
|
*/
|
|
1104
1338
|
get colorScheme() {
|
|
1339
|
+
const { customColorScheme, colorSchemeName } = self;
|
|
1105
1340
|
// colorSchemeName is a free string (menus, snapshots, URL params), so a
|
|
1106
1341
|
// stale name falls back to the default
|
|
1107
|
-
return
|
|
1108
|
-
|
|
1342
|
+
return customColorScheme
|
|
1343
|
+
? letterColorTable(customColorScheme)
|
|
1344
|
+
: (colorSchemes[colorSchemeName] ??
|
|
1345
|
+
colorSchemes[defaultColorSchemeName]);
|
|
1346
|
+
},
|
|
1347
|
+
/**
|
|
1348
|
+
* #getter
|
|
1349
|
+
* the scheme coloring each cell from its column's statistics, or
|
|
1350
|
+
* undefined while the cells take a fixed color per letter, from the
|
|
1351
|
+
* built-in table or from `customColorScheme`
|
|
1352
|
+
*/
|
|
1353
|
+
get dynamicColorSchemeName() {
|
|
1354
|
+
const { customColorScheme, colorSchemeName } = self;
|
|
1355
|
+
return !customColorScheme && colorSchemeName.includes('dynamic')
|
|
1356
|
+
? colorSchemeName
|
|
1357
|
+
: undefined;
|
|
1109
1358
|
},
|
|
1110
1359
|
/**
|
|
1111
1360
|
* #getter
|
|
@@ -1126,7 +1375,7 @@ function stateModelFactory() {
|
|
|
1126
1375
|
return !!this.tree.noTree;
|
|
1127
1376
|
},
|
|
1128
1377
|
get noDomains() {
|
|
1129
|
-
return self.
|
|
1378
|
+
return self.allAnnotations.length === 0;
|
|
1130
1379
|
},
|
|
1131
1380
|
/**
|
|
1132
1381
|
* #getter
|
|
@@ -1134,12 +1383,13 @@ function stateModelFactory() {
|
|
|
1134
1383
|
* from disk or pasted in becomes inline text, and DataModel drops an
|
|
1135
1384
|
* inline document past `maxInlineSnapshotBytes`.
|
|
1136
1385
|
*
|
|
1137
|
-
* The header lists these, and the standalone app
|
|
1138
|
-
* address bar while the list is non-empty
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1386
|
+
* The header lists these, and the standalone app drops `#data=` from
|
|
1387
|
+
* the address bar while the list is non-empty or the encoded link would
|
|
1388
|
+
* pass its `maxLinkLength`, so a copied link never opens an empty viewer
|
|
1389
|
+
* unannounced. A document fetched from a URL never appears here, since
|
|
1390
|
+
* the snapshot keeps its filehandle.
|
|
1141
1391
|
*
|
|
1142
|
-
* Empty when `
|
|
1392
|
+
* Empty when `hostCarriesData` is true.
|
|
1143
1393
|
*/
|
|
1144
1394
|
get unshareableData() {
|
|
1145
1395
|
if (self.hostRestoresData) {
|
|
@@ -1172,7 +1422,8 @@ function stateModelFactory() {
|
|
|
1172
1422
|
* `data.treeMetadata`, the name that travels in existing links, so the
|
|
1173
1423
|
* inline size limit and `treeMetadataFilehandle` cover it. labelWidthMap
|
|
1174
1424
|
* reads it on every layout, so a malformed user-supplied file returns {}
|
|
1175
|
-
* instead of throwing out of rendering
|
|
1425
|
+
* instead of throwing out of rendering, and every field is a string by
|
|
1426
|
+
* the time a scale sorts it.
|
|
1176
1427
|
*/
|
|
1177
1428
|
get rowData() {
|
|
1178
1429
|
const text = self.data.treeMetadata;
|
|
@@ -1182,7 +1433,10 @@ function stateModelFactory() {
|
|
|
1182
1433
|
try {
|
|
1183
1434
|
const parsed = JSON.parse(text);
|
|
1184
1435
|
return typeof parsed === 'object' && parsed !== null
|
|
1185
|
-
? parsed
|
|
1436
|
+
? Object.fromEntries(Object.entries(parsed).map(([name, row]) => [
|
|
1437
|
+
name,
|
|
1438
|
+
stringFields(row),
|
|
1439
|
+
]))
|
|
1186
1440
|
: {};
|
|
1187
1441
|
}
|
|
1188
1442
|
catch (e) {
|
|
@@ -1230,8 +1484,9 @@ function stateModelFactory() {
|
|
|
1230
1484
|
},
|
|
1231
1485
|
/**
|
|
1232
1486
|
* #getter
|
|
1487
|
+
* the tree the file or the alignment gives, before `treeRoot`
|
|
1233
1488
|
*/
|
|
1234
|
-
get
|
|
1489
|
+
get inputTree() {
|
|
1235
1490
|
const text = self.data.tree;
|
|
1236
1491
|
return text
|
|
1237
1492
|
? generateNodeIds(parseTreeText(text))
|
|
@@ -1242,6 +1497,18 @@ function stateModelFactory() {
|
|
|
1242
1497
|
name: 'empty',
|
|
1243
1498
|
});
|
|
1244
1499
|
},
|
|
1500
|
+
/**
|
|
1501
|
+
* #getter
|
|
1502
|
+
* the tree as the file or the alignment gives it, rerooted where
|
|
1503
|
+
* `treeRoot` asks. Every node id, and so `collapsed`, `rotated` and
|
|
1504
|
+
* `showOnly`, refers to this tree.
|
|
1505
|
+
*/
|
|
1506
|
+
get tree() {
|
|
1507
|
+
const tree = this.inputTree;
|
|
1508
|
+
return self.treeRoot && !tree.noTree
|
|
1509
|
+
? rootTree(tree, self.treeRoot)
|
|
1510
|
+
: tree;
|
|
1511
|
+
},
|
|
1245
1512
|
/**
|
|
1246
1513
|
* #getter
|
|
1247
1514
|
* Returns the list of row (sequence) names in display order.
|
|
@@ -1261,8 +1528,8 @@ function stateModelFactory() {
|
|
|
1261
1528
|
* `clades` resolved to the rows each one covers. The tip names resolve
|
|
1262
1529
|
* against `tree` rather than `root`, so a clade whose ancestor the user
|
|
1263
1530
|
* collapsed keeps its rows. One leaf pass over the tree serves every
|
|
1264
|
-
* clade. A `range` record names no node, so `collapse
|
|
1265
|
-
* need one, drop it.
|
|
1531
|
+
* clade. A `range` record names no node, so `collapse`, `focus` and
|
|
1532
|
+
* `rotate`, which need one, drop it.
|
|
1266
1533
|
*/
|
|
1267
1534
|
get resolvedClades() {
|
|
1268
1535
|
if (self.clades.length === 0) {
|
|
@@ -1315,7 +1582,24 @@ function stateModelFactory() {
|
|
|
1315
1582
|
* #getter
|
|
1316
1583
|
*/
|
|
1317
1584
|
get root() {
|
|
1318
|
-
return buildTreeRoot(this.tree,
|
|
1585
|
+
return buildTreeRoot(this.tree, {
|
|
1586
|
+
collapsed: self.collapsed,
|
|
1587
|
+
rotated: self.rotated,
|
|
1588
|
+
order: self.treeOrder,
|
|
1589
|
+
showOnly: self.showOnly,
|
|
1590
|
+
});
|
|
1591
|
+
},
|
|
1592
|
+
/**
|
|
1593
|
+
* #getter
|
|
1594
|
+
* the tree as Newick in the order and with the root it is drawn, with
|
|
1595
|
+
* every clade, collapsed or out of focus
|
|
1596
|
+
*/
|
|
1597
|
+
get treeNewick() {
|
|
1598
|
+
return writeNewick(buildTreeRoot(this.tree, {
|
|
1599
|
+
collapsed: [],
|
|
1600
|
+
rotated: self.rotated,
|
|
1601
|
+
order: self.treeOrder,
|
|
1602
|
+
}));
|
|
1319
1603
|
},
|
|
1320
1604
|
/**
|
|
1321
1605
|
* #getter
|
|
@@ -1337,7 +1621,11 @@ function stateModelFactory() {
|
|
|
1337
1621
|
if (!self.showTreeOverview) {
|
|
1338
1622
|
return undefined;
|
|
1339
1623
|
}
|
|
1340
|
-
const root = buildTreeRoot(this.tree,
|
|
1624
|
+
const root = buildTreeRoot(this.tree, {
|
|
1625
|
+
collapsed: self.collapsed,
|
|
1626
|
+
rotated: self.rotated,
|
|
1627
|
+
order: self.treeOrder,
|
|
1628
|
+
});
|
|
1341
1629
|
const numTips = leaves(root).length;
|
|
1342
1630
|
clusterLayout(root, numTips, 1);
|
|
1343
1631
|
const rootLen = Math.max(root.data.length || 0, 0);
|
|
@@ -1409,7 +1697,7 @@ function stateModelFactory() {
|
|
|
1409
1697
|
* alignment, the sum of each record's width
|
|
1410
1698
|
*/
|
|
1411
1699
|
get rowPanelsWidth() {
|
|
1412
|
-
return sum(self.rowPanels.map(p => rowPanelWidth(p, self
|
|
1700
|
+
return sum(self.rowPanels.map(p => rowPanelWidth(p, self)));
|
|
1413
1701
|
},
|
|
1414
1702
|
/**
|
|
1415
1703
|
* #getter
|
|
@@ -1453,9 +1741,10 @@ function stateModelFactory() {
|
|
|
1453
1741
|
return [];
|
|
1454
1742
|
}
|
|
1455
1743
|
// ragged input (a3m, hand-edited fasta) can have rows shorter than the
|
|
1456
|
-
// alignment; the widest row
|
|
1457
|
-
//
|
|
1458
|
-
|
|
1744
|
+
// alignment; the widest row of the whole alignment, collapsed rows
|
|
1745
|
+
// included, defines the column count as it does for numColumns, and a
|
|
1746
|
+
// row that stops early counts as gapped for the remainder
|
|
1747
|
+
const numCols = this.MSA?.getWidth() ?? 0;
|
|
1459
1748
|
const numRows = strs.length;
|
|
1460
1749
|
const threshold = Math.ceil((realAllowedGappyness / 100) * numRows);
|
|
1461
1750
|
const blankCounts = new Uint32Array(numCols);
|
|
@@ -1577,7 +1866,7 @@ function stateModelFactory() {
|
|
|
1577
1866
|
* #getter
|
|
1578
1867
|
*/
|
|
1579
1868
|
get colStats() {
|
|
1580
|
-
return columnCountsFromRows(this.columns2d);
|
|
1869
|
+
return columnCountsFromRows(this.columns2d, this.numColumns);
|
|
1581
1870
|
},
|
|
1582
1871
|
/**
|
|
1583
1872
|
* #getter
|
|
@@ -1669,38 +1958,39 @@ function stateModelFactory() {
|
|
|
1669
1958
|
},
|
|
1670
1959
|
/**
|
|
1671
1960
|
* #getter
|
|
1672
|
-
*
|
|
1961
|
+
* `root` laid out on new nodes in row and branch-length units, read in
|
|
1962
|
+
* pixels through `rowHeight` and `treeWidth`. A zoom leaves it and every
|
|
1963
|
+
* node in it as they are, and an observer reading a node's `x` or `len`
|
|
1964
|
+
* observes the size it scales by.
|
|
1965
|
+
*/
|
|
1966
|
+
get treeLayout() {
|
|
1967
|
+
return layoutTree(this.root, {
|
|
1968
|
+
get rowHeight() {
|
|
1969
|
+
return self.rowHeight;
|
|
1970
|
+
},
|
|
1971
|
+
get treeWidth() {
|
|
1972
|
+
return self.treeWidth;
|
|
1973
|
+
},
|
|
1974
|
+
});
|
|
1975
|
+
},
|
|
1976
|
+
/**
|
|
1977
|
+
* #getter
|
|
1978
|
+
* the laid-out tree, with pixel positions on every node
|
|
1673
1979
|
*/
|
|
1674
1980
|
get hierarchy() {
|
|
1675
|
-
|
|
1676
|
-
clusterLayout(r, this.totalHeight, self.treeWidth);
|
|
1677
|
-
const max = this.rootToTipLength;
|
|
1678
|
-
const k = max ? self.treeWidth / max : 0;
|
|
1679
|
-
// the displayed root starts at x=0, so subtract its length here; `root`
|
|
1680
|
-
// returns the cached parse, which must not be mutated
|
|
1681
|
-
setBrLength(r, -Math.max(r.data.length || 0, 0), k);
|
|
1682
|
-
// for each collapsed clade, record the pixel x-position of its farthest
|
|
1683
|
-
// tip so the renderer can draw a triangle spanning the branch-length
|
|
1684
|
-
// extent of the hidden subtree
|
|
1685
|
-
forEachDescendant(r, node => {
|
|
1686
|
-
if (node._children) {
|
|
1687
|
-
node.collapsedTipXFar =
|
|
1688
|
-
(node.len ?? 0) + collapsedSubtreeMaxLength(node) * k;
|
|
1689
|
-
}
|
|
1690
|
-
});
|
|
1691
|
-
return r;
|
|
1981
|
+
return this.treeLayout.root;
|
|
1692
1982
|
},
|
|
1693
1983
|
/**
|
|
1694
1984
|
* #getter
|
|
1695
1985
|
*/
|
|
1696
1986
|
get totalHeight() {
|
|
1697
|
-
return
|
|
1987
|
+
return this.leaves.length * self.rowHeight;
|
|
1698
1988
|
},
|
|
1699
1989
|
/**
|
|
1700
1990
|
* #getter
|
|
1701
1991
|
*/
|
|
1702
1992
|
get leaves() {
|
|
1703
|
-
return
|
|
1993
|
+
return this.treeLayout.leaves;
|
|
1704
1994
|
},
|
|
1705
1995
|
/**
|
|
1706
1996
|
* #getter
|
|
@@ -1708,8 +1998,7 @@ function stateModelFactory() {
|
|
|
1708
1998
|
* the tree's own units
|
|
1709
1999
|
*/
|
|
1710
2000
|
get rootToTipLength() {
|
|
1711
|
-
|
|
1712
|
-
return maxLength(r) - Math.max(r.data.length || 0, 0);
|
|
2001
|
+
return this.treeLayout.rootToTipLength;
|
|
1713
2002
|
},
|
|
1714
2003
|
/**
|
|
1715
2004
|
* #getter
|
|
@@ -1733,7 +2022,7 @@ function stateModelFactory() {
|
|
|
1733
2022
|
* max topological depth to a tip, used to scale cladogram x-positions
|
|
1734
2023
|
*/
|
|
1735
2024
|
get maxDepthToLeaf() {
|
|
1736
|
-
return
|
|
2025
|
+
return this.treeLayout.root.depthToLeaf;
|
|
1737
2026
|
},
|
|
1738
2027
|
/**
|
|
1739
2028
|
* #getter
|
|
@@ -1942,12 +2231,6 @@ function stateModelFactory() {
|
|
|
1942
2231
|
get secondaryStructureConsensus() {
|
|
1943
2232
|
return self.MSA?.secondaryStructureConsensus;
|
|
1944
2233
|
},
|
|
1945
|
-
/**
|
|
1946
|
-
* #getter
|
|
1947
|
-
*/
|
|
1948
|
-
get seqConsensus() {
|
|
1949
|
-
return self.MSA?.seqConsensus;
|
|
1950
|
-
},
|
|
1951
2234
|
/**
|
|
1952
2235
|
* #getter
|
|
1953
2236
|
* the base pairs of the consensus secondary structure, as arcs, in
|
|
@@ -2080,11 +2363,13 @@ function stateModelFactory() {
|
|
|
2080
2363
|
},
|
|
2081
2364
|
/**
|
|
2082
2365
|
* #method
|
|
2083
|
-
* Return a row
|
|
2366
|
+
* Return the character a row holds at a visible column: a residue, or
|
|
2367
|
+
* the gap character (`-` or `.`) on a gap. Undefined for a row name the
|
|
2368
|
+
* alignment does not have or a column past the row's end.
|
|
2084
2369
|
*
|
|
2085
2370
|
* @param rowName - The name of the row
|
|
2086
2371
|
* @param visibleCol - The visible column index (what the user sees on screen)
|
|
2087
|
-
* @returns The
|
|
2372
|
+
* @returns The character at that position
|
|
2088
2373
|
*/
|
|
2089
2374
|
visibleColToRowLetter(rowName, visibleCol) {
|
|
2090
2375
|
return self.rowMap.get(rowName)?.[this.visibleColToGlobalCol(visibleCol)];
|
|
@@ -2171,6 +2456,21 @@ function stateModelFactory() {
|
|
|
2171
2456
|
// row, otherwise one past the last column
|
|
2172
2457
|
return col ?? (seqPos === 0 ? 0 : seq.length);
|
|
2173
2458
|
},
|
|
2459
|
+
/**
|
|
2460
|
+
* #method
|
|
2461
|
+
* the global column of a span's last residue, clamping a position past
|
|
2462
|
+
* the end of the row to its last residue. Undefined for a row name the
|
|
2463
|
+
* alignment does not have, or a row with no residues.
|
|
2464
|
+
*
|
|
2465
|
+
* @param rowName - The name of the row
|
|
2466
|
+
* @param seqPos - The sequence position (0-based, ungapped)
|
|
2467
|
+
*/
|
|
2468
|
+
seqEndToGlobalCol(rowName, seqPos) {
|
|
2469
|
+
const index = self.seqPosIndex(rowName);
|
|
2470
|
+
return index && index.length > 0
|
|
2471
|
+
? index[Math.min(seqPos, index.length - 1)]
|
|
2472
|
+
: undefined;
|
|
2473
|
+
},
|
|
2174
2474
|
/**
|
|
2175
2475
|
* #method
|
|
2176
2476
|
* Convert a sequence position (ungapped) directly to a visible column index.
|
|
@@ -2199,10 +2499,10 @@ function stateModelFactory() {
|
|
|
2199
2499
|
const start = Math.max(1, Math.floor(rawStart));
|
|
2200
2500
|
const end = Math.ceil(rawEnd);
|
|
2201
2501
|
let startGlobal = start - 1;
|
|
2202
|
-
let endGlobal = end - 1;
|
|
2502
|
+
let endGlobal = Math.min(end, self.MSA?.getWidth() ?? end) - 1;
|
|
2203
2503
|
if (row !== undefined) {
|
|
2204
2504
|
const rowStart = this.seqPosToGlobalCol(row, start - 1);
|
|
2205
|
-
const rowEnd = this.
|
|
2505
|
+
const rowEnd = this.seqEndToGlobalCol(row, end - 1);
|
|
2206
2506
|
if (rowStart === undefined || rowEnd === undefined) {
|
|
2207
2507
|
return undefined;
|
|
2208
2508
|
}
|
|
@@ -2402,7 +2702,7 @@ function stateModelFactory() {
|
|
|
2402
2702
|
// first occurrence wins; only name, description and segment start are
|
|
2403
2703
|
// read from it
|
|
2404
2704
|
const types = new Map();
|
|
2405
|
-
for (const annot of self.
|
|
2705
|
+
for (const annot of self.allAnnotations) {
|
|
2406
2706
|
if (!types.has(annot.accession)) {
|
|
2407
2707
|
types.set(annot.accession, annot);
|
|
2408
2708
|
}
|
|
@@ -2410,7 +2710,7 @@ function stateModelFactory() {
|
|
|
2410
2710
|
return types;
|
|
2411
2711
|
},
|
|
2412
2712
|
get filteredAnnotations() {
|
|
2413
|
-
return self.
|
|
2713
|
+
return self.allAnnotations.filter(r => !self.turnedOffFeatures.get(r.accession));
|
|
2414
2714
|
},
|
|
2415
2715
|
get annotationsByRow() {
|
|
2416
2716
|
return groupBy(this.filteredAnnotations, r => r.id);
|
|
@@ -2457,6 +2757,28 @@ function stateModelFactory() {
|
|
|
2457
2757
|
},
|
|
2458
2758
|
}))
|
|
2459
2759
|
.views(self => ({
|
|
2760
|
+
/**
|
|
2761
|
+
* #getter
|
|
2762
|
+
*/
|
|
2763
|
+
get verticalScrollbarWidth() {
|
|
2764
|
+
return self.showVerticalScrollbar ? 20 : 0;
|
|
2765
|
+
},
|
|
2766
|
+
/**
|
|
2767
|
+
* #getter
|
|
2768
|
+
* width of the alignment canvas: the msa area less the vertical
|
|
2769
|
+
* scrollbar. showHorizontalScrollbar must not read it, since that feeds
|
|
2770
|
+
* msaAreaHeight -> showVerticalScrollbar and would form a cycle
|
|
2771
|
+
*/
|
|
2772
|
+
get msaCanvasWidth() {
|
|
2773
|
+
return self.msaAreaWidth - this.verticalScrollbarWidth;
|
|
2774
|
+
},
|
|
2775
|
+
/**
|
|
2776
|
+
* #getter
|
|
2777
|
+
* most-negative allowed scrollX, which keeps the last column in view
|
|
2778
|
+
*/
|
|
2779
|
+
get maxScrollX() {
|
|
2780
|
+
return Math.min(-self.totalWidth + this.msaCanvasWidth, 0);
|
|
2781
|
+
},
|
|
2460
2782
|
/**
|
|
2461
2783
|
* #getter
|
|
2462
2784
|
*/
|
|
@@ -2471,9 +2793,29 @@ function stateModelFactory() {
|
|
|
2471
2793
|
},
|
|
2472
2794
|
/**
|
|
2473
2795
|
* #getter
|
|
2796
|
+
* warnings that follow from the loaded data and clear when it changes. A
|
|
2797
|
+
* GFF whose first column names rows of some other alignment parses
|
|
2798
|
+
* without error and draws nothing
|
|
2799
|
+
*/
|
|
2800
|
+
get dataWarnings() {
|
|
2801
|
+
const { annotations, rowNamesSet } = self;
|
|
2802
|
+
return self.dataInitialized &&
|
|
2803
|
+
annotations.length > 0 &&
|
|
2804
|
+
!annotations.some(a => rowNamesSet.has(a.id))
|
|
2805
|
+
? [
|
|
2806
|
+
`0 of ${annotations.length} annotations name a row in this alignment`,
|
|
2807
|
+
]
|
|
2808
|
+
: [];
|
|
2809
|
+
},
|
|
2810
|
+
/**
|
|
2811
|
+
* #getter
|
|
2812
|
+
* the load warnings and the undismissed data warnings, for the header
|
|
2474
2813
|
*/
|
|
2475
|
-
get
|
|
2476
|
-
return
|
|
2814
|
+
get warnings() {
|
|
2815
|
+
return [
|
|
2816
|
+
...self.loadWarnings,
|
|
2817
|
+
...this.dataWarnings.filter(w => !self.dismissedWarnings.includes(w)),
|
|
2818
|
+
];
|
|
2477
2819
|
},
|
|
2478
2820
|
/**
|
|
2479
2821
|
* #getter
|
|
@@ -2499,6 +2841,13 @@ function stateModelFactory() {
|
|
|
2499
2841
|
setDrawMsaLetters(arg) {
|
|
2500
2842
|
self.drawMsaLetters = arg;
|
|
2501
2843
|
},
|
|
2844
|
+
/**
|
|
2845
|
+
* #action
|
|
2846
|
+
*/
|
|
2847
|
+
clearWarnings() {
|
|
2848
|
+
self.loadWarnings = [];
|
|
2849
|
+
self.dismissedWarnings = self.dataWarnings;
|
|
2850
|
+
},
|
|
2502
2851
|
/**
|
|
2503
2852
|
* #action
|
|
2504
2853
|
*/
|
|
@@ -2553,13 +2902,36 @@ function stateModelFactory() {
|
|
|
2553
2902
|
},
|
|
2554
2903
|
/**
|
|
2555
2904
|
* #action
|
|
2556
|
-
*
|
|
2557
|
-
* and
|
|
2905
|
+
* root the tree on the branch above the given node. The node's tips are
|
|
2906
|
+
* a split of the unrooted tree, and the side of it that is a clade in
|
|
2907
|
+
* the tree as written stands for it as an outgroup, by its first and
|
|
2908
|
+
* last tip, so the root survives the path ids changing under it
|
|
2909
|
+
*/
|
|
2910
|
+
rerootAt(nodeId) {
|
|
2911
|
+
const shown = find(hierarchy(self.tree, d => d.children), n => n.data.id === nodeId);
|
|
2912
|
+
if (!shown?.parent) {
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
const node = outgroupNode(hierarchy(self.inputTree, d => d.children), leaves(shown).map(n => n.data.name));
|
|
2916
|
+
if (node) {
|
|
2917
|
+
const tips = leaves(node);
|
|
2918
|
+
self.setTreeRoot({
|
|
2919
|
+
outgroup: [
|
|
2920
|
+
...new Set([tips[0].data.name, tips.at(-1).data.name]),
|
|
2921
|
+
],
|
|
2922
|
+
});
|
|
2923
|
+
}
|
|
2924
|
+
},
|
|
2925
|
+
/**
|
|
2926
|
+
* #action
|
|
2927
|
+
* swap in a different tree over the same alignment. Clears `collapsed`,
|
|
2928
|
+
* `rotated` and `showOnly`, since path-derived node ids (node-0-0-1) from the old
|
|
2558
2929
|
* tree would match unrelated nodes in the new one.
|
|
2559
2930
|
*/
|
|
2560
2931
|
replaceTree(newick) {
|
|
2561
2932
|
transaction(() => {
|
|
2562
2933
|
self.collapsed.clear();
|
|
2934
|
+
self.rotated.clear();
|
|
2563
2935
|
self.setShowOnly(undefined);
|
|
2564
2936
|
self.setTree(newick);
|
|
2565
2937
|
});
|
|
@@ -2572,49 +2944,48 @@ function stateModelFactory() {
|
|
|
2572
2944
|
self.setColWidth(defaultColWidth);
|
|
2573
2945
|
self.setRowHeight(defaultRowHeight);
|
|
2574
2946
|
},
|
|
2947
|
+
/**
|
|
2948
|
+
* #action
|
|
2949
|
+
* zoom by `scaleFactor` about the center of the alignment area
|
|
2950
|
+
*/
|
|
2951
|
+
zoomAtCenter(scaleFactor, axis = 'both') {
|
|
2952
|
+
this.zoomToPos(scaleFactor, self.msaCanvasWidth / 2, self.msaAreaHeight / 2, axis);
|
|
2953
|
+
},
|
|
2575
2954
|
/**
|
|
2576
2955
|
* #action
|
|
2577
2956
|
*/
|
|
2578
2957
|
zoomOutHorizontal() {
|
|
2579
|
-
|
|
2580
|
-
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0);
|
|
2958
|
+
this.zoomAtCenter(0.75, 'horizontal');
|
|
2581
2959
|
},
|
|
2582
2960
|
/**
|
|
2583
2961
|
* #action
|
|
2584
2962
|
*/
|
|
2585
2963
|
zoomInHorizontal() {
|
|
2586
|
-
|
|
2587
|
-
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0);
|
|
2964
|
+
this.zoomAtCenter(1.5, 'horizontal');
|
|
2588
2965
|
},
|
|
2589
2966
|
/**
|
|
2590
2967
|
* #action
|
|
2591
2968
|
*/
|
|
2592
2969
|
zoomInVertical() {
|
|
2593
|
-
|
|
2970
|
+
this.zoomAtCenter(1.5, 'vertical');
|
|
2594
2971
|
},
|
|
2595
2972
|
/**
|
|
2596
2973
|
* #action
|
|
2597
2974
|
*/
|
|
2598
2975
|
zoomOutVertical() {
|
|
2599
|
-
|
|
2976
|
+
this.zoomAtCenter(0.75, 'vertical');
|
|
2600
2977
|
},
|
|
2601
2978
|
/**
|
|
2602
2979
|
* #action
|
|
2603
2980
|
*/
|
|
2604
2981
|
zoomIn() {
|
|
2605
|
-
|
|
2606
|
-
this.zoomInHorizontal();
|
|
2607
|
-
this.zoomInVertical();
|
|
2608
|
-
});
|
|
2982
|
+
this.zoomAtCenter(1.5);
|
|
2609
2983
|
},
|
|
2610
2984
|
/**
|
|
2611
2985
|
* #action
|
|
2612
2986
|
*/
|
|
2613
2987
|
zoomOut() {
|
|
2614
|
-
|
|
2615
|
-
this.zoomOutHorizontal();
|
|
2616
|
-
this.zoomOutVertical();
|
|
2617
|
-
});
|
|
2988
|
+
this.zoomAtCenter(0.75);
|
|
2618
2989
|
},
|
|
2619
2990
|
/**
|
|
2620
2991
|
* #action
|
|
@@ -2744,25 +3115,11 @@ function stateModelFactory() {
|
|
|
2744
3115
|
},
|
|
2745
3116
|
}))
|
|
2746
3117
|
.views(self => ({
|
|
2747
|
-
/**
|
|
2748
|
-
* #getter
|
|
2749
|
-
*/
|
|
2750
|
-
get verticalScrollbarWidth() {
|
|
2751
|
-
return self.showVerticalScrollbar ? 20 : 0;
|
|
2752
|
-
},
|
|
2753
|
-
/**
|
|
2754
|
-
* #getter
|
|
2755
|
-
* width of the alignment canvas: the msa area less the vertical
|
|
2756
|
-
* scrollbar. showHorizontalScrollbar must not read it, since that feeds
|
|
2757
|
-
* msaAreaHeight -> showVerticalScrollbar and would form a cycle
|
|
2758
|
-
*/
|
|
2759
|
-
get msaCanvasWidth() {
|
|
2760
|
-
return self.msaAreaWidth - this.verticalScrollbarWidth;
|
|
2761
|
-
},
|
|
2762
3118
|
/**
|
|
2763
3119
|
* #method
|
|
2764
3120
|
* the cell at a visible column and row index, in the coordinates a host
|
|
2765
|
-
* writes highlights in
|
|
3121
|
+
* writes highlights in. On a gap `residue` is undefined and `letter` is
|
|
3122
|
+
* the gap character
|
|
2766
3123
|
*/
|
|
2767
3124
|
cellAt(visibleCol, rowIndex) {
|
|
2768
3125
|
const column = self.visibleColToGlobalCol(visibleCol);
|
|
@@ -2809,7 +3166,7 @@ function stateModelFactory() {
|
|
|
2809
3166
|
}
|
|
2810
3167
|
const { xStart, xEnd } = visibleColRange({
|
|
2811
3168
|
offsetX: -scrollX,
|
|
2812
|
-
blockWidth:
|
|
3169
|
+
blockWidth: self.msaCanvasWidth,
|
|
2813
3170
|
colWidth,
|
|
2814
3171
|
});
|
|
2815
3172
|
const last = Math.max(0, Math.min(xEnd, numColumns) - 1);
|
|
@@ -2946,7 +3303,7 @@ function stateModelFactory() {
|
|
|
2946
3303
|
add(field, legend);
|
|
2947
3304
|
}
|
|
2948
3305
|
}
|
|
2949
|
-
for (const { legendTitle, legend } of this.
|
|
3306
|
+
for (const { legendTitle, legend } of this.rowPanelScales) {
|
|
2950
3307
|
add(legendTitle, legend);
|
|
2951
3308
|
}
|
|
2952
3309
|
return [...byKey.values()];
|
|
@@ -2988,7 +3345,7 @@ function stateModelFactory() {
|
|
|
2988
3345
|
// column moves to the neighboring boundary. Bands with no visible
|
|
2989
3346
|
// columns, or naming a missing row, are dropped.
|
|
2990
3347
|
const start = self.seqPosToGlobalCol(name, annotation.start - 1);
|
|
2991
|
-
const end = self.
|
|
3348
|
+
const end = self.seqEndToGlobalCol(name, annotation.end - 1);
|
|
2992
3349
|
if (start === undefined || end === undefined) {
|
|
2993
3350
|
return undefined;
|
|
2994
3351
|
}
|
|
@@ -3065,18 +3422,7 @@ function stateModelFactory() {
|
|
|
3065
3422
|
* on every mouse move.
|
|
3066
3423
|
*/
|
|
3067
3424
|
get highlightedColumnRuns() {
|
|
3068
|
-
|
|
3069
|
-
const runs = [];
|
|
3070
|
-
for (const col of [...(highlightedColumns ?? [])].sort((a, b) => a - b)) {
|
|
3071
|
-
const last = runs.at(-1);
|
|
3072
|
-
if (last && col === last.end + 1) {
|
|
3073
|
-
last.end = col;
|
|
3074
|
-
}
|
|
3075
|
-
else {
|
|
3076
|
-
runs.push({ start: col, end: col });
|
|
3077
|
-
}
|
|
3078
|
-
}
|
|
3079
|
-
return runs;
|
|
3425
|
+
return consecutiveRuns([...(self.highlightedColumns ?? [])].sort((a, b) => a - b)).map(([start, end]) => ({ start, end }));
|
|
3080
3426
|
},
|
|
3081
3427
|
/**
|
|
3082
3428
|
* #method
|
|
@@ -3127,6 +3473,78 @@ function stateModelFactory() {
|
|
|
3127
3473
|
return span ? [{ ...base, ...span, rowIndices: [] }] : [];
|
|
3128
3474
|
});
|
|
3129
3475
|
},
|
|
3476
|
+
/**
|
|
3477
|
+
* #getter
|
|
3478
|
+
* `selection` projected onto what is on screen, the way
|
|
3479
|
+
* `resolvedHighlights` projects a column span and a row set. A
|
|
3480
|
+
* selection whose columns are all hidden, or whose rows are all
|
|
3481
|
+
* collapsed away or unknown, resolves to undefined.
|
|
3482
|
+
*/
|
|
3483
|
+
get resolvedSelection() {
|
|
3484
|
+
const { selection, rowNamesSet, numRows } = self;
|
|
3485
|
+
const span = selection &&
|
|
3486
|
+
self.visibleSpan({ start: selection.start, end: selection.end });
|
|
3487
|
+
if (!selection || !span) {
|
|
3488
|
+
return undefined;
|
|
3489
|
+
}
|
|
3490
|
+
const rowRuns = selection.rows
|
|
3491
|
+
? consecutiveRuns(selection.rows
|
|
3492
|
+
.map(name => rowNamesSet.get(name))
|
|
3493
|
+
.filter(notEmpty)
|
|
3494
|
+
.sort((a, b) => a - b))
|
|
3495
|
+
: numRows > 0
|
|
3496
|
+
? [[0, numRows - 1]]
|
|
3497
|
+
: [];
|
|
3498
|
+
return rowRuns.length > 0 ? { ...span, rowRuns } : undefined;
|
|
3499
|
+
},
|
|
3500
|
+
/**
|
|
3501
|
+
* #getter
|
|
3502
|
+
* the columns and rows the selection covers, for the header's readout.
|
|
3503
|
+
* Columns count the file's columns from `start` to `end`, hidden ones
|
|
3504
|
+
* included, as `selectionFasta` copies them.
|
|
3505
|
+
*/
|
|
3506
|
+
get selectionSize() {
|
|
3507
|
+
const resolved = this.resolvedSelection;
|
|
3508
|
+
const span = this.selectionFileSpan;
|
|
3509
|
+
return resolved && span
|
|
3510
|
+
? {
|
|
3511
|
+
columns: span.end - span.start + 1,
|
|
3512
|
+
rows: sum(resolved.rowRuns.map(([a, b]) => b - a + 1)),
|
|
3513
|
+
}
|
|
3514
|
+
: undefined;
|
|
3515
|
+
},
|
|
3516
|
+
/**
|
|
3517
|
+
* #getter
|
|
3518
|
+
* `selection`'s columns clamped to the file's
|
|
3519
|
+
*/
|
|
3520
|
+
get selectionFileSpan() {
|
|
3521
|
+
const { selection } = self;
|
|
3522
|
+
const width = self.MSA?.getWidth() ?? 0;
|
|
3523
|
+
const start = Math.max(1, Math.floor(selection?.start ?? 0));
|
|
3524
|
+
const end = Math.min(width, Math.ceil(selection?.end ?? 0));
|
|
3525
|
+
return selection && start <= end ? { start, end } : undefined;
|
|
3526
|
+
},
|
|
3527
|
+
/**
|
|
3528
|
+
* #getter
|
|
3529
|
+
* the selected block as FASTA: the selected rows on screen, top to
|
|
3530
|
+
* bottom, each with its letters across the file's columns from `start`
|
|
3531
|
+
* to `end`, gaps included
|
|
3532
|
+
*/
|
|
3533
|
+
get selectionFasta() {
|
|
3534
|
+
const { rowNames, rowMap } = self;
|
|
3535
|
+
const resolved = this.resolvedSelection;
|
|
3536
|
+
const span = this.selectionFileSpan;
|
|
3537
|
+
if (!resolved || !span) {
|
|
3538
|
+
return '';
|
|
3539
|
+
}
|
|
3540
|
+
const rows = resolved.rowRuns
|
|
3541
|
+
.flatMap(([first, last]) => rowNames.slice(first, last + 1))
|
|
3542
|
+
.flatMap(name => {
|
|
3543
|
+
const seq = rowMap.get(name);
|
|
3544
|
+
return seq === undefined ? [] : [[name, seq]];
|
|
3545
|
+
});
|
|
3546
|
+
return blockFasta(rows, span.start, span.end);
|
|
3547
|
+
},
|
|
3130
3548
|
/**
|
|
3131
3549
|
* #method
|
|
3132
3550
|
* per-column summary statistics: consensus residue and its identity
|
|
@@ -3155,63 +3573,45 @@ function stateModelFactory() {
|
|
|
3155
3573
|
/**
|
|
3156
3574
|
* #getter
|
|
3157
3575
|
* each encoding with its scale resolved against the values its field
|
|
3158
|
-
* takes: a feature channel reads them across
|
|
3576
|
+
* takes: a feature channel reads them across every feature loaded, every
|
|
3159
3577
|
* other channel across the row table. Resolved once per change of that
|
|
3160
3578
|
* table or the encodings, never per row per frame.
|
|
3161
3579
|
*/
|
|
3162
3580
|
get resolvedEncodings() {
|
|
3163
3581
|
const rows = Object.values(self.rowData);
|
|
3164
|
-
const features = self.filteredAnnotations;
|
|
3165
3582
|
return self.encodings.map(encoding => ({
|
|
3166
3583
|
...encoding,
|
|
3167
|
-
...
|
|
3168
|
-
?
|
|
3169
|
-
|
|
3170
|
-
.filter(notEmpty)
|
|
3171
|
-
: rows.map(row => row?.[encoding.field]).filter(notEmpty)),
|
|
3584
|
+
...(featureChannels.has(encoding.channel)
|
|
3585
|
+
? resolveFeatureScale(encoding.field, encoding.scale, self.allAnnotations, self.filteredAnnotations)
|
|
3586
|
+
: resolveScale(encoding.scale, rows.map(row => row?.[encoding.field]).filter(notEmpty))),
|
|
3172
3587
|
}));
|
|
3173
3588
|
},
|
|
3174
3589
|
/**
|
|
3175
3590
|
* #getter
|
|
3176
|
-
* each row panel
|
|
3177
|
-
*
|
|
3178
|
-
*
|
|
3179
|
-
* per change of that table or the panels, never per block per frame.
|
|
3591
|
+
* each row panel's scale resolved against the values its field takes,
|
|
3592
|
+
* giving the colors, labels and legend entries. Reads no cell size, so
|
|
3593
|
+
* a zoom reuses it and the legends built from it.
|
|
3180
3594
|
*/
|
|
3181
|
-
get
|
|
3182
|
-
const
|
|
3595
|
+
get rowPanelScales() {
|
|
3596
|
+
const { rowData } = self;
|
|
3183
3597
|
const { fillPalette, featureFillEncoding, featureLabels } = this;
|
|
3184
3598
|
const annotations = self.filteredAnnotations;
|
|
3185
|
-
|
|
3186
|
-
return self.rowPanels.map((panel, index) => {
|
|
3187
|
-
const width = rowPanelWidth(panel, self.rowHeight);
|
|
3188
|
-
const base = { id: `rowpanel-${index}`, width, offsetX };
|
|
3189
|
-
offsetX += width;
|
|
3599
|
+
return self.rowPanels.map(panel => {
|
|
3190
3600
|
if (panel.kind === 'features') {
|
|
3191
3601
|
const color = panel.encoding?.color;
|
|
3192
3602
|
const encoding = color
|
|
3193
|
-
? resolveFeatureScale(color.field, color.scale, annotations)
|
|
3603
|
+
? resolveFeatureScale(color.field, color.scale, self.allAnnotations, annotations)
|
|
3194
3604
|
: featureFillEncoding;
|
|
3195
3605
|
const label = panel.encoding?.label;
|
|
3196
|
-
const align = panel.transform?.find(t => t.type === 'align');
|
|
3197
3606
|
return {
|
|
3198
|
-
...base,
|
|
3199
3607
|
kind: panel.kind,
|
|
3200
3608
|
x: panel.x,
|
|
3201
3609
|
header: panel.header ?? '',
|
|
3202
3610
|
field: encoding?.field,
|
|
3203
3611
|
legendTitle: encoding?.field,
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
colWidth: self.colWidth,
|
|
3208
|
-
domainBands: this.domainBands,
|
|
3209
|
-
annotationsByRow: self.annotationsByRow,
|
|
3210
|
-
shifts: align
|
|
3211
|
-
? this.featureAlignShifts.get(align.on)
|
|
3212
|
-
: undefined,
|
|
3213
|
-
}),
|
|
3214
|
-
colors: featureColorMap(annotations, encoding, fillPalette),
|
|
3612
|
+
colors: color
|
|
3613
|
+
? featureColorMap(annotations, encoding, fillPalette)
|
|
3614
|
+
: this.featureColors,
|
|
3215
3615
|
labels: label
|
|
3216
3616
|
? featureLabelMap(annotations, label)
|
|
3217
3617
|
: featureLabels,
|
|
@@ -3223,26 +3623,53 @@ function stateModelFactory() {
|
|
|
3223
3623
|
})),
|
|
3224
3624
|
};
|
|
3225
3625
|
}
|
|
3226
|
-
const { colorOf, legend } = resolveScale(panel.scale,
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
const value = row?.[panel.field];
|
|
3230
|
-
const color = value === undefined ? undefined : colorOf(value);
|
|
3231
|
-
if (color) {
|
|
3232
|
-
colors.set(name, color);
|
|
3233
|
-
}
|
|
3234
|
-
}
|
|
3626
|
+
const { colorOf, legend } = resolveScale(panel.scale, Object.values(rowData)
|
|
3627
|
+
.map(row => row?.[panel.field])
|
|
3628
|
+
.filter(notEmpty));
|
|
3235
3629
|
return {
|
|
3236
|
-
...base,
|
|
3237
3630
|
kind: panel.kind,
|
|
3238
3631
|
field: panel.field,
|
|
3239
3632
|
header: panel.header ?? panel.field,
|
|
3240
3633
|
legendTitle: panel.legend ?? panel.field,
|
|
3241
|
-
colors,
|
|
3634
|
+
colors: colorByRow(rowData, panel.field, colorOf),
|
|
3242
3635
|
legend,
|
|
3243
3636
|
};
|
|
3244
3637
|
});
|
|
3245
3638
|
},
|
|
3639
|
+
/**
|
|
3640
|
+
* #getter
|
|
3641
|
+
* each row panel with its scale from `rowPanelScales` and its geometry:
|
|
3642
|
+
* the pixel column it draws in and, for a features panel, the spans per
|
|
3643
|
+
* row. Resolved once per change of those inputs, never per block per
|
|
3644
|
+
* frame.
|
|
3645
|
+
*/
|
|
3646
|
+
get resolvedRowPanels() {
|
|
3647
|
+
const scales = this.rowPanelScales;
|
|
3648
|
+
let offsetX = 0;
|
|
3649
|
+
return self.rowPanels.map((panel, index) => {
|
|
3650
|
+
const width = rowPanelWidth(panel, self);
|
|
3651
|
+
const base = { id: `rowpanel-${index}`, width, offsetX };
|
|
3652
|
+
offsetX += width;
|
|
3653
|
+
if (panel.kind === 'features') {
|
|
3654
|
+
const align = panel.transform?.find(t => t.type === 'align');
|
|
3655
|
+
return {
|
|
3656
|
+
...base,
|
|
3657
|
+
...scales[index],
|
|
3658
|
+
spans: featurePanelSpans({
|
|
3659
|
+
panel,
|
|
3660
|
+
width,
|
|
3661
|
+
colWidth: self.colWidth,
|
|
3662
|
+
domainBands: this.domainBands,
|
|
3663
|
+
annotationsByRow: self.annotationsByRow,
|
|
3664
|
+
shifts: align
|
|
3665
|
+
? this.featureAlignShifts.get(align.on)
|
|
3666
|
+
: undefined,
|
|
3667
|
+
}),
|
|
3668
|
+
};
|
|
3669
|
+
}
|
|
3670
|
+
return { ...base, ...scales[index] };
|
|
3671
|
+
});
|
|
3672
|
+
},
|
|
3246
3673
|
/**
|
|
3247
3674
|
* #getter
|
|
3248
3675
|
* the shift an `align` transform gives each row, keyed by the feature
|
|
@@ -3274,18 +3701,9 @@ function stateModelFactory() {
|
|
|
3274
3701
|
*/
|
|
3275
3702
|
get tipLabelColors() {
|
|
3276
3703
|
const encoding = this.resolvedEncodings.find(e => e.channel === 'tipLabel');
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
const colors = new Map();
|
|
3281
|
-
for (const [name, row] of Object.entries(self.rowData)) {
|
|
3282
|
-
const value = row?.[encoding.field];
|
|
3283
|
-
const color = value === undefined ? undefined : encoding.colorOf(value);
|
|
3284
|
-
if (color) {
|
|
3285
|
-
colors.set(name, color);
|
|
3286
|
-
}
|
|
3287
|
-
}
|
|
3288
|
-
return colors;
|
|
3704
|
+
return encoding
|
|
3705
|
+
? colorByRow(self.rowData, encoding.field, encoding.colorOf)
|
|
3706
|
+
: undefined;
|
|
3289
3707
|
},
|
|
3290
3708
|
/**
|
|
3291
3709
|
* #getter
|
|
@@ -3298,13 +3716,10 @@ function stateModelFactory() {
|
|
|
3298
3716
|
if (!encoding) {
|
|
3299
3717
|
return undefined;
|
|
3300
3718
|
}
|
|
3719
|
+
const colors = colorByRow(self.rowData, encoding.field, encoding.colorOf);
|
|
3301
3720
|
return self.rowNames.map(name => {
|
|
3302
|
-
const
|
|
3303
|
-
|
|
3304
|
-
if (!color) {
|
|
3305
|
-
return undefined;
|
|
3306
|
-
}
|
|
3307
|
-
return withAlpha(color, rowTintAlpha);
|
|
3721
|
+
const color = colors.get(name);
|
|
3722
|
+
return color && withAlpha(color, rowTintAlpha);
|
|
3308
3723
|
});
|
|
3309
3724
|
},
|
|
3310
3725
|
/**
|
|
@@ -3380,7 +3795,8 @@ function stateModelFactory() {
|
|
|
3380
3795
|
*/
|
|
3381
3796
|
reset() {
|
|
3382
3797
|
self.resetCount++;
|
|
3383
|
-
self.
|
|
3798
|
+
self.loadWarnings = [];
|
|
3799
|
+
self.dismissedWarnings = [];
|
|
3384
3800
|
self.setStatus(undefined);
|
|
3385
3801
|
applySnapshot(self, Object.fromEntries(Object.entries(getSnapshot(self)).filter(([key]) => preservedOnReset.has(key))));
|
|
3386
3802
|
self.setError(undefined);
|
|
@@ -3400,6 +3816,14 @@ function stateModelFactory() {
|
|
|
3400
3816
|
const blob = new Blob([html], { type: 'image/svg+xml' });
|
|
3401
3817
|
saveAs(blob, exportFileName(self.msaFilehandle, 'svg'));
|
|
3402
3818
|
},
|
|
3819
|
+
/**
|
|
3820
|
+
* #action
|
|
3821
|
+
* save `treeNewick` as a file
|
|
3822
|
+
*/
|
|
3823
|
+
exportNewick() {
|
|
3824
|
+
const blob = new Blob([self.treeNewick], { type: 'text/plain' });
|
|
3825
|
+
saveAs(blob, exportFileName(self.treeFilehandle ?? self.msaFilehandle, 'nwk', 'tree'));
|
|
3826
|
+
},
|
|
3403
3827
|
/**
|
|
3404
3828
|
* #action
|
|
3405
3829
|
* show or hide an annotation type. Only hidden types are recorded; see
|
|
@@ -3451,6 +3875,51 @@ function stateModelFactory() {
|
|
|
3451
3875
|
self.scrollX = clamp(-span.startCol * self.colWidth, self.maxScrollX, 0);
|
|
3452
3876
|
});
|
|
3453
3877
|
},
|
|
3878
|
+
/**
|
|
3879
|
+
* #action
|
|
3880
|
+
* zoom and scroll so the selected columns fill the alignment's width,
|
|
3881
|
+
* and, where the selection names its rows, so those rows fill its height
|
|
3882
|
+
*/
|
|
3883
|
+
zoomToSelection() {
|
|
3884
|
+
const { selection, resolvedSelection } = self;
|
|
3885
|
+
if (!selection || !resolvedSelection) {
|
|
3886
|
+
return;
|
|
3887
|
+
}
|
|
3888
|
+
transaction(() => {
|
|
3889
|
+
this.zoomToRegion({ start: selection.start, end: selection.end });
|
|
3890
|
+
if (selection.rows && self.viewInitialized) {
|
|
3891
|
+
const { rowRuns } = resolvedSelection;
|
|
3892
|
+
const first = rowRuns[0][0];
|
|
3893
|
+
const last = rowRuns.at(-1)[1];
|
|
3894
|
+
self.rowHeight = clamp(self.msaAreaHeight / (last - first + 1), minRowHeight, maxCellSize);
|
|
3895
|
+
self.scrollY = clamp(-first * self.rowHeight, self.maxScrollY, 0);
|
|
3896
|
+
}
|
|
3897
|
+
});
|
|
3898
|
+
},
|
|
3899
|
+
/**
|
|
3900
|
+
* #action
|
|
3901
|
+
* select the block between two cells given as visible column and row
|
|
3902
|
+
* indices, the coordinates a drag reports, clamped to the alignment. A
|
|
3903
|
+
* cell with no `row` selects the columns across every row, and so does a
|
|
3904
|
+
* block spanning every row on screen.
|
|
3905
|
+
*/
|
|
3906
|
+
selectBlock(anchor, head) {
|
|
3907
|
+
const { numColumns, numRows, rowNames } = self;
|
|
3908
|
+
if (numColumns === 0 || numRows === 0) {
|
|
3909
|
+
return;
|
|
3910
|
+
}
|
|
3911
|
+
const ordered = (a, b, last) => [clamp(a, 0, last), clamp(b, 0, last)].sort((x, y) => x - y);
|
|
3912
|
+
const [startCol, endCol] = ordered(anchor.col, head.col, numColumns - 1);
|
|
3913
|
+
const [firstRow, lastRow] = anchor.row === undefined || head.row === undefined
|
|
3914
|
+
? [0, numRows - 1]
|
|
3915
|
+
: ordered(anchor.row, head.row, numRows - 1);
|
|
3916
|
+
const everyRow = firstRow === 0 && lastRow === numRows - 1;
|
|
3917
|
+
self.setSelection({
|
|
3918
|
+
start: self.visibleColToGlobalCol(startCol) + 1,
|
|
3919
|
+
end: self.visibleColToGlobalCol(endCol) + 1,
|
|
3920
|
+
...(everyRow ? {} : { rows: rowNames.slice(firstRow, lastRow + 1) }),
|
|
3921
|
+
});
|
|
3922
|
+
},
|
|
3454
3923
|
/**
|
|
3455
3924
|
* #action
|
|
3456
3925
|
*/
|
|
@@ -3465,23 +3934,22 @@ function stateModelFactory() {
|
|
|
3465
3934
|
if (self.highlightColumns?.length) {
|
|
3466
3935
|
self.setHighlightedColumns(self.highlightColumns);
|
|
3467
3936
|
}
|
|
3468
|
-
// The `collapse` and `
|
|
3469
|
-
// the subtree in focus, which the tree,
|
|
3470
|
-
// alignment all read. The tree arrives with
|
|
3471
|
-
// and later for a filehandle, so the seeding
|
|
3472
|
-
//
|
|
3473
|
-
//
|
|
3474
|
-
//
|
|
3475
|
-
//
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3937
|
+
// The `collapse`, `focus` and `rotate` clade marks seed the collapsed
|
|
3938
|
+
// list, the subtree in focus and the rotated nodes, which the tree,
|
|
3939
|
+
// `hideGapsEffective` and the alignment all read. The tree arrives with
|
|
3940
|
+
// the model for inline data and later for a filehandle, so the seeding
|
|
3941
|
+
// waits for it and then runs once per data load, per change of
|
|
3942
|
+
// `clades` and per reroot, which clears the node ids the seeding
|
|
3943
|
+
// wrote: expanding a seeded clade sticks until one of those changes.
|
|
3944
|
+
// `dataInitialized` is true once the MSA alone has loaded, when the
|
|
3945
|
+
// tree is still the flat stub, so a tree filehandle holds the seeding
|
|
3946
|
+
// until its text lands.
|
|
3947
|
+
addDisposer(self, reaction(() => self.dataInitialized && !(self.treeFilehandle && !self.data.tree)
|
|
3948
|
+
? { clades: getSnapshot(self.clades), treeRoot: self.treeRoot }
|
|
3949
|
+
: undefined, seed => {
|
|
3950
|
+
if (!seed?.clades.length) {
|
|
3482
3951
|
return;
|
|
3483
3952
|
}
|
|
3484
|
-
cladesSeeded = true;
|
|
3485
3953
|
for (const { mark, nodeId } of self.resolvedClades) {
|
|
3486
3954
|
if (nodeId === undefined) {
|
|
3487
3955
|
continue;
|
|
@@ -3492,8 +3960,12 @@ function stateModelFactory() {
|
|
|
3492
3960
|
else if (mark === 'focus') {
|
|
3493
3961
|
self.setShowOnly(nodeId);
|
|
3494
3962
|
}
|
|
3963
|
+
else if (mark === 'rotate' &&
|
|
3964
|
+
!self.rotated.includes(nodeId)) {
|
|
3965
|
+
self.toggleRotated(nodeId);
|
|
3966
|
+
}
|
|
3495
3967
|
}
|
|
3496
|
-
}));
|
|
3968
|
+
}, { fireImmediately: true, equals: compareStructural }));
|
|
3497
3969
|
// zoomToRegion needs a width and resolves residues through the visible
|
|
3498
3970
|
// columns, so this waits for both, and for a tree file whose collapsed
|
|
3499
3971
|
// clades would hide columns and move the target after the zoom
|
|
@@ -3646,16 +4118,6 @@ function stateModelFactory() {
|
|
|
3646
4118
|
self.setAnnotations([]);
|
|
3647
4119
|
}
|
|
3648
4120
|
}));
|
|
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
|
-
}));
|
|
3659
4121
|
// gffFilehandle loads into data.gff, which the autorun above parses
|
|
3660
4122
|
loadOnFilehandleChange({
|
|
3661
4123
|
what: 'annotations',
|
|
@@ -3688,7 +4150,7 @@ function stateModelFactory() {
|
|
|
3688
4150
|
// xref solution https://github.com/mobxjs/mobx/issues/266#issuecomment-222007278
|
|
3689
4151
|
// xref problem https://github.com/GMOD/react-msaview/issues/75
|
|
3690
4152
|
addDisposer(self, autorun(() => {
|
|
3691
|
-
if (self.
|
|
4153
|
+
if (self.dynamicColorSchemeName) {
|
|
3692
4154
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
3693
4155
|
self.colStats;
|
|
3694
4156
|
}
|
|
@@ -3738,7 +4200,7 @@ function stateModelFactory() {
|
|
|
3738
4200
|
// stripDefault handles per-property defaults; this drops inline documents
|
|
3739
4201
|
// whose filehandle can refetch them
|
|
3740
4202
|
...rest,
|
|
3741
|
-
...smallColumnTracks(columnTracks),
|
|
4203
|
+
...smallColumnTracks(columnTracks, maxInlineSnapshotBytes),
|
|
3742
4204
|
data: {
|
|
3743
4205
|
...(rest.treeFilehandle ? {} : { tree: data.tree }),
|
|
3744
4206
|
...(rest.msaFilehandle ? {} : { msa: data.msa }),
|