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/src/model.ts
CHANGED
|
@@ -9,7 +9,13 @@ import {
|
|
|
9
9
|
isAlive,
|
|
10
10
|
types,
|
|
11
11
|
} from '@jbrowse/mobx-state-tree'
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
autorun,
|
|
14
|
+
compareStructural,
|
|
15
|
+
computed,
|
|
16
|
+
reaction,
|
|
17
|
+
transaction,
|
|
18
|
+
} from 'mobx'
|
|
13
19
|
import {
|
|
14
20
|
generateNodeIds,
|
|
15
21
|
gffToAnnotations,
|
|
@@ -20,9 +26,10 @@ import {
|
|
|
20
26
|
parseNewick,
|
|
21
27
|
} from 'msa-parsers'
|
|
22
28
|
|
|
29
|
+
import { blockFasta } from './blockFasta.ts'
|
|
23
30
|
import { calculateBlocks } from './calculateBlocks.ts'
|
|
24
31
|
import { clustalXColumnColors } from './clustalX.ts'
|
|
25
|
-
import colorSchemes from './colorSchemes.ts'
|
|
32
|
+
import colorSchemes, { letterColorTable } from './colorSchemes.ts'
|
|
26
33
|
import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.ts'
|
|
27
34
|
import { columnStats } from './columnStats.ts'
|
|
28
35
|
import { packDomainLanes } from './components/msa/packDomainLanes.ts'
|
|
@@ -52,7 +59,7 @@ import {
|
|
|
52
59
|
defaultTreeWidth,
|
|
53
60
|
labelReferenceFontSize,
|
|
54
61
|
maxCellSize,
|
|
55
|
-
maxInlineSnapshotBytes,
|
|
62
|
+
maxInlineSnapshotBytes as defaultMaxInlineSnapshotBytes,
|
|
56
63
|
maxNeighborJoiningRows,
|
|
57
64
|
minColWidth,
|
|
58
65
|
minLetterColWidth,
|
|
@@ -68,16 +75,17 @@ import {
|
|
|
68
75
|
import { createPaletteMap } from './createPaletteMap.ts'
|
|
69
76
|
import { exportFileName } from './exportFileName.ts'
|
|
70
77
|
import { featureField, featureName } from './featureFields.ts'
|
|
78
|
+
import { featuresToAnnotations } from './featuresLayer.ts'
|
|
71
79
|
import { fetchTextWithProgress, isAbortError } from './fetchUtils.ts'
|
|
72
80
|
import { flatToTree } from './flatToTree.ts'
|
|
73
81
|
import {
|
|
74
82
|
calcDepthToLeaf,
|
|
75
83
|
clusterLayout,
|
|
76
84
|
collapse,
|
|
77
|
-
collapsedSubtreeMaxLength,
|
|
78
85
|
find,
|
|
79
86
|
forEachDescendant,
|
|
80
87
|
hierarchy,
|
|
88
|
+
layoutTree,
|
|
81
89
|
leafIndex,
|
|
82
90
|
leaves,
|
|
83
91
|
maxLength,
|
|
@@ -95,6 +103,7 @@ import { TreeModelF } from './model/treeModel.ts'
|
|
|
95
103
|
import { calculateNeighborJoiningTree } from './neighborJoining.ts'
|
|
96
104
|
import { parseAsn1 } from './parseAsn1.ts'
|
|
97
105
|
import { calculatePropertyConservation } from './propertyConservation.ts'
|
|
106
|
+
import { midpointRoot, rerootTree } from './rerootTree.ts'
|
|
98
107
|
import {
|
|
99
108
|
globalColToVisibleCol,
|
|
100
109
|
seqPosOfGlobalCol,
|
|
@@ -114,12 +123,14 @@ import {
|
|
|
114
123
|
withAlpha,
|
|
115
124
|
} from './util.ts'
|
|
116
125
|
import { saveAs } from './vendor/fileSaver.ts'
|
|
126
|
+
import { writeNewick } from './writeNewick.ts'
|
|
117
127
|
import { parseWuss } from './wuss.ts'
|
|
118
128
|
|
|
119
129
|
import type { ColumnStats } from './columnStats.ts'
|
|
120
130
|
import type { ScrollZoomAxis } from './constants.ts'
|
|
121
131
|
import type { HierarchyNode } from './hierarchy.ts'
|
|
122
132
|
import type { ExportSvgOptions } from './renderToSvg.tsx'
|
|
133
|
+
import type { TreeNode } from './rerootTree.ts'
|
|
123
134
|
import type { ScaleSpec } from './scales.ts'
|
|
124
135
|
import type {
|
|
125
136
|
Annotation,
|
|
@@ -127,25 +138,33 @@ import type {
|
|
|
127
138
|
BasicTrack,
|
|
128
139
|
Cell,
|
|
129
140
|
Clade,
|
|
141
|
+
CladeMark,
|
|
130
142
|
ResidueMappingProblem,
|
|
131
143
|
ColumnTrackSpec,
|
|
132
144
|
DomainBand,
|
|
133
145
|
Encoding,
|
|
134
146
|
EncodingChannel,
|
|
147
|
+
Feature,
|
|
135
148
|
Highlight,
|
|
136
149
|
Legend,
|
|
137
150
|
LegendEntry,
|
|
151
|
+
MsaSelection,
|
|
138
152
|
NodeWithIds,
|
|
139
|
-
NodeWithIdsAndLength,
|
|
140
153
|
Region,
|
|
141
154
|
ResidueMapping,
|
|
142
155
|
ResidueSegment,
|
|
143
156
|
ResolvedClade,
|
|
144
157
|
ResolvedEncoding,
|
|
158
|
+
ResolvedFeaturePanel,
|
|
145
159
|
ResolvedHighlight,
|
|
160
|
+
ResolvedSelection,
|
|
161
|
+
TreeOrder,
|
|
162
|
+
TreeRoot,
|
|
146
163
|
ResolvedRowPanel,
|
|
164
|
+
ResolvedStripPanel,
|
|
147
165
|
RowFeaturesSpec,
|
|
148
166
|
RowPanelSpan,
|
|
167
|
+
RowDataInput,
|
|
149
168
|
RowPanelSpec,
|
|
150
169
|
RowResidue,
|
|
151
170
|
StructureResidue,
|
|
@@ -202,14 +221,56 @@ const UNSCALED_FEATURE = '#d9d9d9'
|
|
|
202
221
|
const laneOverlap = 0.1
|
|
203
222
|
|
|
204
223
|
// a row panel's own width, or a default: the row height, which makes a strip
|
|
205
|
-
// cell square, and a gene neighborhood's width for a features panel
|
|
206
|
-
|
|
224
|
+
// cell square, and a gene neighborhood's width for a features panel. The row
|
|
225
|
+
// height is read only when used, so a panel with a width of its own does not
|
|
226
|
+
// recompute on a vertical zoom
|
|
227
|
+
function rowPanelWidth(panel: RowPanelSpec, view: { rowHeight: number }) {
|
|
207
228
|
return (
|
|
208
229
|
panel.width ??
|
|
209
|
-
(panel.kind === 'features' ? defaultFeaturePanelWidth : rowHeight)
|
|
230
|
+
(panel.kind === 'features' ? defaultFeaturePanelWidth : view.rowHeight)
|
|
210
231
|
)
|
|
211
232
|
}
|
|
212
233
|
|
|
234
|
+
// one row of the row table as the channels read it. A number or a boolean
|
|
235
|
+
// reads as its string, the way a GFF value does, and null or an object drops
|
|
236
|
+
function stringFields(row: unknown) {
|
|
237
|
+
if (typeof row !== 'object' || row === null) {
|
|
238
|
+
return undefined
|
|
239
|
+
}
|
|
240
|
+
const fields: Record<string, string> = {}
|
|
241
|
+
for (const [field, value] of Object.entries(row)) {
|
|
242
|
+
if (typeof value === 'string') {
|
|
243
|
+
fields[field] = value
|
|
244
|
+
} else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
245
|
+
fields[field] = String(value)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return fields
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// the color each row takes from a field of the row table under a scale
|
|
252
|
+
function colorByRow(
|
|
253
|
+
rowData: Record<string, Record<string, string> | undefined>,
|
|
254
|
+
field: string,
|
|
255
|
+
colorOf: (value: string) => string | undefined,
|
|
256
|
+
) {
|
|
257
|
+
const colors = new Map<string, string>()
|
|
258
|
+
for (const [name, row] of Object.entries(rowData)) {
|
|
259
|
+
const value = row?.[field]
|
|
260
|
+
const color = value === undefined ? undefined : colorOf(value)
|
|
261
|
+
if (color) {
|
|
262
|
+
colors.set(name, color)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return colors
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
type StripPanelScale = Omit<ResolvedStripPanel, 'id' | 'width' | 'offsetX'>
|
|
269
|
+
type FeaturePanelScale = Omit<
|
|
270
|
+
ResolvedFeaturePanel,
|
|
271
|
+
'id' | 'width' | 'offsetX' | 'spans'
|
|
272
|
+
>
|
|
273
|
+
|
|
213
274
|
// the fill and outline of every feature in a list: its own GFF `color=` first,
|
|
214
275
|
// then the scale an encoding resolves over one of its fields, then the
|
|
215
276
|
// accession palette
|
|
@@ -387,28 +448,47 @@ function featurePanelSpans({
|
|
|
387
448
|
)
|
|
388
449
|
}
|
|
389
450
|
|
|
390
|
-
// a scale over the values a field takes across
|
|
451
|
+
// a scale over the values a field takes across every feature loaded, so hiding
|
|
452
|
+
// a type in the filter dialog leaves the rest in their colors, and a legend of
|
|
453
|
+
// the values the features drawn carry
|
|
391
454
|
function resolveFeatureScale(
|
|
392
455
|
field: string,
|
|
393
456
|
scale: ScaleSpec | undefined,
|
|
394
457
|
annotations: Annotation[],
|
|
458
|
+
drawn: Annotation[],
|
|
395
459
|
) {
|
|
460
|
+
const valuesOf = (list: Annotation[]) =>
|
|
461
|
+
list.map(a => featureField(a, field)).filter(notEmpty)
|
|
462
|
+
const resolved = resolveScale(scale, valuesOf(annotations))
|
|
463
|
+
const shown = new Set(valuesOf(drawn))
|
|
396
464
|
return {
|
|
397
465
|
field,
|
|
398
|
-
...
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
466
|
+
...resolved,
|
|
467
|
+
legend: resolved.legend.filter(entry => shown.has(entry.id)),
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const columnTrackBytes = new WeakMap<ColumnTrackSpec, number>()
|
|
472
|
+
|
|
473
|
+
function columnTrackSize(track: ColumnTrackSpec) {
|
|
474
|
+
let bytes = columnTrackBytes.get(track)
|
|
475
|
+
if (bytes === undefined) {
|
|
476
|
+
bytes = JSON.stringify(track).length
|
|
477
|
+
columnTrackBytes.set(track, bytes)
|
|
402
478
|
}
|
|
479
|
+
return bytes
|
|
403
480
|
}
|
|
404
481
|
|
|
405
482
|
// a data track over this size stays in the live model but leaves the snapshot,
|
|
406
483
|
// the same rule DataModel applies to an inline document
|
|
407
484
|
function columnTrackSizes(tracks?: readonly ColumnTrackSpec[]) {
|
|
408
|
-
return (tracks ?? []).map(
|
|
485
|
+
return (tracks ?? []).map(columnTrackSize)
|
|
409
486
|
}
|
|
410
487
|
|
|
411
|
-
function smallColumnTracks(
|
|
488
|
+
function smallColumnTracks(
|
|
489
|
+
tracks: ColumnTrackSpec[] | undefined,
|
|
490
|
+
maxInlineSnapshotBytes: number,
|
|
491
|
+
) {
|
|
412
492
|
const sizes = columnTrackSizes(tracks)
|
|
413
493
|
const kept = tracks?.filter((_t, i) => sizes[i]! <= maxInlineSnapshotBytes)
|
|
414
494
|
return kept?.length ? { columnTracks: kept } : {}
|
|
@@ -435,12 +515,14 @@ export const preservedOnReset = new Set([
|
|
|
435
515
|
'scrollZoomAxis',
|
|
436
516
|
'bgColor',
|
|
437
517
|
'colorSchemeName',
|
|
518
|
+
'customColorScheme',
|
|
438
519
|
'showColumnStats',
|
|
439
520
|
'drawLabels',
|
|
440
521
|
'labelsAlignRight',
|
|
441
522
|
'treeAreaWidth',
|
|
442
523
|
'treeWidth',
|
|
443
524
|
'showBranchLen',
|
|
525
|
+
'treeOrder',
|
|
444
526
|
'drawTree',
|
|
445
527
|
'drawNodeBubbles',
|
|
446
528
|
'drawNodeLabels',
|
|
@@ -467,6 +549,9 @@ function trackIsOff(
|
|
|
467
549
|
return turnedOffTracks.get(id) ?? (defaultOff || defaultOffTracks.has(id))
|
|
468
550
|
}
|
|
469
551
|
|
|
552
|
+
// the marks that set tree state from a node rather than drawing
|
|
553
|
+
const seedingMarks = new Set<CladeMark>(['collapse', 'focus', 'rotate'])
|
|
554
|
+
|
|
470
555
|
// shared empty results, so observers don't see a fresh [] as a change
|
|
471
556
|
const noDomains: Annotation[] = []
|
|
472
557
|
const noClades: ResolvedClade[] = []
|
|
@@ -519,8 +604,7 @@ function cladeRows(
|
|
|
519
604
|
* Every clade that resolves, with its fill color settled, against the leaf
|
|
520
605
|
* order `rowNamesSet` gives. The tree panel resolves against the displayed
|
|
521
606
|
* rows and the overview against its own, so both take the tree they draw. A
|
|
522
|
-
* `range` record names no node, so
|
|
523
|
-
* drop it.
|
|
607
|
+
* `range` record names no node, so the seeding marks, which need one, drop it.
|
|
524
608
|
*/
|
|
525
609
|
function resolveClades(
|
|
526
610
|
clades: Clade[],
|
|
@@ -530,7 +614,7 @@ function resolveClades(
|
|
|
530
614
|
const index = leafIndex(root)
|
|
531
615
|
return clades.flatMap(clade => {
|
|
532
616
|
const resolved = cladeRows(clade, root, index, rowNamesSet)
|
|
533
|
-
const seeding =
|
|
617
|
+
const seeding = seedingMarks.has(clade.mark)
|
|
534
618
|
return resolved && !(seeding && resolved.nodeId === undefined)
|
|
535
619
|
? [
|
|
536
620
|
{
|
|
@@ -548,20 +632,50 @@ function resolveClades(
|
|
|
548
632
|
})
|
|
549
633
|
}
|
|
550
634
|
|
|
635
|
+
const childOrders: Record<
|
|
636
|
+
TreeOrder,
|
|
637
|
+
((a: HierarchyNode, b: HierarchyNode) => number) | undefined
|
|
638
|
+
> = {
|
|
639
|
+
input: undefined,
|
|
640
|
+
branchLength: (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1),
|
|
641
|
+
ladderize: (a, b) => a.value! - b.value!,
|
|
642
|
+
ladderizeReverse: (a, b) => b.value! - a.value!,
|
|
643
|
+
}
|
|
644
|
+
|
|
551
645
|
/**
|
|
552
646
|
* The tree with the display transforms applied: leaf counts summed, children
|
|
553
|
-
*
|
|
554
|
-
* `collapsed` clade folded. The overview builds the same
|
|
555
|
-
* focus, so it shows the focused subtree inside the whole.
|
|
647
|
+
* put in `order` and each `rotated` node's reversed, `showOnly` taken as the
|
|
648
|
+
* new root, and each `collapsed` clade folded. The overview builds the same
|
|
649
|
+
* tree without the focus, so it shows the focused subtree inside the whole.
|
|
556
650
|
*/
|
|
557
651
|
function buildTreeRoot(
|
|
558
652
|
tree: NodeWithIds,
|
|
559
|
-
|
|
560
|
-
|
|
653
|
+
{
|
|
654
|
+
collapsed,
|
|
655
|
+
rotated,
|
|
656
|
+
order,
|
|
657
|
+
showOnly,
|
|
658
|
+
}: {
|
|
659
|
+
collapsed: readonly string[]
|
|
660
|
+
rotated: readonly string[]
|
|
661
|
+
order: TreeOrder
|
|
662
|
+
showOnly?: string
|
|
663
|
+
},
|
|
561
664
|
) {
|
|
562
665
|
let hier = hierarchy(tree, d => d.children)
|
|
563
666
|
hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1))
|
|
564
|
-
|
|
667
|
+
const compare = childOrders[order]
|
|
668
|
+
if (compare) {
|
|
669
|
+
sort(hier, compare)
|
|
670
|
+
}
|
|
671
|
+
if (rotated.length > 0) {
|
|
672
|
+
const flip = new Set(rotated)
|
|
673
|
+
forEachDescendant(hier, n => {
|
|
674
|
+
if (flip.has(n.data.id)) {
|
|
675
|
+
n.children?.reverse()
|
|
676
|
+
}
|
|
677
|
+
})
|
|
678
|
+
}
|
|
565
679
|
|
|
566
680
|
if (showOnly) {
|
|
567
681
|
const res = find(hier, n => n.data.id === showOnly)
|
|
@@ -586,6 +700,53 @@ function buildTreeRoot(
|
|
|
586
700
|
return hier
|
|
587
701
|
}
|
|
588
702
|
|
|
703
|
+
/**
|
|
704
|
+
* The node whose branch an outgroup roots the tree on: the outgroup's most
|
|
705
|
+
* recent common ancestor, or the rest of the tips' where that ancestor is the
|
|
706
|
+
* root, since the outgroup then straddles the root and the branch above the
|
|
707
|
+
* other side is the same branch of the unrooted tree.
|
|
708
|
+
*/
|
|
709
|
+
function outgroupNode(root: HierarchyNode, outgroup: string[]) {
|
|
710
|
+
const index = leafIndex(root)
|
|
711
|
+
const node = mrca(root, outgroup, index)
|
|
712
|
+
if (node !== root) {
|
|
713
|
+
return node
|
|
714
|
+
}
|
|
715
|
+
const out = new Set(outgroup)
|
|
716
|
+
const rest = leaves(root)
|
|
717
|
+
.map(n => n.data.name)
|
|
718
|
+
.filter(name => !out.has(name))
|
|
719
|
+
const other = mrca(root, rest, index)
|
|
720
|
+
return other === root ? undefined : other
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* `tree` rerooted as `treeRoot` asks, with fresh path ids. The ids of the tree
|
|
725
|
+
* as written become names on unnamed nodes, and a rerooting would carry them
|
|
726
|
+
* along as labels, so they come off first.
|
|
727
|
+
*/
|
|
728
|
+
function rootTree(tree: NodeWithIds, treeRoot: TreeRoot) {
|
|
729
|
+
const plain = new Map<NodeWithIds, TreeNode>()
|
|
730
|
+
for (const node of preorder(tree).reverse()) {
|
|
731
|
+
plain.set(node, {
|
|
732
|
+
name: node.name === node.id ? undefined : node.name,
|
|
733
|
+
length: node.length,
|
|
734
|
+
children: node.children.map(c => plain.get(c)!),
|
|
735
|
+
})
|
|
736
|
+
}
|
|
737
|
+
const input = plain.get(tree)!
|
|
738
|
+
if (treeRoot === 'midpoint') {
|
|
739
|
+
return generateNodeIds(midpointRoot(input))
|
|
740
|
+
}
|
|
741
|
+
const target = outgroupNode(
|
|
742
|
+
hierarchy(tree, d => d.children),
|
|
743
|
+
treeRoot.outgroup,
|
|
744
|
+
)
|
|
745
|
+
return target
|
|
746
|
+
? generateNodeIds(rerootTree(input, plain.get(target.data)!))
|
|
747
|
+
: tree
|
|
748
|
+
}
|
|
749
|
+
|
|
589
750
|
// the inclusive tip indices a node covers, from the row-space extent
|
|
590
751
|
// clusterLayout writes to xMin/xMax as tip centers
|
|
591
752
|
function tipRange(node: HierarchyNode): [number, number] {
|
|
@@ -623,6 +784,20 @@ function inRanges(ranges: [number, number][] | undefined, position: number) {
|
|
|
623
784
|
return !!ranges?.some(([start, end]) => position >= start && position <= end)
|
|
624
785
|
}
|
|
625
786
|
|
|
787
|
+
// sorted integers as inclusive [first, last] runs of consecutive values
|
|
788
|
+
function consecutiveRuns(sorted: number[]) {
|
|
789
|
+
const runs: [number, number][] = []
|
|
790
|
+
for (const n of sorted) {
|
|
791
|
+
const last = runs.at(-1)
|
|
792
|
+
if (last && n <= last[1] + 1) {
|
|
793
|
+
last[1] = Math.max(last[1], n)
|
|
794
|
+
} else {
|
|
795
|
+
runs.push([n, n])
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return runs
|
|
799
|
+
}
|
|
800
|
+
|
|
626
801
|
// the value every one of a node's children reports, or undefined where one of
|
|
627
802
|
// them has no value or they disagree
|
|
628
803
|
function sharedValue(values: (string | undefined)[]) {
|
|
@@ -665,8 +840,15 @@ function preorder(tree: NodeWithIds) {
|
|
|
665
840
|
* const root = RootModel.create({})
|
|
666
841
|
* root.view.setData({ msa: '>seq1\nACGT\n>seq2\nACGT' })
|
|
667
842
|
* ```
|
|
843
|
+
*
|
|
844
|
+
* `maxInlineSnapshotBytes` is the largest pasted or local document, and the
|
|
845
|
+
* largest data track, the snapshot keeps; a larger one stays in the live model
|
|
846
|
+
* and `unshareableData` names it. The standalone app, whose links carry the
|
|
847
|
+
* snapshot in the URL fragment, raises it.
|
|
668
848
|
*/
|
|
669
|
-
function stateModelFactory(
|
|
849
|
+
function stateModelFactory({
|
|
850
|
+
maxInlineSnapshotBytes = defaultMaxInlineSnapshotBytes,
|
|
851
|
+
}: { maxInlineSnapshotBytes?: number } = {}) {
|
|
670
852
|
return types
|
|
671
853
|
.compose(
|
|
672
854
|
DialogQueueSessionMixin(),
|
|
@@ -796,6 +978,22 @@ function stateModelFactory() {
|
|
|
796
978
|
*/
|
|
797
979
|
collapsed: stripDefault(types.array(types.string), []),
|
|
798
980
|
|
|
981
|
+
/**
|
|
982
|
+
* #property
|
|
983
|
+
* tree nodes whose children draw in reverse order, which is ggtree's
|
|
984
|
+
* `rotate`: the same tree, with the clades on either side of the node
|
|
985
|
+
* swapped
|
|
986
|
+
*/
|
|
987
|
+
rotated: stripDefault(types.array(types.string), []),
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* #property
|
|
991
|
+
* where the tree is rooted: `midpoint` roots it halfway along the
|
|
992
|
+
* longest path between two tips, `{outgroup}` on the branch above
|
|
993
|
+
* the tips named, and undefined keeps the file's root
|
|
994
|
+
*/
|
|
995
|
+
treeRoot: types.frozen<TreeRoot | undefined>(),
|
|
996
|
+
|
|
799
997
|
/**
|
|
800
998
|
* #property
|
|
801
999
|
* focus on particular subtree
|
|
@@ -846,7 +1044,7 @@ function stateModelFactory() {
|
|
|
846
1044
|
* data from the loaded tree/msa/treeMetadata, generally loaded by
|
|
847
1045
|
* autorun
|
|
848
1046
|
*/
|
|
849
|
-
data: types.optional(DataModelF(), {}),
|
|
1047
|
+
data: types.optional(DataModelF(maxInlineSnapshotBytes), {}),
|
|
850
1048
|
|
|
851
1049
|
/**
|
|
852
1050
|
* #property
|
|
@@ -877,6 +1075,21 @@ function stateModelFactory() {
|
|
|
877
1075
|
* Persists in the snapshot and the URL.
|
|
878
1076
|
*/
|
|
879
1077
|
highlights: stripDefault(types.array(types.frozen<Highlight>()), []),
|
|
1078
|
+
/**
|
|
1079
|
+
* #property
|
|
1080
|
+
* features on the rows as data, `{row, start, end, name, ...}` in
|
|
1081
|
+
* residues of each row, drawn with the GFF's annotations. Persists in
|
|
1082
|
+
* the snapshot and the URL.
|
|
1083
|
+
*/
|
|
1084
|
+
features: stripDefault(types.array(types.frozen<Feature>()), []),
|
|
1085
|
+
/**
|
|
1086
|
+
* #property
|
|
1087
|
+
* the block the reader selected: `{start, end}` columns of the file,
|
|
1088
|
+
* 1-based inclusive like a column highlight, and `rows` by name, every
|
|
1089
|
+
* row where absent. Undefined until something is selected, so a view
|
|
1090
|
+
* without one adds nothing to the shared URL.
|
|
1091
|
+
*/
|
|
1092
|
+
selection: types.frozen<MsaSelection | undefined>(),
|
|
880
1093
|
/**
|
|
881
1094
|
* #property
|
|
882
1095
|
* where the view opens, in `highlights` coordinates: `{row, start,
|
|
@@ -890,7 +1103,8 @@ function stateModelFactory() {
|
|
|
890
1103
|
* clades of the tree with a mark drawn over them. `mrca` names tips
|
|
891
1104
|
* whose common ancestor is the clade, or `range` its first and last
|
|
892
1105
|
* tip in display order, and `tips` is the leaf count the producer
|
|
893
|
-
* measured. `mark` is `highlight`, `bracket`, `collapse` or
|
|
1106
|
+
* measured. `mark` is `highlight`, `bracket`, `collapse`, `focus` or
|
|
1107
|
+
* `rotate`.
|
|
894
1108
|
* See docs/layers.md
|
|
895
1109
|
*/
|
|
896
1110
|
clades: stripDefault(types.array(types.frozen<Clade>()), []),
|
|
@@ -1023,9 +1237,16 @@ function stateModelFactory() {
|
|
|
1023
1237
|
* #volatile
|
|
1024
1238
|
* non-fatal load problems: an optional layer that failed to load, an
|
|
1025
1239
|
* overlay that failed to parse. `error` replaces the view and is for the
|
|
1026
|
-
* alignment itself
|
|
1240
|
+
* alignment itself. `warnings` adds the ones derived from the data
|
|
1027
1241
|
*/
|
|
1028
|
-
|
|
1242
|
+
loadWarnings: [] as string[],
|
|
1243
|
+
|
|
1244
|
+
/**
|
|
1245
|
+
* #volatile
|
|
1246
|
+
* derived warnings the user dismissed, which stay hidden while the data
|
|
1247
|
+
* behind them holds
|
|
1248
|
+
*/
|
|
1249
|
+
dismissedWarnings: [] as string[],
|
|
1029
1250
|
|
|
1030
1251
|
/**
|
|
1031
1252
|
* #volatile
|
|
@@ -1119,18 +1340,11 @@ function stateModelFactory() {
|
|
|
1119
1340
|
* that failed to parse. A message already on the list is not added again
|
|
1120
1341
|
*/
|
|
1121
1342
|
addWarning(warning: string) {
|
|
1122
|
-
if (!self.
|
|
1123
|
-
self.
|
|
1343
|
+
if (!self.loadWarnings.includes(warning)) {
|
|
1344
|
+
self.loadWarnings = [...self.loadWarnings, warning]
|
|
1124
1345
|
}
|
|
1125
1346
|
},
|
|
1126
1347
|
|
|
1127
|
-
/**
|
|
1128
|
-
* #action
|
|
1129
|
-
*/
|
|
1130
|
-
clearWarnings() {
|
|
1131
|
-
self.warnings = []
|
|
1132
|
-
},
|
|
1133
|
-
|
|
1134
1348
|
/**
|
|
1135
1349
|
* #action
|
|
1136
1350
|
* declare that this host restores the loaded documents itself, which
|
|
@@ -1176,6 +1390,33 @@ function stateModelFactory() {
|
|
|
1176
1390
|
setHighlights(highlights: Highlight[]) {
|
|
1177
1391
|
self.highlights.replace(highlights)
|
|
1178
1392
|
},
|
|
1393
|
+
/**
|
|
1394
|
+
* #action
|
|
1395
|
+
*/
|
|
1396
|
+
setFeatures(features: Feature[]) {
|
|
1397
|
+
self.features.replace(features)
|
|
1398
|
+
},
|
|
1399
|
+
/**
|
|
1400
|
+
* #action
|
|
1401
|
+
* select a block, in `selection` coordinates. `start` and `end` may come
|
|
1402
|
+
* in either order.
|
|
1403
|
+
*/
|
|
1404
|
+
setSelection(selection?: MsaSelection) {
|
|
1405
|
+
const next = selection && {
|
|
1406
|
+
start: Math.min(selection.start, selection.end),
|
|
1407
|
+
end: Math.max(selection.start, selection.end),
|
|
1408
|
+
...(selection.rows ? { rows: selection.rows } : {}),
|
|
1409
|
+
}
|
|
1410
|
+
if (!compareStructural(next, self.selection)) {
|
|
1411
|
+
self.selection = next
|
|
1412
|
+
}
|
|
1413
|
+
},
|
|
1414
|
+
/**
|
|
1415
|
+
* #action
|
|
1416
|
+
*/
|
|
1417
|
+
clearSelection() {
|
|
1418
|
+
this.setSelection(undefined)
|
|
1419
|
+
},
|
|
1179
1420
|
|
|
1180
1421
|
/**
|
|
1181
1422
|
* #action
|
|
@@ -1253,8 +1494,9 @@ function stateModelFactory() {
|
|
|
1253
1494
|
/**
|
|
1254
1495
|
* #action
|
|
1255
1496
|
* switch to another alignment of a multi-alignment file (Stockholm).
|
|
1256
|
-
* Clears the collapsed node ids, the subtree in focus, the
|
|
1257
|
-
* and the scroll position, which all refer to the
|
|
1497
|
+
* Clears the collapsed and rotated node ids, the subtree in focus, the
|
|
1498
|
+
* reference row and the scroll position, which all refer to the
|
|
1499
|
+
* previous alignment
|
|
1258
1500
|
*/
|
|
1259
1501
|
setCurrentAlignment(n: number) {
|
|
1260
1502
|
if (n === self.currentAlignment) {
|
|
@@ -1263,6 +1505,7 @@ function stateModelFactory() {
|
|
|
1263
1505
|
transaction(() => {
|
|
1264
1506
|
self.currentAlignment = n
|
|
1265
1507
|
self.collapsed.clear()
|
|
1508
|
+
self.rotated.clear()
|
|
1266
1509
|
this.setShowOnly(undefined)
|
|
1267
1510
|
this.drawRelativeTo(undefined)
|
|
1268
1511
|
self.scrollX = 0
|
|
@@ -1282,6 +1525,42 @@ function stateModelFactory() {
|
|
|
1282
1525
|
}
|
|
1283
1526
|
},
|
|
1284
1527
|
|
|
1528
|
+
/**
|
|
1529
|
+
* #action
|
|
1530
|
+
* reverse the order of the given node's children, or restore it
|
|
1531
|
+
*/
|
|
1532
|
+
toggleRotated(node: string) {
|
|
1533
|
+
if (self.rotated.includes(node)) {
|
|
1534
|
+
self.rotated.remove(node)
|
|
1535
|
+
} else {
|
|
1536
|
+
self.rotated.push(node)
|
|
1537
|
+
}
|
|
1538
|
+
},
|
|
1539
|
+
|
|
1540
|
+
/**
|
|
1541
|
+
* #action
|
|
1542
|
+
*/
|
|
1543
|
+
clearRotated() {
|
|
1544
|
+
self.rotated.clear()
|
|
1545
|
+
},
|
|
1546
|
+
|
|
1547
|
+
/**
|
|
1548
|
+
* #action
|
|
1549
|
+
* reroot the tree. Clears `collapsed`, `rotated` and `showOnly`, whose
|
|
1550
|
+
* path-derived node ids name other nodes in the rerooted tree
|
|
1551
|
+
*/
|
|
1552
|
+
setTreeRoot(treeRoot?: TreeRoot) {
|
|
1553
|
+
if (compareStructural(self.treeRoot, treeRoot)) {
|
|
1554
|
+
return
|
|
1555
|
+
}
|
|
1556
|
+
transaction(() => {
|
|
1557
|
+
self.treeRoot = treeRoot
|
|
1558
|
+
self.collapsed.clear()
|
|
1559
|
+
self.rotated.clear()
|
|
1560
|
+
this.setShowOnly(undefined)
|
|
1561
|
+
})
|
|
1562
|
+
},
|
|
1563
|
+
|
|
1285
1564
|
/**
|
|
1286
1565
|
* #action
|
|
1287
1566
|
* show only the subtree rooted at the given node id (pass undefined to
|
|
@@ -1359,9 +1638,9 @@ function stateModelFactory() {
|
|
|
1359
1638
|
* store the GFF text in the snapshot like the alignment and tree. The
|
|
1360
1639
|
* parsed annotations are volatile and a blob filehandle is cleared once
|
|
1361
1640
|
* read, so the text is the only persisted copy. An autorun parses it
|
|
1362
|
-
* into annotations.
|
|
1641
|
+
* into annotations, and clears the ones it drew when the text goes.
|
|
1363
1642
|
*/
|
|
1364
|
-
setGFF(result
|
|
1643
|
+
setGFF(result?: string) {
|
|
1365
1644
|
self.data.setGFF(result)
|
|
1366
1645
|
},
|
|
1367
1646
|
}))
|
|
@@ -1383,11 +1662,21 @@ function stateModelFactory() {
|
|
|
1383
1662
|
get realAllowedGappyness() {
|
|
1384
1663
|
return this.hideGapsEffective ? self.allowedGappyness : 100
|
|
1385
1664
|
},
|
|
1665
|
+
/**
|
|
1666
|
+
* #getter
|
|
1667
|
+
* the annotations the overlay, the legend and the encodings draw: the
|
|
1668
|
+
* GFF's, then the `features` layer's
|
|
1669
|
+
*/
|
|
1670
|
+
get allAnnotations(): Annotation[] {
|
|
1671
|
+
return self.features.length > 0
|
|
1672
|
+
? [...self.annotations, ...featuresToAnnotations(self.features)]
|
|
1673
|
+
: self.annotations
|
|
1674
|
+
},
|
|
1386
1675
|
/**
|
|
1387
1676
|
* #getter
|
|
1388
1677
|
*/
|
|
1389
1678
|
get actuallyShowDomains() {
|
|
1390
|
-
return self.showDomains &&
|
|
1679
|
+
return self.showDomains && this.allAnnotations.length > 0
|
|
1391
1680
|
},
|
|
1392
1681
|
/**
|
|
1393
1682
|
* #getter
|
|
@@ -1421,12 +1710,26 @@ function stateModelFactory() {
|
|
|
1421
1710
|
* #getter
|
|
1422
1711
|
*/
|
|
1423
1712
|
get colorScheme() {
|
|
1713
|
+
const { customColorScheme, colorSchemeName } = self
|
|
1424
1714
|
// colorSchemeName is a free string (menus, snapshots, URL params), so a
|
|
1425
1715
|
// stale name falls back to the default
|
|
1426
|
-
return
|
|
1427
|
-
|
|
1428
|
-
colorSchemes[
|
|
1429
|
-
|
|
1716
|
+
return customColorScheme
|
|
1717
|
+
? letterColorTable(customColorScheme)
|
|
1718
|
+
: (colorSchemes[colorSchemeName] ??
|
|
1719
|
+
colorSchemes[defaultColorSchemeName]!)
|
|
1720
|
+
},
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* #getter
|
|
1724
|
+
* the scheme coloring each cell from its column's statistics, or
|
|
1725
|
+
* undefined while the cells take a fixed color per letter, from the
|
|
1726
|
+
* built-in table or from `customColorScheme`
|
|
1727
|
+
*/
|
|
1728
|
+
get dynamicColorSchemeName() {
|
|
1729
|
+
const { customColorScheme, colorSchemeName } = self
|
|
1730
|
+
return !customColorScheme && colorSchemeName.includes('dynamic')
|
|
1731
|
+
? colorSchemeName
|
|
1732
|
+
: undefined
|
|
1430
1733
|
},
|
|
1431
1734
|
|
|
1432
1735
|
/**
|
|
@@ -1449,7 +1752,7 @@ function stateModelFactory() {
|
|
|
1449
1752
|
return !!this.tree.noTree
|
|
1450
1753
|
},
|
|
1451
1754
|
get noDomains() {
|
|
1452
|
-
return self.
|
|
1755
|
+
return self.allAnnotations.length === 0
|
|
1453
1756
|
},
|
|
1454
1757
|
|
|
1455
1758
|
/**
|
|
@@ -1458,12 +1761,13 @@ function stateModelFactory() {
|
|
|
1458
1761
|
* from disk or pasted in becomes inline text, and DataModel drops an
|
|
1459
1762
|
* inline document past `maxInlineSnapshotBytes`.
|
|
1460
1763
|
*
|
|
1461
|
-
* The header lists these, and the standalone app
|
|
1462
|
-
* address bar while the list is non-empty
|
|
1463
|
-
*
|
|
1464
|
-
*
|
|
1764
|
+
* The header lists these, and the standalone app drops `#data=` from
|
|
1765
|
+
* the address bar while the list is non-empty or the encoded link would
|
|
1766
|
+
* pass its `maxLinkLength`, so a copied link never opens an empty viewer
|
|
1767
|
+
* unannounced. A document fetched from a URL never appears here, since
|
|
1768
|
+
* the snapshot keeps its filehandle.
|
|
1465
1769
|
*
|
|
1466
|
-
* Empty when `
|
|
1770
|
+
* Empty when `hostCarriesData` is true.
|
|
1467
1771
|
*/
|
|
1468
1772
|
get unshareableData(): UnshareableData[] {
|
|
1469
1773
|
if (self.hostRestoresData) {
|
|
@@ -1502,7 +1806,8 @@ function stateModelFactory() {
|
|
|
1502
1806
|
* `data.treeMetadata`, the name that travels in existing links, so the
|
|
1503
1807
|
* inline size limit and `treeMetadataFilehandle` cover it. labelWidthMap
|
|
1504
1808
|
* reads it on every layout, so a malformed user-supplied file returns {}
|
|
1505
|
-
* instead of throwing out of rendering
|
|
1809
|
+
* instead of throwing out of rendering, and every field is a string by
|
|
1810
|
+
* the time a scale sorts it.
|
|
1506
1811
|
*/
|
|
1507
1812
|
get rowData(): Record<string, Record<string, string> | undefined> {
|
|
1508
1813
|
const text = self.data.treeMetadata
|
|
@@ -1512,7 +1817,12 @@ function stateModelFactory() {
|
|
|
1512
1817
|
try {
|
|
1513
1818
|
const parsed: unknown = JSON.parse(text)
|
|
1514
1819
|
return typeof parsed === 'object' && parsed !== null
|
|
1515
|
-
? (
|
|
1820
|
+
? Object.fromEntries(
|
|
1821
|
+
Object.entries(parsed).map(([name, row]) => [
|
|
1822
|
+
name,
|
|
1823
|
+
stringFields(row),
|
|
1824
|
+
]),
|
|
1825
|
+
)
|
|
1516
1826
|
: {}
|
|
1517
1827
|
} catch (e) {
|
|
1518
1828
|
console.error('failed to parse rowData', e)
|
|
@@ -1560,8 +1870,9 @@ function stateModelFactory() {
|
|
|
1560
1870
|
|
|
1561
1871
|
/**
|
|
1562
1872
|
* #getter
|
|
1873
|
+
* the tree the file or the alignment gives, before `treeRoot`
|
|
1563
1874
|
*/
|
|
1564
|
-
get
|
|
1875
|
+
get inputTree(): NodeWithIds {
|
|
1565
1876
|
const text = self.data.tree
|
|
1566
1877
|
return text
|
|
1567
1878
|
? generateNodeIds(parseTreeText(text))
|
|
@@ -1573,6 +1884,19 @@ function stateModelFactory() {
|
|
|
1573
1884
|
})
|
|
1574
1885
|
},
|
|
1575
1886
|
|
|
1887
|
+
/**
|
|
1888
|
+
* #getter
|
|
1889
|
+
* the tree as the file or the alignment gives it, rerooted where
|
|
1890
|
+
* `treeRoot` asks. Every node id, and so `collapsed`, `rotated` and
|
|
1891
|
+
* `showOnly`, refers to this tree.
|
|
1892
|
+
*/
|
|
1893
|
+
get tree(): NodeWithIds {
|
|
1894
|
+
const tree = this.inputTree
|
|
1895
|
+
return self.treeRoot && !tree.noTree
|
|
1896
|
+
? rootTree(tree, self.treeRoot)
|
|
1897
|
+
: tree
|
|
1898
|
+
},
|
|
1899
|
+
|
|
1576
1900
|
/**
|
|
1577
1901
|
* #getter
|
|
1578
1902
|
* Returns the list of row (sequence) names in display order.
|
|
@@ -1594,8 +1918,8 @@ function stateModelFactory() {
|
|
|
1594
1918
|
* `clades` resolved to the rows each one covers. The tip names resolve
|
|
1595
1919
|
* against `tree` rather than `root`, so a clade whose ancestor the user
|
|
1596
1920
|
* collapsed keeps its rows. One leaf pass over the tree serves every
|
|
1597
|
-
* clade. A `range` record names no node, so `collapse
|
|
1598
|
-
* need one, drop it.
|
|
1921
|
+
* clade. A `range` record names no node, so `collapse`, `focus` and
|
|
1922
|
+
* `rotate`, which need one, drop it.
|
|
1599
1923
|
*/
|
|
1600
1924
|
get resolvedClades(): ResolvedClade[] {
|
|
1601
1925
|
if (self.clades.length === 0) {
|
|
@@ -1653,7 +1977,27 @@ function stateModelFactory() {
|
|
|
1653
1977
|
* #getter
|
|
1654
1978
|
*/
|
|
1655
1979
|
get root() {
|
|
1656
|
-
return buildTreeRoot(this.tree,
|
|
1980
|
+
return buildTreeRoot(this.tree, {
|
|
1981
|
+
collapsed: self.collapsed,
|
|
1982
|
+
rotated: self.rotated,
|
|
1983
|
+
order: self.treeOrder,
|
|
1984
|
+
showOnly: self.showOnly,
|
|
1985
|
+
})
|
|
1986
|
+
},
|
|
1987
|
+
|
|
1988
|
+
/**
|
|
1989
|
+
* #getter
|
|
1990
|
+
* the tree as Newick in the order and with the root it is drawn, with
|
|
1991
|
+
* every clade, collapsed or out of focus
|
|
1992
|
+
*/
|
|
1993
|
+
get treeNewick() {
|
|
1994
|
+
return writeNewick(
|
|
1995
|
+
buildTreeRoot(this.tree, {
|
|
1996
|
+
collapsed: [],
|
|
1997
|
+
rotated: self.rotated,
|
|
1998
|
+
order: self.treeOrder,
|
|
1999
|
+
}),
|
|
2000
|
+
)
|
|
1657
2001
|
},
|
|
1658
2002
|
|
|
1659
2003
|
/**
|
|
@@ -1677,7 +2021,11 @@ function stateModelFactory() {
|
|
|
1677
2021
|
if (!self.showTreeOverview) {
|
|
1678
2022
|
return undefined
|
|
1679
2023
|
}
|
|
1680
|
-
const root = buildTreeRoot(this.tree,
|
|
2024
|
+
const root = buildTreeRoot(this.tree, {
|
|
2025
|
+
collapsed: self.collapsed,
|
|
2026
|
+
rotated: self.rotated,
|
|
2027
|
+
order: self.treeOrder,
|
|
2028
|
+
})
|
|
1681
2029
|
const numTips = leaves(root).length
|
|
1682
2030
|
clusterLayout(root, numTips, 1)
|
|
1683
2031
|
const rootLen = Math.max(root.data.length || 0, 0)
|
|
@@ -1759,7 +2107,7 @@ function stateModelFactory() {
|
|
|
1759
2107
|
* alignment, the sum of each record's width
|
|
1760
2108
|
*/
|
|
1761
2109
|
get rowPanelsWidth() {
|
|
1762
|
-
return sum(self.rowPanels.map(p => rowPanelWidth(p, self
|
|
2110
|
+
return sum(self.rowPanels.map(p => rowPanelWidth(p, self)))
|
|
1763
2111
|
},
|
|
1764
2112
|
/**
|
|
1765
2113
|
* #getter
|
|
@@ -1806,9 +2154,10 @@ function stateModelFactory() {
|
|
|
1806
2154
|
return []
|
|
1807
2155
|
}
|
|
1808
2156
|
// ragged input (a3m, hand-edited fasta) can have rows shorter than the
|
|
1809
|
-
// alignment; the widest row
|
|
1810
|
-
//
|
|
1811
|
-
|
|
2157
|
+
// alignment; the widest row of the whole alignment, collapsed rows
|
|
2158
|
+
// included, defines the column count as it does for numColumns, and a
|
|
2159
|
+
// row that stops early counts as gapped for the remainder
|
|
2160
|
+
const numCols = this.MSA?.getWidth() ?? 0
|
|
1812
2161
|
const numRows = strs.length
|
|
1813
2162
|
const threshold = Math.ceil((realAllowedGappyness / 100) * numRows)
|
|
1814
2163
|
const blankCounts = new Uint32Array(numCols)
|
|
@@ -1939,7 +2288,7 @@ function stateModelFactory() {
|
|
|
1939
2288
|
* #getter
|
|
1940
2289
|
*/
|
|
1941
2290
|
get colStats() {
|
|
1942
|
-
return columnCountsFromRows(this.columns2d)
|
|
2291
|
+
return columnCountsFromRows(this.columns2d, this.numColumns)
|
|
1943
2292
|
},
|
|
1944
2293
|
|
|
1945
2294
|
/**
|
|
@@ -2038,40 +2387,42 @@ function stateModelFactory() {
|
|
|
2038
2387
|
},
|
|
2039
2388
|
/**
|
|
2040
2389
|
* #getter
|
|
2041
|
-
*
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
forEachDescendant(r, node => {
|
|
2055
|
-
if (node._children) {
|
|
2056
|
-
node.collapsedTipXFar =
|
|
2057
|
-
(node.len ?? 0) + collapsedSubtreeMaxLength(node) * k
|
|
2058
|
-
}
|
|
2390
|
+
* `root` laid out on new nodes in row and branch-length units, read in
|
|
2391
|
+
* pixels through `rowHeight` and `treeWidth`. A zoom leaves it and every
|
|
2392
|
+
* node in it as they are, and an observer reading a node's `x` or `len`
|
|
2393
|
+
* observes the size it scales by.
|
|
2394
|
+
*/
|
|
2395
|
+
get treeLayout() {
|
|
2396
|
+
return layoutTree(this.root, {
|
|
2397
|
+
get rowHeight() {
|
|
2398
|
+
return self.rowHeight
|
|
2399
|
+
},
|
|
2400
|
+
get treeWidth() {
|
|
2401
|
+
return self.treeWidth
|
|
2402
|
+
},
|
|
2059
2403
|
})
|
|
2060
|
-
|
|
2404
|
+
},
|
|
2405
|
+
|
|
2406
|
+
/**
|
|
2407
|
+
* #getter
|
|
2408
|
+
* the laid-out tree, with pixel positions on every node
|
|
2409
|
+
*/
|
|
2410
|
+
get hierarchy(): HierarchyNode<NodeWithIds> {
|
|
2411
|
+
return this.treeLayout.root
|
|
2061
2412
|
},
|
|
2062
2413
|
|
|
2063
2414
|
/**
|
|
2064
2415
|
* #getter
|
|
2065
2416
|
*/
|
|
2066
2417
|
get totalHeight() {
|
|
2067
|
-
return
|
|
2418
|
+
return this.leaves.length * self.rowHeight
|
|
2068
2419
|
},
|
|
2069
2420
|
|
|
2070
2421
|
/**
|
|
2071
2422
|
* #getter
|
|
2072
2423
|
*/
|
|
2073
|
-
get leaves() {
|
|
2074
|
-
return
|
|
2424
|
+
get leaves(): HierarchyNode<NodeWithIds>[] {
|
|
2425
|
+
return this.treeLayout.leaves
|
|
2075
2426
|
},
|
|
2076
2427
|
|
|
2077
2428
|
/**
|
|
@@ -2080,8 +2431,7 @@ function stateModelFactory() {
|
|
|
2080
2431
|
* the tree's own units
|
|
2081
2432
|
*/
|
|
2082
2433
|
get rootToTipLength() {
|
|
2083
|
-
|
|
2084
|
-
return maxLength(r) - Math.max(r.data.length || 0, 0)
|
|
2434
|
+
return this.treeLayout.rootToTipLength
|
|
2085
2435
|
},
|
|
2086
2436
|
|
|
2087
2437
|
/**
|
|
@@ -2108,7 +2458,7 @@ function stateModelFactory() {
|
|
|
2108
2458
|
* max topological depth to a tip, used to scale cladogram x-positions
|
|
2109
2459
|
*/
|
|
2110
2460
|
get maxDepthToLeaf() {
|
|
2111
|
-
return
|
|
2461
|
+
return this.treeLayout.root.depthToLeaf
|
|
2112
2462
|
},
|
|
2113
2463
|
|
|
2114
2464
|
/**
|
|
@@ -2343,13 +2693,6 @@ function stateModelFactory() {
|
|
|
2343
2693
|
return self.MSA?.secondaryStructureConsensus
|
|
2344
2694
|
},
|
|
2345
2695
|
|
|
2346
|
-
/**
|
|
2347
|
-
* #getter
|
|
2348
|
-
*/
|
|
2349
|
-
get seqConsensus() {
|
|
2350
|
-
return self.MSA?.seqConsensus
|
|
2351
|
-
},
|
|
2352
|
-
|
|
2353
2696
|
/**
|
|
2354
2697
|
* #getter
|
|
2355
2698
|
* the base pairs of the consensus secondary structure, as arcs, in
|
|
@@ -2494,11 +2837,13 @@ function stateModelFactory() {
|
|
|
2494
2837
|
|
|
2495
2838
|
/**
|
|
2496
2839
|
* #method
|
|
2497
|
-
* Return a row
|
|
2840
|
+
* Return the character a row holds at a visible column: a residue, or
|
|
2841
|
+
* the gap character (`-` or `.`) on a gap. Undefined for a row name the
|
|
2842
|
+
* alignment does not have or a column past the row's end.
|
|
2498
2843
|
*
|
|
2499
2844
|
* @param rowName - The name of the row
|
|
2500
2845
|
* @param visibleCol - The visible column index (what the user sees on screen)
|
|
2501
|
-
* @returns The
|
|
2846
|
+
* @returns The character at that position
|
|
2502
2847
|
*/
|
|
2503
2848
|
visibleColToRowLetter(rowName: string, visibleCol: number) {
|
|
2504
2849
|
return self.rowMap.get(rowName)?.[
|
|
@@ -2596,6 +2941,22 @@ function stateModelFactory() {
|
|
|
2596
2941
|
return col ?? (seqPos === 0 ? 0 : seq.length)
|
|
2597
2942
|
},
|
|
2598
2943
|
|
|
2944
|
+
/**
|
|
2945
|
+
* #method
|
|
2946
|
+
* the global column of a span's last residue, clamping a position past
|
|
2947
|
+
* the end of the row to its last residue. Undefined for a row name the
|
|
2948
|
+
* alignment does not have, or a row with no residues.
|
|
2949
|
+
*
|
|
2950
|
+
* @param rowName - The name of the row
|
|
2951
|
+
* @param seqPos - The sequence position (0-based, ungapped)
|
|
2952
|
+
*/
|
|
2953
|
+
seqEndToGlobalCol(rowName: string, seqPos: number) {
|
|
2954
|
+
const index = self.seqPosIndex(rowName)
|
|
2955
|
+
return index && index.length > 0
|
|
2956
|
+
? index[Math.min(seqPos, index.length - 1)]
|
|
2957
|
+
: undefined
|
|
2958
|
+
},
|
|
2959
|
+
|
|
2599
2960
|
/**
|
|
2600
2961
|
* #method
|
|
2601
2962
|
* Convert a sequence position (ungapped) directly to a visible column index.
|
|
@@ -2625,10 +2986,10 @@ function stateModelFactory() {
|
|
|
2625
2986
|
const start = Math.max(1, Math.floor(rawStart))
|
|
2626
2987
|
const end = Math.ceil(rawEnd)
|
|
2627
2988
|
let startGlobal = start - 1
|
|
2628
|
-
let endGlobal = end - 1
|
|
2989
|
+
let endGlobal = Math.min(end, self.MSA?.getWidth() ?? end) - 1
|
|
2629
2990
|
if (row !== undefined) {
|
|
2630
2991
|
const rowStart = this.seqPosToGlobalCol(row, start - 1)
|
|
2631
|
-
const rowEnd = this.
|
|
2992
|
+
const rowEnd = this.seqEndToGlobalCol(row, end - 1)
|
|
2632
2993
|
if (rowStart === undefined || rowEnd === undefined) {
|
|
2633
2994
|
return undefined
|
|
2634
2995
|
}
|
|
@@ -2865,7 +3226,7 @@ function stateModelFactory() {
|
|
|
2865
3226
|
// first occurrence wins; only name, description and segment start are
|
|
2866
3227
|
// read from it
|
|
2867
3228
|
const types = new Map<string, Annotation>()
|
|
2868
|
-
for (const annot of self.
|
|
3229
|
+
for (const annot of self.allAnnotations) {
|
|
2869
3230
|
if (!types.has(annot.accession)) {
|
|
2870
3231
|
types.set(annot.accession, annot)
|
|
2871
3232
|
}
|
|
@@ -2873,7 +3234,7 @@ function stateModelFactory() {
|
|
|
2873
3234
|
return types
|
|
2874
3235
|
},
|
|
2875
3236
|
get filteredAnnotations() {
|
|
2876
|
-
return self.
|
|
3237
|
+
return self.allAnnotations.filter(
|
|
2877
3238
|
r => !self.turnedOffFeatures.get(r.accession),
|
|
2878
3239
|
)
|
|
2879
3240
|
},
|
|
@@ -2922,6 +3283,28 @@ function stateModelFactory() {
|
|
|
2922
3283
|
},
|
|
2923
3284
|
}))
|
|
2924
3285
|
.views(self => ({
|
|
3286
|
+
/**
|
|
3287
|
+
* #getter
|
|
3288
|
+
*/
|
|
3289
|
+
get verticalScrollbarWidth() {
|
|
3290
|
+
return self.showVerticalScrollbar ? 20 : 0
|
|
3291
|
+
},
|
|
3292
|
+
/**
|
|
3293
|
+
* #getter
|
|
3294
|
+
* width of the alignment canvas: the msa area less the vertical
|
|
3295
|
+
* scrollbar. showHorizontalScrollbar must not read it, since that feeds
|
|
3296
|
+
* msaAreaHeight -> showVerticalScrollbar and would form a cycle
|
|
3297
|
+
*/
|
|
3298
|
+
get msaCanvasWidth() {
|
|
3299
|
+
return self.msaAreaWidth - this.verticalScrollbarWidth
|
|
3300
|
+
},
|
|
3301
|
+
/**
|
|
3302
|
+
* #getter
|
|
3303
|
+
* most-negative allowed scrollX, which keeps the last column in view
|
|
3304
|
+
*/
|
|
3305
|
+
get maxScrollX() {
|
|
3306
|
+
return Math.min(-self.totalWidth + this.msaCanvasWidth, 0)
|
|
3307
|
+
},
|
|
2925
3308
|
/**
|
|
2926
3309
|
* #getter
|
|
2927
3310
|
*/
|
|
@@ -2939,9 +3322,29 @@ function stateModelFactory() {
|
|
|
2939
3322
|
},
|
|
2940
3323
|
/**
|
|
2941
3324
|
* #getter
|
|
3325
|
+
* warnings that follow from the loaded data and clear when it changes. A
|
|
3326
|
+
* GFF whose first column names rows of some other alignment parses
|
|
3327
|
+
* without error and draws nothing
|
|
3328
|
+
*/
|
|
3329
|
+
get dataWarnings(): string[] {
|
|
3330
|
+
const { annotations, rowNamesSet } = self
|
|
3331
|
+
return self.dataInitialized &&
|
|
3332
|
+
annotations.length > 0 &&
|
|
3333
|
+
!annotations.some(a => rowNamesSet.has(a.id))
|
|
3334
|
+
? [
|
|
3335
|
+
`0 of ${annotations.length} annotations name a row in this alignment`,
|
|
3336
|
+
]
|
|
3337
|
+
: []
|
|
3338
|
+
},
|
|
3339
|
+
/**
|
|
3340
|
+
* #getter
|
|
3341
|
+
* the load warnings and the undismissed data warnings, for the header
|
|
2942
3342
|
*/
|
|
2943
|
-
get
|
|
2944
|
-
return
|
|
3343
|
+
get warnings(): string[] {
|
|
3344
|
+
return [
|
|
3345
|
+
...self.loadWarnings,
|
|
3346
|
+
...this.dataWarnings.filter(w => !self.dismissedWarnings.includes(w)),
|
|
3347
|
+
]
|
|
2945
3348
|
},
|
|
2946
3349
|
/**
|
|
2947
3350
|
* #getter
|
|
@@ -2968,6 +3371,14 @@ function stateModelFactory() {
|
|
|
2968
3371
|
self.drawMsaLetters = arg
|
|
2969
3372
|
},
|
|
2970
3373
|
|
|
3374
|
+
/**
|
|
3375
|
+
* #action
|
|
3376
|
+
*/
|
|
3377
|
+
clearWarnings() {
|
|
3378
|
+
self.loadWarnings = []
|
|
3379
|
+
self.dismissedWarnings = self.dataWarnings
|
|
3380
|
+
},
|
|
3381
|
+
|
|
2971
3382
|
/**
|
|
2972
3383
|
* #action
|
|
2973
3384
|
*/
|
|
@@ -3028,13 +3439,43 @@ function stateModelFactory() {
|
|
|
3028
3439
|
|
|
3029
3440
|
/**
|
|
3030
3441
|
* #action
|
|
3031
|
-
*
|
|
3032
|
-
* and
|
|
3442
|
+
* root the tree on the branch above the given node. The node's tips are
|
|
3443
|
+
* a split of the unrooted tree, and the side of it that is a clade in
|
|
3444
|
+
* the tree as written stands for it as an outgroup, by its first and
|
|
3445
|
+
* last tip, so the root survives the path ids changing under it
|
|
3446
|
+
*/
|
|
3447
|
+
rerootAt(nodeId: string) {
|
|
3448
|
+
const shown = find(
|
|
3449
|
+
hierarchy(self.tree, d => d.children),
|
|
3450
|
+
n => n.data.id === nodeId,
|
|
3451
|
+
)
|
|
3452
|
+
if (!shown?.parent) {
|
|
3453
|
+
return
|
|
3454
|
+
}
|
|
3455
|
+
const node = outgroupNode(
|
|
3456
|
+
hierarchy(self.inputTree, d => d.children),
|
|
3457
|
+
leaves(shown).map(n => n.data.name),
|
|
3458
|
+
)
|
|
3459
|
+
if (node) {
|
|
3460
|
+
const tips = leaves(node)
|
|
3461
|
+
self.setTreeRoot({
|
|
3462
|
+
outgroup: [
|
|
3463
|
+
...new Set([tips[0]!.data.name, tips.at(-1)!.data.name]),
|
|
3464
|
+
],
|
|
3465
|
+
})
|
|
3466
|
+
}
|
|
3467
|
+
},
|
|
3468
|
+
|
|
3469
|
+
/**
|
|
3470
|
+
* #action
|
|
3471
|
+
* swap in a different tree over the same alignment. Clears `collapsed`,
|
|
3472
|
+
* `rotated` and `showOnly`, since path-derived node ids (node-0-0-1) from the old
|
|
3033
3473
|
* tree would match unrelated nodes in the new one.
|
|
3034
3474
|
*/
|
|
3035
3475
|
replaceTree(newick: string) {
|
|
3036
3476
|
transaction(() => {
|
|
3037
3477
|
self.collapsed.clear()
|
|
3478
|
+
self.rotated.clear()
|
|
3038
3479
|
self.setShowOnly(undefined)
|
|
3039
3480
|
self.setTree(newick)
|
|
3040
3481
|
})
|
|
@@ -3048,52 +3489,53 @@ function stateModelFactory() {
|
|
|
3048
3489
|
self.setColWidth(defaultColWidth)
|
|
3049
3490
|
self.setRowHeight(defaultRowHeight)
|
|
3050
3491
|
},
|
|
3492
|
+
/**
|
|
3493
|
+
* #action
|
|
3494
|
+
* zoom by `scaleFactor` about the center of the alignment area
|
|
3495
|
+
*/
|
|
3496
|
+
zoomAtCenter(scaleFactor: number, axis: ScrollZoomAxis = 'both') {
|
|
3497
|
+
this.zoomToPos(
|
|
3498
|
+
scaleFactor,
|
|
3499
|
+
self.msaCanvasWidth / 2,
|
|
3500
|
+
self.msaAreaHeight / 2,
|
|
3501
|
+
axis,
|
|
3502
|
+
)
|
|
3503
|
+
},
|
|
3051
3504
|
/**
|
|
3052
3505
|
* #action
|
|
3053
3506
|
*/
|
|
3054
3507
|
zoomOutHorizontal() {
|
|
3055
|
-
|
|
3056
|
-
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
3508
|
+
this.zoomAtCenter(0.75, 'horizontal')
|
|
3057
3509
|
},
|
|
3058
3510
|
/**
|
|
3059
3511
|
* #action
|
|
3060
3512
|
*/
|
|
3061
3513
|
zoomInHorizontal() {
|
|
3062
|
-
|
|
3063
|
-
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
3514
|
+
this.zoomAtCenter(1.5, 'horizontal')
|
|
3064
3515
|
},
|
|
3065
3516
|
/**
|
|
3066
3517
|
* #action
|
|
3067
3518
|
*/
|
|
3068
3519
|
zoomInVertical() {
|
|
3069
|
-
|
|
3520
|
+
this.zoomAtCenter(1.5, 'vertical')
|
|
3070
3521
|
},
|
|
3071
3522
|
/**
|
|
3072
3523
|
* #action
|
|
3073
3524
|
*/
|
|
3074
3525
|
zoomOutVertical() {
|
|
3075
|
-
|
|
3076
|
-
minRowHeight,
|
|
3077
|
-
Math.floor(self.rowHeight * 0.75),
|
|
3078
|
-
)
|
|
3526
|
+
this.zoomAtCenter(0.75, 'vertical')
|
|
3079
3527
|
},
|
|
3080
3528
|
/**
|
|
3081
3529
|
* #action
|
|
3082
3530
|
*/
|
|
3083
3531
|
zoomIn() {
|
|
3084
|
-
|
|
3085
|
-
this.zoomInHorizontal()
|
|
3086
|
-
this.zoomInVertical()
|
|
3087
|
-
})
|
|
3532
|
+
this.zoomAtCenter(1.5)
|
|
3088
3533
|
},
|
|
3089
3534
|
/**
|
|
3090
3535
|
* #action
|
|
3091
3536
|
*/
|
|
3092
3537
|
zoomOut() {
|
|
3093
|
-
|
|
3094
|
-
this.zoomOutHorizontal()
|
|
3095
|
-
this.zoomOutVertical()
|
|
3096
|
-
})
|
|
3538
|
+
this.zoomAtCenter(0.75)
|
|
3097
3539
|
},
|
|
3098
3540
|
/**
|
|
3099
3541
|
* #action
|
|
@@ -3214,7 +3656,7 @@ function stateModelFactory() {
|
|
|
3214
3656
|
* replace the row table, which the model keeps as the JSON string
|
|
3215
3657
|
* `data.treeMetadata` (see docs/layers.md)
|
|
3216
3658
|
*/
|
|
3217
|
-
setRowData(rowData:
|
|
3659
|
+
setRowData(rowData: RowDataInput) {
|
|
3218
3660
|
self.data.setTreeMetadata(JSON.stringify(rowData))
|
|
3219
3661
|
},
|
|
3220
3662
|
/**
|
|
@@ -3256,25 +3698,11 @@ function stateModelFactory() {
|
|
|
3256
3698
|
},
|
|
3257
3699
|
}))
|
|
3258
3700
|
.views(self => ({
|
|
3259
|
-
/**
|
|
3260
|
-
* #getter
|
|
3261
|
-
*/
|
|
3262
|
-
get verticalScrollbarWidth() {
|
|
3263
|
-
return self.showVerticalScrollbar ? 20 : 0
|
|
3264
|
-
},
|
|
3265
|
-
/**
|
|
3266
|
-
* #getter
|
|
3267
|
-
* width of the alignment canvas: the msa area less the vertical
|
|
3268
|
-
* scrollbar. showHorizontalScrollbar must not read it, since that feeds
|
|
3269
|
-
* msaAreaHeight -> showVerticalScrollbar and would form a cycle
|
|
3270
|
-
*/
|
|
3271
|
-
get msaCanvasWidth() {
|
|
3272
|
-
return self.msaAreaWidth - this.verticalScrollbarWidth
|
|
3273
|
-
},
|
|
3274
3701
|
/**
|
|
3275
3702
|
* #method
|
|
3276
3703
|
* the cell at a visible column and row index, in the coordinates a host
|
|
3277
|
-
* writes highlights in
|
|
3704
|
+
* writes highlights in. On a gap `residue` is undefined and `letter` is
|
|
3705
|
+
* the gap character
|
|
3278
3706
|
*/
|
|
3279
3707
|
cellAt(visibleCol: number, rowIndex?: number): Cell {
|
|
3280
3708
|
const column = self.visibleColToGlobalCol(visibleCol)
|
|
@@ -3322,7 +3750,7 @@ function stateModelFactory() {
|
|
|
3322
3750
|
}
|
|
3323
3751
|
const { xStart, xEnd } = visibleColRange({
|
|
3324
3752
|
offsetX: -scrollX,
|
|
3325
|
-
blockWidth:
|
|
3753
|
+
blockWidth: self.msaCanvasWidth,
|
|
3326
3754
|
colWidth,
|
|
3327
3755
|
})
|
|
3328
3756
|
const last = Math.max(0, Math.min(xEnd, numColumns) - 1)
|
|
@@ -3485,7 +3913,7 @@ function stateModelFactory() {
|
|
|
3485
3913
|
add(field, legend)
|
|
3486
3914
|
}
|
|
3487
3915
|
}
|
|
3488
|
-
for (const { legendTitle, legend } of this.
|
|
3916
|
+
for (const { legendTitle, legend } of this.rowPanelScales) {
|
|
3489
3917
|
add(legendTitle, legend)
|
|
3490
3918
|
}
|
|
3491
3919
|
return [...byKey.values()]
|
|
@@ -3533,7 +3961,7 @@ function stateModelFactory() {
|
|
|
3533
3961
|
// column moves to the neighboring boundary. Bands with no visible
|
|
3534
3962
|
// columns, or naming a missing row, are dropped.
|
|
3535
3963
|
const start = self.seqPosToGlobalCol(name, annotation.start - 1)
|
|
3536
|
-
const end = self.
|
|
3964
|
+
const end = self.seqEndToGlobalCol(name, annotation.end - 1)
|
|
3537
3965
|
if (start === undefined || end === undefined) {
|
|
3538
3966
|
return undefined
|
|
3539
3967
|
}
|
|
@@ -3620,19 +4048,9 @@ function stateModelFactory() {
|
|
|
3620
4048
|
* on every mouse move.
|
|
3621
4049
|
*/
|
|
3622
4050
|
get highlightedColumnRuns() {
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
(a, b) => a - b,
|
|
3627
|
-
)) {
|
|
3628
|
-
const last = runs.at(-1)
|
|
3629
|
-
if (last && col === last.end + 1) {
|
|
3630
|
-
last.end = col
|
|
3631
|
-
} else {
|
|
3632
|
-
runs.push({ start: col, end: col })
|
|
3633
|
-
}
|
|
3634
|
-
}
|
|
3635
|
-
return runs
|
|
4051
|
+
return consecutiveRuns(
|
|
4052
|
+
[...(self.highlightedColumns ?? [])].sort((a, b) => a - b),
|
|
4053
|
+
).map(([start, end]) => ({ start, end }))
|
|
3636
4054
|
},
|
|
3637
4055
|
|
|
3638
4056
|
/**
|
|
@@ -3688,6 +4106,85 @@ function stateModelFactory() {
|
|
|
3688
4106
|
})
|
|
3689
4107
|
},
|
|
3690
4108
|
|
|
4109
|
+
/**
|
|
4110
|
+
* #getter
|
|
4111
|
+
* `selection` projected onto what is on screen, the way
|
|
4112
|
+
* `resolvedHighlights` projects a column span and a row set. A
|
|
4113
|
+
* selection whose columns are all hidden, or whose rows are all
|
|
4114
|
+
* collapsed away or unknown, resolves to undefined.
|
|
4115
|
+
*/
|
|
4116
|
+
get resolvedSelection(): ResolvedSelection | undefined {
|
|
4117
|
+
const { selection, rowNamesSet, numRows } = self
|
|
4118
|
+
const span =
|
|
4119
|
+
selection &&
|
|
4120
|
+
self.visibleSpan({ start: selection.start, end: selection.end })
|
|
4121
|
+
if (!selection || !span) {
|
|
4122
|
+
return undefined
|
|
4123
|
+
}
|
|
4124
|
+
const rowRuns = selection.rows
|
|
4125
|
+
? consecutiveRuns(
|
|
4126
|
+
selection.rows
|
|
4127
|
+
.map(name => rowNamesSet.get(name))
|
|
4128
|
+
.filter(notEmpty)
|
|
4129
|
+
.sort((a, b) => a - b),
|
|
4130
|
+
)
|
|
4131
|
+
: numRows > 0
|
|
4132
|
+
? [[0, numRows - 1] as [number, number]]
|
|
4133
|
+
: []
|
|
4134
|
+
return rowRuns.length > 0 ? { ...span, rowRuns } : undefined
|
|
4135
|
+
},
|
|
4136
|
+
|
|
4137
|
+
/**
|
|
4138
|
+
* #getter
|
|
4139
|
+
* the columns and rows the selection covers, for the header's readout.
|
|
4140
|
+
* Columns count the file's columns from `start` to `end`, hidden ones
|
|
4141
|
+
* included, as `selectionFasta` copies them.
|
|
4142
|
+
*/
|
|
4143
|
+
get selectionSize() {
|
|
4144
|
+
const resolved = this.resolvedSelection
|
|
4145
|
+
const span = this.selectionFileSpan
|
|
4146
|
+
return resolved && span
|
|
4147
|
+
? {
|
|
4148
|
+
columns: span.end - span.start + 1,
|
|
4149
|
+
rows: sum(resolved.rowRuns.map(([a, b]) => b - a + 1)),
|
|
4150
|
+
}
|
|
4151
|
+
: undefined
|
|
4152
|
+
},
|
|
4153
|
+
|
|
4154
|
+
/**
|
|
4155
|
+
* #getter
|
|
4156
|
+
* `selection`'s columns clamped to the file's
|
|
4157
|
+
*/
|
|
4158
|
+
get selectionFileSpan() {
|
|
4159
|
+
const { selection } = self
|
|
4160
|
+
const width = self.MSA?.getWidth() ?? 0
|
|
4161
|
+
const start = Math.max(1, Math.floor(selection?.start ?? 0))
|
|
4162
|
+
const end = Math.min(width, Math.ceil(selection?.end ?? 0))
|
|
4163
|
+
return selection && start <= end ? { start, end } : undefined
|
|
4164
|
+
},
|
|
4165
|
+
|
|
4166
|
+
/**
|
|
4167
|
+
* #getter
|
|
4168
|
+
* the selected block as FASTA: the selected rows on screen, top to
|
|
4169
|
+
* bottom, each with its letters across the file's columns from `start`
|
|
4170
|
+
* to `end`, gaps included
|
|
4171
|
+
*/
|
|
4172
|
+
get selectionFasta() {
|
|
4173
|
+
const { rowNames, rowMap } = self
|
|
4174
|
+
const resolved = this.resolvedSelection
|
|
4175
|
+
const span = this.selectionFileSpan
|
|
4176
|
+
if (!resolved || !span) {
|
|
4177
|
+
return ''
|
|
4178
|
+
}
|
|
4179
|
+
const rows = resolved.rowRuns
|
|
4180
|
+
.flatMap(([first, last]) => rowNames.slice(first, last + 1))
|
|
4181
|
+
.flatMap(name => {
|
|
4182
|
+
const seq = rowMap.get(name)
|
|
4183
|
+
return seq === undefined ? [] : [[name, seq] as const]
|
|
4184
|
+
})
|
|
4185
|
+
return blockFasta(rows, span.start, span.end)
|
|
4186
|
+
},
|
|
4187
|
+
|
|
3691
4188
|
/**
|
|
3692
4189
|
* #method
|
|
3693
4190
|
* per-column summary statistics: consensus residue and its identity
|
|
@@ -3718,67 +4215,59 @@ function stateModelFactory() {
|
|
|
3718
4215
|
/**
|
|
3719
4216
|
* #getter
|
|
3720
4217
|
* each encoding with its scale resolved against the values its field
|
|
3721
|
-
* takes: a feature channel reads them across
|
|
4218
|
+
* takes: a feature channel reads them across every feature loaded, every
|
|
3722
4219
|
* other channel across the row table. Resolved once per change of that
|
|
3723
4220
|
* table or the encodings, never per row per frame.
|
|
3724
4221
|
*/
|
|
3725
4222
|
get resolvedEncodings(): ResolvedEncoding[] {
|
|
3726
4223
|
const rows = Object.values(self.rowData)
|
|
3727
|
-
const features = self.filteredAnnotations
|
|
3728
4224
|
return self.encodings.map(encoding => ({
|
|
3729
4225
|
...encoding,
|
|
3730
|
-
...
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
4226
|
+
...(featureChannels.has(encoding.channel)
|
|
4227
|
+
? resolveFeatureScale(
|
|
4228
|
+
encoding.field,
|
|
4229
|
+
encoding.scale,
|
|
4230
|
+
self.allAnnotations,
|
|
4231
|
+
self.filteredAnnotations,
|
|
4232
|
+
)
|
|
4233
|
+
: resolveScale(
|
|
4234
|
+
encoding.scale,
|
|
4235
|
+
rows.map(row => row?.[encoding.field]).filter(notEmpty),
|
|
4236
|
+
)),
|
|
3738
4237
|
}))
|
|
3739
4238
|
},
|
|
3740
4239
|
|
|
3741
4240
|
/**
|
|
3742
4241
|
* #getter
|
|
3743
|
-
* each row panel
|
|
3744
|
-
*
|
|
3745
|
-
*
|
|
3746
|
-
* per change of that table or the panels, never per block per frame.
|
|
4242
|
+
* each row panel's scale resolved against the values its field takes,
|
|
4243
|
+
* giving the colors, labels and legend entries. Reads no cell size, so
|
|
4244
|
+
* a zoom reuses it and the legends built from it.
|
|
3747
4245
|
*/
|
|
3748
|
-
get
|
|
3749
|
-
const
|
|
4246
|
+
get rowPanelScales(): (StripPanelScale | FeaturePanelScale)[] {
|
|
4247
|
+
const { rowData } = self
|
|
3750
4248
|
const { fillPalette, featureFillEncoding, featureLabels } = this
|
|
3751
4249
|
const annotations = self.filteredAnnotations
|
|
3752
|
-
|
|
3753
|
-
return self.rowPanels.map((panel, index) => {
|
|
3754
|
-
const width = rowPanelWidth(panel, self.rowHeight)
|
|
3755
|
-
const base = { id: `rowpanel-${index}`, width, offsetX }
|
|
3756
|
-
offsetX += width
|
|
4250
|
+
return self.rowPanels.map(panel => {
|
|
3757
4251
|
if (panel.kind === 'features') {
|
|
3758
4252
|
const color = panel.encoding?.color
|
|
3759
4253
|
const encoding = color
|
|
3760
|
-
? resolveFeatureScale(
|
|
4254
|
+
? resolveFeatureScale(
|
|
4255
|
+
color.field,
|
|
4256
|
+
color.scale,
|
|
4257
|
+
self.allAnnotations,
|
|
4258
|
+
annotations,
|
|
4259
|
+
)
|
|
3761
4260
|
: featureFillEncoding
|
|
3762
4261
|
const label = panel.encoding?.label
|
|
3763
|
-
const align = panel.transform?.find(t => t.type === 'align')
|
|
3764
4262
|
return {
|
|
3765
|
-
...base,
|
|
3766
4263
|
kind: panel.kind,
|
|
3767
4264
|
x: panel.x,
|
|
3768
4265
|
header: panel.header ?? '',
|
|
3769
4266
|
field: encoding?.field,
|
|
3770
4267
|
legendTitle: encoding?.field,
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
colWidth: self.colWidth,
|
|
3775
|
-
domainBands: this.domainBands,
|
|
3776
|
-
annotationsByRow: self.annotationsByRow,
|
|
3777
|
-
shifts: align
|
|
3778
|
-
? this.featureAlignShifts.get(align.on)
|
|
3779
|
-
: undefined,
|
|
3780
|
-
}),
|
|
3781
|
-
colors: featureColorMap(annotations, encoding, fillPalette),
|
|
4268
|
+
colors: color
|
|
4269
|
+
? featureColorMap(annotations, encoding, fillPalette)
|
|
4270
|
+
: this.featureColors,
|
|
3782
4271
|
labels: label
|
|
3783
4272
|
? featureLabelMap(annotations, label)
|
|
3784
4273
|
: featureLabels,
|
|
@@ -3793,28 +4282,56 @@ function stateModelFactory() {
|
|
|
3793
4282
|
}
|
|
3794
4283
|
const { colorOf, legend } = resolveScale(
|
|
3795
4284
|
panel.scale,
|
|
3796
|
-
|
|
4285
|
+
Object.values(rowData)
|
|
4286
|
+
.map(row => row?.[panel.field])
|
|
4287
|
+
.filter(notEmpty),
|
|
3797
4288
|
)
|
|
3798
|
-
const colors = new Map<string, string>()
|
|
3799
|
-
for (const [name, row] of rows) {
|
|
3800
|
-
const value = row?.[panel.field]
|
|
3801
|
-
const color = value === undefined ? undefined : colorOf(value)
|
|
3802
|
-
if (color) {
|
|
3803
|
-
colors.set(name, color)
|
|
3804
|
-
}
|
|
3805
|
-
}
|
|
3806
4289
|
return {
|
|
3807
|
-
...base,
|
|
3808
4290
|
kind: panel.kind,
|
|
3809
4291
|
field: panel.field,
|
|
3810
4292
|
header: panel.header ?? panel.field,
|
|
3811
4293
|
legendTitle: panel.legend ?? panel.field,
|
|
3812
|
-
colors,
|
|
4294
|
+
colors: colorByRow(rowData, panel.field, colorOf),
|
|
3813
4295
|
legend,
|
|
3814
4296
|
}
|
|
3815
4297
|
})
|
|
3816
4298
|
},
|
|
3817
4299
|
|
|
4300
|
+
/**
|
|
4301
|
+
* #getter
|
|
4302
|
+
* each row panel with its scale from `rowPanelScales` and its geometry:
|
|
4303
|
+
* the pixel column it draws in and, for a features panel, the spans per
|
|
4304
|
+
* row. Resolved once per change of those inputs, never per block per
|
|
4305
|
+
* frame.
|
|
4306
|
+
*/
|
|
4307
|
+
get resolvedRowPanels(): ResolvedRowPanel[] {
|
|
4308
|
+
const scales = this.rowPanelScales
|
|
4309
|
+
let offsetX = 0
|
|
4310
|
+
return self.rowPanels.map((panel, index) => {
|
|
4311
|
+
const width = rowPanelWidth(panel, self)
|
|
4312
|
+
const base = { id: `rowpanel-${index}`, width, offsetX }
|
|
4313
|
+
offsetX += width
|
|
4314
|
+
if (panel.kind === 'features') {
|
|
4315
|
+
const align = panel.transform?.find(t => t.type === 'align')
|
|
4316
|
+
return {
|
|
4317
|
+
...base,
|
|
4318
|
+
...(scales[index] as FeaturePanelScale),
|
|
4319
|
+
spans: featurePanelSpans({
|
|
4320
|
+
panel,
|
|
4321
|
+
width,
|
|
4322
|
+
colWidth: self.colWidth,
|
|
4323
|
+
domainBands: this.domainBands,
|
|
4324
|
+
annotationsByRow: self.annotationsByRow,
|
|
4325
|
+
shifts: align
|
|
4326
|
+
? this.featureAlignShifts.get(align.on)
|
|
4327
|
+
: undefined,
|
|
4328
|
+
}),
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
return { ...base, ...(scales[index] as StripPanelScale) }
|
|
4332
|
+
})
|
|
4333
|
+
},
|
|
4334
|
+
|
|
3818
4335
|
/**
|
|
3819
4336
|
* #getter
|
|
3820
4337
|
* the shift an `align` transform gives each row, keyed by the feature
|
|
@@ -3857,19 +4374,9 @@ function stateModelFactory() {
|
|
|
3857
4374
|
const encoding = this.resolvedEncodings.find(
|
|
3858
4375
|
e => e.channel === 'tipLabel',
|
|
3859
4376
|
)
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
const colors = new Map<string, string>()
|
|
3864
|
-
for (const [name, row] of Object.entries(self.rowData)) {
|
|
3865
|
-
const value = row?.[encoding.field]
|
|
3866
|
-
const color =
|
|
3867
|
-
value === undefined ? undefined : encoding.colorOf(value)
|
|
3868
|
-
if (color) {
|
|
3869
|
-
colors.set(name, color)
|
|
3870
|
-
}
|
|
3871
|
-
}
|
|
3872
|
-
return colors
|
|
4377
|
+
return encoding
|
|
4378
|
+
? colorByRow(self.rowData, encoding.field, encoding.colorOf)
|
|
4379
|
+
: undefined
|
|
3873
4380
|
},
|
|
3874
4381
|
|
|
3875
4382
|
/**
|
|
@@ -3885,14 +4392,14 @@ function stateModelFactory() {
|
|
|
3885
4392
|
if (!encoding) {
|
|
3886
4393
|
return undefined
|
|
3887
4394
|
}
|
|
4395
|
+
const colors = colorByRow(
|
|
4396
|
+
self.rowData,
|
|
4397
|
+
encoding.field,
|
|
4398
|
+
encoding.colorOf,
|
|
4399
|
+
)
|
|
3888
4400
|
return self.rowNames.map(name => {
|
|
3889
|
-
const
|
|
3890
|
-
|
|
3891
|
-
value === undefined ? undefined : encoding.colorOf(value)
|
|
3892
|
-
if (!color) {
|
|
3893
|
-
return undefined
|
|
3894
|
-
}
|
|
3895
|
-
return withAlpha(color, rowTintAlpha)
|
|
4401
|
+
const color = colors.get(name)
|
|
4402
|
+
return color && withAlpha(color, rowTintAlpha)
|
|
3896
4403
|
})
|
|
3897
4404
|
},
|
|
3898
4405
|
|
|
@@ -3974,7 +4481,8 @@ function stateModelFactory() {
|
|
|
3974
4481
|
*/
|
|
3975
4482
|
reset() {
|
|
3976
4483
|
self.resetCount++
|
|
3977
|
-
self.
|
|
4484
|
+
self.loadWarnings = []
|
|
4485
|
+
self.dismissedWarnings = []
|
|
3978
4486
|
self.setStatus(undefined)
|
|
3979
4487
|
applySnapshot(
|
|
3980
4488
|
self,
|
|
@@ -4001,6 +4509,21 @@ function stateModelFactory() {
|
|
|
4001
4509
|
const blob = new Blob([html], { type: 'image/svg+xml' })
|
|
4002
4510
|
saveAs(blob, exportFileName(self.msaFilehandle, 'svg'))
|
|
4003
4511
|
},
|
|
4512
|
+
/**
|
|
4513
|
+
* #action
|
|
4514
|
+
* save `treeNewick` as a file
|
|
4515
|
+
*/
|
|
4516
|
+
exportNewick() {
|
|
4517
|
+
const blob = new Blob([self.treeNewick], { type: 'text/plain' })
|
|
4518
|
+
saveAs(
|
|
4519
|
+
blob,
|
|
4520
|
+
exportFileName(
|
|
4521
|
+
self.treeFilehandle ?? self.msaFilehandle,
|
|
4522
|
+
'nwk',
|
|
4523
|
+
'tree',
|
|
4524
|
+
),
|
|
4525
|
+
)
|
|
4526
|
+
},
|
|
4004
4527
|
/**
|
|
4005
4528
|
* #action
|
|
4006
4529
|
* show or hide an annotation type. Only hidden types are recorded; see
|
|
@@ -4064,6 +4587,63 @@ function stateModelFactory() {
|
|
|
4064
4587
|
)
|
|
4065
4588
|
})
|
|
4066
4589
|
},
|
|
4590
|
+
/**
|
|
4591
|
+
* #action
|
|
4592
|
+
* zoom and scroll so the selected columns fill the alignment's width,
|
|
4593
|
+
* and, where the selection names its rows, so those rows fill its height
|
|
4594
|
+
*/
|
|
4595
|
+
zoomToSelection() {
|
|
4596
|
+
const { selection, resolvedSelection } = self
|
|
4597
|
+
if (!selection || !resolvedSelection) {
|
|
4598
|
+
return
|
|
4599
|
+
}
|
|
4600
|
+
transaction(() => {
|
|
4601
|
+
this.zoomToRegion({ start: selection.start, end: selection.end })
|
|
4602
|
+
if (selection.rows && self.viewInitialized) {
|
|
4603
|
+
const { rowRuns } = resolvedSelection
|
|
4604
|
+
const first = rowRuns[0]![0]
|
|
4605
|
+
const last = rowRuns.at(-1)![1]
|
|
4606
|
+
self.rowHeight = clamp(
|
|
4607
|
+
self.msaAreaHeight / (last - first + 1),
|
|
4608
|
+
minRowHeight,
|
|
4609
|
+
maxCellSize,
|
|
4610
|
+
)
|
|
4611
|
+
self.scrollY = clamp(-first * self.rowHeight, self.maxScrollY, 0)
|
|
4612
|
+
}
|
|
4613
|
+
})
|
|
4614
|
+
},
|
|
4615
|
+
/**
|
|
4616
|
+
* #action
|
|
4617
|
+
* select the block between two cells given as visible column and row
|
|
4618
|
+
* indices, the coordinates a drag reports, clamped to the alignment. A
|
|
4619
|
+
* cell with no `row` selects the columns across every row, and so does a
|
|
4620
|
+
* block spanning every row on screen.
|
|
4621
|
+
*/
|
|
4622
|
+
selectBlock(
|
|
4623
|
+
anchor: { col: number; row?: number },
|
|
4624
|
+
head: { col: number; row?: number },
|
|
4625
|
+
) {
|
|
4626
|
+
const { numColumns, numRows, rowNames } = self
|
|
4627
|
+
if (numColumns === 0 || numRows === 0) {
|
|
4628
|
+
return
|
|
4629
|
+
}
|
|
4630
|
+
const ordered = (a: number, b: number, last: number) =>
|
|
4631
|
+
[clamp(a, 0, last), clamp(b, 0, last)].sort((x, y) => x - y) as [
|
|
4632
|
+
number,
|
|
4633
|
+
number,
|
|
4634
|
+
]
|
|
4635
|
+
const [startCol, endCol] = ordered(anchor.col, head.col, numColumns - 1)
|
|
4636
|
+
const [firstRow, lastRow] =
|
|
4637
|
+
anchor.row === undefined || head.row === undefined
|
|
4638
|
+
? [0, numRows - 1]
|
|
4639
|
+
: ordered(anchor.row, head.row, numRows - 1)
|
|
4640
|
+
const everyRow = firstRow === 0 && lastRow === numRows - 1
|
|
4641
|
+
self.setSelection({
|
|
4642
|
+
start: self.visibleColToGlobalCol(startCol) + 1,
|
|
4643
|
+
end: self.visibleColToGlobalCol(endCol) + 1,
|
|
4644
|
+
...(everyRow ? {} : { rows: rowNames.slice(firstRow, lastRow + 1) }),
|
|
4645
|
+
})
|
|
4646
|
+
},
|
|
4067
4647
|
/**
|
|
4068
4648
|
* #action
|
|
4069
4649
|
*/
|
|
@@ -4084,38 +4664,45 @@ function stateModelFactory() {
|
|
|
4084
4664
|
self.setHighlightedColumns(self.highlightColumns)
|
|
4085
4665
|
}
|
|
4086
4666
|
|
|
4087
|
-
// The `collapse` and `
|
|
4088
|
-
// the subtree in focus, which the tree,
|
|
4089
|
-
// alignment all read. The tree arrives with
|
|
4090
|
-
// and later for a filehandle, so the seeding
|
|
4091
|
-
//
|
|
4092
|
-
//
|
|
4093
|
-
//
|
|
4094
|
-
//
|
|
4095
|
-
|
|
4667
|
+
// The `collapse`, `focus` and `rotate` clade marks seed the collapsed
|
|
4668
|
+
// list, the subtree in focus and the rotated nodes, which the tree,
|
|
4669
|
+
// `hideGapsEffective` and the alignment all read. The tree arrives with
|
|
4670
|
+
// the model for inline data and later for a filehandle, so the seeding
|
|
4671
|
+
// waits for it and then runs once per data load, per change of
|
|
4672
|
+
// `clades` and per reroot, which clears the node ids the seeding
|
|
4673
|
+
// wrote: expanding a seeded clade sticks until one of those changes.
|
|
4674
|
+
// `dataInitialized` is true once the MSA alone has loaded, when the
|
|
4675
|
+
// tree is still the flat stub, so a tree filehandle holds the seeding
|
|
4676
|
+
// until its text lands.
|
|
4096
4677
|
addDisposer(
|
|
4097
4678
|
self,
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
}
|
|
4107
|
-
cladesSeeded = true
|
|
4108
|
-
for (const { mark, nodeId } of self.resolvedClades) {
|
|
4109
|
-
if (nodeId === undefined) {
|
|
4110
|
-
continue
|
|
4679
|
+
reaction(
|
|
4680
|
+
() =>
|
|
4681
|
+
self.dataInitialized && !(self.treeFilehandle && !self.data.tree)
|
|
4682
|
+
? { clades: getSnapshot(self.clades), treeRoot: self.treeRoot }
|
|
4683
|
+
: undefined,
|
|
4684
|
+
seed => {
|
|
4685
|
+
if (!seed?.clades.length) {
|
|
4686
|
+
return
|
|
4111
4687
|
}
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4688
|
+
for (const { mark, nodeId } of self.resolvedClades) {
|
|
4689
|
+
if (nodeId === undefined) {
|
|
4690
|
+
continue
|
|
4691
|
+
}
|
|
4692
|
+
if (mark === 'collapse' && !self.collapsed.includes(nodeId)) {
|
|
4693
|
+
self.toggleCollapsed(nodeId)
|
|
4694
|
+
} else if (mark === 'focus') {
|
|
4695
|
+
self.setShowOnly(nodeId)
|
|
4696
|
+
} else if (
|
|
4697
|
+
mark === 'rotate' &&
|
|
4698
|
+
!self.rotated.includes(nodeId)
|
|
4699
|
+
) {
|
|
4700
|
+
self.toggleRotated(nodeId)
|
|
4701
|
+
}
|
|
4116
4702
|
}
|
|
4117
|
-
}
|
|
4118
|
-
|
|
4703
|
+
},
|
|
4704
|
+
{ fireImmediately: true, equals: compareStructural },
|
|
4705
|
+
),
|
|
4119
4706
|
)
|
|
4120
4707
|
|
|
4121
4708
|
// zoomToRegion needs a width and resolves residues through the visible
|
|
@@ -4302,24 +4889,6 @@ function stateModelFactory() {
|
|
|
4302
4889
|
}),
|
|
4303
4890
|
)
|
|
4304
4891
|
|
|
4305
|
-
// a GFF whose first column names rows of some other alignment parses
|
|
4306
|
-
// without error and draws nothing
|
|
4307
|
-
addDisposer(
|
|
4308
|
-
self,
|
|
4309
|
-
autorun(() => {
|
|
4310
|
-
const { annotations, rowNamesSet } = self
|
|
4311
|
-
if (
|
|
4312
|
-
annotations.length > 0 &&
|
|
4313
|
-
rowNamesSet.size > 0 &&
|
|
4314
|
-
!annotations.some(a => rowNamesSet.has(a.id))
|
|
4315
|
-
) {
|
|
4316
|
-
self.addWarning(
|
|
4317
|
-
`0 of ${annotations.length} annotations name a row in this alignment`,
|
|
4318
|
-
)
|
|
4319
|
-
}
|
|
4320
|
-
}),
|
|
4321
|
-
)
|
|
4322
|
-
|
|
4323
4892
|
// gffFilehandle loads into data.gff, which the autorun above parses
|
|
4324
4893
|
loadOnFilehandleChange({
|
|
4325
4894
|
what: 'annotations',
|
|
@@ -4356,7 +4925,7 @@ function stateModelFactory() {
|
|
|
4356
4925
|
addDisposer(
|
|
4357
4926
|
self,
|
|
4358
4927
|
autorun(() => {
|
|
4359
|
-
if (self.
|
|
4928
|
+
if (self.dynamicColorSchemeName) {
|
|
4360
4929
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
4361
4930
|
self.colStats
|
|
4362
4931
|
}
|
|
@@ -4425,7 +4994,7 @@ function stateModelFactory() {
|
|
|
4425
4994
|
// stripDefault handles per-property defaults; this drops inline documents
|
|
4426
4995
|
// whose filehandle can refetch them
|
|
4427
4996
|
...rest,
|
|
4428
|
-
...smallColumnTracks(columnTracks),
|
|
4997
|
+
...smallColumnTracks(columnTracks, maxInlineSnapshotBytes),
|
|
4429
4998
|
data: {
|
|
4430
4999
|
...(rest.treeFilehandle ? {} : { tree: data.tree }),
|
|
4431
5000
|
...(rest.msaFilehandle ? {} : { msa: data.msa }),
|