react-msaview 8.1.0 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/index.js +104 -117
- package/bundle/index.js.map +4 -4
- package/dist/columnCounts.d.ts +4 -3
- package/dist/columnCounts.js +11 -6
- package/dist/columnCounts.js.map +1 -1
- package/dist/components/CopyButton.d.ts +1 -1
- package/dist/components/CopyButton.js +1 -1
- package/dist/components/CopyButton.js.map +1 -1
- package/dist/components/DragHandle.js +14 -2
- package/dist/components/DragHandle.js.map +1 -1
- package/dist/components/Loading.js +10 -11
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +12 -1
- package/dist/components/MSAViewer.js +12 -1
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/SequenceTextArea.d.ts +11 -0
- package/dist/components/SequenceTextArea.js +40 -8
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/StatusMessage.d.ts +12 -0
- package/dist/components/StatusMessage.js +43 -0
- package/dist/components/StatusMessage.js.map +1 -0
- package/dist/components/Track.js +3 -3
- package/dist/components/Track.js.map +1 -1
- package/dist/components/VerticalScrollbar.js +6 -1
- package/dist/components/VerticalScrollbar.js.map +1 -1
- package/dist/components/dialogs/ExportSVGDialog.d.ts +3 -2
- package/dist/components/dialogs/ExportSVGDialog.js +4 -2
- package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
- package/dist/components/dialogs/FeatureDialog.js +15 -10
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/getVisibleLeaves.d.ts +1 -1
- package/dist/components/header/FileMenu.js +9 -0
- package/dist/components/header/FileMenu.js.map +1 -1
- package/dist/components/header/GappynessSlider.js +15 -3
- package/dist/components/header/GappynessSlider.js.map +1 -1
- package/dist/components/header/Header.js +6 -7
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/{HeaderStatusArea.d.ts → RowSearch.d.ts} +2 -2
- package/dist/components/header/RowSearch.js +15 -0
- package/dist/components/header/RowSearch.js.map +1 -0
- package/dist/components/header/revealRow.d.ts +3 -0
- package/dist/components/header/revealRow.js +15 -0
- package/dist/components/header/revealRow.js.map +1 -0
- package/dist/components/header/settingsMenuItems.js +57 -1
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/import/ImportForm.js +10 -4
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/import/ImportFormExamples.js +19 -27
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/minimap/Minimap.js +6 -1
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/msa/AnnotationLegend.js +11 -5
- package/dist/components/msa/AnnotationLegend.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +6 -1
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +3 -7
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/msaKeys.d.ts +5 -0
- package/dist/components/msa/msaKeys.js +35 -0
- package/dist/components/msa/msaKeys.js.map +1 -0
- package/dist/components/msa/useMsaBlockMouse.d.ts +2 -4
- package/dist/components/msa/useMsaBlockMouse.js +18 -5
- package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
- package/dist/components/rowpanels/RowPanelBlock.js +10 -14
- package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
- package/dist/components/tracks/drawTracks.d.ts +1 -0
- package/dist/components/tracks/drawTracks.js +14 -2
- package/dist/components/tracks/drawTracks.js.map +1 -1
- package/dist/components/tracks/useTrackHover.d.ts +1 -5
- package/dist/components/tracks/useTrackHover.js +5 -5
- package/dist/components/tracks/useTrackHover.js.map +1 -1
- package/dist/components/tree/ScaleBarSVG.d.ts +14 -0
- package/dist/components/tree/ScaleBarSVG.js +11 -0
- package/dist/components/tree/ScaleBarSVG.js.map +1 -0
- package/dist/components/tree/TreeCanvas.js +8 -26
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +11 -20
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/{TreeBranchMenu.d.ts → TreeMenu.d.ts} +5 -4
- package/dist/components/tree/TreeMenu.js +48 -0
- package/dist/components/tree/TreeMenu.js.map +1 -0
- package/dist/components/tree/TreeRuler.js +3 -3
- package/dist/components/tree/TreeRuler.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +19 -9
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/renderTreeMouseover.d.ts +5 -0
- package/dist/components/tree/renderTreeMouseover.js +20 -0
- package/dist/components/tree/renderTreeMouseover.js.map +1 -0
- package/dist/components/tree/useTreeHover.d.ts +3 -8
- package/dist/components/tree/useTreeHover.js +8 -10
- package/dist/components/tree/useTreeHover.js.map +1 -1
- package/dist/components/useHoverAnchor.d.ts +19 -0
- package/dist/components/useHoverAnchor.js +18 -0
- package/dist/components/useHoverAnchor.js.map +1 -0
- package/dist/constants.d.ts +4 -1
- package/dist/constants.js +12 -3
- package/dist/constants.js.map +1 -1
- package/dist/expandSpec.d.ts +46 -0
- package/dist/expandSpec.js +127 -0
- package/dist/expandSpec.js.map +1 -0
- package/dist/exportFileName.d.ts +3 -3
- package/dist/exportFileName.js +4 -4
- package/dist/exportFileName.js.map +1 -1
- package/dist/hierarchy.d.ts +51 -0
- package/dist/hierarchy.js +125 -0
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/model/treeModel.d.ts +12 -0
- package/dist/model/treeModel.js +14 -1
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +188 -47
- package/dist/model.js +482 -192
- package/dist/model.js.map +1 -1
- package/dist/renderToSvg.js +2 -4
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rerootTree.d.ts +33 -0
- package/dist/rerootTree.js +144 -0
- package/dist/rerootTree.js.map +1 -0
- package/dist/svgTestUtil.d.ts +114 -20
- package/dist/types.d.ts +25 -12
- package/dist/useMsaSvgFigure.d.ts +2 -1
- package/dist/useMsaSvgFigure.js +11 -4
- package/dist/useMsaSvgFigure.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/writeNewick.d.ts +8 -0
- package/dist/writeNewick.js +34 -0
- package/dist/writeNewick.js.map +1 -0
- package/package.json +3 -3
- package/src/columnCounts.ts +14 -6
- package/src/components/CopyButton.tsx +2 -2
- package/src/components/DragHandle.tsx +21 -1
- package/src/components/Loading.tsx +14 -30
- package/src/components/MSAViewer.tsx +26 -0
- package/src/components/SequenceTextArea.tsx +54 -8
- package/src/components/StatusMessage.tsx +70 -0
- package/src/components/Track.tsx +8 -4
- package/src/components/VerticalScrollbar.tsx +7 -0
- package/src/components/dialogs/ExportSVGDialog.tsx +5 -2
- package/src/components/dialogs/FeatureDialog.tsx +59 -38
- package/src/components/header/FileMenu.tsx +9 -0
- package/src/components/header/GappynessSlider.tsx +17 -4
- package/src/components/header/Header.tsx +7 -8
- package/src/components/header/RowSearch.tsx +34 -0
- package/src/components/header/revealRow.ts +17 -0
- package/src/components/header/settingsMenuItems.ts +66 -0
- package/src/components/import/ImportForm.tsx +14 -7
- package/src/components/import/ImportFormExamples.tsx +20 -28
- package/src/components/minimap/Minimap.tsx +7 -0
- package/src/components/msa/AnnotationLegend.tsx +12 -6
- package/src/components/msa/MSACanvas.tsx +8 -0
- package/src/components/msa/MSACanvasBlock.tsx +5 -11
- package/src/components/msa/msaKeys.ts +39 -0
- package/src/components/msa/useMsaBlockMouse.ts +23 -7
- package/src/components/rowpanels/RowPanelBlock.tsx +16 -23
- package/src/components/tracks/drawTracks.ts +15 -2
- package/src/components/tracks/useTrackHover.ts +5 -5
- package/src/components/tree/ScaleBarSVG.tsx +30 -0
- package/src/components/tree/TreeCanvas.tsx +9 -40
- package/src/components/tree/TreeCanvasBlock.tsx +19 -39
- package/src/components/tree/TreeMenu.tsx +129 -0
- package/src/components/tree/TreeRuler.tsx +7 -13
- package/src/components/tree/renderTreeCanvas.ts +20 -8
- package/src/components/tree/renderTreeMouseover.ts +41 -0
- package/src/components/tree/useTreeHover.ts +8 -17
- package/src/components/useHoverAnchor.ts +31 -0
- package/src/constants.ts +14 -3
- package/src/expandSpec.ts +196 -0
- package/src/exportFileName.ts +4 -3
- package/src/hierarchy.ts +151 -4
- package/src/index.ts +11 -1
- package/src/model/treeModel.ts +22 -0
- package/src/model.ts +588 -221
- package/src/renderToSvg.tsx +6 -9
- package/src/rerootTree.ts +172 -0
- package/src/types.ts +34 -12
- package/src/useMsaSvgFigure.ts +11 -4
- package/src/version.ts +1 -1
- package/src/writeNewick.ts +38 -0
- package/dist/components/PortalTooltip.d.ts +0 -11
- package/dist/components/PortalTooltip.js +0 -29
- package/dist/components/PortalTooltip.js.map +0 -1
- package/dist/components/header/HeaderStatusArea.js +0 -21
- package/dist/components/header/HeaderStatusArea.js.map +0 -1
- package/dist/components/tree/TreeBranchMenu.js +0 -29
- package/dist/components/tree/TreeBranchMenu.js.map +0 -1
- package/dist/components/tree/TreeNodeMenu.d.ts +0 -13
- package/dist/components/tree/TreeNodeMenu.js +0 -41
- package/dist/components/tree/TreeNodeMenu.js.map +0 -1
- package/src/components/PortalTooltip.tsx +0 -47
- package/src/components/header/HeaderStatusArea.tsx +0 -32
- package/src/components/tree/TreeBranchMenu.tsx +0 -68
- package/src/components/tree/TreeNodeMenu.tsx +0 -95
package/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,
|
|
@@ -74,10 +80,10 @@ import {
|
|
|
74
80
|
calcDepthToLeaf,
|
|
75
81
|
clusterLayout,
|
|
76
82
|
collapse,
|
|
77
|
-
collapsedSubtreeMaxLength,
|
|
78
83
|
find,
|
|
79
84
|
forEachDescendant,
|
|
80
85
|
hierarchy,
|
|
86
|
+
layoutTree,
|
|
81
87
|
leafIndex,
|
|
82
88
|
leaves,
|
|
83
89
|
maxLength,
|
|
@@ -95,6 +101,7 @@ import { TreeModelF } from './model/treeModel.ts'
|
|
|
95
101
|
import { calculateNeighborJoiningTree } from './neighborJoining.ts'
|
|
96
102
|
import { parseAsn1 } from './parseAsn1.ts'
|
|
97
103
|
import { calculatePropertyConservation } from './propertyConservation.ts'
|
|
104
|
+
import { midpointRoot, rerootTree } from './rerootTree.ts'
|
|
98
105
|
import {
|
|
99
106
|
globalColToVisibleCol,
|
|
100
107
|
seqPosOfGlobalCol,
|
|
@@ -114,12 +121,14 @@ import {
|
|
|
114
121
|
withAlpha,
|
|
115
122
|
} from './util.ts'
|
|
116
123
|
import { saveAs } from './vendor/fileSaver.ts'
|
|
124
|
+
import { writeNewick } from './writeNewick.ts'
|
|
117
125
|
import { parseWuss } from './wuss.ts'
|
|
118
126
|
|
|
119
127
|
import type { ColumnStats } from './columnStats.ts'
|
|
120
128
|
import type { ScrollZoomAxis } from './constants.ts'
|
|
121
129
|
import type { HierarchyNode } from './hierarchy.ts'
|
|
122
130
|
import type { ExportSvgOptions } from './renderToSvg.tsx'
|
|
131
|
+
import type { TreeNode } from './rerootTree.ts'
|
|
123
132
|
import type { ScaleSpec } from './scales.ts'
|
|
124
133
|
import type {
|
|
125
134
|
Annotation,
|
|
@@ -127,6 +136,7 @@ import type {
|
|
|
127
136
|
BasicTrack,
|
|
128
137
|
Cell,
|
|
129
138
|
Clade,
|
|
139
|
+
CladeMark,
|
|
130
140
|
ResidueMappingProblem,
|
|
131
141
|
ColumnTrackSpec,
|
|
132
142
|
DomainBand,
|
|
@@ -136,14 +146,17 @@ import type {
|
|
|
136
146
|
Legend,
|
|
137
147
|
LegendEntry,
|
|
138
148
|
NodeWithIds,
|
|
139
|
-
NodeWithIdsAndLength,
|
|
140
149
|
Region,
|
|
141
150
|
ResidueMapping,
|
|
142
151
|
ResidueSegment,
|
|
143
152
|
ResolvedClade,
|
|
144
153
|
ResolvedEncoding,
|
|
154
|
+
ResolvedFeaturePanel,
|
|
145
155
|
ResolvedHighlight,
|
|
156
|
+
TreeOrder,
|
|
157
|
+
TreeRoot,
|
|
146
158
|
ResolvedRowPanel,
|
|
159
|
+
ResolvedStripPanel,
|
|
147
160
|
RowFeaturesSpec,
|
|
148
161
|
RowPanelSpan,
|
|
149
162
|
RowPanelSpec,
|
|
@@ -202,14 +215,39 @@ const UNSCALED_FEATURE = '#d9d9d9'
|
|
|
202
215
|
const laneOverlap = 0.1
|
|
203
216
|
|
|
204
217
|
// 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
|
-
|
|
218
|
+
// cell square, and a gene neighborhood's width for a features panel. The row
|
|
219
|
+
// height is read only when used, so a panel with a width of its own does not
|
|
220
|
+
// recompute on a vertical zoom
|
|
221
|
+
function rowPanelWidth(panel: RowPanelSpec, view: { rowHeight: number }) {
|
|
207
222
|
return (
|
|
208
223
|
panel.width ??
|
|
209
|
-
(panel.kind === 'features' ? defaultFeaturePanelWidth : rowHeight)
|
|
224
|
+
(panel.kind === 'features' ? defaultFeaturePanelWidth : view.rowHeight)
|
|
210
225
|
)
|
|
211
226
|
}
|
|
212
227
|
|
|
228
|
+
// the color each row takes from a field of the row table under a scale
|
|
229
|
+
function colorByRow(
|
|
230
|
+
rowData: Record<string, Record<string, string> | undefined>,
|
|
231
|
+
field: string,
|
|
232
|
+
colorOf: (value: string) => string | undefined,
|
|
233
|
+
) {
|
|
234
|
+
const colors = new Map<string, string>()
|
|
235
|
+
for (const [name, row] of Object.entries(rowData)) {
|
|
236
|
+
const value = row?.[field]
|
|
237
|
+
const color = value === undefined ? undefined : colorOf(value)
|
|
238
|
+
if (color) {
|
|
239
|
+
colors.set(name, color)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return colors
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
type StripPanelScale = Omit<ResolvedStripPanel, 'id' | 'width' | 'offsetX'>
|
|
246
|
+
type FeaturePanelScale = Omit<
|
|
247
|
+
ResolvedFeaturePanel,
|
|
248
|
+
'id' | 'width' | 'offsetX' | 'spans'
|
|
249
|
+
>
|
|
250
|
+
|
|
213
251
|
// the fill and outline of every feature in a list: its own GFF `color=` first,
|
|
214
252
|
// then the scale an encoding resolves over one of its fields, then the
|
|
215
253
|
// accession palette
|
|
@@ -402,10 +440,21 @@ function resolveFeatureScale(
|
|
|
402
440
|
}
|
|
403
441
|
}
|
|
404
442
|
|
|
443
|
+
const columnTrackBytes = new WeakMap<ColumnTrackSpec, number>()
|
|
444
|
+
|
|
445
|
+
function columnTrackSize(track: ColumnTrackSpec) {
|
|
446
|
+
let bytes = columnTrackBytes.get(track)
|
|
447
|
+
if (bytes === undefined) {
|
|
448
|
+
bytes = JSON.stringify(track).length
|
|
449
|
+
columnTrackBytes.set(track, bytes)
|
|
450
|
+
}
|
|
451
|
+
return bytes
|
|
452
|
+
}
|
|
453
|
+
|
|
405
454
|
// a data track over this size stays in the live model but leaves the snapshot,
|
|
406
455
|
// the same rule DataModel applies to an inline document
|
|
407
456
|
function columnTrackSizes(tracks?: readonly ColumnTrackSpec[]) {
|
|
408
|
-
return (tracks ?? []).map(
|
|
457
|
+
return (tracks ?? []).map(columnTrackSize)
|
|
409
458
|
}
|
|
410
459
|
|
|
411
460
|
function smallColumnTracks(tracks?: ColumnTrackSpec[]) {
|
|
@@ -441,6 +490,7 @@ export const preservedOnReset = new Set([
|
|
|
441
490
|
'treeAreaWidth',
|
|
442
491
|
'treeWidth',
|
|
443
492
|
'showBranchLen',
|
|
493
|
+
'treeOrder',
|
|
444
494
|
'drawTree',
|
|
445
495
|
'drawNodeBubbles',
|
|
446
496
|
'drawNodeLabels',
|
|
@@ -467,6 +517,9 @@ function trackIsOff(
|
|
|
467
517
|
return turnedOffTracks.get(id) ?? (defaultOff || defaultOffTracks.has(id))
|
|
468
518
|
}
|
|
469
519
|
|
|
520
|
+
// the marks that set tree state from a node rather than drawing
|
|
521
|
+
const seedingMarks = new Set<CladeMark>(['collapse', 'focus', 'rotate'])
|
|
522
|
+
|
|
470
523
|
// shared empty results, so observers don't see a fresh [] as a change
|
|
471
524
|
const noDomains: Annotation[] = []
|
|
472
525
|
const noClades: ResolvedClade[] = []
|
|
@@ -519,8 +572,7 @@ function cladeRows(
|
|
|
519
572
|
* Every clade that resolves, with its fill color settled, against the leaf
|
|
520
573
|
* order `rowNamesSet` gives. The tree panel resolves against the displayed
|
|
521
574
|
* 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.
|
|
575
|
+
* `range` record names no node, so the seeding marks, which need one, drop it.
|
|
524
576
|
*/
|
|
525
577
|
function resolveClades(
|
|
526
578
|
clades: Clade[],
|
|
@@ -530,7 +582,7 @@ function resolveClades(
|
|
|
530
582
|
const index = leafIndex(root)
|
|
531
583
|
return clades.flatMap(clade => {
|
|
532
584
|
const resolved = cladeRows(clade, root, index, rowNamesSet)
|
|
533
|
-
const seeding =
|
|
585
|
+
const seeding = seedingMarks.has(clade.mark)
|
|
534
586
|
return resolved && !(seeding && resolved.nodeId === undefined)
|
|
535
587
|
? [
|
|
536
588
|
{
|
|
@@ -548,20 +600,50 @@ function resolveClades(
|
|
|
548
600
|
})
|
|
549
601
|
}
|
|
550
602
|
|
|
603
|
+
const childOrders: Record<
|
|
604
|
+
TreeOrder,
|
|
605
|
+
((a: HierarchyNode, b: HierarchyNode) => number) | undefined
|
|
606
|
+
> = {
|
|
607
|
+
input: undefined,
|
|
608
|
+
branchLength: (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1),
|
|
609
|
+
ladderize: (a, b) => a.value! - b.value!,
|
|
610
|
+
ladderizeReverse: (a, b) => b.value! - a.value!,
|
|
611
|
+
}
|
|
612
|
+
|
|
551
613
|
/**
|
|
552
614
|
* 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.
|
|
615
|
+
* put in `order` and each `rotated` node's reversed, `showOnly` taken as the
|
|
616
|
+
* new root, and each `collapsed` clade folded. The overview builds the same
|
|
617
|
+
* tree without the focus, so it shows the focused subtree inside the whole.
|
|
556
618
|
*/
|
|
557
619
|
function buildTreeRoot(
|
|
558
620
|
tree: NodeWithIds,
|
|
559
|
-
|
|
560
|
-
|
|
621
|
+
{
|
|
622
|
+
collapsed,
|
|
623
|
+
rotated,
|
|
624
|
+
order,
|
|
625
|
+
showOnly,
|
|
626
|
+
}: {
|
|
627
|
+
collapsed: readonly string[]
|
|
628
|
+
rotated: readonly string[]
|
|
629
|
+
order: TreeOrder
|
|
630
|
+
showOnly?: string
|
|
631
|
+
},
|
|
561
632
|
) {
|
|
562
633
|
let hier = hierarchy(tree, d => d.children)
|
|
563
634
|
hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1))
|
|
564
|
-
|
|
635
|
+
const compare = childOrders[order]
|
|
636
|
+
if (compare) {
|
|
637
|
+
sort(hier, compare)
|
|
638
|
+
}
|
|
639
|
+
if (rotated.length > 0) {
|
|
640
|
+
const flip = new Set(rotated)
|
|
641
|
+
forEachDescendant(hier, n => {
|
|
642
|
+
if (flip.has(n.data.id)) {
|
|
643
|
+
n.children?.reverse()
|
|
644
|
+
}
|
|
645
|
+
})
|
|
646
|
+
}
|
|
565
647
|
|
|
566
648
|
if (showOnly) {
|
|
567
649
|
const res = find(hier, n => n.data.id === showOnly)
|
|
@@ -586,6 +668,53 @@ function buildTreeRoot(
|
|
|
586
668
|
return hier
|
|
587
669
|
}
|
|
588
670
|
|
|
671
|
+
/**
|
|
672
|
+
* The node whose branch an outgroup roots the tree on: the outgroup's most
|
|
673
|
+
* recent common ancestor, or the rest of the tips' where that ancestor is the
|
|
674
|
+
* root, since the outgroup then straddles the root and the branch above the
|
|
675
|
+
* other side is the same branch of the unrooted tree.
|
|
676
|
+
*/
|
|
677
|
+
function outgroupNode(root: HierarchyNode, outgroup: string[]) {
|
|
678
|
+
const index = leafIndex(root)
|
|
679
|
+
const node = mrca(root, outgroup, index)
|
|
680
|
+
if (node !== root) {
|
|
681
|
+
return node
|
|
682
|
+
}
|
|
683
|
+
const out = new Set(outgroup)
|
|
684
|
+
const rest = leaves(root)
|
|
685
|
+
.map(n => n.data.name)
|
|
686
|
+
.filter(name => !out.has(name))
|
|
687
|
+
const other = mrca(root, rest, index)
|
|
688
|
+
return other === root ? undefined : other
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* `tree` rerooted as `treeRoot` asks, with fresh path ids. The ids of the tree
|
|
693
|
+
* as written become names on unnamed nodes, and a rerooting would carry them
|
|
694
|
+
* along as labels, so they come off first.
|
|
695
|
+
*/
|
|
696
|
+
function rootTree(tree: NodeWithIds, treeRoot: TreeRoot) {
|
|
697
|
+
const plain = new Map<NodeWithIds, TreeNode>()
|
|
698
|
+
for (const node of preorder(tree).reverse()) {
|
|
699
|
+
plain.set(node, {
|
|
700
|
+
name: node.name === node.id ? undefined : node.name,
|
|
701
|
+
length: node.length,
|
|
702
|
+
children: node.children.map(c => plain.get(c)!),
|
|
703
|
+
})
|
|
704
|
+
}
|
|
705
|
+
const input = plain.get(tree)!
|
|
706
|
+
if (treeRoot === 'midpoint') {
|
|
707
|
+
return generateNodeIds(midpointRoot(input))
|
|
708
|
+
}
|
|
709
|
+
const target = outgroupNode(
|
|
710
|
+
hierarchy(tree, d => d.children),
|
|
711
|
+
treeRoot.outgroup,
|
|
712
|
+
)
|
|
713
|
+
return target
|
|
714
|
+
? generateNodeIds(rerootTree(input, plain.get(target.data)!))
|
|
715
|
+
: tree
|
|
716
|
+
}
|
|
717
|
+
|
|
589
718
|
// the inclusive tip indices a node covers, from the row-space extent
|
|
590
719
|
// clusterLayout writes to xMin/xMax as tip centers
|
|
591
720
|
function tipRange(node: HierarchyNode): [number, number] {
|
|
@@ -796,6 +925,22 @@ function stateModelFactory() {
|
|
|
796
925
|
*/
|
|
797
926
|
collapsed: stripDefault(types.array(types.string), []),
|
|
798
927
|
|
|
928
|
+
/**
|
|
929
|
+
* #property
|
|
930
|
+
* tree nodes whose children draw in reverse order, which is ggtree's
|
|
931
|
+
* `rotate`: the same tree, with the clades on either side of the node
|
|
932
|
+
* swapped
|
|
933
|
+
*/
|
|
934
|
+
rotated: stripDefault(types.array(types.string), []),
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* #property
|
|
938
|
+
* where the tree is rooted: `midpoint` roots it halfway along the
|
|
939
|
+
* longest path between two tips, `{outgroup}` on the branch above
|
|
940
|
+
* the tips named, and undefined keeps the file's root
|
|
941
|
+
*/
|
|
942
|
+
treeRoot: types.frozen<TreeRoot | undefined>(),
|
|
943
|
+
|
|
799
944
|
/**
|
|
800
945
|
* #property
|
|
801
946
|
* focus on particular subtree
|
|
@@ -877,12 +1022,21 @@ function stateModelFactory() {
|
|
|
877
1022
|
* Persists in the snapshot and the URL.
|
|
878
1023
|
*/
|
|
879
1024
|
highlights: stripDefault(types.array(types.frozen<Highlight>()), []),
|
|
1025
|
+
/**
|
|
1026
|
+
* #property
|
|
1027
|
+
* where the view opens, in `highlights` coordinates: `{row, start,
|
|
1028
|
+
* end}` zooms onto residues of that row, `{start, end}` onto alignment
|
|
1029
|
+
* columns. Applied once the alignment and any tree file have loaded,
|
|
1030
|
+
* then cleared, so a reloaded session keeps the reader's own scroll.
|
|
1031
|
+
*/
|
|
1032
|
+
region: types.frozen<Region | undefined>(),
|
|
880
1033
|
/**
|
|
881
1034
|
* #property
|
|
882
1035
|
* clades of the tree with a mark drawn over them. `mrca` names tips
|
|
883
1036
|
* whose common ancestor is the clade, or `range` its first and last
|
|
884
1037
|
* tip in display order, and `tips` is the leaf count the producer
|
|
885
|
-
* measured. `mark` is `highlight`, `bracket`, `collapse` or
|
|
1038
|
+
* measured. `mark` is `highlight`, `bracket`, `collapse`, `focus` or
|
|
1039
|
+
* `rotate`.
|
|
886
1040
|
* See docs/layers.md
|
|
887
1041
|
*/
|
|
888
1042
|
clades: stripDefault(types.array(types.frozen<Clade>()), []),
|
|
@@ -1015,9 +1169,16 @@ function stateModelFactory() {
|
|
|
1015
1169
|
* #volatile
|
|
1016
1170
|
* non-fatal load problems: an optional layer that failed to load, an
|
|
1017
1171
|
* overlay that failed to parse. `error` replaces the view and is for the
|
|
1018
|
-
* alignment itself
|
|
1172
|
+
* alignment itself. `warnings` adds the ones derived from the data
|
|
1173
|
+
*/
|
|
1174
|
+
loadWarnings: [] as string[],
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* #volatile
|
|
1178
|
+
* derived warnings the user dismissed, which stay hidden while the data
|
|
1179
|
+
* behind them holds
|
|
1019
1180
|
*/
|
|
1020
|
-
|
|
1181
|
+
dismissedWarnings: [] as string[],
|
|
1021
1182
|
|
|
1022
1183
|
/**
|
|
1023
1184
|
* #volatile
|
|
@@ -1111,18 +1272,11 @@ function stateModelFactory() {
|
|
|
1111
1272
|
* that failed to parse. A message already on the list is not added again
|
|
1112
1273
|
*/
|
|
1113
1274
|
addWarning(warning: string) {
|
|
1114
|
-
if (!self.
|
|
1115
|
-
self.
|
|
1275
|
+
if (!self.loadWarnings.includes(warning)) {
|
|
1276
|
+
self.loadWarnings = [...self.loadWarnings, warning]
|
|
1116
1277
|
}
|
|
1117
1278
|
},
|
|
1118
1279
|
|
|
1119
|
-
/**
|
|
1120
|
-
* #action
|
|
1121
|
-
*/
|
|
1122
|
-
clearWarnings() {
|
|
1123
|
-
self.warnings = []
|
|
1124
|
-
},
|
|
1125
|
-
|
|
1126
1280
|
/**
|
|
1127
1281
|
* #action
|
|
1128
1282
|
* declare that this host restores the loaded documents itself, which
|
|
@@ -1156,6 +1310,12 @@ function stateModelFactory() {
|
|
|
1156
1310
|
setHighlightedColumns(columns?: number[]) {
|
|
1157
1311
|
self.highlightedColumns = columns
|
|
1158
1312
|
},
|
|
1313
|
+
/**
|
|
1314
|
+
* #action
|
|
1315
|
+
*/
|
|
1316
|
+
setRegion(region?: Region) {
|
|
1317
|
+
self.region = region
|
|
1318
|
+
},
|
|
1159
1319
|
/**
|
|
1160
1320
|
* #action
|
|
1161
1321
|
*/
|
|
@@ -1239,8 +1399,9 @@ function stateModelFactory() {
|
|
|
1239
1399
|
/**
|
|
1240
1400
|
* #action
|
|
1241
1401
|
* switch to another alignment of a multi-alignment file (Stockholm).
|
|
1242
|
-
* Clears the collapsed node ids, the subtree in focus, the
|
|
1243
|
-
* and the scroll position, which all refer to the
|
|
1402
|
+
* Clears the collapsed and rotated node ids, the subtree in focus, the
|
|
1403
|
+
* reference row and the scroll position, which all refer to the
|
|
1404
|
+
* previous alignment
|
|
1244
1405
|
*/
|
|
1245
1406
|
setCurrentAlignment(n: number) {
|
|
1246
1407
|
if (n === self.currentAlignment) {
|
|
@@ -1249,6 +1410,7 @@ function stateModelFactory() {
|
|
|
1249
1410
|
transaction(() => {
|
|
1250
1411
|
self.currentAlignment = n
|
|
1251
1412
|
self.collapsed.clear()
|
|
1413
|
+
self.rotated.clear()
|
|
1252
1414
|
this.setShowOnly(undefined)
|
|
1253
1415
|
this.drawRelativeTo(undefined)
|
|
1254
1416
|
self.scrollX = 0
|
|
@@ -1268,6 +1430,42 @@ function stateModelFactory() {
|
|
|
1268
1430
|
}
|
|
1269
1431
|
},
|
|
1270
1432
|
|
|
1433
|
+
/**
|
|
1434
|
+
* #action
|
|
1435
|
+
* reverse the order of the given node's children, or restore it
|
|
1436
|
+
*/
|
|
1437
|
+
toggleRotated(node: string) {
|
|
1438
|
+
if (self.rotated.includes(node)) {
|
|
1439
|
+
self.rotated.remove(node)
|
|
1440
|
+
} else {
|
|
1441
|
+
self.rotated.push(node)
|
|
1442
|
+
}
|
|
1443
|
+
},
|
|
1444
|
+
|
|
1445
|
+
/**
|
|
1446
|
+
* #action
|
|
1447
|
+
*/
|
|
1448
|
+
clearRotated() {
|
|
1449
|
+
self.rotated.clear()
|
|
1450
|
+
},
|
|
1451
|
+
|
|
1452
|
+
/**
|
|
1453
|
+
* #action
|
|
1454
|
+
* reroot the tree. Clears `collapsed`, `rotated` and `showOnly`, whose
|
|
1455
|
+
* path-derived node ids name other nodes in the rerooted tree
|
|
1456
|
+
*/
|
|
1457
|
+
setTreeRoot(treeRoot?: TreeRoot) {
|
|
1458
|
+
if (compareStructural(self.treeRoot, treeRoot)) {
|
|
1459
|
+
return
|
|
1460
|
+
}
|
|
1461
|
+
transaction(() => {
|
|
1462
|
+
self.treeRoot = treeRoot
|
|
1463
|
+
self.collapsed.clear()
|
|
1464
|
+
self.rotated.clear()
|
|
1465
|
+
this.setShowOnly(undefined)
|
|
1466
|
+
})
|
|
1467
|
+
},
|
|
1468
|
+
|
|
1271
1469
|
/**
|
|
1272
1470
|
* #action
|
|
1273
1471
|
* show only the subtree rooted at the given node id (pass undefined to
|
|
@@ -1444,12 +1642,13 @@ function stateModelFactory() {
|
|
|
1444
1642
|
* from disk or pasted in becomes inline text, and DataModel drops an
|
|
1445
1643
|
* inline document past `maxInlineSnapshotBytes`.
|
|
1446
1644
|
*
|
|
1447
|
-
* The header lists these, and the standalone app
|
|
1448
|
-
* address bar while the list is non-empty
|
|
1449
|
-
*
|
|
1450
|
-
*
|
|
1645
|
+
* The header lists these, and the standalone app drops `?data=` from
|
|
1646
|
+
* the address bar while the list is non-empty or the encoded link would
|
|
1647
|
+
* pass 8,000 characters, so a copied link never opens an empty viewer
|
|
1648
|
+
* unannounced. A document fetched from a URL never appears here, since
|
|
1649
|
+
* the snapshot keeps its filehandle.
|
|
1451
1650
|
*
|
|
1452
|
-
* Empty when `
|
|
1651
|
+
* Empty when `hostCarriesData` is true.
|
|
1453
1652
|
*/
|
|
1454
1653
|
get unshareableData(): UnshareableData[] {
|
|
1455
1654
|
if (self.hostRestoresData) {
|
|
@@ -1546,8 +1745,9 @@ function stateModelFactory() {
|
|
|
1546
1745
|
|
|
1547
1746
|
/**
|
|
1548
1747
|
* #getter
|
|
1748
|
+
* the tree the file or the alignment gives, before `treeRoot`
|
|
1549
1749
|
*/
|
|
1550
|
-
get
|
|
1750
|
+
get inputTree(): NodeWithIds {
|
|
1551
1751
|
const text = self.data.tree
|
|
1552
1752
|
return text
|
|
1553
1753
|
? generateNodeIds(parseTreeText(text))
|
|
@@ -1559,6 +1759,19 @@ function stateModelFactory() {
|
|
|
1559
1759
|
})
|
|
1560
1760
|
},
|
|
1561
1761
|
|
|
1762
|
+
/**
|
|
1763
|
+
* #getter
|
|
1764
|
+
* the tree as the file or the alignment gives it, rerooted where
|
|
1765
|
+
* `treeRoot` asks. Every node id, and so `collapsed`, `rotated` and
|
|
1766
|
+
* `showOnly`, refers to this tree.
|
|
1767
|
+
*/
|
|
1768
|
+
get tree(): NodeWithIds {
|
|
1769
|
+
const tree = this.inputTree
|
|
1770
|
+
return self.treeRoot && !tree.noTree
|
|
1771
|
+
? rootTree(tree, self.treeRoot)
|
|
1772
|
+
: tree
|
|
1773
|
+
},
|
|
1774
|
+
|
|
1562
1775
|
/**
|
|
1563
1776
|
* #getter
|
|
1564
1777
|
* Returns the list of row (sequence) names in display order.
|
|
@@ -1580,8 +1793,8 @@ function stateModelFactory() {
|
|
|
1580
1793
|
* `clades` resolved to the rows each one covers. The tip names resolve
|
|
1581
1794
|
* against `tree` rather than `root`, so a clade whose ancestor the user
|
|
1582
1795
|
* collapsed keeps its rows. One leaf pass over the tree serves every
|
|
1583
|
-
* clade. A `range` record names no node, so `collapse
|
|
1584
|
-
* need one, drop it.
|
|
1796
|
+
* clade. A `range` record names no node, so `collapse`, `focus` and
|
|
1797
|
+
* `rotate`, which need one, drop it.
|
|
1585
1798
|
*/
|
|
1586
1799
|
get resolvedClades(): ResolvedClade[] {
|
|
1587
1800
|
if (self.clades.length === 0) {
|
|
@@ -1639,7 +1852,27 @@ function stateModelFactory() {
|
|
|
1639
1852
|
* #getter
|
|
1640
1853
|
*/
|
|
1641
1854
|
get root() {
|
|
1642
|
-
return buildTreeRoot(this.tree,
|
|
1855
|
+
return buildTreeRoot(this.tree, {
|
|
1856
|
+
collapsed: self.collapsed,
|
|
1857
|
+
rotated: self.rotated,
|
|
1858
|
+
order: self.treeOrder,
|
|
1859
|
+
showOnly: self.showOnly,
|
|
1860
|
+
})
|
|
1861
|
+
},
|
|
1862
|
+
|
|
1863
|
+
/**
|
|
1864
|
+
* #getter
|
|
1865
|
+
* the tree as Newick in the order and with the root it is drawn, with
|
|
1866
|
+
* every clade, collapsed or out of focus
|
|
1867
|
+
*/
|
|
1868
|
+
get treeNewick() {
|
|
1869
|
+
return writeNewick(
|
|
1870
|
+
buildTreeRoot(this.tree, {
|
|
1871
|
+
collapsed: [],
|
|
1872
|
+
rotated: self.rotated,
|
|
1873
|
+
order: self.treeOrder,
|
|
1874
|
+
}),
|
|
1875
|
+
)
|
|
1643
1876
|
},
|
|
1644
1877
|
|
|
1645
1878
|
/**
|
|
@@ -1663,7 +1896,11 @@ function stateModelFactory() {
|
|
|
1663
1896
|
if (!self.showTreeOverview) {
|
|
1664
1897
|
return undefined
|
|
1665
1898
|
}
|
|
1666
|
-
const root = buildTreeRoot(this.tree,
|
|
1899
|
+
const root = buildTreeRoot(this.tree, {
|
|
1900
|
+
collapsed: self.collapsed,
|
|
1901
|
+
rotated: self.rotated,
|
|
1902
|
+
order: self.treeOrder,
|
|
1903
|
+
})
|
|
1667
1904
|
const numTips = leaves(root).length
|
|
1668
1905
|
clusterLayout(root, numTips, 1)
|
|
1669
1906
|
const rootLen = Math.max(root.data.length || 0, 0)
|
|
@@ -1745,7 +1982,7 @@ function stateModelFactory() {
|
|
|
1745
1982
|
* alignment, the sum of each record's width
|
|
1746
1983
|
*/
|
|
1747
1984
|
get rowPanelsWidth() {
|
|
1748
|
-
return sum(self.rowPanels.map(p => rowPanelWidth(p, self
|
|
1985
|
+
return sum(self.rowPanels.map(p => rowPanelWidth(p, self)))
|
|
1749
1986
|
},
|
|
1750
1987
|
/**
|
|
1751
1988
|
* #getter
|
|
@@ -1792,9 +2029,10 @@ function stateModelFactory() {
|
|
|
1792
2029
|
return []
|
|
1793
2030
|
}
|
|
1794
2031
|
// ragged input (a3m, hand-edited fasta) can have rows shorter than the
|
|
1795
|
-
// alignment; the widest row
|
|
1796
|
-
//
|
|
1797
|
-
|
|
2032
|
+
// alignment; the widest row of the whole alignment, collapsed rows
|
|
2033
|
+
// included, defines the column count as it does for numColumns, and a
|
|
2034
|
+
// row that stops early counts as gapped for the remainder
|
|
2035
|
+
const numCols = this.MSA?.getWidth() ?? 0
|
|
1798
2036
|
const numRows = strs.length
|
|
1799
2037
|
const threshold = Math.ceil((realAllowedGappyness / 100) * numRows)
|
|
1800
2038
|
const blankCounts = new Uint32Array(numCols)
|
|
@@ -1925,7 +2163,7 @@ function stateModelFactory() {
|
|
|
1925
2163
|
* #getter
|
|
1926
2164
|
*/
|
|
1927
2165
|
get colStats() {
|
|
1928
|
-
return columnCountsFromRows(this.columns2d)
|
|
2166
|
+
return columnCountsFromRows(this.columns2d, this.numColumns)
|
|
1929
2167
|
},
|
|
1930
2168
|
|
|
1931
2169
|
/**
|
|
@@ -2024,40 +2262,42 @@ function stateModelFactory() {
|
|
|
2024
2262
|
},
|
|
2025
2263
|
/**
|
|
2026
2264
|
* #getter
|
|
2027
|
-
*
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
forEachDescendant(r, node => {
|
|
2041
|
-
if (node._children) {
|
|
2042
|
-
node.collapsedTipXFar =
|
|
2043
|
-
(node.len ?? 0) + collapsedSubtreeMaxLength(node) * k
|
|
2044
|
-
}
|
|
2265
|
+
* `root` laid out on new nodes in row and branch-length units, read in
|
|
2266
|
+
* pixels through `rowHeight` and `treeWidth`. A zoom leaves it and every
|
|
2267
|
+
* node in it as they are, and an observer reading a node's `x` or `len`
|
|
2268
|
+
* observes the size it scales by.
|
|
2269
|
+
*/
|
|
2270
|
+
get treeLayout() {
|
|
2271
|
+
return layoutTree(this.root, {
|
|
2272
|
+
get rowHeight() {
|
|
2273
|
+
return self.rowHeight
|
|
2274
|
+
},
|
|
2275
|
+
get treeWidth() {
|
|
2276
|
+
return self.treeWidth
|
|
2277
|
+
},
|
|
2045
2278
|
})
|
|
2046
|
-
|
|
2279
|
+
},
|
|
2280
|
+
|
|
2281
|
+
/**
|
|
2282
|
+
* #getter
|
|
2283
|
+
* the laid-out tree, with pixel positions on every node
|
|
2284
|
+
*/
|
|
2285
|
+
get hierarchy(): HierarchyNode<NodeWithIds> {
|
|
2286
|
+
return this.treeLayout.root
|
|
2047
2287
|
},
|
|
2048
2288
|
|
|
2049
2289
|
/**
|
|
2050
2290
|
* #getter
|
|
2051
2291
|
*/
|
|
2052
2292
|
get totalHeight() {
|
|
2053
|
-
return
|
|
2293
|
+
return this.leaves.length * self.rowHeight
|
|
2054
2294
|
},
|
|
2055
2295
|
|
|
2056
2296
|
/**
|
|
2057
2297
|
* #getter
|
|
2058
2298
|
*/
|
|
2059
|
-
get leaves() {
|
|
2060
|
-
return
|
|
2299
|
+
get leaves(): HierarchyNode<NodeWithIds>[] {
|
|
2300
|
+
return this.treeLayout.leaves
|
|
2061
2301
|
},
|
|
2062
2302
|
|
|
2063
2303
|
/**
|
|
@@ -2066,8 +2306,7 @@ function stateModelFactory() {
|
|
|
2066
2306
|
* the tree's own units
|
|
2067
2307
|
*/
|
|
2068
2308
|
get rootToTipLength() {
|
|
2069
|
-
|
|
2070
|
-
return maxLength(r) - Math.max(r.data.length || 0, 0)
|
|
2309
|
+
return this.treeLayout.rootToTipLength
|
|
2071
2310
|
},
|
|
2072
2311
|
|
|
2073
2312
|
/**
|
|
@@ -2094,7 +2333,7 @@ function stateModelFactory() {
|
|
|
2094
2333
|
* max topological depth to a tip, used to scale cladogram x-positions
|
|
2095
2334
|
*/
|
|
2096
2335
|
get maxDepthToLeaf() {
|
|
2097
|
-
return
|
|
2336
|
+
return this.treeLayout.root.depthToLeaf
|
|
2098
2337
|
},
|
|
2099
2338
|
|
|
2100
2339
|
/**
|
|
@@ -2329,13 +2568,6 @@ function stateModelFactory() {
|
|
|
2329
2568
|
return self.MSA?.secondaryStructureConsensus
|
|
2330
2569
|
},
|
|
2331
2570
|
|
|
2332
|
-
/**
|
|
2333
|
-
* #getter
|
|
2334
|
-
*/
|
|
2335
|
-
get seqConsensus() {
|
|
2336
|
-
return self.MSA?.seqConsensus
|
|
2337
|
-
},
|
|
2338
|
-
|
|
2339
2571
|
/**
|
|
2340
2572
|
* #getter
|
|
2341
2573
|
* the base pairs of the consensus secondary structure, as arcs, in
|
|
@@ -2480,11 +2712,13 @@ function stateModelFactory() {
|
|
|
2480
2712
|
|
|
2481
2713
|
/**
|
|
2482
2714
|
* #method
|
|
2483
|
-
* Return a row
|
|
2715
|
+
* Return the character a row holds at a visible column: a residue, or
|
|
2716
|
+
* the gap character (`-` or `.`) on a gap. Undefined for a row name the
|
|
2717
|
+
* alignment does not have or a column past the row's end.
|
|
2484
2718
|
*
|
|
2485
2719
|
* @param rowName - The name of the row
|
|
2486
2720
|
* @param visibleCol - The visible column index (what the user sees on screen)
|
|
2487
|
-
* @returns The
|
|
2721
|
+
* @returns The character at that position
|
|
2488
2722
|
*/
|
|
2489
2723
|
visibleColToRowLetter(rowName: string, visibleCol: number) {
|
|
2490
2724
|
return self.rowMap.get(rowName)?.[
|
|
@@ -2582,6 +2816,22 @@ function stateModelFactory() {
|
|
|
2582
2816
|
return col ?? (seqPos === 0 ? 0 : seq.length)
|
|
2583
2817
|
},
|
|
2584
2818
|
|
|
2819
|
+
/**
|
|
2820
|
+
* #method
|
|
2821
|
+
* the global column of a span's last residue, clamping a position past
|
|
2822
|
+
* the end of the row to its last residue. Undefined for a row name the
|
|
2823
|
+
* alignment does not have, or a row with no residues.
|
|
2824
|
+
*
|
|
2825
|
+
* @param rowName - The name of the row
|
|
2826
|
+
* @param seqPos - The sequence position (0-based, ungapped)
|
|
2827
|
+
*/
|
|
2828
|
+
seqEndToGlobalCol(rowName: string, seqPos: number) {
|
|
2829
|
+
const index = self.seqPosIndex(rowName)
|
|
2830
|
+
return index && index.length > 0
|
|
2831
|
+
? index[Math.min(seqPos, index.length - 1)]
|
|
2832
|
+
: undefined
|
|
2833
|
+
},
|
|
2834
|
+
|
|
2585
2835
|
/**
|
|
2586
2836
|
* #method
|
|
2587
2837
|
* Convert a sequence position (ungapped) directly to a visible column index.
|
|
@@ -2611,10 +2861,10 @@ function stateModelFactory() {
|
|
|
2611
2861
|
const start = Math.max(1, Math.floor(rawStart))
|
|
2612
2862
|
const end = Math.ceil(rawEnd)
|
|
2613
2863
|
let startGlobal = start - 1
|
|
2614
|
-
let endGlobal = end - 1
|
|
2864
|
+
let endGlobal = Math.min(end, self.MSA?.getWidth() ?? end) - 1
|
|
2615
2865
|
if (row !== undefined) {
|
|
2616
2866
|
const rowStart = this.seqPosToGlobalCol(row, start - 1)
|
|
2617
|
-
const rowEnd = this.
|
|
2867
|
+
const rowEnd = this.seqEndToGlobalCol(row, end - 1)
|
|
2618
2868
|
if (rowStart === undefined || rowEnd === undefined) {
|
|
2619
2869
|
return undefined
|
|
2620
2870
|
}
|
|
@@ -2908,6 +3158,28 @@ function stateModelFactory() {
|
|
|
2908
3158
|
},
|
|
2909
3159
|
}))
|
|
2910
3160
|
.views(self => ({
|
|
3161
|
+
/**
|
|
3162
|
+
* #getter
|
|
3163
|
+
*/
|
|
3164
|
+
get verticalScrollbarWidth() {
|
|
3165
|
+
return self.showVerticalScrollbar ? 20 : 0
|
|
3166
|
+
},
|
|
3167
|
+
/**
|
|
3168
|
+
* #getter
|
|
3169
|
+
* width of the alignment canvas: the msa area less the vertical
|
|
3170
|
+
* scrollbar. showHorizontalScrollbar must not read it, since that feeds
|
|
3171
|
+
* msaAreaHeight -> showVerticalScrollbar and would form a cycle
|
|
3172
|
+
*/
|
|
3173
|
+
get msaCanvasWidth() {
|
|
3174
|
+
return self.msaAreaWidth - this.verticalScrollbarWidth
|
|
3175
|
+
},
|
|
3176
|
+
/**
|
|
3177
|
+
* #getter
|
|
3178
|
+
* most-negative allowed scrollX, which keeps the last column in view
|
|
3179
|
+
*/
|
|
3180
|
+
get maxScrollX() {
|
|
3181
|
+
return Math.min(-self.totalWidth + this.msaCanvasWidth, 0)
|
|
3182
|
+
},
|
|
2911
3183
|
/**
|
|
2912
3184
|
* #getter
|
|
2913
3185
|
*/
|
|
@@ -2925,9 +3197,29 @@ function stateModelFactory() {
|
|
|
2925
3197
|
},
|
|
2926
3198
|
/**
|
|
2927
3199
|
* #getter
|
|
3200
|
+
* warnings that follow from the loaded data and clear when it changes. A
|
|
3201
|
+
* GFF whose first column names rows of some other alignment parses
|
|
3202
|
+
* without error and draws nothing
|
|
3203
|
+
*/
|
|
3204
|
+
get dataWarnings(): string[] {
|
|
3205
|
+
const { annotations, rowNamesSet } = self
|
|
3206
|
+
return self.dataInitialized &&
|
|
3207
|
+
annotations.length > 0 &&
|
|
3208
|
+
!annotations.some(a => rowNamesSet.has(a.id))
|
|
3209
|
+
? [
|
|
3210
|
+
`0 of ${annotations.length} annotations name a row in this alignment`,
|
|
3211
|
+
]
|
|
3212
|
+
: []
|
|
3213
|
+
},
|
|
3214
|
+
/**
|
|
3215
|
+
* #getter
|
|
3216
|
+
* the load warnings and the undismissed data warnings, for the header
|
|
2928
3217
|
*/
|
|
2929
|
-
get
|
|
2930
|
-
return
|
|
3218
|
+
get warnings(): string[] {
|
|
3219
|
+
return [
|
|
3220
|
+
...self.loadWarnings,
|
|
3221
|
+
...this.dataWarnings.filter(w => !self.dismissedWarnings.includes(w)),
|
|
3222
|
+
]
|
|
2931
3223
|
},
|
|
2932
3224
|
/**
|
|
2933
3225
|
* #getter
|
|
@@ -2954,6 +3246,14 @@ function stateModelFactory() {
|
|
|
2954
3246
|
self.drawMsaLetters = arg
|
|
2955
3247
|
},
|
|
2956
3248
|
|
|
3249
|
+
/**
|
|
3250
|
+
* #action
|
|
3251
|
+
*/
|
|
3252
|
+
clearWarnings() {
|
|
3253
|
+
self.loadWarnings = []
|
|
3254
|
+
self.dismissedWarnings = self.dataWarnings
|
|
3255
|
+
},
|
|
3256
|
+
|
|
2957
3257
|
/**
|
|
2958
3258
|
* #action
|
|
2959
3259
|
*/
|
|
@@ -3014,13 +3314,43 @@ function stateModelFactory() {
|
|
|
3014
3314
|
|
|
3015
3315
|
/**
|
|
3016
3316
|
* #action
|
|
3017
|
-
*
|
|
3018
|
-
* and
|
|
3317
|
+
* root the tree on the branch above the given node. The node's tips are
|
|
3318
|
+
* a split of the unrooted tree, and the side of it that is a clade in
|
|
3319
|
+
* the tree as written stands for it as an outgroup, by its first and
|
|
3320
|
+
* last tip, so the root survives the path ids changing under it
|
|
3321
|
+
*/
|
|
3322
|
+
rerootAt(nodeId: string) {
|
|
3323
|
+
const shown = find(
|
|
3324
|
+
hierarchy(self.tree, d => d.children),
|
|
3325
|
+
n => n.data.id === nodeId,
|
|
3326
|
+
)
|
|
3327
|
+
if (!shown?.parent) {
|
|
3328
|
+
return
|
|
3329
|
+
}
|
|
3330
|
+
const node = outgroupNode(
|
|
3331
|
+
hierarchy(self.inputTree, d => d.children),
|
|
3332
|
+
leaves(shown).map(n => n.data.name),
|
|
3333
|
+
)
|
|
3334
|
+
if (node) {
|
|
3335
|
+
const tips = leaves(node)
|
|
3336
|
+
self.setTreeRoot({
|
|
3337
|
+
outgroup: [
|
|
3338
|
+
...new Set([tips[0]!.data.name, tips.at(-1)!.data.name]),
|
|
3339
|
+
],
|
|
3340
|
+
})
|
|
3341
|
+
}
|
|
3342
|
+
},
|
|
3343
|
+
|
|
3344
|
+
/**
|
|
3345
|
+
* #action
|
|
3346
|
+
* swap in a different tree over the same alignment. Clears `collapsed`,
|
|
3347
|
+
* `rotated` and `showOnly`, since path-derived node ids (node-0-0-1) from the old
|
|
3019
3348
|
* tree would match unrelated nodes in the new one.
|
|
3020
3349
|
*/
|
|
3021
3350
|
replaceTree(newick: string) {
|
|
3022
3351
|
transaction(() => {
|
|
3023
3352
|
self.collapsed.clear()
|
|
3353
|
+
self.rotated.clear()
|
|
3024
3354
|
self.setShowOnly(undefined)
|
|
3025
3355
|
self.setTree(newick)
|
|
3026
3356
|
})
|
|
@@ -3034,52 +3364,53 @@ function stateModelFactory() {
|
|
|
3034
3364
|
self.setColWidth(defaultColWidth)
|
|
3035
3365
|
self.setRowHeight(defaultRowHeight)
|
|
3036
3366
|
},
|
|
3367
|
+
/**
|
|
3368
|
+
* #action
|
|
3369
|
+
* zoom by `scaleFactor` about the center of the alignment area
|
|
3370
|
+
*/
|
|
3371
|
+
zoomAtCenter(scaleFactor: number, axis: ScrollZoomAxis = 'both') {
|
|
3372
|
+
this.zoomToPos(
|
|
3373
|
+
scaleFactor,
|
|
3374
|
+
self.msaCanvasWidth / 2,
|
|
3375
|
+
self.msaAreaHeight / 2,
|
|
3376
|
+
axis,
|
|
3377
|
+
)
|
|
3378
|
+
},
|
|
3037
3379
|
/**
|
|
3038
3380
|
* #action
|
|
3039
3381
|
*/
|
|
3040
3382
|
zoomOutHorizontal() {
|
|
3041
|
-
|
|
3042
|
-
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
3383
|
+
this.zoomAtCenter(0.75, 'horizontal')
|
|
3043
3384
|
},
|
|
3044
3385
|
/**
|
|
3045
3386
|
* #action
|
|
3046
3387
|
*/
|
|
3047
3388
|
zoomInHorizontal() {
|
|
3048
|
-
|
|
3049
|
-
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
3389
|
+
this.zoomAtCenter(1.5, 'horizontal')
|
|
3050
3390
|
},
|
|
3051
3391
|
/**
|
|
3052
3392
|
* #action
|
|
3053
3393
|
*/
|
|
3054
3394
|
zoomInVertical() {
|
|
3055
|
-
|
|
3395
|
+
this.zoomAtCenter(1.5, 'vertical')
|
|
3056
3396
|
},
|
|
3057
3397
|
/**
|
|
3058
3398
|
* #action
|
|
3059
3399
|
*/
|
|
3060
3400
|
zoomOutVertical() {
|
|
3061
|
-
|
|
3062
|
-
minRowHeight,
|
|
3063
|
-
Math.floor(self.rowHeight * 0.75),
|
|
3064
|
-
)
|
|
3401
|
+
this.zoomAtCenter(0.75, 'vertical')
|
|
3065
3402
|
},
|
|
3066
3403
|
/**
|
|
3067
3404
|
* #action
|
|
3068
3405
|
*/
|
|
3069
3406
|
zoomIn() {
|
|
3070
|
-
|
|
3071
|
-
this.zoomInHorizontal()
|
|
3072
|
-
this.zoomInVertical()
|
|
3073
|
-
})
|
|
3407
|
+
this.zoomAtCenter(1.5)
|
|
3074
3408
|
},
|
|
3075
3409
|
/**
|
|
3076
3410
|
* #action
|
|
3077
3411
|
*/
|
|
3078
3412
|
zoomOut() {
|
|
3079
|
-
|
|
3080
|
-
this.zoomOutHorizontal()
|
|
3081
|
-
this.zoomOutVertical()
|
|
3082
|
-
})
|
|
3413
|
+
this.zoomAtCenter(0.75)
|
|
3083
3414
|
},
|
|
3084
3415
|
/**
|
|
3085
3416
|
* #action
|
|
@@ -3242,25 +3573,11 @@ function stateModelFactory() {
|
|
|
3242
3573
|
},
|
|
3243
3574
|
}))
|
|
3244
3575
|
.views(self => ({
|
|
3245
|
-
/**
|
|
3246
|
-
* #getter
|
|
3247
|
-
*/
|
|
3248
|
-
get verticalScrollbarWidth() {
|
|
3249
|
-
return self.showVerticalScrollbar ? 20 : 0
|
|
3250
|
-
},
|
|
3251
|
-
/**
|
|
3252
|
-
* #getter
|
|
3253
|
-
* width of the alignment canvas: the msa area less the vertical
|
|
3254
|
-
* scrollbar. showHorizontalScrollbar must not read it, since that feeds
|
|
3255
|
-
* msaAreaHeight -> showVerticalScrollbar and would form a cycle
|
|
3256
|
-
*/
|
|
3257
|
-
get msaCanvasWidth() {
|
|
3258
|
-
return self.msaAreaWidth - this.verticalScrollbarWidth
|
|
3259
|
-
},
|
|
3260
3576
|
/**
|
|
3261
3577
|
* #method
|
|
3262
3578
|
* the cell at a visible column and row index, in the coordinates a host
|
|
3263
|
-
* writes highlights in
|
|
3579
|
+
* writes highlights in. On a gap `residue` is undefined and `letter` is
|
|
3580
|
+
* the gap character
|
|
3264
3581
|
*/
|
|
3265
3582
|
cellAt(visibleCol: number, rowIndex?: number): Cell {
|
|
3266
3583
|
const column = self.visibleColToGlobalCol(visibleCol)
|
|
@@ -3308,7 +3625,7 @@ function stateModelFactory() {
|
|
|
3308
3625
|
}
|
|
3309
3626
|
const { xStart, xEnd } = visibleColRange({
|
|
3310
3627
|
offsetX: -scrollX,
|
|
3311
|
-
blockWidth:
|
|
3628
|
+
blockWidth: self.msaCanvasWidth,
|
|
3312
3629
|
colWidth,
|
|
3313
3630
|
})
|
|
3314
3631
|
const last = Math.max(0, Math.min(xEnd, numColumns) - 1)
|
|
@@ -3471,7 +3788,7 @@ function stateModelFactory() {
|
|
|
3471
3788
|
add(field, legend)
|
|
3472
3789
|
}
|
|
3473
3790
|
}
|
|
3474
|
-
for (const { legendTitle, legend } of this.
|
|
3791
|
+
for (const { legendTitle, legend } of this.rowPanelScales) {
|
|
3475
3792
|
add(legendTitle, legend)
|
|
3476
3793
|
}
|
|
3477
3794
|
return [...byKey.values()]
|
|
@@ -3519,7 +3836,7 @@ function stateModelFactory() {
|
|
|
3519
3836
|
// column moves to the neighboring boundary. Bands with no visible
|
|
3520
3837
|
// columns, or naming a missing row, are dropped.
|
|
3521
3838
|
const start = self.seqPosToGlobalCol(name, annotation.start - 1)
|
|
3522
|
-
const end = self.
|
|
3839
|
+
const end = self.seqEndToGlobalCol(name, annotation.end - 1)
|
|
3523
3840
|
if (start === undefined || end === undefined) {
|
|
3524
3841
|
return undefined
|
|
3525
3842
|
}
|
|
@@ -3621,6 +3938,26 @@ function stateModelFactory() {
|
|
|
3621
3938
|
return runs
|
|
3622
3939
|
},
|
|
3623
3940
|
|
|
3941
|
+
/**
|
|
3942
|
+
* #method
|
|
3943
|
+
* a highlight label with `{residue}` and `{position}` filled in from the
|
|
3944
|
+
* row's letter at `start`, which is how the `175` shorthand draws "R175"
|
|
3945
|
+
*/
|
|
3946
|
+
fillHighlightLabel(label: string, row?: string, start?: number) {
|
|
3947
|
+
if (!label.includes('{')) {
|
|
3948
|
+
return label
|
|
3949
|
+
}
|
|
3950
|
+
const col =
|
|
3951
|
+
row && start !== undefined
|
|
3952
|
+
? self.seqPosIndex(row)?.[start - 1]
|
|
3953
|
+
: undefined
|
|
3954
|
+
const residue =
|
|
3955
|
+
row && col !== undefined ? (self.MSA?.getRow(row)[col] ?? '') : ''
|
|
3956
|
+
return label
|
|
3957
|
+
.replaceAll('{residue}', residue)
|
|
3958
|
+
.replaceAll('{position}', start === undefined ? '' : String(start))
|
|
3959
|
+
},
|
|
3960
|
+
|
|
3624
3961
|
/**
|
|
3625
3962
|
* #getter
|
|
3626
3963
|
* `highlights` projected onto what is on screen: residue spans go
|
|
@@ -3636,7 +3973,10 @@ function stateModelFactory() {
|
|
|
3636
3973
|
...Object.values(transientHighlights).flat(),
|
|
3637
3974
|
]
|
|
3638
3975
|
return all.flatMap(({ row, rows, start, end, label, color }) => {
|
|
3639
|
-
const base = {
|
|
3976
|
+
const base = {
|
|
3977
|
+
label: label && this.fillHighlightLabel(label, row, start),
|
|
3978
|
+
color,
|
|
3979
|
+
}
|
|
3640
3980
|
if (rows) {
|
|
3641
3981
|
const rowIndices = rows
|
|
3642
3982
|
.map(name => rowNamesSet.get(name))
|
|
@@ -3703,45 +4043,30 @@ function stateModelFactory() {
|
|
|
3703
4043
|
|
|
3704
4044
|
/**
|
|
3705
4045
|
* #getter
|
|
3706
|
-
* each row panel
|
|
3707
|
-
*
|
|
3708
|
-
*
|
|
3709
|
-
* per change of that table or the panels, never per block per frame.
|
|
4046
|
+
* each row panel's scale resolved against the values its field takes,
|
|
4047
|
+
* giving the colors, labels and legend entries. Reads no cell size, so
|
|
4048
|
+
* a zoom reuses it and the legends built from it.
|
|
3710
4049
|
*/
|
|
3711
|
-
get
|
|
3712
|
-
const
|
|
4050
|
+
get rowPanelScales(): (StripPanelScale | FeaturePanelScale)[] {
|
|
4051
|
+
const { rowData } = self
|
|
3713
4052
|
const { fillPalette, featureFillEncoding, featureLabels } = this
|
|
3714
4053
|
const annotations = self.filteredAnnotations
|
|
3715
|
-
|
|
3716
|
-
return self.rowPanels.map((panel, index) => {
|
|
3717
|
-
const width = rowPanelWidth(panel, self.rowHeight)
|
|
3718
|
-
const base = { id: `rowpanel-${index}`, width, offsetX }
|
|
3719
|
-
offsetX += width
|
|
4054
|
+
return self.rowPanels.map(panel => {
|
|
3720
4055
|
if (panel.kind === 'features') {
|
|
3721
4056
|
const color = panel.encoding?.color
|
|
3722
4057
|
const encoding = color
|
|
3723
4058
|
? resolveFeatureScale(color.field, color.scale, annotations)
|
|
3724
4059
|
: featureFillEncoding
|
|
3725
4060
|
const label = panel.encoding?.label
|
|
3726
|
-
const align = panel.transform?.find(t => t.type === 'align')
|
|
3727
4061
|
return {
|
|
3728
|
-
...base,
|
|
3729
4062
|
kind: panel.kind,
|
|
3730
4063
|
x: panel.x,
|
|
3731
4064
|
header: panel.header ?? '',
|
|
3732
4065
|
field: encoding?.field,
|
|
3733
4066
|
legendTitle: encoding?.field,
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
colWidth: self.colWidth,
|
|
3738
|
-
domainBands: this.domainBands,
|
|
3739
|
-
annotationsByRow: self.annotationsByRow,
|
|
3740
|
-
shifts: align
|
|
3741
|
-
? this.featureAlignShifts.get(align.on)
|
|
3742
|
-
: undefined,
|
|
3743
|
-
}),
|
|
3744
|
-
colors: featureColorMap(annotations, encoding, fillPalette),
|
|
4067
|
+
colors: color
|
|
4068
|
+
? featureColorMap(annotations, encoding, fillPalette)
|
|
4069
|
+
: this.featureColors,
|
|
3745
4070
|
labels: label
|
|
3746
4071
|
? featureLabelMap(annotations, label)
|
|
3747
4072
|
: featureLabels,
|
|
@@ -3756,28 +4081,56 @@ function stateModelFactory() {
|
|
|
3756
4081
|
}
|
|
3757
4082
|
const { colorOf, legend } = resolveScale(
|
|
3758
4083
|
panel.scale,
|
|
3759
|
-
|
|
4084
|
+
Object.values(rowData)
|
|
4085
|
+
.map(row => row?.[panel.field])
|
|
4086
|
+
.filter(notEmpty),
|
|
3760
4087
|
)
|
|
3761
|
-
const colors = new Map<string, string>()
|
|
3762
|
-
for (const [name, row] of rows) {
|
|
3763
|
-
const value = row?.[panel.field]
|
|
3764
|
-
const color = value === undefined ? undefined : colorOf(value)
|
|
3765
|
-
if (color) {
|
|
3766
|
-
colors.set(name, color)
|
|
3767
|
-
}
|
|
3768
|
-
}
|
|
3769
4088
|
return {
|
|
3770
|
-
...base,
|
|
3771
4089
|
kind: panel.kind,
|
|
3772
4090
|
field: panel.field,
|
|
3773
4091
|
header: panel.header ?? panel.field,
|
|
3774
4092
|
legendTitle: panel.legend ?? panel.field,
|
|
3775
|
-
colors,
|
|
4093
|
+
colors: colorByRow(rowData, panel.field, colorOf),
|
|
3776
4094
|
legend,
|
|
3777
4095
|
}
|
|
3778
4096
|
})
|
|
3779
4097
|
},
|
|
3780
4098
|
|
|
4099
|
+
/**
|
|
4100
|
+
* #getter
|
|
4101
|
+
* each row panel with its scale from `rowPanelScales` and its geometry:
|
|
4102
|
+
* the pixel column it draws in and, for a features panel, the spans per
|
|
4103
|
+
* row. Resolved once per change of those inputs, never per block per
|
|
4104
|
+
* frame.
|
|
4105
|
+
*/
|
|
4106
|
+
get resolvedRowPanels(): ResolvedRowPanel[] {
|
|
4107
|
+
const scales = this.rowPanelScales
|
|
4108
|
+
let offsetX = 0
|
|
4109
|
+
return self.rowPanels.map((panel, index) => {
|
|
4110
|
+
const width = rowPanelWidth(panel, self)
|
|
4111
|
+
const base = { id: `rowpanel-${index}`, width, offsetX }
|
|
4112
|
+
offsetX += width
|
|
4113
|
+
if (panel.kind === 'features') {
|
|
4114
|
+
const align = panel.transform?.find(t => t.type === 'align')
|
|
4115
|
+
return {
|
|
4116
|
+
...base,
|
|
4117
|
+
...(scales[index] as FeaturePanelScale),
|
|
4118
|
+
spans: featurePanelSpans({
|
|
4119
|
+
panel,
|
|
4120
|
+
width,
|
|
4121
|
+
colWidth: self.colWidth,
|
|
4122
|
+
domainBands: this.domainBands,
|
|
4123
|
+
annotationsByRow: self.annotationsByRow,
|
|
4124
|
+
shifts: align
|
|
4125
|
+
? this.featureAlignShifts.get(align.on)
|
|
4126
|
+
: undefined,
|
|
4127
|
+
}),
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
return { ...base, ...(scales[index] as StripPanelScale) }
|
|
4131
|
+
})
|
|
4132
|
+
},
|
|
4133
|
+
|
|
3781
4134
|
/**
|
|
3782
4135
|
* #getter
|
|
3783
4136
|
* the shift an `align` transform gives each row, keyed by the feature
|
|
@@ -3820,19 +4173,9 @@ function stateModelFactory() {
|
|
|
3820
4173
|
const encoding = this.resolvedEncodings.find(
|
|
3821
4174
|
e => e.channel === 'tipLabel',
|
|
3822
4175
|
)
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
const colors = new Map<string, string>()
|
|
3827
|
-
for (const [name, row] of Object.entries(self.rowData)) {
|
|
3828
|
-
const value = row?.[encoding.field]
|
|
3829
|
-
const color =
|
|
3830
|
-
value === undefined ? undefined : encoding.colorOf(value)
|
|
3831
|
-
if (color) {
|
|
3832
|
-
colors.set(name, color)
|
|
3833
|
-
}
|
|
3834
|
-
}
|
|
3835
|
-
return colors
|
|
4176
|
+
return encoding
|
|
4177
|
+
? colorByRow(self.rowData, encoding.field, encoding.colorOf)
|
|
4178
|
+
: undefined
|
|
3836
4179
|
},
|
|
3837
4180
|
|
|
3838
4181
|
/**
|
|
@@ -3848,14 +4191,14 @@ function stateModelFactory() {
|
|
|
3848
4191
|
if (!encoding) {
|
|
3849
4192
|
return undefined
|
|
3850
4193
|
}
|
|
4194
|
+
const colors = colorByRow(
|
|
4195
|
+
self.rowData,
|
|
4196
|
+
encoding.field,
|
|
4197
|
+
encoding.colorOf,
|
|
4198
|
+
)
|
|
3851
4199
|
return self.rowNames.map(name => {
|
|
3852
|
-
const
|
|
3853
|
-
|
|
3854
|
-
value === undefined ? undefined : encoding.colorOf(value)
|
|
3855
|
-
if (!color) {
|
|
3856
|
-
return undefined
|
|
3857
|
-
}
|
|
3858
|
-
return withAlpha(color, rowTintAlpha)
|
|
4200
|
+
const color = colors.get(name)
|
|
4201
|
+
return color && withAlpha(color, rowTintAlpha)
|
|
3859
4202
|
})
|
|
3860
4203
|
},
|
|
3861
4204
|
|
|
@@ -3937,7 +4280,8 @@ function stateModelFactory() {
|
|
|
3937
4280
|
*/
|
|
3938
4281
|
reset() {
|
|
3939
4282
|
self.resetCount++
|
|
3940
|
-
self.
|
|
4283
|
+
self.loadWarnings = []
|
|
4284
|
+
self.dismissedWarnings = []
|
|
3941
4285
|
self.setStatus(undefined)
|
|
3942
4286
|
applySnapshot(
|
|
3943
4287
|
self,
|
|
@@ -3964,6 +4308,21 @@ function stateModelFactory() {
|
|
|
3964
4308
|
const blob = new Blob([html], { type: 'image/svg+xml' })
|
|
3965
4309
|
saveAs(blob, exportFileName(self.msaFilehandle, 'svg'))
|
|
3966
4310
|
},
|
|
4311
|
+
/**
|
|
4312
|
+
* #action
|
|
4313
|
+
* save `treeNewick` as a file
|
|
4314
|
+
*/
|
|
4315
|
+
exportNewick() {
|
|
4316
|
+
const blob = new Blob([self.treeNewick], { type: 'text/plain' })
|
|
4317
|
+
saveAs(
|
|
4318
|
+
blob,
|
|
4319
|
+
exportFileName(
|
|
4320
|
+
self.treeFilehandle ?? self.msaFilehandle,
|
|
4321
|
+
'nwk',
|
|
4322
|
+
'tree',
|
|
4323
|
+
),
|
|
4324
|
+
)
|
|
4325
|
+
},
|
|
3967
4326
|
/**
|
|
3968
4327
|
* #action
|
|
3969
4328
|
* show or hide an annotation type. Only hidden types are recorded; see
|
|
@@ -4047,36 +4406,62 @@ function stateModelFactory() {
|
|
|
4047
4406
|
self.setHighlightedColumns(self.highlightColumns)
|
|
4048
4407
|
}
|
|
4049
4408
|
|
|
4050
|
-
// The `collapse` and `
|
|
4051
|
-
// the subtree in focus, which the tree,
|
|
4052
|
-
// alignment all read. The tree arrives with
|
|
4053
|
-
// and later for a filehandle, so the seeding
|
|
4054
|
-
//
|
|
4055
|
-
//
|
|
4056
|
-
//
|
|
4057
|
-
//
|
|
4058
|
-
|
|
4409
|
+
// The `collapse`, `focus` and `rotate` clade marks seed the collapsed
|
|
4410
|
+
// list, the subtree in focus and the rotated nodes, which the tree,
|
|
4411
|
+
// `hideGapsEffective` and the alignment all read. The tree arrives with
|
|
4412
|
+
// the model for inline data and later for a filehandle, so the seeding
|
|
4413
|
+
// waits for it and then runs once per data load, per change of
|
|
4414
|
+
// `clades` and per reroot, which clears the node ids the seeding
|
|
4415
|
+
// wrote: expanding a seeded clade sticks until one of those changes.
|
|
4416
|
+
// `dataInitialized` is true once the MSA alone has loaded, when the
|
|
4417
|
+
// tree is still the flat stub, so a tree filehandle holds the seeding
|
|
4418
|
+
// until its text lands.
|
|
4419
|
+
addDisposer(
|
|
4420
|
+
self,
|
|
4421
|
+
reaction(
|
|
4422
|
+
() =>
|
|
4423
|
+
self.dataInitialized && !(self.treeFilehandle && !self.data.tree)
|
|
4424
|
+
? { clades: getSnapshot(self.clades), treeRoot: self.treeRoot }
|
|
4425
|
+
: undefined,
|
|
4426
|
+
seed => {
|
|
4427
|
+
if (!seed?.clades.length) {
|
|
4428
|
+
return
|
|
4429
|
+
}
|
|
4430
|
+
for (const { mark, nodeId } of self.resolvedClades) {
|
|
4431
|
+
if (nodeId === undefined) {
|
|
4432
|
+
continue
|
|
4433
|
+
}
|
|
4434
|
+
if (mark === 'collapse' && !self.collapsed.includes(nodeId)) {
|
|
4435
|
+
self.toggleCollapsed(nodeId)
|
|
4436
|
+
} else if (mark === 'focus') {
|
|
4437
|
+
self.setShowOnly(nodeId)
|
|
4438
|
+
} else if (
|
|
4439
|
+
mark === 'rotate' &&
|
|
4440
|
+
!self.rotated.includes(nodeId)
|
|
4441
|
+
) {
|
|
4442
|
+
self.toggleRotated(nodeId)
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
},
|
|
4446
|
+
{ fireImmediately: true, equals: compareStructural },
|
|
4447
|
+
),
|
|
4448
|
+
)
|
|
4449
|
+
|
|
4450
|
+
// zoomToRegion needs a width and resolves residues through the visible
|
|
4451
|
+
// columns, so this waits for both, and for a tree file whose collapsed
|
|
4452
|
+
// clades would hide columns and move the target after the zoom
|
|
4059
4453
|
addDisposer(
|
|
4060
4454
|
self,
|
|
4061
4455
|
autorun(() => {
|
|
4456
|
+
const { region } = self
|
|
4062
4457
|
if (
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
self.
|
|
4458
|
+
region &&
|
|
4459
|
+
self.viewInitialized &&
|
|
4460
|
+
self.numColumns > 0 &&
|
|
4461
|
+
!(self.treeFilehandle && !self.data.tree)
|
|
4067
4462
|
) {
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
cladesSeeded = true
|
|
4071
|
-
for (const { mark, nodeId } of self.resolvedClades) {
|
|
4072
|
-
if (nodeId === undefined) {
|
|
4073
|
-
continue
|
|
4074
|
-
}
|
|
4075
|
-
if (mark === 'collapse' && !self.collapsed.includes(nodeId)) {
|
|
4076
|
-
self.toggleCollapsed(nodeId)
|
|
4077
|
-
} else if (mark === 'focus') {
|
|
4078
|
-
self.setShowOnly(nodeId)
|
|
4079
|
-
}
|
|
4463
|
+
this.zoomToRegion(region)
|
|
4464
|
+
self.setRegion(undefined)
|
|
4080
4465
|
}
|
|
4081
4466
|
}),
|
|
4082
4467
|
)
|
|
@@ -4246,24 +4631,6 @@ function stateModelFactory() {
|
|
|
4246
4631
|
}),
|
|
4247
4632
|
)
|
|
4248
4633
|
|
|
4249
|
-
// a GFF whose first column names rows of some other alignment parses
|
|
4250
|
-
// without error and draws nothing
|
|
4251
|
-
addDisposer(
|
|
4252
|
-
self,
|
|
4253
|
-
autorun(() => {
|
|
4254
|
-
const { annotations, rowNamesSet } = self
|
|
4255
|
-
if (
|
|
4256
|
-
annotations.length > 0 &&
|
|
4257
|
-
rowNamesSet.size > 0 &&
|
|
4258
|
-
!annotations.some(a => rowNamesSet.has(a.id))
|
|
4259
|
-
) {
|
|
4260
|
-
self.addWarning(
|
|
4261
|
-
`0 of ${annotations.length} annotations name a row in this alignment`,
|
|
4262
|
-
)
|
|
4263
|
-
}
|
|
4264
|
-
}),
|
|
4265
|
-
)
|
|
4266
|
-
|
|
4267
4634
|
// gffFilehandle loads into data.gff, which the autorun above parses
|
|
4268
4635
|
loadOnFilehandleChange({
|
|
4269
4636
|
what: 'annotations',
|