react-msaview 7.0.0 → 8.1.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/README.md +11 -10
- package/bundle/index.js +122 -111
- package/bundle/index.js.map +4 -4
- package/dist/calculateBlocks.js +5 -3
- package/dist/calculateBlocks.js.map +1 -1
- package/dist/colorSchemes.js +1 -2
- package/dist/colorSchemes.js.map +1 -1
- package/dist/columnStats.d.ts +28 -0
- package/dist/columnStats.js +34 -0
- package/dist/columnStats.js.map +1 -0
- package/dist/components/DragHandle.d.ts +3 -2
- package/dist/components/DragHandle.js +11 -5
- package/dist/components/DragHandle.js.map +1 -1
- package/dist/components/Loading.js +2 -8
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAView.js +17 -6
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +64 -7
- package/dist/components/MSAViewer.js +128 -23
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/PortalTooltip.d.ts +11 -0
- package/dist/components/PortalTooltip.js +29 -0
- package/dist/components/PortalTooltip.js.map +1 -0
- package/dist/components/ResizeHandles.js +11 -29
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/SequenceTextArea.js +3 -4
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/Track.js +43 -24
- package/dist/components/Track.js.map +1 -1
- package/dist/components/dialogs/AboutDialog.js +6 -6
- package/dist/components/dialogs/AboutDialog.js.map +1 -1
- package/dist/components/dialogs/AnnotationFileDialog.js +5 -2
- package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
- package/dist/components/dialogs/ExportSVGDialog.js +6 -4
- package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
- package/dist/components/dialogs/FeatureDialog.js +8 -3
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/dialogs/TrackInfoDialog.js +1 -1
- package/dist/components/dialogs/TrackInfoDialog.js.map +1 -1
- package/dist/components/header/GappynessSlider.js +6 -11
- package/dist/components/header/GappynessSlider.js.map +1 -1
- package/dist/components/header/Header.js +4 -1
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/LoadWarnings.js +6 -23
- package/dist/components/header/LoadWarnings.js.map +1 -1
- package/dist/components/header/ScrollZoomIcons.d.ts +6 -0
- package/dist/components/header/ScrollZoomIcons.js +35 -0
- package/dist/components/header/ScrollZoomIcons.js.map +1 -0
- package/dist/components/header/TreeSettingsMenu.js +1 -3
- package/dist/components/header/TreeSettingsMenu.js.map +1 -1
- package/dist/components/header/UnshareableDataWarning.js +2 -4
- package/dist/components/header/UnshareableDataWarning.js.map +1 -1
- package/dist/components/header/ZoomControls.js +49 -6
- package/dist/components/header/ZoomControls.js.map +1 -1
- package/dist/components/header/getAnnotationsMenu.js +3 -6
- package/dist/components/header/getAnnotationsMenu.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +12 -9
- package/dist/components/header/settingsMenuItems.js.map +1 -1
- package/dist/components/import/ImportForm.js +1 -1
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/import/util.js +1 -2
- package/dist/components/import/util.js.map +1 -1
- package/dist/components/minimap/Minimap.js +1 -2
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/msa/AnnotationLegend.js +27 -19
- package/dist/components/msa/AnnotationLegend.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +2 -4
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +13 -15
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +2 -1
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/domainBandCursor.d.ts +7 -11
- package/dist/components/msa/domainBandCursor.js +7 -11
- package/dist/components/msa/domainBandCursor.js.map +1 -1
- package/dist/components/msa/drawFeatureSpans.d.ts +59 -0
- package/dist/components/msa/drawFeatureSpans.js +113 -0
- package/dist/components/msa/drawFeatureSpans.js.map +1 -0
- package/dist/components/msa/legendRows.d.ts +8 -0
- package/dist/components/msa/legendRows.js +16 -0
- package/dist/components/msa/legendRows.js.map +1 -0
- package/dist/components/msa/msaRaster.d.ts +24 -31
- package/dist/components/msa/msaRaster.js +30 -39
- package/dist/components/msa/msaRaster.js.map +1 -1
- package/dist/components/msa/packDomainLanes.d.ts +20 -0
- package/dist/components/msa/packDomainLanes.js +33 -0
- package/dist/components/msa/packDomainLanes.js.map +1 -0
- package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +3 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +50 -87
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderHighlights.d.ts +15 -13
- package/dist/components/msa/renderHighlights.js +43 -20
- package/dist/components/msa/renderHighlights.js.map +1 -1
- package/dist/components/msa/renderMSABlock.js +12 -12
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/tileColor.js +4 -7
- package/dist/components/msa/tileColor.js.map +1 -1
- package/dist/components/msa/useMsaBlockMouse.d.ts +5 -3
- package/dist/components/msa/useMsaBlockMouse.js +6 -6
- package/dist/components/msa/useMsaBlockMouse.js.map +1 -1
- package/dist/components/rowpanels/RowPanelBlock.d.ts +9 -0
- package/dist/components/rowpanels/RowPanelBlock.js +86 -0
- package/dist/components/rowpanels/RowPanelBlock.js.map +1 -0
- package/dist/components/rowpanels/RowPanelHeaders.d.ts +12 -0
- package/dist/components/rowpanels/RowPanelHeaders.js +38 -0
- package/dist/components/rowpanels/RowPanelHeaders.js.map +1 -0
- package/dist/components/{msa/ColumnStats.d.ts → rowpanels/RowPanels.d.ts} +2 -2
- package/dist/components/rowpanels/RowPanels.js +32 -0
- package/dist/components/rowpanels/RowPanels.js.map +1 -0
- package/dist/components/rowpanels/headerLayout.d.ts +1 -0
- package/dist/components/rowpanels/headerLayout.js +5 -0
- package/dist/components/rowpanels/headerLayout.js.map +1 -0
- package/dist/components/rowpanels/renderFeaturePanel.d.ts +21 -0
- package/dist/components/rowpanels/renderFeaturePanel.js +37 -0
- package/dist/components/rowpanels/renderFeaturePanel.js.map +1 -0
- package/dist/components/rowpanels/renderRowPanel.d.ts +23 -0
- package/dist/components/rowpanels/renderRowPanel.js +22 -0
- package/dist/components/rowpanels/renderRowPanel.js.map +1 -0
- package/dist/components/rowpanels/renderStrip.d.ts +20 -0
- package/dist/components/rowpanels/renderStrip.js +26 -0
- package/dist/components/rowpanels/renderStrip.js.map +1 -0
- package/dist/components/tracks/TrackBlocks.js +3 -5
- package/dist/components/tracks/TrackBlocks.js.map +1 -1
- package/dist/components/tracks/TrackTooltipContent.d.ts +15 -0
- package/dist/components/tracks/TrackTooltipContent.js +109 -0
- package/dist/components/tracks/TrackTooltipContent.js.map +1 -0
- package/dist/components/tracks/drawTracks.d.ts +4 -4
- package/dist/components/tracks/drawTracks.js +25 -39
- package/dist/components/tracks/drawTracks.js.map +1 -1
- package/dist/components/tracks/useTrackHover.d.ts +22 -0
- package/dist/components/tracks/useTrackHover.js +30 -0
- package/dist/components/tracks/useTrackHover.js.map +1 -0
- package/dist/components/tree/CladeLabels.d.ts +12 -0
- package/dist/components/tree/CladeLabels.js +48 -0
- package/dist/components/tree/CladeLabels.js.map +1 -0
- package/dist/components/tree/TreeBranchMenu.js +1 -1
- package/dist/components/tree/TreeBranchMenu.js.map +1 -1
- package/dist/components/tree/TreeCanvas.js +5 -4
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +4 -21
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeNodeMenu.js +1 -1
- package/dist/components/tree/TreeNodeMenu.js.map +1 -1
- package/dist/components/tree/TreeOverview.d.ts +14 -0
- package/dist/components/tree/TreeOverview.js +75 -0
- package/dist/components/tree/TreeOverview.js.map +1 -0
- package/dist/components/tree/TreeRuler.js +9 -13
- package/dist/components/tree/TreeRuler.js.map +1 -1
- package/dist/components/tree/cladeBrackets.d.ts +25 -0
- package/dist/components/tree/cladeBrackets.js +51 -0
- package/dist/components/tree/cladeBrackets.js.map +1 -0
- package/dist/components/tree/dialogs/TreeNodeInfoDialog.js +2 -2
- package/dist/components/tree/dialogs/TreeNodeInfoDialog.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +160 -25
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/renderTreeOverview.d.ts +52 -0
- package/dist/components/tree/renderTreeOverview.js +138 -0
- package/dist/components/tree/renderTreeOverview.js.map +1 -0
- package/dist/components/tree/useTreeHover.js +2 -4
- package/dist/components/tree/useTreeHover.js.map +1 -1
- package/dist/constants.d.ts +16 -0
- package/dist/constants.js +52 -22
- package/dist/constants.js.map +1 -1
- package/dist/createPaletteMap.d.ts +7 -6
- package/dist/createPaletteMap.js +10 -9
- package/dist/createPaletteMap.js.map +1 -1
- package/dist/element.d.ts +13 -0
- package/dist/element.js +269 -0
- package/dist/element.js.map +1 -0
- package/dist/featureFields.d.ts +11 -0
- package/dist/featureFields.js +17 -0
- package/dist/featureFields.js.map +1 -0
- package/dist/flatToTree.js +1 -2
- package/dist/flatToTree.js.map +1 -1
- package/dist/ggplotPalettes.d.ts +10 -0
- package/dist/ggplotPalettes.js +58 -0
- package/dist/ggplotPalettes.js.map +1 -1
- package/dist/hierarchy.d.ts +27 -0
- package/dist/hierarchy.js +66 -9
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/model/DataModel.js +2 -5
- package/dist/model/DataModel.js.map +1 -1
- package/dist/model/msaModel.d.ts +3 -2
- package/dist/model/msaModel.js +3 -2
- package/dist/model/msaModel.js.map +1 -1
- package/dist/model/treeModel.d.ts +30 -0
- package/dist/model/treeModel.js +37 -1
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +549 -294
- package/dist/model.js +1412 -621
- package/dist/model.js.map +1 -1
- package/dist/mount.d.ts +11 -0
- package/dist/mount.js +22 -0
- package/dist/mount.js.map +1 -0
- package/dist/neighborJoining.js +8 -32
- package/dist/neighborJoining.js.map +1 -1
- package/dist/renderToStaticMarkup.d.ts +10 -19
- package/dist/renderToStaticMarkup.js +13 -24
- package/dist/renderToStaticMarkup.js.map +1 -1
- package/dist/renderToSvg.js +180 -56
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rowCoordinateCalculations.d.ts +4 -6
- package/dist/rowCoordinateCalculations.js +4 -6
- package/dist/rowCoordinateCalculations.js.map +1 -1
- package/dist/scales.d.ts +22 -0
- package/dist/scales.js +30 -0
- package/dist/scales.js.map +1 -0
- package/dist/seqPosToGlobalCol.d.ts +2 -3
- package/dist/seqPosToGlobalCol.js +2 -3
- package/dist/seqPosToGlobalCol.js.map +1 -1
- package/dist/sequenceLogo.d.ts +6 -11
- package/dist/sequenceLogo.js +6 -11
- package/dist/sequenceLogo.js.map +1 -1
- package/dist/stripDefault.d.ts +7 -14
- package/dist/stripDefault.js +7 -14
- package/dist/stripDefault.js.map +1 -1
- package/dist/svgTestUtil.d.ts +186 -70
- package/dist/types.d.ts +221 -21
- package/dist/umd.d.ts +2 -0
- package/dist/umd.js +4 -2
- package/dist/umd.js.map +1 -1
- package/dist/useCanvasAutorun.d.ts +5 -10
- package/dist/useCanvasAutorun.js +5 -10
- package/dist/useCanvasAutorun.js.map +1 -1
- package/dist/useMsaSvgFigure.d.ts +20 -0
- package/dist/useMsaSvgFigure.js +46 -0
- package/dist/useMsaSvgFigure.js.map +1 -0
- package/dist/useWheelScroll.js +0 -4
- package/dist/useWheelScroll.js.map +1 -1
- package/dist/util.d.ts +5 -6
- package/dist/util.js +13 -6
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +19 -12
- package/src/calculateBlocks.ts +5 -3
- package/src/colorSchemes.ts +1 -2
- package/src/columnStats.ts +58 -0
- package/src/components/DragHandle.tsx +18 -7
- package/src/components/Loading.tsx +4 -36
- package/src/components/MSAView.tsx +19 -5
- package/src/components/MSAViewer.tsx +240 -28
- package/src/components/PortalTooltip.tsx +47 -0
- package/src/components/ResizeHandles.tsx +17 -32
- package/src/components/SequenceTextArea.tsx +3 -4
- package/src/components/Track.tsx +57 -19
- package/src/components/dialogs/AboutDialog.tsx +35 -6
- package/src/components/dialogs/AnnotationFileDialog.tsx +10 -1
- package/src/components/dialogs/ExportSVGDialog.tsx +8 -3
- package/src/components/dialogs/FeatureDialog.tsx +11 -3
- package/src/components/dialogs/TrackInfoDialog.tsx +1 -1
- package/src/components/header/GappynessSlider.tsx +6 -11
- package/src/components/header/Header.tsx +7 -1
- package/src/components/header/LoadWarnings.tsx +11 -25
- package/src/components/header/ScrollZoomIcons.tsx +52 -0
- package/src/components/header/TreeSettingsMenu.tsx +1 -3
- package/src/components/header/UnshareableDataWarning.tsx +2 -4
- package/src/components/header/ZoomControls.tsx +74 -14
- package/src/components/header/getAnnotationsMenu.ts +3 -6
- package/src/components/header/settingsMenuItems.ts +13 -8
- package/src/components/import/ImportForm.tsx +3 -5
- package/src/components/import/ImportFormExamples.tsx +2 -2
- package/src/components/import/util.ts +1 -2
- package/src/components/minimap/Minimap.tsx +1 -2
- package/src/components/msa/AnnotationLegend.tsx +48 -38
- package/src/components/msa/MSACanvas.tsx +3 -8
- package/src/components/msa/MSACanvasBlock.tsx +13 -16
- package/src/components/msa/MSAMouseoverCanvas.tsx +2 -1
- package/src/components/msa/domainBandCursor.ts +7 -11
- package/src/components/msa/drawFeatureSpans.ts +205 -0
- package/src/components/msa/legendRows.ts +24 -0
- package/src/components/msa/msaRaster.ts +30 -39
- package/src/components/msa/packDomainLanes.ts +36 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +67 -123
- package/src/components/msa/renderHighlights.ts +53 -20
- package/src/components/msa/renderMSABlock.ts +14 -11
- package/src/components/msa/tileColor.ts +4 -7
- package/src/components/msa/useMsaBlockMouse.ts +6 -6
- package/src/components/rowpanels/RowPanelBlock.tsx +139 -0
- package/src/components/rowpanels/RowPanelHeaders.tsx +57 -0
- package/src/components/rowpanels/RowPanels.tsx +77 -0
- package/src/components/rowpanels/headerLayout.ts +5 -0
- package/src/components/rowpanels/renderFeaturePanel.ts +66 -0
- package/src/components/rowpanels/renderRowPanel.ts +41 -0
- package/src/components/rowpanels/renderStrip.ts +46 -0
- package/src/components/tracks/TrackBlocks.tsx +3 -5
- package/src/components/tracks/TrackTooltipContent.tsx +156 -0
- package/src/components/tracks/drawTracks.ts +25 -50
- package/src/components/tracks/useTrackHover.ts +42 -0
- package/src/components/tree/CladeLabels.tsx +77 -0
- package/src/components/tree/TreeBranchMenu.tsx +1 -2
- package/src/components/tree/TreeCanvas.tsx +2 -3
- package/src/components/tree/TreeCanvasBlock.tsx +8 -32
- package/src/components/tree/TreeNodeMenu.tsx +1 -2
- package/src/components/tree/TreeOverview.tsx +106 -0
- package/src/components/tree/TreeRuler.tsx +11 -22
- package/src/components/tree/cladeBrackets.ts +73 -0
- package/src/components/tree/dialogs/TreeNodeInfoDialog.tsx +2 -2
- package/src/components/tree/renderTreeCanvas.ts +242 -21
- package/src/components/tree/renderTreeOverview.ts +212 -0
- package/src/components/tree/useTreeHover.ts +2 -4
- package/src/constants.ts +61 -22
- package/src/createPaletteMap.ts +10 -9
- package/src/element.ts +321 -0
- package/src/featureFields.ts +19 -0
- package/src/flatToTree.ts +1 -2
- package/src/ggplotPalettes.ts +60 -0
- package/src/hierarchy.ts +77 -9
- package/src/index.ts +31 -7
- package/src/model/DataModel.ts +2 -5
- package/src/model/msaModel.ts +3 -2
- package/src/model/treeModel.ts +42 -0
- package/src/model.ts +1677 -643
- package/src/mount.tsx +32 -0
- package/src/neighborJoining.ts +8 -32
- package/src/renderToStaticMarkup.ts +13 -24
- package/src/renderToSvg.tsx +335 -68
- package/src/rowCoordinateCalculations.ts +4 -6
- package/src/scales.ts +47 -0
- package/src/seqPosToGlobalCol.ts +2 -3
- package/src/sequenceLogo.ts +6 -11
- package/src/stripDefault.ts +7 -14
- package/src/types.ts +260 -45
- package/src/umd.ts +4 -2
- package/src/useCanvasAutorun.ts +5 -10
- package/src/useMsaSvgFigure.ts +72 -0
- package/src/useWheelScroll.ts +0 -4
- package/src/util.ts +15 -6
- package/src/version.ts +1 -1
- package/dist/components/msa/ColumnStats.js +0 -42
- package/dist/components/msa/ColumnStats.js.map +0 -1
- package/src/components/msa/ColumnStats.tsx +0 -54
package/src/model.ts
CHANGED
|
@@ -9,8 +9,7 @@ import {
|
|
|
9
9
|
isAlive,
|
|
10
10
|
types,
|
|
11
11
|
} from '@jbrowse/mobx-state-tree'
|
|
12
|
-
import {
|
|
13
|
-
import { autorun, transaction } from 'mobx'
|
|
12
|
+
import { autorun, computed, transaction } from 'mobx'
|
|
14
13
|
import {
|
|
15
14
|
generateNodeIds,
|
|
16
15
|
gffToAnnotations,
|
|
@@ -25,19 +24,28 @@ import { calculateBlocks } from './calculateBlocks.ts'
|
|
|
25
24
|
import { clustalXColumnColors } from './clustalX.ts'
|
|
26
25
|
import colorSchemes from './colorSchemes.ts'
|
|
27
26
|
import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.ts'
|
|
27
|
+
import { columnStats } from './columnStats.ts'
|
|
28
|
+
import { packDomainLanes } from './components/msa/packDomainLanes.ts'
|
|
29
|
+
import { visibleColRange } from './components/msa/visibleColRange.ts'
|
|
28
30
|
import TrackBlocks from './components/tracks/TrackBlocks.tsx'
|
|
31
|
+
import { cladeGutterWidth } from './components/tree/cladeBrackets.ts'
|
|
32
|
+
import { scaleBarLength } from './components/tree/scaleBar.ts'
|
|
29
33
|
import {
|
|
34
|
+
cladeHighlightAlpha,
|
|
35
|
+
cladeHighlightColor,
|
|
30
36
|
defaultAllowedGappyness,
|
|
31
37
|
defaultColWidth,
|
|
32
38
|
defaultColorSchemeName,
|
|
33
39
|
defaultCurrentAlignment,
|
|
34
40
|
defaultDrawMsaLetters,
|
|
41
|
+
defaultFeaturePanelWidth,
|
|
35
42
|
defaultHeight,
|
|
36
43
|
defaultHideGaps,
|
|
37
44
|
defaultRowHeight,
|
|
38
45
|
defaultScrollX,
|
|
39
46
|
defaultScrollY,
|
|
40
47
|
defaultScrollZoom,
|
|
48
|
+
defaultScrollZoomAxis,
|
|
41
49
|
defaultShowDomainLegend,
|
|
42
50
|
defaultShowDomains,
|
|
43
51
|
defaultSubFeatureRows,
|
|
@@ -50,11 +58,16 @@ import {
|
|
|
50
58
|
minLetterColWidth,
|
|
51
59
|
minLetterRowHeight,
|
|
52
60
|
minRowHeight,
|
|
61
|
+
rowPanelHeaderHeight,
|
|
62
|
+
rowTintAlpha,
|
|
63
|
+
scrollZoomAxes,
|
|
53
64
|
segmentFeatureTypes,
|
|
54
65
|
segmentShades,
|
|
66
|
+
treeScaleBarHeight,
|
|
55
67
|
} from './constants.ts'
|
|
56
68
|
import { createPaletteMap } from './createPaletteMap.ts'
|
|
57
69
|
import { exportFileName } from './exportFileName.ts'
|
|
70
|
+
import { featureField, featureName } from './featureFields.ts'
|
|
58
71
|
import { fetchTextWithProgress, isAbortError } from './fetchUtils.ts'
|
|
59
72
|
import { flatToTree } from './flatToTree.ts'
|
|
60
73
|
import {
|
|
@@ -65,8 +78,11 @@ import {
|
|
|
65
78
|
find,
|
|
66
79
|
forEachDescendant,
|
|
67
80
|
hierarchy,
|
|
81
|
+
leafIndex,
|
|
68
82
|
leaves,
|
|
69
83
|
maxLength,
|
|
84
|
+
mrca,
|
|
85
|
+
nodeCoveringRows,
|
|
70
86
|
setBrLength,
|
|
71
87
|
sort,
|
|
72
88
|
sum as hierarchySum,
|
|
@@ -85,6 +101,7 @@ import {
|
|
|
85
101
|
visibleColToGlobalCol,
|
|
86
102
|
visibleColsBefore,
|
|
87
103
|
} from './rowCoordinateCalculations.ts'
|
|
104
|
+
import { resolveScale } from './scales.ts'
|
|
88
105
|
import { buildSeqPosIndex } from './seqPosToGlobalCol.ts'
|
|
89
106
|
import { maxBitsFor } from './sequenceLogo.ts'
|
|
90
107
|
import { stripDefault } from './stripDefault.ts'
|
|
@@ -92,33 +109,53 @@ import {
|
|
|
92
109
|
computeRowInsertions,
|
|
93
110
|
dropBlanks,
|
|
94
111
|
len,
|
|
112
|
+
outlineColor,
|
|
95
113
|
skipBlanks,
|
|
96
|
-
|
|
114
|
+
withAlpha,
|
|
97
115
|
} from './util.ts'
|
|
98
116
|
import { saveAs } from './vendor/fileSaver.ts'
|
|
99
117
|
import { parseWuss } from './wuss.ts'
|
|
100
118
|
|
|
119
|
+
import type { ColumnStats } from './columnStats.ts'
|
|
120
|
+
import type { ScrollZoomAxis } from './constants.ts'
|
|
101
121
|
import type { HierarchyNode } from './hierarchy.ts'
|
|
102
122
|
import type { ExportSvgOptions } from './renderToSvg.tsx'
|
|
123
|
+
import type { ScaleSpec } from './scales.ts'
|
|
103
124
|
import type {
|
|
104
125
|
Annotation,
|
|
105
126
|
Arc,
|
|
106
127
|
BasicTrack,
|
|
128
|
+
Cell,
|
|
129
|
+
Clade,
|
|
107
130
|
ResidueMappingProblem,
|
|
108
131
|
ColumnTrackSpec,
|
|
109
132
|
DomainBand,
|
|
133
|
+
Encoding,
|
|
134
|
+
EncodingChannel,
|
|
110
135
|
Highlight,
|
|
136
|
+
Legend,
|
|
137
|
+
LegendEntry,
|
|
111
138
|
NodeWithIds,
|
|
112
139
|
NodeWithIdsAndLength,
|
|
140
|
+
Region,
|
|
113
141
|
ResidueMapping,
|
|
114
142
|
ResidueSegment,
|
|
143
|
+
ResolvedClade,
|
|
144
|
+
ResolvedEncoding,
|
|
115
145
|
ResolvedHighlight,
|
|
146
|
+
ResolvedRowPanel,
|
|
147
|
+
RowFeaturesSpec,
|
|
148
|
+
RowPanelSpan,
|
|
149
|
+
RowPanelSpec,
|
|
116
150
|
RowResidue,
|
|
117
151
|
StructureResidue,
|
|
152
|
+
TrackKind,
|
|
118
153
|
UnshareableData,
|
|
154
|
+
Viewport,
|
|
119
155
|
} from './types.ts'
|
|
120
156
|
import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
|
|
121
157
|
import type { Instance } from '@jbrowse/mobx-state-tree'
|
|
158
|
+
import type { IComputedValue } from 'mobx'
|
|
122
159
|
import type { InterProScanResults } from 'msa-parsers'
|
|
123
160
|
|
|
124
161
|
function parseTreeText(text: string) {
|
|
@@ -128,16 +165,243 @@ function parseTreeText(text: string) {
|
|
|
128
165
|
return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text)
|
|
129
166
|
}
|
|
130
167
|
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
168
|
+
// The height each kind draws at before the user drags a divider. A text track
|
|
169
|
+
// is absent: it is one alignment row tall and follows rowHeight, so the zoom
|
|
170
|
+
// controls already size it.
|
|
171
|
+
export const defaultTrackHeights: Partial<Record<TrackKind, number>> = {
|
|
172
|
+
bar: 40,
|
|
173
|
+
// taller than a bar track: in a 40px stack of four residues each glyph is
|
|
174
|
+
// too short to identify. Twice the height of the tracks above it, so it
|
|
175
|
+
// starts hidden
|
|
176
|
+
logo: 80,
|
|
177
|
+
arc: 50,
|
|
178
|
+
ruler: 20,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// the kinds a divider resizes. The ruler is a fixed scale, and a text track
|
|
182
|
+
// follows the vertical zoom.
|
|
183
|
+
const resizableKinds = new Set<TrackKind>(['bar', 'logo', 'arc'])
|
|
184
|
+
|
|
134
185
|
const defaultOffTracks = new Set(['sequence-logo', 'position-ruler'])
|
|
135
186
|
|
|
136
|
-
//
|
|
137
|
-
//
|
|
187
|
+
// a data track carries its own height rather than its kind's, so its divider
|
|
188
|
+
// resizes it alone
|
|
189
|
+
const ownHeightKey = (id: string) => `own:${id}`
|
|
190
|
+
|
|
191
|
+
// base-pair arcs: one color for nested helices, one for pseudoknots, which cross
|
|
192
|
+
// them
|
|
138
193
|
const HELIX_ARC = '#4e79a7'
|
|
139
194
|
const PSEUDOKNOT_ARC = '#e15759'
|
|
140
195
|
|
|
196
|
+
// the fill of a span under a scale that gives its value no color, such as a
|
|
197
|
+
// gene a `map` over gene names leaves out
|
|
198
|
+
const UNSCALED_FEATURE = '#d9d9d9'
|
|
199
|
+
|
|
200
|
+
// the fraction of a span a following one may cover before it takes a lane of
|
|
201
|
+
// its own in a `features` panel
|
|
202
|
+
const laneOverlap = 0.1
|
|
203
|
+
|
|
204
|
+
// 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
|
+
function rowPanelWidth(panel: RowPanelSpec, rowHeight: number) {
|
|
207
|
+
return (
|
|
208
|
+
panel.width ??
|
|
209
|
+
(panel.kind === 'features' ? defaultFeaturePanelWidth : rowHeight)
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// the fill and outline of every feature in a list: its own GFF `color=` first,
|
|
214
|
+
// then the scale an encoding resolves over one of its fields, then the
|
|
215
|
+
// accession palette
|
|
216
|
+
function featureColorMap(
|
|
217
|
+
annotations: Annotation[],
|
|
218
|
+
encoding:
|
|
219
|
+
| { field: string; colorOf: (value: string) => string | undefined }
|
|
220
|
+
| undefined,
|
|
221
|
+
fillPalette: Record<string, string>,
|
|
222
|
+
) {
|
|
223
|
+
const strokes = new Map<string, string>()
|
|
224
|
+
const strokeOf = (fill: string) => {
|
|
225
|
+
const hit = strokes.get(fill)
|
|
226
|
+
if (hit !== undefined) {
|
|
227
|
+
return hit
|
|
228
|
+
}
|
|
229
|
+
const stroke = outlineColor(fill)
|
|
230
|
+
strokes.set(fill, stroke)
|
|
231
|
+
return stroke
|
|
232
|
+
}
|
|
233
|
+
return new Map(
|
|
234
|
+
annotations.map(annotation => {
|
|
235
|
+
const value = encoding
|
|
236
|
+
? featureField(annotation, encoding.field)
|
|
237
|
+
: undefined
|
|
238
|
+
const fill =
|
|
239
|
+
annotation.color ??
|
|
240
|
+
(value === undefined ? undefined : encoding!.colorOf(value)) ??
|
|
241
|
+
(encoding ? UNSCALED_FEATURE : fillPalette[annotation.accession]!)
|
|
242
|
+
return [annotation, { fill, stroke: strokeOf(fill) }]
|
|
243
|
+
}),
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// the text a label channel draws inside each span, for the features carrying
|
|
248
|
+
// the field it names
|
|
249
|
+
function featureLabelMap(annotations: Annotation[], field: string) {
|
|
250
|
+
const labels = new Map<Annotation, string>()
|
|
251
|
+
for (const annotation of annotations) {
|
|
252
|
+
const value = featureField(annotation, field)
|
|
253
|
+
if (value !== undefined) {
|
|
254
|
+
labels.set(annotation, value)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return labels
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** what `packDomainLanes` needs to lay a row out, before it has its lane */
|
|
261
|
+
type Unlaned = Omit<RowPanelSpan, 'lane' | 'laneCount'> & {
|
|
262
|
+
startCol: number
|
|
263
|
+
endCol: number
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Lanes for `position: "strandpile"`, gggenomes' `position_strandpile`: the
|
|
268
|
+
* forward features stack above the line and the reverse below it, each strand
|
|
269
|
+
* packing on its own. The deepest row on each side sets the grid every row lays
|
|
270
|
+
* out on, so the line between the strands sits at one height down the panel and
|
|
271
|
+
* a reader can scan it. A feature with no strand piles with the forward ones.
|
|
272
|
+
*/
|
|
273
|
+
function strandpileLanes(
|
|
274
|
+
rows: [string, Unlaned[]][],
|
|
275
|
+
): Map<string, RowPanelSpan[]> {
|
|
276
|
+
const piled = rows.map(([name, bands]) => {
|
|
277
|
+
const reverse = packDomainLanes(
|
|
278
|
+
bands.filter(b => b.annotation.strand === -1),
|
|
279
|
+
)
|
|
280
|
+
const forward = packDomainLanes(
|
|
281
|
+
bands.filter(b => b.annotation.strand !== -1),
|
|
282
|
+
)
|
|
283
|
+
return { name, forward, reverse }
|
|
284
|
+
})
|
|
285
|
+
const up = Math.max(0, ...piled.map(p => p.forward[0]?.laneCount ?? 0))
|
|
286
|
+
const down = Math.max(0, ...piled.map(p => p.reverse[0]?.laneCount ?? 0))
|
|
287
|
+
const laneCount = up + down
|
|
288
|
+
return new Map(
|
|
289
|
+
piled.map(({ name, forward, reverse }) => [
|
|
290
|
+
name,
|
|
291
|
+
[
|
|
292
|
+
// forward level 0 is the lane just above the line, stacking upward
|
|
293
|
+
...forward.map(b => ({ ...b, lane: up - 1 - b.lane, laneCount })),
|
|
294
|
+
...reverse.map(b => ({ ...b, lane: up + b.lane, laneCount })),
|
|
295
|
+
],
|
|
296
|
+
]),
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** one row's spans laid out by the panel's `position` */
|
|
301
|
+
function panelLanes(
|
|
302
|
+
panel: RowFeaturesSpec,
|
|
303
|
+
rows: [string, Unlaned[]][],
|
|
304
|
+
): Map<string, RowPanelSpan[]> {
|
|
305
|
+
return panel.position === 'strandpile'
|
|
306
|
+
? strandpileLanes(rows)
|
|
307
|
+
: new Map(rows.map(([name, bands]) => [name, packDomainLanes(bands)]))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The spans a `features` panel draws, keyed by row name and measured in the
|
|
312
|
+
* panel's own pixels. `column` takes the bands the overlay draws, at the
|
|
313
|
+
* alignment's column width. `position` packs each row's features in its own
|
|
314
|
+
* residue positions, shifted by an `align` transform, and maps the extent they
|
|
315
|
+
* cover across every row onto the panel width, so a row with no alignment
|
|
316
|
+
* still has an x.
|
|
317
|
+
*/
|
|
318
|
+
function featurePanelSpans({
|
|
319
|
+
panel,
|
|
320
|
+
width,
|
|
321
|
+
colWidth,
|
|
322
|
+
domainBands,
|
|
323
|
+
annotationsByRow,
|
|
324
|
+
shifts,
|
|
325
|
+
}: {
|
|
326
|
+
panel: RowFeaturesSpec
|
|
327
|
+
width: number
|
|
328
|
+
colWidth: number
|
|
329
|
+
domainBands: Map<string, DomainBand[]>
|
|
330
|
+
annotationsByRow: Record<string, Annotation[]>
|
|
331
|
+
shifts: Map<string, number> | undefined
|
|
332
|
+
}): Map<string, RowPanelSpan[]> {
|
|
333
|
+
if (panel.x === 'column') {
|
|
334
|
+
return panelLanes(
|
|
335
|
+
panel,
|
|
336
|
+
[...domainBands].map(([name, bands]) => [
|
|
337
|
+
name,
|
|
338
|
+
bands.map(band => ({
|
|
339
|
+
...band,
|
|
340
|
+
xStart: band.startCol * colWidth,
|
|
341
|
+
xEnd: band.endCol * colWidth,
|
|
342
|
+
})),
|
|
343
|
+
]),
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
const shifted = Object.entries(annotationsByRow).map(
|
|
347
|
+
([name, annotations]) => {
|
|
348
|
+
const shift = shifts?.get(name) ?? 0
|
|
349
|
+
return [
|
|
350
|
+
name,
|
|
351
|
+
annotations.map(annotation => ({
|
|
352
|
+
annotation,
|
|
353
|
+
start: annotation.start - 1 + shift,
|
|
354
|
+
end: annotation.end + shift,
|
|
355
|
+
})),
|
|
356
|
+
] as const
|
|
357
|
+
},
|
|
358
|
+
)
|
|
359
|
+
let min = Infinity
|
|
360
|
+
let max = -Infinity
|
|
361
|
+
for (const [, features] of shifted) {
|
|
362
|
+
for (const { start, end } of features) {
|
|
363
|
+
min = Math.min(min, start)
|
|
364
|
+
max = Math.max(max, end)
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// a gene arrow ends where its feature does, so the extent maps onto the whole
|
|
368
|
+
// panel less the pixel the rightmost stroke needs
|
|
369
|
+
const drawable = Math.max(1, width - 1)
|
|
370
|
+
const scale = max > min ? drawable / (max - min) : 0
|
|
371
|
+
// a lane opens where a span covers more than a tenth of the one before it,
|
|
372
|
+
// which keeps the genes of an operon on one lane: adjacent genes commonly
|
|
373
|
+
// share a few bases, and a stop codon overlapping the next start reads as a
|
|
374
|
+
// second lane over the whole row
|
|
375
|
+
return panelLanes(
|
|
376
|
+
panel,
|
|
377
|
+
shifted.map(([name, features]) => [
|
|
378
|
+
name,
|
|
379
|
+
features.map(({ annotation, start, end }) => ({
|
|
380
|
+
annotation,
|
|
381
|
+
xStart: (start - min) * scale,
|
|
382
|
+
xEnd: (end - min) * scale,
|
|
383
|
+
startCol: start,
|
|
384
|
+
endCol: end - (end - start) * laneOverlap,
|
|
385
|
+
})),
|
|
386
|
+
]),
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// a scale over the values a field takes across the features drawn
|
|
391
|
+
function resolveFeatureScale(
|
|
392
|
+
field: string,
|
|
393
|
+
scale: ScaleSpec | undefined,
|
|
394
|
+
annotations: Annotation[],
|
|
395
|
+
) {
|
|
396
|
+
return {
|
|
397
|
+
field,
|
|
398
|
+
...resolveScale(
|
|
399
|
+
scale,
|
|
400
|
+
annotations.map(a => featureField(a, field)).filter(notEmpty),
|
|
401
|
+
),
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
141
405
|
// a data track over this size stays in the live model but leaves the snapshot,
|
|
142
406
|
// the same rule DataModel applies to an inline document
|
|
143
407
|
function columnTrackSizes(tracks?: readonly ColumnTrackSpec[]) {
|
|
@@ -154,17 +418,13 @@ function smallColumnTracks(tracks?: ColumnTrackSpec[]) {
|
|
|
154
418
|
* The snapshot properties reset() carries across a return to the import form:
|
|
155
419
|
* display preferences and layout, nothing derived from the loaded file.
|
|
156
420
|
*
|
|
157
|
-
* reset() applies a default snapshot filtered to this list, so
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
* are path-derived (node-0-0-1), a stale `collapsed` or `showOnly` id matched
|
|
163
|
-
* a real node in the new tree and folded it. Downstream composed properties
|
|
164
|
-
* (e.g. the jbrowse plugin's) are not on the list, so they reset too.
|
|
421
|
+
* reset() applies a default snapshot filtered to this list, so any property
|
|
422
|
+
* left off it resets, including downstream composed ones (e.g. the jbrowse
|
|
423
|
+
* plugin's). Node ids are path-derived (node-0-0-1), so a carried-over
|
|
424
|
+
* `collapsed` or `showOnly` id would match a real node in the next tree and
|
|
425
|
+
* fold it.
|
|
165
426
|
*
|
|
166
|
-
* Exported for modelReset.test.ts
|
|
167
|
-
* matches a freshly created model after reset().
|
|
427
|
+
* Exported for modelReset.test.ts.
|
|
168
428
|
*/
|
|
169
429
|
export const preservedOnReset = new Set([
|
|
170
430
|
'id',
|
|
@@ -172,6 +432,7 @@ export const preservedOnReset = new Set([
|
|
|
172
432
|
'height',
|
|
173
433
|
'drawMsaLetters',
|
|
174
434
|
'scrollZoom',
|
|
435
|
+
'scrollZoomAxis',
|
|
175
436
|
'bgColor',
|
|
176
437
|
'colorSchemeName',
|
|
177
438
|
'showColumnStats',
|
|
@@ -182,21 +443,22 @@ export const preservedOnReset = new Set([
|
|
|
182
443
|
'showBranchLen',
|
|
183
444
|
'drawTree',
|
|
184
445
|
'drawNodeBubbles',
|
|
446
|
+
'drawNodeLabels',
|
|
447
|
+
'showTreeOverview',
|
|
448
|
+
'overviewHeight',
|
|
185
449
|
'autoTreeAreaWidth',
|
|
186
450
|
'turnedOffTracks',
|
|
451
|
+
'trackHeights',
|
|
187
452
|
'hideGaps',
|
|
188
453
|
'allowedGappyness',
|
|
189
454
|
'subFeatureRows',
|
|
190
455
|
'showDomainLegend',
|
|
191
456
|
])
|
|
192
457
|
|
|
193
|
-
// `turnedOffTracks`
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
// A track the file itself supplies says whether it starts hidden, since only
|
|
198
|
-
// the file knows how many of them there are: a Pfam seed carries a couple of
|
|
199
|
-
// #=GR lines, an Rfam family one per row.
|
|
458
|
+
// `turnedOffTracks` holds only the user's explicit choices, so a hidden-by-default
|
|
459
|
+
// track writes nothing into the snapshot. A file-supplied track passes its own
|
|
460
|
+
// `defaultOff`, since the count varies by file: a Pfam seed has a couple of #=GR
|
|
461
|
+
// lines, an Rfam family one per row.
|
|
200
462
|
function trackIsOff(
|
|
201
463
|
turnedOffTracks: { get: (id: string) => boolean | undefined },
|
|
202
464
|
id: string,
|
|
@@ -205,20 +467,144 @@ function trackIsOff(
|
|
|
205
467
|
return turnedOffTracks.get(id) ?? (defaultOff || defaultOffTracks.has(id))
|
|
206
468
|
}
|
|
207
469
|
|
|
208
|
-
//
|
|
209
|
-
// value to every observer of it
|
|
470
|
+
// shared empty results, so observers don't see a fresh [] as a change
|
|
210
471
|
const noDomains: Annotation[] = []
|
|
472
|
+
const noClades: ResolvedClade[] = []
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The rows a clade covers and the node an `mrca` names, or undefined when the
|
|
476
|
+
* clade does not resolve: a tip name the tree does not have or has twice, or a
|
|
477
|
+
* leaf count `tips` disagrees with. `index` and `rowNamesSet` are the memoized
|
|
478
|
+
* passes over the tree.
|
|
479
|
+
*/
|
|
480
|
+
function cladeRows(
|
|
481
|
+
clade: Clade,
|
|
482
|
+
root: HierarchyNode<NodeWithIds>,
|
|
483
|
+
index: Map<string, HierarchyNode<NodeWithIds> | undefined>,
|
|
484
|
+
rowNamesSet: Map<string, number>,
|
|
485
|
+
): { rows: [number, number]; nodeId?: string } | undefined {
|
|
486
|
+
if (clade.range) {
|
|
487
|
+
const [a, b] = clade.range.map(name =>
|
|
488
|
+
index.get(name) ? rowNamesSet.get(name) : undefined,
|
|
489
|
+
)
|
|
490
|
+
if (a === undefined || b === undefined) {
|
|
491
|
+
return undefined
|
|
492
|
+
}
|
|
493
|
+
const rows: [number, number] = a <= b ? [a, b] : [b, a]
|
|
494
|
+
return rows[1] - rows[0] + 1 === clade.tips ? { rows } : undefined
|
|
495
|
+
}
|
|
496
|
+
const node = clade.mrca ? mrca(root, clade.mrca, index) : undefined
|
|
497
|
+
if (!node) {
|
|
498
|
+
return undefined
|
|
499
|
+
}
|
|
500
|
+
const tips = leaves(node)
|
|
501
|
+
if (tips.length !== clade.tips) {
|
|
502
|
+
return undefined
|
|
503
|
+
}
|
|
504
|
+
let first = Infinity
|
|
505
|
+
let last = -Infinity
|
|
506
|
+
for (const tip of tips) {
|
|
507
|
+
const row = rowNamesSet.get(tip.data.name)
|
|
508
|
+
if (row !== undefined) {
|
|
509
|
+
first = Math.min(first, row)
|
|
510
|
+
last = Math.max(last, row)
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
return first <= last
|
|
514
|
+
? { rows: [first, last], nodeId: node.data.id }
|
|
515
|
+
: undefined
|
|
516
|
+
}
|
|
211
517
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
518
|
+
/**
|
|
519
|
+
* Every clade that resolves, with its fill color settled, against the leaf
|
|
520
|
+
* order `rowNamesSet` gives. The tree panel resolves against the displayed
|
|
521
|
+
* rows and the overview against its own, so both take the tree they draw. A
|
|
522
|
+
* `range` record names no node, so `collapse` and `focus`, which need one,
|
|
523
|
+
* drop it.
|
|
524
|
+
*/
|
|
525
|
+
function resolveClades(
|
|
526
|
+
clades: Clade[],
|
|
527
|
+
root: HierarchyNode<NodeWithIds>,
|
|
528
|
+
rowNamesSet: Map<string, number>,
|
|
529
|
+
): ResolvedClade[] {
|
|
530
|
+
const index = leafIndex(root)
|
|
531
|
+
return clades.flatMap(clade => {
|
|
532
|
+
const resolved = cladeRows(clade, root, index, rowNamesSet)
|
|
533
|
+
const seeding = clade.mark === 'collapse' || clade.mark === 'focus'
|
|
534
|
+
return resolved && !(seeding && resolved.nodeId === undefined)
|
|
535
|
+
? [
|
|
536
|
+
{
|
|
537
|
+
...resolved,
|
|
538
|
+
mark: clade.mark,
|
|
539
|
+
color: withAlpha(
|
|
540
|
+
clade.color ?? cladeHighlightColor,
|
|
541
|
+
cladeHighlightAlpha,
|
|
542
|
+
),
|
|
543
|
+
markColor: clade.color,
|
|
544
|
+
label: clade.label,
|
|
545
|
+
},
|
|
546
|
+
]
|
|
547
|
+
: []
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* The tree with the display transforms applied: leaf counts summed, children
|
|
553
|
+
* sorted by branch length, `showOnly` taken as the new root, and each
|
|
554
|
+
* `collapsed` clade folded. The overview builds the same tree without the
|
|
555
|
+
* focus, so it shows the focused subtree inside the whole.
|
|
556
|
+
*/
|
|
557
|
+
function buildTreeRoot(
|
|
558
|
+
tree: NodeWithIds,
|
|
559
|
+
collapsed: readonly string[],
|
|
560
|
+
showOnly?: string,
|
|
561
|
+
) {
|
|
562
|
+
let hier = hierarchy(tree, d => d.children)
|
|
563
|
+
hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1))
|
|
564
|
+
sort(hier, (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1))
|
|
565
|
+
|
|
566
|
+
if (showOnly) {
|
|
567
|
+
const res = find(hier, n => n.data.id === showOnly)
|
|
568
|
+
if (res) {
|
|
569
|
+
hier = res
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
for (const collapsedId of collapsed) {
|
|
574
|
+
const node = find(hier, n => n.data.id === collapsedId)
|
|
575
|
+
if (node) {
|
|
576
|
+
if (node.children) {
|
|
577
|
+
collapse(node)
|
|
578
|
+
} else if (node.parent?.children) {
|
|
579
|
+
node.parent.children = node.parent.children.filter(
|
|
580
|
+
c => c.data.id !== collapsedId,
|
|
581
|
+
)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return hier
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// the inclusive tip indices a node covers, from the row-space extent
|
|
590
|
+
// clusterLayout writes to xMin/xMax as tip centers
|
|
591
|
+
function tipRange(node: HierarchyNode): [number, number] {
|
|
592
|
+
return [Math.round(node.xMin! - 0.5), Math.round(node.xMax! - 0.5)]
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// the channels reading the feature table; every other channel reads rowData
|
|
596
|
+
const featureChannels = new Set<EncodingChannel>([
|
|
597
|
+
'featureFill',
|
|
598
|
+
'featureLabel',
|
|
599
|
+
])
|
|
600
|
+
|
|
601
|
+
// seqPos -> column indexes per row, keyed on the parse so they are garbage
|
|
602
|
+
// collected with it. A computed would rebuild every row's index when read
|
|
603
|
+
// outside a reactive context.
|
|
216
604
|
const seqPosIndexCache = new WeakMap<object, Map<string, Int32Array>>()
|
|
217
605
|
|
|
218
|
-
// A segment
|
|
219
|
-
//
|
|
220
|
-
// anyway -- with a residue that is off by however much the sides disagree. Skip
|
|
221
|
-
// it, the same refusal an uncovered position gets.
|
|
606
|
+
// A segment is a 1:1 run. A segment whose sides differ in length is malformed,
|
|
607
|
+
// and the lookups treat it like an uncovered position.
|
|
222
608
|
function sameLength(segment: ResidueSegment) {
|
|
223
609
|
return (
|
|
224
610
|
segment.rowEnd - segment.rowStart ===
|
|
@@ -226,11 +612,9 @@ function sameLength(segment: ResidueSegment) {
|
|
|
226
612
|
)
|
|
227
613
|
}
|
|
228
614
|
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
// a scrollbar for half a pixel of nothing, which then shrinks the viewport and
|
|
233
|
-
// leaves a gap.
|
|
615
|
+
// fit() divides the viewport by the row or column count and multiplies back, so
|
|
616
|
+
// an exact fit can land a fraction of a pixel over. A plain `>` would then show
|
|
617
|
+
// a scrollbar that shrinks the viewport and leaves a gap.
|
|
234
618
|
function overflows(content: number, viewport: number) {
|
|
235
619
|
return content - viewport > 0.5
|
|
236
620
|
}
|
|
@@ -239,6 +623,29 @@ function inRanges(ranges: [number, number][] | undefined, position: number) {
|
|
|
239
623
|
return !!ranges?.some(([start, end]) => position >= start && position <= end)
|
|
240
624
|
}
|
|
241
625
|
|
|
626
|
+
// the value every one of a node's children reports, or undefined where one of
|
|
627
|
+
// them has no value or they disagree
|
|
628
|
+
function sharedValue(values: (string | undefined)[]) {
|
|
629
|
+
const [first] = values
|
|
630
|
+
return first !== undefined && values.every(v => v === first)
|
|
631
|
+
? first
|
|
632
|
+
: undefined
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// preorder, so reversing the list puts every node after its descendants
|
|
636
|
+
function preorder(tree: NodeWithIds) {
|
|
637
|
+
const order: NodeWithIds[] = []
|
|
638
|
+
const stack = [tree]
|
|
639
|
+
while (stack.length > 0) {
|
|
640
|
+
const node = stack.pop()!
|
|
641
|
+
order.push(node)
|
|
642
|
+
for (const child of node.children) {
|
|
643
|
+
stack.push(child)
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return order
|
|
647
|
+
}
|
|
648
|
+
|
|
242
649
|
/**
|
|
243
650
|
* #stateModel MsaView
|
|
244
651
|
*
|
|
@@ -279,10 +686,8 @@ function stateModelFactory() {
|
|
|
279
686
|
/**
|
|
280
687
|
* #property
|
|
281
688
|
* whether the domain legend is expanded. The legend floats over the
|
|
282
|
-
* top-right of the alignment, so
|
|
283
|
-
*
|
|
284
|
-
* and keep it collapsed, and what lets a session or a figure open with
|
|
285
|
-
* it already out of the way.
|
|
689
|
+
* top-right of the alignment and covers residues, so a session or
|
|
690
|
+
* figure can open with it collapsed.
|
|
286
691
|
*/
|
|
287
692
|
showDomainLegend: stripDefault(types.boolean, defaultShowDomainLegend),
|
|
288
693
|
/**
|
|
@@ -315,6 +720,15 @@ function stateModelFactory() {
|
|
|
315
720
|
*/
|
|
316
721
|
scrollZoom: stripDefault(types.boolean, defaultScrollZoom),
|
|
317
722
|
|
|
723
|
+
/**
|
|
724
|
+
* #property
|
|
725
|
+
* which cell dimensions a wheel zoom scales, while `scrollZoom` is on
|
|
726
|
+
*/
|
|
727
|
+
scrollZoomAxis: stripDefault(
|
|
728
|
+
types.enumeration('ScrollZoomAxis', [...scrollZoomAxes]),
|
|
729
|
+
defaultScrollZoomAxis,
|
|
730
|
+
),
|
|
731
|
+
|
|
318
732
|
/**
|
|
319
733
|
* #property
|
|
320
734
|
* height of the div containing the view, px
|
|
@@ -395,11 +809,20 @@ function stateModelFactory() {
|
|
|
395
809
|
* hidden-by-default track adds nothing to the shared URL.
|
|
396
810
|
*/
|
|
397
811
|
turnedOffTracks: stripDefault(types.map(types.boolean), {}),
|
|
812
|
+
|
|
813
|
+
/**
|
|
814
|
+
* #property
|
|
815
|
+
* the height of every track one divider resizes, keyed by `heightKey`:
|
|
816
|
+
* the `kind` for the computed tracks, `own:<id>` for a data track. A
|
|
817
|
+
* key is absent until the user drags that divider, and
|
|
818
|
+
* `defaultTrackHeights` answers for it until then, so an untouched
|
|
819
|
+
* viewer adds nothing to the shared URL.
|
|
820
|
+
*/
|
|
821
|
+
trackHeights: stripDefault(types.map(types.number), {}),
|
|
398
822
|
/**
|
|
399
823
|
* #property
|
|
400
|
-
* tracks supplied as data
|
|
401
|
-
* per-column
|
|
402
|
-
* text track. See docs/layers.md
|
|
824
|
+
* tracks supplied as data: per-column values drawn as bars, or a
|
|
825
|
+
* per-column string drawn as a text track. See docs/layers.md
|
|
403
826
|
*/
|
|
404
827
|
columnTracks: stripDefault(
|
|
405
828
|
types.array(types.frozen<ColumnTrackSpec>()),
|
|
@@ -408,11 +831,10 @@ function stateModelFactory() {
|
|
|
408
831
|
|
|
409
832
|
/**
|
|
410
833
|
* #property
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
* worked -- so it arrives computed. See docs/layers.md
|
|
834
|
+
* row-to-structure residue correspondence, computed outside the viewer
|
|
835
|
+
* (e.g. from SIFTS). Matching by sequence equality places a tagged
|
|
836
|
+
* construct, a truncation or a subsequence row on the wrong residue.
|
|
837
|
+
* See docs/layers.md
|
|
416
838
|
*/
|
|
417
839
|
residueMappings: stripDefault(
|
|
418
840
|
types.array(types.frozen<ResidueMapping>()),
|
|
@@ -424,19 +846,14 @@ function stateModelFactory() {
|
|
|
424
846
|
* data from the loaded tree/msa/treeMetadata, generally loaded by
|
|
425
847
|
* autorun
|
|
426
848
|
*/
|
|
427
|
-
data: types.optional(DataModelF(), {
|
|
428
|
-
tree: '',
|
|
429
|
-
msa: '',
|
|
430
|
-
treeMetadata: '',
|
|
431
|
-
}),
|
|
849
|
+
data: types.optional(DataModelF(), {}),
|
|
432
850
|
|
|
433
851
|
/**
|
|
434
852
|
* #property
|
|
435
853
|
* the user's explicit hide choices per annotation accession, keyed by
|
|
436
|
-
* accession with the value meaning "off",
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
* shared URL until someone filters one out
|
|
854
|
+
* accession with the value meaning "off", like `turnedOffTracks`. An
|
|
855
|
+
* untouched accession is absent and drawn, so the shared URL grows
|
|
856
|
+
* only with the user's filters
|
|
440
857
|
*/
|
|
441
858
|
turnedOffFeatures: stripDefault(types.map(types.boolean), {}),
|
|
442
859
|
/**
|
|
@@ -457,9 +874,34 @@ function stateModelFactory() {
|
|
|
457
874
|
* labeled highlights in 1-based inclusive coordinates: a column span
|
|
458
875
|
* `{start, end}`, a residue span `{row, start, end}` of a named row,
|
|
459
876
|
* or a row set `{rows}`, each with an optional `label` and `color`.
|
|
460
|
-
* Persists in the snapshot
|
|
877
|
+
* Persists in the snapshot and the URL.
|
|
461
878
|
*/
|
|
462
879
|
highlights: stripDefault(types.array(types.frozen<Highlight>()), []),
|
|
880
|
+
/**
|
|
881
|
+
* #property
|
|
882
|
+
* clades of the tree with a mark drawn over them. `mrca` names tips
|
|
883
|
+
* whose common ancestor is the clade, or `range` its first and last
|
|
884
|
+
* tip in display order, and `tips` is the leaf count the producer
|
|
885
|
+
* measured. `mark` is `highlight`, `bracket`, `collapse` or `focus`.
|
|
886
|
+
* See docs/layers.md
|
|
887
|
+
*/
|
|
888
|
+
clades: stripDefault(types.array(types.frozen<Clade>()), []),
|
|
889
|
+
/**
|
|
890
|
+
* #property
|
|
891
|
+
* what the viewer's own marks read from `rowData`:
|
|
892
|
+
* `{channel, field, scale?}` per channel, where `channel` is
|
|
893
|
+
* `tipLabel` or `rowTint`. See docs/layers.md
|
|
894
|
+
*/
|
|
895
|
+
encodings: stripDefault(types.array(types.frozen<Encoding>()), []),
|
|
896
|
+
/**
|
|
897
|
+
* #property
|
|
898
|
+
* panels drawn between the tree and the alignment:
|
|
899
|
+
* `{kind: "strip", field, scale?, width?, header?}` colors one cell
|
|
900
|
+
* per row from a `rowData` field, and
|
|
901
|
+
* `{kind: "features", x, encoding?, transform?, width?, header?}`
|
|
902
|
+
* draws the spans the GFF carries. See docs/layers.md
|
|
903
|
+
*/
|
|
904
|
+
rowPanels: stripDefault(types.array(types.frozen<RowPanelSpec>()), []),
|
|
463
905
|
}),
|
|
464
906
|
)
|
|
465
907
|
.volatile(() => ({
|
|
@@ -467,15 +909,19 @@ function stateModelFactory() {
|
|
|
467
909
|
* #volatile
|
|
468
910
|
*/
|
|
469
911
|
headerHeight: 0,
|
|
912
|
+
/**
|
|
913
|
+
* #volatile
|
|
914
|
+
* leaves the toolbar out, for a host drawing its own controls. Kept out
|
|
915
|
+
* of the snapshot so a link opened in the full app shows the toolbar.
|
|
916
|
+
*/
|
|
917
|
+
hideHeader: false,
|
|
470
918
|
/**
|
|
471
919
|
* #volatile
|
|
472
920
|
*/
|
|
473
921
|
status: undefined as { msg: string; onCancel?: () => void } | undefined,
|
|
474
922
|
/**
|
|
475
923
|
* #volatile
|
|
476
|
-
*
|
|
477
|
-
* screens. derived from the device pixel ratio so canvases are crisp on
|
|
478
|
-
* retina/4k displays and not needlessly oversized on standard ones
|
|
924
|
+
* canvas scale factor, from the device pixel ratio
|
|
479
925
|
*/
|
|
480
926
|
highResScaleFactor:
|
|
481
927
|
typeof window === 'undefined' ? 1 : window.devicePixelRatio,
|
|
@@ -544,11 +990,8 @@ function stateModelFactory() {
|
|
|
544
990
|
|
|
545
991
|
/**
|
|
546
992
|
* #volatile
|
|
547
|
-
* transient highlights keyed by
|
|
548
|
-
*
|
|
549
|
-
* both want to point at a column, and with one slot whoever clears last
|
|
550
|
-
* erases the other's. Keyed by owner, each source adds and removes only
|
|
551
|
-
* its own. Not persisted: a hover is not part of the document.
|
|
993
|
+
* transient highlights keyed by owner, so a structure viewer's hover and
|
|
994
|
+
* a genome view's hover each clear only their own. Not persisted.
|
|
552
995
|
*/
|
|
553
996
|
transientHighlights: {} as Record<string, Highlight[]>,
|
|
554
997
|
|
|
@@ -557,33 +1000,6 @@ function stateModelFactory() {
|
|
|
557
1000
|
*/
|
|
558
1001
|
minimapHeight: 56,
|
|
559
1002
|
|
|
560
|
-
/**
|
|
561
|
-
* #volatile
|
|
562
|
-
*/
|
|
563
|
-
conservationTrackHeight: 40,
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* #volatile
|
|
567
|
-
* heights of individual `columnTracks`, by track id. A data track is
|
|
568
|
-
* resized on its own: the shared per-kind heights below belong to the
|
|
569
|
-
* tracks the viewer computes, and dragging a data track's handle used to
|
|
570
|
-
* resize those instead.
|
|
571
|
-
*/
|
|
572
|
-
columnTrackHeights: {} as Record<string, number>,
|
|
573
|
-
|
|
574
|
-
/**
|
|
575
|
-
* #volatile
|
|
576
|
-
* taller than the conservation track by default: the logo spends its
|
|
577
|
-
* height on stacked glyphs, and a 40px stack of four residues leaves each
|
|
578
|
-
* one too short to identify
|
|
579
|
-
*/
|
|
580
|
-
sequenceLogoTrackHeight: 80,
|
|
581
|
-
|
|
582
|
-
/**
|
|
583
|
-
* #volatile
|
|
584
|
-
*/
|
|
585
|
-
arcTrackHeight: 50,
|
|
586
|
-
|
|
587
1003
|
/**
|
|
588
1004
|
* #volatile
|
|
589
1005
|
*/
|
|
@@ -597,36 +1013,31 @@ function stateModelFactory() {
|
|
|
597
1013
|
|
|
598
1014
|
/**
|
|
599
1015
|
* #volatile
|
|
600
|
-
* load problems
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
* decorative file
|
|
1016
|
+
* non-fatal load problems: an optional layer that failed to load, an
|
|
1017
|
+
* overlay that failed to parse. `error` replaces the view and is for the
|
|
1018
|
+
* alignment itself
|
|
604
1019
|
*/
|
|
605
1020
|
warnings: [] as string[],
|
|
606
1021
|
|
|
607
1022
|
/**
|
|
608
1023
|
* #volatile
|
|
609
|
-
* bumped by reset(). The
|
|
610
|
-
*
|
|
611
|
-
* nothing after a render error until this became its key
|
|
1024
|
+
* bumped by reset(). The error boundary above the view uses it as its key,
|
|
1025
|
+
* since the boundary keeps its caught error until remounted
|
|
612
1026
|
*/
|
|
613
1027
|
resetCount: 0,
|
|
614
1028
|
|
|
615
1029
|
/**
|
|
616
1030
|
* #volatile
|
|
617
|
-
* set by a host that restores the loaded documents
|
|
618
|
-
*
|
|
619
|
-
*
|
|
620
|
-
* link that opens empty, and under such a host the link does not
|
|
1031
|
+
* set by a host that restores the loaded documents itself, such as a
|
|
1032
|
+
* jbrowse session or a page that refetches them. `unshareableData` then
|
|
1033
|
+
* reports nothing
|
|
621
1034
|
*/
|
|
622
1035
|
hostCarriesData: false,
|
|
623
1036
|
|
|
624
1037
|
/**
|
|
625
1038
|
* #volatile
|
|
626
|
-
* overlay annotations drawn on the alignment,
|
|
627
|
-
*
|
|
628
|
-
* flat list before it reaches the model, so nothing downstream of here
|
|
629
|
-
* knows which one it came from
|
|
1039
|
+
* overlay annotations drawn on the alignment. InterProScan JSON, GFF and
|
|
1040
|
+
* user uploads all convert to this flat list
|
|
630
1041
|
*/
|
|
631
1042
|
annotations: [] as Annotation[],
|
|
632
1043
|
}))
|
|
@@ -672,8 +1083,8 @@ function stateModelFactory() {
|
|
|
672
1083
|
},
|
|
673
1084
|
/**
|
|
674
1085
|
* #action
|
|
675
|
-
*
|
|
676
|
-
*
|
|
1086
|
+
* update the canvas scale factor when the device pixel ratio changes
|
|
1087
|
+
* (moving between monitors, browser zoom)
|
|
677
1088
|
*/
|
|
678
1089
|
setHighResScaleFactor(arg: number) {
|
|
679
1090
|
self.highResScaleFactor = arg
|
|
@@ -696,11 +1107,13 @@ function stateModelFactory() {
|
|
|
696
1107
|
|
|
697
1108
|
/**
|
|
698
1109
|
* #action
|
|
699
|
-
*
|
|
700
|
-
*
|
|
1110
|
+
* record a non-fatal load problem: a layer that failed to load, a file
|
|
1111
|
+
* that failed to parse. A message already on the list is not added again
|
|
701
1112
|
*/
|
|
702
1113
|
addWarning(warning: string) {
|
|
703
|
-
self.warnings
|
|
1114
|
+
if (!self.warnings.includes(warning)) {
|
|
1115
|
+
self.warnings = [...self.warnings, warning]
|
|
1116
|
+
}
|
|
704
1117
|
},
|
|
705
1118
|
|
|
706
1119
|
/**
|
|
@@ -713,7 +1126,7 @@ function stateModelFactory() {
|
|
|
713
1126
|
/**
|
|
714
1127
|
* #action
|
|
715
1128
|
* declare that this host restores the loaded documents itself, which
|
|
716
|
-
*
|
|
1129
|
+
* hides the "Not in the link" warning. See `hostCarriesData`
|
|
717
1130
|
*/
|
|
718
1131
|
setHostCarriesData(arg: boolean) {
|
|
719
1132
|
self.hostCarriesData = arg
|
|
@@ -723,9 +1136,9 @@ function stateModelFactory() {
|
|
|
723
1136
|
* #action
|
|
724
1137
|
* set mouse position (row, column) in the MSA
|
|
725
1138
|
*
|
|
726
|
-
*
|
|
727
|
-
*
|
|
728
|
-
*
|
|
1139
|
+
* Public API: a host calls this (and reads `mouseCol`) to sync hover with
|
|
1140
|
+
* its own view, such as a genome view or 3D structure. Keep the name and
|
|
1141
|
+
* signature stable.
|
|
729
1142
|
*/
|
|
730
1143
|
setMousePos(col?: number, row?: number) {
|
|
731
1144
|
self.mouseCol = col
|
|
@@ -736,9 +1149,9 @@ function stateModelFactory() {
|
|
|
736
1149
|
* #action
|
|
737
1150
|
* set highlighted columns
|
|
738
1151
|
*
|
|
739
|
-
*
|
|
740
|
-
* afterCreateAutoruns
|
|
741
|
-
*
|
|
1152
|
+
* Public API: jbrowse-plugin-msaview calls this from its
|
|
1153
|
+
* afterCreateAutoruns, and MSAViewer passes its `highlightColumns` prop
|
|
1154
|
+
* through it.
|
|
742
1155
|
*/
|
|
743
1156
|
setHighlightedColumns(columns?: number[]) {
|
|
744
1157
|
self.highlightedColumns = columns
|
|
@@ -749,12 +1162,18 @@ function stateModelFactory() {
|
|
|
749
1162
|
setHighlights(highlights: Highlight[]) {
|
|
750
1163
|
self.highlights.replace(highlights)
|
|
751
1164
|
},
|
|
1165
|
+
|
|
752
1166
|
/**
|
|
753
1167
|
* #action
|
|
754
|
-
*
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1168
|
+
* replace the clades the viewer marks (see docs/layers.md)
|
|
1169
|
+
*/
|
|
1170
|
+
setClades(clades: Clade[]) {
|
|
1171
|
+
self.clades.replace(clades)
|
|
1172
|
+
},
|
|
1173
|
+
/**
|
|
1174
|
+
* #action
|
|
1175
|
+
* show `highlights` for `owner`, replacing that owner's previous ones and
|
|
1176
|
+
* leaving other owners' in place
|
|
758
1177
|
*/
|
|
759
1178
|
applyHighlight(owner: string, highlights: Highlight[]) {
|
|
760
1179
|
self.transientHighlights = {
|
|
@@ -764,7 +1183,7 @@ function stateModelFactory() {
|
|
|
764
1183
|
},
|
|
765
1184
|
/**
|
|
766
1185
|
* #action
|
|
767
|
-
*
|
|
1186
|
+
* remove `owner`'s highlights, leaving other owners' in place
|
|
768
1187
|
*/
|
|
769
1188
|
clearHighlight(owner: string) {
|
|
770
1189
|
if (owner in self.transientHighlights) {
|
|
@@ -819,10 +1238,9 @@ function stateModelFactory() {
|
|
|
819
1238
|
|
|
820
1239
|
/**
|
|
821
1240
|
* #action
|
|
822
|
-
* switch to another alignment of a multi-alignment file (Stockholm).
|
|
823
|
-
*
|
|
824
|
-
* the
|
|
825
|
-
* reference row, the scroll position -- goes with it
|
|
1241
|
+
* switch to another alignment of a multi-alignment file (Stockholm).
|
|
1242
|
+
* Clears the collapsed node ids, the subtree in focus, the reference row
|
|
1243
|
+
* and the scroll position, which all refer to the previous alignment
|
|
826
1244
|
*/
|
|
827
1245
|
setCurrentAlignment(n: number) {
|
|
828
1246
|
if (n === self.currentAlignment) {
|
|
@@ -924,11 +1342,10 @@ function stateModelFactory() {
|
|
|
924
1342
|
|
|
925
1343
|
/**
|
|
926
1344
|
* #action
|
|
927
|
-
*
|
|
928
|
-
*
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
* read. An autorun parses this back into annotations.
|
|
1345
|
+
* store the GFF text in the snapshot like the alignment and tree. The
|
|
1346
|
+
* parsed annotations are volatile and a blob filehandle is cleared once
|
|
1347
|
+
* read, so the text is the only persisted copy. An autorun parses it
|
|
1348
|
+
* into annotations.
|
|
932
1349
|
*/
|
|
933
1350
|
setGFF(result: string) {
|
|
934
1351
|
self.data.setGFF(result)
|
|
@@ -960,16 +1377,14 @@ function stateModelFactory() {
|
|
|
960
1377
|
},
|
|
961
1378
|
/**
|
|
962
1379
|
* #getter
|
|
963
|
-
* whether
|
|
964
|
-
*
|
|
965
|
-
* has anything to warn about.
|
|
1380
|
+
* whether the host restores the loaded documents outside the snapshot.
|
|
1381
|
+
* When true, `unshareableData` is empty.
|
|
966
1382
|
*
|
|
967
|
-
* A simple host
|
|
968
|
-
*
|
|
969
|
-
* `.views` block
|
|
970
|
-
*
|
|
971
|
-
*
|
|
972
|
-
* elsewhere. `unshareableData` reads it off `self`, so an override wins.
|
|
1383
|
+
* A simple host sets `hostCarriesData`. A host where this depends on how
|
|
1384
|
+
* the view was opened overrides the getter in its own composed model's
|
|
1385
|
+
* `.views` block; jbrowse-plugin-msaview's indexed-location views refetch
|
|
1386
|
+
* from a URL the session holds, while its data-store views do not.
|
|
1387
|
+
* `unshareableData` reads it off `self`, so an override takes effect.
|
|
973
1388
|
*/
|
|
974
1389
|
get hostRestoresData() {
|
|
975
1390
|
return self.hostCarriesData
|
|
@@ -992,8 +1407,8 @@ function stateModelFactory() {
|
|
|
992
1407
|
* #getter
|
|
993
1408
|
*/
|
|
994
1409
|
get colorScheme() {
|
|
995
|
-
// colorSchemeName is a free string (menus, snapshots, URL params)
|
|
996
|
-
// back to the default
|
|
1410
|
+
// colorSchemeName is a free string (menus, snapshots, URL params), so a
|
|
1411
|
+
// stale name falls back to the default
|
|
997
1412
|
return (
|
|
998
1413
|
colorSchemes[self.colorSchemeName] ??
|
|
999
1414
|
colorSchemes[defaultColorSchemeName]!
|
|
@@ -1025,29 +1440,23 @@ function stateModelFactory() {
|
|
|
1025
1440
|
|
|
1026
1441
|
/**
|
|
1027
1442
|
* #getter
|
|
1028
|
-
*
|
|
1029
|
-
*
|
|
1030
|
-
*
|
|
1031
|
-
* rather than put megabytes of sequence into a session or a URL.
|
|
1443
|
+
* loaded documents left out of the snapshot, largest first. A file opened
|
|
1444
|
+
* from disk or pasted in becomes inline text, and DataModel drops an
|
|
1445
|
+
* inline document past `maxInlineSnapshotBytes`.
|
|
1032
1446
|
*
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1037
|
-
* keeps the filehandle and refetches through it.
|
|
1447
|
+
* The header lists these, and the standalone app stops rewriting the
|
|
1448
|
+
* address bar while the list is non-empty, so a copied link does not
|
|
1449
|
+
* open an empty viewer unannounced. A document fetched from a URL never
|
|
1450
|
+
* appears here, since the snapshot keeps its filehandle.
|
|
1038
1451
|
*
|
|
1039
|
-
*
|
|
1040
|
-
* means (see `hostRestoresData`) -- inside a session that reloads these
|
|
1041
|
-
* documents from somewhere the snapshot does not show, the warning is
|
|
1042
|
-
* simply wrong.
|
|
1452
|
+
* Empty when `hostRestoresData` is true.
|
|
1043
1453
|
*/
|
|
1044
1454
|
get unshareableData(): UnshareableData[] {
|
|
1045
1455
|
if (self.hostRestoresData) {
|
|
1046
1456
|
return []
|
|
1047
1457
|
}
|
|
1048
1458
|
const { data } = self
|
|
1049
|
-
//
|
|
1050
|
-
// inline document does, and left unreported the same way too
|
|
1459
|
+
// data tracks past the limit also leave the snapshot
|
|
1051
1460
|
const trackBytes = columnTrackSizes(self.columnTracks)
|
|
1052
1461
|
.filter(bytes => bytes > maxInlineSnapshotBytes)
|
|
1053
1462
|
.reduce((a, b) => a + b, 0)
|
|
@@ -1074,13 +1483,14 @@ function stateModelFactory() {
|
|
|
1074
1483
|
},
|
|
1075
1484
|
/**
|
|
1076
1485
|
* #getter
|
|
1077
|
-
* extra per
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
|
|
1083
|
-
|
|
1486
|
+
* the row table: extra fields per row, keyed by row name, which the
|
|
1487
|
+
* `encodings` channels read. It is stored as the JSON string
|
|
1488
|
+
* `data.treeMetadata`, the name that travels in existing links, so the
|
|
1489
|
+
* inline size limit and `treeMetadataFilehandle` cover it. labelWidthMap
|
|
1490
|
+
* reads it on every layout, so a malformed user-supplied file returns {}
|
|
1491
|
+
* instead of throwing out of rendering.
|
|
1492
|
+
*/
|
|
1493
|
+
get rowData(): Record<string, Record<string, string> | undefined> {
|
|
1084
1494
|
const text = self.data.treeMetadata
|
|
1085
1495
|
if (!text) {
|
|
1086
1496
|
return {}
|
|
@@ -1091,10 +1501,31 @@ function stateModelFactory() {
|
|
|
1091
1501
|
? (parsed as Record<string, Record<string, string> | undefined>)
|
|
1092
1502
|
: {}
|
|
1093
1503
|
} catch (e) {
|
|
1094
|
-
console.error('failed to parse
|
|
1504
|
+
console.error('failed to parse rowData', e)
|
|
1095
1505
|
return {}
|
|
1096
1506
|
}
|
|
1097
1507
|
},
|
|
1508
|
+
/**
|
|
1509
|
+
* #method
|
|
1510
|
+
* one row's fields, the single reader of the row table
|
|
1511
|
+
*/
|
|
1512
|
+
rowDataOf(name: string) {
|
|
1513
|
+
return this.rowData[name]
|
|
1514
|
+
},
|
|
1515
|
+
/**
|
|
1516
|
+
* #getter
|
|
1517
|
+
* the field names the row table carries, sorted, for a producer or a UI
|
|
1518
|
+
* choosing one to encode
|
|
1519
|
+
*/
|
|
1520
|
+
get rowFields(): string[] {
|
|
1521
|
+
const fields = new Set<string>()
|
|
1522
|
+
for (const row of Object.values(this.rowData)) {
|
|
1523
|
+
for (const field of Object.keys(row ?? {})) {
|
|
1524
|
+
fields.add(field)
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
return [...fields].sort((a, b) => a.localeCompare(b))
|
|
1528
|
+
},
|
|
1098
1529
|
/**
|
|
1099
1530
|
* #getter
|
|
1100
1531
|
*/
|
|
@@ -1144,6 +1575,37 @@ function stateModelFactory() {
|
|
|
1144
1575
|
this.leaves.map((leaf, index) => [leaf.data.name, index] as const),
|
|
1145
1576
|
)
|
|
1146
1577
|
},
|
|
1578
|
+
/**
|
|
1579
|
+
* #getter
|
|
1580
|
+
* `clades` resolved to the rows each one covers. The tip names resolve
|
|
1581
|
+
* against `tree` rather than `root`, so a clade whose ancestor the user
|
|
1582
|
+
* collapsed keeps its rows. One leaf pass over the tree serves every
|
|
1583
|
+
* clade. A `range` record names no node, so `collapse` and `focus`, which
|
|
1584
|
+
* need one, drop it.
|
|
1585
|
+
*/
|
|
1586
|
+
get resolvedClades(): ResolvedClade[] {
|
|
1587
|
+
if (self.clades.length === 0) {
|
|
1588
|
+
return noClades
|
|
1589
|
+
}
|
|
1590
|
+
return resolveClades(
|
|
1591
|
+
self.clades,
|
|
1592
|
+
hierarchy(this.tree, d => d.children),
|
|
1593
|
+
this.rowNamesSet,
|
|
1594
|
+
)
|
|
1595
|
+
},
|
|
1596
|
+
/**
|
|
1597
|
+
* #getter
|
|
1598
|
+
* the pixel column reserved at the right of the tree area for the bracket
|
|
1599
|
+
* mark, which the tip labels and the tree itself stay clear of. Zero
|
|
1600
|
+
* where no clade draws a bar or a label.
|
|
1601
|
+
*/
|
|
1602
|
+
get cladeGutterWidth() {
|
|
1603
|
+
return cladeGutterWidth({
|
|
1604
|
+
clades: this.resolvedClades,
|
|
1605
|
+
rowHeight: self.rowHeight,
|
|
1606
|
+
fontSize: this.fontSize,
|
|
1607
|
+
})
|
|
1608
|
+
},
|
|
1147
1609
|
/**
|
|
1148
1610
|
* #getter
|
|
1149
1611
|
*/
|
|
@@ -1177,46 +1639,143 @@ function stateModelFactory() {
|
|
|
1177
1639
|
* #getter
|
|
1178
1640
|
*/
|
|
1179
1641
|
get root() {
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1642
|
+
return buildTreeRoot(this.tree, self.collapsed, self.showOnly)
|
|
1643
|
+
},
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* #getter
|
|
1647
|
+
* height of the band the tree overview draws in, zero when it is off
|
|
1648
|
+
*/
|
|
1649
|
+
get treeOverviewHeight() {
|
|
1650
|
+
return self.showTreeOverview ? self.overviewHeight : 0
|
|
1651
|
+
},
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* #getter
|
|
1655
|
+
* the whole tree laid out for the overview, or undefined when the
|
|
1656
|
+
* overview is off. The focus is left out, so the focused subtree draws
|
|
1657
|
+
* inside the whole tree, and the collapsed clades are folded, since
|
|
1658
|
+
* those are rows the view no longer has. `x` is in tip-index space and
|
|
1659
|
+
* `len` is a fraction of the root-to-tip length, so one layout serves
|
|
1660
|
+
* any band size.
|
|
1661
|
+
*/
|
|
1662
|
+
get treeOverviewLayout() {
|
|
1663
|
+
if (!self.showTreeOverview) {
|
|
1664
|
+
return undefined
|
|
1665
|
+
}
|
|
1666
|
+
const root = buildTreeRoot(this.tree, self.collapsed)
|
|
1667
|
+
const numTips = leaves(root).length
|
|
1668
|
+
clusterLayout(root, numTips, 1)
|
|
1669
|
+
const rootLen = Math.max(root.data.length || 0, 0)
|
|
1670
|
+
const extent = maxLength(root) - rootLen
|
|
1671
|
+
setBrLength(root, -rootLen, extent ? 1 / extent : 0)
|
|
1672
|
+
return {
|
|
1673
|
+
root,
|
|
1674
|
+
numTips,
|
|
1675
|
+
maxDepthToLeaf: calcDepthToLeaf(root),
|
|
1676
|
+
showBranchLen: self.showBranchLen && extent > 0,
|
|
1189
1677
|
}
|
|
1678
|
+
},
|
|
1190
1679
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1680
|
+
/**
|
|
1681
|
+
* #getter
|
|
1682
|
+
* the `clades` highlights in the overview's own row space, which the
|
|
1683
|
+
* focus does not narrow
|
|
1684
|
+
*/
|
|
1685
|
+
get treeOverviewClades(): ResolvedClade[] {
|
|
1686
|
+
const layout = this.treeOverviewLayout
|
|
1687
|
+
if (!layout || self.clades.length === 0) {
|
|
1688
|
+
return noClades
|
|
1202
1689
|
}
|
|
1690
|
+
const rowNames = new Map(
|
|
1691
|
+
leaves(layout.root).map((leaf, index) => [leaf.data.name, index]),
|
|
1692
|
+
)
|
|
1693
|
+
return resolveClades(self.clades, layout.root, rowNames)
|
|
1694
|
+
},
|
|
1203
1695
|
|
|
1204
|
-
|
|
1696
|
+
/**
|
|
1697
|
+
* #getter
|
|
1698
|
+
* the inclusive tip rows the focused subtree covers in the overview,
|
|
1699
|
+
* which is the box drawn on it. undefined with no focus
|
|
1700
|
+
*/
|
|
1701
|
+
get treeOverviewFocusRows(): [number, number] | undefined {
|
|
1702
|
+
const layout = this.treeOverviewLayout
|
|
1703
|
+
if (!layout || !self.showOnly) {
|
|
1704
|
+
return undefined
|
|
1705
|
+
}
|
|
1706
|
+
const node = find(layout.root, n => n.data.id === self.showOnly)
|
|
1707
|
+
return node ? tipRange(node) : undefined
|
|
1708
|
+
},
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* #method
|
|
1712
|
+
* the subtree a point `y` pixels down the tree overview picks: the
|
|
1713
|
+
* deepest one whose tip range covers every row under that pixel, with
|
|
1714
|
+
* the rows it covers. A pixel stands for several tips on a large tree,
|
|
1715
|
+
* which is what keeps the pick off the individual tips. undefined when
|
|
1716
|
+
* the overview is off or the point picks the whole tree.
|
|
1717
|
+
*/
|
|
1718
|
+
treeOverviewHit(y: number) {
|
|
1719
|
+
const layout = this.treeOverviewLayout
|
|
1720
|
+
if (!layout) {
|
|
1721
|
+
return undefined
|
|
1722
|
+
}
|
|
1723
|
+
const { root, numTips } = layout
|
|
1724
|
+
const perPixel = numTips / self.overviewHeight
|
|
1725
|
+
const first = clamp(Math.floor(y * perPixel), 0, numTips - 1)
|
|
1726
|
+
const last = clamp(
|
|
1727
|
+
Math.ceil((y + 1) * perPixel) - 1,
|
|
1728
|
+
first,
|
|
1729
|
+
numTips - 1,
|
|
1730
|
+
)
|
|
1731
|
+
let node = nodeCoveringRows(root, first + 0.5, last + 0.5)
|
|
1732
|
+
// focusing one tip leaves a single row on screen, so the pick lifts to
|
|
1733
|
+
// the subtree that tip sits in
|
|
1734
|
+
while (!node.children && node.parent) {
|
|
1735
|
+
node = node.parent
|
|
1736
|
+
}
|
|
1737
|
+
return node === root
|
|
1738
|
+
? undefined
|
|
1739
|
+
: { id: node.data.id, rows: tipRange(node) }
|
|
1205
1740
|
},
|
|
1206
1741
|
|
|
1207
1742
|
/**
|
|
1208
1743
|
* #getter
|
|
1209
|
-
*
|
|
1744
|
+
* the pixel column the row panels occupy between the tree and the
|
|
1745
|
+
* alignment, the sum of each record's width
|
|
1746
|
+
*/
|
|
1747
|
+
get rowPanelsWidth() {
|
|
1748
|
+
return sum(self.rowPanels.map(p => rowPanelWidth(p, self.rowHeight)))
|
|
1749
|
+
},
|
|
1750
|
+
/**
|
|
1751
|
+
* #getter
|
|
1752
|
+
* height of the band the row panel headers draw in, which is zero with
|
|
1753
|
+
* no row panels and leaves the top area as it was
|
|
1754
|
+
*/
|
|
1755
|
+
get rowPanelsHeaderHeight() {
|
|
1756
|
+
return self.rowPanels.length > 0 ? rowPanelHeaderHeight : 0
|
|
1757
|
+
},
|
|
1758
|
+
/**
|
|
1759
|
+
* #getter
|
|
1760
|
+
* widget width minus the tree area and the row panels gives the space
|
|
1761
|
+
* for the MSA
|
|
1210
1762
|
*/
|
|
1211
1763
|
get msaAreaWidth() {
|
|
1212
|
-
return
|
|
1764
|
+
return (
|
|
1765
|
+
self.width -
|
|
1766
|
+
self.treeAreaWidth -
|
|
1767
|
+
this.rowPanelsWidth -
|
|
1768
|
+
self.resizeHandleWidth
|
|
1769
|
+
)
|
|
1213
1770
|
},
|
|
1214
1771
|
|
|
1215
1772
|
/**
|
|
1216
1773
|
* #getter
|
|
1774
|
+
* the right edge the tip labels end at, which is the tree area less the
|
|
1775
|
+
* margin and the bracket gutter
|
|
1217
1776
|
*/
|
|
1218
1777
|
get treeAreaWidthMinusMargin() {
|
|
1219
|
-
return self.treeAreaWidth - self.marginLeft
|
|
1778
|
+
return self.treeAreaWidth - self.marginLeft - this.cladeGutterWidth
|
|
1220
1779
|
},
|
|
1221
1780
|
/**
|
|
1222
1781
|
* #getter
|
|
@@ -1263,7 +1822,7 @@ function stateModelFactory() {
|
|
|
1263
1822
|
*/
|
|
1264
1823
|
get insertionPositions() {
|
|
1265
1824
|
const { blanks, rows } = this
|
|
1266
|
-
if (blanks.length === 0
|
|
1825
|
+
if (blanks.length === 0) {
|
|
1267
1826
|
return new Map<string, { pos: number; letters: string }[]>()
|
|
1268
1827
|
}
|
|
1269
1828
|
const result = new Map<string, { pos: number; letters: string }[]>()
|
|
@@ -1286,10 +1845,8 @@ function stateModelFactory() {
|
|
|
1286
1845
|
},
|
|
1287
1846
|
/**
|
|
1288
1847
|
* #getter
|
|
1289
|
-
* number of rows
|
|
1290
|
-
*
|
|
1291
|
-
* row of vertical space (drawn blank), so row hit-testing and fit-to-height
|
|
1292
|
-
* must count it.
|
|
1848
|
+
* number of rows on screen: the leaf count, which includes tree leaves
|
|
1849
|
+
* with no matching MSA row (drawn blank), unlike `rows.length`.
|
|
1293
1850
|
*/
|
|
1294
1851
|
get numRows() {
|
|
1295
1852
|
return this.leaves.length
|
|
@@ -1298,12 +1855,8 @@ function stateModelFactory() {
|
|
|
1298
1855
|
/**
|
|
1299
1856
|
* #method
|
|
1300
1857
|
* index of the global column holding each ungapped sequence position of a
|
|
1301
|
-
* row
|
|
1302
|
-
*
|
|
1303
|
-
*
|
|
1304
|
-
* Built per row, on the row asked for: the first lookup used to index
|
|
1305
|
-
* every row in the alignment. The cache is keyed on the parse the rows
|
|
1306
|
-
* came from, so a new alignment brings a new one.
|
|
1858
|
+
* row. The domain overlay resolves thousands of these per redraw. Built
|
|
1859
|
+
* lazily per row and cached on the parse.
|
|
1307
1860
|
*/
|
|
1308
1861
|
seqPosIndex(rowName: string): Int32Array | undefined {
|
|
1309
1862
|
const MSA = this.MSA
|
|
@@ -1329,10 +1882,9 @@ function stateModelFactory() {
|
|
|
1329
1882
|
|
|
1330
1883
|
/**
|
|
1331
1884
|
* #getter
|
|
1332
|
-
* every sequence the alignment
|
|
1333
|
-
*
|
|
1334
|
-
*
|
|
1335
|
-
* collapsing a clade hides rows, it does not delete their sequence
|
|
1885
|
+
* every sequence in the alignment, keyed by row name, including rows a
|
|
1886
|
+
* collapsed clade hides. `rows` holds only the rows on screen; lookups
|
|
1887
|
+
* by row name use this
|
|
1336
1888
|
*/
|
|
1337
1889
|
get rowMap() {
|
|
1338
1890
|
const MSA = this.MSA
|
|
@@ -1479,10 +2031,8 @@ function stateModelFactory() {
|
|
|
1479
2031
|
clusterLayout(r, this.totalHeight, self.treeWidth)
|
|
1480
2032
|
const max = this.rootToTipLength
|
|
1481
2033
|
const k = max ? self.treeWidth / max : 0
|
|
1482
|
-
// the displayed root starts at x=0
|
|
1483
|
-
//
|
|
1484
|
-
// node -- `root` hands out the cached parse, and writing to it made
|
|
1485
|
-
// showOnly shorten that branch for good
|
|
2034
|
+
// the displayed root starts at x=0, so subtract its length here; `root`
|
|
2035
|
+
// returns the cached parse, which must not be mutated
|
|
1486
2036
|
setBrLength(r, -Math.max(r.data.length || 0, 0), k)
|
|
1487
2037
|
// for each collapsed clade, record the pixel x-position of its farthest
|
|
1488
2038
|
// tip so the renderer can draw a triangle spanning the branch-length
|
|
@@ -1522,10 +2072,8 @@ function stateModelFactory() {
|
|
|
1522
2072
|
|
|
1523
2073
|
/**
|
|
1524
2074
|
* #getter
|
|
1525
|
-
* x-position of the farthest tip in a phylogram, px
|
|
1526
|
-
*
|
|
1527
|
-
* -- and 0 for a tree carrying no lengths at all, which draws as a
|
|
1528
|
-
* cladogram instead
|
|
2075
|
+
* x-position of the farthest tip in a phylogram, px: treeWidth, or 0 for
|
|
2076
|
+
* a tree with no branch lengths (drawn as a cladogram)
|
|
1529
2077
|
*/
|
|
1530
2078
|
get maxBranchLength() {
|
|
1531
2079
|
return this.rootToTipLength ? self.treeWidth : 0
|
|
@@ -1533,12 +2081,11 @@ function stateModelFactory() {
|
|
|
1533
2081
|
|
|
1534
2082
|
/**
|
|
1535
2083
|
* #getter
|
|
1536
|
-
* pixels per unit of branch length in the
|
|
1537
|
-
*
|
|
1538
|
-
* scale bar over the tree is drawn from it.
|
|
2084
|
+
* pixels per unit of branch length in the phylogram layout, 0 in
|
|
2085
|
+
* cladogram mode. The tree's scale bar uses it.
|
|
1539
2086
|
*/
|
|
1540
2087
|
get pxPerBranchLength() {
|
|
1541
|
-
const max =
|
|
2088
|
+
const max = this.rootToTipLength
|
|
1542
2089
|
return this.showBranchLenEffective && max ? self.treeWidth / max : 0
|
|
1543
2090
|
},
|
|
1544
2091
|
|
|
@@ -1593,26 +2140,157 @@ function stateModelFactory() {
|
|
|
1593
2140
|
return self.drawLabels && self.rowHeight >= minLetterRowHeight
|
|
1594
2141
|
},
|
|
1595
2142
|
}))
|
|
2143
|
+
.views(self => {
|
|
2144
|
+
// a spec is a frozen value, so it keys its computed until setColumnTracks
|
|
2145
|
+
// replaces it
|
|
2146
|
+
const columnTrackModels = new WeakMap<
|
|
2147
|
+
ColumnTrackSpec,
|
|
2148
|
+
IComputedValue<BasicTrack>
|
|
2149
|
+
>()
|
|
2150
|
+
return {
|
|
2151
|
+
/**
|
|
2152
|
+
* #getter
|
|
2153
|
+
* a data track's values or string, projected from its row's residues
|
|
2154
|
+
* onto alignment columns when it names a row
|
|
2155
|
+
*/
|
|
2156
|
+
get columnTrackContent() {
|
|
2157
|
+
const { MSA, blanks, hideGapsEffective } = self
|
|
2158
|
+
const width = MSA?.getWidth() ?? 0
|
|
2159
|
+
const project = <T>(track: ColumnTrackSpec, items: T[], fill: T) => {
|
|
2160
|
+
if (!track.row) {
|
|
2161
|
+
return items
|
|
2162
|
+
}
|
|
2163
|
+
const out = Array.from({ length: width }, () => fill)
|
|
2164
|
+
const index = self.seqPosIndex(track.row)
|
|
2165
|
+
items.forEach((item, seqPos) => {
|
|
2166
|
+
const col = index?.[seqPos]
|
|
2167
|
+
if (col !== undefined) {
|
|
2168
|
+
out[col] = item
|
|
2169
|
+
}
|
|
2170
|
+
})
|
|
2171
|
+
return out
|
|
2172
|
+
}
|
|
2173
|
+
const skip = <T>(items: T[]) =>
|
|
2174
|
+
hideGapsEffective ? dropBlanks(blanks, items) : items
|
|
2175
|
+
// an arc endpoint maps row residue -> column -> visible column.
|
|
2176
|
+
// visibleColsBefore, not globalColToVisibleCol, so an endpoint in a
|
|
2177
|
+
// hidden column moves to the neighboring visible one and the arc stays
|
|
2178
|
+
const resolve = (track: ColumnTrackSpec, pos: number) => {
|
|
2179
|
+
const col = track.row
|
|
2180
|
+
? self.seqPosIndex(track.row)?.[pos - 1]
|
|
2181
|
+
: pos - 1
|
|
2182
|
+
if (col === undefined || col < 0 || col >= width) {
|
|
2183
|
+
return undefined
|
|
2184
|
+
}
|
|
2185
|
+
return hideGapsEffective ? visibleColsBefore(blanks, col) : col
|
|
2186
|
+
}
|
|
2187
|
+
return new Map<
|
|
2188
|
+
string,
|
|
2189
|
+
{ values?: number[]; data?: string; arcs?: Arc[] }
|
|
2190
|
+
>(
|
|
2191
|
+
self.columnTracks.map(track => {
|
|
2192
|
+
if (track.kind === 'arc') {
|
|
2193
|
+
const arcs = (track.arcs ?? [])
|
|
2194
|
+
.map(arc => {
|
|
2195
|
+
const start = resolve(track, Math.min(arc.start, arc.end))
|
|
2196
|
+
const end = resolve(track, Math.max(arc.start, arc.end))
|
|
2197
|
+
return start !== undefined &&
|
|
2198
|
+
end !== undefined &&
|
|
2199
|
+
start < end
|
|
2200
|
+
? { start, end, color: arc.color }
|
|
2201
|
+
: undefined
|
|
2202
|
+
})
|
|
2203
|
+
.filter(notEmpty)
|
|
2204
|
+
return [track.id, { arcs }] as const
|
|
2205
|
+
}
|
|
2206
|
+
if (track.kind === 'bar') {
|
|
2207
|
+
const max = track.max ?? 1
|
|
2208
|
+
const values = skip(project(track, track.values ?? [], 0)).map(
|
|
2209
|
+
v => Math.min(1, Math.max(0, v / max)),
|
|
2210
|
+
)
|
|
2211
|
+
return [track.id, { values }] as const
|
|
2212
|
+
}
|
|
2213
|
+
const data = skip(
|
|
2214
|
+
project(track, (track.data ?? '').split(''), ' '),
|
|
2215
|
+
)
|
|
2216
|
+
return [track.id, { data: data.join('') }] as const
|
|
2217
|
+
}),
|
|
2218
|
+
)
|
|
2219
|
+
},
|
|
2220
|
+
/**
|
|
2221
|
+
* #method
|
|
2222
|
+
* the height a track draws at: what the user dragged its divider to,
|
|
2223
|
+
* then the height its snapshot asked for, then its kind's default. Only
|
|
2224
|
+
* a text track falls through to rowHeight
|
|
2225
|
+
*/
|
|
2226
|
+
trackHeight(kind: TrackKind, heightKey: string = kind, given?: number) {
|
|
2227
|
+
return (
|
|
2228
|
+
self.trackHeights.get(heightKey) ??
|
|
2229
|
+
given ??
|
|
2230
|
+
defaultTrackHeights[kind] ??
|
|
2231
|
+
self.rowHeight
|
|
2232
|
+
)
|
|
2233
|
+
},
|
|
2234
|
+
/**
|
|
2235
|
+
* #method
|
|
2236
|
+
* the track a column track spec draws as, computed once per spec. A
|
|
2237
|
+
* text track's height falls through to rowHeight, and its own computed
|
|
2238
|
+
* confines the vertical zoom to it, so a sibling keeps its object and
|
|
2239
|
+
* its canvas skips the redraw
|
|
2240
|
+
*/
|
|
2241
|
+
columnTrackModel(track: ColumnTrackSpec): BasicTrack {
|
|
2242
|
+
let computedModel = columnTrackModels.get(track)
|
|
2243
|
+
if (!computedModel) {
|
|
2244
|
+
computedModel = computed(() => {
|
|
2245
|
+
const heightKey = resizableKinds.has(track.kind)
|
|
2246
|
+
? ownHeightKey(track.id)
|
|
2247
|
+
: undefined
|
|
2248
|
+
const content = this.columnTrackContent.get(track.id)
|
|
2249
|
+
return {
|
|
2250
|
+
model: {
|
|
2251
|
+
id: track.id,
|
|
2252
|
+
name: track.name,
|
|
2253
|
+
kind: track.kind,
|
|
2254
|
+
heightKey,
|
|
2255
|
+
height: this.trackHeight(track.kind, heightKey, track.height),
|
|
2256
|
+
barColor: track.color,
|
|
2257
|
+
arcColor: track.color,
|
|
2258
|
+
customColorScheme: track.colors,
|
|
2259
|
+
data: content?.data,
|
|
2260
|
+
arcs: content?.arcs,
|
|
2261
|
+
},
|
|
2262
|
+
ReactComponent: TrackBlocks,
|
|
2263
|
+
}
|
|
2264
|
+
})
|
|
2265
|
+
columnTrackModels.set(track, computedModel)
|
|
2266
|
+
}
|
|
2267
|
+
return computedModel.get()
|
|
2268
|
+
},
|
|
2269
|
+
/**
|
|
2270
|
+
* #getter
|
|
2271
|
+
*/
|
|
2272
|
+
get columnTrackModels(): BasicTrack[] {
|
|
2273
|
+
return self.columnTracks.map(track => this.columnTrackModel(track))
|
|
2274
|
+
},
|
|
2275
|
+
}
|
|
2276
|
+
})
|
|
1596
2277
|
.views(self => ({
|
|
1597
2278
|
/**
|
|
1598
2279
|
* #getter
|
|
1599
2280
|
*/
|
|
1600
2281
|
get labelWidthMap() {
|
|
1601
|
-
const { showTreeText, leaves
|
|
1602
|
-
// gated on the
|
|
1603
|
-
// gutter
|
|
1604
|
-
//
|
|
1605
|
-
// Measured at a fixed reference size and scaled by labelWidthScale:
|
|
1606
|
-
// re-measuring every leaf on every vertical-zoom frame cost ~200ms on a
|
|
2282
|
+
const { showTreeText, leaves } = self
|
|
2283
|
+
// gated on the renderer's label condition, so hidden labels reserve no
|
|
2284
|
+
// gutter. Measured once at a reference size and scaled by
|
|
2285
|
+
// labelWidthScale: re-measuring per vertical-zoom frame cost ~200ms on a
|
|
1607
2286
|
// 50k-leaf tree
|
|
1608
2287
|
return showTreeText
|
|
1609
2288
|
? new Map(
|
|
1610
2289
|
leaves.map(node => {
|
|
1611
2290
|
const { name } = node.data
|
|
1612
2291
|
// `||`, matching renderTreeLabels: an empty genome falls back
|
|
1613
|
-
// to the row name
|
|
1614
|
-
|
|
1615
|
-
const displayName = treeMetadata[name]?.genome || name
|
|
2292
|
+
// to the row name
|
|
2293
|
+
const displayName = self.rowDataOf(name)?.genome || name
|
|
1616
2294
|
return [
|
|
1617
2295
|
name,
|
|
1618
2296
|
measureTextCanvas(displayName, labelReferenceFontSize),
|
|
@@ -1632,11 +2310,9 @@ function stateModelFactory() {
|
|
|
1632
2310
|
},
|
|
1633
2311
|
|
|
1634
2312
|
get labelsWidth() {
|
|
1635
|
-
// a loop, not Math.max(...
|
|
1636
|
-
//
|
|
1637
|
-
//
|
|
1638
|
-
// "RangeError: Maximum call stack size exceeded" out of a getter the
|
|
1639
|
-
// treeWidth autorun reads on load
|
|
2313
|
+
// a loop, not Math.max(...): spreading passes one argument per leaf,
|
|
2314
|
+
// and the ~125k argument limit throws RangeError on the 230k-tip COVID
|
|
2315
|
+
// tree
|
|
1640
2316
|
let max = 0
|
|
1641
2317
|
for (const width of this.labelWidthMap.values()) {
|
|
1642
2318
|
if (width > max) {
|
|
@@ -1662,9 +2338,8 @@ function stateModelFactory() {
|
|
|
1662
2338
|
|
|
1663
2339
|
/**
|
|
1664
2340
|
* #getter
|
|
1665
|
-
* the base pairs of the consensus secondary structure, as arcs
|
|
1666
|
-
*
|
|
1667
|
-
* the pairs land in the same visible column space the text track does
|
|
2341
|
+
* the base pairs of the consensus secondary structure, as arcs, in
|
|
2342
|
+
* visible column space (hidden columns are removed before parsing)
|
|
1668
2343
|
*/
|
|
1669
2344
|
get secondaryStructureArcs(): Arc[] | undefined {
|
|
1670
2345
|
const { blanks, hideGapsEffective } = self
|
|
@@ -1688,9 +2363,8 @@ function stateModelFactory() {
|
|
|
1688
2363
|
const { MSA, hideGapsEffective, blanks } = self
|
|
1689
2364
|
const tracks = (MSA?.tracks ?? []).filter(t => !!t.data)
|
|
1690
2365
|
if (tracks.length === 0) {
|
|
1691
|
-
// reading rowHeight
|
|
1692
|
-
// list
|
|
1693
|
-
// changing
|
|
2366
|
+
// return before reading rowHeight, so zooming does not rebuild the
|
|
2367
|
+
// list and redraw every track canvas
|
|
1694
2368
|
return []
|
|
1695
2369
|
}
|
|
1696
2370
|
const { rowHeight } = self
|
|
@@ -1707,113 +2381,9 @@ function stateModelFactory() {
|
|
|
1707
2381
|
|
|
1708
2382
|
/**
|
|
1709
2383
|
* #getter
|
|
1710
|
-
*
|
|
1711
|
-
*
|
|
1712
|
-
|
|
1713
|
-
get columnTrackContent() {
|
|
1714
|
-
const { MSA, blanks, hideGapsEffective } = self
|
|
1715
|
-
const width = MSA?.getWidth() ?? 0
|
|
1716
|
-
const project = <T>(track: ColumnTrackSpec, items: T[], fill: T) => {
|
|
1717
|
-
if (!track.row) {
|
|
1718
|
-
return items
|
|
1719
|
-
}
|
|
1720
|
-
const out = Array.from({ length: width }, () => fill)
|
|
1721
|
-
const index = self.seqPosIndex(track.row)
|
|
1722
|
-
items.forEach((item, seqPos) => {
|
|
1723
|
-
const col = index?.[seqPos]
|
|
1724
|
-
if (col !== undefined) {
|
|
1725
|
-
out[col] = item
|
|
1726
|
-
}
|
|
1727
|
-
})
|
|
1728
|
-
return out
|
|
1729
|
-
}
|
|
1730
|
-
const skip = <T>(items: T[]) =>
|
|
1731
|
-
hideGapsEffective ? dropBlanks(blanks, items) : items
|
|
1732
|
-
// an arc names two positions rather than one per column, so it takes
|
|
1733
|
-
// the same two steps the arrays take -- a row's residues onto columns,
|
|
1734
|
-
// then columns onto the visible ones -- as a lookup. visibleColsBefore,
|
|
1735
|
-
// not globalColToVisibleCol: an endpoint in a hidden column collapses
|
|
1736
|
-
// to where that column went instead of taking the whole arc with it
|
|
1737
|
-
const resolve = (track: ColumnTrackSpec, pos: number) => {
|
|
1738
|
-
const col = track.row
|
|
1739
|
-
? self.seqPosIndex(track.row)?.[pos - 1]
|
|
1740
|
-
: pos - 1
|
|
1741
|
-
if (col === undefined || col < 0 || col >= width) {
|
|
1742
|
-
return undefined
|
|
1743
|
-
}
|
|
1744
|
-
return hideGapsEffective ? visibleColsBefore(blanks, col) : col
|
|
1745
|
-
}
|
|
1746
|
-
return new Map<
|
|
1747
|
-
string,
|
|
1748
|
-
{ values?: number[]; data?: string; arcs?: Arc[] }
|
|
1749
|
-
>(
|
|
1750
|
-
self.columnTracks.map(track => {
|
|
1751
|
-
if (track.kind === 'arc') {
|
|
1752
|
-
const arcs = (track.arcs ?? [])
|
|
1753
|
-
.map(arc => {
|
|
1754
|
-
const start = resolve(track, Math.min(arc.start, arc.end))
|
|
1755
|
-
const end = resolve(track, Math.max(arc.start, arc.end))
|
|
1756
|
-
return start !== undefined && end !== undefined && start < end
|
|
1757
|
-
? { start, end, color: arc.color }
|
|
1758
|
-
: undefined
|
|
1759
|
-
})
|
|
1760
|
-
.filter(notEmpty)
|
|
1761
|
-
return [track.id, { arcs }] as const
|
|
1762
|
-
}
|
|
1763
|
-
if (track.kind === 'bar') {
|
|
1764
|
-
const max = track.max ?? 1
|
|
1765
|
-
const values = skip(project(track, track.values ?? [], 0)).map(
|
|
1766
|
-
v => Math.min(1, Math.max(0, v / max)),
|
|
1767
|
-
)
|
|
1768
|
-
return [track.id, { values }] as const
|
|
1769
|
-
}
|
|
1770
|
-
const data = skip(project(track, (track.data ?? '').split(''), ' '))
|
|
1771
|
-
return [track.id, { data: data.join('') }] as const
|
|
1772
|
-
}),
|
|
1773
|
-
)
|
|
1774
|
-
},
|
|
1775
|
-
/**
|
|
1776
|
-
* #getter
|
|
1777
|
-
*/
|
|
1778
|
-
get columnTrackModels(): BasicTrack[] {
|
|
1779
|
-
// read per kind, not up front: a text track is the only kind sized by
|
|
1780
|
-
// the row height, and reading it here rebuilt every data track on every
|
|
1781
|
-
// vertical zoom step
|
|
1782
|
-
const defaultHeight = (kind: ColumnTrackSpec['kind']) =>
|
|
1783
|
-
kind === 'bar'
|
|
1784
|
-
? self.conservationTrackHeight
|
|
1785
|
-
: kind === 'arc'
|
|
1786
|
-
? self.arcTrackHeight
|
|
1787
|
-
: self.rowHeight
|
|
1788
|
-
return self.columnTracks.map(track => ({
|
|
1789
|
-
model: {
|
|
1790
|
-
id: track.id,
|
|
1791
|
-
name: track.name,
|
|
1792
|
-
kind: track.kind,
|
|
1793
|
-
height:
|
|
1794
|
-
self.columnTrackHeights[track.id] ??
|
|
1795
|
-
track.height ??
|
|
1796
|
-
defaultHeight(track.kind),
|
|
1797
|
-
// the spec has one `color`; bar and arc are separate track models
|
|
1798
|
-
// that read it under their own name
|
|
1799
|
-
barColor: track.color,
|
|
1800
|
-
arcColor: track.color,
|
|
1801
|
-
customColorScheme: track.colors,
|
|
1802
|
-
data: this.columnTrackContent.get(track.id)?.data,
|
|
1803
|
-
arcs: this.columnTrackContent.get(track.id)?.arcs,
|
|
1804
|
-
},
|
|
1805
|
-
ReactComponent: TrackBlocks,
|
|
1806
|
-
}))
|
|
1807
|
-
},
|
|
1808
|
-
/**
|
|
1809
|
-
* #getter
|
|
1810
|
-
*/
|
|
1811
|
-
/**
|
|
1812
|
-
* #getter
|
|
1813
|
-
* the consensus secondary structure as a track, when there is one. Its
|
|
1814
|
-
* own getter so the object keeps its identity across a zoom: the canvas
|
|
1815
|
-
* redraws on the track it is handed changing, and rebuilding these
|
|
1816
|
-
* alongside everything else made every zoom frame redraw every track
|
|
2384
|
+
* the consensus secondary structure as a track, when there is one. A
|
|
2385
|
+
* separate getter keeps the object stable across zoom, so its canvas
|
|
2386
|
+
* does not redraw
|
|
1817
2387
|
*/
|
|
1818
2388
|
get basePairTrackModels(): BasicTrack[] {
|
|
1819
2389
|
const arcs = this.secondaryStructureArcs
|
|
@@ -1824,7 +2394,8 @@ function stateModelFactory() {
|
|
|
1824
2394
|
id: 'base-pairs',
|
|
1825
2395
|
name: 'Base pairs',
|
|
1826
2396
|
kind: 'arc' as const,
|
|
1827
|
-
|
|
2397
|
+
heightKey: 'arc',
|
|
2398
|
+
height: self.trackHeight('arc'),
|
|
1828
2399
|
arcs,
|
|
1829
2400
|
},
|
|
1830
2401
|
ReactComponent: TrackBlocks,
|
|
@@ -1835,8 +2406,8 @@ function stateModelFactory() {
|
|
|
1835
2406
|
|
|
1836
2407
|
/**
|
|
1837
2408
|
* #getter
|
|
1838
|
-
* the tracks computed from the alignment
|
|
1839
|
-
*
|
|
2409
|
+
* the tracks computed from the alignment; they depend on their heights
|
|
2410
|
+
* and the alphabet, not on zoom
|
|
1840
2411
|
*/
|
|
1841
2412
|
get computedTrackModels(): BasicTrack[] {
|
|
1842
2413
|
return [
|
|
@@ -1844,7 +2415,6 @@ function stateModelFactory() {
|
|
|
1844
2415
|
id: 'conservation',
|
|
1845
2416
|
name: 'Conservation',
|
|
1846
2417
|
kind: 'bar' as const,
|
|
1847
|
-
height: self.conservationTrackHeight,
|
|
1848
2418
|
barColor: 'gray',
|
|
1849
2419
|
},
|
|
1850
2420
|
...(self.sequenceType === 'amino'
|
|
@@ -1853,7 +2423,6 @@ function stateModelFactory() {
|
|
|
1853
2423
|
id: 'property-conservation',
|
|
1854
2424
|
name: 'Property conservation',
|
|
1855
2425
|
kind: 'bar' as const,
|
|
1856
|
-
height: self.conservationTrackHeight,
|
|
1857
2426
|
barColor: '#6a51a3',
|
|
1858
2427
|
},
|
|
1859
2428
|
]
|
|
@@ -1862,24 +2431,33 @@ function stateModelFactory() {
|
|
|
1862
2431
|
id: 'sequence-logo',
|
|
1863
2432
|
name: 'Sequence logo',
|
|
1864
2433
|
kind: 'logo' as const,
|
|
1865
|
-
height: self.sequenceLogoTrackHeight,
|
|
1866
2434
|
},
|
|
1867
2435
|
// last, so it sits against the alignment it numbers
|
|
1868
2436
|
{
|
|
1869
2437
|
id: 'position-ruler',
|
|
1870
2438
|
name: 'Position',
|
|
1871
2439
|
kind: 'ruler' as const,
|
|
1872
|
-
height: 20,
|
|
1873
2440
|
},
|
|
1874
|
-
].map(model => ({
|
|
2441
|
+
].map(model => ({
|
|
2442
|
+
// every computed track of a kind shares that kind's height, so the
|
|
2443
|
+
// kind is its key
|
|
2444
|
+
model: {
|
|
2445
|
+
...model,
|
|
2446
|
+
heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
|
|
2447
|
+
height: self.trackHeight(model.kind),
|
|
2448
|
+
},
|
|
2449
|
+
ReactComponent: TrackBlocks,
|
|
2450
|
+
}))
|
|
1875
2451
|
},
|
|
1876
2452
|
|
|
1877
2453
|
get tracks(): BasicTrack[] {
|
|
1878
2454
|
return [
|
|
1879
2455
|
...this.adapterTrackModels,
|
|
1880
2456
|
...this.basePairTrackModels,
|
|
1881
|
-
...
|
|
1882
|
-
|
|
2457
|
+
...self.columnTrackModels,
|
|
2458
|
+
// every computed track reads the alignment's columns, and a tree, a
|
|
2459
|
+
// GFF and a features panel draw a figure with none
|
|
2460
|
+
...(self.numColumns > 0 ? this.computedTrackModels : []),
|
|
1883
2461
|
]
|
|
1884
2462
|
},
|
|
1885
2463
|
|
|
@@ -1919,19 +2497,18 @@ function stateModelFactory() {
|
|
|
1919
2497
|
* Convert a visible column to a row-specific sequence position (0-based).
|
|
1920
2498
|
* Returns undefined if the position is a gap in the sequence.
|
|
1921
2499
|
*
|
|
1922
|
-
*
|
|
1923
|
-
*
|
|
1924
|
-
*
|
|
1925
|
-
*
|
|
2500
|
+
* Public API, like the sibling converters (visibleColToGlobalCol,
|
|
2501
|
+
* seqPosToVisibleCol, globalColToVisibleCol, seqPosToGlobalCol) hosts
|
|
2502
|
+
* use to translate between columns and residue positions. Keep them
|
|
2503
|
+
* stable.
|
|
1926
2504
|
*
|
|
1927
2505
|
* @param rowName - The name of the row
|
|
1928
2506
|
* @param visibleCol - The visible column index
|
|
1929
2507
|
* @returns The sequence position (0-based), or undefined if it's a gap
|
|
1930
2508
|
*/
|
|
1931
2509
|
visibleColToSeqPos(rowName: string, visibleCol: number) {
|
|
1932
|
-
//
|
|
1933
|
-
//
|
|
1934
|
-
// is the whole frame
|
|
2510
|
+
// binary search: this runs on every mouse move, and scanning a
|
|
2511
|
+
// 30k-column row per event takes the whole frame
|
|
1935
2512
|
return seqPosOfGlobalCol(
|
|
1936
2513
|
self.seqPosIndex(rowName),
|
|
1937
2514
|
this.visibleColToGlobalCol(visibleCol),
|
|
@@ -1971,10 +2548,9 @@ function stateModelFactory() {
|
|
|
1971
2548
|
|
|
1972
2549
|
/**
|
|
1973
2550
|
* #method
|
|
1974
|
-
* Convert a visible column index (what a mouse handler reports)
|
|
1975
|
-
* column of the full alignment.
|
|
1976
|
-
*
|
|
1977
|
-
* hop before indexing it.
|
|
2551
|
+
* Convert a visible column index (what a mouse handler reports) to a
|
|
2552
|
+
* column of the full alignment. A host indexing its own per-column data
|
|
2553
|
+
* needs this when columns are hidden.
|
|
1978
2554
|
*
|
|
1979
2555
|
* @param visibleCol - The visible column index
|
|
1980
2556
|
* @returns The global column index in the full MSA
|
|
@@ -1989,8 +2565,7 @@ function stateModelFactory() {
|
|
|
1989
2565
|
/**
|
|
1990
2566
|
* #method
|
|
1991
2567
|
* Convert a sequence position (ungapped) to a global column index.
|
|
1992
|
-
* Returns undefined for a row the alignment does not have
|
|
1993
|
-
* anyway is how a mistyped or stale row name came to highlight column 0.
|
|
2568
|
+
* Returns undefined for a row name the alignment does not have.
|
|
1994
2569
|
*
|
|
1995
2570
|
* @param rowName - The name of the row
|
|
1996
2571
|
* @param seqPos - The sequence position (0-based, ungapped)
|
|
@@ -2023,12 +2598,42 @@ function stateModelFactory() {
|
|
|
2023
2598
|
: this.globalColToVisibleCol(globalCol)
|
|
2024
2599
|
},
|
|
2025
2600
|
|
|
2601
|
+
/**
|
|
2602
|
+
* #method
|
|
2603
|
+
* the visible columns a span covers, in highlight coordinates: `start`
|
|
2604
|
+
* and `end` are 1-based residues of `row`, or columns of the file
|
|
2605
|
+
* without it. A fractional position, as a zoom gesture reports one,
|
|
2606
|
+
* widens to the whole residue or column it falls in. A span entirely on
|
|
2607
|
+
* hidden columns, or naming a row the alignment lacks, gives undefined.
|
|
2608
|
+
*/
|
|
2609
|
+
visibleSpan({ row, start: rawStart, end: rawEnd }: Region) {
|
|
2610
|
+
const { blanks } = self
|
|
2611
|
+
const start = Math.max(1, Math.floor(rawStart))
|
|
2612
|
+
const end = Math.ceil(rawEnd)
|
|
2613
|
+
let startGlobal = start - 1
|
|
2614
|
+
let endGlobal = end - 1
|
|
2615
|
+
if (row !== undefined) {
|
|
2616
|
+
const rowStart = this.seqPosToGlobalCol(row, start - 1)
|
|
2617
|
+
const rowEnd = this.seqPosToGlobalCol(row, end - 1)
|
|
2618
|
+
if (rowStart === undefined || rowEnd === undefined) {
|
|
2619
|
+
return undefined
|
|
2620
|
+
}
|
|
2621
|
+
startGlobal = rowStart
|
|
2622
|
+
endGlobal = rowEnd
|
|
2623
|
+
}
|
|
2624
|
+
const startCol =
|
|
2625
|
+
this.globalColToVisibleCol(startGlobal) ??
|
|
2626
|
+
visibleColsBefore(blanks, startGlobal)
|
|
2627
|
+
const endCol =
|
|
2628
|
+
this.globalColToVisibleCol(endGlobal) ??
|
|
2629
|
+
visibleColsBefore(blanks, endGlobal) - 1
|
|
2630
|
+
return startCol <= endCol ? { startCol, endCol } : undefined
|
|
2631
|
+
},
|
|
2632
|
+
|
|
2026
2633
|
/**
|
|
2027
2634
|
* #getter
|
|
2028
|
-
*
|
|
2029
|
-
*
|
|
2030
|
-
* longer agree the lookups have to refuse, and refusing invisibly is how
|
|
2031
|
-
* "there is no structure here" gets confused with "this data is stale".
|
|
2635
|
+
* why each ignored residue mapping is ignored, so a host can tell a
|
|
2636
|
+
* missing structure from a mapping made against a different alignment.
|
|
2032
2637
|
*/
|
|
2033
2638
|
get residueMappingProblems(): ResidueMappingProblem[] {
|
|
2034
2639
|
const problems: ResidueMappingProblem[] = []
|
|
@@ -2080,10 +2685,8 @@ function stateModelFactory() {
|
|
|
2080
2685
|
|
|
2081
2686
|
/**
|
|
2082
2687
|
* #getter
|
|
2083
|
-
* the mappings that
|
|
2084
|
-
*
|
|
2085
|
-
* itself, since the rest of the mapping is still a claim about residues
|
|
2086
|
-
* that exist.
|
|
2688
|
+
* the mappings that fit the loaded alignment. A row-level problem drops
|
|
2689
|
+
* the whole mapping; a malformed segment drops only that segment.
|
|
2087
2690
|
*/
|
|
2088
2691
|
get usableResidueMappings(): ResidueMapping[] {
|
|
2089
2692
|
const unusable = new Set(
|
|
@@ -2101,9 +2704,8 @@ function stateModelFactory() {
|
|
|
2101
2704
|
|
|
2102
2705
|
/**
|
|
2103
2706
|
* #getter
|
|
2104
|
-
* the structures
|
|
2105
|
-
*
|
|
2106
|
-
* a host that means a particular one has to name it.
|
|
2707
|
+
* the structures with usable mappings. A row can map onto several, such
|
|
2708
|
+
* as an experimental entry and a predicted model.
|
|
2107
2709
|
*/
|
|
2108
2710
|
get mappedStructures() {
|
|
2109
2711
|
return this.usableResidueMappings.map(m => ({
|
|
@@ -2114,18 +2716,12 @@ function stateModelFactory() {
|
|
|
2114
2716
|
|
|
2115
2717
|
/**
|
|
2116
2718
|
* #method
|
|
2117
|
-
* The structure residue a row residue
|
|
2118
|
-
*
|
|
2119
|
-
*
|
|
2719
|
+
* The structure residue for a row residue. Returns undefined when no
|
|
2720
|
+
* segment covers `seqPos`, or when the row maps onto several structures
|
|
2721
|
+
* and `structureId` does not pick one (see `mappedStructures`).
|
|
2120
2722
|
*
|
|
2121
|
-
*
|
|
2122
|
-
*
|
|
2123
|
-
* and returning whichever came first would be the same class of wrong,
|
|
2124
|
-
* quieter. Name one with `structureId`, or use `mappedStructures` to see
|
|
2125
|
-
* what there is.
|
|
2126
|
-
*
|
|
2127
|
-
* Positions are 1-based, as `residueMappings` and `highlights` are --
|
|
2128
|
-
* note that the column helpers above take 0-based ones.
|
|
2723
|
+
* Positions are 1-based, like `residueMappings` and `highlights`; the
|
|
2724
|
+
* column helpers above are 0-based.
|
|
2129
2725
|
*
|
|
2130
2726
|
* @param rowName - The alignment row
|
|
2131
2727
|
* @param seqPos - Residue of that row, 1-based
|
|
@@ -2164,10 +2760,10 @@ function stateModelFactory() {
|
|
|
2164
2760
|
|
|
2165
2761
|
/**
|
|
2166
2762
|
* #method
|
|
2167
|
-
* The row residue a structure residue
|
|
2168
|
-
*
|
|
2169
|
-
*
|
|
2170
|
-
*
|
|
2763
|
+
* The row residue for a structure residue; the inverse of
|
|
2764
|
+
* `structureResidue`, returning undefined in the same cases. `asymId`
|
|
2765
|
+
* picks a chain when several mappings share an entry id, as in a
|
|
2766
|
+
* homodimer.
|
|
2171
2767
|
*
|
|
2172
2768
|
* @param structureId - The structure's id, as the mapping names it
|
|
2173
2769
|
* @param position - Residue of that structure, 1-based label_seq_id
|
|
@@ -2203,17 +2799,39 @@ function stateModelFactory() {
|
|
|
2203
2799
|
.views(self => ({
|
|
2204
2800
|
/**
|
|
2205
2801
|
* #getter
|
|
2206
|
-
* the
|
|
2207
|
-
*
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2802
|
+
* the branch-length scale bar over the tree, undefined in cladogram mode
|
|
2803
|
+
* or when the tree area is too narrow for one
|
|
2804
|
+
*/
|
|
2805
|
+
get treeScaleBar() {
|
|
2806
|
+
return scaleBarLength(
|
|
2807
|
+
self.pxPerBranchLength,
|
|
2808
|
+
self.treeAreaWidth - self.marginLeft * 2,
|
|
2809
|
+
)
|
|
2810
|
+
},
|
|
2811
|
+
/**
|
|
2812
|
+
* #getter
|
|
2813
|
+
* the band across the top: the minimap over the alignment, the row panel
|
|
2814
|
+
* headers, and the tree overview stacked on the scale bar, as tall as the
|
|
2815
|
+
* tallest of the three
|
|
2816
|
+
*/
|
|
2817
|
+
get topBandHeight() {
|
|
2818
|
+
return Math.max(
|
|
2819
|
+
self.showHorizontalScrollbar ? self.minimapHeight : 0,
|
|
2820
|
+
self.rowPanelsHeaderHeight,
|
|
2821
|
+
self.treeOverviewHeight +
|
|
2822
|
+
(this.treeScaleBar ? treeScaleBarHeight : 0),
|
|
2823
|
+
)
|
|
2824
|
+
},
|
|
2825
|
+
/**
|
|
2826
|
+
* #getter
|
|
2827
|
+
* the vertical space for alignment rows: the widget height less the
|
|
2828
|
+
* header, the top band and the tracks. Shared by blocksY, maxScrollY, the
|
|
2829
|
+
* vertical scrollbar and fitVertically.
|
|
2212
2830
|
*/
|
|
2213
2831
|
get msaAreaHeight() {
|
|
2214
2832
|
return (
|
|
2215
2833
|
self.height -
|
|
2216
|
-
|
|
2834
|
+
this.topBandHeight -
|
|
2217
2835
|
self.headerHeight -
|
|
2218
2836
|
this.totalTrackAreaHeight
|
|
2219
2837
|
)
|
|
@@ -2226,13 +2844,12 @@ function stateModelFactory() {
|
|
|
2226
2844
|
return sum(self.turnedOnTracks.map(r => r.model.height))
|
|
2227
2845
|
},
|
|
2228
2846
|
/**
|
|
2229
|
-
* one representative annotation per accession,
|
|
2230
|
-
*
|
|
2847
|
+
* one representative annotation per accession, for the legend, the
|
|
2848
|
+
* filter dialog and the palettes
|
|
2231
2849
|
*/
|
|
2232
2850
|
get annotationTypes() {
|
|
2233
|
-
// first occurrence wins
|
|
2234
|
-
//
|
|
2235
|
-
// them, and those agree across an accession's instances
|
|
2851
|
+
// first occurrence wins; only name, description and segment start are
|
|
2852
|
+
// read from it
|
|
2236
2853
|
const types = new Map<string, Annotation>()
|
|
2237
2854
|
for (const annot of self.annotations) {
|
|
2238
2855
|
if (!types.has(annot.accession)) {
|
|
@@ -2263,11 +2880,8 @@ function stateModelFactory() {
|
|
|
2263
2880
|
* #getter
|
|
2264
2881
|
*/
|
|
2265
2882
|
get dataInitialized() {
|
|
2266
|
-
// truthiness, not `!== ''`:
|
|
2267
|
-
//
|
|
2268
|
-
// that inlined a large alignment comes back `undefined` here -- which
|
|
2269
|
-
// `!== ''` reads as initialized and renders an empty view instead of
|
|
2270
|
-
// the import form
|
|
2883
|
+
// truthiness, not `!== ''`: DataModel drops an inline document over
|
|
2884
|
+
// 50kb from the snapshot, so a restored session can hold `undefined`
|
|
2271
2885
|
return !!(self.data.msa || self.data.tree) && !self.error
|
|
2272
2886
|
},
|
|
2273
2887
|
/**
|
|
@@ -2317,12 +2931,20 @@ function stateModelFactory() {
|
|
|
2317
2931
|
},
|
|
2318
2932
|
/**
|
|
2319
2933
|
* #getter
|
|
2320
|
-
* most-negative allowed scrollY,
|
|
2321
|
-
* letting the whole alignment scroll off the top.
|
|
2934
|
+
* most-negative allowed scrollY, which keeps the last row in view
|
|
2322
2935
|
*/
|
|
2323
2936
|
get maxScrollY() {
|
|
2324
2937
|
return Math.min(-self.totalHeight + self.msaAreaHeight, 0)
|
|
2325
2938
|
},
|
|
2939
|
+
/**
|
|
2940
|
+
* #getter
|
|
2941
|
+
* axis a wheel zoom scales, for ctrl+wheel as much as for scroll-zoom.
|
|
2942
|
+
* With scroll-zoom off the toolbar shows no axis, so ctrl+wheel takes
|
|
2943
|
+
* both.
|
|
2944
|
+
*/
|
|
2945
|
+
get wheelZoomAxis(): ScrollZoomAxis {
|
|
2946
|
+
return self.scrollZoom ? self.scrollZoomAxis : 'both'
|
|
2947
|
+
},
|
|
2326
2948
|
}))
|
|
2327
2949
|
.actions(self => ({
|
|
2328
2950
|
/**
|
|
@@ -2339,15 +2961,20 @@ function stateModelFactory() {
|
|
|
2339
2961
|
self.scrollZoom = arg
|
|
2340
2962
|
},
|
|
2341
2963
|
|
|
2964
|
+
/**
|
|
2965
|
+
* #action
|
|
2966
|
+
*/
|
|
2967
|
+
setScrollZoomAxis(arg: ScrollZoomAxis) {
|
|
2968
|
+
self.scrollZoomAxis = arg
|
|
2969
|
+
},
|
|
2970
|
+
|
|
2342
2971
|
/**
|
|
2343
2972
|
* #action
|
|
2344
2973
|
* set hovered tree node and its descendants
|
|
2345
2974
|
*/
|
|
2346
2975
|
setHoveredTreeNode(nodeId?: string) {
|
|
2347
|
-
//
|
|
2348
|
-
//
|
|
2349
|
-
// and a fresh object here invalidates hoveredRowIndices and redraws the
|
|
2350
|
-
// tree and MSA overlays. Re-hovering the same node is the common case
|
|
2976
|
+
// called on every tree mousemove; `find` walks the whole hierarchy and a
|
|
2977
|
+
// new object redraws the tree and MSA overlays
|
|
2351
2978
|
if (nodeId === self.hoveredTreeNode?.nodeId) {
|
|
2352
2979
|
return
|
|
2353
2980
|
}
|
|
@@ -2367,15 +2994,12 @@ function stateModelFactory() {
|
|
|
2367
2994
|
/**
|
|
2368
2995
|
* #action
|
|
2369
2996
|
* Calculate a neighbor joining tree from the current MSA using BLOSUM62
|
|
2370
|
-
* distances.
|
|
2371
|
-
* cubic and runs on the main thread,
|
|
2372
|
-
* with no
|
|
2373
|
-
* for it anyway.
|
|
2997
|
+
* distances. Throws above `maxNeighborJoiningRows`: the join loop is
|
|
2998
|
+
* cubic and runs on the main thread, and 800 rows freeze the tab for ten
|
|
2999
|
+
* seconds with no cancel.
|
|
2374
3000
|
*/
|
|
2375
3001
|
calculateNeighborJoiningTreeFromMSA() {
|
|
2376
|
-
// every sequence
|
|
2377
|
-
// clade is a display state, and building the tree from what it leaves
|
|
2378
|
-
// showing drops the sequences it hides out of the result
|
|
3002
|
+
// every sequence, including rows in collapsed clades
|
|
2379
3003
|
const rows = [...self.rowMap]
|
|
2380
3004
|
if (rows.length < 2) {
|
|
2381
3005
|
throw new Error('Need at least 2 sequences to build a tree')
|
|
@@ -2390,10 +3014,9 @@ function stateModelFactory() {
|
|
|
2390
3014
|
|
|
2391
3015
|
/**
|
|
2392
3016
|
* #action
|
|
2393
|
-
* swap in a different tree over the same alignment.
|
|
2394
|
-
*
|
|
2395
|
-
*
|
|
2396
|
-
* happens to sit there -- the ids go with the tree they name.
|
|
3017
|
+
* swap in a different tree over the same alignment. Clears `collapsed`
|
|
3018
|
+
* and `showOnly`, since path-derived node ids (node-0-0-1) from the old
|
|
3019
|
+
* tree would match unrelated nodes in the new one.
|
|
2397
3020
|
*/
|
|
2398
3021
|
replaceTree(newick: string) {
|
|
2399
3022
|
transaction(() => {
|
|
@@ -2462,26 +3085,36 @@ function stateModelFactory() {
|
|
|
2462
3085
|
* #action
|
|
2463
3086
|
* Smoothly zoom by a continuous scaleFactor. The column under the cursor
|
|
2464
3087
|
* (offsetX/offsetY, px relative to the MSA area) stays anchored
|
|
2465
|
-
* horizontally. Vertically the anchor is biased toward
|
|
2466
|
-
* alignment nearly fits the viewport,
|
|
2467
|
-
*
|
|
2468
|
-
*
|
|
2469
|
-
*
|
|
2470
|
-
|
|
2471
|
-
|
|
3088
|
+
* horizontally. Vertically the anchor is biased toward y=0 when the
|
|
3089
|
+
* alignment nearly fits the viewport, fading to cursor-anchoring as the
|
|
3090
|
+
* alignment grows taller than the viewport.
|
|
3091
|
+
* Drives wheel/trackpad-pinch zoom. `axis` holds one cell dimension
|
|
3092
|
+
* fixed; the held axis still re-anchors its scroll offset, since the
|
|
3093
|
+
* other one can change how much of the alignment fits.
|
|
3094
|
+
*/
|
|
3095
|
+
zoomToPos(
|
|
3096
|
+
scaleFactor: number,
|
|
3097
|
+
offsetX: number,
|
|
3098
|
+
offsetY: number,
|
|
3099
|
+
axis: ScrollZoomAxis = 'both',
|
|
3100
|
+
) {
|
|
2472
3101
|
transaction(() => {
|
|
2473
3102
|
const colInView = (-self.scrollX + offsetX) / self.colWidth
|
|
2474
3103
|
const rowInView = (-self.scrollY + offsetY) / self.rowHeight
|
|
2475
|
-
|
|
2476
|
-
self.colWidth
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
3104
|
+
if (axis !== 'vertical') {
|
|
3105
|
+
self.colWidth = clamp(
|
|
3106
|
+
self.colWidth * scaleFactor,
|
|
3107
|
+
minColWidth,
|
|
3108
|
+
maxCellSize,
|
|
3109
|
+
)
|
|
3110
|
+
}
|
|
3111
|
+
if (axis !== 'horizontal') {
|
|
3112
|
+
self.rowHeight = clamp(
|
|
3113
|
+
self.rowHeight * scaleFactor,
|
|
3114
|
+
minRowHeight,
|
|
3115
|
+
maxCellSize,
|
|
3116
|
+
)
|
|
3117
|
+
}
|
|
2485
3118
|
self.scrollX = clamp(
|
|
2486
3119
|
offsetX - colInView * self.colWidth,
|
|
2487
3120
|
self.maxScrollX,
|
|
@@ -2489,9 +3122,7 @@ function stateModelFactory() {
|
|
|
2489
3122
|
)
|
|
2490
3123
|
|
|
2491
3124
|
const anchoredScrollY = offsetY - rowInView * self.rowHeight
|
|
2492
|
-
// maxScrollY is
|
|
2493
|
-
// overflows, so -maxScrollY is exactly that overflow past the
|
|
2494
|
-
// scrollable MSA viewport (0 when it fits)
|
|
3125
|
+
// -maxScrollY is the overflow past the viewport, 0 when it fits
|
|
2495
3126
|
const overflow = Math.max(0, -self.maxScrollY)
|
|
2496
3127
|
const visibleHeight = self.totalHeight - overflow
|
|
2497
3128
|
const topBias =
|
|
@@ -2520,13 +3151,11 @@ function stateModelFactory() {
|
|
|
2520
3151
|
|
|
2521
3152
|
/**
|
|
2522
3153
|
* #action
|
|
2523
|
-
* Set the overlay annotations (an empty list clears them).
|
|
2524
|
-
*
|
|
2525
|
-
* InterProScan, GFF, user uploads, NCBI CDD.
|
|
3154
|
+
* Set the overlay annotations (an empty list clears them). InterProScan,
|
|
3155
|
+
* GFF, user uploads and NCBI CDD all arrive here as Annotation[].
|
|
2526
3156
|
*
|
|
2527
|
-
*
|
|
2528
|
-
*
|
|
2529
|
-
* shared with the overlay hidden reopened with it drawn.
|
|
3157
|
+
* Leaves `showDomains` alone, because a restored snapshot reloads its
|
|
3158
|
+
* GFF and must keep a hidden overlay hidden.
|
|
2530
3159
|
*/
|
|
2531
3160
|
setAnnotations(annotations: Annotation[]) {
|
|
2532
3161
|
self.annotations = annotations
|
|
@@ -2566,6 +3195,28 @@ function stateModelFactory() {
|
|
|
2566
3195
|
setColumnTracks(tracks: ColumnTrackSpec[]) {
|
|
2567
3196
|
self.columnTracks.replace(tracks)
|
|
2568
3197
|
},
|
|
3198
|
+
/**
|
|
3199
|
+
* #action
|
|
3200
|
+
* replace the row table, which the model keeps as the JSON string
|
|
3201
|
+
* `data.treeMetadata` (see docs/layers.md)
|
|
3202
|
+
*/
|
|
3203
|
+
setRowData(rowData: Record<string, Record<string, string>>) {
|
|
3204
|
+
self.data.setTreeMetadata(JSON.stringify(rowData))
|
|
3205
|
+
},
|
|
3206
|
+
/**
|
|
3207
|
+
* #action
|
|
3208
|
+
* replace what the viewer's marks read from the row table
|
|
3209
|
+
*/
|
|
3210
|
+
setEncodings(encodings: Encoding[]) {
|
|
3211
|
+
self.encodings.replace(encodings)
|
|
3212
|
+
},
|
|
3213
|
+
/**
|
|
3214
|
+
* #action
|
|
3215
|
+
* replace the panels drawn between the tree and the alignment
|
|
3216
|
+
*/
|
|
3217
|
+
setRowPanels(panels: RowPanelSpec[]) {
|
|
3218
|
+
self.rowPanels.replace(panels)
|
|
3219
|
+
},
|
|
2569
3220
|
/**
|
|
2570
3221
|
* #action
|
|
2571
3222
|
* replace the alignment<->structure correspondence (see docs/layers.md)
|
|
@@ -2577,8 +3228,6 @@ function stateModelFactory() {
|
|
|
2577
3228
|
* #action
|
|
2578
3229
|
*/
|
|
2579
3230
|
toggleTrack(id: string) {
|
|
2580
|
-
// the stored value is "is off", so the current shown state is exactly
|
|
2581
|
-
// what the flipped entry should hold
|
|
2582
3231
|
const defaultOff = self.MSA?.tracks.find(t => t.id === id)?.defaultOff
|
|
2583
3232
|
self.turnedOffTracks.set(
|
|
2584
3233
|
id,
|
|
@@ -2601,18 +3250,78 @@ function stateModelFactory() {
|
|
|
2601
3250
|
},
|
|
2602
3251
|
/**
|
|
2603
3252
|
* #getter
|
|
2604
|
-
* width of the alignment canvas
|
|
2605
|
-
* scrollbar
|
|
2606
|
-
*
|
|
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
|
|
2607
3256
|
*/
|
|
2608
3257
|
get msaCanvasWidth() {
|
|
2609
3258
|
return self.msaAreaWidth - this.verticalScrollbarWidth
|
|
2610
3259
|
},
|
|
3260
|
+
/**
|
|
3261
|
+
* #method
|
|
3262
|
+
* the cell at a visible column and row index, in the coordinates a host
|
|
3263
|
+
* writes highlights in
|
|
3264
|
+
*/
|
|
3265
|
+
cellAt(visibleCol: number, rowIndex?: number): Cell {
|
|
3266
|
+
const column = self.visibleColToGlobalCol(visibleCol)
|
|
3267
|
+
const row =
|
|
3268
|
+
rowIndex === undefined ? undefined : self.leaves[rowIndex]?.data.name
|
|
3269
|
+
return row === undefined
|
|
3270
|
+
? { column: column + 1 }
|
|
3271
|
+
: {
|
|
3272
|
+
column: column + 1,
|
|
3273
|
+
row,
|
|
3274
|
+
residue: self.visibleColToSeqPosOneBased(row, visibleCol),
|
|
3275
|
+
letter: self.rowMap.get(row)?.[column],
|
|
3276
|
+
}
|
|
3277
|
+
},
|
|
3278
|
+
/**
|
|
3279
|
+
* #getter
|
|
3280
|
+
* the cell under the pointer. Public API: MSAViewer's onCellHover
|
|
3281
|
+
* reports it.
|
|
3282
|
+
*/
|
|
3283
|
+
get hoveredCell() {
|
|
3284
|
+
const { mouseCol, mouseRow } = self
|
|
3285
|
+
return mouseCol === undefined
|
|
3286
|
+
? undefined
|
|
3287
|
+
: this.cellAt(mouseCol, mouseRow)
|
|
3288
|
+
},
|
|
3289
|
+
/**
|
|
3290
|
+
* #getter
|
|
3291
|
+
* the cell a click pinned. Public API: MSAViewer's onCellClick reports it.
|
|
3292
|
+
*/
|
|
3293
|
+
get clickedCell() {
|
|
3294
|
+
const { mouseClickCol, mouseClickRow } = self
|
|
3295
|
+
return mouseClickCol === undefined
|
|
3296
|
+
? undefined
|
|
3297
|
+
: this.cellAt(mouseClickCol, mouseClickRow)
|
|
3298
|
+
},
|
|
3299
|
+
/**
|
|
3300
|
+
* #getter
|
|
3301
|
+
* the columns on screen. Public API: MSAViewer's onViewportChange reports
|
|
3302
|
+
* it.
|
|
3303
|
+
*/
|
|
3304
|
+
get viewport(): Viewport | undefined {
|
|
3305
|
+
const { scrollX, colWidth, numColumns, viewInitialized } = self
|
|
3306
|
+
if (numColumns === 0 || !viewInitialized) {
|
|
3307
|
+
return undefined
|
|
3308
|
+
}
|
|
3309
|
+
const { xStart, xEnd } = visibleColRange({
|
|
3310
|
+
offsetX: -scrollX,
|
|
3311
|
+
blockWidth: this.msaCanvasWidth,
|
|
3312
|
+
colWidth,
|
|
3313
|
+
})
|
|
3314
|
+
const last = Math.max(0, Math.min(xEnd, numColumns) - 1)
|
|
3315
|
+
return {
|
|
3316
|
+
startColumn: self.visibleColToGlobalCol(Math.min(xStart, last)) + 1,
|
|
3317
|
+
endColumn: self.visibleColToGlobalCol(last) + 1,
|
|
3318
|
+
}
|
|
3319
|
+
},
|
|
2611
3320
|
/**
|
|
2612
3321
|
* #getter
|
|
2613
3322
|
* ordinal segment types (exons etc.), ordered by sequence position so
|
|
2614
|
-
* exon-1..exon-14
|
|
2615
|
-
* labeled by number
|
|
3323
|
+
* exon-1..exon-14 run left-to-right; colored by alternating shade and
|
|
3324
|
+
* labeled by number, with no legend row
|
|
2616
3325
|
*/
|
|
2617
3326
|
get segmentDomainTypes() {
|
|
2618
3327
|
return [...self.annotationTypes.values()]
|
|
@@ -2641,11 +3350,42 @@ function stateModelFactory() {
|
|
|
2641
3350
|
)
|
|
2642
3351
|
return { ...segments, ...categorical }
|
|
2643
3352
|
},
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
3353
|
+
/**
|
|
3354
|
+
* #getter
|
|
3355
|
+
* the encoding coloring the overlay's spans, undefined when none does,
|
|
3356
|
+
* which leaves each span the color its accession takes in `fillPalette`
|
|
3357
|
+
*/
|
|
3358
|
+
get featureFillEncoding(): ResolvedEncoding | undefined {
|
|
3359
|
+
return this.resolvedEncodings.find(e => e.channel === 'featureFill')
|
|
3360
|
+
},
|
|
3361
|
+
|
|
3362
|
+
/**
|
|
3363
|
+
* #getter
|
|
3364
|
+
* the fill and outline of every feature's span: its own GFF `color=`
|
|
3365
|
+
* first, then the `featureFill` scale, then the accession palette.
|
|
3366
|
+
* Computed once per change of the features, the encodings or the palette
|
|
3367
|
+
*/
|
|
3368
|
+
get featureColors(): Map<Annotation, { fill: string; stroke: string }> {
|
|
3369
|
+
return featureColorMap(
|
|
3370
|
+
self.filteredAnnotations,
|
|
3371
|
+
this.featureFillEncoding,
|
|
3372
|
+
this.fillPalette,
|
|
3373
|
+
)
|
|
3374
|
+
},
|
|
3375
|
+
|
|
3376
|
+
/**
|
|
3377
|
+
* #getter
|
|
3378
|
+
* the text the `featureLabel` channel draws inside each span, undefined
|
|
3379
|
+
* when no encoding names the channel. A data channel, so it draws
|
|
3380
|
+
* whether or not the residue letters do
|
|
3381
|
+
*/
|
|
3382
|
+
get featureLabels(): Map<Annotation, string> | undefined {
|
|
3383
|
+
const encoding = this.resolvedEncodings.find(
|
|
3384
|
+
e => e.channel === 'featureLabel',
|
|
3385
|
+
)
|
|
3386
|
+
return encoding
|
|
3387
|
+
? featureLabelMap(self.filteredAnnotations, encoding.field)
|
|
3388
|
+
: undefined
|
|
2649
3389
|
},
|
|
2650
3390
|
|
|
2651
3391
|
/**
|
|
@@ -2665,9 +3405,9 @@ function stateModelFactory() {
|
|
|
2665
3405
|
/**
|
|
2666
3406
|
* #getter
|
|
2667
3407
|
* the domain types currently drawn on the alignment (filtered-on), shared
|
|
2668
|
-
* by the on-screen legend and the SVG export legend
|
|
2669
|
-
*
|
|
2670
|
-
*
|
|
3408
|
+
* by the on-screen legend and the SVG export legend: the categorical types
|
|
3409
|
+
* ordered by sequence position. Ordinal segments (exons) are numbered on
|
|
3410
|
+
* the band instead
|
|
2671
3411
|
*/
|
|
2672
3412
|
get visibleDomainTypes() {
|
|
2673
3413
|
return this.categoricalDomainTypes
|
|
@@ -2675,15 +3415,94 @@ function stateModelFactory() {
|
|
|
2675
3415
|
.toSorted((a, b) => a.start - b.start)
|
|
2676
3416
|
},
|
|
2677
3417
|
|
|
3418
|
+
/**
|
|
3419
|
+
* #getter
|
|
3420
|
+
* the categorical color keys drawn for this view, shared by the on-screen
|
|
3421
|
+
* legend overlay and the SVG export's reserved column. The domain overlay
|
|
3422
|
+
* produces the first, listing the `featureFill` scale where an encoding
|
|
3423
|
+
* names one. Every field a row-table encoding or a row panel reads
|
|
3424
|
+
* produces one more, so two channels over one field, or two strips over
|
|
3425
|
+
* it, list that field once. A strip's `legend` names the title it lists
|
|
3426
|
+
* under, so strips over eight fields with one set of colors list one
|
|
3427
|
+
* legend between them
|
|
3428
|
+
*/
|
|
3429
|
+
get legends(): Legend[] {
|
|
3430
|
+
const { featureFillEncoding, fillPalette, visibleDomainTypes } = this
|
|
3431
|
+
const byKey = new Map<string, Legend>()
|
|
3432
|
+
// the accession palette keys on no field, so it takes a key of its own
|
|
3433
|
+
const add = (
|
|
3434
|
+
field: string | undefined,
|
|
3435
|
+
entries: LegendEntry[],
|
|
3436
|
+
id?: string,
|
|
3437
|
+
) => {
|
|
3438
|
+
if (entries.length === 0) {
|
|
3439
|
+
return
|
|
3440
|
+
}
|
|
3441
|
+
const key = field === undefined ? 'domains' : `field:${field}`
|
|
3442
|
+
const legend = byKey.get(key)
|
|
3443
|
+
if (legend) {
|
|
3444
|
+
const seen = new Set(legend.entries.map(e => e.id))
|
|
3445
|
+
legend.entries.push(...entries.filter(e => !seen.has(e.id)))
|
|
3446
|
+
} else {
|
|
3447
|
+
byKey.set(key, {
|
|
3448
|
+
id: id ?? (field === undefined ? 'domains' : `rowData-${field}`),
|
|
3449
|
+
title: field ?? 'Domains',
|
|
3450
|
+
entries: [...entries],
|
|
3451
|
+
})
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
// the overlay draws the domain key; with no columns it draws nothing,
|
|
3455
|
+
// and a features panel taking the same colors lists them instead
|
|
3456
|
+
if (self.actuallyShowDomains && self.numColumns > 0) {
|
|
3457
|
+
add(
|
|
3458
|
+
featureFillEncoding?.field,
|
|
3459
|
+
featureFillEncoding
|
|
3460
|
+
? featureFillEncoding.legend
|
|
3461
|
+
: visibleDomainTypes.map(d => ({
|
|
3462
|
+
id: d.accession,
|
|
3463
|
+
label: d.name,
|
|
3464
|
+
color: fillPalette[d.accession]!,
|
|
3465
|
+
})),
|
|
3466
|
+
'domains',
|
|
3467
|
+
)
|
|
3468
|
+
}
|
|
3469
|
+
for (const { channel, field, legend } of this.resolvedEncodings) {
|
|
3470
|
+
if (!featureChannels.has(channel)) {
|
|
3471
|
+
add(field, legend)
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
for (const { legendTitle, legend } of this.resolvedRowPanels) {
|
|
3475
|
+
add(legendTitle, legend)
|
|
3476
|
+
}
|
|
3477
|
+
return [...byKey.values()]
|
|
3478
|
+
},
|
|
3479
|
+
|
|
3480
|
+
/**
|
|
3481
|
+
* #getter
|
|
3482
|
+
* whether the overlay marks each domain with a bar under its row instead
|
|
3483
|
+
* of filling the row behind the letters. Letter-color mode hands the
|
|
3484
|
+
* background to the color scheme, so a filled box would paint over it and
|
|
3485
|
+
* leave the setting with nothing to show. Sub-row layout already stacks
|
|
3486
|
+
* the boxes clear of the letters, and with the letters too small to draw
|
|
3487
|
+
* the filled box is the only thing left to read.
|
|
3488
|
+
*/
|
|
3489
|
+
get domainUnderline() {
|
|
3490
|
+
return (
|
|
3491
|
+
self.actuallyShowDomains &&
|
|
3492
|
+
!self.bgColor &&
|
|
3493
|
+
!self.subFeatureRows &&
|
|
3494
|
+
self.showMsaLetters
|
|
3495
|
+
)
|
|
3496
|
+
},
|
|
3497
|
+
|
|
2678
3498
|
/**
|
|
2679
3499
|
* #getter
|
|
2680
3500
|
* every filtered-on annotation resolved to the visible column span it is
|
|
2681
3501
|
* drawn across, keyed by row name. Each row is ordered longest-first so a
|
|
2682
|
-
* short domain
|
|
2683
|
-
*
|
|
2684
|
-
* block
|
|
2685
|
-
*
|
|
2686
|
-
* needs to keep residues readable on top of the boxes.
|
|
3502
|
+
* nested short domain draws on top, and each band carries the lane the
|
|
3503
|
+
* sub-row layout puts it in. Resolved once here instead of per canvas
|
|
3504
|
+
* block per redraw; the letter renderer also reads the band colors to
|
|
3505
|
+
* pick legible letter colors.
|
|
2687
3506
|
*/
|
|
2688
3507
|
get domainBands() {
|
|
2689
3508
|
const { blanks } = self
|
|
@@ -2694,14 +3513,11 @@ function stateModelFactory() {
|
|
|
2694
3513
|
const rowBands = annotations
|
|
2695
3514
|
.toSorted((a, b) => len(b) - len(a))
|
|
2696
3515
|
.map(annotation => {
|
|
2697
|
-
// annotation positions are 1-based and inclusive.
|
|
2698
|
-
//
|
|
2699
|
-
//
|
|
2700
|
-
//
|
|
2701
|
-
//
|
|
2702
|
-
// collapses onto the neighbouring boundary instead of dropping
|
|
2703
|
-
// the band. A band whose every column is hidden spans nothing
|
|
2704
|
-
// and is left out, as is one naming a row the alignment lacks.
|
|
3516
|
+
// annotation positions are 1-based and inclusive. endCol is
|
|
3517
|
+
// exclusive, one past the last residue's column, so the band does
|
|
3518
|
+
// not extend over a following gap run. An endpoint in a hidden
|
|
3519
|
+
// column moves to the neighboring boundary. Bands with no visible
|
|
3520
|
+
// columns, or naming a missing row, are dropped.
|
|
2705
3521
|
const start = self.seqPosToGlobalCol(name, annotation.start - 1)
|
|
2706
3522
|
const end = self.seqPosToGlobalCol(name, annotation.end - 1)
|
|
2707
3523
|
if (start === undefined || end === undefined) {
|
|
@@ -2714,11 +3530,10 @@ function stateModelFactory() {
|
|
|
2714
3530
|
: undefined
|
|
2715
3531
|
})
|
|
2716
3532
|
.filter(notEmpty)
|
|
2717
|
-
// numbered after
|
|
2718
|
-
// not leave an empty sub-row behind it
|
|
3533
|
+
// numbered after filtering, so a dropped band leaves no gap
|
|
2719
3534
|
.map((band, stackIndex) => ({ ...band, stackIndex }))
|
|
2720
3535
|
if (rowBands.length > 0) {
|
|
2721
|
-
bands.set(name, rowBands)
|
|
3536
|
+
bands.set(name, packDomainLanes(rowBands))
|
|
2722
3537
|
}
|
|
2723
3538
|
}
|
|
2724
3539
|
return bands
|
|
@@ -2755,9 +3570,6 @@ function stateModelFactory() {
|
|
|
2755
3570
|
const hits = (this.domainBands.get(name) ?? [])
|
|
2756
3571
|
.filter(b => mouseCol >= b.startCol && mouseCol < b.endCol)
|
|
2757
3572
|
.map(b => b.annotation)
|
|
2758
|
-
// the shared empty array, so moving the mouse across an alignment with
|
|
2759
|
-
// no annotations does not hand every canvas block a new value to
|
|
2760
|
-
// re-render on
|
|
2761
3573
|
return hits.length > 0 ? hits : noDomains
|
|
2762
3574
|
},
|
|
2763
3575
|
|
|
@@ -2776,8 +3588,7 @@ function stateModelFactory() {
|
|
|
2776
3588
|
* #getter
|
|
2777
3589
|
* row indices highlighted by the current tree hover (a hovered internal
|
|
2778
3590
|
* node highlights every tip below it). Shared by the tree and MSA overlay
|
|
2779
|
-
* canvases
|
|
2780
|
-
* name->index map rather than rebuilding a lookup on each mouse move.
|
|
3591
|
+
* canvases, via the memoized name->index map.
|
|
2781
3592
|
*/
|
|
2782
3593
|
get hoveredRowIndices() {
|
|
2783
3594
|
const { hoveredTreeNode, rowNamesSet } = self
|
|
@@ -2791,8 +3602,8 @@ function stateModelFactory() {
|
|
|
2791
3602
|
/**
|
|
2792
3603
|
* #getter
|
|
2793
3604
|
* contiguous runs of `highlightedColumns`, so a run of highlighted columns
|
|
2794
|
-
* draws as one bordered band.
|
|
2795
|
-
*
|
|
3605
|
+
* draws as one bordered band. Memoized because the overlay canvas redraws
|
|
3606
|
+
* on every mouse move.
|
|
2796
3607
|
*/
|
|
2797
3608
|
get highlightedColumnRuns() {
|
|
2798
3609
|
const { highlightedColumns } = self
|
|
@@ -2818,13 +3629,8 @@ function stateModelFactory() {
|
|
|
2818
3629
|
* is dropped. Row names that match no row are ignored.
|
|
2819
3630
|
*/
|
|
2820
3631
|
get resolvedHighlights(): ResolvedHighlight[] {
|
|
2821
|
-
const {
|
|
2822
|
-
|
|
2823
|
-
const visible = self.globalColToVisibleCol(globalCol)
|
|
2824
|
-
return visible ?? visibleColsBefore(blanks, globalCol)
|
|
2825
|
-
}
|
|
2826
|
-
// the document's own highlights first, then what each owner is showing
|
|
2827
|
-
// right now, so a hover draws over a persisted band rather than under it
|
|
3632
|
+
const { rowNamesSet, transientHighlights } = self
|
|
3633
|
+
// persisted highlights first, so transient ones draw on top
|
|
2828
3634
|
const all = [
|
|
2829
3635
|
...self.highlights,
|
|
2830
3636
|
...Object.values(transientHighlights).flat(),
|
|
@@ -2840,72 +3646,254 @@ function stateModelFactory() {
|
|
|
2840
3646
|
if (start === undefined || end === undefined) {
|
|
2841
3647
|
return []
|
|
2842
3648
|
}
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
3649
|
+
const span = self.visibleSpan({ row, start, end })
|
|
3650
|
+
return span ? [{ ...base, ...span, rowIndices: [] }] : []
|
|
3651
|
+
})
|
|
3652
|
+
},
|
|
3653
|
+
|
|
3654
|
+
/**
|
|
3655
|
+
* #method
|
|
3656
|
+
* per-column summary statistics: consensus residue and its identity
|
|
3657
|
+
* fraction, both conservation scores, gap fraction, and the sorted non-gap
|
|
3658
|
+
* residue distribution. undefined past the end of the alignment or for an
|
|
3659
|
+
* all-gap column.
|
|
3660
|
+
*/
|
|
3661
|
+
columnStatsAt(col: number): ColumnStats | undefined {
|
|
3662
|
+
const { colStats, conservation, propertyConservation } = self
|
|
3663
|
+
return columnStats({
|
|
3664
|
+
col,
|
|
3665
|
+
colStats,
|
|
3666
|
+
conservation,
|
|
3667
|
+
propertyConservation,
|
|
3668
|
+
})
|
|
3669
|
+
},
|
|
3670
|
+
|
|
3671
|
+
/**
|
|
3672
|
+
* #method
|
|
3673
|
+
*/
|
|
3674
|
+
getRowData(name: string) {
|
|
3675
|
+
return {
|
|
3676
|
+
data: self.MSA?.getRowData(name),
|
|
3677
|
+
rowData: self.rowDataOf(name),
|
|
3678
|
+
}
|
|
3679
|
+
},
|
|
3680
|
+
|
|
3681
|
+
/**
|
|
3682
|
+
* #getter
|
|
3683
|
+
* each encoding with its scale resolved against the values its field
|
|
3684
|
+
* takes: a feature channel reads them across the features drawn, every
|
|
3685
|
+
* other channel across the row table. Resolved once per change of that
|
|
3686
|
+
* table or the encodings, never per row per frame.
|
|
3687
|
+
*/
|
|
3688
|
+
get resolvedEncodings(): ResolvedEncoding[] {
|
|
3689
|
+
const rows = Object.values(self.rowData)
|
|
3690
|
+
const features = self.filteredAnnotations
|
|
3691
|
+
return self.encodings.map(encoding => ({
|
|
3692
|
+
...encoding,
|
|
3693
|
+
...resolveScale(
|
|
3694
|
+
encoding.scale,
|
|
3695
|
+
featureChannels.has(encoding.channel)
|
|
3696
|
+
? features
|
|
3697
|
+
.map(a => featureField(a, encoding.field))
|
|
3698
|
+
.filter(notEmpty)
|
|
3699
|
+
: rows.map(row => row?.[encoding.field]).filter(notEmpty),
|
|
3700
|
+
),
|
|
3701
|
+
}))
|
|
3702
|
+
},
|
|
3703
|
+
|
|
3704
|
+
/**
|
|
3705
|
+
* #getter
|
|
3706
|
+
* each row panel with its scale resolved against the values its field
|
|
3707
|
+
* takes across the row table, giving the color per row name, the pixel
|
|
3708
|
+
* column it draws in, and the entries its legend lists. Resolved once
|
|
3709
|
+
* per change of that table or the panels, never per block per frame.
|
|
3710
|
+
*/
|
|
3711
|
+
get resolvedRowPanels(): ResolvedRowPanel[] {
|
|
3712
|
+
const rows = Object.entries(self.rowData)
|
|
3713
|
+
const { fillPalette, featureFillEncoding, featureLabels } = this
|
|
3714
|
+
const annotations = self.filteredAnnotations
|
|
3715
|
+
let offsetX = 0
|
|
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
|
|
3720
|
+
if (panel.kind === 'features') {
|
|
3721
|
+
const color = panel.encoding?.color
|
|
3722
|
+
const encoding = color
|
|
3723
|
+
? resolveFeatureScale(color.field, color.scale, annotations)
|
|
3724
|
+
: featureFillEncoding
|
|
3725
|
+
const label = panel.encoding?.label
|
|
3726
|
+
const align = panel.transform?.find(t => t.type === 'align')
|
|
3727
|
+
return {
|
|
3728
|
+
...base,
|
|
3729
|
+
kind: panel.kind,
|
|
3730
|
+
x: panel.x,
|
|
3731
|
+
header: panel.header ?? '',
|
|
3732
|
+
field: encoding?.field,
|
|
3733
|
+
legendTitle: encoding?.field,
|
|
3734
|
+
spans: featurePanelSpans({
|
|
3735
|
+
panel,
|
|
3736
|
+
width,
|
|
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),
|
|
3745
|
+
labels: label
|
|
3746
|
+
? featureLabelMap(annotations, label)
|
|
3747
|
+
: featureLabels,
|
|
3748
|
+
legend:
|
|
3749
|
+
encoding?.legend ??
|
|
3750
|
+
this.visibleDomainTypes.map(d => ({
|
|
3751
|
+
id: d.accession,
|
|
3752
|
+
label: d.name,
|
|
3753
|
+
color: fillPalette[d.accession]!,
|
|
3754
|
+
})),
|
|
3755
|
+
}
|
|
3756
|
+
}
|
|
3757
|
+
const { colorOf, legend } = resolveScale(
|
|
3758
|
+
panel.scale,
|
|
3759
|
+
rows.map(([, row]) => row?.[panel.field]).filter(notEmpty),
|
|
3760
|
+
)
|
|
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)
|
|
2850
3767
|
}
|
|
2851
|
-
startGlobal = rowStart
|
|
2852
|
-
endGlobal = rowEnd
|
|
2853
3768
|
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
:
|
|
3769
|
+
return {
|
|
3770
|
+
...base,
|
|
3771
|
+
kind: panel.kind,
|
|
3772
|
+
field: panel.field,
|
|
3773
|
+
header: panel.header ?? panel.field,
|
|
3774
|
+
legendTitle: panel.legend ?? panel.field,
|
|
3775
|
+
colors,
|
|
3776
|
+
legend,
|
|
3777
|
+
}
|
|
2860
3778
|
})
|
|
2861
3779
|
},
|
|
2862
3780
|
|
|
2863
3781
|
/**
|
|
2864
3782
|
* #getter
|
|
2865
|
-
*
|
|
2866
|
-
*
|
|
2867
|
-
*
|
|
3783
|
+
* the shift an `align` transform gives each row, keyed by the feature
|
|
3784
|
+
* name it aligns on: the offset putting the first feature of that name
|
|
3785
|
+
* at zero. A row carrying no such feature is absent, and keeps its own
|
|
3786
|
+
* origin.
|
|
3787
|
+
*/
|
|
3788
|
+
get featureAlignShifts(): Map<string, Map<string, number>> {
|
|
3789
|
+
const names = new Set(
|
|
3790
|
+
self.rowPanels.flatMap(panel =>
|
|
3791
|
+
panel.kind === 'features' && panel.x === 'position'
|
|
3792
|
+
? (panel.transform ?? [])
|
|
3793
|
+
.filter(t => t.type === 'align')
|
|
3794
|
+
.map(t => t.on)
|
|
3795
|
+
: [],
|
|
3796
|
+
),
|
|
3797
|
+
)
|
|
3798
|
+
return new Map(
|
|
3799
|
+
[...names].map(on => [
|
|
3800
|
+
on,
|
|
3801
|
+
new Map(
|
|
3802
|
+
Object.entries(self.annotationsByRow)
|
|
3803
|
+
.map(([name, annotations]) => {
|
|
3804
|
+
const hit = annotations.find(a => featureName(a) === on)
|
|
3805
|
+
return hit ? ([name, 1 - hit.start] as const) : undefined
|
|
3806
|
+
})
|
|
3807
|
+
.filter(notEmpty),
|
|
3808
|
+
),
|
|
3809
|
+
]),
|
|
3810
|
+
)
|
|
3811
|
+
},
|
|
3812
|
+
|
|
3813
|
+
/**
|
|
3814
|
+
* #getter
|
|
3815
|
+
* the color the `tipLabel` channel gives each row, by row name. Undefined
|
|
3816
|
+
* when no encoding names the channel, which leaves the labels the theme's
|
|
3817
|
+
* text color.
|
|
2868
3818
|
*/
|
|
2869
|
-
get
|
|
2870
|
-
const
|
|
2871
|
-
|
|
3819
|
+
get tipLabelColors(): Map<string, string> | undefined {
|
|
3820
|
+
const encoding = this.resolvedEncodings.find(
|
|
3821
|
+
e => e.channel === 'tipLabel',
|
|
3822
|
+
)
|
|
3823
|
+
if (!encoding) {
|
|
2872
3824
|
return undefined
|
|
2873
3825
|
}
|
|
2874
|
-
const
|
|
2875
|
-
|
|
2876
|
-
|
|
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
|
+
}
|
|
2877
3834
|
}
|
|
2878
|
-
|
|
2879
|
-
|
|
3835
|
+
return colors
|
|
3836
|
+
},
|
|
3837
|
+
|
|
3838
|
+
/**
|
|
3839
|
+
* #getter
|
|
3840
|
+
* the wash the `rowTint` channel draws over each row, indexed by row, or
|
|
3841
|
+
* undefined when no encoding names the channel. The overlay draws these,
|
|
3842
|
+
* so a tint stays out of the raster tile cache and its keys.
|
|
3843
|
+
*/
|
|
3844
|
+
get rowTints(): (string | undefined)[] | undefined {
|
|
3845
|
+
const encoding = this.resolvedEncodings.find(
|
|
3846
|
+
e => e.channel === 'rowTint',
|
|
3847
|
+
)
|
|
3848
|
+
if (!encoding) {
|
|
2880
3849
|
return undefined
|
|
2881
3850
|
}
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
gapFraction: gaps / total,
|
|
2892
|
-
conservation: self.conservation[mouseCol] ?? 0,
|
|
2893
|
-
propertyConservation: self.propertyConservation[mouseCol],
|
|
2894
|
-
consensusLetter: consensus?.[0] ?? '',
|
|
2895
|
-
consensusCount: consensus?.[1] ?? 0,
|
|
2896
|
-
consensusFraction: consensus ? consensus[1] / total : 0,
|
|
2897
|
-
distribution,
|
|
2898
|
-
}
|
|
3851
|
+
return self.rowNames.map(name => {
|
|
3852
|
+
const value = self.rowDataOf(name)?.[encoding.field]
|
|
3853
|
+
const color =
|
|
3854
|
+
value === undefined ? undefined : encoding.colorOf(value)
|
|
3855
|
+
if (!color) {
|
|
3856
|
+
return undefined
|
|
3857
|
+
}
|
|
3858
|
+
return withAlpha(color, rowTintAlpha)
|
|
3859
|
+
})
|
|
2899
3860
|
},
|
|
2900
3861
|
|
|
2901
3862
|
/**
|
|
2902
|
-
* #
|
|
3863
|
+
* #getter
|
|
3864
|
+
* the color the `branch` channel gives each tree edge, by the node id at
|
|
3865
|
+
* the edge's far end, or undefined when no encoding names the channel. A
|
|
3866
|
+
* node takes the field value its tips agree on, so a clade of one value
|
|
3867
|
+
* colors down from where it splits off, and a node whose tips disagree or
|
|
3868
|
+
* whose value has no color is absent and draws in the default color.
|
|
3869
|
+
*
|
|
3870
|
+
* The pass runs over the whole tree, never `root`, so a collapsed or
|
|
3871
|
+
* focused clade keeps the color the full tree gives it.
|
|
2903
3872
|
*/
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
3873
|
+
get branchColors(): Map<string, string> | undefined {
|
|
3874
|
+
const encoding = this.resolvedEncodings.find(
|
|
3875
|
+
e => e.channel === 'branch',
|
|
3876
|
+
)
|
|
3877
|
+
if (!encoding) {
|
|
3878
|
+
return undefined
|
|
3879
|
+
}
|
|
3880
|
+
const order = preorder(self.tree)
|
|
3881
|
+
const values = new Map<NodeWithIds, string | undefined>()
|
|
3882
|
+
const colors = new Map<string, string>()
|
|
3883
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
3884
|
+
const node = order[i]!
|
|
3885
|
+
const value =
|
|
3886
|
+
node.children.length > 0
|
|
3887
|
+
? sharedValue(node.children.map(child => values.get(child)))
|
|
3888
|
+
: self.rowDataOf(node.name)?.[encoding.field]
|
|
3889
|
+
values.set(node, value)
|
|
3890
|
+
const color =
|
|
3891
|
+
value === undefined ? undefined : encoding.colorOf(value)
|
|
3892
|
+
if (color) {
|
|
3893
|
+
colors.set(node.id, color)
|
|
3894
|
+
}
|
|
2908
3895
|
}
|
|
3896
|
+
return colors
|
|
2909
3897
|
},
|
|
2910
3898
|
}))
|
|
2911
3899
|
.actions(self => ({
|
|
@@ -2918,33 +3906,34 @@ function stateModelFactory() {
|
|
|
2918
3906
|
/**
|
|
2919
3907
|
* #action
|
|
2920
3908
|
*/
|
|
2921
|
-
|
|
2922
|
-
self.
|
|
3909
|
+
setHideHeader(arg: boolean) {
|
|
3910
|
+
self.hideHeader = arg
|
|
2923
3911
|
},
|
|
2924
3912
|
/**
|
|
2925
3913
|
* #action
|
|
3914
|
+
* focus the subtree a click `y` pixels down the tree overview lands on.
|
|
3915
|
+
* A click inside the box already drawn there clears the focus, the way
|
|
3916
|
+
* clicking the focused branch again does.
|
|
2926
3917
|
*/
|
|
2927
|
-
|
|
2928
|
-
|
|
3918
|
+
treeOverviewClick(y: number) {
|
|
3919
|
+
const hit = self.treeOverviewHit(y)
|
|
3920
|
+
const focus = self.treeOverviewFocusRows
|
|
3921
|
+
const inside =
|
|
3922
|
+
!!focus && !!hit && hit.rows[0] >= focus[0] && hit.rows[1] <= focus[1]
|
|
3923
|
+
self.setShowOnly(inside || !hit ? undefined : hit.id)
|
|
2929
3924
|
},
|
|
2930
3925
|
/**
|
|
2931
3926
|
* #action
|
|
3927
|
+
* resize every track sharing a `heightKey`; see `trackHeights`
|
|
2932
3928
|
*/
|
|
2933
|
-
|
|
2934
|
-
self.
|
|
3929
|
+
setTrackHeight(heightKey: string, height: number) {
|
|
3930
|
+
self.trackHeights.set(heightKey, height)
|
|
2935
3931
|
},
|
|
2936
3932
|
/**
|
|
2937
3933
|
* #action
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
},
|
|
2942
|
-
/**
|
|
2943
|
-
* #action
|
|
2944
|
-
* Return to the import form: every property off `preservedOnReset`
|
|
2945
|
-
* (data, filehandles, collapsed/showOnly, zoom, scroll, ...) goes back
|
|
2946
|
-
* to its default, then the file-derived volatiles applySnapshot cannot
|
|
2947
|
-
* reach are cleared by hand.
|
|
3934
|
+
* Return to the import form: reset every property not in
|
|
3935
|
+
* `preservedOnReset` to its default, then clear the file-derived
|
|
3936
|
+
* volatiles applySnapshot does not touch.
|
|
2948
3937
|
*/
|
|
2949
3938
|
reset() {
|
|
2950
3939
|
self.resetCount++
|
|
@@ -2977,8 +3966,8 @@ function stateModelFactory() {
|
|
|
2977
3966
|
},
|
|
2978
3967
|
/**
|
|
2979
3968
|
* #action
|
|
2980
|
-
*
|
|
2981
|
-
*
|
|
3969
|
+
* show or hide an annotation type. Only hidden types are recorded; see
|
|
3970
|
+
* `turnedOffFeatures`
|
|
2982
3971
|
*/
|
|
2983
3972
|
setFilter(accession: string, shown: boolean) {
|
|
2984
3973
|
if (shown) {
|
|
@@ -2992,12 +3981,8 @@ function stateModelFactory() {
|
|
|
2992
3981
|
* #action
|
|
2993
3982
|
*/
|
|
2994
3983
|
fit() {
|
|
2995
|
-
//
|
|
2996
|
-
//
|
|
2997
|
-
// minimap is taking, and fitting the columns while the rows still
|
|
2998
|
-
// overflow measures against a width the vertical scrollbar is taking.
|
|
2999
|
-
// A second pass measures against the geometry the first pass produced,
|
|
3000
|
-
// which is the one the reader ends up looking at.
|
|
3984
|
+
// two passes: each direction's viewport depends on whether the other
|
|
3985
|
+
// overflows (the minimap takes height, the vertical scrollbar width)
|
|
3001
3986
|
transaction(() => {
|
|
3002
3987
|
for (let pass = 0; pass < 2; pass++) {
|
|
3003
3988
|
this.fitHorizontally()
|
|
@@ -3018,14 +4003,36 @@ function stateModelFactory() {
|
|
|
3018
4003
|
}
|
|
3019
4004
|
self.scrollY = 0
|
|
3020
4005
|
},
|
|
4006
|
+
/**
|
|
4007
|
+
* #action
|
|
4008
|
+
* zoom and scroll so a span fills the alignment's width, in highlight
|
|
4009
|
+
* coordinates (see visibleSpan). Does nothing before the viewer knows
|
|
4010
|
+
* its width, or for a span that resolves to no visible column.
|
|
4011
|
+
*/
|
|
4012
|
+
zoomToRegion(region: Region) {
|
|
4013
|
+
const span = self.visibleSpan(region)
|
|
4014
|
+
if (!span || !self.viewInitialized) {
|
|
4015
|
+
return
|
|
4016
|
+
}
|
|
4017
|
+
transaction(() => {
|
|
4018
|
+
self.colWidth = clamp(
|
|
4019
|
+
self.msaCanvasWidth / (span.endCol - span.startCol + 1),
|
|
4020
|
+
minColWidth,
|
|
4021
|
+
maxCellSize,
|
|
4022
|
+
)
|
|
4023
|
+
self.scrollX = clamp(
|
|
4024
|
+
-span.startCol * self.colWidth,
|
|
4025
|
+
self.maxScrollX,
|
|
4026
|
+
0,
|
|
4027
|
+
)
|
|
4028
|
+
})
|
|
4029
|
+
},
|
|
3021
4030
|
/**
|
|
3022
4031
|
* #action
|
|
3023
4032
|
*/
|
|
3024
4033
|
fitHorizontally() {
|
|
3025
4034
|
if (self.numColumns > 0) {
|
|
3026
|
-
//
|
|
3027
|
-
// the right edge -- and short of the width that shows a minimap, so
|
|
3028
|
-
// nothing on screen said they were there
|
|
4035
|
+
// msaCanvasWidth excludes the vertical scrollbar's 20px
|
|
3029
4036
|
self.colWidth = clamp(
|
|
3030
4037
|
self.msaCanvasWidth / self.numColumns,
|
|
3031
4038
|
minColWidth,
|
|
@@ -3036,17 +4043,46 @@ function stateModelFactory() {
|
|
|
3036
4043
|
},
|
|
3037
4044
|
|
|
3038
4045
|
afterCreate() {
|
|
3039
|
-
// seed the highlighted-columns overlay from the declarative property so
|
|
3040
|
-
// a shared snapshot/URL opens with those columns highlighted (the
|
|
3041
|
-
// volatile highlightedColumns can later be driven by genome-hover sync)
|
|
3042
4046
|
if (self.highlightColumns?.length) {
|
|
3043
4047
|
self.setHighlightedColumns(self.highlightColumns)
|
|
3044
4048
|
}
|
|
3045
4049
|
|
|
3046
|
-
//
|
|
3047
|
-
//
|
|
3048
|
-
//
|
|
3049
|
-
//
|
|
4050
|
+
// The `collapse` and `focus` clade marks seed the collapsed list and
|
|
4051
|
+
// the subtree in focus, which the tree, `hideGapsEffective` and the
|
|
4052
|
+
// alignment all read. The tree arrives with the model for inline data
|
|
4053
|
+
// and later for a filehandle, so the seeding waits for it and then runs
|
|
4054
|
+
// once: expanding a seeded clade sticks, and the record collapses it
|
|
4055
|
+
// again only on reload. `dataInitialized` is true once the MSA alone
|
|
4056
|
+
// has loaded, when the tree is still the flat stub, so a tree
|
|
4057
|
+
// filehandle holds the seeding until its text lands.
|
|
4058
|
+
let cladesSeeded = false
|
|
4059
|
+
addDisposer(
|
|
4060
|
+
self,
|
|
4061
|
+
autorun(() => {
|
|
4062
|
+
if (
|
|
4063
|
+
cladesSeeded ||
|
|
4064
|
+
!self.dataInitialized ||
|
|
4065
|
+
(self.treeFilehandle && !self.data.tree) ||
|
|
4066
|
+
self.clades.length === 0
|
|
4067
|
+
) {
|
|
4068
|
+
return
|
|
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
|
+
}
|
|
4080
|
+
}
|
|
4081
|
+
}),
|
|
4082
|
+
)
|
|
4083
|
+
|
|
4084
|
+
// the matchMedia query is pinned to the current device pixel ratio, so
|
|
4085
|
+
// each change re-registers against the new one
|
|
3050
4086
|
if (
|
|
3051
4087
|
typeof window !== 'undefined' &&
|
|
3052
4088
|
typeof window.matchMedia === 'function'
|
|
@@ -3070,25 +4106,17 @@ function stateModelFactory() {
|
|
|
3070
4106
|
* Fetch a filehandle whenever it changes, and hand the text to
|
|
3071
4107
|
* `onLoad`.
|
|
3072
4108
|
*
|
|
3073
|
-
*
|
|
3074
|
-
*
|
|
3075
|
-
*
|
|
3076
|
-
* loading flag belonging to the newer one. A superseded or cleared
|
|
3077
|
-
* request is also aborted, rather than left downloading a file nothing
|
|
3078
|
-
* is waiting for, and it gives back the status line it was writing --
|
|
3079
|
-
* a reset() mid-download used to leave "Downloading file" and a Cancel
|
|
3080
|
-
* button behind on the import form.
|
|
4109
|
+
* A generation guard keeps a slower superseded request from overwriting
|
|
4110
|
+
* the newer one's data, status or loading flag. A superseded or cleared
|
|
4111
|
+
* request is aborted and clears its status line.
|
|
3081
4112
|
*
|
|
3082
|
-
* `clearFilehandle`
|
|
3083
|
-
*
|
|
3084
|
-
*
|
|
3085
|
-
* it too, returning the view to the import form rather than leaving a
|
|
3086
|
-
* stuck spinner.
|
|
4113
|
+
* `clearFilehandle` runs after a local file loads, since a blob has no
|
|
4114
|
+
* URL to refetch, and when the user cancels, which returns the view to
|
|
4115
|
+
* the import form.
|
|
3087
4116
|
*
|
|
3088
|
-
* `what` names the layer in a failure message. An optional layer
|
|
3089
|
-
* (annotations, row metadata) that fails
|
|
3090
|
-
*
|
|
3091
|
-
* error screen over a decorative file.
|
|
4117
|
+
* `what` names the layer in a failure message. An `optional` layer
|
|
4118
|
+
* (annotations, row metadata) that fails adds a warning instead of an
|
|
4119
|
+
* error.
|
|
3092
4120
|
*/
|
|
3093
4121
|
const loadOnFilehandleChange = ({
|
|
3094
4122
|
what,
|
|
@@ -3186,8 +4214,7 @@ function stateModelFactory() {
|
|
|
3186
4214
|
},
|
|
3187
4215
|
})
|
|
3188
4216
|
|
|
3189
|
-
// treeMetadata
|
|
3190
|
-
// it keeps no loading flag and nothing to return to on cancel
|
|
4217
|
+
// treeMetadata has no import-form step, so no loading flag or cancel
|
|
3191
4218
|
loadOnFilehandleChange({
|
|
3192
4219
|
what: 'row metadata',
|
|
3193
4220
|
optional: true,
|
|
@@ -3197,11 +4224,8 @@ function stateModelFactory() {
|
|
|
3197
4224
|
},
|
|
3198
4225
|
})
|
|
3199
4226
|
|
|
3200
|
-
//
|
|
3201
|
-
//
|
|
3202
|
-
// gff used to leave the previous file's annotations drawn over it --
|
|
3203
|
-
// while annotations a host set directly are left alone, which is why
|
|
3204
|
-
// this tracks what it applied instead of reading the current list
|
|
4227
|
+
// parses data.gff into annotations. Clearing the text clears only the
|
|
4228
|
+
// annotations this autorun applied, not ones a host set directly
|
|
3205
4229
|
let appliedGFF = false
|
|
3206
4230
|
addDisposer(
|
|
3207
4231
|
self,
|
|
@@ -3212,8 +4236,6 @@ function stateModelFactory() {
|
|
|
3212
4236
|
self.applyGFFText(gffText)
|
|
3213
4237
|
appliedGFF = true
|
|
3214
4238
|
} catch (e) {
|
|
3215
|
-
// a malformed overlay is not worth replacing the alignment
|
|
3216
|
-
// with an error screen
|
|
3217
4239
|
console.error(e)
|
|
3218
4240
|
self.addWarning(`The annotations did not parse: ${e}`)
|
|
3219
4241
|
}
|
|
@@ -3224,9 +4246,25 @@ function stateModelFactory() {
|
|
|
3224
4246
|
}),
|
|
3225
4247
|
)
|
|
3226
4248
|
|
|
3227
|
-
//
|
|
3228
|
-
//
|
|
3229
|
-
|
|
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
|
+
// gffFilehandle loads into data.gff, which the autorun above parses
|
|
3230
4268
|
loadOnFilehandleChange({
|
|
3231
4269
|
what: 'annotations',
|
|
3232
4270
|
optional: true,
|
|
@@ -3253,19 +4291,12 @@ function stateModelFactory() {
|
|
|
3253
4291
|
},
|
|
3254
4292
|
})
|
|
3255
4293
|
|
|
3256
|
-
// Keep
|
|
3257
|
-
// self.
|
|
3258
|
-
//
|
|
3259
|
-
//
|
|
4294
|
+
// Keep computeds alive that are read outside reactions, so they are not
|
|
4295
|
+
// recomputed on every access: self.columns (and through it the
|
|
4296
|
+
// parseMSA result), and the column statistics that dynamic color
|
|
4297
|
+
// schemes and the mousemove-driven hover tooltip read.
|
|
3260
4298
|
// xref solution https://github.com/mobxjs/mobx/issues/266#issuecomment-222007278
|
|
3261
4299
|
// xref problem https://github.com/GMOD/react-msaview/issues/75
|
|
3262
|
-
//
|
|
3263
|
-
// The column statistics are held for the same reason whenever something
|
|
3264
|
-
// reads them off the reactive path: dynamic color schemes, and the hover
|
|
3265
|
-
// tooltip, which is read from a mousemove handler. Without this the
|
|
3266
|
-
// tooltip's cost depends on whether some visible track happens to be
|
|
3267
|
-
// observing them, so closing the conservation track would silently turn
|
|
3268
|
-
// every mouse move into a full-alignment recount.
|
|
3269
4300
|
addDisposer(
|
|
3270
4301
|
self,
|
|
3271
4302
|
autorun(() => {
|
|
@@ -3287,10 +4318,9 @@ function stateModelFactory() {
|
|
|
3287
4318
|
}),
|
|
3288
4319
|
)
|
|
3289
4320
|
|
|
3290
|
-
//
|
|
3291
|
-
//
|
|
3292
|
-
//
|
|
3293
|
-
// below (treeAreaWidth here depends only on labelsWidth, not treeWidth).
|
|
4321
|
+
// with autoTreeAreaWidth and no tree drawn, size the tree area to the
|
|
4322
|
+
// labels. Gated on noTree/!drawTree so it does not loop with the
|
|
4323
|
+
// treeWidth autorun below
|
|
3294
4324
|
addDisposer(
|
|
3295
4325
|
self,
|
|
3296
4326
|
autorun(() => {
|
|
@@ -3299,19 +4329,17 @@ function stateModelFactory() {
|
|
|
3299
4329
|
(self.noTree || !self.drawTree) &&
|
|
3300
4330
|
self.labelsWidth
|
|
3301
4331
|
) {
|
|
3302
|
-
self.setTreeAreaWidth(
|
|
4332
|
+
self.setTreeAreaWidth(
|
|
4333
|
+
self.labelsWidth + self.marginLeft + 12 + self.cladeGutterWidth,
|
|
4334
|
+
)
|
|
3303
4335
|
}
|
|
3304
4336
|
}),
|
|
3305
4337
|
)
|
|
3306
4338
|
|
|
3307
|
-
// treeWidth
|
|
3308
|
-
//
|
|
3309
|
-
//
|
|
3310
|
-
//
|
|
3311
|
-
// in a 200px area -- had its value overwritten on the first frame, and
|
|
3312
|
-
// a restored session came back re-derived rather than as it was left.
|
|
3313
|
-
// A getter cannot do this: labelsWidth is measured off the leaves,
|
|
3314
|
-
// which are laid out against treeWidth.
|
|
4339
|
+
// treeWidth follows the tree area less the labels. A non-default
|
|
4340
|
+
// snapshot treeWidth (jbrowse-plugin-msaview opens at 100 in a 200px
|
|
4341
|
+
// area) is kept until the tree area changes. Not a getter, because
|
|
4342
|
+
// labelsWidth is measured off leaves laid out against treeWidth.
|
|
3315
4343
|
let pinnedAreaWidth =
|
|
3316
4344
|
self.treeWidth === defaultTreeWidth ? undefined : self.treeAreaWidth
|
|
3317
4345
|
addDisposer(
|
|
@@ -3324,16 +4352,22 @@ function stateModelFactory() {
|
|
|
3324
4352
|
}
|
|
3325
4353
|
pinnedAreaWidth = undefined
|
|
3326
4354
|
self.setTreeWidth(
|
|
3327
|
-
Math.max(
|
|
4355
|
+
Math.max(
|
|
4356
|
+
50,
|
|
4357
|
+
areaWidth -
|
|
4358
|
+
labelsWidth -
|
|
4359
|
+
10 -
|
|
4360
|
+
self.marginLeft -
|
|
4361
|
+
self.cladeGutterWidth,
|
|
4362
|
+
),
|
|
3328
4363
|
)
|
|
3329
4364
|
}),
|
|
3330
4365
|
)
|
|
3331
4366
|
},
|
|
3332
4367
|
}))
|
|
3333
4368
|
.postProcessSnapshot(({ data, columnTracks, ...rest }) => ({
|
|
3334
|
-
// per-property defaults
|
|
3335
|
-
//
|
|
3336
|
-
// when a sibling filehandle can refetch them, keeping sessions/URLs small
|
|
4369
|
+
// stripDefault handles per-property defaults; this drops inline documents
|
|
4370
|
+
// whose filehandle can refetch them
|
|
3337
4371
|
...rest,
|
|
3338
4372
|
...smallColumnTracks(columnTracks),
|
|
3339
4373
|
data: {
|