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/dist/model.js
CHANGED
|
@@ -2,20 +2,25 @@ import { clamp, groupBy, notEmpty, sum } from '@jbrowse/core/util';
|
|
|
2
2
|
import { openLocation } from '@jbrowse/core/util/io';
|
|
3
3
|
import { ElementId, FileLocation } from '@jbrowse/core/util/types/mst';
|
|
4
4
|
import { addDisposer, applySnapshot, cast, getSnapshot, isAlive, types, } from '@jbrowse/mobx-state-tree';
|
|
5
|
-
import {
|
|
6
|
-
import { autorun, transaction } from 'mobx';
|
|
5
|
+
import { autorun, computed, transaction } from 'mobx';
|
|
7
6
|
import { generateNodeIds, gffToAnnotations, interProScanToAnnotations, parseEmfTree, parseGFF, parseMSA, parseNewick, } from 'msa-parsers';
|
|
8
7
|
import { calculateBlocks } from './calculateBlocks.js';
|
|
9
8
|
import { clustalXColumnColors } from './clustalX.js';
|
|
10
9
|
import colorSchemes from './colorSchemes.js';
|
|
11
10
|
import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.js';
|
|
11
|
+
import { columnStats } from './columnStats.js';
|
|
12
|
+
import { packDomainLanes } from './components/msa/packDomainLanes.js';
|
|
13
|
+
import { visibleColRange } from './components/msa/visibleColRange.js';
|
|
12
14
|
import TrackBlocks from './components/tracks/TrackBlocks.js';
|
|
13
|
-
import {
|
|
15
|
+
import { cladeGutterWidth } from './components/tree/cladeBrackets.js';
|
|
16
|
+
import { scaleBarLength } from './components/tree/scaleBar.js';
|
|
17
|
+
import { cladeHighlightAlpha, cladeHighlightColor, defaultAllowedGappyness, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawMsaLetters, defaultFeaturePanelWidth, defaultHeight, defaultHideGaps, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultScrollZoomAxis, defaultShowDomainLegend, defaultShowDomains, defaultSubFeatureRows, defaultTreeWidth, labelReferenceFontSize, maxCellSize, maxInlineSnapshotBytes, maxNeighborJoiningRows, minColWidth, minLetterColWidth, minLetterRowHeight, minRowHeight, rowPanelHeaderHeight, rowTintAlpha, scrollZoomAxes, segmentFeatureTypes, segmentShades, treeScaleBarHeight, } from './constants.js';
|
|
14
18
|
import { createPaletteMap } from './createPaletteMap.js';
|
|
15
19
|
import { exportFileName } from './exportFileName.js';
|
|
20
|
+
import { featureField, featureName } from './featureFields.js';
|
|
16
21
|
import { fetchTextWithProgress, isAbortError } from './fetchUtils.js';
|
|
17
22
|
import { flatToTree } from './flatToTree.js';
|
|
18
|
-
import { calcDepthToLeaf, clusterLayout, collapse, collapsedSubtreeMaxLength, find, forEachDescendant, hierarchy, leaves, maxLength, setBrLength, sort, sum as hierarchySum, } from './hierarchy.js';
|
|
23
|
+
import { calcDepthToLeaf, clusterLayout, collapse, collapsedSubtreeMaxLength, find, forEachDescendant, hierarchy, leafIndex, leaves, maxLength, mrca, nodeCoveringRows, setBrLength, sort, sum as hierarchySum, } from './hierarchy.js';
|
|
19
24
|
import { measureTextCanvas } from './measureTextCanvas.js';
|
|
20
25
|
import { DataModelF } from './model/DataModel.js';
|
|
21
26
|
import { DialogQueueSessionMixin } from './model/DialogQueue.js';
|
|
@@ -25,10 +30,11 @@ import { calculateNeighborJoiningTree } from './neighborJoining.js';
|
|
|
25
30
|
import { parseAsn1 } from './parseAsn1.js';
|
|
26
31
|
import { calculatePropertyConservation } from './propertyConservation.js';
|
|
27
32
|
import { globalColToVisibleCol, seqPosOfGlobalCol, visibleColToGlobalCol, visibleColsBefore, } from './rowCoordinateCalculations.js';
|
|
33
|
+
import { resolveScale } from './scales.js';
|
|
28
34
|
import { buildSeqPosIndex } from './seqPosToGlobalCol.js';
|
|
29
35
|
import { maxBitsFor } from './sequenceLogo.js';
|
|
30
36
|
import { stripDefault } from './stripDefault.js';
|
|
31
|
-
import { computeRowInsertions, dropBlanks, len, skipBlanks,
|
|
37
|
+
import { computeRowInsertions, dropBlanks, len, outlineColor, skipBlanks, withAlpha, } from './util.js';
|
|
32
38
|
import { saveAs } from './vendor/fileSaver.js';
|
|
33
39
|
import { parseWuss } from './wuss.js';
|
|
34
40
|
function parseTreeText(text) {
|
|
@@ -37,14 +43,172 @@ function parseTreeText(text) {
|
|
|
37
43
|
}
|
|
38
44
|
return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text);
|
|
39
45
|
}
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
46
|
+
// The height each kind draws at before the user drags a divider. A text track
|
|
47
|
+
// is absent: it is one alignment row tall and follows rowHeight, so the zoom
|
|
48
|
+
// controls already size it.
|
|
49
|
+
export const defaultTrackHeights = {
|
|
50
|
+
bar: 40,
|
|
51
|
+
// taller than a bar track: in a 40px stack of four residues each glyph is
|
|
52
|
+
// too short to identify. Twice the height of the tracks above it, so it
|
|
53
|
+
// starts hidden
|
|
54
|
+
logo: 80,
|
|
55
|
+
arc: 50,
|
|
56
|
+
ruler: 20,
|
|
57
|
+
};
|
|
58
|
+
// the kinds a divider resizes. The ruler is a fixed scale, and a text track
|
|
59
|
+
// follows the vertical zoom.
|
|
60
|
+
const resizableKinds = new Set(['bar', 'logo', 'arc']);
|
|
43
61
|
const defaultOffTracks = new Set(['sequence-logo', 'position-ruler']);
|
|
44
|
-
//
|
|
45
|
-
//
|
|
62
|
+
// a data track carries its own height rather than its kind's, so its divider
|
|
63
|
+
// resizes it alone
|
|
64
|
+
const ownHeightKey = (id) => `own:${id}`;
|
|
65
|
+
// base-pair arcs: one color for nested helices, one for pseudoknots, which cross
|
|
66
|
+
// them
|
|
46
67
|
const HELIX_ARC = '#4e79a7';
|
|
47
68
|
const PSEUDOKNOT_ARC = '#e15759';
|
|
69
|
+
// the fill of a span under a scale that gives its value no color, such as a
|
|
70
|
+
// gene a `map` over gene names leaves out
|
|
71
|
+
const UNSCALED_FEATURE = '#d9d9d9';
|
|
72
|
+
// the fraction of a span a following one may cover before it takes a lane of
|
|
73
|
+
// its own in a `features` panel
|
|
74
|
+
const laneOverlap = 0.1;
|
|
75
|
+
// a row panel's own width, or a default: the row height, which makes a strip
|
|
76
|
+
// cell square, and a gene neighborhood's width for a features panel
|
|
77
|
+
function rowPanelWidth(panel, rowHeight) {
|
|
78
|
+
return (panel.width ??
|
|
79
|
+
(panel.kind === 'features' ? defaultFeaturePanelWidth : rowHeight));
|
|
80
|
+
}
|
|
81
|
+
// the fill and outline of every feature in a list: its own GFF `color=` first,
|
|
82
|
+
// then the scale an encoding resolves over one of its fields, then the
|
|
83
|
+
// accession palette
|
|
84
|
+
function featureColorMap(annotations, encoding, fillPalette) {
|
|
85
|
+
const strokes = new Map();
|
|
86
|
+
const strokeOf = (fill) => {
|
|
87
|
+
const hit = strokes.get(fill);
|
|
88
|
+
if (hit !== undefined) {
|
|
89
|
+
return hit;
|
|
90
|
+
}
|
|
91
|
+
const stroke = outlineColor(fill);
|
|
92
|
+
strokes.set(fill, stroke);
|
|
93
|
+
return stroke;
|
|
94
|
+
};
|
|
95
|
+
return new Map(annotations.map(annotation => {
|
|
96
|
+
const value = encoding
|
|
97
|
+
? featureField(annotation, encoding.field)
|
|
98
|
+
: undefined;
|
|
99
|
+
const fill = annotation.color ??
|
|
100
|
+
(value === undefined ? undefined : encoding.colorOf(value)) ??
|
|
101
|
+
(encoding ? UNSCALED_FEATURE : fillPalette[annotation.accession]);
|
|
102
|
+
return [annotation, { fill, stroke: strokeOf(fill) }];
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
// the text a label channel draws inside each span, for the features carrying
|
|
106
|
+
// the field it names
|
|
107
|
+
function featureLabelMap(annotations, field) {
|
|
108
|
+
const labels = new Map();
|
|
109
|
+
for (const annotation of annotations) {
|
|
110
|
+
const value = featureField(annotation, field);
|
|
111
|
+
if (value !== undefined) {
|
|
112
|
+
labels.set(annotation, value);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return labels;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Lanes for `position: "strandpile"`, gggenomes' `position_strandpile`: the
|
|
119
|
+
* forward features stack above the line and the reverse below it, each strand
|
|
120
|
+
* packing on its own. The deepest row on each side sets the grid every row lays
|
|
121
|
+
* out on, so the line between the strands sits at one height down the panel and
|
|
122
|
+
* a reader can scan it. A feature with no strand piles with the forward ones.
|
|
123
|
+
*/
|
|
124
|
+
function strandpileLanes(rows) {
|
|
125
|
+
const piled = rows.map(([name, bands]) => {
|
|
126
|
+
const reverse = packDomainLanes(bands.filter(b => b.annotation.strand === -1));
|
|
127
|
+
const forward = packDomainLanes(bands.filter(b => b.annotation.strand !== -1));
|
|
128
|
+
return { name, forward, reverse };
|
|
129
|
+
});
|
|
130
|
+
const up = Math.max(0, ...piled.map(p => p.forward[0]?.laneCount ?? 0));
|
|
131
|
+
const down = Math.max(0, ...piled.map(p => p.reverse[0]?.laneCount ?? 0));
|
|
132
|
+
const laneCount = up + down;
|
|
133
|
+
return new Map(piled.map(({ name, forward, reverse }) => [
|
|
134
|
+
name,
|
|
135
|
+
[
|
|
136
|
+
// forward level 0 is the lane just above the line, stacking upward
|
|
137
|
+
...forward.map(b => ({ ...b, lane: up - 1 - b.lane, laneCount })),
|
|
138
|
+
...reverse.map(b => ({ ...b, lane: up + b.lane, laneCount })),
|
|
139
|
+
],
|
|
140
|
+
]));
|
|
141
|
+
}
|
|
142
|
+
/** one row's spans laid out by the panel's `position` */
|
|
143
|
+
function panelLanes(panel, rows) {
|
|
144
|
+
return panel.position === 'strandpile'
|
|
145
|
+
? strandpileLanes(rows)
|
|
146
|
+
: new Map(rows.map(([name, bands]) => [name, packDomainLanes(bands)]));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The spans a `features` panel draws, keyed by row name and measured in the
|
|
150
|
+
* panel's own pixels. `column` takes the bands the overlay draws, at the
|
|
151
|
+
* alignment's column width. `position` packs each row's features in its own
|
|
152
|
+
* residue positions, shifted by an `align` transform, and maps the extent they
|
|
153
|
+
* cover across every row onto the panel width, so a row with no alignment
|
|
154
|
+
* still has an x.
|
|
155
|
+
*/
|
|
156
|
+
function featurePanelSpans({ panel, width, colWidth, domainBands, annotationsByRow, shifts, }) {
|
|
157
|
+
if (panel.x === 'column') {
|
|
158
|
+
return panelLanes(panel, [...domainBands].map(([name, bands]) => [
|
|
159
|
+
name,
|
|
160
|
+
bands.map(band => ({
|
|
161
|
+
...band,
|
|
162
|
+
xStart: band.startCol * colWidth,
|
|
163
|
+
xEnd: band.endCol * colWidth,
|
|
164
|
+
})),
|
|
165
|
+
]));
|
|
166
|
+
}
|
|
167
|
+
const shifted = Object.entries(annotationsByRow).map(([name, annotations]) => {
|
|
168
|
+
const shift = shifts?.get(name) ?? 0;
|
|
169
|
+
return [
|
|
170
|
+
name,
|
|
171
|
+
annotations.map(annotation => ({
|
|
172
|
+
annotation,
|
|
173
|
+
start: annotation.start - 1 + shift,
|
|
174
|
+
end: annotation.end + shift,
|
|
175
|
+
})),
|
|
176
|
+
];
|
|
177
|
+
});
|
|
178
|
+
let min = Infinity;
|
|
179
|
+
let max = -Infinity;
|
|
180
|
+
for (const [, features] of shifted) {
|
|
181
|
+
for (const { start, end } of features) {
|
|
182
|
+
min = Math.min(min, start);
|
|
183
|
+
max = Math.max(max, end);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// a gene arrow ends where its feature does, so the extent maps onto the whole
|
|
187
|
+
// panel less the pixel the rightmost stroke needs
|
|
188
|
+
const drawable = Math.max(1, width - 1);
|
|
189
|
+
const scale = max > min ? drawable / (max - min) : 0;
|
|
190
|
+
// a lane opens where a span covers more than a tenth of the one before it,
|
|
191
|
+
// which keeps the genes of an operon on one lane: adjacent genes commonly
|
|
192
|
+
// share a few bases, and a stop codon overlapping the next start reads as a
|
|
193
|
+
// second lane over the whole row
|
|
194
|
+
return panelLanes(panel, shifted.map(([name, features]) => [
|
|
195
|
+
name,
|
|
196
|
+
features.map(({ annotation, start, end }) => ({
|
|
197
|
+
annotation,
|
|
198
|
+
xStart: (start - min) * scale,
|
|
199
|
+
xEnd: (end - min) * scale,
|
|
200
|
+
startCol: start,
|
|
201
|
+
endCol: end - (end - start) * laneOverlap,
|
|
202
|
+
})),
|
|
203
|
+
]));
|
|
204
|
+
}
|
|
205
|
+
// a scale over the values a field takes across the features drawn
|
|
206
|
+
function resolveFeatureScale(field, scale, annotations) {
|
|
207
|
+
return {
|
|
208
|
+
field,
|
|
209
|
+
...resolveScale(scale, annotations.map(a => featureField(a, field)).filter(notEmpty)),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
48
212
|
// a data track over this size stays in the live model but leaves the snapshot,
|
|
49
213
|
// the same rule DataModel applies to an inline document
|
|
50
214
|
function columnTrackSizes(tracks) {
|
|
@@ -59,17 +223,13 @@ function smallColumnTracks(tracks) {
|
|
|
59
223
|
* The snapshot properties reset() carries across a return to the import form:
|
|
60
224
|
* display preferences and layout, nothing derived from the loaded file.
|
|
61
225
|
*
|
|
62
|
-
* reset() applies a default snapshot filtered to this list, so
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* are path-derived (node-0-0-1), a stale `collapsed` or `showOnly` id matched
|
|
68
|
-
* a real node in the new tree and folded it. Downstream composed properties
|
|
69
|
-
* (e.g. the jbrowse plugin's) are not on the list, so they reset too.
|
|
226
|
+
* reset() applies a default snapshot filtered to this list, so any property
|
|
227
|
+
* left off it resets, including downstream composed ones (e.g. the jbrowse
|
|
228
|
+
* plugin's). Node ids are path-derived (node-0-0-1), so a carried-over
|
|
229
|
+
* `collapsed` or `showOnly` id would match a real node in the next tree and
|
|
230
|
+
* fold it.
|
|
70
231
|
*
|
|
71
|
-
* Exported for modelReset.test.ts
|
|
72
|
-
* matches a freshly created model after reset().
|
|
232
|
+
* Exported for modelReset.test.ts.
|
|
73
233
|
*/
|
|
74
234
|
export const preservedOnReset = new Set([
|
|
75
235
|
'id',
|
|
@@ -77,6 +237,7 @@ export const preservedOnReset = new Set([
|
|
|
77
237
|
'height',
|
|
78
238
|
'drawMsaLetters',
|
|
79
239
|
'scrollZoom',
|
|
240
|
+
'scrollZoomAxis',
|
|
80
241
|
'bgColor',
|
|
81
242
|
'colorSchemeName',
|
|
82
243
|
'showColumnStats',
|
|
@@ -87,50 +248,167 @@ export const preservedOnReset = new Set([
|
|
|
87
248
|
'showBranchLen',
|
|
88
249
|
'drawTree',
|
|
89
250
|
'drawNodeBubbles',
|
|
251
|
+
'drawNodeLabels',
|
|
252
|
+
'showTreeOverview',
|
|
253
|
+
'overviewHeight',
|
|
90
254
|
'autoTreeAreaWidth',
|
|
91
255
|
'turnedOffTracks',
|
|
256
|
+
'trackHeights',
|
|
92
257
|
'hideGaps',
|
|
93
258
|
'allowedGappyness',
|
|
94
259
|
'subFeatureRows',
|
|
95
260
|
'showDomainLegend',
|
|
96
261
|
]);
|
|
97
|
-
// `turnedOffTracks`
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
// A track the file itself supplies says whether it starts hidden, since only
|
|
102
|
-
// the file knows how many of them there are: a Pfam seed carries a couple of
|
|
103
|
-
// #=GR lines, an Rfam family one per row.
|
|
262
|
+
// `turnedOffTracks` holds only the user's explicit choices, so a hidden-by-default
|
|
263
|
+
// track writes nothing into the snapshot. A file-supplied track passes its own
|
|
264
|
+
// `defaultOff`, since the count varies by file: a Pfam seed has a couple of #=GR
|
|
265
|
+
// lines, an Rfam family one per row.
|
|
104
266
|
function trackIsOff(turnedOffTracks, id, defaultOff) {
|
|
105
267
|
return turnedOffTracks.get(id) ?? (defaultOff || defaultOffTracks.has(id));
|
|
106
268
|
}
|
|
107
|
-
//
|
|
108
|
-
// value to every observer of it
|
|
269
|
+
// shared empty results, so observers don't see a fresh [] as a change
|
|
109
270
|
const noDomains = [];
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
271
|
+
const noClades = [];
|
|
272
|
+
/**
|
|
273
|
+
* The rows a clade covers and the node an `mrca` names, or undefined when the
|
|
274
|
+
* clade does not resolve: a tip name the tree does not have or has twice, or a
|
|
275
|
+
* leaf count `tips` disagrees with. `index` and `rowNamesSet` are the memoized
|
|
276
|
+
* passes over the tree.
|
|
277
|
+
*/
|
|
278
|
+
function cladeRows(clade, root, index, rowNamesSet) {
|
|
279
|
+
if (clade.range) {
|
|
280
|
+
const [a, b] = clade.range.map(name => index.get(name) ? rowNamesSet.get(name) : undefined);
|
|
281
|
+
if (a === undefined || b === undefined) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
const rows = a <= b ? [a, b] : [b, a];
|
|
285
|
+
return rows[1] - rows[0] + 1 === clade.tips ? { rows } : undefined;
|
|
286
|
+
}
|
|
287
|
+
const node = clade.mrca ? mrca(root, clade.mrca, index) : undefined;
|
|
288
|
+
if (!node) {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
const tips = leaves(node);
|
|
292
|
+
if (tips.length !== clade.tips) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
let first = Infinity;
|
|
296
|
+
let last = -Infinity;
|
|
297
|
+
for (const tip of tips) {
|
|
298
|
+
const row = rowNamesSet.get(tip.data.name);
|
|
299
|
+
if (row !== undefined) {
|
|
300
|
+
first = Math.min(first, row);
|
|
301
|
+
last = Math.max(last, row);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return first <= last
|
|
305
|
+
? { rows: [first, last], nodeId: node.data.id }
|
|
306
|
+
: undefined;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Every clade that resolves, with its fill color settled, against the leaf
|
|
310
|
+
* order `rowNamesSet` gives. The tree panel resolves against the displayed
|
|
311
|
+
* rows and the overview against its own, so both take the tree they draw. A
|
|
312
|
+
* `range` record names no node, so `collapse` and `focus`, which need one,
|
|
313
|
+
* drop it.
|
|
314
|
+
*/
|
|
315
|
+
function resolveClades(clades, root, rowNamesSet) {
|
|
316
|
+
const index = leafIndex(root);
|
|
317
|
+
return clades.flatMap(clade => {
|
|
318
|
+
const resolved = cladeRows(clade, root, index, rowNamesSet);
|
|
319
|
+
const seeding = clade.mark === 'collapse' || clade.mark === 'focus';
|
|
320
|
+
return resolved && !(seeding && resolved.nodeId === undefined)
|
|
321
|
+
? [
|
|
322
|
+
{
|
|
323
|
+
...resolved,
|
|
324
|
+
mark: clade.mark,
|
|
325
|
+
color: withAlpha(clade.color ?? cladeHighlightColor, cladeHighlightAlpha),
|
|
326
|
+
markColor: clade.color,
|
|
327
|
+
label: clade.label,
|
|
328
|
+
},
|
|
329
|
+
]
|
|
330
|
+
: [];
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* The tree with the display transforms applied: leaf counts summed, children
|
|
335
|
+
* sorted by branch length, `showOnly` taken as the new root, and each
|
|
336
|
+
* `collapsed` clade folded. The overview builds the same tree without the
|
|
337
|
+
* focus, so it shows the focused subtree inside the whole.
|
|
338
|
+
*/
|
|
339
|
+
function buildTreeRoot(tree, collapsed, showOnly) {
|
|
340
|
+
let hier = hierarchy(tree, d => d.children);
|
|
341
|
+
hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1));
|
|
342
|
+
sort(hier, (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1));
|
|
343
|
+
if (showOnly) {
|
|
344
|
+
const res = find(hier, n => n.data.id === showOnly);
|
|
345
|
+
if (res) {
|
|
346
|
+
hier = res;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
for (const collapsedId of collapsed) {
|
|
350
|
+
const node = find(hier, n => n.data.id === collapsedId);
|
|
351
|
+
if (node) {
|
|
352
|
+
if (node.children) {
|
|
353
|
+
collapse(node);
|
|
354
|
+
}
|
|
355
|
+
else if (node.parent?.children) {
|
|
356
|
+
node.parent.children = node.parent.children.filter(c => c.data.id !== collapsedId);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return hier;
|
|
361
|
+
}
|
|
362
|
+
// the inclusive tip indices a node covers, from the row-space extent
|
|
363
|
+
// clusterLayout writes to xMin/xMax as tip centers
|
|
364
|
+
function tipRange(node) {
|
|
365
|
+
return [Math.round(node.xMin - 0.5), Math.round(node.xMax - 0.5)];
|
|
366
|
+
}
|
|
367
|
+
// the channels reading the feature table; every other channel reads rowData
|
|
368
|
+
const featureChannels = new Set([
|
|
369
|
+
'featureFill',
|
|
370
|
+
'featureLabel',
|
|
371
|
+
]);
|
|
372
|
+
// seqPos -> column indexes per row, keyed on the parse so they are garbage
|
|
373
|
+
// collected with it. A computed would rebuild every row's index when read
|
|
374
|
+
// outside a reactive context.
|
|
114
375
|
const seqPosIndexCache = new WeakMap();
|
|
115
|
-
// A segment
|
|
116
|
-
//
|
|
117
|
-
// anyway -- with a residue that is off by however much the sides disagree. Skip
|
|
118
|
-
// it, the same refusal an uncovered position gets.
|
|
376
|
+
// A segment is a 1:1 run. A segment whose sides differ in length is malformed,
|
|
377
|
+
// and the lookups treat it like an uncovered position.
|
|
119
378
|
function sameLength(segment) {
|
|
120
379
|
return (segment.rowEnd - segment.rowStart ===
|
|
121
380
|
segment.structEnd - segment.structStart);
|
|
122
381
|
}
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// a scrollbar for half a pixel of nothing, which then shrinks the viewport and
|
|
127
|
-
// leaves a gap.
|
|
382
|
+
// fit() divides the viewport by the row or column count and multiplies back, so
|
|
383
|
+
// an exact fit can land a fraction of a pixel over. A plain `>` would then show
|
|
384
|
+
// a scrollbar that shrinks the viewport and leaves a gap.
|
|
128
385
|
function overflows(content, viewport) {
|
|
129
386
|
return content - viewport > 0.5;
|
|
130
387
|
}
|
|
131
388
|
function inRanges(ranges, position) {
|
|
132
389
|
return !!ranges?.some(([start, end]) => position >= start && position <= end);
|
|
133
390
|
}
|
|
391
|
+
// the value every one of a node's children reports, or undefined where one of
|
|
392
|
+
// them has no value or they disagree
|
|
393
|
+
function sharedValue(values) {
|
|
394
|
+
const [first] = values;
|
|
395
|
+
return first !== undefined && values.every(v => v === first)
|
|
396
|
+
? first
|
|
397
|
+
: undefined;
|
|
398
|
+
}
|
|
399
|
+
// preorder, so reversing the list puts every node after its descendants
|
|
400
|
+
function preorder(tree) {
|
|
401
|
+
const order = [];
|
|
402
|
+
const stack = [tree];
|
|
403
|
+
while (stack.length > 0) {
|
|
404
|
+
const node = stack.pop();
|
|
405
|
+
order.push(node);
|
|
406
|
+
for (const child of node.children) {
|
|
407
|
+
stack.push(child);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return order;
|
|
411
|
+
}
|
|
134
412
|
/**
|
|
135
413
|
* #stateModel MsaView
|
|
136
414
|
*
|
|
@@ -166,10 +444,8 @@ function stateModelFactory() {
|
|
|
166
444
|
/**
|
|
167
445
|
* #property
|
|
168
446
|
* whether the domain legend is expanded. The legend floats over the
|
|
169
|
-
* top-right of the alignment, so
|
|
170
|
-
*
|
|
171
|
-
* and keep it collapsed, and what lets a session or a figure open with
|
|
172
|
-
* it already out of the way.
|
|
447
|
+
* top-right of the alignment and covers residues, so a session or
|
|
448
|
+
* figure can open with it collapsed.
|
|
173
449
|
*/
|
|
174
450
|
showDomainLegend: stripDefault(types.boolean, defaultShowDomainLegend),
|
|
175
451
|
/**
|
|
@@ -198,6 +474,11 @@ function stateModelFactory() {
|
|
|
198
474
|
* zoom in/out on plain mouse-wheel without holding ctrl
|
|
199
475
|
*/
|
|
200
476
|
scrollZoom: stripDefault(types.boolean, defaultScrollZoom),
|
|
477
|
+
/**
|
|
478
|
+
* #property
|
|
479
|
+
* which cell dimensions a wheel zoom scales, while `scrollZoom` is on
|
|
480
|
+
*/
|
|
481
|
+
scrollZoomAxis: stripDefault(types.enumeration('ScrollZoomAxis', [...scrollZoomAxes]), defaultScrollZoomAxis),
|
|
201
482
|
/**
|
|
202
483
|
* #property
|
|
203
484
|
* height of the div containing the view, px
|
|
@@ -269,18 +550,25 @@ function stateModelFactory() {
|
|
|
269
550
|
turnedOffTracks: stripDefault(types.map(types.boolean), {}),
|
|
270
551
|
/**
|
|
271
552
|
* #property
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
553
|
+
* the height of every track one divider resizes, keyed by `heightKey`:
|
|
554
|
+
* the `kind` for the computed tracks, `own:<id>` for a data track. A
|
|
555
|
+
* key is absent until the user drags that divider, and
|
|
556
|
+
* `defaultTrackHeights` answers for it until then, so an untouched
|
|
557
|
+
* viewer adds nothing to the shared URL.
|
|
558
|
+
*/
|
|
559
|
+
trackHeights: stripDefault(types.map(types.number), {}),
|
|
560
|
+
/**
|
|
561
|
+
* #property
|
|
562
|
+
* tracks supplied as data: per-column values drawn as bars, or a
|
|
563
|
+
* per-column string drawn as a text track. See docs/layers.md
|
|
275
564
|
*/
|
|
276
565
|
columnTracks: stripDefault(types.array(types.frozen()), []),
|
|
277
566
|
/**
|
|
278
567
|
* #property
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
* worked -- so it arrives computed. See docs/layers.md
|
|
568
|
+
* row-to-structure residue correspondence, computed outside the viewer
|
|
569
|
+
* (e.g. from SIFTS). Matching by sequence equality places a tagged
|
|
570
|
+
* construct, a truncation or a subsequence row on the wrong residue.
|
|
571
|
+
* See docs/layers.md
|
|
284
572
|
*/
|
|
285
573
|
residueMappings: stripDefault(types.array(types.frozen()), []),
|
|
286
574
|
/**
|
|
@@ -288,18 +576,13 @@ function stateModelFactory() {
|
|
|
288
576
|
* data from the loaded tree/msa/treeMetadata, generally loaded by
|
|
289
577
|
* autorun
|
|
290
578
|
*/
|
|
291
|
-
data: types.optional(DataModelF(), {
|
|
292
|
-
tree: '',
|
|
293
|
-
msa: '',
|
|
294
|
-
treeMetadata: '',
|
|
295
|
-
}),
|
|
579
|
+
data: types.optional(DataModelF(), {}),
|
|
296
580
|
/**
|
|
297
581
|
* #property
|
|
298
582
|
* the user's explicit hide choices per annotation accession, keyed by
|
|
299
|
-
* accession with the value meaning "off",
|
|
300
|
-
*
|
|
301
|
-
*
|
|
302
|
-
* shared URL until someone filters one out
|
|
583
|
+
* accession with the value meaning "off", like `turnedOffTracks`. An
|
|
584
|
+
* untouched accession is absent and drawn, so the shared URL grows
|
|
585
|
+
* only with the user's filters
|
|
303
586
|
*/
|
|
304
587
|
turnedOffFeatures: stripDefault(types.map(types.boolean), {}),
|
|
305
588
|
/**
|
|
@@ -320,24 +603,53 @@ function stateModelFactory() {
|
|
|
320
603
|
* labeled highlights in 1-based inclusive coordinates: a column span
|
|
321
604
|
* `{start, end}`, a residue span `{row, start, end}` of a named row,
|
|
322
605
|
* or a row set `{rows}`, each with an optional `label` and `color`.
|
|
323
|
-
* Persists in the snapshot
|
|
606
|
+
* Persists in the snapshot and the URL.
|
|
324
607
|
*/
|
|
325
608
|
highlights: stripDefault(types.array(types.frozen()), []),
|
|
609
|
+
/**
|
|
610
|
+
* #property
|
|
611
|
+
* clades of the tree with a mark drawn over them. `mrca` names tips
|
|
612
|
+
* whose common ancestor is the clade, or `range` its first and last
|
|
613
|
+
* tip in display order, and `tips` is the leaf count the producer
|
|
614
|
+
* measured. `mark` is `highlight`, `bracket`, `collapse` or `focus`.
|
|
615
|
+
* See docs/layers.md
|
|
616
|
+
*/
|
|
617
|
+
clades: stripDefault(types.array(types.frozen()), []),
|
|
618
|
+
/**
|
|
619
|
+
* #property
|
|
620
|
+
* what the viewer's own marks read from `rowData`:
|
|
621
|
+
* `{channel, field, scale?}` per channel, where `channel` is
|
|
622
|
+
* `tipLabel` or `rowTint`. See docs/layers.md
|
|
623
|
+
*/
|
|
624
|
+
encodings: stripDefault(types.array(types.frozen()), []),
|
|
625
|
+
/**
|
|
626
|
+
* #property
|
|
627
|
+
* panels drawn between the tree and the alignment:
|
|
628
|
+
* `{kind: "strip", field, scale?, width?, header?}` colors one cell
|
|
629
|
+
* per row from a `rowData` field, and
|
|
630
|
+
* `{kind: "features", x, encoding?, transform?, width?, header?}`
|
|
631
|
+
* draws the spans the GFF carries. See docs/layers.md
|
|
632
|
+
*/
|
|
633
|
+
rowPanels: stripDefault(types.array(types.frozen()), []),
|
|
326
634
|
}))
|
|
327
635
|
.volatile(() => ({
|
|
328
636
|
/**
|
|
329
637
|
* #volatile
|
|
330
638
|
*/
|
|
331
639
|
headerHeight: 0,
|
|
640
|
+
/**
|
|
641
|
+
* #volatile
|
|
642
|
+
* leaves the toolbar out, for a host drawing its own controls. Kept out
|
|
643
|
+
* of the snapshot so a link opened in the full app shows the toolbar.
|
|
644
|
+
*/
|
|
645
|
+
hideHeader: false,
|
|
332
646
|
/**
|
|
333
647
|
* #volatile
|
|
334
648
|
*/
|
|
335
649
|
status: undefined,
|
|
336
650
|
/**
|
|
337
651
|
* #volatile
|
|
338
|
-
*
|
|
339
|
-
* screens. derived from the device pixel ratio so canvases are crisp on
|
|
340
|
-
* retina/4k displays and not needlessly oversized on standard ones
|
|
652
|
+
* canvas scale factor, from the device pixel ratio
|
|
341
653
|
*/
|
|
342
654
|
highResScaleFactor: typeof window === 'undefined' ? 1 : window.devicePixelRatio,
|
|
343
655
|
/**
|
|
@@ -394,40 +706,14 @@ function stateModelFactory() {
|
|
|
394
706
|
highlightedColumns: undefined,
|
|
395
707
|
/**
|
|
396
708
|
* #volatile
|
|
397
|
-
* transient highlights keyed by
|
|
398
|
-
*
|
|
399
|
-
* both want to point at a column, and with one slot whoever clears last
|
|
400
|
-
* erases the other's. Keyed by owner, each source adds and removes only
|
|
401
|
-
* its own. Not persisted: a hover is not part of the document.
|
|
709
|
+
* transient highlights keyed by owner, so a structure viewer's hover and
|
|
710
|
+
* a genome view's hover each clear only their own. Not persisted.
|
|
402
711
|
*/
|
|
403
712
|
transientHighlights: {},
|
|
404
713
|
/**
|
|
405
714
|
* #volatile
|
|
406
715
|
*/
|
|
407
716
|
minimapHeight: 56,
|
|
408
|
-
/**
|
|
409
|
-
* #volatile
|
|
410
|
-
*/
|
|
411
|
-
conservationTrackHeight: 40,
|
|
412
|
-
/**
|
|
413
|
-
* #volatile
|
|
414
|
-
* heights of individual `columnTracks`, by track id. A data track is
|
|
415
|
-
* resized on its own: the shared per-kind heights below belong to the
|
|
416
|
-
* tracks the viewer computes, and dragging a data track's handle used to
|
|
417
|
-
* resize those instead.
|
|
418
|
-
*/
|
|
419
|
-
columnTrackHeights: {},
|
|
420
|
-
/**
|
|
421
|
-
* #volatile
|
|
422
|
-
* taller than the conservation track by default: the logo spends its
|
|
423
|
-
* height on stacked glyphs, and a 40px stack of four residues leaves each
|
|
424
|
-
* one too short to identify
|
|
425
|
-
*/
|
|
426
|
-
sequenceLogoTrackHeight: 80,
|
|
427
|
-
/**
|
|
428
|
-
* #volatile
|
|
429
|
-
*/
|
|
430
|
-
arcTrackHeight: 50,
|
|
431
717
|
/**
|
|
432
718
|
* #volatile
|
|
433
719
|
*/
|
|
@@ -439,33 +725,28 @@ function stateModelFactory() {
|
|
|
439
725
|
error: undefined,
|
|
440
726
|
/**
|
|
441
727
|
* #volatile
|
|
442
|
-
* load problems
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
* decorative file
|
|
728
|
+
* non-fatal load problems: an optional layer that failed to load, an
|
|
729
|
+
* overlay that failed to parse. `error` replaces the view and is for the
|
|
730
|
+
* alignment itself
|
|
446
731
|
*/
|
|
447
732
|
warnings: [],
|
|
448
733
|
/**
|
|
449
734
|
* #volatile
|
|
450
|
-
* bumped by reset(). The
|
|
451
|
-
*
|
|
452
|
-
* nothing after a render error until this became its key
|
|
735
|
+
* bumped by reset(). The error boundary above the view uses it as its key,
|
|
736
|
+
* since the boundary keeps its caught error until remounted
|
|
453
737
|
*/
|
|
454
738
|
resetCount: 0,
|
|
455
739
|
/**
|
|
456
740
|
* #volatile
|
|
457
|
-
* set by a host that restores the loaded documents
|
|
458
|
-
*
|
|
459
|
-
*
|
|
460
|
-
* link that opens empty, and under such a host the link does not
|
|
741
|
+
* set by a host that restores the loaded documents itself, such as a
|
|
742
|
+
* jbrowse session or a page that refetches them. `unshareableData` then
|
|
743
|
+
* reports nothing
|
|
461
744
|
*/
|
|
462
745
|
hostCarriesData: false,
|
|
463
746
|
/**
|
|
464
747
|
* #volatile
|
|
465
|
-
* overlay annotations drawn on the alignment,
|
|
466
|
-
*
|
|
467
|
-
* flat list before it reaches the model, so nothing downstream of here
|
|
468
|
-
* knows which one it came from
|
|
748
|
+
* overlay annotations drawn on the alignment. InterProScan JSON, GFF and
|
|
749
|
+
* user uploads all convert to this flat list
|
|
469
750
|
*/
|
|
470
751
|
annotations: [],
|
|
471
752
|
}))
|
|
@@ -511,8 +792,8 @@ function stateModelFactory() {
|
|
|
511
792
|
},
|
|
512
793
|
/**
|
|
513
794
|
* #action
|
|
514
|
-
*
|
|
515
|
-
*
|
|
795
|
+
* update the canvas scale factor when the device pixel ratio changes
|
|
796
|
+
* (moving between monitors, browser zoom)
|
|
516
797
|
*/
|
|
517
798
|
setHighResScaleFactor(arg) {
|
|
518
799
|
self.highResScaleFactor = arg;
|
|
@@ -533,11 +814,13 @@ function stateModelFactory() {
|
|
|
533
814
|
},
|
|
534
815
|
/**
|
|
535
816
|
* #action
|
|
536
|
-
*
|
|
537
|
-
*
|
|
817
|
+
* record a non-fatal load problem: a layer that failed to load, a file
|
|
818
|
+
* that failed to parse. A message already on the list is not added again
|
|
538
819
|
*/
|
|
539
820
|
addWarning(warning) {
|
|
540
|
-
self.warnings
|
|
821
|
+
if (!self.warnings.includes(warning)) {
|
|
822
|
+
self.warnings = [...self.warnings, warning];
|
|
823
|
+
}
|
|
541
824
|
},
|
|
542
825
|
/**
|
|
543
826
|
* #action
|
|
@@ -548,7 +831,7 @@ function stateModelFactory() {
|
|
|
548
831
|
/**
|
|
549
832
|
* #action
|
|
550
833
|
* declare that this host restores the loaded documents itself, which
|
|
551
|
-
*
|
|
834
|
+
* hides the "Not in the link" warning. See `hostCarriesData`
|
|
552
835
|
*/
|
|
553
836
|
setHostCarriesData(arg) {
|
|
554
837
|
self.hostCarriesData = arg;
|
|
@@ -557,9 +840,9 @@ function stateModelFactory() {
|
|
|
557
840
|
* #action
|
|
558
841
|
* set mouse position (row, column) in the MSA
|
|
559
842
|
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
562
|
-
*
|
|
843
|
+
* Public API: a host calls this (and reads `mouseCol`) to sync hover with
|
|
844
|
+
* its own view, such as a genome view or 3D structure. Keep the name and
|
|
845
|
+
* signature stable.
|
|
563
846
|
*/
|
|
564
847
|
setMousePos(col, row) {
|
|
565
848
|
self.mouseCol = col;
|
|
@@ -569,9 +852,9 @@ function stateModelFactory() {
|
|
|
569
852
|
* #action
|
|
570
853
|
* set highlighted columns
|
|
571
854
|
*
|
|
572
|
-
*
|
|
573
|
-
* afterCreateAutoruns
|
|
574
|
-
*
|
|
855
|
+
* Public API: jbrowse-plugin-msaview calls this from its
|
|
856
|
+
* afterCreateAutoruns, and MSAViewer passes its `highlightColumns` prop
|
|
857
|
+
* through it.
|
|
575
858
|
*/
|
|
576
859
|
setHighlightedColumns(columns) {
|
|
577
860
|
self.highlightedColumns = columns;
|
|
@@ -584,10 +867,15 @@ function stateModelFactory() {
|
|
|
584
867
|
},
|
|
585
868
|
/**
|
|
586
869
|
* #action
|
|
587
|
-
*
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
870
|
+
* replace the clades the viewer marks (see docs/layers.md)
|
|
871
|
+
*/
|
|
872
|
+
setClades(clades) {
|
|
873
|
+
self.clades.replace(clades);
|
|
874
|
+
},
|
|
875
|
+
/**
|
|
876
|
+
* #action
|
|
877
|
+
* show `highlights` for `owner`, replacing that owner's previous ones and
|
|
878
|
+
* leaving other owners' in place
|
|
591
879
|
*/
|
|
592
880
|
applyHighlight(owner, highlights) {
|
|
593
881
|
self.transientHighlights = {
|
|
@@ -597,7 +885,7 @@ function stateModelFactory() {
|
|
|
597
885
|
},
|
|
598
886
|
/**
|
|
599
887
|
* #action
|
|
600
|
-
*
|
|
888
|
+
* remove `owner`'s highlights, leaving other owners' in place
|
|
601
889
|
*/
|
|
602
890
|
clearHighlight(owner) {
|
|
603
891
|
if (owner in self.transientHighlights) {
|
|
@@ -649,10 +937,9 @@ function stateModelFactory() {
|
|
|
649
937
|
},
|
|
650
938
|
/**
|
|
651
939
|
* #action
|
|
652
|
-
* switch to another alignment of a multi-alignment file (Stockholm).
|
|
653
|
-
*
|
|
654
|
-
* the
|
|
655
|
-
* reference row, the scroll position -- goes with it
|
|
940
|
+
* switch to another alignment of a multi-alignment file (Stockholm).
|
|
941
|
+
* Clears the collapsed node ids, the subtree in focus, the reference row
|
|
942
|
+
* and the scroll position, which all refer to the previous alignment
|
|
656
943
|
*/
|
|
657
944
|
setCurrentAlignment(n) {
|
|
658
945
|
if (n === self.currentAlignment) {
|
|
@@ -739,11 +1026,10 @@ function stateModelFactory() {
|
|
|
739
1026
|
},
|
|
740
1027
|
/**
|
|
741
1028
|
* #action
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
* read. An autorun parses this back into annotations.
|
|
1029
|
+
* store the GFF text in the snapshot like the alignment and tree. The
|
|
1030
|
+
* parsed annotations are volatile and a blob filehandle is cleared once
|
|
1031
|
+
* read, so the text is the only persisted copy. An autorun parses it
|
|
1032
|
+
* into annotations.
|
|
747
1033
|
*/
|
|
748
1034
|
setGFF(result) {
|
|
749
1035
|
self.data.setGFF(result);
|
|
@@ -772,16 +1058,14 @@ function stateModelFactory() {
|
|
|
772
1058
|
},
|
|
773
1059
|
/**
|
|
774
1060
|
* #getter
|
|
775
|
-
* whether
|
|
776
|
-
*
|
|
777
|
-
* has anything to warn about.
|
|
1061
|
+
* whether the host restores the loaded documents outside the snapshot.
|
|
1062
|
+
* When true, `unshareableData` is empty.
|
|
778
1063
|
*
|
|
779
|
-
* A simple host
|
|
780
|
-
*
|
|
781
|
-
* `.views` block
|
|
782
|
-
*
|
|
783
|
-
*
|
|
784
|
-
* elsewhere. `unshareableData` reads it off `self`, so an override wins.
|
|
1064
|
+
* A simple host sets `hostCarriesData`. A host where this depends on how
|
|
1065
|
+
* the view was opened overrides the getter in its own composed model's
|
|
1066
|
+
* `.views` block; jbrowse-plugin-msaview's indexed-location views refetch
|
|
1067
|
+
* from a URL the session holds, while its data-store views do not.
|
|
1068
|
+
* `unshareableData` reads it off `self`, so an override takes effect.
|
|
785
1069
|
*/
|
|
786
1070
|
get hostRestoresData() {
|
|
787
1071
|
return self.hostCarriesData;
|
|
@@ -804,8 +1088,8 @@ function stateModelFactory() {
|
|
|
804
1088
|
* #getter
|
|
805
1089
|
*/
|
|
806
1090
|
get colorScheme() {
|
|
807
|
-
// colorSchemeName is a free string (menus, snapshots, URL params)
|
|
808
|
-
// back to the default
|
|
1091
|
+
// colorSchemeName is a free string (menus, snapshots, URL params), so a
|
|
1092
|
+
// stale name falls back to the default
|
|
809
1093
|
return (colorSchemes[self.colorSchemeName] ??
|
|
810
1094
|
colorSchemes[defaultColorSchemeName]);
|
|
811
1095
|
},
|
|
@@ -832,29 +1116,23 @@ function stateModelFactory() {
|
|
|
832
1116
|
},
|
|
833
1117
|
/**
|
|
834
1118
|
* #getter
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
* rather than put megabytes of sequence into a session or a URL.
|
|
1119
|
+
* loaded documents left out of the snapshot, largest first. A file opened
|
|
1120
|
+
* from disk or pasted in becomes inline text, and DataModel drops an
|
|
1121
|
+
* inline document past `maxInlineSnapshotBytes`.
|
|
839
1122
|
*
|
|
840
|
-
*
|
|
841
|
-
*
|
|
842
|
-
*
|
|
843
|
-
*
|
|
844
|
-
* keeps the filehandle and refetches through it.
|
|
1123
|
+
* The header lists these, and the standalone app stops rewriting the
|
|
1124
|
+
* address bar while the list is non-empty, so a copied link does not
|
|
1125
|
+
* open an empty viewer unannounced. A document fetched from a URL never
|
|
1126
|
+
* appears here, since the snapshot keeps its filehandle.
|
|
845
1127
|
*
|
|
846
|
-
*
|
|
847
|
-
* means (see `hostRestoresData`) -- inside a session that reloads these
|
|
848
|
-
* documents from somewhere the snapshot does not show, the warning is
|
|
849
|
-
* simply wrong.
|
|
1128
|
+
* Empty when `hostRestoresData` is true.
|
|
850
1129
|
*/
|
|
851
1130
|
get unshareableData() {
|
|
852
1131
|
if (self.hostRestoresData) {
|
|
853
1132
|
return [];
|
|
854
1133
|
}
|
|
855
1134
|
const { data } = self;
|
|
856
|
-
//
|
|
857
|
-
// inline document does, and left unreported the same way too
|
|
1135
|
+
// data tracks past the limit also leave the snapshot
|
|
858
1136
|
const trackBytes = columnTrackSizes(self.columnTracks)
|
|
859
1137
|
.filter(bytes => bytes > maxInlineSnapshotBytes)
|
|
860
1138
|
.reduce((a, b) => a + b, 0);
|
|
@@ -875,13 +1153,14 @@ function stateModelFactory() {
|
|
|
875
1153
|
},
|
|
876
1154
|
/**
|
|
877
1155
|
* #getter
|
|
878
|
-
* extra per
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
*
|
|
882
|
-
*
|
|
1156
|
+
* the row table: extra fields per row, keyed by row name, which the
|
|
1157
|
+
* `encodings` channels read. It is stored as the JSON string
|
|
1158
|
+
* `data.treeMetadata`, the name that travels in existing links, so the
|
|
1159
|
+
* inline size limit and `treeMetadataFilehandle` cover it. labelWidthMap
|
|
1160
|
+
* reads it on every layout, so a malformed user-supplied file returns {}
|
|
1161
|
+
* instead of throwing out of rendering.
|
|
883
1162
|
*/
|
|
884
|
-
get
|
|
1163
|
+
get rowData() {
|
|
885
1164
|
const text = self.data.treeMetadata;
|
|
886
1165
|
if (!text) {
|
|
887
1166
|
return {};
|
|
@@ -893,10 +1172,31 @@ function stateModelFactory() {
|
|
|
893
1172
|
: {};
|
|
894
1173
|
}
|
|
895
1174
|
catch (e) {
|
|
896
|
-
console.error('failed to parse
|
|
1175
|
+
console.error('failed to parse rowData', e);
|
|
897
1176
|
return {};
|
|
898
1177
|
}
|
|
899
1178
|
},
|
|
1179
|
+
/**
|
|
1180
|
+
* #method
|
|
1181
|
+
* one row's fields, the single reader of the row table
|
|
1182
|
+
*/
|
|
1183
|
+
rowDataOf(name) {
|
|
1184
|
+
return this.rowData[name];
|
|
1185
|
+
},
|
|
1186
|
+
/**
|
|
1187
|
+
* #getter
|
|
1188
|
+
* the field names the row table carries, sorted, for a producer or a UI
|
|
1189
|
+
* choosing one to encode
|
|
1190
|
+
*/
|
|
1191
|
+
get rowFields() {
|
|
1192
|
+
const fields = new Set();
|
|
1193
|
+
for (const row of Object.values(this.rowData)) {
|
|
1194
|
+
for (const field of Object.keys(row ?? {})) {
|
|
1195
|
+
fields.add(field);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return [...fields].sort((a, b) => a.localeCompare(b));
|
|
1199
|
+
},
|
|
900
1200
|
/**
|
|
901
1201
|
* #getter
|
|
902
1202
|
*/
|
|
@@ -942,6 +1242,33 @@ function stateModelFactory() {
|
|
|
942
1242
|
get rowNamesSet() {
|
|
943
1243
|
return new Map(this.leaves.map((leaf, index) => [leaf.data.name, index]));
|
|
944
1244
|
},
|
|
1245
|
+
/**
|
|
1246
|
+
* #getter
|
|
1247
|
+
* `clades` resolved to the rows each one covers. The tip names resolve
|
|
1248
|
+
* against `tree` rather than `root`, so a clade whose ancestor the user
|
|
1249
|
+
* collapsed keeps its rows. One leaf pass over the tree serves every
|
|
1250
|
+
* clade. A `range` record names no node, so `collapse` and `focus`, which
|
|
1251
|
+
* need one, drop it.
|
|
1252
|
+
*/
|
|
1253
|
+
get resolvedClades() {
|
|
1254
|
+
if (self.clades.length === 0) {
|
|
1255
|
+
return noClades;
|
|
1256
|
+
}
|
|
1257
|
+
return resolveClades(self.clades, hierarchy(this.tree, d => d.children), this.rowNamesSet);
|
|
1258
|
+
},
|
|
1259
|
+
/**
|
|
1260
|
+
* #getter
|
|
1261
|
+
* the pixel column reserved at the right of the tree area for the bracket
|
|
1262
|
+
* mark, which the tip labels and the tree itself stay clear of. Zero
|
|
1263
|
+
* where no clade draws a bar or a label.
|
|
1264
|
+
*/
|
|
1265
|
+
get cladeGutterWidth() {
|
|
1266
|
+
return cladeGutterWidth({
|
|
1267
|
+
clades: this.resolvedClades,
|
|
1268
|
+
rowHeight: self.rowHeight,
|
|
1269
|
+
fontSize: this.fontSize,
|
|
1270
|
+
});
|
|
1271
|
+
},
|
|
945
1272
|
/**
|
|
946
1273
|
* #getter
|
|
947
1274
|
*/
|
|
@@ -974,40 +1301,128 @@ function stateModelFactory() {
|
|
|
974
1301
|
* #getter
|
|
975
1302
|
*/
|
|
976
1303
|
get root() {
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
1304
|
+
return buildTreeRoot(this.tree, self.collapsed, self.showOnly);
|
|
1305
|
+
},
|
|
1306
|
+
/**
|
|
1307
|
+
* #getter
|
|
1308
|
+
* height of the band the tree overview draws in, zero when it is off
|
|
1309
|
+
*/
|
|
1310
|
+
get treeOverviewHeight() {
|
|
1311
|
+
return self.showTreeOverview ? self.overviewHeight : 0;
|
|
1312
|
+
},
|
|
1313
|
+
/**
|
|
1314
|
+
* #getter
|
|
1315
|
+
* the whole tree laid out for the overview, or undefined when the
|
|
1316
|
+
* overview is off. The focus is left out, so the focused subtree draws
|
|
1317
|
+
* inside the whole tree, and the collapsed clades are folded, since
|
|
1318
|
+
* those are rows the view no longer has. `x` is in tip-index space and
|
|
1319
|
+
* `len` is a fraction of the root-to-tip length, so one layout serves
|
|
1320
|
+
* any band size.
|
|
1321
|
+
*/
|
|
1322
|
+
get treeOverviewLayout() {
|
|
1323
|
+
if (!self.showTreeOverview) {
|
|
1324
|
+
return undefined;
|
|
985
1325
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1326
|
+
const root = buildTreeRoot(this.tree, self.collapsed);
|
|
1327
|
+
const numTips = leaves(root).length;
|
|
1328
|
+
clusterLayout(root, numTips, 1);
|
|
1329
|
+
const rootLen = Math.max(root.data.length || 0, 0);
|
|
1330
|
+
const extent = maxLength(root) - rootLen;
|
|
1331
|
+
setBrLength(root, -rootLen, extent ? 1 / extent : 0);
|
|
1332
|
+
return {
|
|
1333
|
+
root,
|
|
1334
|
+
numTips,
|
|
1335
|
+
maxDepthToLeaf: calcDepthToLeaf(root),
|
|
1336
|
+
showBranchLen: self.showBranchLen && extent > 0,
|
|
1337
|
+
};
|
|
1338
|
+
},
|
|
1339
|
+
/**
|
|
1340
|
+
* #getter
|
|
1341
|
+
* the `clades` highlights in the overview's own row space, which the
|
|
1342
|
+
* focus does not narrow
|
|
1343
|
+
*/
|
|
1344
|
+
get treeOverviewClades() {
|
|
1345
|
+
const layout = this.treeOverviewLayout;
|
|
1346
|
+
if (!layout || self.clades.length === 0) {
|
|
1347
|
+
return noClades;
|
|
1348
|
+
}
|
|
1349
|
+
const rowNames = new Map(leaves(layout.root).map((leaf, index) => [leaf.data.name, index]));
|
|
1350
|
+
return resolveClades(self.clades, layout.root, rowNames);
|
|
1351
|
+
},
|
|
1352
|
+
/**
|
|
1353
|
+
* #getter
|
|
1354
|
+
* the inclusive tip rows the focused subtree covers in the overview,
|
|
1355
|
+
* which is the box drawn on it. undefined with no focus
|
|
1356
|
+
*/
|
|
1357
|
+
get treeOverviewFocusRows() {
|
|
1358
|
+
const layout = this.treeOverviewLayout;
|
|
1359
|
+
if (!layout || !self.showOnly) {
|
|
1360
|
+
return undefined;
|
|
1361
|
+
}
|
|
1362
|
+
const node = find(layout.root, n => n.data.id === self.showOnly);
|
|
1363
|
+
return node ? tipRange(node) : undefined;
|
|
1364
|
+
},
|
|
1365
|
+
/**
|
|
1366
|
+
* #method
|
|
1367
|
+
* the subtree a point `y` pixels down the tree overview picks: the
|
|
1368
|
+
* deepest one whose tip range covers every row under that pixel, with
|
|
1369
|
+
* the rows it covers. A pixel stands for several tips on a large tree,
|
|
1370
|
+
* which is what keeps the pick off the individual tips. undefined when
|
|
1371
|
+
* the overview is off or the point picks the whole tree.
|
|
1372
|
+
*/
|
|
1373
|
+
treeOverviewHit(y) {
|
|
1374
|
+
const layout = this.treeOverviewLayout;
|
|
1375
|
+
if (!layout) {
|
|
1376
|
+
return undefined;
|
|
1377
|
+
}
|
|
1378
|
+
const { root, numTips } = layout;
|
|
1379
|
+
const perPixel = numTips / self.overviewHeight;
|
|
1380
|
+
const first = clamp(Math.floor(y * perPixel), 0, numTips - 1);
|
|
1381
|
+
const last = clamp(Math.ceil((y + 1) * perPixel) - 1, first, numTips - 1);
|
|
1382
|
+
let node = nodeCoveringRows(root, first + 0.5, last + 0.5);
|
|
1383
|
+
// focusing one tip leaves a single row on screen, so the pick lifts to
|
|
1384
|
+
// the subtree that tip sits in
|
|
1385
|
+
while (!node.children && node.parent) {
|
|
1386
|
+
node = node.parent;
|
|
996
1387
|
}
|
|
997
|
-
return
|
|
1388
|
+
return node === root
|
|
1389
|
+
? undefined
|
|
1390
|
+
: { id: node.data.id, rows: tipRange(node) };
|
|
1391
|
+
},
|
|
1392
|
+
/**
|
|
1393
|
+
* #getter
|
|
1394
|
+
* the pixel column the row panels occupy between the tree and the
|
|
1395
|
+
* alignment, the sum of each record's width
|
|
1396
|
+
*/
|
|
1397
|
+
get rowPanelsWidth() {
|
|
1398
|
+
return sum(self.rowPanels.map(p => rowPanelWidth(p, self.rowHeight)));
|
|
1399
|
+
},
|
|
1400
|
+
/**
|
|
1401
|
+
* #getter
|
|
1402
|
+
* height of the band the row panel headers draw in, which is zero with
|
|
1403
|
+
* no row panels and leaves the top area as it was
|
|
1404
|
+
*/
|
|
1405
|
+
get rowPanelsHeaderHeight() {
|
|
1406
|
+
return self.rowPanels.length > 0 ? rowPanelHeaderHeight : 0;
|
|
998
1407
|
},
|
|
999
1408
|
/**
|
|
1000
1409
|
* #getter
|
|
1001
|
-
* widget width minus the tree area
|
|
1410
|
+
* widget width minus the tree area and the row panels gives the space
|
|
1411
|
+
* for the MSA
|
|
1002
1412
|
*/
|
|
1003
1413
|
get msaAreaWidth() {
|
|
1004
|
-
return self.width -
|
|
1414
|
+
return (self.width -
|
|
1415
|
+
self.treeAreaWidth -
|
|
1416
|
+
this.rowPanelsWidth -
|
|
1417
|
+
self.resizeHandleWidth);
|
|
1005
1418
|
},
|
|
1006
1419
|
/**
|
|
1007
1420
|
* #getter
|
|
1421
|
+
* the right edge the tip labels end at, which is the tree area less the
|
|
1422
|
+
* margin and the bracket gutter
|
|
1008
1423
|
*/
|
|
1009
1424
|
get treeAreaWidthMinusMargin() {
|
|
1010
|
-
return self.treeAreaWidth - self.marginLeft;
|
|
1425
|
+
return self.treeAreaWidth - self.marginLeft - this.cladeGutterWidth;
|
|
1011
1426
|
},
|
|
1012
1427
|
/**
|
|
1013
1428
|
* #getter
|
|
@@ -1054,7 +1469,7 @@ function stateModelFactory() {
|
|
|
1054
1469
|
*/
|
|
1055
1470
|
get insertionPositions() {
|
|
1056
1471
|
const { blanks, rows } = this;
|
|
1057
|
-
if (blanks.length === 0
|
|
1472
|
+
if (blanks.length === 0) {
|
|
1058
1473
|
return new Map();
|
|
1059
1474
|
}
|
|
1060
1475
|
const result = new Map();
|
|
@@ -1077,10 +1492,8 @@ function stateModelFactory() {
|
|
|
1077
1492
|
},
|
|
1078
1493
|
/**
|
|
1079
1494
|
* #getter
|
|
1080
|
-
* number of rows
|
|
1081
|
-
*
|
|
1082
|
-
* row of vertical space (drawn blank), so row hit-testing and fit-to-height
|
|
1083
|
-
* must count it.
|
|
1495
|
+
* number of rows on screen: the leaf count, which includes tree leaves
|
|
1496
|
+
* with no matching MSA row (drawn blank), unlike `rows.length`.
|
|
1084
1497
|
*/
|
|
1085
1498
|
get numRows() {
|
|
1086
1499
|
return this.leaves.length;
|
|
@@ -1088,12 +1501,8 @@ function stateModelFactory() {
|
|
|
1088
1501
|
/**
|
|
1089
1502
|
* #method
|
|
1090
1503
|
* index of the global column holding each ungapped sequence position of a
|
|
1091
|
-
* row
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
* Built per row, on the row asked for: the first lookup used to index
|
|
1095
|
-
* every row in the alignment. The cache is keyed on the parse the rows
|
|
1096
|
-
* came from, so a new alignment brings a new one.
|
|
1504
|
+
* row. The domain overlay resolves thousands of these per redraw. Built
|
|
1505
|
+
* lazily per row and cached on the parse.
|
|
1097
1506
|
*/
|
|
1098
1507
|
seqPosIndex(rowName) {
|
|
1099
1508
|
const MSA = this.MSA;
|
|
@@ -1118,10 +1527,9 @@ function stateModelFactory() {
|
|
|
1118
1527
|
},
|
|
1119
1528
|
/**
|
|
1120
1529
|
* #getter
|
|
1121
|
-
* every sequence the alignment
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
1124
|
-
* collapsing a clade hides rows, it does not delete their sequence
|
|
1530
|
+
* every sequence in the alignment, keyed by row name, including rows a
|
|
1531
|
+
* collapsed clade hides. `rows` holds only the rows on screen; lookups
|
|
1532
|
+
* by row name use this
|
|
1125
1533
|
*/
|
|
1126
1534
|
get rowMap() {
|
|
1127
1535
|
const MSA = this.MSA;
|
|
@@ -1254,10 +1662,8 @@ function stateModelFactory() {
|
|
|
1254
1662
|
clusterLayout(r, this.totalHeight, self.treeWidth);
|
|
1255
1663
|
const max = this.rootToTipLength;
|
|
1256
1664
|
const k = max ? self.treeWidth / max : 0;
|
|
1257
|
-
// the displayed root starts at x=0
|
|
1258
|
-
//
|
|
1259
|
-
// node -- `root` hands out the cached parse, and writing to it made
|
|
1260
|
-
// showOnly shorten that branch for good
|
|
1665
|
+
// the displayed root starts at x=0, so subtract its length here; `root`
|
|
1666
|
+
// returns the cached parse, which must not be mutated
|
|
1261
1667
|
setBrLength(r, -Math.max(r.data.length || 0, 0), k);
|
|
1262
1668
|
// for each collapsed clade, record the pixel x-position of its farthest
|
|
1263
1669
|
// tip so the renderer can draw a triangle spanning the branch-length
|
|
@@ -1293,22 +1699,19 @@ function stateModelFactory() {
|
|
|
1293
1699
|
},
|
|
1294
1700
|
/**
|
|
1295
1701
|
* #getter
|
|
1296
|
-
* x-position of the farthest tip in a phylogram, px
|
|
1297
|
-
*
|
|
1298
|
-
* -- and 0 for a tree carrying no lengths at all, which draws as a
|
|
1299
|
-
* cladogram instead
|
|
1702
|
+
* x-position of the farthest tip in a phylogram, px: treeWidth, or 0 for
|
|
1703
|
+
* a tree with no branch lengths (drawn as a cladogram)
|
|
1300
1704
|
*/
|
|
1301
1705
|
get maxBranchLength() {
|
|
1302
1706
|
return this.rootToTipLength ? self.treeWidth : 0;
|
|
1303
1707
|
},
|
|
1304
1708
|
/**
|
|
1305
1709
|
* #getter
|
|
1306
|
-
* pixels per unit of branch length in the
|
|
1307
|
-
*
|
|
1308
|
-
* scale bar over the tree is drawn from it.
|
|
1710
|
+
* pixels per unit of branch length in the phylogram layout, 0 in
|
|
1711
|
+
* cladogram mode. The tree's scale bar uses it.
|
|
1309
1712
|
*/
|
|
1310
1713
|
get pxPerBranchLength() {
|
|
1311
|
-
const max =
|
|
1714
|
+
const max = this.rootToTipLength;
|
|
1312
1715
|
return this.showBranchLenEffective && max ? self.treeWidth / max : 0;
|
|
1313
1716
|
},
|
|
1314
1717
|
/**
|
|
@@ -1357,25 +1760,141 @@ function stateModelFactory() {
|
|
|
1357
1760
|
return self.drawLabels && self.rowHeight >= minLetterRowHeight;
|
|
1358
1761
|
},
|
|
1359
1762
|
}))
|
|
1763
|
+
.views(self => {
|
|
1764
|
+
// a spec is a frozen value, so it keys its computed until setColumnTracks
|
|
1765
|
+
// replaces it
|
|
1766
|
+
const columnTrackModels = new WeakMap();
|
|
1767
|
+
return {
|
|
1768
|
+
/**
|
|
1769
|
+
* #getter
|
|
1770
|
+
* a data track's values or string, projected from its row's residues
|
|
1771
|
+
* onto alignment columns when it names a row
|
|
1772
|
+
*/
|
|
1773
|
+
get columnTrackContent() {
|
|
1774
|
+
const { MSA, blanks, hideGapsEffective } = self;
|
|
1775
|
+
const width = MSA?.getWidth() ?? 0;
|
|
1776
|
+
const project = (track, items, fill) => {
|
|
1777
|
+
if (!track.row) {
|
|
1778
|
+
return items;
|
|
1779
|
+
}
|
|
1780
|
+
const out = Array.from({ length: width }, () => fill);
|
|
1781
|
+
const index = self.seqPosIndex(track.row);
|
|
1782
|
+
items.forEach((item, seqPos) => {
|
|
1783
|
+
const col = index?.[seqPos];
|
|
1784
|
+
if (col !== undefined) {
|
|
1785
|
+
out[col] = item;
|
|
1786
|
+
}
|
|
1787
|
+
});
|
|
1788
|
+
return out;
|
|
1789
|
+
};
|
|
1790
|
+
const skip = (items) => hideGapsEffective ? dropBlanks(blanks, items) : items;
|
|
1791
|
+
// an arc endpoint maps row residue -> column -> visible column.
|
|
1792
|
+
// visibleColsBefore, not globalColToVisibleCol, so an endpoint in a
|
|
1793
|
+
// hidden column moves to the neighboring visible one and the arc stays
|
|
1794
|
+
const resolve = (track, pos) => {
|
|
1795
|
+
const col = track.row
|
|
1796
|
+
? self.seqPosIndex(track.row)?.[pos - 1]
|
|
1797
|
+
: pos - 1;
|
|
1798
|
+
if (col === undefined || col < 0 || col >= width) {
|
|
1799
|
+
return undefined;
|
|
1800
|
+
}
|
|
1801
|
+
return hideGapsEffective ? visibleColsBefore(blanks, col) : col;
|
|
1802
|
+
};
|
|
1803
|
+
return new Map(self.columnTracks.map(track => {
|
|
1804
|
+
if (track.kind === 'arc') {
|
|
1805
|
+
const arcs = (track.arcs ?? [])
|
|
1806
|
+
.map(arc => {
|
|
1807
|
+
const start = resolve(track, Math.min(arc.start, arc.end));
|
|
1808
|
+
const end = resolve(track, Math.max(arc.start, arc.end));
|
|
1809
|
+
return start !== undefined &&
|
|
1810
|
+
end !== undefined &&
|
|
1811
|
+
start < end
|
|
1812
|
+
? { start, end, color: arc.color }
|
|
1813
|
+
: undefined;
|
|
1814
|
+
})
|
|
1815
|
+
.filter(notEmpty);
|
|
1816
|
+
return [track.id, { arcs }];
|
|
1817
|
+
}
|
|
1818
|
+
if (track.kind === 'bar') {
|
|
1819
|
+
const max = track.max ?? 1;
|
|
1820
|
+
const values = skip(project(track, track.values ?? [], 0)).map(v => Math.min(1, Math.max(0, v / max)));
|
|
1821
|
+
return [track.id, { values }];
|
|
1822
|
+
}
|
|
1823
|
+
const data = skip(project(track, (track.data ?? '').split(''), ' '));
|
|
1824
|
+
return [track.id, { data: data.join('') }];
|
|
1825
|
+
}));
|
|
1826
|
+
},
|
|
1827
|
+
/**
|
|
1828
|
+
* #method
|
|
1829
|
+
* the height a track draws at: what the user dragged its divider to,
|
|
1830
|
+
* then the height its snapshot asked for, then its kind's default. Only
|
|
1831
|
+
* a text track falls through to rowHeight
|
|
1832
|
+
*/
|
|
1833
|
+
trackHeight(kind, heightKey = kind, given) {
|
|
1834
|
+
return (self.trackHeights.get(heightKey) ??
|
|
1835
|
+
given ??
|
|
1836
|
+
defaultTrackHeights[kind] ??
|
|
1837
|
+
self.rowHeight);
|
|
1838
|
+
},
|
|
1839
|
+
/**
|
|
1840
|
+
* #method
|
|
1841
|
+
* the track a column track spec draws as, computed once per spec. A
|
|
1842
|
+
* text track's height falls through to rowHeight, and its own computed
|
|
1843
|
+
* confines the vertical zoom to it, so a sibling keeps its object and
|
|
1844
|
+
* its canvas skips the redraw
|
|
1845
|
+
*/
|
|
1846
|
+
columnTrackModel(track) {
|
|
1847
|
+
let computedModel = columnTrackModels.get(track);
|
|
1848
|
+
if (!computedModel) {
|
|
1849
|
+
computedModel = computed(() => {
|
|
1850
|
+
const heightKey = resizableKinds.has(track.kind)
|
|
1851
|
+
? ownHeightKey(track.id)
|
|
1852
|
+
: undefined;
|
|
1853
|
+
const content = this.columnTrackContent.get(track.id);
|
|
1854
|
+
return {
|
|
1855
|
+
model: {
|
|
1856
|
+
id: track.id,
|
|
1857
|
+
name: track.name,
|
|
1858
|
+
kind: track.kind,
|
|
1859
|
+
heightKey,
|
|
1860
|
+
height: this.trackHeight(track.kind, heightKey, track.height),
|
|
1861
|
+
barColor: track.color,
|
|
1862
|
+
arcColor: track.color,
|
|
1863
|
+
customColorScheme: track.colors,
|
|
1864
|
+
data: content?.data,
|
|
1865
|
+
arcs: content?.arcs,
|
|
1866
|
+
},
|
|
1867
|
+
ReactComponent: TrackBlocks,
|
|
1868
|
+
};
|
|
1869
|
+
});
|
|
1870
|
+
columnTrackModels.set(track, computedModel);
|
|
1871
|
+
}
|
|
1872
|
+
return computedModel.get();
|
|
1873
|
+
},
|
|
1874
|
+
/**
|
|
1875
|
+
* #getter
|
|
1876
|
+
*/
|
|
1877
|
+
get columnTrackModels() {
|
|
1878
|
+
return self.columnTracks.map(track => this.columnTrackModel(track));
|
|
1879
|
+
},
|
|
1880
|
+
};
|
|
1881
|
+
})
|
|
1360
1882
|
.views(self => ({
|
|
1361
1883
|
/**
|
|
1362
1884
|
* #getter
|
|
1363
1885
|
*/
|
|
1364
1886
|
get labelWidthMap() {
|
|
1365
|
-
const { showTreeText, leaves
|
|
1366
|
-
// gated on the
|
|
1367
|
-
// gutter
|
|
1368
|
-
//
|
|
1369
|
-
// Measured at a fixed reference size and scaled by labelWidthScale:
|
|
1370
|
-
// re-measuring every leaf on every vertical-zoom frame cost ~200ms on a
|
|
1887
|
+
const { showTreeText, leaves } = self;
|
|
1888
|
+
// gated on the renderer's label condition, so hidden labels reserve no
|
|
1889
|
+
// gutter. Measured once at a reference size and scaled by
|
|
1890
|
+
// labelWidthScale: re-measuring per vertical-zoom frame cost ~200ms on a
|
|
1371
1891
|
// 50k-leaf tree
|
|
1372
1892
|
return showTreeText
|
|
1373
1893
|
? new Map(leaves.map(node => {
|
|
1374
1894
|
const { name } = node.data;
|
|
1375
1895
|
// `||`, matching renderTreeLabels: an empty genome falls back
|
|
1376
|
-
// to the row name
|
|
1377
|
-
|
|
1378
|
-
const displayName = treeMetadata[name]?.genome || name;
|
|
1896
|
+
// to the row name
|
|
1897
|
+
const displayName = self.rowDataOf(name)?.genome || name;
|
|
1379
1898
|
return [
|
|
1380
1899
|
name,
|
|
1381
1900
|
measureTextCanvas(displayName, labelReferenceFontSize),
|
|
@@ -1392,11 +1911,9 @@ function stateModelFactory() {
|
|
|
1392
1911
|
return self.fontSize / labelReferenceFontSize;
|
|
1393
1912
|
},
|
|
1394
1913
|
get labelsWidth() {
|
|
1395
|
-
// a loop, not Math.max(...
|
|
1396
|
-
//
|
|
1397
|
-
//
|
|
1398
|
-
// "RangeError: Maximum call stack size exceeded" out of a getter the
|
|
1399
|
-
// treeWidth autorun reads on load
|
|
1914
|
+
// a loop, not Math.max(...): spreading passes one argument per leaf,
|
|
1915
|
+
// and the ~125k argument limit throws RangeError on the 230k-tip COVID
|
|
1916
|
+
// tree
|
|
1400
1917
|
let max = 0;
|
|
1401
1918
|
for (const width of this.labelWidthMap.values()) {
|
|
1402
1919
|
if (width > max) {
|
|
@@ -1419,9 +1936,8 @@ function stateModelFactory() {
|
|
|
1419
1936
|
},
|
|
1420
1937
|
/**
|
|
1421
1938
|
* #getter
|
|
1422
|
-
* the base pairs of the consensus secondary structure, as arcs
|
|
1423
|
-
*
|
|
1424
|
-
* the pairs land in the same visible column space the text track does
|
|
1939
|
+
* the base pairs of the consensus secondary structure, as arcs, in
|
|
1940
|
+
* visible column space (hidden columns are removed before parsing)
|
|
1425
1941
|
*/
|
|
1426
1942
|
get secondaryStructureArcs() {
|
|
1427
1943
|
const { blanks, hideGapsEffective } = self;
|
|
@@ -1442,9 +1958,8 @@ function stateModelFactory() {
|
|
|
1442
1958
|
const { MSA, hideGapsEffective, blanks } = self;
|
|
1443
1959
|
const tracks = (MSA?.tracks ?? []).filter(t => !!t.data);
|
|
1444
1960
|
if (tracks.length === 0) {
|
|
1445
|
-
// reading rowHeight
|
|
1446
|
-
// list
|
|
1447
|
-
// changing
|
|
1961
|
+
// return before reading rowHeight, so zooming does not rebuild the
|
|
1962
|
+
// list and redraw every track canvas
|
|
1448
1963
|
return [];
|
|
1449
1964
|
}
|
|
1450
1965
|
const { rowHeight } = self;
|
|
@@ -1460,103 +1975,9 @@ function stateModelFactory() {
|
|
|
1460
1975
|
},
|
|
1461
1976
|
/**
|
|
1462
1977
|
* #getter
|
|
1463
|
-
*
|
|
1464
|
-
*
|
|
1465
|
-
|
|
1466
|
-
get columnTrackContent() {
|
|
1467
|
-
const { MSA, blanks, hideGapsEffective } = self;
|
|
1468
|
-
const width = MSA?.getWidth() ?? 0;
|
|
1469
|
-
const project = (track, items, fill) => {
|
|
1470
|
-
if (!track.row) {
|
|
1471
|
-
return items;
|
|
1472
|
-
}
|
|
1473
|
-
const out = Array.from({ length: width }, () => fill);
|
|
1474
|
-
const index = self.seqPosIndex(track.row);
|
|
1475
|
-
items.forEach((item, seqPos) => {
|
|
1476
|
-
const col = index?.[seqPos];
|
|
1477
|
-
if (col !== undefined) {
|
|
1478
|
-
out[col] = item;
|
|
1479
|
-
}
|
|
1480
|
-
});
|
|
1481
|
-
return out;
|
|
1482
|
-
};
|
|
1483
|
-
const skip = (items) => hideGapsEffective ? dropBlanks(blanks, items) : items;
|
|
1484
|
-
// an arc names two positions rather than one per column, so it takes
|
|
1485
|
-
// the same two steps the arrays take -- a row's residues onto columns,
|
|
1486
|
-
// then columns onto the visible ones -- as a lookup. visibleColsBefore,
|
|
1487
|
-
// not globalColToVisibleCol: an endpoint in a hidden column collapses
|
|
1488
|
-
// to where that column went instead of taking the whole arc with it
|
|
1489
|
-
const resolve = (track, pos) => {
|
|
1490
|
-
const col = track.row
|
|
1491
|
-
? self.seqPosIndex(track.row)?.[pos - 1]
|
|
1492
|
-
: pos - 1;
|
|
1493
|
-
if (col === undefined || col < 0 || col >= width) {
|
|
1494
|
-
return undefined;
|
|
1495
|
-
}
|
|
1496
|
-
return hideGapsEffective ? visibleColsBefore(blanks, col) : col;
|
|
1497
|
-
};
|
|
1498
|
-
return new Map(self.columnTracks.map(track => {
|
|
1499
|
-
if (track.kind === 'arc') {
|
|
1500
|
-
const arcs = (track.arcs ?? [])
|
|
1501
|
-
.map(arc => {
|
|
1502
|
-
const start = resolve(track, Math.min(arc.start, arc.end));
|
|
1503
|
-
const end = resolve(track, Math.max(arc.start, arc.end));
|
|
1504
|
-
return start !== undefined && end !== undefined && start < end
|
|
1505
|
-
? { start, end, color: arc.color }
|
|
1506
|
-
: undefined;
|
|
1507
|
-
})
|
|
1508
|
-
.filter(notEmpty);
|
|
1509
|
-
return [track.id, { arcs }];
|
|
1510
|
-
}
|
|
1511
|
-
if (track.kind === 'bar') {
|
|
1512
|
-
const max = track.max ?? 1;
|
|
1513
|
-
const values = skip(project(track, track.values ?? [], 0)).map(v => Math.min(1, Math.max(0, v / max)));
|
|
1514
|
-
return [track.id, { values }];
|
|
1515
|
-
}
|
|
1516
|
-
const data = skip(project(track, (track.data ?? '').split(''), ' '));
|
|
1517
|
-
return [track.id, { data: data.join('') }];
|
|
1518
|
-
}));
|
|
1519
|
-
},
|
|
1520
|
-
/**
|
|
1521
|
-
* #getter
|
|
1522
|
-
*/
|
|
1523
|
-
get columnTrackModels() {
|
|
1524
|
-
// read per kind, not up front: a text track is the only kind sized by
|
|
1525
|
-
// the row height, and reading it here rebuilt every data track on every
|
|
1526
|
-
// vertical zoom step
|
|
1527
|
-
const defaultHeight = (kind) => kind === 'bar'
|
|
1528
|
-
? self.conservationTrackHeight
|
|
1529
|
-
: kind === 'arc'
|
|
1530
|
-
? self.arcTrackHeight
|
|
1531
|
-
: self.rowHeight;
|
|
1532
|
-
return self.columnTracks.map(track => ({
|
|
1533
|
-
model: {
|
|
1534
|
-
id: track.id,
|
|
1535
|
-
name: track.name,
|
|
1536
|
-
kind: track.kind,
|
|
1537
|
-
height: self.columnTrackHeights[track.id] ??
|
|
1538
|
-
track.height ??
|
|
1539
|
-
defaultHeight(track.kind),
|
|
1540
|
-
// the spec has one `color`; bar and arc are separate track models
|
|
1541
|
-
// that read it under their own name
|
|
1542
|
-
barColor: track.color,
|
|
1543
|
-
arcColor: track.color,
|
|
1544
|
-
customColorScheme: track.colors,
|
|
1545
|
-
data: this.columnTrackContent.get(track.id)?.data,
|
|
1546
|
-
arcs: this.columnTrackContent.get(track.id)?.arcs,
|
|
1547
|
-
},
|
|
1548
|
-
ReactComponent: TrackBlocks,
|
|
1549
|
-
}));
|
|
1550
|
-
},
|
|
1551
|
-
/**
|
|
1552
|
-
* #getter
|
|
1553
|
-
*/
|
|
1554
|
-
/**
|
|
1555
|
-
* #getter
|
|
1556
|
-
* the consensus secondary structure as a track, when there is one. Its
|
|
1557
|
-
* own getter so the object keeps its identity across a zoom: the canvas
|
|
1558
|
-
* redraws on the track it is handed changing, and rebuilding these
|
|
1559
|
-
* alongside everything else made every zoom frame redraw every track
|
|
1978
|
+
* the consensus secondary structure as a track, when there is one. A
|
|
1979
|
+
* separate getter keeps the object stable across zoom, so its canvas
|
|
1980
|
+
* does not redraw
|
|
1560
1981
|
*/
|
|
1561
1982
|
get basePairTrackModels() {
|
|
1562
1983
|
const arcs = this.secondaryStructureArcs;
|
|
@@ -1567,7 +1988,8 @@ function stateModelFactory() {
|
|
|
1567
1988
|
id: 'base-pairs',
|
|
1568
1989
|
name: 'Base pairs',
|
|
1569
1990
|
kind: 'arc',
|
|
1570
|
-
|
|
1991
|
+
heightKey: 'arc',
|
|
1992
|
+
height: self.trackHeight('arc'),
|
|
1571
1993
|
arcs,
|
|
1572
1994
|
},
|
|
1573
1995
|
ReactComponent: TrackBlocks,
|
|
@@ -1577,8 +1999,8 @@ function stateModelFactory() {
|
|
|
1577
1999
|
},
|
|
1578
2000
|
/**
|
|
1579
2001
|
* #getter
|
|
1580
|
-
* the tracks computed from the alignment
|
|
1581
|
-
*
|
|
2002
|
+
* the tracks computed from the alignment; they depend on their heights
|
|
2003
|
+
* and the alphabet, not on zoom
|
|
1582
2004
|
*/
|
|
1583
2005
|
get computedTrackModels() {
|
|
1584
2006
|
return [
|
|
@@ -1586,7 +2008,6 @@ function stateModelFactory() {
|
|
|
1586
2008
|
id: 'conservation',
|
|
1587
2009
|
name: 'Conservation',
|
|
1588
2010
|
kind: 'bar',
|
|
1589
|
-
height: self.conservationTrackHeight,
|
|
1590
2011
|
barColor: 'gray',
|
|
1591
2012
|
},
|
|
1592
2013
|
...(self.sequenceType === 'amino'
|
|
@@ -1595,7 +2016,6 @@ function stateModelFactory() {
|
|
|
1595
2016
|
id: 'property-conservation',
|
|
1596
2017
|
name: 'Property conservation',
|
|
1597
2018
|
kind: 'bar',
|
|
1598
|
-
height: self.conservationTrackHeight,
|
|
1599
2019
|
barColor: '#6a51a3',
|
|
1600
2020
|
},
|
|
1601
2021
|
]
|
|
@@ -1604,23 +2024,32 @@ function stateModelFactory() {
|
|
|
1604
2024
|
id: 'sequence-logo',
|
|
1605
2025
|
name: 'Sequence logo',
|
|
1606
2026
|
kind: 'logo',
|
|
1607
|
-
height: self.sequenceLogoTrackHeight,
|
|
1608
2027
|
},
|
|
1609
2028
|
// last, so it sits against the alignment it numbers
|
|
1610
2029
|
{
|
|
1611
2030
|
id: 'position-ruler',
|
|
1612
2031
|
name: 'Position',
|
|
1613
2032
|
kind: 'ruler',
|
|
1614
|
-
height: 20,
|
|
1615
2033
|
},
|
|
1616
|
-
].map(model => ({
|
|
2034
|
+
].map(model => ({
|
|
2035
|
+
// every computed track of a kind shares that kind's height, so the
|
|
2036
|
+
// kind is its key
|
|
2037
|
+
model: {
|
|
2038
|
+
...model,
|
|
2039
|
+
heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
|
|
2040
|
+
height: self.trackHeight(model.kind),
|
|
2041
|
+
},
|
|
2042
|
+
ReactComponent: TrackBlocks,
|
|
2043
|
+
}));
|
|
1617
2044
|
},
|
|
1618
2045
|
get tracks() {
|
|
1619
2046
|
return [
|
|
1620
2047
|
...this.adapterTrackModels,
|
|
1621
2048
|
...this.basePairTrackModels,
|
|
1622
|
-
...
|
|
1623
|
-
|
|
2049
|
+
...self.columnTrackModels,
|
|
2050
|
+
// every computed track reads the alignment's columns, and a tree, a
|
|
2051
|
+
// GFF and a features panel draw a figure with none
|
|
2052
|
+
...(self.numColumns > 0 ? this.computedTrackModels : []),
|
|
1624
2053
|
];
|
|
1625
2054
|
},
|
|
1626
2055
|
/**
|
|
@@ -1651,19 +2080,18 @@ function stateModelFactory() {
|
|
|
1651
2080
|
* Convert a visible column to a row-specific sequence position (0-based).
|
|
1652
2081
|
* Returns undefined if the position is a gap in the sequence.
|
|
1653
2082
|
*
|
|
1654
|
-
*
|
|
1655
|
-
*
|
|
1656
|
-
*
|
|
1657
|
-
*
|
|
2083
|
+
* Public API, like the sibling converters (visibleColToGlobalCol,
|
|
2084
|
+
* seqPosToVisibleCol, globalColToVisibleCol, seqPosToGlobalCol) hosts
|
|
2085
|
+
* use to translate between columns and residue positions. Keep them
|
|
2086
|
+
* stable.
|
|
1658
2087
|
*
|
|
1659
2088
|
* @param rowName - The name of the row
|
|
1660
2089
|
* @param visibleCol - The visible column index
|
|
1661
2090
|
* @returns The sequence position (0-based), or undefined if it's a gap
|
|
1662
2091
|
*/
|
|
1663
2092
|
visibleColToSeqPos(rowName, visibleCol) {
|
|
1664
|
-
//
|
|
1665
|
-
//
|
|
1666
|
-
// is the whole frame
|
|
2093
|
+
// binary search: this runs on every mouse move, and scanning a
|
|
2094
|
+
// 30k-column row per event takes the whole frame
|
|
1667
2095
|
return seqPosOfGlobalCol(self.seqPosIndex(rowName), this.visibleColToGlobalCol(visibleCol));
|
|
1668
2096
|
},
|
|
1669
2097
|
/**
|
|
@@ -1697,10 +2125,9 @@ function stateModelFactory() {
|
|
|
1697
2125
|
},
|
|
1698
2126
|
/**
|
|
1699
2127
|
* #method
|
|
1700
|
-
* Convert a visible column index (what a mouse handler reports)
|
|
1701
|
-
* column of the full alignment.
|
|
1702
|
-
*
|
|
1703
|
-
* hop before indexing it.
|
|
2128
|
+
* Convert a visible column index (what a mouse handler reports) to a
|
|
2129
|
+
* column of the full alignment. A host indexing its own per-column data
|
|
2130
|
+
* needs this when columns are hidden.
|
|
1704
2131
|
*
|
|
1705
2132
|
* @param visibleCol - The visible column index
|
|
1706
2133
|
* @returns The global column index in the full MSA
|
|
@@ -1714,8 +2141,7 @@ function stateModelFactory() {
|
|
|
1714
2141
|
/**
|
|
1715
2142
|
* #method
|
|
1716
2143
|
* Convert a sequence position (ungapped) to a global column index.
|
|
1717
|
-
* Returns undefined for a row the alignment does not have
|
|
1718
|
-
* anyway is how a mistyped or stale row name came to highlight column 0.
|
|
2144
|
+
* Returns undefined for a row name the alignment does not have.
|
|
1719
2145
|
*
|
|
1720
2146
|
* @param rowName - The name of the row
|
|
1721
2147
|
* @param seqPos - The sequence position (0-based, ungapped)
|
|
@@ -1746,12 +2172,39 @@ function stateModelFactory() {
|
|
|
1746
2172
|
? undefined
|
|
1747
2173
|
: this.globalColToVisibleCol(globalCol);
|
|
1748
2174
|
},
|
|
2175
|
+
/**
|
|
2176
|
+
* #method
|
|
2177
|
+
* the visible columns a span covers, in highlight coordinates: `start`
|
|
2178
|
+
* and `end` are 1-based residues of `row`, or columns of the file
|
|
2179
|
+
* without it. A fractional position, as a zoom gesture reports one,
|
|
2180
|
+
* widens to the whole residue or column it falls in. A span entirely on
|
|
2181
|
+
* hidden columns, or naming a row the alignment lacks, gives undefined.
|
|
2182
|
+
*/
|
|
2183
|
+
visibleSpan({ row, start: rawStart, end: rawEnd }) {
|
|
2184
|
+
const { blanks } = self;
|
|
2185
|
+
const start = Math.max(1, Math.floor(rawStart));
|
|
2186
|
+
const end = Math.ceil(rawEnd);
|
|
2187
|
+
let startGlobal = start - 1;
|
|
2188
|
+
let endGlobal = end - 1;
|
|
2189
|
+
if (row !== undefined) {
|
|
2190
|
+
const rowStart = this.seqPosToGlobalCol(row, start - 1);
|
|
2191
|
+
const rowEnd = this.seqPosToGlobalCol(row, end - 1);
|
|
2192
|
+
if (rowStart === undefined || rowEnd === undefined) {
|
|
2193
|
+
return undefined;
|
|
2194
|
+
}
|
|
2195
|
+
startGlobal = rowStart;
|
|
2196
|
+
endGlobal = rowEnd;
|
|
2197
|
+
}
|
|
2198
|
+
const startCol = this.globalColToVisibleCol(startGlobal) ??
|
|
2199
|
+
visibleColsBefore(blanks, startGlobal);
|
|
2200
|
+
const endCol = this.globalColToVisibleCol(endGlobal) ??
|
|
2201
|
+
visibleColsBefore(blanks, endGlobal) - 1;
|
|
2202
|
+
return startCol <= endCol ? { startCol, endCol } : undefined;
|
|
2203
|
+
},
|
|
1749
2204
|
/**
|
|
1750
2205
|
* #getter
|
|
1751
|
-
*
|
|
1752
|
-
*
|
|
1753
|
-
* longer agree the lookups have to refuse, and refusing invisibly is how
|
|
1754
|
-
* "there is no structure here" gets confused with "this data is stale".
|
|
2206
|
+
* why each ignored residue mapping is ignored, so a host can tell a
|
|
2207
|
+
* missing structure from a mapping made against a different alignment.
|
|
1755
2208
|
*/
|
|
1756
2209
|
get residueMappingProblems() {
|
|
1757
2210
|
const problems = [];
|
|
@@ -1798,10 +2251,8 @@ function stateModelFactory() {
|
|
|
1798
2251
|
},
|
|
1799
2252
|
/**
|
|
1800
2253
|
* #getter
|
|
1801
|
-
* the mappings that
|
|
1802
|
-
*
|
|
1803
|
-
* itself, since the rest of the mapping is still a claim about residues
|
|
1804
|
-
* that exist.
|
|
2254
|
+
* the mappings that fit the loaded alignment. A row-level problem drops
|
|
2255
|
+
* the whole mapping; a malformed segment drops only that segment.
|
|
1805
2256
|
*/
|
|
1806
2257
|
get usableResidueMappings() {
|
|
1807
2258
|
const unusable = new Set(this.residueMappingProblems
|
|
@@ -1816,9 +2267,8 @@ function stateModelFactory() {
|
|
|
1816
2267
|
},
|
|
1817
2268
|
/**
|
|
1818
2269
|
* #getter
|
|
1819
|
-
* the structures
|
|
1820
|
-
*
|
|
1821
|
-
* a host that means a particular one has to name it.
|
|
2270
|
+
* the structures with usable mappings. A row can map onto several, such
|
|
2271
|
+
* as an experimental entry and a predicted model.
|
|
1822
2272
|
*/
|
|
1823
2273
|
get mappedStructures() {
|
|
1824
2274
|
return this.usableResidueMappings.map(m => ({
|
|
@@ -1828,18 +2278,12 @@ function stateModelFactory() {
|
|
|
1828
2278
|
},
|
|
1829
2279
|
/**
|
|
1830
2280
|
* #method
|
|
1831
|
-
* The structure residue a row residue
|
|
1832
|
-
*
|
|
1833
|
-
*
|
|
1834
|
-
*
|
|
1835
|
-
* It also refuses when the answer is not unique. A row commonly maps onto
|
|
1836
|
-
* several structures -- an experimental entry and two predicted models --
|
|
1837
|
-
* and returning whichever came first would be the same class of wrong,
|
|
1838
|
-
* quieter. Name one with `structureId`, or use `mappedStructures` to see
|
|
1839
|
-
* what there is.
|
|
2281
|
+
* The structure residue for a row residue. Returns undefined when no
|
|
2282
|
+
* segment covers `seqPos`, or when the row maps onto several structures
|
|
2283
|
+
* and `structureId` does not pick one (see `mappedStructures`).
|
|
1840
2284
|
*
|
|
1841
|
-
* Positions are 1-based,
|
|
1842
|
-
*
|
|
2285
|
+
* Positions are 1-based, like `residueMappings` and `highlights`; the
|
|
2286
|
+
* column helpers above are 0-based.
|
|
1843
2287
|
*
|
|
1844
2288
|
* @param rowName - The alignment row
|
|
1845
2289
|
* @param seqPos - Residue of that row, 1-based
|
|
@@ -1869,10 +2313,10 @@ function stateModelFactory() {
|
|
|
1869
2313
|
},
|
|
1870
2314
|
/**
|
|
1871
2315
|
* #method
|
|
1872
|
-
* The row residue a structure residue
|
|
1873
|
-
*
|
|
1874
|
-
*
|
|
1875
|
-
*
|
|
2316
|
+
* The row residue for a structure residue; the inverse of
|
|
2317
|
+
* `structureResidue`, returning undefined in the same cases. `asymId`
|
|
2318
|
+
* picks a chain when several mappings share an entry id, as in a
|
|
2319
|
+
* homodimer.
|
|
1876
2320
|
*
|
|
1877
2321
|
* @param structureId - The structure's id, as the mapping names it
|
|
1878
2322
|
* @param position - Residue of that structure, 1-based label_seq_id
|
|
@@ -1901,16 +2345,31 @@ function stateModelFactory() {
|
|
|
1901
2345
|
.views(self => ({
|
|
1902
2346
|
/**
|
|
1903
2347
|
* #getter
|
|
1904
|
-
* the
|
|
1905
|
-
*
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
2348
|
+
* the branch-length scale bar over the tree, undefined in cladogram mode
|
|
2349
|
+
* or when the tree area is too narrow for one
|
|
2350
|
+
*/
|
|
2351
|
+
get treeScaleBar() {
|
|
2352
|
+
return scaleBarLength(self.pxPerBranchLength, self.treeAreaWidth - self.marginLeft * 2);
|
|
2353
|
+
},
|
|
2354
|
+
/**
|
|
2355
|
+
* #getter
|
|
2356
|
+
* the band across the top: the minimap over the alignment, the row panel
|
|
2357
|
+
* headers, and the tree overview stacked on the scale bar, as tall as the
|
|
2358
|
+
* tallest of the three
|
|
2359
|
+
*/
|
|
2360
|
+
get topBandHeight() {
|
|
2361
|
+
return Math.max(self.showHorizontalScrollbar ? self.minimapHeight : 0, self.rowPanelsHeaderHeight, self.treeOverviewHeight +
|
|
2362
|
+
(this.treeScaleBar ? treeScaleBarHeight : 0));
|
|
2363
|
+
},
|
|
2364
|
+
/**
|
|
2365
|
+
* #getter
|
|
2366
|
+
* the vertical space for alignment rows: the widget height less the
|
|
2367
|
+
* header, the top band and the tracks. Shared by blocksY, maxScrollY, the
|
|
2368
|
+
* vertical scrollbar and fitVertically.
|
|
1910
2369
|
*/
|
|
1911
2370
|
get msaAreaHeight() {
|
|
1912
2371
|
return (self.height -
|
|
1913
|
-
|
|
2372
|
+
this.topBandHeight -
|
|
1914
2373
|
self.headerHeight -
|
|
1915
2374
|
this.totalTrackAreaHeight);
|
|
1916
2375
|
},
|
|
@@ -1922,13 +2381,12 @@ function stateModelFactory() {
|
|
|
1922
2381
|
return sum(self.turnedOnTracks.map(r => r.model.height));
|
|
1923
2382
|
},
|
|
1924
2383
|
/**
|
|
1925
|
-
* one representative annotation per accession,
|
|
1926
|
-
*
|
|
2384
|
+
* one representative annotation per accession, for the legend, the
|
|
2385
|
+
* filter dialog and the palettes
|
|
1927
2386
|
*/
|
|
1928
2387
|
get annotationTypes() {
|
|
1929
|
-
// first occurrence wins
|
|
1930
|
-
//
|
|
1931
|
-
// them, and those agree across an accession's instances
|
|
2388
|
+
// first occurrence wins; only name, description and segment start are
|
|
2389
|
+
// read from it
|
|
1932
2390
|
const types = new Map();
|
|
1933
2391
|
for (const annot of self.annotations) {
|
|
1934
2392
|
if (!types.has(annot.accession)) {
|
|
@@ -1957,11 +2415,8 @@ function stateModelFactory() {
|
|
|
1957
2415
|
* #getter
|
|
1958
2416
|
*/
|
|
1959
2417
|
get dataInitialized() {
|
|
1960
|
-
// truthiness, not `!== ''`:
|
|
1961
|
-
//
|
|
1962
|
-
// that inlined a large alignment comes back `undefined` here -- which
|
|
1963
|
-
// `!== ''` reads as initialized and renders an empty view instead of
|
|
1964
|
-
// the import form
|
|
2418
|
+
// truthiness, not `!== ''`: DataModel drops an inline document over
|
|
2419
|
+
// 50kb from the snapshot, so a restored session can hold `undefined`
|
|
1965
2420
|
return !!(self.data.msa || self.data.tree) && !self.error;
|
|
1966
2421
|
},
|
|
1967
2422
|
/**
|
|
@@ -2008,12 +2463,20 @@ function stateModelFactory() {
|
|
|
2008
2463
|
},
|
|
2009
2464
|
/**
|
|
2010
2465
|
* #getter
|
|
2011
|
-
* most-negative allowed scrollY,
|
|
2012
|
-
* letting the whole alignment scroll off the top.
|
|
2466
|
+
* most-negative allowed scrollY, which keeps the last row in view
|
|
2013
2467
|
*/
|
|
2014
2468
|
get maxScrollY() {
|
|
2015
2469
|
return Math.min(-self.totalHeight + self.msaAreaHeight, 0);
|
|
2016
2470
|
},
|
|
2471
|
+
/**
|
|
2472
|
+
* #getter
|
|
2473
|
+
* axis a wheel zoom scales, for ctrl+wheel as much as for scroll-zoom.
|
|
2474
|
+
* With scroll-zoom off the toolbar shows no axis, so ctrl+wheel takes
|
|
2475
|
+
* both.
|
|
2476
|
+
*/
|
|
2477
|
+
get wheelZoomAxis() {
|
|
2478
|
+
return self.scrollZoom ? self.scrollZoomAxis : 'both';
|
|
2479
|
+
},
|
|
2017
2480
|
}))
|
|
2018
2481
|
.actions(self => ({
|
|
2019
2482
|
/**
|
|
@@ -2028,15 +2491,19 @@ function stateModelFactory() {
|
|
|
2028
2491
|
setScrollZoom(arg) {
|
|
2029
2492
|
self.scrollZoom = arg;
|
|
2030
2493
|
},
|
|
2494
|
+
/**
|
|
2495
|
+
* #action
|
|
2496
|
+
*/
|
|
2497
|
+
setScrollZoomAxis(arg) {
|
|
2498
|
+
self.scrollZoomAxis = arg;
|
|
2499
|
+
},
|
|
2031
2500
|
/**
|
|
2032
2501
|
* #action
|
|
2033
2502
|
* set hovered tree node and its descendants
|
|
2034
2503
|
*/
|
|
2035
2504
|
setHoveredTreeNode(nodeId) {
|
|
2036
|
-
//
|
|
2037
|
-
//
|
|
2038
|
-
// and a fresh object here invalidates hoveredRowIndices and redraws the
|
|
2039
|
-
// tree and MSA overlays. Re-hovering the same node is the common case
|
|
2505
|
+
// called on every tree mousemove; `find` walks the whole hierarchy and a
|
|
2506
|
+
// new object redraws the tree and MSA overlays
|
|
2040
2507
|
if (nodeId === self.hoveredTreeNode?.nodeId) {
|
|
2041
2508
|
return;
|
|
2042
2509
|
}
|
|
@@ -2055,15 +2522,12 @@ function stateModelFactory() {
|
|
|
2055
2522
|
/**
|
|
2056
2523
|
* #action
|
|
2057
2524
|
* Calculate a neighbor joining tree from the current MSA using BLOSUM62
|
|
2058
|
-
* distances.
|
|
2059
|
-
* cubic and runs on the main thread,
|
|
2060
|
-
* with no
|
|
2061
|
-
* for it anyway.
|
|
2525
|
+
* distances. Throws above `maxNeighborJoiningRows`: the join loop is
|
|
2526
|
+
* cubic and runs on the main thread, and 800 rows freeze the tab for ten
|
|
2527
|
+
* seconds with no cancel.
|
|
2062
2528
|
*/
|
|
2063
2529
|
calculateNeighborJoiningTreeFromMSA() {
|
|
2064
|
-
// every sequence
|
|
2065
|
-
// clade is a display state, and building the tree from what it leaves
|
|
2066
|
-
// showing drops the sequences it hides out of the result
|
|
2530
|
+
// every sequence, including rows in collapsed clades
|
|
2067
2531
|
const rows = [...self.rowMap];
|
|
2068
2532
|
if (rows.length < 2) {
|
|
2069
2533
|
throw new Error('Need at least 2 sequences to build a tree');
|
|
@@ -2075,10 +2539,9 @@ function stateModelFactory() {
|
|
|
2075
2539
|
},
|
|
2076
2540
|
/**
|
|
2077
2541
|
* #action
|
|
2078
|
-
* swap in a different tree over the same alignment.
|
|
2079
|
-
*
|
|
2080
|
-
*
|
|
2081
|
-
* happens to sit there -- the ids go with the tree they name.
|
|
2542
|
+
* swap in a different tree over the same alignment. Clears `collapsed`
|
|
2543
|
+
* and `showOnly`, since path-derived node ids (node-0-0-1) from the old
|
|
2544
|
+
* tree would match unrelated nodes in the new one.
|
|
2082
2545
|
*/
|
|
2083
2546
|
replaceTree(newick) {
|
|
2084
2547
|
transaction(() => {
|
|
@@ -2143,23 +2606,26 @@ function stateModelFactory() {
|
|
|
2143
2606
|
* #action
|
|
2144
2607
|
* Smoothly zoom by a continuous scaleFactor. The column under the cursor
|
|
2145
2608
|
* (offsetX/offsetY, px relative to the MSA area) stays anchored
|
|
2146
|
-
* horizontally. Vertically the anchor is biased toward
|
|
2147
|
-
* alignment nearly fits the viewport,
|
|
2148
|
-
*
|
|
2149
|
-
*
|
|
2150
|
-
*
|
|
2151
|
-
|
|
2152
|
-
|
|
2609
|
+
* horizontally. Vertically the anchor is biased toward y=0 when the
|
|
2610
|
+
* alignment nearly fits the viewport, fading to cursor-anchoring as the
|
|
2611
|
+
* alignment grows taller than the viewport.
|
|
2612
|
+
* Drives wheel/trackpad-pinch zoom. `axis` holds one cell dimension
|
|
2613
|
+
* fixed; the held axis still re-anchors its scroll offset, since the
|
|
2614
|
+
* other one can change how much of the alignment fits.
|
|
2615
|
+
*/
|
|
2616
|
+
zoomToPos(scaleFactor, offsetX, offsetY, axis = 'both') {
|
|
2153
2617
|
transaction(() => {
|
|
2154
2618
|
const colInView = (-self.scrollX + offsetX) / self.colWidth;
|
|
2155
2619
|
const rowInView = (-self.scrollY + offsetY) / self.rowHeight;
|
|
2156
|
-
|
|
2157
|
-
|
|
2620
|
+
if (axis !== 'vertical') {
|
|
2621
|
+
self.colWidth = clamp(self.colWidth * scaleFactor, minColWidth, maxCellSize);
|
|
2622
|
+
}
|
|
2623
|
+
if (axis !== 'horizontal') {
|
|
2624
|
+
self.rowHeight = clamp(self.rowHeight * scaleFactor, minRowHeight, maxCellSize);
|
|
2625
|
+
}
|
|
2158
2626
|
self.scrollX = clamp(offsetX - colInView * self.colWidth, self.maxScrollX, 0);
|
|
2159
2627
|
const anchoredScrollY = offsetY - rowInView * self.rowHeight;
|
|
2160
|
-
// maxScrollY is
|
|
2161
|
-
// overflows, so -maxScrollY is exactly that overflow past the
|
|
2162
|
-
// scrollable MSA viewport (0 when it fits)
|
|
2628
|
+
// -maxScrollY is the overflow past the viewport, 0 when it fits
|
|
2163
2629
|
const overflow = Math.max(0, -self.maxScrollY);
|
|
2164
2630
|
const visibleHeight = self.totalHeight - overflow;
|
|
2165
2631
|
const topBias = visibleHeight > 0 ? clamp(1 - overflow / visibleHeight, 0, 1) : 1;
|
|
@@ -2181,13 +2647,11 @@ function stateModelFactory() {
|
|
|
2181
2647
|
},
|
|
2182
2648
|
/**
|
|
2183
2649
|
* #action
|
|
2184
|
-
* Set the overlay annotations (an empty list clears them).
|
|
2185
|
-
*
|
|
2186
|
-
* InterProScan, GFF, user uploads, NCBI CDD.
|
|
2650
|
+
* Set the overlay annotations (an empty list clears them). InterProScan,
|
|
2651
|
+
* GFF, user uploads and NCBI CDD all arrive here as Annotation[].
|
|
2187
2652
|
*
|
|
2188
|
-
*
|
|
2189
|
-
*
|
|
2190
|
-
* shared with the overlay hidden reopened with it drawn.
|
|
2653
|
+
* Leaves `showDomains` alone, because a restored snapshot reloads its
|
|
2654
|
+
* GFF and must keep a hidden overlay hidden.
|
|
2191
2655
|
*/
|
|
2192
2656
|
setAnnotations(annotations) {
|
|
2193
2657
|
self.annotations = annotations;
|
|
@@ -2222,6 +2686,28 @@ function stateModelFactory() {
|
|
|
2222
2686
|
setColumnTracks(tracks) {
|
|
2223
2687
|
self.columnTracks.replace(tracks);
|
|
2224
2688
|
},
|
|
2689
|
+
/**
|
|
2690
|
+
* #action
|
|
2691
|
+
* replace the row table, which the model keeps as the JSON string
|
|
2692
|
+
* `data.treeMetadata` (see docs/layers.md)
|
|
2693
|
+
*/
|
|
2694
|
+
setRowData(rowData) {
|
|
2695
|
+
self.data.setTreeMetadata(JSON.stringify(rowData));
|
|
2696
|
+
},
|
|
2697
|
+
/**
|
|
2698
|
+
* #action
|
|
2699
|
+
* replace what the viewer's marks read from the row table
|
|
2700
|
+
*/
|
|
2701
|
+
setEncodings(encodings) {
|
|
2702
|
+
self.encodings.replace(encodings);
|
|
2703
|
+
},
|
|
2704
|
+
/**
|
|
2705
|
+
* #action
|
|
2706
|
+
* replace the panels drawn between the tree and the alignment
|
|
2707
|
+
*/
|
|
2708
|
+
setRowPanels(panels) {
|
|
2709
|
+
self.rowPanels.replace(panels);
|
|
2710
|
+
},
|
|
2225
2711
|
/**
|
|
2226
2712
|
* #action
|
|
2227
2713
|
* replace the alignment<->structure correspondence (see docs/layers.md)
|
|
@@ -2233,8 +2719,6 @@ function stateModelFactory() {
|
|
|
2233
2719
|
* #action
|
|
2234
2720
|
*/
|
|
2235
2721
|
toggleTrack(id) {
|
|
2236
|
-
// the stored value is "is off", so the current shown state is exactly
|
|
2237
|
-
// what the flipped entry should hold
|
|
2238
2722
|
const defaultOff = self.MSA?.tracks.find(t => t.id === id)?.defaultOff;
|
|
2239
2723
|
self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id, defaultOff));
|
|
2240
2724
|
},
|
|
@@ -2254,18 +2738,77 @@ function stateModelFactory() {
|
|
|
2254
2738
|
},
|
|
2255
2739
|
/**
|
|
2256
2740
|
* #getter
|
|
2257
|
-
* width of the alignment canvas
|
|
2258
|
-
* scrollbar
|
|
2259
|
-
*
|
|
2741
|
+
* width of the alignment canvas: the msa area less the vertical
|
|
2742
|
+
* scrollbar. showHorizontalScrollbar must not read it, since that feeds
|
|
2743
|
+
* msaAreaHeight -> showVerticalScrollbar and would form a cycle
|
|
2260
2744
|
*/
|
|
2261
2745
|
get msaCanvasWidth() {
|
|
2262
2746
|
return self.msaAreaWidth - this.verticalScrollbarWidth;
|
|
2263
2747
|
},
|
|
2748
|
+
/**
|
|
2749
|
+
* #method
|
|
2750
|
+
* the cell at a visible column and row index, in the coordinates a host
|
|
2751
|
+
* writes highlights in
|
|
2752
|
+
*/
|
|
2753
|
+
cellAt(visibleCol, rowIndex) {
|
|
2754
|
+
const column = self.visibleColToGlobalCol(visibleCol);
|
|
2755
|
+
const row = rowIndex === undefined ? undefined : self.leaves[rowIndex]?.data.name;
|
|
2756
|
+
return row === undefined
|
|
2757
|
+
? { column: column + 1 }
|
|
2758
|
+
: {
|
|
2759
|
+
column: column + 1,
|
|
2760
|
+
row,
|
|
2761
|
+
residue: self.visibleColToSeqPosOneBased(row, visibleCol),
|
|
2762
|
+
letter: self.rowMap.get(row)?.[column],
|
|
2763
|
+
};
|
|
2764
|
+
},
|
|
2765
|
+
/**
|
|
2766
|
+
* #getter
|
|
2767
|
+
* the cell under the pointer. Public API: MSAViewer's onCellHover
|
|
2768
|
+
* reports it.
|
|
2769
|
+
*/
|
|
2770
|
+
get hoveredCell() {
|
|
2771
|
+
const { mouseCol, mouseRow } = self;
|
|
2772
|
+
return mouseCol === undefined
|
|
2773
|
+
? undefined
|
|
2774
|
+
: this.cellAt(mouseCol, mouseRow);
|
|
2775
|
+
},
|
|
2776
|
+
/**
|
|
2777
|
+
* #getter
|
|
2778
|
+
* the cell a click pinned. Public API: MSAViewer's onCellClick reports it.
|
|
2779
|
+
*/
|
|
2780
|
+
get clickedCell() {
|
|
2781
|
+
const { mouseClickCol, mouseClickRow } = self;
|
|
2782
|
+
return mouseClickCol === undefined
|
|
2783
|
+
? undefined
|
|
2784
|
+
: this.cellAt(mouseClickCol, mouseClickRow);
|
|
2785
|
+
},
|
|
2786
|
+
/**
|
|
2787
|
+
* #getter
|
|
2788
|
+
* the columns on screen. Public API: MSAViewer's onViewportChange reports
|
|
2789
|
+
* it.
|
|
2790
|
+
*/
|
|
2791
|
+
get viewport() {
|
|
2792
|
+
const { scrollX, colWidth, numColumns, viewInitialized } = self;
|
|
2793
|
+
if (numColumns === 0 || !viewInitialized) {
|
|
2794
|
+
return undefined;
|
|
2795
|
+
}
|
|
2796
|
+
const { xStart, xEnd } = visibleColRange({
|
|
2797
|
+
offsetX: -scrollX,
|
|
2798
|
+
blockWidth: this.msaCanvasWidth,
|
|
2799
|
+
colWidth,
|
|
2800
|
+
});
|
|
2801
|
+
const last = Math.max(0, Math.min(xEnd, numColumns) - 1);
|
|
2802
|
+
return {
|
|
2803
|
+
startColumn: self.visibleColToGlobalCol(Math.min(xStart, last)) + 1,
|
|
2804
|
+
endColumn: self.visibleColToGlobalCol(last) + 1,
|
|
2805
|
+
};
|
|
2806
|
+
},
|
|
2264
2807
|
/**
|
|
2265
2808
|
* #getter
|
|
2266
2809
|
* ordinal segment types (exons etc.), ordered by sequence position so
|
|
2267
|
-
* exon-1..exon-14
|
|
2268
|
-
* labeled by number
|
|
2810
|
+
* exon-1..exon-14 run left-to-right; colored by alternating shade and
|
|
2811
|
+
* labeled by number, with no legend row
|
|
2269
2812
|
*/
|
|
2270
2813
|
get segmentDomainTypes() {
|
|
2271
2814
|
return [...self.annotationTypes.values()]
|
|
@@ -2288,11 +2831,34 @@ function stateModelFactory() {
|
|
|
2288
2831
|
const categorical = createPaletteMap(this.categoricalDomainTypes.map(d => d.accession));
|
|
2289
2832
|
return { ...segments, ...categorical };
|
|
2290
2833
|
},
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2834
|
+
/**
|
|
2835
|
+
* #getter
|
|
2836
|
+
* the encoding coloring the overlay's spans, undefined when none does,
|
|
2837
|
+
* which leaves each span the color its accession takes in `fillPalette`
|
|
2838
|
+
*/
|
|
2839
|
+
get featureFillEncoding() {
|
|
2840
|
+
return this.resolvedEncodings.find(e => e.channel === 'featureFill');
|
|
2841
|
+
},
|
|
2842
|
+
/**
|
|
2843
|
+
* #getter
|
|
2844
|
+
* the fill and outline of every feature's span: its own GFF `color=`
|
|
2845
|
+
* first, then the `featureFill` scale, then the accession palette.
|
|
2846
|
+
* Computed once per change of the features, the encodings or the palette
|
|
2847
|
+
*/
|
|
2848
|
+
get featureColors() {
|
|
2849
|
+
return featureColorMap(self.filteredAnnotations, this.featureFillEncoding, this.fillPalette);
|
|
2850
|
+
},
|
|
2851
|
+
/**
|
|
2852
|
+
* #getter
|
|
2853
|
+
* the text the `featureLabel` channel draws inside each span, undefined
|
|
2854
|
+
* when no encoding names the channel. A data channel, so it draws
|
|
2855
|
+
* whether or not the residue letters do
|
|
2856
|
+
*/
|
|
2857
|
+
get featureLabels() {
|
|
2858
|
+
const encoding = this.resolvedEncodings.find(e => e.channel === 'featureLabel');
|
|
2859
|
+
return encoding
|
|
2860
|
+
? featureLabelMap(self.filteredAnnotations, encoding.field)
|
|
2861
|
+
: undefined;
|
|
2296
2862
|
},
|
|
2297
2863
|
/**
|
|
2298
2864
|
* #getter
|
|
@@ -2308,24 +2874,92 @@ function stateModelFactory() {
|
|
|
2308
2874
|
/**
|
|
2309
2875
|
* #getter
|
|
2310
2876
|
* the domain types currently drawn on the alignment (filtered-on), shared
|
|
2311
|
-
* by the on-screen legend and the SVG export legend
|
|
2312
|
-
*
|
|
2313
|
-
*
|
|
2877
|
+
* by the on-screen legend and the SVG export legend: the categorical types
|
|
2878
|
+
* ordered by sequence position. Ordinal segments (exons) are numbered on
|
|
2879
|
+
* the band instead
|
|
2314
2880
|
*/
|
|
2315
2881
|
get visibleDomainTypes() {
|
|
2316
2882
|
return this.categoricalDomainTypes
|
|
2317
2883
|
.filter(d => !self.turnedOffFeatures.get(d.accession))
|
|
2318
2884
|
.toSorted((a, b) => a.start - b.start);
|
|
2319
2885
|
},
|
|
2886
|
+
/**
|
|
2887
|
+
* #getter
|
|
2888
|
+
* the categorical color keys drawn for this view, shared by the on-screen
|
|
2889
|
+
* legend overlay and the SVG export's reserved column. The domain overlay
|
|
2890
|
+
* produces the first, listing the `featureFill` scale where an encoding
|
|
2891
|
+
* names one. Every field a row-table encoding or a row panel reads
|
|
2892
|
+
* produces one more, so two channels over one field, or two strips over
|
|
2893
|
+
* it, list that field once. A strip's `legend` names the title it lists
|
|
2894
|
+
* under, so strips over eight fields with one set of colors list one
|
|
2895
|
+
* legend between them
|
|
2896
|
+
*/
|
|
2897
|
+
get legends() {
|
|
2898
|
+
const { featureFillEncoding, fillPalette, visibleDomainTypes } = this;
|
|
2899
|
+
const byKey = new Map();
|
|
2900
|
+
// the accession palette keys on no field, so it takes a key of its own
|
|
2901
|
+
const add = (field, entries, id) => {
|
|
2902
|
+
if (entries.length === 0) {
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
const key = field === undefined ? 'domains' : `field:${field}`;
|
|
2906
|
+
const legend = byKey.get(key);
|
|
2907
|
+
if (legend) {
|
|
2908
|
+
const seen = new Set(legend.entries.map(e => e.id));
|
|
2909
|
+
legend.entries.push(...entries.filter(e => !seen.has(e.id)));
|
|
2910
|
+
}
|
|
2911
|
+
else {
|
|
2912
|
+
byKey.set(key, {
|
|
2913
|
+
id: id ?? (field === undefined ? 'domains' : `rowData-${field}`),
|
|
2914
|
+
title: field ?? 'Domains',
|
|
2915
|
+
entries: [...entries],
|
|
2916
|
+
});
|
|
2917
|
+
}
|
|
2918
|
+
};
|
|
2919
|
+
// the overlay draws the domain key; with no columns it draws nothing,
|
|
2920
|
+
// and a features panel taking the same colors lists them instead
|
|
2921
|
+
if (self.actuallyShowDomains && self.numColumns > 0) {
|
|
2922
|
+
add(featureFillEncoding?.field, featureFillEncoding
|
|
2923
|
+
? featureFillEncoding.legend
|
|
2924
|
+
: visibleDomainTypes.map(d => ({
|
|
2925
|
+
id: d.accession,
|
|
2926
|
+
label: d.name,
|
|
2927
|
+
color: fillPalette[d.accession],
|
|
2928
|
+
})), 'domains');
|
|
2929
|
+
}
|
|
2930
|
+
for (const { channel, field, legend } of this.resolvedEncodings) {
|
|
2931
|
+
if (!featureChannels.has(channel)) {
|
|
2932
|
+
add(field, legend);
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
for (const { legendTitle, legend } of this.resolvedRowPanels) {
|
|
2936
|
+
add(legendTitle, legend);
|
|
2937
|
+
}
|
|
2938
|
+
return [...byKey.values()];
|
|
2939
|
+
},
|
|
2940
|
+
/**
|
|
2941
|
+
* #getter
|
|
2942
|
+
* whether the overlay marks each domain with a bar under its row instead
|
|
2943
|
+
* of filling the row behind the letters. Letter-color mode hands the
|
|
2944
|
+
* background to the color scheme, so a filled box would paint over it and
|
|
2945
|
+
* leave the setting with nothing to show. Sub-row layout already stacks
|
|
2946
|
+
* the boxes clear of the letters, and with the letters too small to draw
|
|
2947
|
+
* the filled box is the only thing left to read.
|
|
2948
|
+
*/
|
|
2949
|
+
get domainUnderline() {
|
|
2950
|
+
return (self.actuallyShowDomains &&
|
|
2951
|
+
!self.bgColor &&
|
|
2952
|
+
!self.subFeatureRows &&
|
|
2953
|
+
self.showMsaLetters);
|
|
2954
|
+
},
|
|
2320
2955
|
/**
|
|
2321
2956
|
* #getter
|
|
2322
2957
|
* every filtered-on annotation resolved to the visible column span it is
|
|
2323
2958
|
* drawn across, keyed by row name. Each row is ordered longest-first so a
|
|
2324
|
-
* short domain
|
|
2325
|
-
*
|
|
2326
|
-
* block
|
|
2327
|
-
*
|
|
2328
|
-
* needs to keep residues readable on top of the boxes.
|
|
2959
|
+
* nested short domain draws on top, and each band carries the lane the
|
|
2960
|
+
* sub-row layout puts it in. Resolved once here instead of per canvas
|
|
2961
|
+
* block per redraw; the letter renderer also reads the band colors to
|
|
2962
|
+
* pick legible letter colors.
|
|
2329
2963
|
*/
|
|
2330
2964
|
get domainBands() {
|
|
2331
2965
|
const { blanks } = self;
|
|
@@ -2334,14 +2968,11 @@ function stateModelFactory() {
|
|
|
2334
2968
|
const rowBands = annotations
|
|
2335
2969
|
.toSorted((a, b) => len(b) - len(a))
|
|
2336
2970
|
.map(annotation => {
|
|
2337
|
-
// annotation positions are 1-based and inclusive.
|
|
2338
|
-
//
|
|
2339
|
-
//
|
|
2340
|
-
//
|
|
2341
|
-
//
|
|
2342
|
-
// collapses onto the neighbouring boundary instead of dropping
|
|
2343
|
-
// the band. A band whose every column is hidden spans nothing
|
|
2344
|
-
// and is left out, as is one naming a row the alignment lacks.
|
|
2971
|
+
// annotation positions are 1-based and inclusive. endCol is
|
|
2972
|
+
// exclusive, one past the last residue's column, so the band does
|
|
2973
|
+
// not extend over a following gap run. An endpoint in a hidden
|
|
2974
|
+
// column moves to the neighboring boundary. Bands with no visible
|
|
2975
|
+
// columns, or naming a missing row, are dropped.
|
|
2345
2976
|
const start = self.seqPosToGlobalCol(name, annotation.start - 1);
|
|
2346
2977
|
const end = self.seqPosToGlobalCol(name, annotation.end - 1);
|
|
2347
2978
|
if (start === undefined || end === undefined) {
|
|
@@ -2354,11 +2985,10 @@ function stateModelFactory() {
|
|
|
2354
2985
|
: undefined;
|
|
2355
2986
|
})
|
|
2356
2987
|
.filter(notEmpty)
|
|
2357
|
-
// numbered after
|
|
2358
|
-
// not leave an empty sub-row behind it
|
|
2988
|
+
// numbered after filtering, so a dropped band leaves no gap
|
|
2359
2989
|
.map((band, stackIndex) => ({ ...band, stackIndex }));
|
|
2360
2990
|
if (rowBands.length > 0) {
|
|
2361
|
-
bands.set(name, rowBands);
|
|
2991
|
+
bands.set(name, packDomainLanes(rowBands));
|
|
2362
2992
|
}
|
|
2363
2993
|
}
|
|
2364
2994
|
return bands;
|
|
@@ -2388,9 +3018,6 @@ function stateModelFactory() {
|
|
|
2388
3018
|
const hits = (this.domainBands.get(name) ?? [])
|
|
2389
3019
|
.filter(b => mouseCol >= b.startCol && mouseCol < b.endCol)
|
|
2390
3020
|
.map(b => b.annotation);
|
|
2391
|
-
// the shared empty array, so moving the mouse across an alignment with
|
|
2392
|
-
// no annotations does not hand every canvas block a new value to
|
|
2393
|
-
// re-render on
|
|
2394
3021
|
return hits.length > 0 ? hits : noDomains;
|
|
2395
3022
|
},
|
|
2396
3023
|
/**
|
|
@@ -2407,8 +3034,7 @@ function stateModelFactory() {
|
|
|
2407
3034
|
* #getter
|
|
2408
3035
|
* row indices highlighted by the current tree hover (a hovered internal
|
|
2409
3036
|
* node highlights every tip below it). Shared by the tree and MSA overlay
|
|
2410
|
-
* canvases
|
|
2411
|
-
* name->index map rather than rebuilding a lookup on each mouse move.
|
|
3037
|
+
* canvases, via the memoized name->index map.
|
|
2412
3038
|
*/
|
|
2413
3039
|
get hoveredRowIndices() {
|
|
2414
3040
|
const { hoveredTreeNode, rowNamesSet } = self;
|
|
@@ -2421,8 +3047,8 @@ function stateModelFactory() {
|
|
|
2421
3047
|
/**
|
|
2422
3048
|
* #getter
|
|
2423
3049
|
* contiguous runs of `highlightedColumns`, so a run of highlighted columns
|
|
2424
|
-
* draws as one bordered band.
|
|
2425
|
-
*
|
|
3050
|
+
* draws as one bordered band. Memoized because the overlay canvas redraws
|
|
3051
|
+
* on every mouse move.
|
|
2426
3052
|
*/
|
|
2427
3053
|
get highlightedColumnRuns() {
|
|
2428
3054
|
const { highlightedColumns } = self;
|
|
@@ -2446,13 +3072,8 @@ function stateModelFactory() {
|
|
|
2446
3072
|
* is dropped. Row names that match no row are ignored.
|
|
2447
3073
|
*/
|
|
2448
3074
|
get resolvedHighlights() {
|
|
2449
|
-
const {
|
|
2450
|
-
|
|
2451
|
-
const visible = self.globalColToVisibleCol(globalCol);
|
|
2452
|
-
return visible ?? visibleColsBefore(blanks, globalCol);
|
|
2453
|
-
};
|
|
2454
|
-
// the document's own highlights first, then what each owner is showing
|
|
2455
|
-
// right now, so a hover draws over a persisted band rather than under it
|
|
3075
|
+
const { rowNamesSet, transientHighlights } = self;
|
|
3076
|
+
// persisted highlights first, so transient ones draw on top
|
|
2456
3077
|
const all = [
|
|
2457
3078
|
...self.highlights,
|
|
2458
3079
|
...Object.values(transientHighlights).flat(),
|
|
@@ -2468,70 +3089,221 @@ function stateModelFactory() {
|
|
|
2468
3089
|
if (start === undefined || end === undefined) {
|
|
2469
3090
|
return [];
|
|
2470
3091
|
}
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
3092
|
+
const span = self.visibleSpan({ row, start, end });
|
|
3093
|
+
return span ? [{ ...base, ...span, rowIndices: [] }] : [];
|
|
3094
|
+
});
|
|
3095
|
+
},
|
|
3096
|
+
/**
|
|
3097
|
+
* #method
|
|
3098
|
+
* per-column summary statistics: consensus residue and its identity
|
|
3099
|
+
* fraction, both conservation scores, gap fraction, and the sorted non-gap
|
|
3100
|
+
* residue distribution. undefined past the end of the alignment or for an
|
|
3101
|
+
* all-gap column.
|
|
3102
|
+
*/
|
|
3103
|
+
columnStatsAt(col) {
|
|
3104
|
+
const { colStats, conservation, propertyConservation } = self;
|
|
3105
|
+
return columnStats({
|
|
3106
|
+
col,
|
|
3107
|
+
colStats,
|
|
3108
|
+
conservation,
|
|
3109
|
+
propertyConservation,
|
|
3110
|
+
});
|
|
3111
|
+
},
|
|
3112
|
+
/**
|
|
3113
|
+
* #method
|
|
3114
|
+
*/
|
|
3115
|
+
getRowData(name) {
|
|
3116
|
+
return {
|
|
3117
|
+
data: self.MSA?.getRowData(name),
|
|
3118
|
+
rowData: self.rowDataOf(name),
|
|
3119
|
+
};
|
|
3120
|
+
},
|
|
3121
|
+
/**
|
|
3122
|
+
* #getter
|
|
3123
|
+
* each encoding with its scale resolved against the values its field
|
|
3124
|
+
* takes: a feature channel reads them across the features drawn, every
|
|
3125
|
+
* other channel across the row table. Resolved once per change of that
|
|
3126
|
+
* table or the encodings, never per row per frame.
|
|
3127
|
+
*/
|
|
3128
|
+
get resolvedEncodings() {
|
|
3129
|
+
const rows = Object.values(self.rowData);
|
|
3130
|
+
const features = self.filteredAnnotations;
|
|
3131
|
+
return self.encodings.map(encoding => ({
|
|
3132
|
+
...encoding,
|
|
3133
|
+
...resolveScale(encoding.scale, featureChannels.has(encoding.channel)
|
|
3134
|
+
? features
|
|
3135
|
+
.map(a => featureField(a, encoding.field))
|
|
3136
|
+
.filter(notEmpty)
|
|
3137
|
+
: rows.map(row => row?.[encoding.field]).filter(notEmpty)),
|
|
3138
|
+
}));
|
|
3139
|
+
},
|
|
3140
|
+
/**
|
|
3141
|
+
* #getter
|
|
3142
|
+
* each row panel with its scale resolved against the values its field
|
|
3143
|
+
* takes across the row table, giving the color per row name, the pixel
|
|
3144
|
+
* column it draws in, and the entries its legend lists. Resolved once
|
|
3145
|
+
* per change of that table or the panels, never per block per frame.
|
|
3146
|
+
*/
|
|
3147
|
+
get resolvedRowPanels() {
|
|
3148
|
+
const rows = Object.entries(self.rowData);
|
|
3149
|
+
const { fillPalette, featureFillEncoding, featureLabels } = this;
|
|
3150
|
+
const annotations = self.filteredAnnotations;
|
|
3151
|
+
let offsetX = 0;
|
|
3152
|
+
return self.rowPanels.map((panel, index) => {
|
|
3153
|
+
const width = rowPanelWidth(panel, self.rowHeight);
|
|
3154
|
+
const base = { id: `rowpanel-${index}`, width, offsetX };
|
|
3155
|
+
offsetX += width;
|
|
3156
|
+
if (panel.kind === 'features') {
|
|
3157
|
+
const color = panel.encoding?.color;
|
|
3158
|
+
const encoding = color
|
|
3159
|
+
? resolveFeatureScale(color.field, color.scale, annotations)
|
|
3160
|
+
: featureFillEncoding;
|
|
3161
|
+
const label = panel.encoding?.label;
|
|
3162
|
+
const align = panel.transform?.find(t => t.type === 'align');
|
|
3163
|
+
return {
|
|
3164
|
+
...base,
|
|
3165
|
+
kind: panel.kind,
|
|
3166
|
+
x: panel.x,
|
|
3167
|
+
header: panel.header ?? '',
|
|
3168
|
+
field: encoding?.field,
|
|
3169
|
+
legendTitle: encoding?.field,
|
|
3170
|
+
spans: featurePanelSpans({
|
|
3171
|
+
panel,
|
|
3172
|
+
width,
|
|
3173
|
+
colWidth: self.colWidth,
|
|
3174
|
+
domainBands: this.domainBands,
|
|
3175
|
+
annotationsByRow: self.annotationsByRow,
|
|
3176
|
+
shifts: align
|
|
3177
|
+
? this.featureAlignShifts.get(align.on)
|
|
3178
|
+
: undefined,
|
|
3179
|
+
}),
|
|
3180
|
+
colors: featureColorMap(annotations, encoding, fillPalette),
|
|
3181
|
+
labels: label
|
|
3182
|
+
? featureLabelMap(annotations, label)
|
|
3183
|
+
: featureLabels,
|
|
3184
|
+
legend: encoding?.legend ??
|
|
3185
|
+
this.visibleDomainTypes.map(d => ({
|
|
3186
|
+
id: d.accession,
|
|
3187
|
+
label: d.name,
|
|
3188
|
+
color: fillPalette[d.accession],
|
|
3189
|
+
})),
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
const { colorOf, legend } = resolveScale(panel.scale, rows.map(([, row]) => row?.[panel.field]).filter(notEmpty));
|
|
3193
|
+
const colors = new Map();
|
|
3194
|
+
for (const [name, row] of rows) {
|
|
3195
|
+
const value = row?.[panel.field];
|
|
3196
|
+
const color = value === undefined ? undefined : colorOf(value);
|
|
3197
|
+
if (color) {
|
|
3198
|
+
colors.set(name, color);
|
|
2478
3199
|
}
|
|
2479
|
-
startGlobal = rowStart;
|
|
2480
|
-
endGlobal = rowEnd;
|
|
2481
3200
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
:
|
|
3201
|
+
return {
|
|
3202
|
+
...base,
|
|
3203
|
+
kind: panel.kind,
|
|
3204
|
+
field: panel.field,
|
|
3205
|
+
header: panel.header ?? panel.field,
|
|
3206
|
+
legendTitle: panel.legend ?? panel.field,
|
|
3207
|
+
colors,
|
|
3208
|
+
legend,
|
|
3209
|
+
};
|
|
2488
3210
|
});
|
|
2489
3211
|
},
|
|
2490
3212
|
/**
|
|
2491
3213
|
* #getter
|
|
2492
|
-
*
|
|
2493
|
-
*
|
|
2494
|
-
*
|
|
3214
|
+
* the shift an `align` transform gives each row, keyed by the feature
|
|
3215
|
+
* name it aligns on: the offset putting the first feature of that name
|
|
3216
|
+
* at zero. A row carrying no such feature is absent, and keeps its own
|
|
3217
|
+
* origin.
|
|
3218
|
+
*/
|
|
3219
|
+
get featureAlignShifts() {
|
|
3220
|
+
const names = new Set(self.rowPanels.flatMap(panel => panel.kind === 'features' && panel.x === 'position'
|
|
3221
|
+
? (panel.transform ?? [])
|
|
3222
|
+
.filter(t => t.type === 'align')
|
|
3223
|
+
.map(t => t.on)
|
|
3224
|
+
: []));
|
|
3225
|
+
return new Map([...names].map(on => [
|
|
3226
|
+
on,
|
|
3227
|
+
new Map(Object.entries(self.annotationsByRow)
|
|
3228
|
+
.map(([name, annotations]) => {
|
|
3229
|
+
const hit = annotations.find(a => featureName(a) === on);
|
|
3230
|
+
return hit ? [name, 1 - hit.start] : undefined;
|
|
3231
|
+
})
|
|
3232
|
+
.filter(notEmpty)),
|
|
3233
|
+
]));
|
|
3234
|
+
},
|
|
3235
|
+
/**
|
|
3236
|
+
* #getter
|
|
3237
|
+
* the color the `tipLabel` channel gives each row, by row name. Undefined
|
|
3238
|
+
* when no encoding names the channel, which leaves the labels the theme's
|
|
3239
|
+
* text color.
|
|
2495
3240
|
*/
|
|
2496
|
-
get
|
|
2497
|
-
const
|
|
2498
|
-
if (
|
|
3241
|
+
get tipLabelColors() {
|
|
3242
|
+
const encoding = this.resolvedEncodings.find(e => e.channel === 'tipLabel');
|
|
3243
|
+
if (!encoding) {
|
|
2499
3244
|
return undefined;
|
|
2500
3245
|
}
|
|
2501
|
-
const
|
|
2502
|
-
|
|
2503
|
-
|
|
3246
|
+
const colors = new Map();
|
|
3247
|
+
for (const [name, row] of Object.entries(self.rowData)) {
|
|
3248
|
+
const value = row?.[encoding.field];
|
|
3249
|
+
const color = value === undefined ? undefined : encoding.colorOf(value);
|
|
3250
|
+
if (color) {
|
|
3251
|
+
colors.set(name, color);
|
|
3252
|
+
}
|
|
2504
3253
|
}
|
|
2505
|
-
|
|
2506
|
-
|
|
3254
|
+
return colors;
|
|
3255
|
+
},
|
|
3256
|
+
/**
|
|
3257
|
+
* #getter
|
|
3258
|
+
* the wash the `rowTint` channel draws over each row, indexed by row, or
|
|
3259
|
+
* undefined when no encoding names the channel. The overlay draws these,
|
|
3260
|
+
* so a tint stays out of the raster tile cache and its keys.
|
|
3261
|
+
*/
|
|
3262
|
+
get rowTints() {
|
|
3263
|
+
const encoding = this.resolvedEncodings.find(e => e.channel === 'rowTint');
|
|
3264
|
+
if (!encoding) {
|
|
2507
3265
|
return undefined;
|
|
2508
3266
|
}
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
.
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
gaps,
|
|
2518
|
-
gapFraction: gaps / total,
|
|
2519
|
-
conservation: self.conservation[mouseCol] ?? 0,
|
|
2520
|
-
propertyConservation: self.propertyConservation[mouseCol],
|
|
2521
|
-
consensusLetter: consensus?.[0] ?? '',
|
|
2522
|
-
consensusCount: consensus?.[1] ?? 0,
|
|
2523
|
-
consensusFraction: consensus ? consensus[1] / total : 0,
|
|
2524
|
-
distribution,
|
|
2525
|
-
};
|
|
3267
|
+
return self.rowNames.map(name => {
|
|
3268
|
+
const value = self.rowDataOf(name)?.[encoding.field];
|
|
3269
|
+
const color = value === undefined ? undefined : encoding.colorOf(value);
|
|
3270
|
+
if (!color) {
|
|
3271
|
+
return undefined;
|
|
3272
|
+
}
|
|
3273
|
+
return withAlpha(color, rowTintAlpha);
|
|
3274
|
+
});
|
|
2526
3275
|
},
|
|
2527
3276
|
/**
|
|
2528
|
-
* #
|
|
3277
|
+
* #getter
|
|
3278
|
+
* the color the `branch` channel gives each tree edge, by the node id at
|
|
3279
|
+
* the edge's far end, or undefined when no encoding names the channel. A
|
|
3280
|
+
* node takes the field value its tips agree on, so a clade of one value
|
|
3281
|
+
* colors down from where it splits off, and a node whose tips disagree or
|
|
3282
|
+
* whose value has no color is absent and draws in the default color.
|
|
3283
|
+
*
|
|
3284
|
+
* The pass runs over the whole tree, never `root`, so a collapsed or
|
|
3285
|
+
* focused clade keeps the color the full tree gives it.
|
|
2529
3286
|
*/
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
}
|
|
3287
|
+
get branchColors() {
|
|
3288
|
+
const encoding = this.resolvedEncodings.find(e => e.channel === 'branch');
|
|
3289
|
+
if (!encoding) {
|
|
3290
|
+
return undefined;
|
|
3291
|
+
}
|
|
3292
|
+
const order = preorder(self.tree);
|
|
3293
|
+
const values = new Map();
|
|
3294
|
+
const colors = new Map();
|
|
3295
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
3296
|
+
const node = order[i];
|
|
3297
|
+
const value = node.children.length > 0
|
|
3298
|
+
? sharedValue(node.children.map(child => values.get(child)))
|
|
3299
|
+
: self.rowDataOf(node.name)?.[encoding.field];
|
|
3300
|
+
values.set(node, value);
|
|
3301
|
+
const color = value === undefined ? undefined : encoding.colorOf(value);
|
|
3302
|
+
if (color) {
|
|
3303
|
+
colors.set(node.id, color);
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
return colors;
|
|
2535
3307
|
},
|
|
2536
3308
|
}))
|
|
2537
3309
|
.actions(self => ({
|
|
@@ -2544,33 +3316,33 @@ function stateModelFactory() {
|
|
|
2544
3316
|
/**
|
|
2545
3317
|
* #action
|
|
2546
3318
|
*/
|
|
2547
|
-
|
|
2548
|
-
self.
|
|
3319
|
+
setHideHeader(arg) {
|
|
3320
|
+
self.hideHeader = arg;
|
|
2549
3321
|
},
|
|
2550
3322
|
/**
|
|
2551
3323
|
* #action
|
|
3324
|
+
* focus the subtree a click `y` pixels down the tree overview lands on.
|
|
3325
|
+
* A click inside the box already drawn there clears the focus, the way
|
|
3326
|
+
* clicking the focused branch again does.
|
|
2552
3327
|
*/
|
|
2553
|
-
|
|
2554
|
-
|
|
3328
|
+
treeOverviewClick(y) {
|
|
3329
|
+
const hit = self.treeOverviewHit(y);
|
|
3330
|
+
const focus = self.treeOverviewFocusRows;
|
|
3331
|
+
const inside = !!focus && !!hit && hit.rows[0] >= focus[0] && hit.rows[1] <= focus[1];
|
|
3332
|
+
self.setShowOnly(inside || !hit ? undefined : hit.id);
|
|
2555
3333
|
},
|
|
2556
3334
|
/**
|
|
2557
3335
|
* #action
|
|
3336
|
+
* resize every track sharing a `heightKey`; see `trackHeights`
|
|
2558
3337
|
*/
|
|
2559
|
-
|
|
2560
|
-
self.
|
|
3338
|
+
setTrackHeight(heightKey, height) {
|
|
3339
|
+
self.trackHeights.set(heightKey, height);
|
|
2561
3340
|
},
|
|
2562
3341
|
/**
|
|
2563
3342
|
* #action
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
},
|
|
2568
|
-
/**
|
|
2569
|
-
* #action
|
|
2570
|
-
* Return to the import form: every property off `preservedOnReset`
|
|
2571
|
-
* (data, filehandles, collapsed/showOnly, zoom, scroll, ...) goes back
|
|
2572
|
-
* to its default, then the file-derived volatiles applySnapshot cannot
|
|
2573
|
-
* reach are cleared by hand.
|
|
3343
|
+
* Return to the import form: reset every property not in
|
|
3344
|
+
* `preservedOnReset` to its default, then clear the file-derived
|
|
3345
|
+
* volatiles applySnapshot does not touch.
|
|
2574
3346
|
*/
|
|
2575
3347
|
reset() {
|
|
2576
3348
|
self.resetCount++;
|
|
@@ -2596,8 +3368,8 @@ function stateModelFactory() {
|
|
|
2596
3368
|
},
|
|
2597
3369
|
/**
|
|
2598
3370
|
* #action
|
|
2599
|
-
*
|
|
2600
|
-
*
|
|
3371
|
+
* show or hide an annotation type. Only hidden types are recorded; see
|
|
3372
|
+
* `turnedOffFeatures`
|
|
2601
3373
|
*/
|
|
2602
3374
|
setFilter(accession, shown) {
|
|
2603
3375
|
if (shown) {
|
|
@@ -2611,12 +3383,8 @@ function stateModelFactory() {
|
|
|
2611
3383
|
* #action
|
|
2612
3384
|
*/
|
|
2613
3385
|
fit() {
|
|
2614
|
-
//
|
|
2615
|
-
//
|
|
2616
|
-
// minimap is taking, and fitting the columns while the rows still
|
|
2617
|
-
// overflow measures against a width the vertical scrollbar is taking.
|
|
2618
|
-
// A second pass measures against the geometry the first pass produced,
|
|
2619
|
-
// which is the one the reader ends up looking at.
|
|
3386
|
+
// two passes: each direction's viewport depends on whether the other
|
|
3387
|
+
// overflows (the minimap takes height, the vertical scrollbar width)
|
|
2620
3388
|
transaction(() => {
|
|
2621
3389
|
for (let pass = 0; pass < 2; pass++) {
|
|
2622
3390
|
this.fitHorizontally();
|
|
@@ -2633,29 +3401,67 @@ function stateModelFactory() {
|
|
|
2633
3401
|
}
|
|
2634
3402
|
self.scrollY = 0;
|
|
2635
3403
|
},
|
|
3404
|
+
/**
|
|
3405
|
+
* #action
|
|
3406
|
+
* zoom and scroll so a span fills the alignment's width, in highlight
|
|
3407
|
+
* coordinates (see visibleSpan). Does nothing before the viewer knows
|
|
3408
|
+
* its width, or for a span that resolves to no visible column.
|
|
3409
|
+
*/
|
|
3410
|
+
zoomToRegion(region) {
|
|
3411
|
+
const span = self.visibleSpan(region);
|
|
3412
|
+
if (!span || !self.viewInitialized) {
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
transaction(() => {
|
|
3416
|
+
self.colWidth = clamp(self.msaCanvasWidth / (span.endCol - span.startCol + 1), minColWidth, maxCellSize);
|
|
3417
|
+
self.scrollX = clamp(-span.startCol * self.colWidth, self.maxScrollX, 0);
|
|
3418
|
+
});
|
|
3419
|
+
},
|
|
2636
3420
|
/**
|
|
2637
3421
|
* #action
|
|
2638
3422
|
*/
|
|
2639
3423
|
fitHorizontally() {
|
|
2640
3424
|
if (self.numColumns > 0) {
|
|
2641
|
-
//
|
|
2642
|
-
// the right edge -- and short of the width that shows a minimap, so
|
|
2643
|
-
// nothing on screen said they were there
|
|
3425
|
+
// msaCanvasWidth excludes the vertical scrollbar's 20px
|
|
2644
3426
|
self.colWidth = clamp(self.msaCanvasWidth / self.numColumns, minColWidth, maxCellSize);
|
|
2645
3427
|
}
|
|
2646
3428
|
self.scrollX = 0;
|
|
2647
3429
|
},
|
|
2648
3430
|
afterCreate() {
|
|
2649
|
-
// seed the highlighted-columns overlay from the declarative property so
|
|
2650
|
-
// a shared snapshot/URL opens with those columns highlighted (the
|
|
2651
|
-
// volatile highlightedColumns can later be driven by genome-hover sync)
|
|
2652
3431
|
if (self.highlightColumns?.length) {
|
|
2653
3432
|
self.setHighlightedColumns(self.highlightColumns);
|
|
2654
3433
|
}
|
|
2655
|
-
//
|
|
2656
|
-
//
|
|
2657
|
-
//
|
|
2658
|
-
//
|
|
3434
|
+
// The `collapse` and `focus` clade marks seed the collapsed list and
|
|
3435
|
+
// the subtree in focus, which the tree, `hideGapsEffective` and the
|
|
3436
|
+
// alignment all read. The tree arrives with the model for inline data
|
|
3437
|
+
// and later for a filehandle, so the seeding waits for it and then runs
|
|
3438
|
+
// once: expanding a seeded clade sticks, and the record collapses it
|
|
3439
|
+
// again only on reload. `dataInitialized` is true once the MSA alone
|
|
3440
|
+
// has loaded, when the tree is still the flat stub, so a tree
|
|
3441
|
+
// filehandle holds the seeding until its text lands.
|
|
3442
|
+
let cladesSeeded = false;
|
|
3443
|
+
addDisposer(self, autorun(() => {
|
|
3444
|
+
if (cladesSeeded ||
|
|
3445
|
+
!self.dataInitialized ||
|
|
3446
|
+
(self.treeFilehandle && !self.data.tree) ||
|
|
3447
|
+
self.clades.length === 0) {
|
|
3448
|
+
return;
|
|
3449
|
+
}
|
|
3450
|
+
cladesSeeded = true;
|
|
3451
|
+
for (const { mark, nodeId } of self.resolvedClades) {
|
|
3452
|
+
if (nodeId === undefined) {
|
|
3453
|
+
continue;
|
|
3454
|
+
}
|
|
3455
|
+
if (mark === 'collapse' && !self.collapsed.includes(nodeId)) {
|
|
3456
|
+
self.toggleCollapsed(nodeId);
|
|
3457
|
+
}
|
|
3458
|
+
else if (mark === 'focus') {
|
|
3459
|
+
self.setShowOnly(nodeId);
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
}));
|
|
3463
|
+
// the matchMedia query is pinned to the current device pixel ratio, so
|
|
3464
|
+
// each change re-registers against the new one
|
|
2659
3465
|
if (typeof window !== 'undefined' &&
|
|
2660
3466
|
typeof window.matchMedia === 'function') {
|
|
2661
3467
|
const query = () => window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
@@ -2675,25 +3481,17 @@ function stateModelFactory() {
|
|
|
2675
3481
|
* Fetch a filehandle whenever it changes, and hand the text to
|
|
2676
3482
|
* `onLoad`.
|
|
2677
3483
|
*
|
|
2678
|
-
*
|
|
2679
|
-
*
|
|
2680
|
-
*
|
|
2681
|
-
* loading flag belonging to the newer one. A superseded or cleared
|
|
2682
|
-
* request is also aborted, rather than left downloading a file nothing
|
|
2683
|
-
* is waiting for, and it gives back the status line it was writing --
|
|
2684
|
-
* a reset() mid-download used to leave "Downloading file" and a Cancel
|
|
2685
|
-
* button behind on the import form.
|
|
3484
|
+
* A generation guard keeps a slower superseded request from overwriting
|
|
3485
|
+
* the newer one's data, status or loading flag. A superseded or cleared
|
|
3486
|
+
* request is aborted and clears its status line.
|
|
2686
3487
|
*
|
|
2687
|
-
* `clearFilehandle`
|
|
2688
|
-
*
|
|
2689
|
-
*
|
|
2690
|
-
* it too, returning the view to the import form rather than leaving a
|
|
2691
|
-
* stuck spinner.
|
|
3488
|
+
* `clearFilehandle` runs after a local file loads, since a blob has no
|
|
3489
|
+
* URL to refetch, and when the user cancels, which returns the view to
|
|
3490
|
+
* the import form.
|
|
2692
3491
|
*
|
|
2693
|
-
* `what` names the layer in a failure message. An optional layer
|
|
2694
|
-
* (annotations, row metadata) that fails
|
|
2695
|
-
*
|
|
2696
|
-
* error screen over a decorative file.
|
|
3492
|
+
* `what` names the layer in a failure message. An `optional` layer
|
|
3493
|
+
* (annotations, row metadata) that fails adds a warning instead of an
|
|
3494
|
+
* error.
|
|
2697
3495
|
*/
|
|
2698
3496
|
const loadOnFilehandleChange = ({ what, getFilehandle, onLoad, setLoading, clearFilehandle, optional = false, }) => {
|
|
2699
3497
|
let generation = 0;
|
|
@@ -2772,8 +3570,7 @@ function stateModelFactory() {
|
|
|
2772
3570
|
self.setTreeFilehandle(undefined);
|
|
2773
3571
|
},
|
|
2774
3572
|
});
|
|
2775
|
-
// treeMetadata
|
|
2776
|
-
// it keeps no loading flag and nothing to return to on cancel
|
|
3573
|
+
// treeMetadata has no import-form step, so no loading flag or cancel
|
|
2777
3574
|
loadOnFilehandleChange({
|
|
2778
3575
|
what: 'row metadata',
|
|
2779
3576
|
optional: true,
|
|
@@ -2782,11 +3579,8 @@ function stateModelFactory() {
|
|
|
2782
3579
|
self.setTreeMetadata(text);
|
|
2783
3580
|
},
|
|
2784
3581
|
});
|
|
2785
|
-
//
|
|
2786
|
-
//
|
|
2787
|
-
// gff used to leave the previous file's annotations drawn over it --
|
|
2788
|
-
// while annotations a host set directly are left alone, which is why
|
|
2789
|
-
// this tracks what it applied instead of reading the current list
|
|
3582
|
+
// parses data.gff into annotations. Clearing the text clears only the
|
|
3583
|
+
// annotations this autorun applied, not ones a host set directly
|
|
2790
3584
|
let appliedGFF = false;
|
|
2791
3585
|
addDisposer(self, autorun(() => {
|
|
2792
3586
|
const gffText = self.data.gff;
|
|
@@ -2796,8 +3590,6 @@ function stateModelFactory() {
|
|
|
2796
3590
|
appliedGFF = true;
|
|
2797
3591
|
}
|
|
2798
3592
|
catch (e) {
|
|
2799
|
-
// a malformed overlay is not worth replacing the alignment
|
|
2800
|
-
// with an error screen
|
|
2801
3593
|
console.error(e);
|
|
2802
3594
|
self.addWarning(`The annotations did not parse: ${e}`);
|
|
2803
3595
|
}
|
|
@@ -2807,9 +3599,17 @@ function stateModelFactory() {
|
|
|
2807
3599
|
self.setAnnotations([]);
|
|
2808
3600
|
}
|
|
2809
3601
|
}));
|
|
2810
|
-
//
|
|
2811
|
-
//
|
|
2812
|
-
|
|
3602
|
+
// a GFF whose first column names rows of some other alignment parses
|
|
3603
|
+
// without error and draws nothing
|
|
3604
|
+
addDisposer(self, autorun(() => {
|
|
3605
|
+
const { annotations, rowNamesSet } = self;
|
|
3606
|
+
if (annotations.length > 0 &&
|
|
3607
|
+
rowNamesSet.size > 0 &&
|
|
3608
|
+
!annotations.some(a => rowNamesSet.has(a.id))) {
|
|
3609
|
+
self.addWarning(`0 of ${annotations.length} annotations name a row in this alignment`);
|
|
3610
|
+
}
|
|
3611
|
+
}));
|
|
3612
|
+
// gffFilehandle loads into data.gff, which the autorun above parses
|
|
2813
3613
|
loadOnFilehandleChange({
|
|
2814
3614
|
what: 'annotations',
|
|
2815
3615
|
optional: true,
|
|
@@ -2834,19 +3634,12 @@ function stateModelFactory() {
|
|
|
2834
3634
|
self.setMSAFilehandle(undefined);
|
|
2835
3635
|
},
|
|
2836
3636
|
});
|
|
2837
|
-
// Keep
|
|
2838
|
-
// self.
|
|
2839
|
-
//
|
|
2840
|
-
//
|
|
3637
|
+
// Keep computeds alive that are read outside reactions, so they are not
|
|
3638
|
+
// recomputed on every access: self.columns (and through it the
|
|
3639
|
+
// parseMSA result), and the column statistics that dynamic color
|
|
3640
|
+
// schemes and the mousemove-driven hover tooltip read.
|
|
2841
3641
|
// xref solution https://github.com/mobxjs/mobx/issues/266#issuecomment-222007278
|
|
2842
3642
|
// xref problem https://github.com/GMOD/react-msaview/issues/75
|
|
2843
|
-
//
|
|
2844
|
-
// The column statistics are held for the same reason whenever something
|
|
2845
|
-
// reads them off the reactive path: dynamic color schemes, and the hover
|
|
2846
|
-
// tooltip, which is read from a mousemove handler. Without this the
|
|
2847
|
-
// tooltip's cost depends on whether some visible track happens to be
|
|
2848
|
-
// observing them, so closing the conservation track would silently turn
|
|
2849
|
-
// every mouse move into a full-alignment recount.
|
|
2850
3643
|
addDisposer(self, autorun(() => {
|
|
2851
3644
|
if (self.colorSchemeName.includes('dynamic')) {
|
|
2852
3645
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
@@ -2864,25 +3657,20 @@ function stateModelFactory() {
|
|
|
2864
3657
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
2865
3658
|
self.columns;
|
|
2866
3659
|
}));
|
|
2867
|
-
//
|
|
2868
|
-
//
|
|
2869
|
-
//
|
|
2870
|
-
// below (treeAreaWidth here depends only on labelsWidth, not treeWidth).
|
|
3660
|
+
// with autoTreeAreaWidth and no tree drawn, size the tree area to the
|
|
3661
|
+
// labels. Gated on noTree/!drawTree so it does not loop with the
|
|
3662
|
+
// treeWidth autorun below
|
|
2871
3663
|
addDisposer(self, autorun(() => {
|
|
2872
3664
|
if (self.autoTreeAreaWidth &&
|
|
2873
3665
|
(self.noTree || !self.drawTree) &&
|
|
2874
3666
|
self.labelsWidth) {
|
|
2875
|
-
self.setTreeAreaWidth(self.labelsWidth + self.marginLeft + 12);
|
|
3667
|
+
self.setTreeAreaWidth(self.labelsWidth + self.marginLeft + 12 + self.cladeGutterWidth);
|
|
2876
3668
|
}
|
|
2877
3669
|
}));
|
|
2878
|
-
// treeWidth
|
|
2879
|
-
//
|
|
2880
|
-
//
|
|
2881
|
-
//
|
|
2882
|
-
// in a 200px area -- had its value overwritten on the first frame, and
|
|
2883
|
-
// a restored session came back re-derived rather than as it was left.
|
|
2884
|
-
// A getter cannot do this: labelsWidth is measured off the leaves,
|
|
2885
|
-
// which are laid out against treeWidth.
|
|
3670
|
+
// treeWidth follows the tree area less the labels. A non-default
|
|
3671
|
+
// snapshot treeWidth (jbrowse-plugin-msaview opens at 100 in a 200px
|
|
3672
|
+
// area) is kept until the tree area changes. Not a getter, because
|
|
3673
|
+
// labelsWidth is measured off leaves laid out against treeWidth.
|
|
2886
3674
|
let pinnedAreaWidth = self.treeWidth === defaultTreeWidth ? undefined : self.treeAreaWidth;
|
|
2887
3675
|
addDisposer(self, autorun(() => {
|
|
2888
3676
|
const areaWidth = self.treeAreaWidth;
|
|
@@ -2891,14 +3679,17 @@ function stateModelFactory() {
|
|
|
2891
3679
|
return;
|
|
2892
3680
|
}
|
|
2893
3681
|
pinnedAreaWidth = undefined;
|
|
2894
|
-
self.setTreeWidth(Math.max(50, areaWidth -
|
|
3682
|
+
self.setTreeWidth(Math.max(50, areaWidth -
|
|
3683
|
+
labelsWidth -
|
|
3684
|
+
10 -
|
|
3685
|
+
self.marginLeft -
|
|
3686
|
+
self.cladeGutterWidth));
|
|
2895
3687
|
}));
|
|
2896
3688
|
},
|
|
2897
3689
|
}))
|
|
2898
3690
|
.postProcessSnapshot(({ data, columnTracks, ...rest }) => ({
|
|
2899
|
-
// per-property defaults
|
|
2900
|
-
//
|
|
2901
|
-
// when a sibling filehandle can refetch them, keeping sessions/URLs small
|
|
3691
|
+
// stripDefault handles per-property defaults; this drops inline documents
|
|
3692
|
+
// whose filehandle can refetch them
|
|
2902
3693
|
...rest,
|
|
2903
3694
|
...smallColumnTracks(columnTracks),
|
|
2904
3695
|
data: {
|