react-msaview 9.1.0 → 9.1.1
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 +111 -115
- package/bundle/index.js.map +4 -4
- package/dist/blosum62.d.ts +3 -0
- package/dist/blosum62.js +43 -0
- package/dist/blosum62.js.map +1 -0
- package/dist/colorSchemes.js +1 -0
- package/dist/colorSchemes.js.map +1 -1
- package/dist/components/Loading.d.ts +3 -2
- package/dist/components/Loading.js +2 -2
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAView.d.ts +3 -2
- package/dist/components/MSAView.js +7 -4
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +62 -12
- package/dist/components/MetadataTable.d.ts +4 -1
- package/dist/components/MetadataTable.js +55 -16
- package/dist/components/MetadataTable.js.map +1 -1
- package/dist/components/Track.js +6 -0
- package/dist/components/Track.js.map +1 -1
- package/dist/components/controls.js +2 -1
- package/dist/components/controls.js.map +1 -1
- package/dist/components/dialogs/AnnotationFileDialog.js +7 -2
- package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
- package/dist/components/dialogs/FeatureDialog.js +2 -2
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/{header/MSASettingsMenu.d.ts → dialogs/GapThresholdDialog.d.ts} +3 -2
- package/dist/components/dialogs/GapThresholdDialog.js +15 -0
- package/dist/components/dialogs/GapThresholdDialog.js.map +1 -0
- package/dist/components/{header → dialogs}/GappynessSlider.d.ts +1 -1
- package/dist/components/{header → dialogs}/GappynessSlider.js +3 -10
- package/dist/components/dialogs/GappynessSlider.js.map +1 -0
- package/dist/components/{header → dialogs}/GoToBox.d.ts +4 -2
- package/dist/components/dialogs/GoToBox.js +48 -0
- package/dist/components/dialogs/GoToBox.js.map +1 -0
- package/dist/components/dialogs/GoToDialog.d.ts +6 -0
- package/dist/components/dialogs/GoToDialog.js +10 -0
- package/dist/components/dialogs/GoToDialog.js.map +1 -0
- package/dist/components/dialogs/MetadataDialog.js +11 -3
- package/dist/components/dialogs/MetadataDialog.js.map +1 -1
- package/dist/components/dialogs/goTo.d.ts +5 -0
- package/dist/components/{header → dialogs}/goTo.js +1 -1
- package/dist/components/dialogs/goTo.js.map +1 -0
- package/dist/components/dialogs/metadataLabels.d.ts +4 -0
- package/dist/components/dialogs/metadataLabels.js +52 -0
- package/dist/components/dialogs/metadataLabels.js.map +1 -0
- package/dist/components/dialogs/metadataRecord.d.ts +1 -0
- package/dist/components/dialogs/metadataRecord.js +38 -0
- package/dist/components/dialogs/metadataRecord.js.map +1 -0
- package/dist/components/header/Header.d.ts +6 -2
- package/dist/components/header/Header.js +30 -32
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/{TreeSettingsMenu.d.ts → HiddenColumnsChip.d.ts} +2 -2
- package/dist/components/header/HiddenColumnsChip.js +15 -0
- package/dist/components/header/HiddenColumnsChip.js.map +1 -0
- package/dist/components/header/MenuBar.d.ts +6 -0
- package/dist/components/header/MenuBar.js +33 -0
- package/dist/components/header/MenuBar.js.map +1 -0
- package/dist/components/header/ScrollZoomCheckbox.d.ts +7 -0
- package/dist/components/header/ScrollZoomCheckbox.js +19 -0
- package/dist/components/header/ScrollZoomCheckbox.js.map +1 -0
- package/dist/components/header/ZoomControls.js +10 -30
- package/dist/components/header/ZoomControls.js.map +1 -1
- package/dist/components/header/colorSchemeMenuItems.d.ts +4 -0
- package/dist/components/header/colorSchemeMenuItems.js +106 -0
- package/dist/components/header/colorSchemeMenuItems.js.map +1 -0
- package/dist/components/header/getAnnotationsMenu.d.ts +3 -30
- package/dist/components/header/getAnnotationsMenu.js +48 -54
- package/dist/components/header/getAnnotationsMenu.js.map +1 -1
- package/dist/components/header/menuBarItems.d.ts +11 -0
- package/dist/components/header/menuBarItems.js +277 -0
- package/dist/components/header/menuBarItems.js.map +1 -0
- package/dist/components/import/ImportForm.js +12 -6
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/minimap/Minimap.js +7 -3
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/minimap/MinimapSVG.js +2 -1
- package/dist/components/minimap/MinimapSVG.js.map +1 -1
- package/dist/components/minimap/minimapLayout.d.ts +12 -0
- package/dist/components/minimap/minimapLayout.js +20 -0
- package/dist/components/minimap/minimapLayout.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +26 -9
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +15 -11
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +2 -2
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/WheelZoomHint.d.ts +7 -0
- package/dist/components/msa/WheelZoomHint.js +38 -0
- package/dist/components/msa/WheelZoomHint.js.map +1 -0
- package/dist/components/msa/msaKeys.js +3 -0
- package/dist/components/msa/msaKeys.js.map +1 -1
- package/dist/components/msa/msaRaster.d.ts +4 -5
- package/dist/components/msa/msaRaster.js +5 -5
- package/dist/components/msa/msaRaster.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +22 -2
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderHighlights.d.ts +0 -3
- package/dist/components/msa/renderHighlights.js +3 -3
- package/dist/components/msa/renderHighlights.js.map +1 -1
- package/dist/components/msa/renderMSABlock.d.ts +6 -1
- package/dist/components/msa/renderMSABlock.js +44 -17
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/tileColor.js +7 -0
- package/dist/components/msa/tileColor.js.map +1 -1
- package/dist/components/rowpanels/RowPanelBlock.js +2 -2
- package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
- package/dist/components/settings/MenuItemsForm.d.ts +10 -0
- package/dist/components/settings/MenuItemsForm.js +80 -0
- package/dist/components/settings/MenuItemsForm.js.map +1 -0
- package/dist/components/settings/SettingsPanel.d.ts +12 -0
- package/dist/components/settings/SettingsPanel.js +92 -0
- package/dist/components/settings/SettingsPanel.js.map +1 -0
- package/dist/components/{header/FileMenu.d.ts → settings/TrackList.d.ts} +2 -2
- package/dist/components/settings/TrackList.js +55 -0
- package/dist/components/settings/TrackList.js.map +1 -0
- package/dist/components/tracks/TrackBlocks.js +2 -2
- package/dist/components/tracks/TrackBlocks.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +2 -2
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeMenu.js +4 -6
- package/dist/components/tree/TreeMenu.js.map +1 -1
- package/dist/components/tree/TreeOverview.js +2 -2
- package/dist/components/tree/TreeOverview.js.map +1 -1
- package/dist/constants.d.ts +4 -1
- package/dist/constants.js +7 -1
- package/dist/constants.js.map +1 -1
- package/dist/darkReader.d.ts +7 -0
- package/dist/darkReader.js +42 -0
- package/dist/darkReader.js.map +1 -0
- package/dist/element.js +22 -47
- package/dist/element.js.map +1 -1
- package/dist/featurePanels.d.ts +37 -0
- package/dist/featurePanels.js +187 -0
- package/dist/featurePanels.js.map +1 -0
- package/dist/filehandleLoader.d.ts +41 -0
- package/dist/filehandleLoader.js +99 -0
- package/dist/filehandleLoader.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/model/treeModel.d.ts +9 -0
- package/dist/model/treeModel.js +12 -1
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +142 -32
- package/dist/model.js +268 -569
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.js +1 -40
- package/dist/neighborJoining.js.map +1 -1
- package/dist/renderToSvg.js +1 -13
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rerootTree.d.ts +1 -0
- package/dist/rerootTree.js +7 -6
- package/dist/rerootTree.js.map +1 -1
- package/dist/schemeResidueColor.d.ts +12 -0
- package/dist/schemeResidueColor.js +33 -0
- package/dist/schemeResidueColor.js.map +1 -0
- package/dist/similarityColors.d.ts +11 -0
- package/dist/similarityColors.js +90 -0
- package/dist/similarityColors.js.map +1 -0
- package/dist/svgTestUtil.d.ts +62 -12
- package/dist/treeDisplay.d.ts +37 -0
- package/dist/treeDisplay.js +209 -0
- package/dist/treeDisplay.js.map +1 -0
- package/dist/types.d.ts +6 -2
- package/dist/useWheelScroll.d.ts +3 -1
- package/dist/useWheelScroll.js +36 -17
- package/dist/useWheelScroll.js.map +1 -1
- package/dist/util.d.ts +2 -0
- package/dist/util.js +2 -0
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wheelZoom.d.ts +8 -3
- package/dist/wheelZoom.js +44 -32
- package/dist/wheelZoom.js.map +1 -1
- package/dist/zoomSnapshot.d.ts +29 -0
- package/dist/zoomSnapshot.js +47 -0
- package/dist/zoomSnapshot.js.map +1 -0
- package/package.json +11 -11
- package/src/blosum62.ts +45 -0
- package/src/colorSchemes.ts +1 -0
- package/src/components/Loading.tsx +6 -2
- package/src/components/MSAView.tsx +22 -5
- package/src/components/MetadataTable.tsx +83 -26
- package/src/components/Track.tsx +13 -0
- package/src/components/controls.ts +2 -1
- package/src/components/dialogs/AnnotationFileDialog.tsx +12 -1
- package/src/components/dialogs/FeatureDialog.tsx +2 -5
- package/src/components/dialogs/GapThresholdDialog.tsx +36 -0
- package/src/components/{header → dialogs}/GappynessSlider.tsx +3 -10
- package/src/components/{header → dialogs}/GoToBox.tsx +36 -10
- package/src/components/dialogs/GoToDialog.tsx +30 -0
- package/src/components/dialogs/MetadataDialog.tsx +32 -4
- package/src/components/{header → dialogs}/goTo.ts +1 -1
- package/src/components/dialogs/metadataLabels.ts +54 -0
- package/src/components/dialogs/metadataRecord.tsx +61 -0
- package/src/components/header/Header.tsx +75 -42
- package/src/components/header/HiddenColumnsChip.tsx +39 -0
- package/src/components/header/MenuBar.tsx +64 -0
- package/src/components/header/ScrollZoomCheckbox.tsx +46 -0
- package/src/components/header/ZoomControls.tsx +20 -35
- package/src/components/header/colorSchemeMenuItems.ts +121 -0
- package/src/components/header/getAnnotationsMenu.ts +52 -56
- package/src/components/header/menuBarItems.ts +357 -0
- package/src/components/import/ImportForm.tsx +19 -7
- package/src/components/minimap/Minimap.tsx +11 -3
- package/src/components/minimap/MinimapSVG.tsx +17 -1
- package/src/components/minimap/minimapLayout.ts +27 -0
- package/src/components/msa/MSACanvas.tsx +22 -3
- package/src/components/msa/MSACanvasBlock.tsx +18 -11
- package/src/components/msa/MSAMouseoverCanvas.tsx +2 -2
- package/src/components/msa/WheelZoomHint.tsx +56 -0
- package/src/components/msa/msaKeys.ts +3 -0
- package/src/components/msa/msaRaster.ts +6 -8
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +28 -5
- package/src/components/msa/renderHighlights.ts +3 -3
- package/src/components/msa/renderMSABlock.ts +51 -15
- package/src/components/msa/tileColor.ts +7 -0
- package/src/components/rowpanels/RowPanelBlock.tsx +2 -2
- package/src/components/settings/MenuItemsForm.tsx +225 -0
- package/src/components/settings/SettingsPanel.tsx +156 -0
- package/src/components/settings/TrackList.tsx +119 -0
- package/src/components/tracks/TrackBlocks.tsx +2 -2
- package/src/components/tree/TreeCanvasBlock.tsx +2 -2
- package/src/components/tree/TreeMenu.tsx +4 -6
- package/src/components/tree/TreeOverview.tsx +2 -2
- package/src/constants.ts +9 -1
- package/src/darkReader.ts +57 -0
- package/src/element.ts +34 -57
- package/src/featurePanels.ts +269 -0
- package/src/filehandleLoader.ts +136 -0
- package/src/index.ts +4 -0
- package/src/model/treeModel.ts +13 -0
- package/src/model.ts +345 -748
- package/src/neighborJoining.ts +1 -42
- package/src/renderToSvg.tsx +1 -13
- package/src/rerootTree.ts +7 -6
- package/src/schemeResidueColor.ts +37 -0
- package/src/similarityColors.ts +112 -0
- package/src/treeDisplay.ts +278 -0
- package/src/types.ts +6 -3
- package/src/useWheelScroll.ts +43 -22
- package/src/util.ts +3 -0
- package/src/version.ts +1 -1
- package/src/wheelZoom.ts +55 -33
- package/src/zoomSnapshot.ts +80 -0
- package/dist/components/header/ColorSchemeMenu.d.ts +0 -12
- package/dist/components/header/ColorSchemeMenu.js +0 -37
- package/dist/components/header/ColorSchemeMenu.js.map +0 -1
- package/dist/components/header/FileMenu.js +0 -92
- package/dist/components/header/FileMenu.js.map +0 -1
- package/dist/components/header/GappynessSlider.js.map +0 -1
- package/dist/components/header/GoToBox.js +0 -31
- package/dist/components/header/GoToBox.js.map +0 -1
- package/dist/components/header/MSASettingsMenu.js +0 -11
- package/dist/components/header/MSASettingsMenu.js.map +0 -1
- package/dist/components/header/ScrollZoomIcons.d.ts +0 -6
- package/dist/components/header/ScrollZoomIcons.js +0 -35
- package/dist/components/header/ScrollZoomIcons.js.map +0 -1
- package/dist/components/header/TreeSettingsMenu.js +0 -11
- package/dist/components/header/TreeSettingsMenu.js.map +0 -1
- package/dist/components/header/goTo.d.ts +0 -20
- package/dist/components/header/goTo.js.map +0 -1
- package/dist/components/header/settingsMenuItems.d.ts +0 -5
- package/dist/components/header/settingsMenuItems.js +0 -171
- package/dist/components/header/settingsMenuItems.js.map +0 -1
- package/src/components/header/ColorSchemeMenu.tsx +0 -56
- package/src/components/header/FileMenu.tsx +0 -107
- package/src/components/header/MSASettingsMenu.tsx +0 -26
- package/src/components/header/ScrollZoomIcons.tsx +0 -52
- package/src/components/header/TreeSettingsMenu.tsx +0 -30
- package/src/components/header/settingsMenuItems.ts +0 -216
package/src/model.ts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { clamp, groupBy, notEmpty, sum } from '@jbrowse/core/util'
|
|
2
|
-
import { openLocation } from '@jbrowse/core/util/io'
|
|
3
2
|
import { ElementId, FileLocation } from '@jbrowse/core/util/types/mst'
|
|
4
3
|
import {
|
|
5
4
|
addDisposer,
|
|
6
5
|
applySnapshot,
|
|
7
6
|
cast,
|
|
8
7
|
getSnapshot,
|
|
9
|
-
isAlive,
|
|
10
8
|
types,
|
|
11
9
|
} from '@jbrowse/mobx-state-tree'
|
|
12
10
|
import {
|
|
@@ -19,10 +17,8 @@ import {
|
|
|
19
17
|
import {
|
|
20
18
|
generateNodeIds,
|
|
21
19
|
gffToAnnotations,
|
|
22
|
-
parseEmfTree,
|
|
23
20
|
parseGFF,
|
|
24
21
|
parseMSA,
|
|
25
|
-
parseNewick,
|
|
26
22
|
} from 'msa-parsers'
|
|
27
23
|
|
|
28
24
|
import { blockFasta } from './blockFasta.ts'
|
|
@@ -36,14 +32,11 @@ import { visibleColRange } from './components/msa/visibleColRange.ts'
|
|
|
36
32
|
import { cladeGutterWidth } from './components/tree/cladeBrackets.ts'
|
|
37
33
|
import { scaleBarLength } from './components/tree/scaleBar.ts'
|
|
38
34
|
import {
|
|
39
|
-
cladeHighlightAlpha,
|
|
40
|
-
cladeHighlightColor,
|
|
41
35
|
defaultAllowedGappyness,
|
|
42
36
|
defaultColWidth,
|
|
43
37
|
defaultColorSchemeName,
|
|
44
38
|
defaultCurrentAlignment,
|
|
45
39
|
defaultDrawMsaLetters,
|
|
46
|
-
defaultFeaturePanelWidth,
|
|
47
40
|
defaultHeight,
|
|
48
41
|
defaultHideGaps,
|
|
49
42
|
defaultRowHeight,
|
|
@@ -72,35 +65,37 @@ import {
|
|
|
72
65
|
scrollZoomAxes,
|
|
73
66
|
segmentFeatureTypes,
|
|
74
67
|
segmentShades,
|
|
68
|
+
settingsPanelWidth,
|
|
75
69
|
treeScaleBarHeight,
|
|
76
70
|
} from './constants.ts'
|
|
77
71
|
import { createPaletteMap } from './createPaletteMap.ts'
|
|
78
72
|
import { exportFileName } from './exportFileName.ts'
|
|
79
|
-
import {
|
|
73
|
+
import { featureName } from './featureFields.ts'
|
|
74
|
+
import {
|
|
75
|
+
colorByRow,
|
|
76
|
+
featureColorMap,
|
|
77
|
+
featureLabelMap,
|
|
78
|
+
featurePanelSpans,
|
|
79
|
+
resolveFeatureScale,
|
|
80
|
+
rowPanelWidth,
|
|
81
|
+
stringFields,
|
|
82
|
+
} from './featurePanels.ts'
|
|
80
83
|
import { featuresToAnnotations } from './featuresLayer.ts'
|
|
81
|
-
import {
|
|
82
|
-
import { flatToTree } from './flatToTree.ts'
|
|
84
|
+
import { installFilehandleLoader } from './filehandleLoader.ts'
|
|
83
85
|
import {
|
|
84
|
-
collapse,
|
|
85
86
|
find,
|
|
86
|
-
forEachDescendant,
|
|
87
87
|
hierarchy,
|
|
88
88
|
layoutTree,
|
|
89
|
-
leafIndex,
|
|
90
89
|
leaves,
|
|
91
|
-
mrca,
|
|
92
90
|
nodeCoveringRows,
|
|
93
|
-
sort,
|
|
94
|
-
sum as hierarchySum,
|
|
95
91
|
} from './hierarchy.ts'
|
|
96
92
|
import { DataModelF } from './model/DataModel.ts'
|
|
97
93
|
import { DialogQueueSessionMixin } from './model/DialogQueue.ts'
|
|
98
94
|
import { MSAModelF } from './model/msaModel.ts'
|
|
99
95
|
import { TreeModelF } from './model/treeModel.ts'
|
|
100
96
|
import { calculateNeighborJoiningTree } from './neighborJoining.ts'
|
|
101
|
-
import { parseAsn1 } from './parseAsn1.ts'
|
|
102
97
|
import { calculatePropertyConservation } from './propertyConservation.ts'
|
|
103
|
-
import {
|
|
98
|
+
import { preorder } from './rerootTree.ts'
|
|
104
99
|
import {
|
|
105
100
|
globalColToVisibleCol,
|
|
106
101
|
seqPosOfGlobalCol,
|
|
@@ -110,33 +105,43 @@ import {
|
|
|
110
105
|
import { resolveScale } from './scales.ts'
|
|
111
106
|
import { buildSeqPosIndex } from './seqPosToGlobalCol.ts'
|
|
112
107
|
import { maxBitsFor } from './sequenceLogo.ts'
|
|
108
|
+
import {
|
|
109
|
+
blosum62ColumnColors,
|
|
110
|
+
identityColumnColors,
|
|
111
|
+
} from './similarityColors.ts'
|
|
113
112
|
import { stripDefault } from './stripDefault.ts'
|
|
114
113
|
import { treeTips, widestLabel } from './tipLabels.ts'
|
|
114
|
+
import {
|
|
115
|
+
buildTreeRoot,
|
|
116
|
+
outgroupNode,
|
|
117
|
+
parseTreeText,
|
|
118
|
+
resolveClades,
|
|
119
|
+
rootTree,
|
|
120
|
+
tipRange,
|
|
121
|
+
} from './treeDisplay.ts'
|
|
115
122
|
import {
|
|
116
123
|
computeRowInsertions,
|
|
117
124
|
dropBlanks,
|
|
118
125
|
len,
|
|
119
|
-
|
|
126
|
+
readComputed,
|
|
120
127
|
skipBlanks,
|
|
121
128
|
withAlpha,
|
|
122
129
|
} from './util.ts'
|
|
123
130
|
import { saveAs } from './vendor/fileSaver.ts'
|
|
124
131
|
import { writeNewick } from './writeNewick.ts'
|
|
125
132
|
import { parseWuss } from './wuss.ts'
|
|
133
|
+
import { takeZoomSnapshot, zoomSnapshotCovers } from './zoomSnapshot.ts'
|
|
126
134
|
|
|
127
135
|
import type { ColumnStats } from './columnStats.ts'
|
|
128
136
|
import type { ScrollZoomAxis } from './constants.ts'
|
|
129
137
|
import type { HierarchyNode } from './hierarchy.ts'
|
|
130
138
|
import type { ExportSvgOptions } from './renderToSvg.tsx'
|
|
131
|
-
import type { TreeNode } from './rerootTree.ts'
|
|
132
|
-
import type { ScaleSpec } from './scales.ts'
|
|
133
139
|
import type {
|
|
134
140
|
Annotation,
|
|
135
141
|
Arc,
|
|
136
142
|
BasicTrack,
|
|
137
143
|
Cell,
|
|
138
144
|
Clade,
|
|
139
|
-
CladeMark,
|
|
140
145
|
ResidueMappingProblem,
|
|
141
146
|
ColumnTrackSpec,
|
|
142
147
|
DomainBand,
|
|
@@ -154,14 +159,11 @@ import type {
|
|
|
154
159
|
ResolvedClade,
|
|
155
160
|
ResolvedEncoding,
|
|
156
161
|
ResolvedFeaturePanel,
|
|
162
|
+
ResolvedStripPanel,
|
|
157
163
|
ResolvedHighlight,
|
|
158
164
|
ResolvedSelection,
|
|
159
|
-
TreeOrder,
|
|
160
165
|
TreeRoot,
|
|
161
166
|
ResolvedRowPanel,
|
|
162
|
-
ResolvedStripPanel,
|
|
163
|
-
RowFeaturesSpec,
|
|
164
|
-
RowPanelSpan,
|
|
165
167
|
RowDataInput,
|
|
166
168
|
RowPanelSpec,
|
|
167
169
|
RowResidue,
|
|
@@ -170,17 +172,11 @@ import type {
|
|
|
170
172
|
UnshareableData,
|
|
171
173
|
Viewport,
|
|
172
174
|
} from './types.ts'
|
|
175
|
+
import type { ZoomSnapshot } from './zoomSnapshot.ts'
|
|
173
176
|
import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
|
|
174
177
|
import type { Instance } from '@jbrowse/mobx-state-tree'
|
|
175
178
|
import type { IComputedValue } from 'mobx'
|
|
176
179
|
|
|
177
|
-
function parseTreeText(text: string) {
|
|
178
|
-
if (text.startsWith('BioTreeContainer')) {
|
|
179
|
-
return flatToTree(parseAsn1(text))
|
|
180
|
-
}
|
|
181
|
-
return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text)
|
|
182
|
-
}
|
|
183
|
-
|
|
184
180
|
// The height each kind draws at before the user drags a divider. A text track
|
|
185
181
|
// is absent: it is one alignment row tall and follows rowHeight, so the zoom
|
|
186
182
|
// controls already size it.
|
|
@@ -209,262 +205,6 @@ const ownHeightKey = (id: string) => `own:${id}`
|
|
|
209
205
|
const HELIX_ARC = '#4e79a7'
|
|
210
206
|
const PSEUDOKNOT_ARC = '#e15759'
|
|
211
207
|
|
|
212
|
-
// the fill of a span under a scale that gives its value no color, such as a
|
|
213
|
-
// gene a `map` over gene names leaves out
|
|
214
|
-
const UNSCALED_FEATURE = '#d9d9d9'
|
|
215
|
-
|
|
216
|
-
// the fraction of a span a following one may cover before it takes a lane of
|
|
217
|
-
// its own in a `features` panel
|
|
218
|
-
const laneOverlap = 0.1
|
|
219
|
-
|
|
220
|
-
// a row panel's own width, or a default: the row height, which makes a strip
|
|
221
|
-
// cell square, and a gene neighborhood's width for a features panel. The row
|
|
222
|
-
// height is read only when used, so a panel with a width of its own does not
|
|
223
|
-
// recompute on a vertical zoom
|
|
224
|
-
function rowPanelWidth(panel: RowPanelSpec, view: { rowHeight: number }) {
|
|
225
|
-
return (
|
|
226
|
-
panel.width ??
|
|
227
|
-
(panel.kind === 'features' ? defaultFeaturePanelWidth : view.rowHeight)
|
|
228
|
-
)
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// one row of the row table as the channels read it. A number or a boolean
|
|
232
|
-
// reads as its string, the way a GFF value does, and null or an object drops
|
|
233
|
-
function stringFields(row: unknown) {
|
|
234
|
-
if (typeof row !== 'object' || row === null) {
|
|
235
|
-
return undefined
|
|
236
|
-
}
|
|
237
|
-
const fields: Record<string, string> = {}
|
|
238
|
-
for (const [field, value] of Object.entries(row)) {
|
|
239
|
-
if (typeof value === 'string') {
|
|
240
|
-
fields[field] = value
|
|
241
|
-
} else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
242
|
-
fields[field] = String(value)
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return fields
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// the color each row takes from a field of the row table under a scale
|
|
249
|
-
function colorByRow(
|
|
250
|
-
rowData: Record<string, Record<string, string> | undefined>,
|
|
251
|
-
field: string,
|
|
252
|
-
colorOf: (value: string) => string | undefined,
|
|
253
|
-
) {
|
|
254
|
-
const colors = new Map<string, string>()
|
|
255
|
-
for (const [name, row] of Object.entries(rowData)) {
|
|
256
|
-
const value = row?.[field]
|
|
257
|
-
const color = value === undefined ? undefined : colorOf(value)
|
|
258
|
-
if (color) {
|
|
259
|
-
colors.set(name, color)
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
return colors
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
type StripPanelScale = Omit<ResolvedStripPanel, 'id' | 'width' | 'offsetX'>
|
|
266
|
-
type FeaturePanelScale = Omit<
|
|
267
|
-
ResolvedFeaturePanel,
|
|
268
|
-
'id' | 'width' | 'offsetX' | 'spans'
|
|
269
|
-
>
|
|
270
|
-
|
|
271
|
-
// the fill and outline of every feature in a list: its own GFF `color=` first,
|
|
272
|
-
// then the scale an encoding resolves over one of its fields, then the
|
|
273
|
-
// accession palette
|
|
274
|
-
function featureColorMap(
|
|
275
|
-
annotations: Annotation[],
|
|
276
|
-
encoding:
|
|
277
|
-
| { field: string; colorOf: (value: string) => string | undefined }
|
|
278
|
-
| undefined,
|
|
279
|
-
fillPalette: Record<string, string>,
|
|
280
|
-
) {
|
|
281
|
-
const strokes = new Map<string, string>()
|
|
282
|
-
const strokeOf = (fill: string) => {
|
|
283
|
-
const hit = strokes.get(fill)
|
|
284
|
-
if (hit !== undefined) {
|
|
285
|
-
return hit
|
|
286
|
-
}
|
|
287
|
-
const stroke = outlineColor(fill)
|
|
288
|
-
strokes.set(fill, stroke)
|
|
289
|
-
return stroke
|
|
290
|
-
}
|
|
291
|
-
return new Map(
|
|
292
|
-
annotations.map(annotation => {
|
|
293
|
-
const value = encoding
|
|
294
|
-
? featureField(annotation, encoding.field)
|
|
295
|
-
: undefined
|
|
296
|
-
const fill =
|
|
297
|
-
annotation.color ??
|
|
298
|
-
(value === undefined ? undefined : encoding!.colorOf(value)) ??
|
|
299
|
-
(encoding ? UNSCALED_FEATURE : fillPalette[annotation.accession]!)
|
|
300
|
-
return [annotation, { fill, stroke: strokeOf(fill) }]
|
|
301
|
-
}),
|
|
302
|
-
)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// the text a label channel draws inside each span, for the features carrying
|
|
306
|
-
// the field it names
|
|
307
|
-
function featureLabelMap(annotations: Annotation[], field: string) {
|
|
308
|
-
const labels = new Map<Annotation, string>()
|
|
309
|
-
for (const annotation of annotations) {
|
|
310
|
-
const value = featureField(annotation, field)
|
|
311
|
-
if (value !== undefined) {
|
|
312
|
-
labels.set(annotation, value)
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return labels
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/** what `packDomainLanes` needs to lay a row out, before it has its lane */
|
|
319
|
-
type Unlaned = Omit<RowPanelSpan, 'lane' | 'laneCount'> & {
|
|
320
|
-
startCol: number
|
|
321
|
-
endCol: number
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/**
|
|
325
|
-
* Lanes for `position: "strandpile"`, gggenomes' `position_strandpile`: the
|
|
326
|
-
* forward features stack above the line and the reverse below it, each strand
|
|
327
|
-
* packing on its own. The deepest row on each side sets the grid every row lays
|
|
328
|
-
* out on, so the line between the strands sits at one height down the panel and
|
|
329
|
-
* a reader can scan it. A feature with no strand piles with the forward ones.
|
|
330
|
-
*/
|
|
331
|
-
function strandpileLanes(
|
|
332
|
-
rows: [string, Unlaned[]][],
|
|
333
|
-
): Map<string, RowPanelSpan[]> {
|
|
334
|
-
const piled = rows.map(([name, bands]) => {
|
|
335
|
-
const reverse = packDomainLanes(
|
|
336
|
-
bands.filter(b => b.annotation.strand === -1),
|
|
337
|
-
)
|
|
338
|
-
const forward = packDomainLanes(
|
|
339
|
-
bands.filter(b => b.annotation.strand !== -1),
|
|
340
|
-
)
|
|
341
|
-
return { name, forward, reverse }
|
|
342
|
-
})
|
|
343
|
-
const up = Math.max(0, ...piled.map(p => p.forward[0]?.laneCount ?? 0))
|
|
344
|
-
const down = Math.max(0, ...piled.map(p => p.reverse[0]?.laneCount ?? 0))
|
|
345
|
-
const laneCount = up + down
|
|
346
|
-
return new Map(
|
|
347
|
-
piled.map(({ name, forward, reverse }) => [
|
|
348
|
-
name,
|
|
349
|
-
[
|
|
350
|
-
// forward level 0 is the lane just above the line, stacking upward
|
|
351
|
-
...forward.map(b => ({ ...b, lane: up - 1 - b.lane, laneCount })),
|
|
352
|
-
...reverse.map(b => ({ ...b, lane: up + b.lane, laneCount })),
|
|
353
|
-
],
|
|
354
|
-
]),
|
|
355
|
-
)
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/** one row's spans laid out by the panel's `position` */
|
|
359
|
-
function panelLanes(
|
|
360
|
-
panel: RowFeaturesSpec,
|
|
361
|
-
rows: [string, Unlaned[]][],
|
|
362
|
-
): Map<string, RowPanelSpan[]> {
|
|
363
|
-
return panel.position === 'strandpile'
|
|
364
|
-
? strandpileLanes(rows)
|
|
365
|
-
: new Map(rows.map(([name, bands]) => [name, packDomainLanes(bands)]))
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/**
|
|
369
|
-
* The spans a `features` panel draws, keyed by row name and measured in the
|
|
370
|
-
* panel's own pixels. `column` takes the bands the overlay draws, at the
|
|
371
|
-
* alignment's column width. `position` packs each row's features in its own
|
|
372
|
-
* residue positions, shifted by an `align` transform, and maps the extent they
|
|
373
|
-
* cover across every row onto the panel width, so a row with no alignment
|
|
374
|
-
* still has an x.
|
|
375
|
-
*/
|
|
376
|
-
function featurePanelSpans({
|
|
377
|
-
panel,
|
|
378
|
-
width,
|
|
379
|
-
colWidth,
|
|
380
|
-
domainBands,
|
|
381
|
-
annotationsByRow,
|
|
382
|
-
shifts,
|
|
383
|
-
}: {
|
|
384
|
-
panel: RowFeaturesSpec
|
|
385
|
-
width: number
|
|
386
|
-
colWidth: number
|
|
387
|
-
domainBands: Map<string, DomainBand[]>
|
|
388
|
-
annotationsByRow: Record<string, Annotation[]>
|
|
389
|
-
shifts: Map<string, number> | undefined
|
|
390
|
-
}): Map<string, RowPanelSpan[]> {
|
|
391
|
-
if (panel.x === 'column') {
|
|
392
|
-
return panelLanes(
|
|
393
|
-
panel,
|
|
394
|
-
[...domainBands].map(([name, bands]) => [
|
|
395
|
-
name,
|
|
396
|
-
bands.map(band => ({
|
|
397
|
-
...band,
|
|
398
|
-
xStart: band.startCol * colWidth,
|
|
399
|
-
xEnd: band.endCol * colWidth,
|
|
400
|
-
})),
|
|
401
|
-
]),
|
|
402
|
-
)
|
|
403
|
-
}
|
|
404
|
-
const shifted = Object.entries(annotationsByRow).map(
|
|
405
|
-
([name, annotations]) => {
|
|
406
|
-
const shift = shifts?.get(name) ?? 0
|
|
407
|
-
return [
|
|
408
|
-
name,
|
|
409
|
-
annotations.map(annotation => ({
|
|
410
|
-
annotation,
|
|
411
|
-
start: annotation.start - 1 + shift,
|
|
412
|
-
end: annotation.end + shift,
|
|
413
|
-
})),
|
|
414
|
-
] as const
|
|
415
|
-
},
|
|
416
|
-
)
|
|
417
|
-
let min = Infinity
|
|
418
|
-
let max = -Infinity
|
|
419
|
-
for (const [, features] of shifted) {
|
|
420
|
-
for (const { start, end } of features) {
|
|
421
|
-
min = Math.min(min, start)
|
|
422
|
-
max = Math.max(max, end)
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
// a gene arrow ends where its feature does, so the extent maps onto the whole
|
|
426
|
-
// panel less the pixel the rightmost stroke needs
|
|
427
|
-
const drawable = Math.max(1, width - 1)
|
|
428
|
-
const scale = max > min ? drawable / (max - min) : 0
|
|
429
|
-
// a lane opens where a span covers more than a tenth of the one before it,
|
|
430
|
-
// which keeps the genes of an operon on one lane: adjacent genes commonly
|
|
431
|
-
// share a few bases, and a stop codon overlapping the next start reads as a
|
|
432
|
-
// second lane over the whole row
|
|
433
|
-
return panelLanes(
|
|
434
|
-
panel,
|
|
435
|
-
shifted.map(([name, features]) => [
|
|
436
|
-
name,
|
|
437
|
-
features.map(({ annotation, start, end }) => ({
|
|
438
|
-
annotation,
|
|
439
|
-
xStart: (start - min) * scale,
|
|
440
|
-
xEnd: (end - min) * scale,
|
|
441
|
-
startCol: start,
|
|
442
|
-
endCol: end - (end - start) * laneOverlap,
|
|
443
|
-
})),
|
|
444
|
-
]),
|
|
445
|
-
)
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// a scale over the values a field takes across every feature loaded, so hiding
|
|
449
|
-
// a type in the filter dialog leaves the rest in their colors, and a legend of
|
|
450
|
-
// the values the features drawn carry
|
|
451
|
-
function resolveFeatureScale(
|
|
452
|
-
field: string,
|
|
453
|
-
scale: ScaleSpec | undefined,
|
|
454
|
-
annotations: Annotation[],
|
|
455
|
-
drawn: Annotation[],
|
|
456
|
-
) {
|
|
457
|
-
const valuesOf = (list: Annotation[]) =>
|
|
458
|
-
list.map(a => featureField(a, field)).filter(notEmpty)
|
|
459
|
-
const resolved = resolveScale(scale, valuesOf(annotations))
|
|
460
|
-
const shown = new Set(valuesOf(drawn))
|
|
461
|
-
return {
|
|
462
|
-
field,
|
|
463
|
-
...resolved,
|
|
464
|
-
legend: resolved.legend.filter(entry => shown.has(entry.id)),
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
208
|
const columnTrackBytes = new WeakMap<ColumnTrackSpec, number>()
|
|
469
209
|
|
|
470
210
|
function columnTrackSize(track: ColumnTrackSpec) {
|
|
@@ -524,6 +264,7 @@ export const preservedOnReset = new Set([
|
|
|
524
264
|
'drawNodeBubbles',
|
|
525
265
|
'drawNodeLabels',
|
|
526
266
|
'showTreeOverview',
|
|
267
|
+
'showTreeScaleBar',
|
|
527
268
|
'overviewHeight',
|
|
528
269
|
'turnedOffTracks',
|
|
529
270
|
'trackHeights',
|
|
@@ -531,266 +272,23 @@ export const preservedOnReset = new Set([
|
|
|
531
272
|
'allowedGappyness',
|
|
532
273
|
'subFeatureRows',
|
|
533
274
|
'showDomainLegend',
|
|
275
|
+
'domainFill',
|
|
276
|
+
'embeddedMode',
|
|
534
277
|
])
|
|
535
278
|
|
|
536
279
|
// `turnedOffTracks` holds only the user's explicit choices, so a hidden-by-default
|
|
537
|
-
// track writes nothing into the snapshot
|
|
538
|
-
// `defaultOff`, since the count varies by file: a Pfam seed has a couple of #=GR
|
|
539
|
-
// lines, an Rfam family one per row.
|
|
280
|
+
// track writes nothing into the snapshot
|
|
540
281
|
function trackIsOff(
|
|
541
282
|
turnedOffTracks: { get: (id: string) => boolean | undefined },
|
|
542
283
|
id: string,
|
|
543
|
-
defaultOff?: boolean,
|
|
544
284
|
) {
|
|
545
|
-
return turnedOffTracks.get(id) ??
|
|
285
|
+
return turnedOffTracks.get(id) ?? defaultOffTracks.has(id)
|
|
546
286
|
}
|
|
547
287
|
|
|
548
|
-
// the marks that set tree state from a node rather than drawing
|
|
549
|
-
const seedingMarks = new Set<CladeMark>(['collapse', 'focus', 'rotate'])
|
|
550
|
-
|
|
551
288
|
// shared empty results, so observers don't see a fresh [] as a change
|
|
552
289
|
const noDomains: Annotation[] = []
|
|
553
290
|
const noClades: ResolvedClade[] = []
|
|
554
291
|
|
|
555
|
-
/**
|
|
556
|
-
* Each tip of the full tree in the order the tree draws it, with the on-screen
|
|
557
|
-
* row that shows it: its own, or the row of the collapsed node standing for it
|
|
558
|
-
*/
|
|
559
|
-
function tipRows(root: HierarchyNode<NodeWithIds>) {
|
|
560
|
-
const rows = new Map<string, { order: number; row: number }>()
|
|
561
|
-
let row = 0
|
|
562
|
-
const pushReversed = (
|
|
563
|
-
stack: HierarchyNode<NodeWithIds>[],
|
|
564
|
-
nodes: HierarchyNode<NodeWithIds>[],
|
|
565
|
-
) => {
|
|
566
|
-
for (let i = nodes.length - 1; i >= 0; i--) {
|
|
567
|
-
stack.push(nodes[i]!)
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
const stack = [root]
|
|
571
|
-
while (stack.length > 0) {
|
|
572
|
-
const node = stack.pop()!
|
|
573
|
-
if (node.children?.length) {
|
|
574
|
-
pushReversed(stack, node.children)
|
|
575
|
-
} else {
|
|
576
|
-
const hidden = [node]
|
|
577
|
-
while (hidden.length > 0) {
|
|
578
|
-
const tip = hidden.pop()!
|
|
579
|
-
const kids = tip.children ?? tip._children
|
|
580
|
-
if (kids?.length) {
|
|
581
|
-
pushReversed(hidden, kids)
|
|
582
|
-
} else {
|
|
583
|
-
rows.set(tip.data.name, { order: rows.size, row })
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
row++
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
return rows
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/**
|
|
593
|
-
* The rows a clade covers and the node an `mrca` names, or undefined when the
|
|
594
|
-
* clade does not resolve: a tip name the tree does not have or has twice, or a
|
|
595
|
-
* leaf count `tips` disagrees with. A `range` counts its tips in the order
|
|
596
|
-
* `tips` gives them, so a collapsed clade inside it counts every leaf it
|
|
597
|
-
* stands for. `index` is the memoized leaf pass over the tree.
|
|
598
|
-
*/
|
|
599
|
-
function cladeRows(
|
|
600
|
-
clade: Clade,
|
|
601
|
-
root: HierarchyNode<NodeWithIds>,
|
|
602
|
-
index: Map<string, HierarchyNode<NodeWithIds> | undefined>,
|
|
603
|
-
tips: Map<string, { order: number; row: number }>,
|
|
604
|
-
): { rows: [number, number]; nodeId?: string } | undefined {
|
|
605
|
-
if (clade.range) {
|
|
606
|
-
const [a, b] = clade.range.map(name =>
|
|
607
|
-
index.get(name) ? tips.get(name) : undefined,
|
|
608
|
-
)
|
|
609
|
-
if (a === undefined || b === undefined) {
|
|
610
|
-
return undefined
|
|
611
|
-
}
|
|
612
|
-
const [first, last] = a.order <= b.order ? [a, b] : [b, a]
|
|
613
|
-
return last.order - first.order + 1 === clade.tips
|
|
614
|
-
? { rows: [first.row, last.row] }
|
|
615
|
-
: undefined
|
|
616
|
-
}
|
|
617
|
-
const node = clade.mrca ? mrca(root, clade.mrca, index) : undefined
|
|
618
|
-
if (!node) {
|
|
619
|
-
return undefined
|
|
620
|
-
}
|
|
621
|
-
const nodeTips = leaves(node)
|
|
622
|
-
if (nodeTips.length !== clade.tips) {
|
|
623
|
-
return undefined
|
|
624
|
-
}
|
|
625
|
-
let first = Infinity
|
|
626
|
-
let last = -Infinity
|
|
627
|
-
for (const tip of nodeTips) {
|
|
628
|
-
const row = tips.get(tip.data.name)?.row
|
|
629
|
-
if (row !== undefined) {
|
|
630
|
-
first = Math.min(first, row)
|
|
631
|
-
last = Math.max(last, row)
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
return first <= last
|
|
635
|
-
? { rows: [first, last], nodeId: node.data.id }
|
|
636
|
-
: undefined
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* Every clade that resolves, with its fill color settled, against the rows of
|
|
641
|
-
* `displayed`, the tree as drawn. The tree panel resolves against the
|
|
642
|
-
* displayed rows and the overview against its own, so both take the tree they
|
|
643
|
-
* draw. A `range` record names no node, so the seeding marks, which need one,
|
|
644
|
-
* drop it.
|
|
645
|
-
*/
|
|
646
|
-
function resolveClades(
|
|
647
|
-
clades: Clade[],
|
|
648
|
-
root: HierarchyNode<NodeWithIds>,
|
|
649
|
-
displayed: HierarchyNode<NodeWithIds>,
|
|
650
|
-
): ResolvedClade[] {
|
|
651
|
-
const index = leafIndex(root)
|
|
652
|
-
const tips = tipRows(displayed)
|
|
653
|
-
return clades.flatMap(clade => {
|
|
654
|
-
const resolved = cladeRows(clade, root, index, tips)
|
|
655
|
-
const seeding = seedingMarks.has(clade.mark)
|
|
656
|
-
return resolved && !(seeding && resolved.nodeId === undefined)
|
|
657
|
-
? [
|
|
658
|
-
{
|
|
659
|
-
...resolved,
|
|
660
|
-
mark: clade.mark,
|
|
661
|
-
color: withAlpha(
|
|
662
|
-
clade.color ?? cladeHighlightColor,
|
|
663
|
-
cladeHighlightAlpha,
|
|
664
|
-
),
|
|
665
|
-
markColor: clade.color,
|
|
666
|
-
label: clade.label,
|
|
667
|
-
},
|
|
668
|
-
]
|
|
669
|
-
: []
|
|
670
|
-
})
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
const childOrders: Record<
|
|
674
|
-
TreeOrder,
|
|
675
|
-
((a: HierarchyNode, b: HierarchyNode) => number) | undefined
|
|
676
|
-
> = {
|
|
677
|
-
input: undefined,
|
|
678
|
-
branchLength: (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1),
|
|
679
|
-
ladderize: (a, b) => a.value! - b.value!,
|
|
680
|
-
ladderizeReverse: (a, b) => b.value! - a.value!,
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
/**
|
|
684
|
-
* The tree with the display transforms applied: leaf counts summed, children
|
|
685
|
-
* put in `order` and each `rotated` node's reversed, `showOnly` taken as the
|
|
686
|
-
* new root, and each `collapsed` clade folded. The overview builds the same
|
|
687
|
-
* tree without the focus, so it shows the focused subtree inside the whole.
|
|
688
|
-
*/
|
|
689
|
-
function buildTreeRoot(
|
|
690
|
-
tree: NodeWithIds,
|
|
691
|
-
{
|
|
692
|
-
collapsed,
|
|
693
|
-
rotated,
|
|
694
|
-
order,
|
|
695
|
-
showOnly,
|
|
696
|
-
}: {
|
|
697
|
-
collapsed: readonly string[]
|
|
698
|
-
rotated: readonly string[]
|
|
699
|
-
order: TreeOrder
|
|
700
|
-
showOnly?: string
|
|
701
|
-
},
|
|
702
|
-
) {
|
|
703
|
-
let hier = hierarchy(tree, d => d.children)
|
|
704
|
-
hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1))
|
|
705
|
-
const compare = childOrders[order]
|
|
706
|
-
if (compare) {
|
|
707
|
-
sort(hier, compare)
|
|
708
|
-
}
|
|
709
|
-
if (rotated.length > 0) {
|
|
710
|
-
const flip = new Set(rotated)
|
|
711
|
-
forEachDescendant(hier, n => {
|
|
712
|
-
if (flip.has(n.data.id)) {
|
|
713
|
-
n.children?.reverse()
|
|
714
|
-
}
|
|
715
|
-
})
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if (showOnly) {
|
|
719
|
-
const res = find(hier, n => n.data.id === showOnly)
|
|
720
|
-
if (res) {
|
|
721
|
-
hier = res
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
for (const collapsedId of collapsed) {
|
|
726
|
-
const node = find(hier, n => n.data.id === collapsedId)
|
|
727
|
-
if (node) {
|
|
728
|
-
if (node.children) {
|
|
729
|
-
collapse(node)
|
|
730
|
-
} else if (node.parent?.children) {
|
|
731
|
-
node.parent.children = node.parent.children.filter(
|
|
732
|
-
c => c.data.id !== collapsedId,
|
|
733
|
-
)
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
return hier
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
/**
|
|
742
|
-
* The node whose branch an outgroup roots the tree on: the outgroup's most
|
|
743
|
-
* recent common ancestor, or the rest of the tips' where that ancestor is the
|
|
744
|
-
* root, since the outgroup then straddles the root and the branch above the
|
|
745
|
-
* other side is the same branch of the unrooted tree.
|
|
746
|
-
*/
|
|
747
|
-
function outgroupNode(root: HierarchyNode, outgroup: string[]) {
|
|
748
|
-
const index = leafIndex(root)
|
|
749
|
-
const node = mrca(root, outgroup, index)
|
|
750
|
-
if (node !== root) {
|
|
751
|
-
return node
|
|
752
|
-
}
|
|
753
|
-
const out = new Set(outgroup)
|
|
754
|
-
const rest = leaves(root)
|
|
755
|
-
.map(n => n.data.name)
|
|
756
|
-
.filter(name => !out.has(name))
|
|
757
|
-
const other = mrca(root, rest, index)
|
|
758
|
-
return other === root ? undefined : other
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
/**
|
|
762
|
-
* `tree` rerooted as `treeRoot` asks, with fresh path ids. The ids of the tree
|
|
763
|
-
* as written become names on unnamed nodes, and a rerooting would carry them
|
|
764
|
-
* along as labels, so they come off first.
|
|
765
|
-
*/
|
|
766
|
-
function rootTree(tree: NodeWithIds, treeRoot: TreeRoot) {
|
|
767
|
-
const plain = new Map<NodeWithIds, TreeNode>()
|
|
768
|
-
for (const node of preorder(tree).reverse()) {
|
|
769
|
-
plain.set(node, {
|
|
770
|
-
name: node.name === node.id ? undefined : node.name,
|
|
771
|
-
length: node.length,
|
|
772
|
-
children: node.children.map(c => plain.get(c)!),
|
|
773
|
-
})
|
|
774
|
-
}
|
|
775
|
-
const input = plain.get(tree)!
|
|
776
|
-
if (treeRoot === 'midpoint') {
|
|
777
|
-
return generateNodeIds(midpointRoot(input))
|
|
778
|
-
}
|
|
779
|
-
const target = outgroupNode(
|
|
780
|
-
hierarchy(tree, d => d.children),
|
|
781
|
-
treeRoot.outgroup,
|
|
782
|
-
)
|
|
783
|
-
return target
|
|
784
|
-
? generateNodeIds(rerootTree(input, plain.get(target.data)!))
|
|
785
|
-
: tree
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
// the inclusive tip indices a node covers, from its xMin/xMax in a layout one
|
|
789
|
-
// row per tip
|
|
790
|
-
function tipRange(node: HierarchyNode): [number, number] {
|
|
791
|
-
return [Math.round(node.xMin! - 0.5), Math.round(node.xMax! - 0.5)]
|
|
792
|
-
}
|
|
793
|
-
|
|
794
292
|
// the channels reading the feature table; every other channel reads rowData
|
|
795
293
|
const featureChannels = new Set<EncodingChannel>([
|
|
796
294
|
'featureFill',
|
|
@@ -821,6 +319,31 @@ function sameLength(segment: ResidueSegment) {
|
|
|
821
319
|
)
|
|
822
320
|
}
|
|
823
321
|
|
|
322
|
+
// A zoom that passes the default cell size stops on it, and one that starts on
|
|
323
|
+
// it holds there until the wheel has gathered `zoomDetent` worth of scale, so
|
|
324
|
+
// a zoom in or out finds its way back to the default
|
|
325
|
+
const zoomDetent = Math.log(1.3)
|
|
326
|
+
|
|
327
|
+
function detentZoom(
|
|
328
|
+
value: number,
|
|
329
|
+
scale: number,
|
|
330
|
+
home: number,
|
|
331
|
+
held: number,
|
|
332
|
+
min: number,
|
|
333
|
+
canHold: boolean,
|
|
334
|
+
) {
|
|
335
|
+
if (value === home && canHold) {
|
|
336
|
+
const total = held + Math.log(scale)
|
|
337
|
+
return Math.abs(total) < zoomDetent
|
|
338
|
+
? { value, held: total }
|
|
339
|
+
: { value: clamp(home * Math.exp(total), min, maxCellSize), held: 0 }
|
|
340
|
+
}
|
|
341
|
+
const next = clamp(value * scale, min, maxCellSize)
|
|
342
|
+
return (value - home) * (next - home) < 0
|
|
343
|
+
? { value: home, held: 0 }
|
|
344
|
+
: { value: next, held: 0 }
|
|
345
|
+
}
|
|
346
|
+
|
|
824
347
|
// fit() divides the viewport by the row or column count and multiplies back, so
|
|
825
348
|
// an exact fit can land a fraction of a pixel over. A plain `>` would then show
|
|
826
349
|
// a scrollbar that shrinks the viewport and leaves a gap.
|
|
@@ -855,19 +378,11 @@ function sharedValue(values: (string | undefined)[]) {
|
|
|
855
378
|
: undefined
|
|
856
379
|
}
|
|
857
380
|
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
const node = stack.pop()!
|
|
864
|
-
order.push(node)
|
|
865
|
-
for (const child of node.children) {
|
|
866
|
-
stack.push(child)
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
return order
|
|
870
|
-
}
|
|
381
|
+
type StripPanelScale = Omit<ResolvedStripPanel, 'id' | 'width' | 'offsetX'>
|
|
382
|
+
type FeaturePanelScale = Omit<
|
|
383
|
+
ResolvedFeaturePanel,
|
|
384
|
+
'id' | 'width' | 'offsetX' | 'spans'
|
|
385
|
+
>
|
|
871
386
|
|
|
872
387
|
/**
|
|
873
388
|
* #stateModel MsaView
|
|
@@ -913,6 +428,13 @@ function stateModelFactory({
|
|
|
913
428
|
* #property
|
|
914
429
|
*/
|
|
915
430
|
showDomains: stripDefault(types.boolean, defaultShowDomains),
|
|
431
|
+
/**
|
|
432
|
+
* #property
|
|
433
|
+
* whether shown domains fill the cells they cover. Off, the residue
|
|
434
|
+
* scheme keeps the cells and each domain draws as a bar under its
|
|
435
|
+
* row. See `cellFill`.
|
|
436
|
+
*/
|
|
437
|
+
domainFill: stripDefault(types.boolean, true),
|
|
916
438
|
/**
|
|
917
439
|
* #property
|
|
918
440
|
* whether the domain legend is expanded. The legend floats over the
|
|
@@ -944,6 +466,13 @@ function stateModelFactory({
|
|
|
944
466
|
*/
|
|
945
467
|
drawMsaLetters: stripDefault(types.boolean, defaultDrawMsaLetters),
|
|
946
468
|
|
|
469
|
+
/**
|
|
470
|
+
* #property
|
|
471
|
+
* draws the toolbar's menus and Settings as icons without text, for a
|
|
472
|
+
* host with little room, such as a JBrowse view
|
|
473
|
+
*/
|
|
474
|
+
embeddedMode: stripDefault(types.boolean, false),
|
|
475
|
+
|
|
947
476
|
/**
|
|
948
477
|
* #property
|
|
949
478
|
* zoom in/out on plain mouse-wheel without holding ctrl
|
|
@@ -1096,10 +625,10 @@ function stateModelFactory({
|
|
|
1096
625
|
|
|
1097
626
|
/**
|
|
1098
627
|
* #property
|
|
1099
|
-
* the user's
|
|
1100
|
-
* accession with the value meaning "off", like
|
|
1101
|
-
* untouched accession is absent and
|
|
1102
|
-
* only with the user's filters
|
|
628
|
+
* the user's departures from each annotation accession's default,
|
|
629
|
+
* keyed by accession with the value meaning "off", like
|
|
630
|
+
* `turnedOffTracks`. An untouched accession is absent and takes its
|
|
631
|
+
* default, so the shared URL grows only with the user's filters
|
|
1103
632
|
*/
|
|
1104
633
|
turnedOffFeatures: stripDefault(types.map(types.boolean), {}),
|
|
1105
634
|
/**
|
|
@@ -1180,6 +709,18 @@ function stateModelFactory({
|
|
|
1180
709
|
* #volatile
|
|
1181
710
|
*/
|
|
1182
711
|
headerHeight: 0,
|
|
712
|
+
/**
|
|
713
|
+
* #volatile
|
|
714
|
+
* whether the settings panel docks beside the alignment, taking
|
|
715
|
+
* `settingsPanelWidth` out of the width the view lays out in
|
|
716
|
+
*/
|
|
717
|
+
settingsOpen: false,
|
|
718
|
+
/**
|
|
719
|
+
* #volatile
|
|
720
|
+
* the log scale a wheel zoom has gathered against the detent at the
|
|
721
|
+
* default column width and row height, per axis
|
|
722
|
+
*/
|
|
723
|
+
zoomHeld: { col: 0, row: 0 },
|
|
1183
724
|
/**
|
|
1184
725
|
* #volatile
|
|
1185
726
|
* leaves the toolbar out, for a host drawing its own controls. Kept out
|
|
@@ -1203,6 +744,13 @@ function stateModelFactory({
|
|
|
1203
744
|
highResScaleFactor:
|
|
1204
745
|
typeof window === 'undefined' ? 1 : window.devicePixelRatio,
|
|
1205
746
|
|
|
747
|
+
/**
|
|
748
|
+
* #volatile
|
|
749
|
+
* the cell size and block grid the alignment last drew at, held from a
|
|
750
|
+
* wheel-zoom frame until the gesture settles. MSACanvas stretches those
|
|
751
|
+
* blocks to the current cell size with one CSS transform
|
|
752
|
+
*/
|
|
753
|
+
zoomSnapshot: undefined as ZoomSnapshot | undefined,
|
|
1206
754
|
/**
|
|
1207
755
|
* #volatile
|
|
1208
756
|
*/
|
|
@@ -1365,6 +913,12 @@ function stateModelFactory({
|
|
|
1365
913
|
setWidth(arg: number) {
|
|
1366
914
|
self.volatileWidth = arg
|
|
1367
915
|
},
|
|
916
|
+
/**
|
|
917
|
+
* #action
|
|
918
|
+
*/
|
|
919
|
+
setSettingsOpen(arg: boolean) {
|
|
920
|
+
self.settingsOpen = arg
|
|
921
|
+
},
|
|
1368
922
|
/**
|
|
1369
923
|
* #action
|
|
1370
924
|
* update the canvas scale factor when the device pixel ratio changes
|
|
@@ -1508,6 +1062,23 @@ function stateModelFactory({
|
|
|
1508
1062
|
setShowDomains(arg: boolean) {
|
|
1509
1063
|
self.showDomains = arg
|
|
1510
1064
|
},
|
|
1065
|
+
/**
|
|
1066
|
+
* #action
|
|
1067
|
+
*/
|
|
1068
|
+
setDomainFill(arg: boolean) {
|
|
1069
|
+
self.domainFill = arg
|
|
1070
|
+
},
|
|
1071
|
+
/**
|
|
1072
|
+
* #action
|
|
1073
|
+
* hand the cell fill to the residue scheme, the domains, or neither,
|
|
1074
|
+
* which colors the letters instead
|
|
1075
|
+
*/
|
|
1076
|
+
setCellFill(mode: 'residue' | 'domain' | 'none') {
|
|
1077
|
+
self.setBgColor(mode !== 'none')
|
|
1078
|
+
if (mode !== 'none') {
|
|
1079
|
+
self.domainFill = mode === 'domain'
|
|
1080
|
+
}
|
|
1081
|
+
},
|
|
1511
1082
|
/**
|
|
1512
1083
|
* #action
|
|
1513
1084
|
* expand or collapse the domain legend that floats over the alignment
|
|
@@ -1536,6 +1107,7 @@ function stateModelFactory({
|
|
|
1536
1107
|
*/
|
|
1537
1108
|
setRowHeight(n: number) {
|
|
1538
1109
|
self.rowHeight = n
|
|
1110
|
+
self.zoomHeld = { ...self.zoomHeld, row: 0 }
|
|
1539
1111
|
},
|
|
1540
1112
|
|
|
1541
1113
|
/**
|
|
@@ -1544,6 +1116,7 @@ function stateModelFactory({
|
|
|
1544
1116
|
*/
|
|
1545
1117
|
setColWidth(n: number) {
|
|
1546
1118
|
self.colWidth = n
|
|
1119
|
+
self.zoomHeld = { ...self.zoomHeld, col: 0 }
|
|
1547
1120
|
},
|
|
1548
1121
|
|
|
1549
1122
|
/**
|
|
@@ -1715,22 +1288,7 @@ function stateModelFactory({
|
|
|
1715
1288
|
self.allowedGappyness < 100)
|
|
1716
1289
|
)
|
|
1717
1290
|
},
|
|
1718
|
-
|
|
1719
|
-
* #getter
|
|
1720
|
-
* the annotations the overlay, the legend and the encodings draw: the
|
|
1721
|
-
* GFF's, then the `features` layer's
|
|
1722
|
-
*/
|
|
1723
|
-
get allAnnotations(): Annotation[] {
|
|
1724
|
-
return self.features.length > 0
|
|
1725
|
-
? [...self.annotations, ...featuresToAnnotations(self.features)]
|
|
1726
|
-
: self.annotations
|
|
1727
|
-
},
|
|
1728
|
-
/**
|
|
1729
|
-
* #getter
|
|
1730
|
-
*/
|
|
1731
|
-
get actuallyShowDomains() {
|
|
1732
|
-
return self.showDomains && this.allAnnotations.length > 0
|
|
1733
|
-
},
|
|
1291
|
+
|
|
1734
1292
|
/**
|
|
1735
1293
|
* #getter
|
|
1736
1294
|
* whether the host restores the loaded documents outside the snapshot.
|
|
@@ -1755,7 +1313,7 @@ function stateModelFactory({
|
|
|
1755
1313
|
if (self.volatileWidth === undefined) {
|
|
1756
1314
|
throw new Error('not initialized')
|
|
1757
1315
|
}
|
|
1758
|
-
return self.volatileWidth
|
|
1316
|
+
return self.volatileWidth - (self.settingsOpen ? settingsPanelWidth : 0)
|
|
1759
1317
|
},
|
|
1760
1318
|
}))
|
|
1761
1319
|
.views(self => ({
|
|
@@ -1805,7 +1363,7 @@ function stateModelFactory({
|
|
|
1805
1363
|
return !!this.tree.noTree
|
|
1806
1364
|
},
|
|
1807
1365
|
get noDomains() {
|
|
1808
|
-
return
|
|
1366
|
+
return this.allAnnotations.length === 0
|
|
1809
1367
|
},
|
|
1810
1368
|
|
|
1811
1369
|
/**
|
|
@@ -1914,6 +1472,27 @@ function stateModelFactory({
|
|
|
1914
1472
|
? parseMSA(text, self.currentAlignment, self.msaFormat)
|
|
1915
1473
|
: null
|
|
1916
1474
|
},
|
|
1475
|
+
/**
|
|
1476
|
+
* #getter
|
|
1477
|
+
* the annotations the overlay, the legend and the encodings draw: the
|
|
1478
|
+
* GFF's, the `features` layer's, then the file's own per-row lines
|
|
1479
|
+
*/
|
|
1480
|
+
get allAnnotations(): Annotation[] {
|
|
1481
|
+
const inline = this.MSA?.annotations ?? []
|
|
1482
|
+
return self.features.length > 0 || inline.length > 0
|
|
1483
|
+
? [
|
|
1484
|
+
...self.annotations,
|
|
1485
|
+
...featuresToAnnotations(self.features),
|
|
1486
|
+
...inline,
|
|
1487
|
+
]
|
|
1488
|
+
: self.annotations
|
|
1489
|
+
},
|
|
1490
|
+
/**
|
|
1491
|
+
* #getter
|
|
1492
|
+
*/
|
|
1493
|
+
get actuallyShowDomains() {
|
|
1494
|
+
return self.showDomains && this.allAnnotations.length > 0
|
|
1495
|
+
},
|
|
1917
1496
|
/**
|
|
1918
1497
|
* #getter
|
|
1919
1498
|
*/
|
|
@@ -2507,6 +2086,22 @@ function stateModelFactory({
|
|
|
2507
2086
|
)
|
|
2508
2087
|
},
|
|
2509
2088
|
|
|
2089
|
+
/**
|
|
2090
|
+
* #getter
|
|
2091
|
+
* Pre-computed similarity shades per column, for blosum62_dynamic.
|
|
2092
|
+
* Returns a map of letter -> color for each column. Amino acids shade by
|
|
2093
|
+
* BLOSUM62 similarity to the rest of the column, and an alignment of
|
|
2094
|
+
* nucleotides by identity, since the matrix names amino acids.
|
|
2095
|
+
*/
|
|
2096
|
+
get colSimilarity() {
|
|
2097
|
+
const { colStats, sequenceType } = this
|
|
2098
|
+
const columnColors =
|
|
2099
|
+
sequenceType === 'amino' ? blosum62ColumnColors : identityColumnColors
|
|
2100
|
+
return Array.from({ length: colStats.numColumns }, (_, col) =>
|
|
2101
|
+
columnColors(colStats, col),
|
|
2102
|
+
)
|
|
2103
|
+
},
|
|
2104
|
+
|
|
2510
2105
|
/**
|
|
2511
2106
|
* #getter
|
|
2512
2107
|
* Conservation score per column using Shannon entropy (biojs-msa style).
|
|
@@ -2919,8 +2514,7 @@ function stateModelFactory({
|
|
|
2919
2514
|
*/
|
|
2920
2515
|
get turnedOnTracks() {
|
|
2921
2516
|
return this.tracks.filter(
|
|
2922
|
-
f =>
|
|
2923
|
-
!trackIsOff(self.turnedOffTracks, f.model.id, f.model.defaultOff),
|
|
2517
|
+
f => !trackIsOff(self.turnedOffTracks, f.model.id),
|
|
2924
2518
|
)
|
|
2925
2519
|
},
|
|
2926
2520
|
|
|
@@ -3250,7 +2844,11 @@ function stateModelFactory({
|
|
|
3250
2844
|
if (mapping.structure.id !== structureId) {
|
|
3251
2845
|
continue
|
|
3252
2846
|
}
|
|
3253
|
-
if (
|
|
2847
|
+
if (
|
|
2848
|
+
asymId !== undefined &&
|
|
2849
|
+
mapping.structure.asymId !== undefined &&
|
|
2850
|
+
mapping.structure.asymId !== asymId
|
|
2851
|
+
) {
|
|
3254
2852
|
continue
|
|
3255
2853
|
}
|
|
3256
2854
|
const segment = mapping.segments.find(
|
|
@@ -3270,10 +2868,14 @@ function stateModelFactory({
|
|
|
3270
2868
|
.views(self => ({
|
|
3271
2869
|
/**
|
|
3272
2870
|
* #getter
|
|
3273
|
-
* the branch-length scale bar over the tree, undefined
|
|
3274
|
-
* or when the tree area is
|
|
2871
|
+
* the branch-length scale bar over the tree, undefined while
|
|
2872
|
+
* `showTreeScaleBar` is off, in cladogram mode, or when the tree area is
|
|
2873
|
+
* too narrow for one
|
|
3275
2874
|
*/
|
|
3276
2875
|
get treeScaleBar() {
|
|
2876
|
+
if (!self.showTreeScaleBar) {
|
|
2877
|
+
return undefined
|
|
2878
|
+
}
|
|
3277
2879
|
return scaleBarLength(
|
|
3278
2880
|
self.pxPerBranchLength,
|
|
3279
2881
|
self.effectiveTreeAreaWidth - self.marginLeft * 2,
|
|
@@ -3329,11 +2931,43 @@ function stateModelFactory({
|
|
|
3329
2931
|
}
|
|
3330
2932
|
return types
|
|
3331
2933
|
},
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
2934
|
+
/**
|
|
2935
|
+
* accessions hidden until the user turns them on, such as a Stockholm
|
|
2936
|
+
* file's #=GR lines
|
|
2937
|
+
*/
|
|
2938
|
+
get defaultOffAccessions() {
|
|
2939
|
+
return new Set(
|
|
2940
|
+
self.allAnnotations.filter(a => a.defaultOff).map(a => a.accession),
|
|
2941
|
+
)
|
|
2942
|
+
},
|
|
2943
|
+
/**
|
|
2944
|
+
* #method
|
|
2945
|
+
*/
|
|
2946
|
+
featureOff(accession: string) {
|
|
2947
|
+
return (
|
|
2948
|
+
self.turnedOffFeatures.get(accession) ??
|
|
2949
|
+
this.defaultOffAccessions.has(accession)
|
|
3335
2950
|
)
|
|
3336
2951
|
},
|
|
2952
|
+
get filteredAnnotations() {
|
|
2953
|
+
return self.allAnnotations.filter(r => !this.featureOff(r.accession))
|
|
2954
|
+
},
|
|
2955
|
+
/**
|
|
2956
|
+
* #getter
|
|
2957
|
+
* what colors the alignment's cells: the residue scheme, the domains
|
|
2958
|
+
* covering them, or nothing, which leaves the scheme to the letters.
|
|
2959
|
+
* Domains own the fill only while some are drawn, so a file whose
|
|
2960
|
+
* annotations all start hidden keeps its residue colors
|
|
2961
|
+
*/
|
|
2962
|
+
get cellFill(): 'residue' | 'domain' | 'none' {
|
|
2963
|
+
return !self.bgColor
|
|
2964
|
+
? 'none'
|
|
2965
|
+
: self.actuallyShowDomains &&
|
|
2966
|
+
self.domainFill &&
|
|
2967
|
+
this.filteredAnnotations.length > 0
|
|
2968
|
+
? 'domain'
|
|
2969
|
+
: 'residue'
|
|
2970
|
+
},
|
|
3337
2971
|
get annotationsByRow() {
|
|
3338
2972
|
const { rowNamesSet } = self
|
|
3339
2973
|
return groupBy(this.filteredAnnotations, r =>
|
|
@@ -3412,6 +3046,12 @@ function stateModelFactory({
|
|
|
3412
3046
|
self.blocksX.map(bx => [bx, by] as const),
|
|
3413
3047
|
)
|
|
3414
3048
|
},
|
|
3049
|
+
/**
|
|
3050
|
+
* #getter
|
|
3051
|
+
*/
|
|
3052
|
+
get wheelZooming() {
|
|
3053
|
+
return self.zoomSnapshot !== undefined
|
|
3054
|
+
},
|
|
3415
3055
|
|
|
3416
3056
|
/**
|
|
3417
3057
|
* #getter
|
|
@@ -3637,19 +3277,35 @@ function stateModelFactory({
|
|
|
3637
3277
|
transaction(() => {
|
|
3638
3278
|
const colInView = (-self.scrollX + offsetX) / self.colWidth
|
|
3639
3279
|
const rowInView = (-self.scrollY + offsetY) / self.rowHeight
|
|
3280
|
+
// zooming both axes, an axis holds at the default only while the
|
|
3281
|
+
// other sits there too, or one would hold while the other grows
|
|
3282
|
+
const canHold =
|
|
3283
|
+
axis !== 'both' ||
|
|
3284
|
+
(self.colWidth === defaultColWidth &&
|
|
3285
|
+
self.rowHeight === defaultRowHeight)
|
|
3640
3286
|
if (axis !== 'vertical') {
|
|
3641
|
-
|
|
3642
|
-
self.colWidth
|
|
3287
|
+
const { value, held } = detentZoom(
|
|
3288
|
+
self.colWidth,
|
|
3289
|
+
scaleFactor,
|
|
3290
|
+
defaultColWidth,
|
|
3291
|
+
self.zoomHeld.col,
|
|
3643
3292
|
minColWidth,
|
|
3644
|
-
|
|
3293
|
+
canHold,
|
|
3645
3294
|
)
|
|
3295
|
+
self.colWidth = value
|
|
3296
|
+
self.zoomHeld = { ...self.zoomHeld, col: held }
|
|
3646
3297
|
}
|
|
3647
3298
|
if (axis !== 'horizontal') {
|
|
3648
|
-
|
|
3649
|
-
self.rowHeight
|
|
3299
|
+
const { value, held } = detentZoom(
|
|
3300
|
+
self.rowHeight,
|
|
3301
|
+
scaleFactor,
|
|
3302
|
+
defaultRowHeight,
|
|
3303
|
+
self.zoomHeld.row,
|
|
3650
3304
|
minRowHeight,
|
|
3651
|
-
|
|
3305
|
+
canHold,
|
|
3652
3306
|
)
|
|
3307
|
+
self.rowHeight = value
|
|
3308
|
+
self.zoomHeld = { ...self.zoomHeld, row: held }
|
|
3653
3309
|
}
|
|
3654
3310
|
self.scrollX = clamp(
|
|
3655
3311
|
offsetX - colInView * self.colWidth,
|
|
@@ -3670,6 +3326,25 @@ function stateModelFactory({
|
|
|
3670
3326
|
)
|
|
3671
3327
|
})
|
|
3672
3328
|
},
|
|
3329
|
+
/**
|
|
3330
|
+
* #action
|
|
3331
|
+
* zoomToPos for one wheel or pinch frame. The alignment keeps the blocks
|
|
3332
|
+
* it drew at the start of the gesture, scaled, and redraws them only once
|
|
3333
|
+
* they stop covering the viewport; endWheelZoom redraws them sharp
|
|
3334
|
+
*/
|
|
3335
|
+
wheelZoomToPos(scaleFactor: number, offsetX: number, offsetY: number) {
|
|
3336
|
+
self.zoomSnapshot ??= takeZoomSnapshot(self, false)
|
|
3337
|
+
this.zoomToPos(scaleFactor, offsetX, offsetY, self.wheelZoomAxis)
|
|
3338
|
+
if (!zoomSnapshotCovers(self, self.zoomSnapshot)) {
|
|
3339
|
+
self.zoomSnapshot = takeZoomSnapshot(self, true)
|
|
3340
|
+
}
|
|
3341
|
+
},
|
|
3342
|
+
/**
|
|
3343
|
+
* #action
|
|
3344
|
+
*/
|
|
3345
|
+
endWheelZoom() {
|
|
3346
|
+
self.zoomSnapshot = undefined
|
|
3347
|
+
},
|
|
3673
3348
|
/**
|
|
3674
3349
|
* #action
|
|
3675
3350
|
*/
|
|
@@ -3754,11 +3429,22 @@ function stateModelFactory({
|
|
|
3754
3429
|
* #action
|
|
3755
3430
|
*/
|
|
3756
3431
|
toggleTrack(id: string) {
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3432
|
+
self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id))
|
|
3433
|
+
},
|
|
3434
|
+
/**
|
|
3435
|
+
* #action
|
|
3436
|
+
* show or hide several tracks at once, as the track selector's Show all
|
|
3437
|
+
* and Hide all do. A choice matching the track's default is dropped, so
|
|
3438
|
+
* it stays out of the shared URL
|
|
3439
|
+
*/
|
|
3440
|
+
setTracksShown(ids: string[], shown: boolean) {
|
|
3441
|
+
for (const id of ids) {
|
|
3442
|
+
if (defaultOffTracks.has(id) === !shown) {
|
|
3443
|
+
self.turnedOffTracks.delete(id)
|
|
3444
|
+
} else {
|
|
3445
|
+
self.turnedOffTracks.set(id, !shown)
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3762
3448
|
},
|
|
3763
3449
|
/**
|
|
3764
3450
|
* #action
|
|
@@ -3923,7 +3609,7 @@ function stateModelFactory({
|
|
|
3923
3609
|
*/
|
|
3924
3610
|
get visibleDomainTypes() {
|
|
3925
3611
|
return this.categoricalDomainTypes
|
|
3926
|
-
.filter(d => !self.
|
|
3612
|
+
.filter(d => !self.featureOff(d.accession))
|
|
3927
3613
|
.toSorted((a, b) => a.start - b.start)
|
|
3928
3614
|
},
|
|
3929
3615
|
|
|
@@ -3992,16 +3678,16 @@ function stateModelFactory({
|
|
|
3992
3678
|
/**
|
|
3993
3679
|
* #getter
|
|
3994
3680
|
* whether the overlay marks each domain with a bar under its row instead
|
|
3995
|
-
* of filling the row behind the letters
|
|
3996
|
-
*
|
|
3997
|
-
*
|
|
3681
|
+
* of filling the row behind the letters: whenever the domains do not own
|
|
3682
|
+
* the cell fill (`cellFill`), a filled box would paint over the residue
|
|
3683
|
+
* colors the user picked. Sub-row layout already stacks
|
|
3998
3684
|
* the boxes clear of the letters, and with the letters too small to draw
|
|
3999
3685
|
* the filled box is the only thing left to read.
|
|
4000
3686
|
*/
|
|
4001
3687
|
get domainUnderline() {
|
|
4002
3688
|
return (
|
|
4003
3689
|
self.actuallyShowDomains &&
|
|
4004
|
-
|
|
3690
|
+
self.cellFill !== 'domain' &&
|
|
4005
3691
|
!self.subFeatureRows &&
|
|
4006
3692
|
self.showMsaLetters
|
|
4007
3693
|
)
|
|
@@ -4523,6 +4209,12 @@ function stateModelFactory({
|
|
|
4523
4209
|
setHideHeader(arg: boolean) {
|
|
4524
4210
|
self.hideHeader = arg
|
|
4525
4211
|
},
|
|
4212
|
+
/**
|
|
4213
|
+
* #action
|
|
4214
|
+
*/
|
|
4215
|
+
setEmbeddedMode(arg: boolean) {
|
|
4216
|
+
self.embeddedMode = arg
|
|
4217
|
+
},
|
|
4526
4218
|
/**
|
|
4527
4219
|
* #action
|
|
4528
4220
|
*/
|
|
@@ -4602,14 +4294,15 @@ function stateModelFactory({
|
|
|
4602
4294
|
},
|
|
4603
4295
|
/**
|
|
4604
4296
|
* #action
|
|
4605
|
-
* show or hide an annotation type. Only
|
|
4606
|
-
* `turnedOffFeatures`
|
|
4297
|
+
* show or hide an annotation type. Only a choice that differs from the
|
|
4298
|
+
* type's default is recorded; see `turnedOffFeatures`
|
|
4607
4299
|
*/
|
|
4608
4300
|
setFilter(accession: string, shown: boolean) {
|
|
4609
|
-
|
|
4610
|
-
|
|
4301
|
+
// only a departure from the accession's default is recorded
|
|
4302
|
+
if (shown === self.defaultOffAccessions.has(accession)) {
|
|
4303
|
+
self.turnedOffFeatures.set(accession, !shown)
|
|
4611
4304
|
} else {
|
|
4612
|
-
self.turnedOffFeatures.
|
|
4305
|
+
self.turnedOffFeatures.delete(accession)
|
|
4613
4306
|
}
|
|
4614
4307
|
},
|
|
4615
4308
|
|
|
@@ -4618,23 +4311,28 @@ function stateModelFactory({
|
|
|
4618
4311
|
*/
|
|
4619
4312
|
fit() {
|
|
4620
4313
|
// two passes: each direction's viewport depends on whether the other
|
|
4621
|
-
// overflows (the minimap takes height, the vertical scrollbar width)
|
|
4314
|
+
// overflows (the minimap takes height, the vertical scrollbar width).
|
|
4315
|
+
// Fit shrinks an alignment into the window but never grows one past
|
|
4316
|
+
// the default cell size, which would stretch a small alignment's rows
|
|
4317
|
+
// and columns out of proportion
|
|
4622
4318
|
transaction(() => {
|
|
4623
4319
|
for (let pass = 0; pass < 2; pass++) {
|
|
4624
|
-
this.fitHorizontally()
|
|
4625
|
-
this.fitVertically()
|
|
4320
|
+
this.fitHorizontally(defaultColWidth)
|
|
4321
|
+
this.fitVertically(defaultRowHeight)
|
|
4626
4322
|
}
|
|
4627
4323
|
})
|
|
4628
4324
|
},
|
|
4629
4325
|
/**
|
|
4630
4326
|
* #action
|
|
4631
4327
|
*/
|
|
4632
|
-
fitVertically() {
|
|
4328
|
+
fitVertically(maxRowHeight = maxCellSize) {
|
|
4633
4329
|
if (self.numRows > 0) {
|
|
4634
|
-
self.
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4330
|
+
self.setRowHeight(
|
|
4331
|
+
clamp(
|
|
4332
|
+
self.msaAreaHeight / self.numRows,
|
|
4333
|
+
minRowHeight,
|
|
4334
|
+
maxRowHeight,
|
|
4335
|
+
),
|
|
4638
4336
|
)
|
|
4639
4337
|
}
|
|
4640
4338
|
self.scrollY = 0
|
|
@@ -4651,10 +4349,12 @@ function stateModelFactory({
|
|
|
4651
4349
|
return
|
|
4652
4350
|
}
|
|
4653
4351
|
transaction(() => {
|
|
4654
|
-
self.
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4352
|
+
self.setColWidth(
|
|
4353
|
+
clamp(
|
|
4354
|
+
self.msaCanvasWidth / (span.endCol - span.startCol + 1),
|
|
4355
|
+
minColWidth,
|
|
4356
|
+
maxCellSize,
|
|
4357
|
+
),
|
|
4658
4358
|
)
|
|
4659
4359
|
self.scrollX = clamp(
|
|
4660
4360
|
-span.startCol * self.colWidth,
|
|
@@ -4700,10 +4400,12 @@ function stateModelFactory({
|
|
|
4700
4400
|
const { rowRuns } = resolvedSelection
|
|
4701
4401
|
const first = rowRuns[0]![0]
|
|
4702
4402
|
const last = rowRuns.at(-1)![1]
|
|
4703
|
-
self.
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4403
|
+
self.setRowHeight(
|
|
4404
|
+
clamp(
|
|
4405
|
+
self.msaAreaHeight / (last - first + 1),
|
|
4406
|
+
minRowHeight,
|
|
4407
|
+
maxCellSize,
|
|
4408
|
+
),
|
|
4707
4409
|
)
|
|
4708
4410
|
self.scrollY = clamp(-first * self.rowHeight, self.maxScrollY, 0)
|
|
4709
4411
|
}
|
|
@@ -4744,13 +4446,15 @@ function stateModelFactory({
|
|
|
4744
4446
|
/**
|
|
4745
4447
|
* #action
|
|
4746
4448
|
*/
|
|
4747
|
-
fitHorizontally() {
|
|
4449
|
+
fitHorizontally(maxColWidth = maxCellSize) {
|
|
4748
4450
|
if (self.numColumns > 0) {
|
|
4749
4451
|
// msaCanvasWidth excludes the vertical scrollbar's 20px
|
|
4750
|
-
self.
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4452
|
+
self.setColWidth(
|
|
4453
|
+
clamp(
|
|
4454
|
+
self.msaCanvasWidth / self.numColumns,
|
|
4455
|
+
minColWidth,
|
|
4456
|
+
maxColWidth,
|
|
4457
|
+
),
|
|
4754
4458
|
)
|
|
4755
4459
|
}
|
|
4756
4460
|
self.scrollX = 0
|
|
@@ -4761,21 +4465,18 @@ function stateModelFactory({
|
|
|
4761
4465
|
self.setHighlightedColumns(self.highlightColumns)
|
|
4762
4466
|
}
|
|
4763
4467
|
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
// `
|
|
4767
|
-
//
|
|
4768
|
-
//
|
|
4769
|
-
//
|
|
4770
|
-
//
|
|
4771
|
-
// `dataInitialized` is true once the MSA alone has loaded, when the
|
|
4772
|
-
// tree is still the flat stub, so a tree filehandle holds the seeding
|
|
4773
|
-
// until its text lands.
|
|
4468
|
+
const awaitingTree = () => !!self.treeFilehandle && !self.data.tree
|
|
4469
|
+
|
|
4470
|
+
// `collapse`, `focus` and `rotate` marks seed `collapsed`, `showOnly`
|
|
4471
|
+
// and `rotated`. Seeding runs once per data load, `clades` change and
|
|
4472
|
+
// reroot, so expanding a seeded clade sticks until one of those. A tree
|
|
4473
|
+
// filehandle holds it back, since `dataInitialized` is true while the
|
|
4474
|
+
// tree is still the flat stub.
|
|
4774
4475
|
addDisposer(
|
|
4775
4476
|
self,
|
|
4776
4477
|
reaction(
|
|
4777
4478
|
() =>
|
|
4778
|
-
self.dataInitialized && !(
|
|
4479
|
+
self.dataInitialized && !awaitingTree()
|
|
4779
4480
|
? { clades: getSnapshot(self.clades), treeRoot: self.treeRoot }
|
|
4780
4481
|
: undefined,
|
|
4781
4482
|
seed => {
|
|
@@ -4813,7 +4514,7 @@ function stateModelFactory({
|
|
|
4813
4514
|
region &&
|
|
4814
4515
|
self.viewInitialized &&
|
|
4815
4516
|
self.numColumns > 0 &&
|
|
4816
|
-
!(
|
|
4517
|
+
!awaitingTree()
|
|
4817
4518
|
) {
|
|
4818
4519
|
this.revealRegion(region)
|
|
4819
4520
|
self.setRegion(undefined)
|
|
@@ -4842,107 +4543,7 @@ function stateModelFactory({
|
|
|
4842
4543
|
})
|
|
4843
4544
|
}
|
|
4844
4545
|
|
|
4845
|
-
|
|
4846
|
-
* Fetch a filehandle whenever it changes, and hand the text to
|
|
4847
|
-
* `onLoad`.
|
|
4848
|
-
*
|
|
4849
|
-
* A generation guard keeps a slower superseded request from overwriting
|
|
4850
|
-
* the newer one's data, status or loading flag. A superseded or cleared
|
|
4851
|
-
* request is aborted and clears its status line.
|
|
4852
|
-
*
|
|
4853
|
-
* `clearFilehandle` runs after a local file loads, since a blob has no
|
|
4854
|
-
* URL to refetch, and when the user cancels, which returns the view to
|
|
4855
|
-
* the import form.
|
|
4856
|
-
*
|
|
4857
|
-
* `what` names the layer in a failure message. An `optional` layer
|
|
4858
|
-
* (annotations, row metadata) that fails adds a warning instead of an
|
|
4859
|
-
* error.
|
|
4860
|
-
*/
|
|
4861
|
-
const loadOnFilehandleChange = ({
|
|
4862
|
-
what,
|
|
4863
|
-
getFilehandle,
|
|
4864
|
-
onLoad,
|
|
4865
|
-
setLoading,
|
|
4866
|
-
clearFilehandle,
|
|
4867
|
-
optional = false,
|
|
4868
|
-
}: {
|
|
4869
|
-
what: string
|
|
4870
|
-
getFilehandle: () => FileLocationType | undefined
|
|
4871
|
-
onLoad: (text: string) => void
|
|
4872
|
-
setLoading?: (arg: boolean) => void
|
|
4873
|
-
clearFilehandle?: () => void
|
|
4874
|
-
optional?: boolean
|
|
4875
|
-
}) => {
|
|
4876
|
-
let generation = 0
|
|
4877
|
-
let cancelInFlight: (() => void) | undefined
|
|
4878
|
-
addDisposer(self, () => {
|
|
4879
|
-
cancelInFlight?.()
|
|
4880
|
-
})
|
|
4881
|
-
addDisposer(
|
|
4882
|
-
self,
|
|
4883
|
-
autorun(async () => {
|
|
4884
|
-
const filehandle = getFilehandle()
|
|
4885
|
-
const current = ++generation
|
|
4886
|
-
cancelInFlight?.()
|
|
4887
|
-
cancelInFlight = undefined
|
|
4888
|
-
if (!filehandle) {
|
|
4889
|
-
setLoading?.(false)
|
|
4890
|
-
return
|
|
4891
|
-
}
|
|
4892
|
-
const isCurrent = () => current === generation && isAlive(self)
|
|
4893
|
-
const controller = new AbortController()
|
|
4894
|
-
cancelInFlight = () => {
|
|
4895
|
-
controller.abort()
|
|
4896
|
-
if (isAlive(self)) {
|
|
4897
|
-
self.setStatus(undefined)
|
|
4898
|
-
}
|
|
4899
|
-
}
|
|
4900
|
-
try {
|
|
4901
|
-
setLoading?.(true)
|
|
4902
|
-
if (!optional) {
|
|
4903
|
-
self.setError(undefined)
|
|
4904
|
-
}
|
|
4905
|
-
const text = await fetchTextWithProgress(
|
|
4906
|
-
openLocation(filehandle),
|
|
4907
|
-
status => {
|
|
4908
|
-
if (isCurrent()) {
|
|
4909
|
-
self.setStatus(status)
|
|
4910
|
-
}
|
|
4911
|
-
},
|
|
4912
|
-
{ controller },
|
|
4913
|
-
)
|
|
4914
|
-
if (isCurrent()) {
|
|
4915
|
-
transaction(() => {
|
|
4916
|
-
onLoad(text)
|
|
4917
|
-
if (filehandle.locationType === 'BlobLocation') {
|
|
4918
|
-
clearFilehandle?.()
|
|
4919
|
-
}
|
|
4920
|
-
})
|
|
4921
|
-
}
|
|
4922
|
-
} catch (e) {
|
|
4923
|
-
if (isCurrent()) {
|
|
4924
|
-
if (isAbortError(e)) {
|
|
4925
|
-
clearFilehandle?.()
|
|
4926
|
-
} else {
|
|
4927
|
-
console.error(e)
|
|
4928
|
-
if (optional) {
|
|
4929
|
-
self.addWarning(`The ${what} did not load: ${e}`)
|
|
4930
|
-
} else {
|
|
4931
|
-
self.setError(e)
|
|
4932
|
-
}
|
|
4933
|
-
}
|
|
4934
|
-
}
|
|
4935
|
-
} finally {
|
|
4936
|
-
if (isCurrent()) {
|
|
4937
|
-
cancelInFlight = undefined
|
|
4938
|
-
setLoading?.(false)
|
|
4939
|
-
}
|
|
4940
|
-
}
|
|
4941
|
-
}),
|
|
4942
|
-
)
|
|
4943
|
-
}
|
|
4944
|
-
|
|
4945
|
-
loadOnFilehandleChange({
|
|
4546
|
+
installFilehandleLoader(self, {
|
|
4946
4547
|
what: 'tree',
|
|
4947
4548
|
getFilehandle: () => self.treeFilehandle,
|
|
4948
4549
|
onLoad: text => {
|
|
@@ -4957,7 +4558,7 @@ function stateModelFactory({
|
|
|
4957
4558
|
})
|
|
4958
4559
|
|
|
4959
4560
|
// treeMetadata has no import-form step, so no loading flag or cancel
|
|
4960
|
-
|
|
4561
|
+
installFilehandleLoader(self, {
|
|
4961
4562
|
what: 'row metadata',
|
|
4962
4563
|
optional: true,
|
|
4963
4564
|
getFilehandle: () => self.treeMetadataFilehandle,
|
|
@@ -4989,7 +4590,7 @@ function stateModelFactory({
|
|
|
4989
4590
|
)
|
|
4990
4591
|
|
|
4991
4592
|
// gffFilehandle loads into data.gff, which the autorun above parses
|
|
4992
|
-
|
|
4593
|
+
installFilehandleLoader(self, {
|
|
4993
4594
|
what: 'annotations',
|
|
4994
4595
|
optional: true,
|
|
4995
4596
|
getFilehandle: () => self.gffFilehandle,
|
|
@@ -5001,7 +4602,7 @@ function stateModelFactory({
|
|
|
5001
4602
|
},
|
|
5002
4603
|
})
|
|
5003
4604
|
|
|
5004
|
-
|
|
4605
|
+
installFilehandleLoader(self, {
|
|
5005
4606
|
what: 'alignment',
|
|
5006
4607
|
getFilehandle: () => self.msaFilehandle,
|
|
5007
4608
|
onLoad: text => {
|
|
@@ -5025,20 +4626,16 @@ function stateModelFactory({
|
|
|
5025
4626
|
self,
|
|
5026
4627
|
autorun(() => {
|
|
5027
4628
|
if (self.dynamicColorSchemeName) {
|
|
5028
|
-
|
|
5029
|
-
self.colStats
|
|
4629
|
+
readComputed(self.colStats)
|
|
5030
4630
|
}
|
|
5031
4631
|
if (self.showColumnStats) {
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
5038
|
-
self.propertyConservation
|
|
4632
|
+
readComputed(
|
|
4633
|
+
self.colStats,
|
|
4634
|
+
self.conservation,
|
|
4635
|
+
self.propertyConservation,
|
|
4636
|
+
)
|
|
5039
4637
|
}
|
|
5040
|
-
|
|
5041
|
-
self.columns
|
|
4638
|
+
readComputed(self.columns)
|
|
5042
4639
|
}),
|
|
5043
4640
|
)
|
|
5044
4641
|
},
|