react-msaview 6.5.0 → 8.0.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 +13 -11
- package/bundle/index.js +122 -112
- package/bundle/index.js.map +4 -4
- 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 -2
- 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 +69 -4
- package/dist/components/MSAViewer.js +201 -8
- 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.d.ts +3 -3
- package/dist/components/ResizeHandles.js +13 -24
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/SequenceTextArea.js +3 -3
- package/dist/components/SequenceTextArea.js.map +1 -1
- package/dist/components/Track.js +41 -22
- package/dist/components/Track.js.map +1 -1
- package/dist/components/dialogs/{InterProScanDialog.d.ts → AnnotationFileDialog.d.ts} +2 -2
- package/dist/components/dialogs/AnnotationFileDialog.js +92 -0
- package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -0
- package/dist/components/dialogs/ExportSVGDialog.js +5 -3
- package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
- package/dist/components/dialogs/FeatureDialog.js +13 -9
- 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 +7 -0
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/HeaderStatusArea.js +0 -2
- package/dist/components/header/HeaderStatusArea.js.map +1 -1
- package/dist/components/header/LoadWarnings.d.ts +6 -0
- package/dist/components/header/LoadWarnings.js +35 -0
- package/dist/components/header/LoadWarnings.js.map +1 -0
- package/dist/components/header/MultiAlignmentSelector.js +0 -0
- package/dist/components/header/MultiAlignmentSelector.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 +7 -1
- package/dist/components/header/TreeSettingsMenu.js.map +1 -1
- package/dist/components/header/UnshareableDataWarning.d.ts +6 -0
- package/dist/components/header/UnshareableDataWarning.js +40 -0
- package/dist/components/header/UnshareableDataWarning.js.map +1 -0
- package/dist/components/header/ZoomControls.js +49 -6
- package/dist/components/header/ZoomControls.js.map +1 -1
- package/dist/components/header/getAnnotationsMenu.js +11 -14
- package/dist/components/header/getAnnotationsMenu.js.map +1 -1
- package/dist/components/header/settingsMenuItems.js +10 -7
- 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/ImportFormExamples.js +2 -6
- package/dist/components/import/ImportFormExamples.js.map +1 -1
- package/dist/components/import/util.js +2 -0
- 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 +1 -1
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +17 -20
- 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 +53 -0
- package/dist/components/msa/drawFeatureSpans.js +78 -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 +30 -27
- package/dist/components/msa/msaRaster.js +193 -90
- 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 +51 -87
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderHighlights.d.ts +44 -4
- package/dist/components/msa/renderHighlights.js +99 -11
- package/dist/components/msa/renderHighlights.js.map +1 -1
- package/dist/components/msa/renderMSABlock.d.ts +1 -2
- package/dist/components/msa/renderMSABlock.js +28 -31
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +9 -30
- package/dist/components/msa/renderMSAMouseover.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/renderCtx.d.ts +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 +13 -16
- 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 +30 -12
- package/dist/components/tracks/drawTracks.js +127 -34
- 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/TreeCanvas.js +6 -4
- package/dist/components/tree/TreeCanvas.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +6 -21
- package/dist/components/tree/TreeCanvasBlock.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 +11 -5
- 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.d.ts +0 -1
- package/dist/components/tree/renderTreeCanvas.js +221 -47
- 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/scaleBar.d.ts +10 -0
- package/dist/components/tree/scaleBar.js +24 -0
- package/dist/components/tree/scaleBar.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 +18 -1
- package/dist/constants.js +51 -8
- 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/fetchUtils.d.ts +7 -7
- package/dist/fetchUtils.js +4 -32
- package/dist/fetchUtils.js.map +1 -1
- 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 +29 -1
- package/dist/hierarchy.js +82 -19
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/model/DataModel.js +4 -4
- 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 +792 -259
- package/dist/model.js +2092 -675
- 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 +15 -13
- package/dist/renderToStaticMarkup.js +37 -25
- package/dist/renderToStaticMarkup.js.map +1 -1
- package/dist/renderToSvg.js +198 -68
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rowCoordinateCalculations.d.ts +26 -28
- package/dist/rowCoordinateCalculations.js +13 -39
- 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 +5 -4
- 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 +306 -124
- package/dist/types.d.ts +295 -3
- 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/util.d.ts +10 -4
- package/dist/util.js +37 -10
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wuss.d.ts +14 -0
- package/dist/wuss.js +55 -0
- package/dist/wuss.js.map +1 -0
- package/package.json +23 -14
- package/src/colorSchemes.ts +1 -2
- package/src/columnStats.ts +58 -0
- package/src/components/DragHandle.tsx +18 -7
- package/src/components/Loading.tsx +14 -8
- package/src/components/MSAView.tsx +19 -5
- package/src/components/MSAViewer.tsx +326 -10
- package/src/components/PortalTooltip.tsx +47 -0
- package/src/components/ResizeHandles.tsx +21 -36
- package/src/components/SequenceTextArea.tsx +3 -3
- package/src/components/Track.tsx +55 -18
- package/src/components/dialogs/AnnotationFileDialog.tsx +162 -0
- package/src/components/dialogs/ExportSVGDialog.tsx +7 -2
- package/src/components/dialogs/FeatureDialog.tsx +13 -9
- package/src/components/dialogs/TrackInfoDialog.tsx +1 -1
- package/src/components/header/GappynessSlider.tsx +6 -11
- package/src/components/header/Header.tsx +10 -0
- package/src/components/header/HeaderStatusArea.tsx +0 -5
- package/src/components/header/LoadWarnings.tsx +48 -0
- package/src/components/header/MultiAlignmentSelector.tsx +0 -0
- package/src/components/header/ScrollZoomIcons.tsx +52 -0
- package/src/components/header/TreeSettingsMenu.tsx +7 -1
- package/src/components/header/UnshareableDataWarning.tsx +56 -0
- package/src/components/header/ZoomControls.tsx +74 -14
- package/src/components/header/getAnnotationsMenu.ts +13 -17
- package/src/components/header/settingsMenuItems.ts +11 -6
- package/src/components/import/ImportForm.tsx +2 -2
- package/src/components/import/ImportFormExamples.tsx +4 -8
- package/src/components/import/util.ts +2 -0
- package/src/components/minimap/Minimap.tsx +1 -2
- package/src/components/msa/AnnotationLegend.tsx +48 -38
- package/src/components/msa/MSACanvas.tsx +3 -5
- package/src/components/msa/MSACanvasBlock.tsx +17 -21
- package/src/components/msa/MSAMouseoverCanvas.tsx +2 -1
- package/src/components/msa/domainBandCursor.ts +7 -11
- package/src/components/msa/drawFeatureSpans.ts +150 -0
- package/src/components/msa/legendRows.ts +24 -0
- package/src/components/msa/msaRaster.ts +236 -88
- package/src/components/msa/packDomainLanes.ts +36 -0
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +69 -123
- package/src/components/msa/renderHighlights.ts +141 -10
- package/src/components/msa/renderMSABlock.ts +29 -38
- package/src/components/msa/renderMSAMouseover.ts +9 -39
- package/src/components/msa/tileColor.ts +4 -7
- package/src/components/msa/useMsaBlockMouse.ts +6 -6
- package/src/components/renderCtx.ts +1 -0
- 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 +13 -18
- package/src/components/tracks/TrackTooltipContent.tsx +156 -0
- package/src/components/tracks/drawTracks.ts +169 -38
- package/src/components/tracks/useTrackHover.ts +42 -0
- package/src/components/tree/CladeLabels.tsx +77 -0
- package/src/components/tree/TreeCanvas.tsx +4 -3
- package/src/components/tree/TreeCanvasBlock.tsx +10 -32
- package/src/components/tree/TreeOverview.tsx +106 -0
- package/src/components/tree/TreeRuler.tsx +29 -5
- package/src/components/tree/cladeBrackets.ts +73 -0
- package/src/components/tree/dialogs/TreeNodeInfoDialog.tsx +2 -2
- package/src/components/tree/renderTreeCanvas.ts +312 -48
- package/src/components/tree/renderTreeOverview.ts +212 -0
- package/src/components/tree/scaleBar.ts +23 -0
- package/src/components/tree/useTreeHover.ts +2 -4
- package/src/constants.ts +61 -8
- package/src/createPaletteMap.ts +10 -9
- package/src/element.ts +321 -0
- package/src/featureFields.ts +19 -0
- package/src/fetchUtils.ts +7 -40
- package/src/flatToTree.ts +1 -2
- package/src/ggplotPalettes.ts +60 -0
- package/src/hierarchy.ts +97 -20
- package/src/index.ts +46 -6
- package/src/model/DataModel.ts +5 -4
- package/src/model/msaModel.ts +3 -2
- package/src/model/treeModel.ts +42 -0
- package/src/model.ts +2492 -793
- package/src/mount.tsx +32 -0
- package/src/neighborJoining.ts +8 -32
- package/src/rawImports.d.ts +9 -0
- package/src/renderToStaticMarkup.ts +41 -25
- package/src/renderToSvg.tsx +365 -92
- package/src/rowCoordinateCalculations.ts +16 -62
- package/src/scales.ts +47 -0
- package/src/seqPosToGlobalCol.ts +5 -4
- package/src/sequenceLogo.ts +6 -11
- package/src/stripDefault.ts +7 -14
- package/src/types.ts +352 -12
- package/src/umd.ts +4 -2
- package/src/useCanvasAutorun.ts +5 -10
- package/src/useMsaSvgFigure.ts +72 -0
- package/src/util.ts +40 -14
- package/src/version.ts +1 -1
- package/src/wuss.ts +62 -0
- package/dist/components/dialogs/InterProScanDialog.js +0 -137
- package/dist/components/dialogs/InterProScanDialog.js.map +0 -1
- package/dist/components/dialogs/InterProScanFileDialog.d.ts +0 -7
- package/dist/components/dialogs/InterProScanFileDialog.js +0 -64
- package/dist/components/dialogs/InterProScanFileDialog.js.map +0 -1
- package/dist/components/msa/ColumnStats.js +0 -42
- package/dist/components/msa/ColumnStats.js.map +0 -1
- package/dist/launchInterProScan.d.ts +0 -13
- package/dist/launchInterProScan.js +0 -110
- package/dist/launchInterProScan.js.map +0 -1
- package/dist/useColorContrast.d.ts +0 -10
- package/dist/useColorContrast.js +0 -13
- package/dist/useColorContrast.js.map +0 -1
- package/src/clustalX.test.ts +0 -74
- package/src/colSpace.test.ts +0 -67
- package/src/collapseLogic.test.ts +0 -115
- package/src/colorSchemes.docs.test.ts +0 -57
- package/src/columnCounts.test.ts +0 -73
- package/src/columnStats.test.ts +0 -94
- package/src/columnTracks.test.ts +0 -97
- package/src/components/dialogs/InterProScanDialog.tsx +0 -214
- package/src/components/dialogs/InterProScanFileDialog.tsx +0 -138
- package/src/components/minimap/minimapLayout.test.ts +0 -71
- package/src/components/msa/ColumnStats.tsx +0 -54
- package/src/components/msa/msaRaster.test.ts +0 -145
- package/src/components/msa/renderBoxFeatureCanvasBlock.test.ts +0 -51
- package/src/components/tree/renderTreeCanvas.test.ts +0 -296
- package/src/dataInitialized.test.ts +0 -42
- package/src/domainBands.test.ts +0 -122
- package/src/domainLetterColor.test.ts +0 -80
- package/src/exportFileName.test.ts +0 -40
- package/src/fetchUtils.test.ts +0 -83
- package/src/flatToTree.test.ts +0 -51
- package/src/hierarchy.test.ts +0 -228
- package/src/highlights.test.ts +0 -70
- package/src/impgRender.test.tsx +0 -68
- package/src/largeTree.test.ts +0 -67
- package/src/launchInterProScan.ts +0 -167
- package/src/loadErrorDisplay.test.tsx +0 -102
- package/src/modelFilehandleLoaders.test.ts +0 -137
- package/src/modelReset.test.ts +0 -120
- package/src/neighborJoining.test.ts +0 -164
- package/src/parseAsn1.test.ts +0 -36
- package/src/propertyConservation.test.ts +0 -36
- package/src/renderColumnTracksSvg.test.tsx +0 -108
- package/src/renderDomainsSvg.test.tsx +0 -118
- package/src/renderHighlightsSvg.test.tsx +0 -85
- package/src/renderMinimapSvg.test.tsx +0 -62
- package/src/renderRasterSvg.test.tsx +0 -144
- package/src/renderSequenceLogoSvg.test.tsx +0 -165
- package/src/renderToSvg.test.tsx +0 -65
- package/src/reset.test.ts +0 -61
- package/src/rowCoordinateCalculations.test.ts +0 -211
- package/src/rowSpace.test.ts +0 -46
- package/src/segmentOverlay.test.ts +0 -70
- package/src/seqPosToGlobalCol.test.ts +0 -59
- package/src/sequenceLogo.test.ts +0 -88
- package/src/setFontSize.test.ts +0 -33
- package/src/stripDefault.test.ts +0 -48
- package/src/stripDefaultSnapshot.test.ts +0 -87
- package/src/treeMetadata.test.ts +0 -39
- package/src/useCanvasAutorun.test.tsx +0 -98
- package/src/useColorContrast.ts +0 -18
- package/src/viewChrome.test.tsx +0 -134
- package/src/wheelZoom.test.ts +0 -46
package/src/model.ts
CHANGED
|
@@ -6,9 +6,9 @@ import {
|
|
|
6
6
|
applySnapshot,
|
|
7
7
|
cast,
|
|
8
8
|
getSnapshot,
|
|
9
|
+
isAlive,
|
|
9
10
|
types,
|
|
10
11
|
} from '@jbrowse/mobx-state-tree'
|
|
11
|
-
import { colord } from 'colord'
|
|
12
12
|
import { autorun, transaction } from 'mobx'
|
|
13
13
|
import {
|
|
14
14
|
generateNodeIds,
|
|
@@ -24,33 +24,50 @@ import { calculateBlocks } from './calculateBlocks.ts'
|
|
|
24
24
|
import { clustalXColumnColors } from './clustalX.ts'
|
|
25
25
|
import colorSchemes from './colorSchemes.ts'
|
|
26
26
|
import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.ts'
|
|
27
|
+
import { columnStats } from './columnStats.ts'
|
|
28
|
+
import { packDomainLanes } from './components/msa/packDomainLanes.ts'
|
|
29
|
+
import { visibleColRange } from './components/msa/visibleColRange.ts'
|
|
27
30
|
import TrackBlocks from './components/tracks/TrackBlocks.tsx'
|
|
31
|
+
import { cladeGutterWidth } from './components/tree/cladeBrackets.ts'
|
|
32
|
+
import { scaleBarLength } from './components/tree/scaleBar.ts'
|
|
28
33
|
import {
|
|
34
|
+
cladeHighlightAlpha,
|
|
35
|
+
cladeHighlightColor,
|
|
29
36
|
defaultAllowedGappyness,
|
|
30
37
|
defaultColWidth,
|
|
31
38
|
defaultColorSchemeName,
|
|
32
39
|
defaultCurrentAlignment,
|
|
33
40
|
defaultDrawMsaLetters,
|
|
41
|
+
defaultFeaturePanelWidth,
|
|
34
42
|
defaultHeight,
|
|
35
43
|
defaultHideGaps,
|
|
36
44
|
defaultRowHeight,
|
|
37
45
|
defaultScrollX,
|
|
38
46
|
defaultScrollY,
|
|
39
47
|
defaultScrollZoom,
|
|
48
|
+
defaultScrollZoomAxis,
|
|
40
49
|
defaultShowDomainLegend,
|
|
41
50
|
defaultShowDomains,
|
|
42
51
|
defaultSubFeatureRows,
|
|
52
|
+
defaultTreeWidth,
|
|
43
53
|
labelReferenceFontSize,
|
|
44
54
|
maxCellSize,
|
|
55
|
+
maxInlineSnapshotBytes,
|
|
56
|
+
maxNeighborJoiningRows,
|
|
45
57
|
minColWidth,
|
|
46
58
|
minLetterColWidth,
|
|
47
59
|
minLetterRowHeight,
|
|
48
60
|
minRowHeight,
|
|
61
|
+
rowPanelHeaderHeight,
|
|
62
|
+
rowTintAlpha,
|
|
63
|
+
scrollZoomAxes,
|
|
49
64
|
segmentFeatureTypes,
|
|
50
65
|
segmentShades,
|
|
66
|
+
treeScaleBarHeight,
|
|
51
67
|
} from './constants.ts'
|
|
52
68
|
import { createPaletteMap } from './createPaletteMap.ts'
|
|
53
69
|
import { exportFileName } from './exportFileName.ts'
|
|
70
|
+
import { featureField, featureName } from './featureFields.ts'
|
|
54
71
|
import { fetchTextWithProgress, isAbortError } from './fetchUtils.ts'
|
|
55
72
|
import { flatToTree } from './flatToTree.ts'
|
|
56
73
|
import {
|
|
@@ -59,12 +76,13 @@ import {
|
|
|
59
76
|
collapse,
|
|
60
77
|
collapsedSubtreeMaxLength,
|
|
61
78
|
find,
|
|
62
|
-
findMaxBranchLen,
|
|
63
79
|
forEachDescendant,
|
|
64
80
|
hierarchy,
|
|
81
|
+
leafIndex,
|
|
65
82
|
leaves,
|
|
66
|
-
links,
|
|
67
83
|
maxLength,
|
|
84
|
+
mrca,
|
|
85
|
+
nodeCoveringRows,
|
|
68
86
|
setBrLength,
|
|
69
87
|
sort,
|
|
70
88
|
sum as hierarchySum,
|
|
@@ -79,10 +97,11 @@ import { parseAsn1 } from './parseAsn1.ts'
|
|
|
79
97
|
import { calculatePropertyConservation } from './propertyConservation.ts'
|
|
80
98
|
import {
|
|
81
99
|
globalColToVisibleCol,
|
|
100
|
+
seqPosOfGlobalCol,
|
|
82
101
|
visibleColToGlobalCol,
|
|
83
|
-
visibleColToSeqPosForRow,
|
|
84
102
|
visibleColsBefore,
|
|
85
103
|
} from './rowCoordinateCalculations.ts'
|
|
104
|
+
import { resolveScale } from './scales.ts'
|
|
86
105
|
import { buildSeqPosIndex } from './seqPosToGlobalCol.ts'
|
|
87
106
|
import { maxBitsFor } from './sequenceLogo.ts'
|
|
88
107
|
import { stripDefault } from './stripDefault.ts'
|
|
@@ -90,22 +109,50 @@ import {
|
|
|
90
109
|
computeRowInsertions,
|
|
91
110
|
dropBlanks,
|
|
92
111
|
len,
|
|
112
|
+
outlineColor,
|
|
93
113
|
skipBlanks,
|
|
94
114
|
transform,
|
|
115
|
+
withAlpha,
|
|
95
116
|
} from './util.ts'
|
|
96
117
|
import { saveAs } from './vendor/fileSaver.ts'
|
|
118
|
+
import { parseWuss } from './wuss.ts'
|
|
97
119
|
|
|
120
|
+
import type { ColumnStats } from './columnStats.ts'
|
|
121
|
+
import type { ScrollZoomAxis } from './constants.ts'
|
|
98
122
|
import type { HierarchyNode } from './hierarchy.ts'
|
|
99
123
|
import type { ExportSvgOptions } from './renderToSvg.tsx'
|
|
124
|
+
import type { ScaleSpec } from './scales.ts'
|
|
100
125
|
import type {
|
|
101
126
|
Annotation,
|
|
127
|
+
Arc,
|
|
102
128
|
BasicTrack,
|
|
129
|
+
Cell,
|
|
130
|
+
Clade,
|
|
131
|
+
ResidueMappingProblem,
|
|
103
132
|
ColumnTrackSpec,
|
|
104
133
|
DomainBand,
|
|
134
|
+
Encoding,
|
|
135
|
+
EncodingChannel,
|
|
105
136
|
Highlight,
|
|
137
|
+
Legend,
|
|
138
|
+
LegendEntry,
|
|
106
139
|
NodeWithIds,
|
|
107
140
|
NodeWithIdsAndLength,
|
|
141
|
+
Region,
|
|
142
|
+
ResidueMapping,
|
|
143
|
+
ResidueSegment,
|
|
144
|
+
ResolvedClade,
|
|
145
|
+
ResolvedEncoding,
|
|
108
146
|
ResolvedHighlight,
|
|
147
|
+
ResolvedRowPanel,
|
|
148
|
+
RowFeaturesSpec,
|
|
149
|
+
RowPanelSpan,
|
|
150
|
+
RowPanelSpec,
|
|
151
|
+
RowResidue,
|
|
152
|
+
StructureResidue,
|
|
153
|
+
TrackKind,
|
|
154
|
+
UnshareableData,
|
|
155
|
+
Viewport,
|
|
109
156
|
} from './types.ts'
|
|
110
157
|
import type { FileLocation as FileLocationType } from '@jbrowse/core/util/types'
|
|
111
158
|
import type { Instance } from '@jbrowse/mobx-state-tree'
|
|
@@ -118,19 +165,204 @@ function parseTreeText(text: string) {
|
|
|
118
165
|
return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text)
|
|
119
166
|
}
|
|
120
167
|
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
const
|
|
168
|
+
// The height each kind draws at before the user drags a divider. A text track
|
|
169
|
+
// is absent: it is one alignment row tall and follows rowHeight, so the zoom
|
|
170
|
+
// controls already size it.
|
|
171
|
+
export const defaultTrackHeights: Partial<Record<TrackKind, number>> = {
|
|
172
|
+
bar: 40,
|
|
173
|
+
// taller than a bar track: in a 40px stack of four residues each glyph is
|
|
174
|
+
// too short to identify. Twice the height of the tracks above it, so it
|
|
175
|
+
// starts hidden
|
|
176
|
+
logo: 80,
|
|
177
|
+
arc: 50,
|
|
178
|
+
ruler: 20,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// the kinds a divider resizes. The ruler is a fixed scale, and a text track
|
|
182
|
+
// follows the vertical zoom.
|
|
183
|
+
const resizableKinds = new Set<TrackKind>(['bar', 'logo', 'arc'])
|
|
184
|
+
|
|
185
|
+
const defaultOffTracks = new Set(['sequence-logo', 'position-ruler'])
|
|
186
|
+
|
|
187
|
+
// a data track carries its own height rather than its kind's, so its divider
|
|
188
|
+
// resizes it alone
|
|
189
|
+
const ownHeightKey = (id: string) => `own:${id}`
|
|
190
|
+
|
|
191
|
+
// base-pair arcs: one color for nested helices, one for pseudoknots, which cross
|
|
192
|
+
// them
|
|
193
|
+
const HELIX_ARC = '#4e79a7'
|
|
194
|
+
const PSEUDOKNOT_ARC = '#e15759'
|
|
195
|
+
|
|
196
|
+
// the fill of a span under a scale that gives its value no color, such as a
|
|
197
|
+
// gene a `map` over gene names leaves out
|
|
198
|
+
const UNSCALED_FEATURE = '#d9d9d9'
|
|
199
|
+
|
|
200
|
+
// the fraction of a span a following one may cover before it takes a lane of
|
|
201
|
+
// its own in a `features` panel
|
|
202
|
+
const laneOverlap = 0.1
|
|
203
|
+
|
|
204
|
+
// a row panel's own width, or a default: the row height, which makes a strip
|
|
205
|
+
// cell square, and a gene neighborhood's width for a features panel
|
|
206
|
+
function rowPanelWidth(panel: RowPanelSpec, rowHeight: number) {
|
|
207
|
+
return (
|
|
208
|
+
panel.width ??
|
|
209
|
+
(panel.kind === 'features' ? defaultFeaturePanelWidth : rowHeight)
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// the fill and outline of every feature in a list: its own GFF `color=` first,
|
|
214
|
+
// then the scale an encoding resolves over one of its fields, then the
|
|
215
|
+
// accession palette
|
|
216
|
+
function featureColorMap(
|
|
217
|
+
annotations: Annotation[],
|
|
218
|
+
encoding:
|
|
219
|
+
| { field: string; colorOf: (value: string) => string | undefined }
|
|
220
|
+
| undefined,
|
|
221
|
+
fillPalette: Record<string, string>,
|
|
222
|
+
) {
|
|
223
|
+
const strokes = new Map<string, string>()
|
|
224
|
+
const strokeOf = (fill: string) => {
|
|
225
|
+
const hit = strokes.get(fill)
|
|
226
|
+
if (hit !== undefined) {
|
|
227
|
+
return hit
|
|
228
|
+
}
|
|
229
|
+
const stroke = outlineColor(fill)
|
|
230
|
+
strokes.set(fill, stroke)
|
|
231
|
+
return stroke
|
|
232
|
+
}
|
|
233
|
+
return new Map(
|
|
234
|
+
annotations.map(annotation => {
|
|
235
|
+
const value = encoding
|
|
236
|
+
? featureField(annotation, encoding.field)
|
|
237
|
+
: undefined
|
|
238
|
+
const fill =
|
|
239
|
+
annotation.color ??
|
|
240
|
+
(value === undefined ? undefined : encoding!.colorOf(value)) ??
|
|
241
|
+
(encoding ? UNSCALED_FEATURE : fillPalette[annotation.accession]!)
|
|
242
|
+
return [annotation, { fill, stroke: strokeOf(fill) }]
|
|
243
|
+
}),
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// the text a label channel draws inside each span, for the features carrying
|
|
248
|
+
// the field it names
|
|
249
|
+
function featureLabelMap(annotations: Annotation[], field: string) {
|
|
250
|
+
const labels = new Map<Annotation, string>()
|
|
251
|
+
for (const annotation of annotations) {
|
|
252
|
+
const value = featureField(annotation, field)
|
|
253
|
+
if (value !== undefined) {
|
|
254
|
+
labels.set(annotation, value)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return labels
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The spans a `features` panel draws, keyed by row name and measured in the
|
|
262
|
+
* panel's own pixels. `column` takes the bands the overlay draws, at the
|
|
263
|
+
* alignment's column width. `position` packs each row's features in its own
|
|
264
|
+
* residue positions, shifted by an `align` transform, and maps the extent they
|
|
265
|
+
* cover across every row onto the panel width, so a row with no alignment
|
|
266
|
+
* still has an x.
|
|
267
|
+
*/
|
|
268
|
+
function featurePanelSpans({
|
|
269
|
+
panel,
|
|
270
|
+
width,
|
|
271
|
+
rowHeight,
|
|
272
|
+
colWidth,
|
|
273
|
+
domainBands,
|
|
274
|
+
annotationsByRow,
|
|
275
|
+
shifts,
|
|
276
|
+
}: {
|
|
277
|
+
panel: RowFeaturesSpec
|
|
278
|
+
width: number
|
|
279
|
+
rowHeight: number
|
|
280
|
+
colWidth: number
|
|
281
|
+
domainBands: Map<string, DomainBand[]>
|
|
282
|
+
annotationsByRow: Record<string, Annotation[]>
|
|
283
|
+
shifts: Map<string, number> | undefined
|
|
284
|
+
}): Map<string, RowPanelSpan[]> {
|
|
285
|
+
if (panel.x === 'column') {
|
|
286
|
+
return new Map(
|
|
287
|
+
[...domainBands].map(([name, bands]) => [
|
|
288
|
+
name,
|
|
289
|
+
bands.map(band => ({
|
|
290
|
+
...band,
|
|
291
|
+
xStart: band.startCol * colWidth,
|
|
292
|
+
xEnd: band.endCol * colWidth,
|
|
293
|
+
})),
|
|
294
|
+
]),
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
const shifted = Object.entries(annotationsByRow).map(
|
|
298
|
+
([name, annotations]) => {
|
|
299
|
+
const shift = shifts?.get(name) ?? 0
|
|
300
|
+
return [
|
|
301
|
+
name,
|
|
302
|
+
annotations.map(annotation => ({
|
|
303
|
+
annotation,
|
|
304
|
+
start: annotation.start - 1 + shift,
|
|
305
|
+
end: annotation.end + shift,
|
|
306
|
+
})),
|
|
307
|
+
] as const
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
let min = Infinity
|
|
311
|
+
let max = -Infinity
|
|
312
|
+
for (const [, features] of shifted) {
|
|
313
|
+
for (const { start, end } of features) {
|
|
314
|
+
min = Math.min(min, start)
|
|
315
|
+
max = Math.max(max, end)
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// the arrowhead on the rightmost feature reaches a row height past its end,
|
|
319
|
+
// so the extent maps onto the panel less that much
|
|
320
|
+
const drawable = Math.max(1, width - rowHeight)
|
|
321
|
+
const scale = max > min ? drawable / (max - min) : 0
|
|
322
|
+
// a lane opens where a span covers more than a tenth of the one before it,
|
|
323
|
+
// which keeps the genes of an operon on one lane: adjacent genes commonly
|
|
324
|
+
// share a few bases, and a stop codon overlapping the next start reads as a
|
|
325
|
+
// second lane over the whole row
|
|
326
|
+
return new Map(
|
|
327
|
+
shifted.map(([name, features]) => [
|
|
328
|
+
name,
|
|
329
|
+
packDomainLanes(
|
|
330
|
+
features.map(({ annotation, start, end }) => ({
|
|
331
|
+
annotation,
|
|
332
|
+
xStart: (start - min) * scale,
|
|
333
|
+
xEnd: (end - min) * scale,
|
|
334
|
+
startCol: start,
|
|
335
|
+
endCol: end - (end - start) * laneOverlap,
|
|
336
|
+
})),
|
|
337
|
+
),
|
|
338
|
+
]),
|
|
339
|
+
)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// a scale over the values a field takes across the features drawn
|
|
343
|
+
function resolveFeatureScale(
|
|
344
|
+
field: string,
|
|
345
|
+
scale: ScaleSpec | undefined,
|
|
346
|
+
annotations: Annotation[],
|
|
347
|
+
) {
|
|
348
|
+
return {
|
|
349
|
+
field,
|
|
350
|
+
...resolveScale(
|
|
351
|
+
scale,
|
|
352
|
+
annotations.map(a => featureField(a, field)).filter(notEmpty),
|
|
353
|
+
),
|
|
354
|
+
}
|
|
355
|
+
}
|
|
125
356
|
|
|
126
357
|
// a data track over this size stays in the live model but leaves the snapshot,
|
|
127
358
|
// the same rule DataModel applies to an inline document
|
|
128
|
-
|
|
359
|
+
function columnTrackSizes(tracks?: readonly ColumnTrackSpec[]) {
|
|
360
|
+
return (tracks ?? []).map(t => JSON.stringify(t).length)
|
|
361
|
+
}
|
|
129
362
|
|
|
130
363
|
function smallColumnTracks(tracks?: ColumnTrackSpec[]) {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
)
|
|
364
|
+
const sizes = columnTrackSizes(tracks)
|
|
365
|
+
const kept = tracks?.filter((_t, i) => sizes[i]! <= maxInlineSnapshotBytes)
|
|
134
366
|
return kept?.length ? { columnTracks: kept } : {}
|
|
135
367
|
}
|
|
136
368
|
|
|
@@ -138,17 +370,13 @@ function smallColumnTracks(tracks?: ColumnTrackSpec[]) {
|
|
|
138
370
|
* The snapshot properties reset() carries across a return to the import form:
|
|
139
371
|
* display preferences and layout, nothing derived from the loaded file.
|
|
140
372
|
*
|
|
141
|
-
* reset() applies a default snapshot filtered to this list, so
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
* are path-derived (node-0-0-1), a stale `collapsed` or `showOnly` id matched
|
|
147
|
-
* a real node in the new tree and folded it. Downstream composed properties
|
|
148
|
-
* (e.g. the jbrowse plugin's) are not on the list, so they reset too.
|
|
373
|
+
* reset() applies a default snapshot filtered to this list, so any property
|
|
374
|
+
* left off it resets, including downstream composed ones (e.g. the jbrowse
|
|
375
|
+
* plugin's). Node ids are path-derived (node-0-0-1), so a carried-over
|
|
376
|
+
* `collapsed` or `showOnly` id would match a real node in the next tree and
|
|
377
|
+
* fold it.
|
|
149
378
|
*
|
|
150
|
-
* Exported for modelReset.test.ts
|
|
151
|
-
* matches a freshly created model after reset().
|
|
379
|
+
* Exported for modelReset.test.ts.
|
|
152
380
|
*/
|
|
153
381
|
export const preservedOnReset = new Set([
|
|
154
382
|
'id',
|
|
@@ -156,6 +384,7 @@ export const preservedOnReset = new Set([
|
|
|
156
384
|
'height',
|
|
157
385
|
'drawMsaLetters',
|
|
158
386
|
'scrollZoom',
|
|
387
|
+
'scrollZoomAxis',
|
|
159
388
|
'bgColor',
|
|
160
389
|
'colorSchemeName',
|
|
161
390
|
'showColumnStats',
|
|
@@ -166,23 +395,207 @@ export const preservedOnReset = new Set([
|
|
|
166
395
|
'showBranchLen',
|
|
167
396
|
'drawTree',
|
|
168
397
|
'drawNodeBubbles',
|
|
398
|
+
'drawNodeLabels',
|
|
399
|
+
'showTreeOverview',
|
|
400
|
+
'overviewHeight',
|
|
169
401
|
'autoTreeAreaWidth',
|
|
170
402
|
'turnedOffTracks',
|
|
403
|
+
'trackHeights',
|
|
171
404
|
'hideGaps',
|
|
172
405
|
'allowedGappyness',
|
|
173
406
|
'subFeatureRows',
|
|
174
407
|
'showDomainLegend',
|
|
175
408
|
])
|
|
176
409
|
|
|
177
|
-
// `turnedOffTracks`
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
410
|
+
// `turnedOffTracks` holds only the user's explicit choices, so a hidden-by-default
|
|
411
|
+
// track writes nothing into the snapshot. A file-supplied track passes its own
|
|
412
|
+
// `defaultOff`, since the count varies by file: a Pfam seed has a couple of #=GR
|
|
413
|
+
// lines, an Rfam family one per row.
|
|
181
414
|
function trackIsOff(
|
|
182
415
|
turnedOffTracks: { get: (id: string) => boolean | undefined },
|
|
183
416
|
id: string,
|
|
417
|
+
defaultOff?: boolean,
|
|
418
|
+
) {
|
|
419
|
+
return turnedOffTracks.get(id) ?? (defaultOff || defaultOffTracks.has(id))
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// shared empty results, so observers don't see a fresh [] as a change
|
|
423
|
+
const noDomains: Annotation[] = []
|
|
424
|
+
const noClades: ResolvedClade[] = []
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* The rows a clade covers and the node an `mrca` names, or undefined when the
|
|
428
|
+
* clade does not resolve: a tip name the tree does not have or has twice, or a
|
|
429
|
+
* leaf count `tips` disagrees with. `index` and `rowNamesSet` are the memoized
|
|
430
|
+
* passes over the tree.
|
|
431
|
+
*/
|
|
432
|
+
function cladeRows(
|
|
433
|
+
clade: Clade,
|
|
434
|
+
root: HierarchyNode<NodeWithIds>,
|
|
435
|
+
index: Map<string, HierarchyNode<NodeWithIds> | undefined>,
|
|
436
|
+
rowNamesSet: Map<string, number>,
|
|
437
|
+
): { rows: [number, number]; nodeId?: string } | undefined {
|
|
438
|
+
if (clade.range) {
|
|
439
|
+
const [a, b] = clade.range.map(name =>
|
|
440
|
+
index.get(name) ? rowNamesSet.get(name) : undefined,
|
|
441
|
+
)
|
|
442
|
+
if (a === undefined || b === undefined) {
|
|
443
|
+
return undefined
|
|
444
|
+
}
|
|
445
|
+
const rows: [number, number] = a <= b ? [a, b] : [b, a]
|
|
446
|
+
return rows[1] - rows[0] + 1 === clade.tips ? { rows } : undefined
|
|
447
|
+
}
|
|
448
|
+
const node = clade.mrca ? mrca(root, clade.mrca, index) : undefined
|
|
449
|
+
if (!node) {
|
|
450
|
+
return undefined
|
|
451
|
+
}
|
|
452
|
+
const tips = leaves(node)
|
|
453
|
+
if (tips.length !== clade.tips) {
|
|
454
|
+
return undefined
|
|
455
|
+
}
|
|
456
|
+
let first = Infinity
|
|
457
|
+
let last = -Infinity
|
|
458
|
+
for (const tip of tips) {
|
|
459
|
+
const row = rowNamesSet.get(tip.data.name)
|
|
460
|
+
if (row !== undefined) {
|
|
461
|
+
first = Math.min(first, row)
|
|
462
|
+
last = Math.max(last, row)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return first <= last
|
|
466
|
+
? { rows: [first, last], nodeId: node.data.id }
|
|
467
|
+
: undefined
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Every clade that resolves, with its fill color settled, against the leaf
|
|
472
|
+
* order `rowNamesSet` gives. The tree panel resolves against the displayed
|
|
473
|
+
* rows and the overview against its own, so both take the tree they draw. A
|
|
474
|
+
* `range` record names no node, so `collapse` and `focus`, which need one,
|
|
475
|
+
* drop it.
|
|
476
|
+
*/
|
|
477
|
+
function resolveClades(
|
|
478
|
+
clades: Clade[],
|
|
479
|
+
root: HierarchyNode<NodeWithIds>,
|
|
480
|
+
rowNamesSet: Map<string, number>,
|
|
481
|
+
): ResolvedClade[] {
|
|
482
|
+
const index = leafIndex(root)
|
|
483
|
+
return clades.flatMap(clade => {
|
|
484
|
+
const resolved = cladeRows(clade, root, index, rowNamesSet)
|
|
485
|
+
const seeding = clade.mark === 'collapse' || clade.mark === 'focus'
|
|
486
|
+
return resolved && !(seeding && resolved.nodeId === undefined)
|
|
487
|
+
? [
|
|
488
|
+
{
|
|
489
|
+
...resolved,
|
|
490
|
+
mark: clade.mark,
|
|
491
|
+
color: withAlpha(
|
|
492
|
+
clade.color ?? cladeHighlightColor,
|
|
493
|
+
cladeHighlightAlpha,
|
|
494
|
+
),
|
|
495
|
+
markColor: clade.color,
|
|
496
|
+
label: clade.label,
|
|
497
|
+
},
|
|
498
|
+
]
|
|
499
|
+
: []
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* The tree with the display transforms applied: leaf counts summed, children
|
|
505
|
+
* sorted by branch length, `showOnly` taken as the new root, and each
|
|
506
|
+
* `collapsed` clade folded. The overview builds the same tree without the
|
|
507
|
+
* focus, so it shows the focused subtree inside the whole.
|
|
508
|
+
*/
|
|
509
|
+
function buildTreeRoot(
|
|
510
|
+
tree: NodeWithIds,
|
|
511
|
+
collapsed: readonly string[],
|
|
512
|
+
showOnly?: string,
|
|
184
513
|
) {
|
|
185
|
-
|
|
514
|
+
let hier = hierarchy(tree, d => d.children)
|
|
515
|
+
hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1))
|
|
516
|
+
sort(hier, (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1))
|
|
517
|
+
|
|
518
|
+
if (showOnly) {
|
|
519
|
+
const res = find(hier, n => n.data.id === showOnly)
|
|
520
|
+
if (res) {
|
|
521
|
+
hier = res
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
for (const collapsedId of collapsed) {
|
|
526
|
+
const node = find(hier, n => n.data.id === collapsedId)
|
|
527
|
+
if (node) {
|
|
528
|
+
if (node.children) {
|
|
529
|
+
collapse(node)
|
|
530
|
+
} else if (node.parent?.children) {
|
|
531
|
+
node.parent.children = node.parent.children.filter(
|
|
532
|
+
c => c.data.id !== collapsedId,
|
|
533
|
+
)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return hier
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// the inclusive tip indices a node covers, from the row-space extent
|
|
542
|
+
// clusterLayout writes to xMin/xMax as tip centers
|
|
543
|
+
function tipRange(node: HierarchyNode): [number, number] {
|
|
544
|
+
return [Math.round(node.xMin! - 0.5), Math.round(node.xMax! - 0.5)]
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// the channels reading the feature table; every other channel reads rowData
|
|
548
|
+
const featureChannels = new Set<EncodingChannel>([
|
|
549
|
+
'featureFill',
|
|
550
|
+
'featureLabel',
|
|
551
|
+
])
|
|
552
|
+
|
|
553
|
+
// seqPos -> column indexes per row, keyed on the parse so they are garbage
|
|
554
|
+
// collected with it. A computed would rebuild every row's index when read
|
|
555
|
+
// outside a reactive context.
|
|
556
|
+
const seqPosIndexCache = new WeakMap<object, Map<string, Int32Array>>()
|
|
557
|
+
|
|
558
|
+
// A segment is a 1:1 run. A segment whose sides differ in length is malformed,
|
|
559
|
+
// and the lookups treat it like an uncovered position.
|
|
560
|
+
function sameLength(segment: ResidueSegment) {
|
|
561
|
+
return (
|
|
562
|
+
segment.rowEnd - segment.rowStart ===
|
|
563
|
+
segment.structEnd - segment.structStart
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// fit() divides the viewport by the row or column count and multiplies back, so
|
|
568
|
+
// an exact fit can land a fraction of a pixel over. A plain `>` would then show
|
|
569
|
+
// a scrollbar that shrinks the viewport and leaves a gap.
|
|
570
|
+
function overflows(content: number, viewport: number) {
|
|
571
|
+
return content - viewport > 0.5
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function inRanges(ranges: [number, number][] | undefined, position: number) {
|
|
575
|
+
return !!ranges?.some(([start, end]) => position >= start && position <= end)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// the value every one of a node's children reports, or undefined where one of
|
|
579
|
+
// them has no value or they disagree
|
|
580
|
+
function sharedValue(values: (string | undefined)[]) {
|
|
581
|
+
const [first] = values
|
|
582
|
+
return first !== undefined && values.every(v => v === first)
|
|
583
|
+
? first
|
|
584
|
+
: undefined
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// preorder, so reversing the list puts every node after its descendants
|
|
588
|
+
function preorder(tree: NodeWithIds) {
|
|
589
|
+
const order: NodeWithIds[] = []
|
|
590
|
+
const stack = [tree]
|
|
591
|
+
while (stack.length > 0) {
|
|
592
|
+
const node = stack.pop()!
|
|
593
|
+
order.push(node)
|
|
594
|
+
for (const child of node.children) {
|
|
595
|
+
stack.push(child)
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return order
|
|
186
599
|
}
|
|
187
600
|
|
|
188
601
|
/**
|
|
@@ -225,10 +638,8 @@ function stateModelFactory() {
|
|
|
225
638
|
/**
|
|
226
639
|
* #property
|
|
227
640
|
* whether the domain legend is expanded. The legend floats over the
|
|
228
|
-
* top-right of the alignment, so
|
|
229
|
-
*
|
|
230
|
-
* and keep it collapsed, and what lets a session or a figure open with
|
|
231
|
-
* it already out of the way.
|
|
641
|
+
* top-right of the alignment and covers residues, so a session or
|
|
642
|
+
* figure can open with it collapsed.
|
|
232
643
|
*/
|
|
233
644
|
showDomainLegend: stripDefault(types.boolean, defaultShowDomainLegend),
|
|
234
645
|
/**
|
|
@@ -261,6 +672,15 @@ function stateModelFactory() {
|
|
|
261
672
|
*/
|
|
262
673
|
scrollZoom: stripDefault(types.boolean, defaultScrollZoom),
|
|
263
674
|
|
|
675
|
+
/**
|
|
676
|
+
* #property
|
|
677
|
+
* which cell dimensions a wheel zoom scales, while `scrollZoom` is on
|
|
678
|
+
*/
|
|
679
|
+
scrollZoomAxis: stripDefault(
|
|
680
|
+
types.enumeration('ScrollZoomAxis', [...scrollZoomAxes]),
|
|
681
|
+
defaultScrollZoomAxis,
|
|
682
|
+
),
|
|
683
|
+
|
|
264
684
|
/**
|
|
265
685
|
* #property
|
|
266
686
|
* height of the div containing the view, px
|
|
@@ -341,17 +761,38 @@ function stateModelFactory() {
|
|
|
341
761
|
* hidden-by-default track adds nothing to the shared URL.
|
|
342
762
|
*/
|
|
343
763
|
turnedOffTracks: stripDefault(types.map(types.boolean), {}),
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* #property
|
|
767
|
+
* the height of every track one divider resizes, keyed by `heightKey`:
|
|
768
|
+
* the `kind` for the computed tracks, `own:<id>` for a data track. A
|
|
769
|
+
* key is absent until the user drags that divider, and
|
|
770
|
+
* `defaultTrackHeights` answers for it until then, so an untouched
|
|
771
|
+
* viewer adds nothing to the shared URL.
|
|
772
|
+
*/
|
|
773
|
+
trackHeights: stripDefault(types.map(types.number), {}),
|
|
344
774
|
/**
|
|
345
775
|
* #property
|
|
346
|
-
* tracks supplied as data
|
|
347
|
-
* per-column
|
|
348
|
-
* text track. See docs/layers.md
|
|
776
|
+
* tracks supplied as data: per-column values drawn as bars, or a
|
|
777
|
+
* per-column string drawn as a text track. See docs/layers.md
|
|
349
778
|
*/
|
|
350
779
|
columnTracks: stripDefault(
|
|
351
780
|
types.array(types.frozen<ColumnTrackSpec>()),
|
|
352
781
|
[],
|
|
353
782
|
),
|
|
354
783
|
|
|
784
|
+
/**
|
|
785
|
+
* #property
|
|
786
|
+
* row-to-structure residue correspondence, computed outside the viewer
|
|
787
|
+
* (e.g. from SIFTS). Matching by sequence equality places a tagged
|
|
788
|
+
* construct, a truncation or a subsequence row on the wrong residue.
|
|
789
|
+
* See docs/layers.md
|
|
790
|
+
*/
|
|
791
|
+
residueMappings: stripDefault(
|
|
792
|
+
types.array(types.frozen<ResidueMapping>()),
|
|
793
|
+
[],
|
|
794
|
+
),
|
|
795
|
+
|
|
355
796
|
/**
|
|
356
797
|
* #property
|
|
357
798
|
* data from the loaded tree/msa/treeMetadata, generally loaded by
|
|
@@ -365,8 +806,12 @@ function stateModelFactory() {
|
|
|
365
806
|
|
|
366
807
|
/**
|
|
367
808
|
* #property
|
|
809
|
+
* the user's explicit hide choices per annotation accession, keyed by
|
|
810
|
+
* accession with the value meaning "off", like `turnedOffTracks`. An
|
|
811
|
+
* untouched accession is absent and drawn, so the shared URL grows
|
|
812
|
+
* only with the user's filters
|
|
368
813
|
*/
|
|
369
|
-
|
|
814
|
+
turnedOffFeatures: stripDefault(types.map(types.boolean), {}),
|
|
370
815
|
/**
|
|
371
816
|
* #property
|
|
372
817
|
*/
|
|
@@ -385,9 +830,34 @@ function stateModelFactory() {
|
|
|
385
830
|
* labeled highlights in 1-based inclusive coordinates: a column span
|
|
386
831
|
* `{start, end}`, a residue span `{row, start, end}` of a named row,
|
|
387
832
|
* or a row set `{rows}`, each with an optional `label` and `color`.
|
|
388
|
-
* Persists in the snapshot
|
|
833
|
+
* Persists in the snapshot and the URL.
|
|
389
834
|
*/
|
|
390
835
|
highlights: stripDefault(types.array(types.frozen<Highlight>()), []),
|
|
836
|
+
/**
|
|
837
|
+
* #property
|
|
838
|
+
* clades of the tree with a mark drawn over them. `mrca` names tips
|
|
839
|
+
* whose common ancestor is the clade, or `range` its first and last
|
|
840
|
+
* tip in display order, and `tips` is the leaf count the producer
|
|
841
|
+
* measured. `mark` is `highlight`, `bracket`, `collapse` or `focus`.
|
|
842
|
+
* See docs/layers.md
|
|
843
|
+
*/
|
|
844
|
+
clades: stripDefault(types.array(types.frozen<Clade>()), []),
|
|
845
|
+
/**
|
|
846
|
+
* #property
|
|
847
|
+
* what the viewer's own marks read from `rowData`:
|
|
848
|
+
* `{channel, field, scale?}` per channel, where `channel` is
|
|
849
|
+
* `tipLabel` or `rowTint`. See docs/layers.md
|
|
850
|
+
*/
|
|
851
|
+
encodings: stripDefault(types.array(types.frozen<Encoding>()), []),
|
|
852
|
+
/**
|
|
853
|
+
* #property
|
|
854
|
+
* panels drawn between the tree and the alignment:
|
|
855
|
+
* `{kind: "strip", field, scale?, width?, header?}` colors one cell
|
|
856
|
+
* per row from a `rowData` field, and
|
|
857
|
+
* `{kind: "features", x, encoding?, transform?, width?, header?}`
|
|
858
|
+
* draws the spans the GFF carries. See docs/layers.md
|
|
859
|
+
*/
|
|
860
|
+
rowPanels: stripDefault(types.array(types.frozen<RowPanelSpec>()), []),
|
|
391
861
|
}),
|
|
392
862
|
)
|
|
393
863
|
.volatile(() => ({
|
|
@@ -397,15 +867,17 @@ function stateModelFactory() {
|
|
|
397
867
|
headerHeight: 0,
|
|
398
868
|
/**
|
|
399
869
|
* #volatile
|
|
870
|
+
* leaves the toolbar out, for a host drawing its own controls. Kept out
|
|
871
|
+
* of the snapshot so a link opened in the full app shows the toolbar.
|
|
400
872
|
*/
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
873
|
+
hideHeader: false,
|
|
874
|
+
/**
|
|
875
|
+
* #volatile
|
|
876
|
+
*/
|
|
877
|
+
status: undefined as { msg: string; onCancel?: () => void } | undefined,
|
|
404
878
|
/**
|
|
405
879
|
* #volatile
|
|
406
|
-
*
|
|
407
|
-
* screens. derived from the device pixel ratio so canvases are crisp on
|
|
408
|
-
* retina/4k displays and not needlessly oversized on standard ones
|
|
880
|
+
* canvas scale factor, from the device pixel ratio
|
|
409
881
|
*/
|
|
410
882
|
highResScaleFactor:
|
|
411
883
|
typeof window === 'undefined' ? 1 : window.devicePixelRatio,
|
|
@@ -472,6 +944,13 @@ function stateModelFactory() {
|
|
|
472
944
|
*/
|
|
473
945
|
highlightedColumns: undefined as number[] | undefined,
|
|
474
946
|
|
|
947
|
+
/**
|
|
948
|
+
* #volatile
|
|
949
|
+
* transient highlights keyed by owner, so a structure viewer's hover and
|
|
950
|
+
* a genome view's hover each clear only their own. Not persisted.
|
|
951
|
+
*/
|
|
952
|
+
transientHighlights: {} as Record<string, Highlight[]>,
|
|
953
|
+
|
|
475
954
|
/**
|
|
476
955
|
* #volatile
|
|
477
956
|
*/
|
|
@@ -480,33 +959,41 @@ function stateModelFactory() {
|
|
|
480
959
|
/**
|
|
481
960
|
* #volatile
|
|
482
961
|
*/
|
|
483
|
-
|
|
962
|
+
marginLeft: 20,
|
|
484
963
|
|
|
485
964
|
/**
|
|
486
965
|
* #volatile
|
|
487
|
-
* taller than the conservation track by default: the logo spends its
|
|
488
|
-
* height on stacked glyphs, and a 40px stack of four residues leaves each
|
|
489
|
-
* one too short to identify
|
|
490
966
|
*/
|
|
491
|
-
|
|
967
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
968
|
+
error: undefined as unknown,
|
|
492
969
|
|
|
493
970
|
/**
|
|
494
971
|
* #volatile
|
|
972
|
+
* non-fatal load problems: an optional layer that failed to load, an
|
|
973
|
+
* overlay that failed to parse. `error` replaces the view and is for the
|
|
974
|
+
* alignment itself
|
|
495
975
|
*/
|
|
496
|
-
|
|
976
|
+
warnings: [] as string[],
|
|
497
977
|
|
|
498
978
|
/**
|
|
499
979
|
* #volatile
|
|
980
|
+
* bumped by reset(). The error boundary above the view uses it as its key,
|
|
981
|
+
* since the boundary keeps its caught error until remounted
|
|
500
982
|
*/
|
|
501
|
-
|
|
502
|
-
|
|
983
|
+
resetCount: 0,
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* #volatile
|
|
987
|
+
* set by a host that restores the loaded documents itself, such as a
|
|
988
|
+
* jbrowse session or a page that refetches them. `unshareableData` then
|
|
989
|
+
* reports nothing
|
|
990
|
+
*/
|
|
991
|
+
hostCarriesData: false,
|
|
503
992
|
|
|
504
993
|
/**
|
|
505
994
|
* #volatile
|
|
506
|
-
* overlay annotations drawn on the alignment,
|
|
507
|
-
*
|
|
508
|
-
* flat list before it reaches the model, so nothing downstream of here
|
|
509
|
-
* knows which one it came from
|
|
995
|
+
* overlay annotations drawn on the alignment. InterProScan JSON, GFF and
|
|
996
|
+
* user uploads all convert to this flat list
|
|
510
997
|
*/
|
|
511
998
|
annotations: [] as Annotation[],
|
|
512
999
|
}))
|
|
@@ -552,8 +1039,8 @@ function stateModelFactory() {
|
|
|
552
1039
|
},
|
|
553
1040
|
/**
|
|
554
1041
|
* #action
|
|
555
|
-
*
|
|
556
|
-
*
|
|
1042
|
+
* update the canvas scale factor when the device pixel ratio changes
|
|
1043
|
+
* (moving between monitors, browser zoom)
|
|
557
1044
|
*/
|
|
558
1045
|
setHighResScaleFactor(arg: number) {
|
|
559
1046
|
self.highResScaleFactor = arg
|
|
@@ -574,13 +1061,38 @@ function stateModelFactory() {
|
|
|
574
1061
|
self.error = error
|
|
575
1062
|
},
|
|
576
1063
|
|
|
1064
|
+
/**
|
|
1065
|
+
* #action
|
|
1066
|
+
* record a non-fatal load problem: a layer that failed to load, a file
|
|
1067
|
+
* that failed to parse
|
|
1068
|
+
*/
|
|
1069
|
+
addWarning(warning: string) {
|
|
1070
|
+
self.warnings = [...self.warnings, warning]
|
|
1071
|
+
},
|
|
1072
|
+
|
|
1073
|
+
/**
|
|
1074
|
+
* #action
|
|
1075
|
+
*/
|
|
1076
|
+
clearWarnings() {
|
|
1077
|
+
self.warnings = []
|
|
1078
|
+
},
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* #action
|
|
1082
|
+
* declare that this host restores the loaded documents itself, which
|
|
1083
|
+
* hides the "Not in the link" warning. See `hostCarriesData`
|
|
1084
|
+
*/
|
|
1085
|
+
setHostCarriesData(arg: boolean) {
|
|
1086
|
+
self.hostCarriesData = arg
|
|
1087
|
+
},
|
|
1088
|
+
|
|
577
1089
|
/**
|
|
578
1090
|
* #action
|
|
579
1091
|
* set mouse position (row, column) in the MSA
|
|
580
1092
|
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
* signature stable
|
|
1093
|
+
* Public API: a host calls this (and reads `mouseCol`) to sync hover with
|
|
1094
|
+
* its own view, such as a genome view or 3D structure. Keep the name and
|
|
1095
|
+
* signature stable.
|
|
584
1096
|
*/
|
|
585
1097
|
setMousePos(col?: number, row?: number) {
|
|
586
1098
|
self.mouseCol = col
|
|
@@ -591,9 +1103,9 @@ function stateModelFactory() {
|
|
|
591
1103
|
* #action
|
|
592
1104
|
* set highlighted columns
|
|
593
1105
|
*
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
1106
|
+
* Public API: jbrowse-plugin-msaview calls this from its
|
|
1107
|
+
* afterCreateAutoruns, and MSAViewer passes its `highlightColumns` prop
|
|
1108
|
+
* through it.
|
|
597
1109
|
*/
|
|
598
1110
|
setHighlightedColumns(columns?: number[]) {
|
|
599
1111
|
self.highlightedColumns = columns
|
|
@@ -604,6 +1116,35 @@ function stateModelFactory() {
|
|
|
604
1116
|
setHighlights(highlights: Highlight[]) {
|
|
605
1117
|
self.highlights.replace(highlights)
|
|
606
1118
|
},
|
|
1119
|
+
|
|
1120
|
+
/**
|
|
1121
|
+
* #action
|
|
1122
|
+
* replace the clades the viewer marks (see docs/layers.md)
|
|
1123
|
+
*/
|
|
1124
|
+
setClades(clades: Clade[]) {
|
|
1125
|
+
self.clades.replace(clades)
|
|
1126
|
+
},
|
|
1127
|
+
/**
|
|
1128
|
+
* #action
|
|
1129
|
+
* show `highlights` for `owner`, replacing that owner's previous ones and
|
|
1130
|
+
* leaving other owners' in place
|
|
1131
|
+
*/
|
|
1132
|
+
applyHighlight(owner: string, highlights: Highlight[]) {
|
|
1133
|
+
self.transientHighlights = {
|
|
1134
|
+
...self.transientHighlights,
|
|
1135
|
+
[owner]: highlights,
|
|
1136
|
+
}
|
|
1137
|
+
},
|
|
1138
|
+
/**
|
|
1139
|
+
* #action
|
|
1140
|
+
* remove `owner`'s highlights, leaving other owners' in place
|
|
1141
|
+
*/
|
|
1142
|
+
clearHighlight(owner: string) {
|
|
1143
|
+
if (owner in self.transientHighlights) {
|
|
1144
|
+
const { [owner]: _dropped, ...rest } = self.transientHighlights
|
|
1145
|
+
self.transientHighlights = rest
|
|
1146
|
+
}
|
|
1147
|
+
},
|
|
607
1148
|
/**
|
|
608
1149
|
* #action
|
|
609
1150
|
* toggle the annotation overlay on the alignment
|
|
@@ -651,10 +1192,22 @@ function stateModelFactory() {
|
|
|
651
1192
|
|
|
652
1193
|
/**
|
|
653
1194
|
* #action
|
|
654
|
-
*
|
|
1195
|
+
* switch to another alignment of a multi-alignment file (Stockholm).
|
|
1196
|
+
* Clears the collapsed node ids, the subtree in focus, the reference row
|
|
1197
|
+
* and the scroll position, which all refer to the previous alignment
|
|
655
1198
|
*/
|
|
656
1199
|
setCurrentAlignment(n: number) {
|
|
657
|
-
self.currentAlignment
|
|
1200
|
+
if (n === self.currentAlignment) {
|
|
1201
|
+
return
|
|
1202
|
+
}
|
|
1203
|
+
transaction(() => {
|
|
1204
|
+
self.currentAlignment = n
|
|
1205
|
+
self.collapsed.clear()
|
|
1206
|
+
this.setShowOnly(undefined)
|
|
1207
|
+
this.drawRelativeTo(undefined)
|
|
1208
|
+
self.scrollX = 0
|
|
1209
|
+
self.scrollY = 0
|
|
1210
|
+
})
|
|
658
1211
|
},
|
|
659
1212
|
|
|
660
1213
|
/**
|
|
@@ -740,6 +1293,17 @@ function stateModelFactory() {
|
|
|
740
1293
|
setTreeMetadata(result: string) {
|
|
741
1294
|
self.data.setTreeMetadata(result)
|
|
742
1295
|
},
|
|
1296
|
+
|
|
1297
|
+
/**
|
|
1298
|
+
* #action
|
|
1299
|
+
* store the GFF text in the snapshot like the alignment and tree. The
|
|
1300
|
+
* parsed annotations are volatile and a blob filehandle is cleared once
|
|
1301
|
+
* read, so the text is the only persisted copy. An autorun parses it
|
|
1302
|
+
* into annotations.
|
|
1303
|
+
*/
|
|
1304
|
+
setGFF(result: string) {
|
|
1305
|
+
self.data.setGFF(result)
|
|
1306
|
+
},
|
|
743
1307
|
}))
|
|
744
1308
|
|
|
745
1309
|
.views(self => ({
|
|
@@ -765,6 +1329,20 @@ function stateModelFactory() {
|
|
|
765
1329
|
get actuallyShowDomains() {
|
|
766
1330
|
return self.showDomains && self.annotations.length > 0
|
|
767
1331
|
},
|
|
1332
|
+
/**
|
|
1333
|
+
* #getter
|
|
1334
|
+
* whether the host restores the loaded documents outside the snapshot.
|
|
1335
|
+
* When true, `unshareableData` is empty.
|
|
1336
|
+
*
|
|
1337
|
+
* A simple host sets `hostCarriesData`. A host where this depends on how
|
|
1338
|
+
* the view was opened overrides the getter in its own composed model's
|
|
1339
|
+
* `.views` block; jbrowse-plugin-msaview's indexed-location views refetch
|
|
1340
|
+
* from a URL the session holds, while its data-store views do not.
|
|
1341
|
+
* `unshareableData` reads it off `self`, so an override takes effect.
|
|
1342
|
+
*/
|
|
1343
|
+
get hostRestoresData() {
|
|
1344
|
+
return self.hostCarriesData
|
|
1345
|
+
},
|
|
768
1346
|
get viewInitialized() {
|
|
769
1347
|
return self.volatileWidth !== undefined
|
|
770
1348
|
},
|
|
@@ -779,19 +1357,12 @@ function stateModelFactory() {
|
|
|
779
1357
|
},
|
|
780
1358
|
}))
|
|
781
1359
|
.views(self => ({
|
|
782
|
-
/**
|
|
783
|
-
* #method
|
|
784
|
-
* unused here, but can be used by derived classes to add extra items
|
|
785
|
-
*/
|
|
786
|
-
extraViewMenuItems() {
|
|
787
|
-
return []
|
|
788
|
-
},
|
|
789
1360
|
/**
|
|
790
1361
|
* #getter
|
|
791
1362
|
*/
|
|
792
1363
|
get colorScheme() {
|
|
793
|
-
// colorSchemeName is a free string (menus, snapshots, URL params)
|
|
794
|
-
// back to the default
|
|
1364
|
+
// colorSchemeName is a free string (menus, snapshots, URL params), so a
|
|
1365
|
+
// stale name falls back to the default
|
|
795
1366
|
return (
|
|
796
1367
|
colorSchemes[self.colorSchemeName] ??
|
|
797
1368
|
colorSchemes[defaultColorSchemeName]!
|
|
@@ -820,18 +1391,60 @@ function stateModelFactory() {
|
|
|
820
1391
|
get noDomains() {
|
|
821
1392
|
return self.annotations.length === 0
|
|
822
1393
|
},
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* #getter
|
|
1397
|
+
* loaded documents left out of the snapshot, largest first. A file opened
|
|
1398
|
+
* from disk or pasted in becomes inline text, and DataModel drops an
|
|
1399
|
+
* inline document past `maxInlineSnapshotBytes`.
|
|
1400
|
+
*
|
|
1401
|
+
* The header lists these, and the standalone app stops rewriting the
|
|
1402
|
+
* address bar while the list is non-empty, so a copied link does not
|
|
1403
|
+
* open an empty viewer unannounced. A document fetched from a URL never
|
|
1404
|
+
* appears here, since the snapshot keeps its filehandle.
|
|
1405
|
+
*
|
|
1406
|
+
* Empty when `hostRestoresData` is true.
|
|
1407
|
+
*/
|
|
1408
|
+
get unshareableData(): UnshareableData[] {
|
|
1409
|
+
if (self.hostRestoresData) {
|
|
1410
|
+
return []
|
|
1411
|
+
}
|
|
1412
|
+
const { data } = self
|
|
1413
|
+
// data tracks past the limit also leave the snapshot
|
|
1414
|
+
const trackBytes = columnTrackSizes(self.columnTracks)
|
|
1415
|
+
.filter(bytes => bytes > maxInlineSnapshotBytes)
|
|
1416
|
+
.reduce((a, b) => a + b, 0)
|
|
1417
|
+
return (
|
|
1418
|
+
[
|
|
1419
|
+
['alignment', data.msa, self.msaFilehandle],
|
|
1420
|
+
['tree', data.tree, self.treeFilehandle],
|
|
1421
|
+
['annotations', data.gff, self.gffFilehandle],
|
|
1422
|
+
['row metadata', data.treeMetadata, self.treeMetadataFilehandle],
|
|
1423
|
+
] as const
|
|
1424
|
+
)
|
|
1425
|
+
.flatMap<UnshareableData>(([what, text, filehandle]) =>
|
|
1426
|
+
!filehandle && text && text.length > maxInlineSnapshotBytes
|
|
1427
|
+
? [{ what, bytes: text.length }]
|
|
1428
|
+
: [],
|
|
1429
|
+
)
|
|
1430
|
+
.concat(
|
|
1431
|
+
trackBytes ? [{ what: 'data tracks', bytes: trackBytes }] : [],
|
|
1432
|
+
)
|
|
1433
|
+
.sort((a, b) => b.bytes - a.bytes)
|
|
1434
|
+
},
|
|
823
1435
|
menuItems() {
|
|
824
1436
|
return []
|
|
825
1437
|
},
|
|
826
1438
|
/**
|
|
827
1439
|
* #getter
|
|
828
|
-
* extra per
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
|
|
834
|
-
|
|
1440
|
+
* the row table: extra fields per row, keyed by row name, which the
|
|
1441
|
+
* `encodings` channels read. It is stored as the JSON string
|
|
1442
|
+
* `data.treeMetadata`, the name that travels in existing links, so the
|
|
1443
|
+
* inline size limit and `treeMetadataFilehandle` cover it. labelWidthMap
|
|
1444
|
+
* reads it on every layout, so a malformed user-supplied file returns {}
|
|
1445
|
+
* instead of throwing out of rendering.
|
|
1446
|
+
*/
|
|
1447
|
+
get rowData(): Record<string, Record<string, string> | undefined> {
|
|
835
1448
|
const text = self.data.treeMetadata
|
|
836
1449
|
if (!text) {
|
|
837
1450
|
return {}
|
|
@@ -842,10 +1455,31 @@ function stateModelFactory() {
|
|
|
842
1455
|
? (parsed as Record<string, Record<string, string> | undefined>)
|
|
843
1456
|
: {}
|
|
844
1457
|
} catch (e) {
|
|
845
|
-
console.error('failed to parse
|
|
1458
|
+
console.error('failed to parse rowData', e)
|
|
846
1459
|
return {}
|
|
847
1460
|
}
|
|
848
1461
|
},
|
|
1462
|
+
/**
|
|
1463
|
+
* #method
|
|
1464
|
+
* one row's fields, the single reader of the row table
|
|
1465
|
+
*/
|
|
1466
|
+
rowDataOf(name: string) {
|
|
1467
|
+
return this.rowData[name]
|
|
1468
|
+
},
|
|
1469
|
+
/**
|
|
1470
|
+
* #getter
|
|
1471
|
+
* the field names the row table carries, sorted, for a producer or a UI
|
|
1472
|
+
* choosing one to encode
|
|
1473
|
+
*/
|
|
1474
|
+
get rowFields(): string[] {
|
|
1475
|
+
const fields = new Set<string>()
|
|
1476
|
+
for (const row of Object.values(this.rowData)) {
|
|
1477
|
+
for (const field of Object.keys(row ?? {})) {
|
|
1478
|
+
fields.add(field)
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
return [...fields].sort((a, b) => a.localeCompare(b))
|
|
1482
|
+
},
|
|
849
1483
|
/**
|
|
850
1484
|
* #getter
|
|
851
1485
|
*/
|
|
@@ -895,6 +1529,37 @@ function stateModelFactory() {
|
|
|
895
1529
|
this.leaves.map((leaf, index) => [leaf.data.name, index] as const),
|
|
896
1530
|
)
|
|
897
1531
|
},
|
|
1532
|
+
/**
|
|
1533
|
+
* #getter
|
|
1534
|
+
* `clades` resolved to the rows each one covers. The tip names resolve
|
|
1535
|
+
* against `tree` rather than `root`, so a clade whose ancestor the user
|
|
1536
|
+
* collapsed keeps its rows. One leaf pass over the tree serves every
|
|
1537
|
+
* clade. A `range` record names no node, so `collapse` and `focus`, which
|
|
1538
|
+
* need one, drop it.
|
|
1539
|
+
*/
|
|
1540
|
+
get resolvedClades(): ResolvedClade[] {
|
|
1541
|
+
if (self.clades.length === 0) {
|
|
1542
|
+
return noClades
|
|
1543
|
+
}
|
|
1544
|
+
return resolveClades(
|
|
1545
|
+
self.clades,
|
|
1546
|
+
hierarchy(this.tree, d => d.children),
|
|
1547
|
+
this.rowNamesSet,
|
|
1548
|
+
)
|
|
1549
|
+
},
|
|
1550
|
+
/**
|
|
1551
|
+
* #getter
|
|
1552
|
+
* the pixel column reserved at the right of the tree area for the bracket
|
|
1553
|
+
* mark, which the tip labels and the tree itself stay clear of. Zero
|
|
1554
|
+
* where no clade draws a bar or a label.
|
|
1555
|
+
*/
|
|
1556
|
+
get cladeGutterWidth() {
|
|
1557
|
+
return cladeGutterWidth({
|
|
1558
|
+
clades: this.resolvedClades,
|
|
1559
|
+
rowHeight: self.rowHeight,
|
|
1560
|
+
fontSize: this.fontSize,
|
|
1561
|
+
})
|
|
1562
|
+
},
|
|
898
1563
|
/**
|
|
899
1564
|
* #getter
|
|
900
1565
|
*/
|
|
@@ -928,46 +1593,143 @@ function stateModelFactory() {
|
|
|
928
1593
|
* #getter
|
|
929
1594
|
*/
|
|
930
1595
|
get root() {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1596
|
+
return buildTreeRoot(this.tree, self.collapsed, self.showOnly)
|
|
1597
|
+
},
|
|
1598
|
+
|
|
1599
|
+
/**
|
|
1600
|
+
* #getter
|
|
1601
|
+
* height of the band the tree overview draws in, zero when it is off
|
|
1602
|
+
*/
|
|
1603
|
+
get treeOverviewHeight() {
|
|
1604
|
+
return self.showTreeOverview ? self.overviewHeight : 0
|
|
1605
|
+
},
|
|
1606
|
+
|
|
1607
|
+
/**
|
|
1608
|
+
* #getter
|
|
1609
|
+
* the whole tree laid out for the overview, or undefined when the
|
|
1610
|
+
* overview is off. The focus is left out, so the focused subtree draws
|
|
1611
|
+
* inside the whole tree, and the collapsed clades are folded, since
|
|
1612
|
+
* those are rows the view no longer has. `x` is in tip-index space and
|
|
1613
|
+
* `len` is a fraction of the root-to-tip length, so one layout serves
|
|
1614
|
+
* any band size.
|
|
1615
|
+
*/
|
|
1616
|
+
get treeOverviewLayout() {
|
|
1617
|
+
if (!self.showTreeOverview) {
|
|
1618
|
+
return undefined
|
|
1619
|
+
}
|
|
1620
|
+
const root = buildTreeRoot(this.tree, self.collapsed)
|
|
1621
|
+
const numTips = leaves(root).length
|
|
1622
|
+
clusterLayout(root, numTips, 1)
|
|
1623
|
+
const rootLen = Math.max(root.data.length || 0, 0)
|
|
1624
|
+
const extent = maxLength(root) - rootLen
|
|
1625
|
+
setBrLength(root, -rootLen, extent ? 1 / extent : 0)
|
|
1626
|
+
return {
|
|
1627
|
+
root,
|
|
1628
|
+
numTips,
|
|
1629
|
+
maxDepthToLeaf: calcDepthToLeaf(root),
|
|
1630
|
+
showBranchLen: self.showBranchLen && extent > 0,
|
|
940
1631
|
}
|
|
1632
|
+
},
|
|
941
1633
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1634
|
+
/**
|
|
1635
|
+
* #getter
|
|
1636
|
+
* the `clades` highlights in the overview's own row space, which the
|
|
1637
|
+
* focus does not narrow
|
|
1638
|
+
*/
|
|
1639
|
+
get treeOverviewClades(): ResolvedClade[] {
|
|
1640
|
+
const layout = this.treeOverviewLayout
|
|
1641
|
+
if (!layout || self.clades.length === 0) {
|
|
1642
|
+
return noClades
|
|
1643
|
+
}
|
|
1644
|
+
const rowNames = new Map(
|
|
1645
|
+
leaves(layout.root).map((leaf, index) => [leaf.data.name, index]),
|
|
1646
|
+
)
|
|
1647
|
+
return resolveClades(self.clades, layout.root, rowNames)
|
|
1648
|
+
},
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* #getter
|
|
1652
|
+
* the inclusive tip rows the focused subtree covers in the overview,
|
|
1653
|
+
* which is the box drawn on it. undefined with no focus
|
|
1654
|
+
*/
|
|
1655
|
+
get treeOverviewFocusRows(): [number, number] | undefined {
|
|
1656
|
+
const layout = this.treeOverviewLayout
|
|
1657
|
+
if (!layout || !self.showOnly) {
|
|
1658
|
+
return undefined
|
|
953
1659
|
}
|
|
1660
|
+
const node = find(layout.root, n => n.data.id === self.showOnly)
|
|
1661
|
+
return node ? tipRange(node) : undefined
|
|
1662
|
+
},
|
|
954
1663
|
|
|
955
|
-
|
|
1664
|
+
/**
|
|
1665
|
+
* #method
|
|
1666
|
+
* the subtree a point `y` pixels down the tree overview picks: the
|
|
1667
|
+
* deepest one whose tip range covers every row under that pixel, with
|
|
1668
|
+
* the rows it covers. A pixel stands for several tips on a large tree,
|
|
1669
|
+
* which is what keeps the pick off the individual tips. undefined when
|
|
1670
|
+
* the overview is off or the point picks the whole tree.
|
|
1671
|
+
*/
|
|
1672
|
+
treeOverviewHit(y: number) {
|
|
1673
|
+
const layout = this.treeOverviewLayout
|
|
1674
|
+
if (!layout) {
|
|
1675
|
+
return undefined
|
|
1676
|
+
}
|
|
1677
|
+
const { root, numTips } = layout
|
|
1678
|
+
const perPixel = numTips / self.overviewHeight
|
|
1679
|
+
const first = clamp(Math.floor(y * perPixel), 0, numTips - 1)
|
|
1680
|
+
const last = clamp(
|
|
1681
|
+
Math.ceil((y + 1) * perPixel) - 1,
|
|
1682
|
+
first,
|
|
1683
|
+
numTips - 1,
|
|
1684
|
+
)
|
|
1685
|
+
let node = nodeCoveringRows(root, first + 0.5, last + 0.5)
|
|
1686
|
+
// focusing one tip leaves a single row on screen, so the pick lifts to
|
|
1687
|
+
// the subtree that tip sits in
|
|
1688
|
+
while (!node.children && node.parent) {
|
|
1689
|
+
node = node.parent
|
|
1690
|
+
}
|
|
1691
|
+
return node === root
|
|
1692
|
+
? undefined
|
|
1693
|
+
: { id: node.data.id, rows: tipRange(node) }
|
|
956
1694
|
},
|
|
957
1695
|
|
|
958
1696
|
/**
|
|
959
1697
|
* #getter
|
|
960
|
-
*
|
|
1698
|
+
* the pixel column the row panels occupy between the tree and the
|
|
1699
|
+
* alignment, the sum of each record's width
|
|
1700
|
+
*/
|
|
1701
|
+
get rowPanelsWidth() {
|
|
1702
|
+
return sum(self.rowPanels.map(p => rowPanelWidth(p, self.rowHeight)))
|
|
1703
|
+
},
|
|
1704
|
+
/**
|
|
1705
|
+
* #getter
|
|
1706
|
+
* height of the band the row panel headers draw in, which is zero with
|
|
1707
|
+
* no row panels and leaves the top area as it was
|
|
1708
|
+
*/
|
|
1709
|
+
get rowPanelsHeaderHeight() {
|
|
1710
|
+
return self.rowPanels.length > 0 ? rowPanelHeaderHeight : 0
|
|
1711
|
+
},
|
|
1712
|
+
/**
|
|
1713
|
+
* #getter
|
|
1714
|
+
* widget width minus the tree area and the row panels gives the space
|
|
1715
|
+
* for the MSA
|
|
961
1716
|
*/
|
|
962
1717
|
get msaAreaWidth() {
|
|
963
|
-
return
|
|
1718
|
+
return (
|
|
1719
|
+
self.width -
|
|
1720
|
+
self.treeAreaWidth -
|
|
1721
|
+
this.rowPanelsWidth -
|
|
1722
|
+
self.resizeHandleWidth
|
|
1723
|
+
)
|
|
964
1724
|
},
|
|
965
1725
|
|
|
966
1726
|
/**
|
|
967
1727
|
* #getter
|
|
1728
|
+
* the right edge the tip labels end at, which is the tree area less the
|
|
1729
|
+
* margin and the bracket gutter
|
|
968
1730
|
*/
|
|
969
1731
|
get treeAreaWidthMinusMargin() {
|
|
970
|
-
return self.treeAreaWidth - self.marginLeft
|
|
1732
|
+
return self.treeAreaWidth - self.marginLeft - this.cladeGutterWidth
|
|
971
1733
|
},
|
|
972
1734
|
/**
|
|
973
1735
|
* #getter
|
|
@@ -1037,34 +1799,54 @@ function stateModelFactory() {
|
|
|
1037
1799
|
},
|
|
1038
1800
|
/**
|
|
1039
1801
|
* #getter
|
|
1040
|
-
* number of rows
|
|
1041
|
-
*
|
|
1042
|
-
* row of vertical space (drawn blank), so row hit-testing and fit-to-height
|
|
1043
|
-
* must count it.
|
|
1802
|
+
* number of rows on screen: the leaf count, which includes tree leaves
|
|
1803
|
+
* with no matching MSA row (drawn blank), unlike `rows.length`.
|
|
1044
1804
|
*/
|
|
1045
1805
|
get numRows() {
|
|
1046
1806
|
return this.leaves.length
|
|
1047
1807
|
},
|
|
1048
1808
|
|
|
1049
1809
|
/**
|
|
1050
|
-
* #
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1053
|
-
*
|
|
1054
|
-
*/
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
)
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1810
|
+
* #method
|
|
1811
|
+
* index of the global column holding each ungapped sequence position of a
|
|
1812
|
+
* row. The domain overlay resolves thousands of these per redraw. Built
|
|
1813
|
+
* lazily per row and cached on the parse.
|
|
1814
|
+
*/
|
|
1815
|
+
seqPosIndex(rowName: string): Int32Array | undefined {
|
|
1816
|
+
const MSA = this.MSA
|
|
1817
|
+
if (!MSA) {
|
|
1818
|
+
return undefined
|
|
1819
|
+
}
|
|
1820
|
+
let cache = seqPosIndexCache.get(MSA)
|
|
1821
|
+
if (!cache) {
|
|
1822
|
+
cache = new Map()
|
|
1823
|
+
seqPosIndexCache.set(MSA, cache)
|
|
1824
|
+
}
|
|
1825
|
+
let index = cache.get(rowName)
|
|
1826
|
+
if (!index) {
|
|
1827
|
+
const seq = MSA.getRow(rowName)
|
|
1828
|
+
if (!seq) {
|
|
1829
|
+
return undefined
|
|
1830
|
+
}
|
|
1831
|
+
index = buildSeqPosIndex(seq)
|
|
1832
|
+
cache.set(rowName, index)
|
|
1833
|
+
}
|
|
1834
|
+
return index
|
|
1835
|
+
},
|
|
1836
|
+
|
|
1837
|
+
/**
|
|
1838
|
+
* #getter
|
|
1839
|
+
* every sequence in the alignment, keyed by row name, including rows a
|
|
1840
|
+
* collapsed clade hides. `rows` holds only the rows on screen; lookups
|
|
1841
|
+
* by row name use this
|
|
1065
1842
|
*/
|
|
1066
1843
|
get rowMap() {
|
|
1067
|
-
|
|
1844
|
+
const MSA = this.MSA
|
|
1845
|
+
return new Map(
|
|
1846
|
+
MSA?.getNames()
|
|
1847
|
+
.map(name => [name, MSA.getRow(name)] as const)
|
|
1848
|
+
.filter(([, seq]) => !!seq),
|
|
1849
|
+
)
|
|
1068
1850
|
},
|
|
1069
1851
|
/**
|
|
1070
1852
|
* #getter
|
|
@@ -1201,10 +1983,11 @@ function stateModelFactory() {
|
|
|
1201
1983
|
get hierarchy(): HierarchyNode<NodeWithIdsAndLength> {
|
|
1202
1984
|
const r = this.root
|
|
1203
1985
|
clusterLayout(r, this.totalHeight, self.treeWidth)
|
|
1204
|
-
|
|
1205
|
-
const max = maxLength(r)
|
|
1986
|
+
const max = this.rootToTipLength
|
|
1206
1987
|
const k = max ? self.treeWidth / max : 0
|
|
1207
|
-
|
|
1988
|
+
// the displayed root starts at x=0, so subtract its length here; `root`
|
|
1989
|
+
// returns the cached parse, which must not be mutated
|
|
1990
|
+
setBrLength(r, -Math.max(r.data.length || 0, 0), k)
|
|
1208
1991
|
// for each collapsed clade, record the pixel x-position of its farthest
|
|
1209
1992
|
// tip so the renderer can draw a triangle spanning the branch-length
|
|
1210
1993
|
// extent of the hidden subtree
|
|
@@ -1233,10 +2016,31 @@ function stateModelFactory() {
|
|
|
1233
2016
|
|
|
1234
2017
|
/**
|
|
1235
2018
|
* #getter
|
|
1236
|
-
*
|
|
2019
|
+
* branch-length extent of the displayed tree, root to farthest tip, in
|
|
2020
|
+
* the tree's own units
|
|
2021
|
+
*/
|
|
2022
|
+
get rootToTipLength() {
|
|
2023
|
+
const r = this.root
|
|
2024
|
+
return maxLength(r) - Math.max(r.data.length || 0, 0)
|
|
2025
|
+
},
|
|
2026
|
+
|
|
2027
|
+
/**
|
|
2028
|
+
* #getter
|
|
2029
|
+
* x-position of the farthest tip in a phylogram, px: treeWidth, or 0 for
|
|
2030
|
+
* a tree with no branch lengths (drawn as a cladogram)
|
|
1237
2031
|
*/
|
|
1238
2032
|
get maxBranchLength() {
|
|
1239
|
-
return
|
|
2033
|
+
return this.rootToTipLength ? self.treeWidth : 0
|
|
2034
|
+
},
|
|
2035
|
+
|
|
2036
|
+
/**
|
|
2037
|
+
* #getter
|
|
2038
|
+
* pixels per unit of branch length in the phylogram layout, 0 in
|
|
2039
|
+
* cladogram mode. The tree's scale bar uses it.
|
|
2040
|
+
*/
|
|
2041
|
+
get pxPerBranchLength() {
|
|
2042
|
+
const max = maxLength(this.root)
|
|
2043
|
+
return this.showBranchLenEffective && max ? self.treeWidth / max : 0
|
|
1240
2044
|
},
|
|
1241
2045
|
|
|
1242
2046
|
/**
|
|
@@ -1251,7 +2055,7 @@ function stateModelFactory() {
|
|
|
1251
2055
|
* #getter
|
|
1252
2056
|
*/
|
|
1253
2057
|
get allBranchesLength0() {
|
|
1254
|
-
return
|
|
2058
|
+
return this.rootToTipLength === 0
|
|
1255
2059
|
},
|
|
1256
2060
|
|
|
1257
2061
|
/**
|
|
@@ -1275,170 +2079,899 @@ function stateModelFactory() {
|
|
|
1275
2079
|
/**
|
|
1276
2080
|
* #getter
|
|
1277
2081
|
*/
|
|
1278
|
-
get
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
2082
|
+
get showMsaLetters() {
|
|
2083
|
+
return (
|
|
2084
|
+
self.drawMsaLetters &&
|
|
2085
|
+
self.rowHeight >= minLetterRowHeight &&
|
|
2086
|
+
self.colWidth >= minLetterColWidth &&
|
|
2087
|
+
self.colWidth > self.rowHeight / 2
|
|
2088
|
+
)
|
|
1285
2089
|
},
|
|
1286
2090
|
/**
|
|
1287
2091
|
* #getter
|
|
1288
2092
|
*/
|
|
1289
|
-
get
|
|
1290
|
-
return
|
|
1291
|
-
viewportSize: self.msaAreaWidth,
|
|
1292
|
-
viewportPos: -self.scrollX,
|
|
1293
|
-
blockSize: self.blockSize,
|
|
1294
|
-
mapSize: self.totalWidth,
|
|
1295
|
-
})
|
|
2093
|
+
get showTreeText() {
|
|
2094
|
+
return self.drawLabels && self.rowHeight >= minLetterRowHeight
|
|
1296
2095
|
},
|
|
2096
|
+
}))
|
|
2097
|
+
.views(self => ({
|
|
1297
2098
|
/**
|
|
1298
2099
|
* #getter
|
|
1299
2100
|
*/
|
|
1300
|
-
get
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
2101
|
+
get labelWidthMap() {
|
|
2102
|
+
const { showTreeText, leaves } = self
|
|
2103
|
+
// gated on the renderer's label condition, so hidden labels reserve no
|
|
2104
|
+
// gutter. Measured once at a reference size and scaled by
|
|
2105
|
+
// labelWidthScale: re-measuring per vertical-zoom frame cost ~200ms on a
|
|
2106
|
+
// 50k-leaf tree
|
|
2107
|
+
return showTreeText
|
|
2108
|
+
? new Map(
|
|
2109
|
+
leaves.map(node => {
|
|
2110
|
+
const { name } = node.data
|
|
2111
|
+
// `||`, matching renderTreeLabels: an empty genome falls back
|
|
2112
|
+
// to the row name
|
|
2113
|
+
const displayName = self.rowDataOf(name)?.genome || name
|
|
2114
|
+
return [
|
|
2115
|
+
name,
|
|
2116
|
+
measureTextCanvas(displayName, labelReferenceFontSize),
|
|
2117
|
+
] as const
|
|
2118
|
+
}),
|
|
2119
|
+
)
|
|
2120
|
+
: new Map<string, number>()
|
|
1307
2121
|
},
|
|
2122
|
+
|
|
1308
2123
|
/**
|
|
1309
2124
|
* #getter
|
|
1310
|
-
*
|
|
1311
|
-
*
|
|
2125
|
+
* factor turning a labelWidthMap entry into its width at the current
|
|
2126
|
+
* font size
|
|
1312
2127
|
*/
|
|
1313
|
-
get
|
|
1314
|
-
return
|
|
1315
|
-
self.height -
|
|
1316
|
-
self.headerHeight -
|
|
1317
|
-
(self.msaAreaWidth < self.totalWidth ? self.minimapHeight : 0)
|
|
1318
|
-
)
|
|
2128
|
+
get labelWidthScale() {
|
|
2129
|
+
return self.fontSize / labelReferenceFontSize
|
|
1319
2130
|
},
|
|
1320
|
-
|
|
1321
|
-
|
|
2131
|
+
|
|
2132
|
+
get labelsWidth() {
|
|
2133
|
+
// a loop, not Math.max(...): spreading passes one argument per leaf,
|
|
2134
|
+
// and the ~125k argument limit throws RangeError on the 230k-tip COVID
|
|
2135
|
+
// tree
|
|
2136
|
+
let max = 0
|
|
2137
|
+
for (const width of this.labelWidthMap.values()) {
|
|
2138
|
+
if (width > max) {
|
|
2139
|
+
max = width
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
return max * this.labelWidthScale
|
|
2143
|
+
},
|
|
2144
|
+
|
|
1322
2145
|
/**
|
|
1323
2146
|
* #getter
|
|
1324
2147
|
*/
|
|
1325
|
-
get
|
|
1326
|
-
return self.
|
|
1327
|
-
self.blocksX.map(bx => [bx, by] as const),
|
|
1328
|
-
)
|
|
2148
|
+
get secondaryStructureConsensus() {
|
|
2149
|
+
return self.MSA?.secondaryStructureConsensus
|
|
1329
2150
|
},
|
|
1330
2151
|
|
|
1331
2152
|
/**
|
|
1332
2153
|
* #getter
|
|
1333
2154
|
*/
|
|
1334
|
-
get
|
|
1335
|
-
return self.
|
|
2155
|
+
get seqConsensus() {
|
|
2156
|
+
return self.MSA?.seqConsensus
|
|
1336
2157
|
},
|
|
2158
|
+
|
|
1337
2159
|
/**
|
|
1338
2160
|
* #getter
|
|
2161
|
+
* the base pairs of the consensus secondary structure, as arcs, in
|
|
2162
|
+
* visible column space (hidden columns are removed before parsing)
|
|
1339
2163
|
*/
|
|
1340
|
-
get
|
|
1341
|
-
|
|
2164
|
+
get secondaryStructureArcs(): Arc[] | undefined {
|
|
2165
|
+
const { blanks, hideGapsEffective } = self
|
|
2166
|
+
const ss = this.secondaryStructureConsensus
|
|
2167
|
+
if (!ss) {
|
|
2168
|
+
return undefined
|
|
2169
|
+
}
|
|
2170
|
+
return parseWuss(hideGapsEffective ? skipBlanks(blanks, ss) : ss).map(
|
|
2171
|
+
({ start, end, pseudoknot }) => ({
|
|
2172
|
+
start,
|
|
2173
|
+
end,
|
|
2174
|
+
color: pseudoknot ? PSEUDOKNOT_ARC : HELIX_ARC,
|
|
2175
|
+
}),
|
|
2176
|
+
)
|
|
1342
2177
|
},
|
|
2178
|
+
|
|
1343
2179
|
/**
|
|
1344
2180
|
* #getter
|
|
1345
|
-
* most-negative allowed scrollY, keeping the last row in view rather than
|
|
1346
|
-
* letting the whole alignment scroll off the top.
|
|
1347
2181
|
*/
|
|
1348
|
-
get
|
|
1349
|
-
|
|
2182
|
+
get adapterTrackModels(): BasicTrack[] {
|
|
2183
|
+
const { MSA, hideGapsEffective, blanks } = self
|
|
2184
|
+
const tracks = (MSA?.tracks ?? []).filter(t => !!t.data)
|
|
2185
|
+
if (tracks.length === 0) {
|
|
2186
|
+
// return before reading rowHeight, so zooming does not rebuild the
|
|
2187
|
+
// list and redraw every track canvas
|
|
2188
|
+
return []
|
|
2189
|
+
}
|
|
2190
|
+
const { rowHeight } = self
|
|
2191
|
+
return tracks.map(t => ({
|
|
2192
|
+
model: {
|
|
2193
|
+
...t,
|
|
2194
|
+
kind: 'text' as const,
|
|
2195
|
+
data: hideGapsEffective ? skipBlanks(blanks, t.data!) : t.data,
|
|
2196
|
+
height: rowHeight,
|
|
2197
|
+
},
|
|
2198
|
+
ReactComponent: TrackBlocks,
|
|
2199
|
+
}))
|
|
1350
2200
|
},
|
|
2201
|
+
|
|
1351
2202
|
/**
|
|
1352
2203
|
* #getter
|
|
2204
|
+
* a data track's values or string, projected from its row's residues
|
|
2205
|
+
* onto alignment columns when it names a row
|
|
1353
2206
|
*/
|
|
1354
|
-
get
|
|
2207
|
+
get columnTrackContent() {
|
|
2208
|
+
const { MSA, blanks, hideGapsEffective } = self
|
|
2209
|
+
const width = MSA?.getWidth() ?? 0
|
|
2210
|
+
const project = <T>(track: ColumnTrackSpec, items: T[], fill: T) => {
|
|
2211
|
+
if (!track.row) {
|
|
2212
|
+
return items
|
|
2213
|
+
}
|
|
2214
|
+
const out = Array.from({ length: width }, () => fill)
|
|
2215
|
+
const index = self.seqPosIndex(track.row)
|
|
2216
|
+
items.forEach((item, seqPos) => {
|
|
2217
|
+
const col = index?.[seqPos]
|
|
2218
|
+
if (col !== undefined) {
|
|
2219
|
+
out[col] = item
|
|
2220
|
+
}
|
|
2221
|
+
})
|
|
2222
|
+
return out
|
|
2223
|
+
}
|
|
2224
|
+
const skip = <T>(items: T[]) =>
|
|
2225
|
+
hideGapsEffective ? dropBlanks(blanks, items) : items
|
|
2226
|
+
// an arc endpoint maps row residue -> column -> visible column.
|
|
2227
|
+
// visibleColsBefore, not globalColToVisibleCol, so an endpoint in a
|
|
2228
|
+
// hidden column moves to the neighboring visible one and the arc stays
|
|
2229
|
+
const resolve = (track: ColumnTrackSpec, pos: number) => {
|
|
2230
|
+
const col = track.row
|
|
2231
|
+
? self.seqPosIndex(track.row)?.[pos - 1]
|
|
2232
|
+
: pos - 1
|
|
2233
|
+
if (col === undefined || col < 0 || col >= width) {
|
|
2234
|
+
return undefined
|
|
2235
|
+
}
|
|
2236
|
+
return hideGapsEffective ? visibleColsBefore(blanks, col) : col
|
|
2237
|
+
}
|
|
2238
|
+
return new Map<
|
|
2239
|
+
string,
|
|
2240
|
+
{ values?: number[]; data?: string; arcs?: Arc[] }
|
|
2241
|
+
>(
|
|
2242
|
+
self.columnTracks.map(track => {
|
|
2243
|
+
if (track.kind === 'arc') {
|
|
2244
|
+
const arcs = (track.arcs ?? [])
|
|
2245
|
+
.map(arc => {
|
|
2246
|
+
const start = resolve(track, Math.min(arc.start, arc.end))
|
|
2247
|
+
const end = resolve(track, Math.max(arc.start, arc.end))
|
|
2248
|
+
return start !== undefined && end !== undefined && start < end
|
|
2249
|
+
? { start, end, color: arc.color }
|
|
2250
|
+
: undefined
|
|
2251
|
+
})
|
|
2252
|
+
.filter(notEmpty)
|
|
2253
|
+
return [track.id, { arcs }] as const
|
|
2254
|
+
}
|
|
2255
|
+
if (track.kind === 'bar') {
|
|
2256
|
+
const max = track.max ?? 1
|
|
2257
|
+
const values = skip(project(track, track.values ?? [], 0)).map(
|
|
2258
|
+
v => Math.min(1, Math.max(0, v / max)),
|
|
2259
|
+
)
|
|
2260
|
+
return [track.id, { values }] as const
|
|
2261
|
+
}
|
|
2262
|
+
const data = skip(project(track, (track.data ?? '').split(''), ' '))
|
|
2263
|
+
return [track.id, { data: data.join('') }] as const
|
|
2264
|
+
}),
|
|
2265
|
+
)
|
|
2266
|
+
},
|
|
2267
|
+
/**
|
|
2268
|
+
* #method
|
|
2269
|
+
* the height a track draws at: what the user dragged its divider to,
|
|
2270
|
+
* then the height its snapshot asked for, then its kind's default. Only
|
|
2271
|
+
* a text track falls through to rowHeight, and `??` short-circuits
|
|
2272
|
+
* before reading it, so vertical zoom does not rebuild the other tracks
|
|
2273
|
+
*/
|
|
2274
|
+
trackHeight(kind: TrackKind, heightKey = kind as string, given?: number) {
|
|
1355
2275
|
return (
|
|
1356
|
-
self.
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
self.
|
|
2276
|
+
self.trackHeights.get(heightKey) ??
|
|
2277
|
+
given ??
|
|
2278
|
+
defaultTrackHeights[kind] ??
|
|
2279
|
+
self.rowHeight
|
|
1360
2280
|
)
|
|
1361
2281
|
},
|
|
1362
2282
|
/**
|
|
1363
2283
|
* #getter
|
|
1364
2284
|
*/
|
|
1365
|
-
get
|
|
1366
|
-
return self.
|
|
2285
|
+
get columnTrackModels(): BasicTrack[] {
|
|
2286
|
+
return self.columnTracks.map(track => {
|
|
2287
|
+
const heightKey = resizableKinds.has(track.kind)
|
|
2288
|
+
? ownHeightKey(track.id)
|
|
2289
|
+
: undefined
|
|
2290
|
+
return {
|
|
2291
|
+
model: {
|
|
2292
|
+
id: track.id,
|
|
2293
|
+
name: track.name,
|
|
2294
|
+
kind: track.kind,
|
|
2295
|
+
heightKey,
|
|
2296
|
+
height: this.trackHeight(track.kind, heightKey, track.height),
|
|
2297
|
+
barColor: track.color,
|
|
2298
|
+
arcColor: track.color,
|
|
2299
|
+
customColorScheme: track.colors,
|
|
2300
|
+
data: this.columnTrackContent.get(track.id)?.data,
|
|
2301
|
+
arcs: this.columnTrackContent.get(track.id)?.arcs,
|
|
2302
|
+
},
|
|
2303
|
+
ReactComponent: TrackBlocks,
|
|
2304
|
+
}
|
|
2305
|
+
})
|
|
1367
2306
|
},
|
|
1368
|
-
}))
|
|
1369
|
-
.actions(self => ({
|
|
1370
2307
|
/**
|
|
1371
|
-
* #
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
2308
|
+
* #getter
|
|
2309
|
+
* the consensus secondary structure as a track, when there is one. A
|
|
2310
|
+
* separate getter keeps the object stable across zoom, so its canvas
|
|
2311
|
+
* does not redraw
|
|
2312
|
+
*/
|
|
2313
|
+
get basePairTrackModels(): BasicTrack[] {
|
|
2314
|
+
const arcs = this.secondaryStructureArcs
|
|
2315
|
+
return arcs?.length
|
|
2316
|
+
? [
|
|
2317
|
+
{
|
|
2318
|
+
model: {
|
|
2319
|
+
id: 'base-pairs',
|
|
2320
|
+
name: 'Base pairs',
|
|
2321
|
+
kind: 'arc' as const,
|
|
2322
|
+
heightKey: 'arc',
|
|
2323
|
+
height: this.trackHeight('arc'),
|
|
2324
|
+
arcs,
|
|
2325
|
+
},
|
|
2326
|
+
ReactComponent: TrackBlocks,
|
|
2327
|
+
},
|
|
2328
|
+
]
|
|
2329
|
+
: []
|
|
1375
2330
|
},
|
|
1376
2331
|
|
|
1377
2332
|
/**
|
|
1378
|
-
* #
|
|
2333
|
+
* #getter
|
|
2334
|
+
* the tracks computed from the alignment; they depend on their heights
|
|
2335
|
+
* and the alphabet, not on zoom
|
|
1379
2336
|
*/
|
|
1380
|
-
|
|
1381
|
-
|
|
2337
|
+
get computedTrackModels(): BasicTrack[] {
|
|
2338
|
+
return [
|
|
2339
|
+
{
|
|
2340
|
+
id: 'conservation',
|
|
2341
|
+
name: 'Conservation',
|
|
2342
|
+
kind: 'bar' as const,
|
|
2343
|
+
barColor: 'gray',
|
|
2344
|
+
},
|
|
2345
|
+
...(self.sequenceType === 'amino'
|
|
2346
|
+
? [
|
|
2347
|
+
{
|
|
2348
|
+
id: 'property-conservation',
|
|
2349
|
+
name: 'Property conservation',
|
|
2350
|
+
kind: 'bar' as const,
|
|
2351
|
+
barColor: '#6a51a3',
|
|
2352
|
+
},
|
|
2353
|
+
]
|
|
2354
|
+
: []),
|
|
2355
|
+
{
|
|
2356
|
+
id: 'sequence-logo',
|
|
2357
|
+
name: 'Sequence logo',
|
|
2358
|
+
kind: 'logo' as const,
|
|
2359
|
+
},
|
|
2360
|
+
// last, so it sits against the alignment it numbers
|
|
2361
|
+
{
|
|
2362
|
+
id: 'position-ruler',
|
|
2363
|
+
name: 'Position',
|
|
2364
|
+
kind: 'ruler' as const,
|
|
2365
|
+
},
|
|
2366
|
+
].map(model => ({
|
|
2367
|
+
// every computed track of a kind shares that kind's height, so the
|
|
2368
|
+
// kind is its key
|
|
2369
|
+
model: {
|
|
2370
|
+
...model,
|
|
2371
|
+
heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
|
|
2372
|
+
height: this.trackHeight(model.kind),
|
|
2373
|
+
},
|
|
2374
|
+
ReactComponent: TrackBlocks,
|
|
2375
|
+
}))
|
|
1382
2376
|
},
|
|
1383
2377
|
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
if (nodeId === self.hoveredTreeNode?.nodeId) {
|
|
1394
|
-
return
|
|
1395
|
-
}
|
|
1396
|
-
if (!nodeId) {
|
|
1397
|
-
self.hoveredTreeNode = undefined
|
|
1398
|
-
return
|
|
1399
|
-
}
|
|
1400
|
-
const node = find(self.hierarchy, n => n.data.id === nodeId)
|
|
1401
|
-
self.hoveredTreeNode = node
|
|
1402
|
-
? {
|
|
1403
|
-
nodeId,
|
|
1404
|
-
descendantNames: leaves(node).map(leaf => leaf.data.name),
|
|
1405
|
-
}
|
|
1406
|
-
: undefined
|
|
2378
|
+
get tracks(): BasicTrack[] {
|
|
2379
|
+
return [
|
|
2380
|
+
...this.adapterTrackModels,
|
|
2381
|
+
...this.basePairTrackModels,
|
|
2382
|
+
...this.columnTrackModels,
|
|
2383
|
+
// every computed track reads the alignment's columns, and a tree, a
|
|
2384
|
+
// GFF and a features panel draw a figure with none
|
|
2385
|
+
...(self.numColumns > 0 ? this.computedTrackModels : []),
|
|
2386
|
+
]
|
|
1407
2387
|
},
|
|
1408
2388
|
|
|
1409
2389
|
/**
|
|
1410
|
-
* #
|
|
1411
|
-
* Calculate a neighbor joining tree from the current MSA using BLOSUM62 distances
|
|
2390
|
+
* #getter
|
|
1412
2391
|
*/
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
self.setTree(newickTree)
|
|
2392
|
+
get turnedOnTracks() {
|
|
2393
|
+
return this.tracks.filter(
|
|
2394
|
+
f =>
|
|
2395
|
+
!trackIsOff(self.turnedOffTracks, f.model.id, f.model.defaultOff),
|
|
2396
|
+
)
|
|
1419
2397
|
},
|
|
1420
2398
|
|
|
1421
2399
|
/**
|
|
1422
|
-
* #
|
|
1423
|
-
* restore the default column width and row height
|
|
2400
|
+
* #getter
|
|
1424
2401
|
*/
|
|
1425
|
-
|
|
1426
|
-
self.
|
|
1427
|
-
self.setRowHeight(defaultRowHeight)
|
|
2402
|
+
get showHorizontalScrollbar() {
|
|
2403
|
+
return overflows(self.totalWidth, self.msaAreaWidth)
|
|
1428
2404
|
},
|
|
2405
|
+
|
|
1429
2406
|
/**
|
|
1430
|
-
* #
|
|
2407
|
+
* #method
|
|
2408
|
+
* Return a row-specific letter at a visible column, or undefined if gap.
|
|
2409
|
+
*
|
|
2410
|
+
* @param rowName - The name of the row
|
|
2411
|
+
* @param visibleCol - The visible column index (what the user sees on screen)
|
|
2412
|
+
* @returns The letter at that position, or undefined if it's a gap
|
|
1431
2413
|
*/
|
|
1432
|
-
|
|
1433
|
-
self.
|
|
1434
|
-
|
|
2414
|
+
visibleColToRowLetter(rowName: string, visibleCol: number) {
|
|
2415
|
+
return self.rowMap.get(rowName)?.[
|
|
2416
|
+
this.visibleColToGlobalCol(visibleCol)
|
|
2417
|
+
]
|
|
1435
2418
|
},
|
|
2419
|
+
|
|
1436
2420
|
/**
|
|
1437
|
-
* #
|
|
2421
|
+
* #method
|
|
2422
|
+
* Convert a visible column to a row-specific sequence position (0-based).
|
|
2423
|
+
* Returns undefined if the position is a gap in the sequence.
|
|
2424
|
+
*
|
|
2425
|
+
* Public API, like the sibling converters (visibleColToGlobalCol,
|
|
2426
|
+
* seqPosToVisibleCol, globalColToVisibleCol, seqPosToGlobalCol) hosts
|
|
2427
|
+
* use to translate between columns and residue positions. Keep them
|
|
2428
|
+
* stable.
|
|
2429
|
+
*
|
|
2430
|
+
* @param rowName - The name of the row
|
|
2431
|
+
* @param visibleCol - The visible column index
|
|
2432
|
+
* @returns The sequence position (0-based), or undefined if it's a gap
|
|
1438
2433
|
*/
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
2434
|
+
visibleColToSeqPos(rowName: string, visibleCol: number) {
|
|
2435
|
+
// binary search: this runs on every mouse move, and scanning a
|
|
2436
|
+
// 30k-column row per event takes the whole frame
|
|
2437
|
+
return seqPosOfGlobalCol(
|
|
2438
|
+
self.seqPosIndex(rowName),
|
|
2439
|
+
this.visibleColToGlobalCol(visibleCol),
|
|
2440
|
+
)
|
|
2441
|
+
},
|
|
2442
|
+
|
|
2443
|
+
/**
|
|
2444
|
+
* #method
|
|
2445
|
+
* Convert a visible column to a row-specific sequence position (1-based).
|
|
2446
|
+
* Returns undefined if the position is a gap in the sequence.
|
|
2447
|
+
*
|
|
2448
|
+
* @param rowName - The name of the row
|
|
2449
|
+
* @param visibleCol - The visible column index
|
|
2450
|
+
* @returns The sequence position (1-based), or undefined if it's a gap
|
|
2451
|
+
*/
|
|
2452
|
+
visibleColToSeqPosOneBased(rowName: string, visibleCol: number) {
|
|
2453
|
+
const val = this.visibleColToSeqPos(rowName, visibleCol)
|
|
2454
|
+
return val !== undefined ? val + 1 : undefined
|
|
2455
|
+
},
|
|
2456
|
+
|
|
2457
|
+
/**
|
|
2458
|
+
* #method
|
|
2459
|
+
* Convert a global column index to a visible column index.
|
|
2460
|
+
* Returns undefined if the column is hidden (in blanks).
|
|
2461
|
+
* This is the inverse of visibleColToGlobalCol.
|
|
2462
|
+
*
|
|
2463
|
+
* @param globalCol - The global column index in the full MSA
|
|
2464
|
+
* @returns The visible column index, or undefined if the column is hidden
|
|
2465
|
+
*/
|
|
2466
|
+
globalColToVisibleCol(globalCol: number) {
|
|
2467
|
+
const { blanks, hideGapsEffective } = self
|
|
2468
|
+
if (!hideGapsEffective) {
|
|
2469
|
+
return globalCol
|
|
2470
|
+
}
|
|
2471
|
+
return globalColToVisibleCol(blanks, globalCol)
|
|
2472
|
+
},
|
|
2473
|
+
|
|
2474
|
+
/**
|
|
2475
|
+
* #method
|
|
2476
|
+
* Convert a visible column index (what a mouse handler reports) to a
|
|
2477
|
+
* column of the full alignment. A host indexing its own per-column data
|
|
2478
|
+
* needs this when columns are hidden.
|
|
2479
|
+
*
|
|
2480
|
+
* @param visibleCol - The visible column index
|
|
2481
|
+
* @returns The global column index in the full MSA
|
|
2482
|
+
*/
|
|
2483
|
+
visibleColToGlobalCol(visibleCol: number) {
|
|
2484
|
+
const { blanks, hideGapsEffective } = self
|
|
2485
|
+
return hideGapsEffective
|
|
2486
|
+
? visibleColToGlobalCol(blanks, visibleCol)
|
|
2487
|
+
: visibleCol
|
|
2488
|
+
},
|
|
2489
|
+
|
|
2490
|
+
/**
|
|
2491
|
+
* #method
|
|
2492
|
+
* Convert a sequence position (ungapped) to a global column index.
|
|
2493
|
+
* Returns undefined for a row name the alignment does not have.
|
|
2494
|
+
*
|
|
2495
|
+
* @param rowName - The name of the row
|
|
2496
|
+
* @param seqPos - The sequence position (0-based, ungapped)
|
|
2497
|
+
* @returns The global column index in the full MSA, or undefined
|
|
2498
|
+
*/
|
|
2499
|
+
seqPosToGlobalCol(rowName: string, seqPos: number) {
|
|
2500
|
+
const seq = self.rowMap.get(rowName)
|
|
2501
|
+
if (seq === undefined) {
|
|
2502
|
+
return undefined
|
|
2503
|
+
}
|
|
2504
|
+
const col = self.seqPosIndex(rowName)?.[seqPos]
|
|
2505
|
+
// past the end of the ungapped sequence: 0 for the degenerate all-gap
|
|
2506
|
+
// row, otherwise one past the last column
|
|
2507
|
+
return col ?? (seqPos === 0 ? 0 : seq.length)
|
|
2508
|
+
},
|
|
2509
|
+
|
|
2510
|
+
/**
|
|
2511
|
+
* #method
|
|
2512
|
+
* Convert a sequence position (ungapped) directly to a visible column index.
|
|
2513
|
+
* This combines seqPosToGlobalCol and globalColToVisibleCol.
|
|
2514
|
+
*
|
|
2515
|
+
* @param rowName - The name of the row
|
|
2516
|
+
* @param seqPos - The sequence position (0-based, ungapped)
|
|
2517
|
+
* @returns The visible column index, or undefined if the column is hidden
|
|
2518
|
+
*/
|
|
2519
|
+
seqPosToVisibleCol(rowName: string, seqPos: number) {
|
|
2520
|
+
const globalCol = this.seqPosToGlobalCol(rowName, seqPos)
|
|
2521
|
+
return globalCol === undefined
|
|
2522
|
+
? undefined
|
|
2523
|
+
: this.globalColToVisibleCol(globalCol)
|
|
2524
|
+
},
|
|
2525
|
+
|
|
2526
|
+
/**
|
|
2527
|
+
* #method
|
|
2528
|
+
* the visible columns a span covers, in highlight coordinates: `start`
|
|
2529
|
+
* and `end` are 1-based residues of `row`, or columns of the file
|
|
2530
|
+
* without it. A fractional position, as a zoom gesture reports one,
|
|
2531
|
+
* widens to the whole residue or column it falls in. A span entirely on
|
|
2532
|
+
* hidden columns, or naming a row the alignment lacks, gives undefined.
|
|
2533
|
+
*/
|
|
2534
|
+
visibleSpan({ row, start: rawStart, end: rawEnd }: Region) {
|
|
2535
|
+
const { blanks } = self
|
|
2536
|
+
const start = Math.max(1, Math.floor(rawStart))
|
|
2537
|
+
const end = Math.ceil(rawEnd)
|
|
2538
|
+
let startGlobal = start - 1
|
|
2539
|
+
let endGlobal = end - 1
|
|
2540
|
+
if (row !== undefined) {
|
|
2541
|
+
const rowStart = this.seqPosToGlobalCol(row, start - 1)
|
|
2542
|
+
const rowEnd = this.seqPosToGlobalCol(row, end - 1)
|
|
2543
|
+
if (rowStart === undefined || rowEnd === undefined) {
|
|
2544
|
+
return undefined
|
|
2545
|
+
}
|
|
2546
|
+
startGlobal = rowStart
|
|
2547
|
+
endGlobal = rowEnd
|
|
2548
|
+
}
|
|
2549
|
+
const startCol =
|
|
2550
|
+
this.globalColToVisibleCol(startGlobal) ??
|
|
2551
|
+
visibleColsBefore(blanks, startGlobal)
|
|
2552
|
+
const endCol =
|
|
2553
|
+
this.globalColToVisibleCol(endGlobal) ??
|
|
2554
|
+
visibleColsBefore(blanks, endGlobal) - 1
|
|
2555
|
+
return startCol <= endCol ? { startCol, endCol } : undefined
|
|
2556
|
+
},
|
|
2557
|
+
|
|
2558
|
+
/**
|
|
2559
|
+
* #getter
|
|
2560
|
+
* why each ignored residue mapping is ignored, so a host can tell a
|
|
2561
|
+
* missing structure from a mapping made against a different alignment.
|
|
2562
|
+
*/
|
|
2563
|
+
get residueMappingProblems(): ResidueMappingProblem[] {
|
|
2564
|
+
const problems: ResidueMappingProblem[] = []
|
|
2565
|
+
for (const mapping of self.residueMappings) {
|
|
2566
|
+
const where = { row: mapping.row, structureId: mapping.structure.id }
|
|
2567
|
+
const residues = self.seqPosIndex(mapping.row)?.length
|
|
2568
|
+
if (residues === undefined) {
|
|
2569
|
+
problems.push({
|
|
2570
|
+
...where,
|
|
2571
|
+
scope: 'mapping',
|
|
2572
|
+
reason: 'no such row in the alignment',
|
|
2573
|
+
})
|
|
2574
|
+
continue
|
|
2575
|
+
}
|
|
2576
|
+
if (
|
|
2577
|
+
mapping.rowLength !== undefined &&
|
|
2578
|
+
mapping.rowLength !== residues
|
|
2579
|
+
) {
|
|
2580
|
+
problems.push({
|
|
2581
|
+
...where,
|
|
2582
|
+
scope: 'mapping',
|
|
2583
|
+
reason: `computed against a ${mapping.rowLength}-residue row; this one has ${residues}`,
|
|
2584
|
+
})
|
|
2585
|
+
continue
|
|
2586
|
+
}
|
|
2587
|
+
const overrun = mapping.segments.find(
|
|
2588
|
+
segment => segment.rowEnd > residues || segment.rowStart < 1,
|
|
2589
|
+
)
|
|
2590
|
+
if (overrun) {
|
|
2591
|
+
problems.push({
|
|
2592
|
+
...where,
|
|
2593
|
+
scope: 'mapping',
|
|
2594
|
+
reason: `segment ${overrun.rowStart}-${overrun.rowEnd} does not fit a ${residues}-residue row`,
|
|
2595
|
+
})
|
|
2596
|
+
continue
|
|
2597
|
+
}
|
|
2598
|
+
for (const segment of mapping.segments) {
|
|
2599
|
+
if (!sameLength(segment)) {
|
|
2600
|
+
problems.push({
|
|
2601
|
+
...where,
|
|
2602
|
+
scope: 'segment',
|
|
2603
|
+
reason: `segment ${segment.rowStart}-${segment.rowEnd} maps to ${segment.structStart}-${segment.structEnd}, which is a different length`,
|
|
2604
|
+
})
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
return problems
|
|
2609
|
+
},
|
|
2610
|
+
|
|
2611
|
+
/**
|
|
2612
|
+
* #getter
|
|
2613
|
+
* the mappings that fit the loaded alignment. A row-level problem drops
|
|
2614
|
+
* the whole mapping; a malformed segment drops only that segment.
|
|
2615
|
+
*/
|
|
2616
|
+
get usableResidueMappings(): ResidueMapping[] {
|
|
2617
|
+
const unusable = new Set(
|
|
2618
|
+
this.residueMappingProblems
|
|
2619
|
+
.filter(problem => problem.scope === 'mapping')
|
|
2620
|
+
.map(problem => `${problem.row}\u0000${problem.structureId}`),
|
|
2621
|
+
)
|
|
2622
|
+
return self.residueMappings
|
|
2623
|
+
.filter(m => !unusable.has(`${m.row}\u0000${m.structure.id}`))
|
|
2624
|
+
.map(m => ({
|
|
2625
|
+
...m,
|
|
2626
|
+
segments: m.segments.filter(sameLength),
|
|
2627
|
+
}))
|
|
2628
|
+
},
|
|
2629
|
+
|
|
2630
|
+
/**
|
|
2631
|
+
* #getter
|
|
2632
|
+
* the structures with usable mappings. A row can map onto several, such
|
|
2633
|
+
* as an experimental entry and a predicted model.
|
|
2634
|
+
*/
|
|
2635
|
+
get mappedStructures() {
|
|
2636
|
+
return this.usableResidueMappings.map(m => ({
|
|
2637
|
+
row: m.row,
|
|
2638
|
+
structure: m.structure,
|
|
2639
|
+
}))
|
|
2640
|
+
},
|
|
2641
|
+
|
|
2642
|
+
/**
|
|
2643
|
+
* #method
|
|
2644
|
+
* The structure residue for a row residue. Returns undefined when no
|
|
2645
|
+
* segment covers `seqPos`, or when the row maps onto several structures
|
|
2646
|
+
* and `structureId` does not pick one (see `mappedStructures`).
|
|
2647
|
+
*
|
|
2648
|
+
* Positions are 1-based, like `residueMappings` and `highlights`; the
|
|
2649
|
+
* column helpers above are 0-based.
|
|
2650
|
+
*
|
|
2651
|
+
* @param rowName - The alignment row
|
|
2652
|
+
* @param seqPos - Residue of that row, 1-based
|
|
2653
|
+
* @param structureId - Which structure, when the row maps onto several
|
|
2654
|
+
*/
|
|
2655
|
+
structureResidue(
|
|
2656
|
+
rowName: string,
|
|
2657
|
+
seqPos: number,
|
|
2658
|
+
structureId?: string,
|
|
2659
|
+
): StructureResidue | undefined {
|
|
2660
|
+
const hits: StructureResidue[] = []
|
|
2661
|
+
for (const mapping of this.usableResidueMappings) {
|
|
2662
|
+
if (mapping.row !== rowName) {
|
|
2663
|
+
continue
|
|
2664
|
+
}
|
|
2665
|
+
if (
|
|
2666
|
+
structureId !== undefined &&
|
|
2667
|
+
mapping.structure.id !== structureId
|
|
2668
|
+
) {
|
|
2669
|
+
continue
|
|
2670
|
+
}
|
|
2671
|
+
const segment = mapping.segments.find(
|
|
2672
|
+
seg => seqPos >= seg.rowStart && seqPos <= seg.rowEnd,
|
|
2673
|
+
)
|
|
2674
|
+
if (segment) {
|
|
2675
|
+
const position = segment.structStart + (seqPos - segment.rowStart)
|
|
2676
|
+
hits.push({
|
|
2677
|
+
structure: mapping.structure,
|
|
2678
|
+
position,
|
|
2679
|
+
observed: !inRanges(mapping.unobserved, position),
|
|
2680
|
+
})
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2683
|
+
return hits.length === 1 ? hits[0] : undefined
|
|
2684
|
+
},
|
|
2685
|
+
|
|
2686
|
+
/**
|
|
2687
|
+
* #method
|
|
2688
|
+
* The row residue for a structure residue; the inverse of
|
|
2689
|
+
* `structureResidue`, returning undefined in the same cases. `asymId`
|
|
2690
|
+
* picks a chain when several mappings share an entry id, as in a
|
|
2691
|
+
* homodimer.
|
|
2692
|
+
*
|
|
2693
|
+
* @param structureId - The structure's id, as the mapping names it
|
|
2694
|
+
* @param position - Residue of that structure, 1-based label_seq_id
|
|
2695
|
+
* @param asymId - Which chain, when the id alone is ambiguous
|
|
2696
|
+
*/
|
|
2697
|
+
rowResidue(
|
|
2698
|
+
structureId: string,
|
|
2699
|
+
position: number,
|
|
2700
|
+
asymId?: string,
|
|
2701
|
+
): RowResidue | undefined {
|
|
2702
|
+
const hits: RowResidue[] = []
|
|
2703
|
+
for (const mapping of this.usableResidueMappings) {
|
|
2704
|
+
if (mapping.structure.id !== structureId) {
|
|
2705
|
+
continue
|
|
2706
|
+
}
|
|
2707
|
+
if (asymId !== undefined && mapping.structure.asymId !== asymId) {
|
|
2708
|
+
continue
|
|
2709
|
+
}
|
|
2710
|
+
const segment = mapping.segments.find(
|
|
2711
|
+
seg => position >= seg.structStart && position <= seg.structEnd,
|
|
2712
|
+
)
|
|
2713
|
+
if (segment) {
|
|
2714
|
+
hits.push({
|
|
2715
|
+
rowName: mapping.row,
|
|
2716
|
+
seqPos: segment.rowStart + (position - segment.structStart),
|
|
2717
|
+
})
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
return hits.length === 1 ? hits[0] : undefined
|
|
2721
|
+
},
|
|
2722
|
+
}))
|
|
2723
|
+
|
|
2724
|
+
.views(self => ({
|
|
2725
|
+
/**
|
|
2726
|
+
* #getter
|
|
2727
|
+
* the branch-length scale bar over the tree, undefined in cladogram mode
|
|
2728
|
+
* or when the tree area is too narrow for one
|
|
2729
|
+
*/
|
|
2730
|
+
get treeScaleBar() {
|
|
2731
|
+
return scaleBarLength(
|
|
2732
|
+
self.pxPerBranchLength,
|
|
2733
|
+
self.treeAreaWidth - self.marginLeft * 2,
|
|
2734
|
+
)
|
|
2735
|
+
},
|
|
2736
|
+
/**
|
|
2737
|
+
* #getter
|
|
2738
|
+
* the band across the top: the minimap over the alignment, the row panel
|
|
2739
|
+
* headers, and the tree overview stacked on the scale bar, as tall as the
|
|
2740
|
+
* tallest of the three
|
|
2741
|
+
*/
|
|
2742
|
+
get topBandHeight() {
|
|
2743
|
+
return Math.max(
|
|
2744
|
+
self.showHorizontalScrollbar ? self.minimapHeight : 0,
|
|
2745
|
+
self.rowPanelsHeaderHeight,
|
|
2746
|
+
self.treeOverviewHeight +
|
|
2747
|
+
(this.treeScaleBar ? treeScaleBarHeight : 0),
|
|
2748
|
+
)
|
|
2749
|
+
},
|
|
2750
|
+
/**
|
|
2751
|
+
* #getter
|
|
2752
|
+
* the vertical space for alignment rows: the widget height less the
|
|
2753
|
+
* header, the top band and the tracks. Shared by blocksY, maxScrollY, the
|
|
2754
|
+
* vertical scrollbar and fitVertically.
|
|
2755
|
+
*/
|
|
2756
|
+
get msaAreaHeight() {
|
|
2757
|
+
return (
|
|
2758
|
+
self.height -
|
|
2759
|
+
this.topBandHeight -
|
|
2760
|
+
self.headerHeight -
|
|
2761
|
+
this.totalTrackAreaHeight
|
|
2762
|
+
)
|
|
2763
|
+
},
|
|
2764
|
+
/**
|
|
2765
|
+
* #getter
|
|
2766
|
+
* total height of track area (px)
|
|
2767
|
+
*/
|
|
2768
|
+
get totalTrackAreaHeight() {
|
|
2769
|
+
return sum(self.turnedOnTracks.map(r => r.model.height))
|
|
2770
|
+
},
|
|
2771
|
+
/**
|
|
2772
|
+
* one representative annotation per accession, for the legend, the
|
|
2773
|
+
* filter dialog and the palettes
|
|
2774
|
+
*/
|
|
2775
|
+
get annotationTypes() {
|
|
2776
|
+
// first occurrence wins; only name, description and segment start are
|
|
2777
|
+
// read from it
|
|
2778
|
+
const types = new Map<string, Annotation>()
|
|
2779
|
+
for (const annot of self.annotations) {
|
|
2780
|
+
if (!types.has(annot.accession)) {
|
|
2781
|
+
types.set(annot.accession, annot)
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
return types
|
|
2785
|
+
},
|
|
2786
|
+
get filteredAnnotations() {
|
|
2787
|
+
return self.annotations.filter(
|
|
2788
|
+
r => !self.turnedOffFeatures.get(r.accession),
|
|
2789
|
+
)
|
|
2790
|
+
},
|
|
2791
|
+
get annotationsByRow() {
|
|
2792
|
+
return groupBy(this.filteredAnnotations, r => r.id)
|
|
2793
|
+
},
|
|
2794
|
+
}))
|
|
2795
|
+
.views(self => ({
|
|
2796
|
+
/**
|
|
2797
|
+
* #getter
|
|
2798
|
+
*/
|
|
2799
|
+
get showVerticalScrollbar() {
|
|
2800
|
+
return overflows(self.totalHeight, self.msaAreaHeight)
|
|
2801
|
+
},
|
|
2802
|
+
}))
|
|
2803
|
+
.views(self => ({
|
|
2804
|
+
/**
|
|
2805
|
+
* #getter
|
|
2806
|
+
*/
|
|
2807
|
+
get dataInitialized() {
|
|
2808
|
+
// truthiness, not `!== ''`: DataModel drops an inline document over
|
|
2809
|
+
// 50kb from the snapshot, so a restored session can hold `undefined`
|
|
2810
|
+
return !!(self.data.msa || self.data.tree) && !self.error
|
|
2811
|
+
},
|
|
2812
|
+
/**
|
|
2813
|
+
* #getter
|
|
2814
|
+
*/
|
|
2815
|
+
get blocksX() {
|
|
2816
|
+
return calculateBlocks({
|
|
2817
|
+
viewportSize: self.msaAreaWidth,
|
|
2818
|
+
viewportPos: -self.scrollX,
|
|
2819
|
+
blockSize: self.blockSize,
|
|
2820
|
+
mapSize: self.totalWidth,
|
|
2821
|
+
})
|
|
2822
|
+
},
|
|
2823
|
+
/**
|
|
2824
|
+
* #getter
|
|
2825
|
+
*/
|
|
2826
|
+
get blocksY() {
|
|
2827
|
+
return calculateBlocks({
|
|
2828
|
+
viewportSize: self.msaAreaHeight,
|
|
2829
|
+
viewportPos: -self.scrollY,
|
|
2830
|
+
blockSize: self.blockSize,
|
|
2831
|
+
mapSize: self.totalHeight,
|
|
2832
|
+
})
|
|
2833
|
+
},
|
|
2834
|
+
}))
|
|
2835
|
+
.views(self => ({
|
|
2836
|
+
/**
|
|
2837
|
+
* #getter
|
|
2838
|
+
*/
|
|
2839
|
+
get blocks2d() {
|
|
2840
|
+
return self.blocksY.flatMap(by =>
|
|
2841
|
+
self.blocksX.map(bx => [bx, by] as const),
|
|
2842
|
+
)
|
|
2843
|
+
},
|
|
2844
|
+
|
|
2845
|
+
/**
|
|
2846
|
+
* #getter
|
|
2847
|
+
*/
|
|
2848
|
+
get isLoading() {
|
|
2849
|
+
return self.loadingMSA || self.loadingTree
|
|
2850
|
+
},
|
|
2851
|
+
/**
|
|
2852
|
+
* #getter
|
|
2853
|
+
*/
|
|
2854
|
+
get maxScrollX() {
|
|
2855
|
+
return Math.min(-self.totalWidth + (self.msaAreaWidth - 100), 0)
|
|
2856
|
+
},
|
|
2857
|
+
/**
|
|
2858
|
+
* #getter
|
|
2859
|
+
* most-negative allowed scrollY, which keeps the last row in view
|
|
2860
|
+
*/
|
|
2861
|
+
get maxScrollY() {
|
|
2862
|
+
return Math.min(-self.totalHeight + self.msaAreaHeight, 0)
|
|
2863
|
+
},
|
|
2864
|
+
/**
|
|
2865
|
+
* #getter
|
|
2866
|
+
* axis a wheel zoom scales, for ctrl+wheel as much as for scroll-zoom.
|
|
2867
|
+
* With scroll-zoom off the toolbar shows no axis, so ctrl+wheel takes
|
|
2868
|
+
* both.
|
|
2869
|
+
*/
|
|
2870
|
+
get wheelZoomAxis(): ScrollZoomAxis {
|
|
2871
|
+
return self.scrollZoom ? self.scrollZoomAxis : 'both'
|
|
2872
|
+
},
|
|
2873
|
+
}))
|
|
2874
|
+
.actions(self => ({
|
|
2875
|
+
/**
|
|
2876
|
+
* #action
|
|
2877
|
+
*/
|
|
2878
|
+
setDrawMsaLetters(arg: boolean) {
|
|
2879
|
+
self.drawMsaLetters = arg
|
|
2880
|
+
},
|
|
2881
|
+
|
|
2882
|
+
/**
|
|
2883
|
+
* #action
|
|
2884
|
+
*/
|
|
2885
|
+
setScrollZoom(arg: boolean) {
|
|
2886
|
+
self.scrollZoom = arg
|
|
2887
|
+
},
|
|
2888
|
+
|
|
2889
|
+
/**
|
|
2890
|
+
* #action
|
|
2891
|
+
*/
|
|
2892
|
+
setScrollZoomAxis(arg: ScrollZoomAxis) {
|
|
2893
|
+
self.scrollZoomAxis = arg
|
|
2894
|
+
},
|
|
2895
|
+
|
|
2896
|
+
/**
|
|
2897
|
+
* #action
|
|
2898
|
+
* set hovered tree node and its descendants
|
|
2899
|
+
*/
|
|
2900
|
+
setHoveredTreeNode(nodeId?: string) {
|
|
2901
|
+
// called on every tree mousemove; `find` walks the whole hierarchy and a
|
|
2902
|
+
// new object redraws the tree and MSA overlays
|
|
2903
|
+
if (nodeId === self.hoveredTreeNode?.nodeId) {
|
|
2904
|
+
return
|
|
2905
|
+
}
|
|
2906
|
+
if (!nodeId) {
|
|
2907
|
+
self.hoveredTreeNode = undefined
|
|
2908
|
+
return
|
|
2909
|
+
}
|
|
2910
|
+
const node = find(self.hierarchy, n => n.data.id === nodeId)
|
|
2911
|
+
self.hoveredTreeNode = node
|
|
2912
|
+
? {
|
|
2913
|
+
nodeId,
|
|
2914
|
+
descendantNames: leaves(node).map(leaf => leaf.data.name),
|
|
2915
|
+
}
|
|
2916
|
+
: undefined
|
|
2917
|
+
},
|
|
2918
|
+
|
|
2919
|
+
/**
|
|
2920
|
+
* #action
|
|
2921
|
+
* Calculate a neighbor joining tree from the current MSA using BLOSUM62
|
|
2922
|
+
* distances. Throws above `maxNeighborJoiningRows`: the join loop is
|
|
2923
|
+
* cubic and runs on the main thread, and 800 rows freeze the tab for ten
|
|
2924
|
+
* seconds with no cancel.
|
|
2925
|
+
*/
|
|
2926
|
+
calculateNeighborJoiningTreeFromMSA() {
|
|
2927
|
+
// every sequence, including rows in collapsed clades
|
|
2928
|
+
const rows = [...self.rowMap]
|
|
2929
|
+
if (rows.length < 2) {
|
|
2930
|
+
throw new Error('Need at least 2 sequences to build a tree')
|
|
2931
|
+
}
|
|
2932
|
+
if (rows.length > maxNeighborJoiningRows) {
|
|
2933
|
+
throw new Error(
|
|
2934
|
+
`Neighbor joining here is capped at ${maxNeighborJoiningRows} sequences and this alignment has ${rows.length}. Build the tree with FastTree or IQ-TREE and open it alongside the alignment: https://gmod.org/JBrowseMSA/tutorials/protein_family`,
|
|
2935
|
+
)
|
|
2936
|
+
}
|
|
2937
|
+
this.replaceTree(calculateNeighborJoiningTree(rows))
|
|
2938
|
+
},
|
|
2939
|
+
|
|
2940
|
+
/**
|
|
2941
|
+
* #action
|
|
2942
|
+
* swap in a different tree over the same alignment. Clears `collapsed`
|
|
2943
|
+
* and `showOnly`, since path-derived node ids (node-0-0-1) from the old
|
|
2944
|
+
* tree would match unrelated nodes in the new one.
|
|
2945
|
+
*/
|
|
2946
|
+
replaceTree(newick: string) {
|
|
2947
|
+
transaction(() => {
|
|
2948
|
+
self.collapsed.clear()
|
|
2949
|
+
self.setShowOnly(undefined)
|
|
2950
|
+
self.setTree(newick)
|
|
2951
|
+
})
|
|
2952
|
+
},
|
|
2953
|
+
|
|
2954
|
+
/**
|
|
2955
|
+
* #action
|
|
2956
|
+
* restore the default column width and row height
|
|
2957
|
+
*/
|
|
2958
|
+
resetZoom() {
|
|
2959
|
+
self.setColWidth(defaultColWidth)
|
|
2960
|
+
self.setRowHeight(defaultRowHeight)
|
|
2961
|
+
},
|
|
2962
|
+
/**
|
|
2963
|
+
* #action
|
|
2964
|
+
*/
|
|
2965
|
+
zoomOutHorizontal() {
|
|
2966
|
+
self.colWidth = Math.max(minColWidth, Math.floor(self.colWidth * 0.75))
|
|
2967
|
+
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
2968
|
+
},
|
|
2969
|
+
/**
|
|
2970
|
+
* #action
|
|
2971
|
+
*/
|
|
2972
|
+
zoomInHorizontal() {
|
|
2973
|
+
self.colWidth = Math.min(maxCellSize, Math.ceil(self.colWidth * 1.5))
|
|
2974
|
+
self.scrollX = clamp(self.scrollX, self.maxScrollX, 0)
|
|
1442
2975
|
},
|
|
1443
2976
|
/**
|
|
1444
2977
|
* #action
|
|
@@ -1477,26 +3010,36 @@ function stateModelFactory() {
|
|
|
1477
3010
|
* #action
|
|
1478
3011
|
* Smoothly zoom by a continuous scaleFactor. The column under the cursor
|
|
1479
3012
|
* (offsetX/offsetY, px relative to the MSA area) stays anchored
|
|
1480
|
-
* horizontally. Vertically the anchor is biased toward
|
|
1481
|
-
* alignment nearly fits the viewport,
|
|
1482
|
-
*
|
|
1483
|
-
*
|
|
1484
|
-
*
|
|
1485
|
-
|
|
1486
|
-
|
|
3013
|
+
* horizontally. Vertically the anchor is biased toward y=0 when the
|
|
3014
|
+
* alignment nearly fits the viewport, fading to cursor-anchoring as the
|
|
3015
|
+
* alignment grows taller than the viewport.
|
|
3016
|
+
* Drives wheel/trackpad-pinch zoom. `axis` holds one cell dimension
|
|
3017
|
+
* fixed; the held axis still re-anchors its scroll offset, since the
|
|
3018
|
+
* other one can change how much of the alignment fits.
|
|
3019
|
+
*/
|
|
3020
|
+
zoomToPos(
|
|
3021
|
+
scaleFactor: number,
|
|
3022
|
+
offsetX: number,
|
|
3023
|
+
offsetY: number,
|
|
3024
|
+
axis: ScrollZoomAxis = 'both',
|
|
3025
|
+
) {
|
|
1487
3026
|
transaction(() => {
|
|
1488
3027
|
const colInView = (-self.scrollX + offsetX) / self.colWidth
|
|
1489
3028
|
const rowInView = (-self.scrollY + offsetY) / self.rowHeight
|
|
1490
|
-
|
|
1491
|
-
self.colWidth
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
3029
|
+
if (axis !== 'vertical') {
|
|
3030
|
+
self.colWidth = clamp(
|
|
3031
|
+
self.colWidth * scaleFactor,
|
|
3032
|
+
minColWidth,
|
|
3033
|
+
maxCellSize,
|
|
3034
|
+
)
|
|
3035
|
+
}
|
|
3036
|
+
if (axis !== 'horizontal') {
|
|
3037
|
+
self.rowHeight = clamp(
|
|
3038
|
+
self.rowHeight * scaleFactor,
|
|
3039
|
+
minRowHeight,
|
|
3040
|
+
maxCellSize,
|
|
3041
|
+
)
|
|
3042
|
+
}
|
|
1500
3043
|
self.scrollX = clamp(
|
|
1501
3044
|
offsetX - colInView * self.colWidth,
|
|
1502
3045
|
self.maxScrollX,
|
|
@@ -1504,9 +3047,7 @@ function stateModelFactory() {
|
|
|
1504
3047
|
)
|
|
1505
3048
|
|
|
1506
3049
|
const anchoredScrollY = offsetY - rowInView * self.rowHeight
|
|
1507
|
-
// maxScrollY is
|
|
1508
|
-
// overflows, so -maxScrollY is exactly that overflow past the
|
|
1509
|
-
// scrollable MSA viewport (0 when it fits)
|
|
3050
|
+
// -maxScrollY is the overflow past the viewport, 0 when it fits
|
|
1510
3051
|
const overflow = Math.max(0, -self.maxScrollY)
|
|
1511
3052
|
const visibleHeight = self.totalHeight - overflow
|
|
1512
3053
|
const topBias =
|
|
@@ -1535,13 +3076,14 @@ function stateModelFactory() {
|
|
|
1535
3076
|
|
|
1536
3077
|
/**
|
|
1537
3078
|
* #action
|
|
1538
|
-
* Set the overlay annotations
|
|
1539
|
-
*
|
|
1540
|
-
*
|
|
3079
|
+
* Set the overlay annotations (an empty list clears them). InterProScan,
|
|
3080
|
+
* GFF, user uploads and NCBI CDD all arrive here as Annotation[].
|
|
3081
|
+
*
|
|
3082
|
+
* Leaves `showDomains` alone, because a restored snapshot reloads its
|
|
3083
|
+
* GFF and must keep a hidden overlay hidden.
|
|
1541
3084
|
*/
|
|
1542
3085
|
setAnnotations(annotations: Annotation[]) {
|
|
1543
3086
|
self.annotations = annotations
|
|
1544
|
-
self.setShowDomains(annotations.length > 0)
|
|
1545
3087
|
},
|
|
1546
3088
|
|
|
1547
3089
|
/**
|
|
@@ -1566,382 +3108,62 @@ function stateModelFactory() {
|
|
|
1566
3108
|
},
|
|
1567
3109
|
|
|
1568
3110
|
/**
|
|
1569
|
-
* #action
|
|
1570
|
-
*/
|
|
1571
|
-
setScrollX(n: number) {
|
|
1572
|
-
self.scrollX = clamp(n, self.maxScrollX, 0)
|
|
1573
|
-
},
|
|
1574
|
-
|
|
1575
|
-
/**
|
|
1576
|
-
* #action
|
|
1577
|
-
*/
|
|
1578
|
-
setColumnTracks(tracks: ColumnTrackSpec[]) {
|
|
1579
|
-
self.columnTracks.replace(tracks)
|
|
1580
|
-
},
|
|
1581
|
-
/**
|
|
1582
|
-
* #action
|
|
1583
|
-
*/
|
|
1584
|
-
toggleTrack(id: string) {
|
|
1585
|
-
// the stored value is "is off", so the current shown state is exactly
|
|
1586
|
-
// what the flipped entry should hold
|
|
1587
|
-
self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id))
|
|
1588
|
-
},
|
|
1589
|
-
/**
|
|
1590
|
-
* #action
|
|
1591
|
-
*/
|
|
1592
|
-
setStatus(status?: { msg: string; url?: string; onCancel?: () => void }) {
|
|
1593
|
-
self.status = status
|
|
1594
|
-
},
|
|
1595
|
-
}))
|
|
1596
|
-
.views(self => ({
|
|
1597
|
-
/**
|
|
1598
|
-
* #getter
|
|
1599
|
-
*/
|
|
1600
|
-
get labelWidthMap() {
|
|
1601
|
-
const { showTreeText, leaves, treeMetadata } = self
|
|
1602
|
-
// gated on the same condition the renderer draws labels under, so the
|
|
1603
|
-
// gutter labelsWidth reserves and the labels actually drawn cannot
|
|
1604
|
-
// disagree -- and so turning labels off hands their space to the tree.
|
|
1605
|
-
// Measured at a fixed reference size and scaled by labelWidthScale:
|
|
1606
|
-
// re-measuring every leaf on every vertical-zoom frame cost ~200ms on a
|
|
1607
|
-
// 50k-leaf tree
|
|
1608
|
-
return showTreeText
|
|
1609
|
-
? new Map(
|
|
1610
|
-
leaves.map(node => {
|
|
1611
|
-
const { name } = node.data
|
|
1612
|
-
// `||`, matching renderTreeLabels: an empty genome falls back
|
|
1613
|
-
// to the row name, and measuring '' would size the gutter (and
|
|
1614
|
-
// the label's click target) to nothing
|
|
1615
|
-
const displayName = treeMetadata[name]?.genome || name
|
|
1616
|
-
return [
|
|
1617
|
-
name,
|
|
1618
|
-
measureTextCanvas(displayName, labelReferenceFontSize),
|
|
1619
|
-
] as const
|
|
1620
|
-
}),
|
|
1621
|
-
)
|
|
1622
|
-
: new Map<string, number>()
|
|
1623
|
-
},
|
|
1624
|
-
|
|
1625
|
-
/**
|
|
1626
|
-
* #getter
|
|
1627
|
-
* factor turning a labelWidthMap entry into its width at the current
|
|
1628
|
-
* font size
|
|
1629
|
-
*/
|
|
1630
|
-
get labelWidthScale() {
|
|
1631
|
-
return self.fontSize / labelReferenceFontSize
|
|
1632
|
-
},
|
|
1633
|
-
|
|
1634
|
-
get labelsWidth() {
|
|
1635
|
-
// a loop, not Math.max(...widths.values()): spreading a map of every
|
|
1636
|
-
// leaf passes one argument per row, and the argument limit is somewhere
|
|
1637
|
-
// around 125k -- so the bundled 230k-tip COVID tree threw
|
|
1638
|
-
// "RangeError: Maximum call stack size exceeded" out of a getter the
|
|
1639
|
-
// treeWidth autorun reads on load
|
|
1640
|
-
let max = 0
|
|
1641
|
-
for (const width of this.labelWidthMap.values()) {
|
|
1642
|
-
if (width > max) {
|
|
1643
|
-
max = width
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1646
|
-
return max * this.labelWidthScale
|
|
1647
|
-
},
|
|
1648
|
-
|
|
1649
|
-
/**
|
|
1650
|
-
* #getter
|
|
1651
|
-
*/
|
|
1652
|
-
get secondaryStructureConsensus() {
|
|
1653
|
-
return self.MSA?.secondaryStructureConsensus
|
|
1654
|
-
},
|
|
1655
|
-
|
|
1656
|
-
/**
|
|
1657
|
-
* #getter
|
|
1658
|
-
*/
|
|
1659
|
-
get seqConsensus() {
|
|
1660
|
-
return self.MSA?.seqConsensus
|
|
1661
|
-
},
|
|
1662
|
-
|
|
1663
|
-
/**
|
|
1664
|
-
* #getter
|
|
1665
|
-
*/
|
|
1666
|
-
get adapterTrackModels(): BasicTrack[] {
|
|
1667
|
-
const { rowHeight, MSA, hideGapsEffective, blanks } = self
|
|
1668
|
-
const tracks = MSA?.tracks ?? []
|
|
1669
|
-
return tracks
|
|
1670
|
-
.filter(t => !!t.data)
|
|
1671
|
-
.map(t => ({
|
|
1672
|
-
model: {
|
|
1673
|
-
...t,
|
|
1674
|
-
kind: 'text' as const,
|
|
1675
|
-
data: hideGapsEffective ? skipBlanks(blanks, t.data!) : t.data,
|
|
1676
|
-
height: rowHeight,
|
|
1677
|
-
},
|
|
1678
|
-
ReactComponent: TrackBlocks,
|
|
1679
|
-
}))
|
|
1680
|
-
},
|
|
1681
|
-
|
|
1682
|
-
/**
|
|
1683
|
-
* #getter
|
|
1684
|
-
* a data track's values or string, projected from its row's residues
|
|
1685
|
-
* onto alignment columns when it names a row
|
|
1686
|
-
*/
|
|
1687
|
-
get columnTrackContent() {
|
|
1688
|
-
const { MSA, blanks, hideGapsEffective } = self
|
|
1689
|
-
const width = MSA?.getWidth() ?? 0
|
|
1690
|
-
const project = <T>(track: ColumnTrackSpec, items: T[], fill: T) => {
|
|
1691
|
-
if (!track.row) {
|
|
1692
|
-
return items
|
|
1693
|
-
}
|
|
1694
|
-
const out = Array.from({ length: width }, () => fill)
|
|
1695
|
-
const index = self.seqPosGlobalColIndex.get(track.row)
|
|
1696
|
-
items.forEach((item, seqPos) => {
|
|
1697
|
-
const col = index?.[seqPos]
|
|
1698
|
-
if (col !== undefined) {
|
|
1699
|
-
out[col] = item
|
|
1700
|
-
}
|
|
1701
|
-
})
|
|
1702
|
-
return out
|
|
1703
|
-
}
|
|
1704
|
-
const skip = <T>(items: T[]) =>
|
|
1705
|
-
hideGapsEffective ? dropBlanks(blanks, items) : items
|
|
1706
|
-
return new Map<string, { values?: number[]; data?: string }>(
|
|
1707
|
-
self.columnTracks.map(track => {
|
|
1708
|
-
if (track.kind === 'bar') {
|
|
1709
|
-
const max = track.max ?? 1
|
|
1710
|
-
const values = skip(project(track, track.values ?? [], 0)).map(
|
|
1711
|
-
v => Math.min(1, Math.max(0, v / max)),
|
|
1712
|
-
)
|
|
1713
|
-
return [track.id, { values }] as const
|
|
1714
|
-
}
|
|
1715
|
-
const data = skip(project(track, (track.data ?? '').split(''), ' '))
|
|
1716
|
-
return [track.id, { data: data.join('') }] as const
|
|
1717
|
-
}),
|
|
1718
|
-
)
|
|
1719
|
-
},
|
|
1720
|
-
/**
|
|
1721
|
-
* #getter
|
|
1722
|
-
*/
|
|
1723
|
-
get columnTrackModels(): BasicTrack[] {
|
|
1724
|
-
return self.columnTracks.map(track => ({
|
|
1725
|
-
model: {
|
|
1726
|
-
id: track.id,
|
|
1727
|
-
name: track.name,
|
|
1728
|
-
kind: track.kind,
|
|
1729
|
-
height:
|
|
1730
|
-
track.height ??
|
|
1731
|
-
(track.kind === 'bar'
|
|
1732
|
-
? self.conservationTrackHeight
|
|
1733
|
-
: self.rowHeight),
|
|
1734
|
-
barColor: track.color,
|
|
1735
|
-
customColorScheme: track.colors,
|
|
1736
|
-
data: this.columnTrackContent.get(track.id)?.data,
|
|
1737
|
-
},
|
|
1738
|
-
ReactComponent: TrackBlocks,
|
|
1739
|
-
}))
|
|
1740
|
-
},
|
|
1741
|
-
/**
|
|
1742
|
-
* #getter
|
|
1743
|
-
*/
|
|
1744
|
-
get tracks(): BasicTrack[] {
|
|
1745
|
-
const conservationTrack = {
|
|
1746
|
-
id: 'conservation',
|
|
1747
|
-
name: 'Conservation',
|
|
1748
|
-
kind: 'bar' as const,
|
|
1749
|
-
height: self.conservationTrackHeight,
|
|
1750
|
-
barColor: 'gray',
|
|
1751
|
-
}
|
|
1752
|
-
const propertyConservationTrack = {
|
|
1753
|
-
id: 'property-conservation',
|
|
1754
|
-
name: 'Property conservation',
|
|
1755
|
-
kind: 'bar' as const,
|
|
1756
|
-
height: self.conservationTrackHeight,
|
|
1757
|
-
barColor: '#6a51a3',
|
|
1758
|
-
}
|
|
1759
|
-
const sequenceLogoTrack = {
|
|
1760
|
-
id: 'sequence-logo',
|
|
1761
|
-
name: 'Sequence logo',
|
|
1762
|
-
kind: 'logo' as const,
|
|
1763
|
-
height: self.sequenceLogoTrackHeight,
|
|
1764
|
-
}
|
|
1765
|
-
return [
|
|
1766
|
-
...this.adapterTrackModels,
|
|
1767
|
-
...this.columnTrackModels,
|
|
1768
|
-
...[
|
|
1769
|
-
conservationTrack,
|
|
1770
|
-
...(self.sequenceType === 'amino'
|
|
1771
|
-
? [propertyConservationTrack]
|
|
1772
|
-
: []),
|
|
1773
|
-
sequenceLogoTrack,
|
|
1774
|
-
].map(model => ({ model, ReactComponent: TrackBlocks })),
|
|
1775
|
-
]
|
|
1776
|
-
},
|
|
1777
|
-
|
|
1778
|
-
/**
|
|
1779
|
-
* #getter
|
|
1780
|
-
*/
|
|
1781
|
-
get turnedOnTracks() {
|
|
1782
|
-
return this.tracks.filter(
|
|
1783
|
-
f => !trackIsOff(self.turnedOffTracks, f.model.id),
|
|
1784
|
-
)
|
|
1785
|
-
},
|
|
1786
|
-
|
|
1787
|
-
/**
|
|
1788
|
-
* #getter
|
|
1789
|
-
*/
|
|
1790
|
-
get showHorizontalScrollbar() {
|
|
1791
|
-
return self.msaAreaWidth < self.totalWidth
|
|
1792
|
-
},
|
|
1793
|
-
|
|
1794
|
-
/**
|
|
1795
|
-
* #method
|
|
1796
|
-
* Return a row-specific letter at a visible column, or undefined if gap.
|
|
1797
|
-
*
|
|
1798
|
-
* @param rowName - The name of the row
|
|
1799
|
-
* @param visibleCol - The visible column index (what the user sees on screen)
|
|
1800
|
-
* @returns The letter at that position, or undefined if it's a gap
|
|
1801
|
-
*/
|
|
1802
|
-
visibleColToRowLetter(rowName: string, visibleCol: number) {
|
|
1803
|
-
const { rowMap, blanks } = self
|
|
1804
|
-
return rowMap.get(rowName)?.[visibleColToGlobalCol(blanks, visibleCol)]
|
|
1805
|
-
},
|
|
1806
|
-
|
|
1807
|
-
/**
|
|
1808
|
-
* #method
|
|
1809
|
-
* Convert a visible column to a row-specific sequence position (0-based).
|
|
1810
|
-
* Returns undefined if the position is a gap in the sequence.
|
|
1811
|
-
*
|
|
1812
|
-
* CROSS-REPO CONTRACT: this and the sibling coordinate converters
|
|
1813
|
-
* (seqPosToVisibleCol, globalColToVisibleCol, seqPosToGlobalCol) are used
|
|
1814
|
-
* by jbrowse-plugin-protein3d to translate between alignment columns and
|
|
1815
|
-
* structure/sequence residue positions across gaps. Keep them stable.
|
|
1816
|
-
*
|
|
1817
|
-
* @param rowName - The name of the row
|
|
1818
|
-
* @param visibleCol - The visible column index
|
|
1819
|
-
* @returns The sequence position (0-based), or undefined if it's a gap
|
|
1820
|
-
*/
|
|
1821
|
-
visibleColToSeqPos(rowName: string, visibleCol: number) {
|
|
1822
|
-
return visibleColToSeqPosForRow({
|
|
1823
|
-
rowName,
|
|
1824
|
-
visibleCol,
|
|
1825
|
-
rowMap: self.rowMap,
|
|
1826
|
-
blanks: self.blanks,
|
|
1827
|
-
})
|
|
1828
|
-
},
|
|
1829
|
-
|
|
1830
|
-
/**
|
|
1831
|
-
* #method
|
|
1832
|
-
* Convert a visible column to a row-specific sequence position (1-based).
|
|
1833
|
-
* Returns undefined if the position is a gap in the sequence.
|
|
1834
|
-
*
|
|
1835
|
-
* @param rowName - The name of the row
|
|
1836
|
-
* @param visibleCol - The visible column index
|
|
1837
|
-
* @returns The sequence position (1-based), or undefined if it's a gap
|
|
1838
|
-
*/
|
|
1839
|
-
visibleColToSeqPosOneBased(rowName: string, visibleCol: number) {
|
|
1840
|
-
const val = this.visibleColToSeqPos(rowName, visibleCol)
|
|
1841
|
-
return val !== undefined ? val + 1 : undefined
|
|
1842
|
-
},
|
|
1843
|
-
|
|
1844
|
-
/**
|
|
1845
|
-
* #method
|
|
1846
|
-
* Convert a global column index to a visible column index.
|
|
1847
|
-
* Returns undefined if the column is hidden (in blanks).
|
|
1848
|
-
* This is the inverse of visibleColToGlobalCol.
|
|
1849
|
-
*
|
|
1850
|
-
* @param globalCol - The global column index in the full MSA
|
|
1851
|
-
* @returns The visible column index, or undefined if the column is hidden
|
|
1852
|
-
*/
|
|
1853
|
-
globalColToVisibleCol(globalCol: number) {
|
|
1854
|
-
const { blanks, hideGapsEffective } = self
|
|
1855
|
-
if (!hideGapsEffective) {
|
|
1856
|
-
return globalCol
|
|
1857
|
-
}
|
|
1858
|
-
return globalColToVisibleCol(blanks, globalCol)
|
|
3111
|
+
* #action
|
|
3112
|
+
*/
|
|
3113
|
+
setScrollX(n: number) {
|
|
3114
|
+
self.scrollX = clamp(n, self.maxScrollX, 0)
|
|
1859
3115
|
},
|
|
1860
3116
|
|
|
1861
3117
|
/**
|
|
1862
|
-
* #
|
|
1863
|
-
* Convert a sequence position (ungapped) to a global column index.
|
|
1864
|
-
*
|
|
1865
|
-
* @param rowName - The name of the row
|
|
1866
|
-
* @param seqPos - The sequence position (0-based, ungapped)
|
|
1867
|
-
* @returns The global column index in the full MSA
|
|
3118
|
+
* #action
|
|
1868
3119
|
*/
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
if (seq === undefined) {
|
|
1872
|
-
return 0
|
|
1873
|
-
}
|
|
1874
|
-
const col = self.seqPosGlobalColIndex.get(rowName)?.[seqPos]
|
|
1875
|
-
// past the end of the ungapped sequence: 0 for the degenerate all-gap
|
|
1876
|
-
// row, otherwise one past the last column
|
|
1877
|
-
return col ?? (seqPos === 0 ? 0 : seq.length)
|
|
3120
|
+
setColumnTracks(tracks: ColumnTrackSpec[]) {
|
|
3121
|
+
self.columnTracks.replace(tracks)
|
|
1878
3122
|
},
|
|
1879
|
-
|
|
1880
3123
|
/**
|
|
1881
|
-
* #
|
|
1882
|
-
*
|
|
1883
|
-
*
|
|
1884
|
-
*
|
|
1885
|
-
* @param rowName - The name of the row
|
|
1886
|
-
* @param seqPos - The sequence position (0-based, ungapped)
|
|
1887
|
-
* @returns The visible column index, or undefined if the column is hidden
|
|
3124
|
+
* #action
|
|
3125
|
+
* replace the row table, which the model keeps as the JSON string
|
|
3126
|
+
* `data.treeMetadata` (see docs/layers.md)
|
|
1888
3127
|
*/
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
return this.globalColToVisibleCol(globalCol)
|
|
3128
|
+
setRowData(rowData: Record<string, Record<string, string>>) {
|
|
3129
|
+
self.data.setTreeMetadata(JSON.stringify(rowData))
|
|
1892
3130
|
},
|
|
1893
|
-
}))
|
|
1894
|
-
|
|
1895
|
-
.views(self => ({
|
|
1896
3131
|
/**
|
|
1897
|
-
* #
|
|
1898
|
-
*
|
|
3132
|
+
* #action
|
|
3133
|
+
* replace what the viewer's marks read from the row table
|
|
1899
3134
|
*/
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
self.height -
|
|
1903
|
-
(self.showHorizontalScrollbar ? self.minimapHeight : 0) -
|
|
1904
|
-
self.headerHeight
|
|
1905
|
-
)
|
|
3135
|
+
setEncodings(encodings: Encoding[]) {
|
|
3136
|
+
self.encodings.replace(encodings)
|
|
1906
3137
|
},
|
|
1907
3138
|
/**
|
|
1908
|
-
* #
|
|
1909
|
-
*
|
|
3139
|
+
* #action
|
|
3140
|
+
* replace the panels drawn between the tree and the alignment
|
|
1910
3141
|
*/
|
|
1911
|
-
|
|
1912
|
-
|
|
3142
|
+
setRowPanels(panels: RowPanelSpec[]) {
|
|
3143
|
+
self.rowPanels.replace(panels)
|
|
1913
3144
|
},
|
|
1914
3145
|
/**
|
|
1915
|
-
*
|
|
1916
|
-
* the
|
|
3146
|
+
* #action
|
|
3147
|
+
* replace the alignment<->structure correspondence (see docs/layers.md)
|
|
1917
3148
|
*/
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
// description and -- for ordinal segments -- the start that orders
|
|
1921
|
-
// them, and those agree across an accession's instances
|
|
1922
|
-
const types = new Map<string, Annotation>()
|
|
1923
|
-
for (const annot of self.annotations) {
|
|
1924
|
-
if (!types.has(annot.accession)) {
|
|
1925
|
-
types.set(annot.accession, annot)
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
return types
|
|
3149
|
+
setResidueMappings(mappings: ResidueMapping[]) {
|
|
3150
|
+
self.residueMappings.replace(mappings)
|
|
1929
3151
|
},
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
3152
|
+
/**
|
|
3153
|
+
* #action
|
|
3154
|
+
*/
|
|
3155
|
+
toggleTrack(id: string) {
|
|
3156
|
+
const defaultOff = self.MSA?.tracks.find(t => t.id === id)?.defaultOff
|
|
3157
|
+
self.turnedOffTracks.set(
|
|
3158
|
+
id,
|
|
3159
|
+
!trackIsOff(self.turnedOffTracks, id, defaultOff),
|
|
1933
3160
|
)
|
|
1934
3161
|
},
|
|
1935
|
-
get annotationsByRow() {
|
|
1936
|
-
return groupBy(this.filteredAnnotations, r => r.id)
|
|
1937
|
-
},
|
|
1938
|
-
}))
|
|
1939
|
-
.views(self => ({
|
|
1940
3162
|
/**
|
|
1941
|
-
* #
|
|
3163
|
+
* #action
|
|
1942
3164
|
*/
|
|
1943
|
-
|
|
1944
|
-
|
|
3165
|
+
setStatus(status?: { msg: string; onCancel?: () => void }) {
|
|
3166
|
+
self.status = status
|
|
1945
3167
|
},
|
|
1946
3168
|
}))
|
|
1947
3169
|
.views(self => ({
|
|
@@ -1953,18 +3175,78 @@ function stateModelFactory() {
|
|
|
1953
3175
|
},
|
|
1954
3176
|
/**
|
|
1955
3177
|
* #getter
|
|
1956
|
-
* width of the alignment canvas
|
|
1957
|
-
* scrollbar
|
|
1958
|
-
*
|
|
3178
|
+
* width of the alignment canvas: the msa area less the vertical
|
|
3179
|
+
* scrollbar. showHorizontalScrollbar must not read it, since that feeds
|
|
3180
|
+
* msaAreaHeight -> showVerticalScrollbar and would form a cycle
|
|
1959
3181
|
*/
|
|
1960
3182
|
get msaCanvasWidth() {
|
|
1961
3183
|
return self.msaAreaWidth - this.verticalScrollbarWidth
|
|
1962
3184
|
},
|
|
3185
|
+
/**
|
|
3186
|
+
* #method
|
|
3187
|
+
* the cell at a visible column and row index, in the coordinates a host
|
|
3188
|
+
* writes highlights in
|
|
3189
|
+
*/
|
|
3190
|
+
cellAt(visibleCol: number, rowIndex?: number): Cell {
|
|
3191
|
+
const column = self.visibleColToGlobalCol(visibleCol)
|
|
3192
|
+
const row =
|
|
3193
|
+
rowIndex === undefined ? undefined : self.leaves[rowIndex]?.data.name
|
|
3194
|
+
return row === undefined
|
|
3195
|
+
? { column: column + 1 }
|
|
3196
|
+
: {
|
|
3197
|
+
column: column + 1,
|
|
3198
|
+
row,
|
|
3199
|
+
residue: self.visibleColToSeqPosOneBased(row, visibleCol),
|
|
3200
|
+
letter: self.rowMap.get(row)?.[column],
|
|
3201
|
+
}
|
|
3202
|
+
},
|
|
3203
|
+
/**
|
|
3204
|
+
* #getter
|
|
3205
|
+
* the cell under the pointer. Public API: MSAViewer's onCellHover
|
|
3206
|
+
* reports it.
|
|
3207
|
+
*/
|
|
3208
|
+
get hoveredCell() {
|
|
3209
|
+
const { mouseCol, mouseRow } = self
|
|
3210
|
+
return mouseCol === undefined
|
|
3211
|
+
? undefined
|
|
3212
|
+
: this.cellAt(mouseCol, mouseRow)
|
|
3213
|
+
},
|
|
3214
|
+
/**
|
|
3215
|
+
* #getter
|
|
3216
|
+
* the cell a click pinned. Public API: MSAViewer's onCellClick reports it.
|
|
3217
|
+
*/
|
|
3218
|
+
get clickedCell() {
|
|
3219
|
+
const { mouseClickCol, mouseClickRow } = self
|
|
3220
|
+
return mouseClickCol === undefined
|
|
3221
|
+
? undefined
|
|
3222
|
+
: this.cellAt(mouseClickCol, mouseClickRow)
|
|
3223
|
+
},
|
|
3224
|
+
/**
|
|
3225
|
+
* #getter
|
|
3226
|
+
* the columns on screen. Public API: MSAViewer's onViewportChange reports
|
|
3227
|
+
* it.
|
|
3228
|
+
*/
|
|
3229
|
+
get viewport(): Viewport | undefined {
|
|
3230
|
+
const { scrollX, colWidth, numColumns, viewInitialized } = self
|
|
3231
|
+
if (numColumns === 0 || !viewInitialized) {
|
|
3232
|
+
return undefined
|
|
3233
|
+
}
|
|
3234
|
+
const { xStart, xEnd } = visibleColRange({
|
|
3235
|
+
offsetX: -scrollX,
|
|
3236
|
+
blockWidth: this.msaCanvasWidth,
|
|
3237
|
+
colWidth,
|
|
3238
|
+
})
|
|
3239
|
+
const last = Math.max(0, Math.min(xEnd, numColumns) - 1)
|
|
3240
|
+
return {
|
|
3241
|
+
startColumn: self.visibleColToGlobalCol(Math.min(xStart, last)) + 1,
|
|
3242
|
+
endColumn: self.visibleColToGlobalCol(last) + 1,
|
|
3243
|
+
}
|
|
3244
|
+
},
|
|
1963
3245
|
/**
|
|
1964
3246
|
* #getter
|
|
1965
3247
|
* ordinal segment types (exons etc.), ordered by sequence position so
|
|
1966
|
-
* exon-1..exon-14
|
|
1967
|
-
* labeled by number
|
|
3248
|
+
* exon-1..exon-14 run left-to-right; colored by alternating shade and
|
|
3249
|
+
* labeled by number, with no legend row
|
|
1968
3250
|
*/
|
|
1969
3251
|
get segmentDomainTypes() {
|
|
1970
3252
|
return [...self.annotationTypes.values()]
|
|
@@ -1996,10 +3278,48 @@ function stateModelFactory() {
|
|
|
1996
3278
|
get strokePalette() {
|
|
1997
3279
|
return transform(this.fillPalette, ([key, val]) => [
|
|
1998
3280
|
key,
|
|
1999
|
-
|
|
3281
|
+
outlineColor(val),
|
|
2000
3282
|
])
|
|
2001
3283
|
},
|
|
2002
3284
|
|
|
3285
|
+
/**
|
|
3286
|
+
* #getter
|
|
3287
|
+
* the encoding coloring the overlay's spans, undefined when none does,
|
|
3288
|
+
* which leaves each span the color its accession takes in `fillPalette`
|
|
3289
|
+
*/
|
|
3290
|
+
get featureFillEncoding(): ResolvedEncoding | undefined {
|
|
3291
|
+
return this.resolvedEncodings.find(e => e.channel === 'featureFill')
|
|
3292
|
+
},
|
|
3293
|
+
|
|
3294
|
+
/**
|
|
3295
|
+
* #getter
|
|
3296
|
+
* the fill and outline of every feature's span: its own GFF `color=`
|
|
3297
|
+
* first, then the `featureFill` scale, then the accession palette.
|
|
3298
|
+
* Computed once per change of the features, the encodings or the palette
|
|
3299
|
+
*/
|
|
3300
|
+
get featureColors(): Map<Annotation, { fill: string; stroke: string }> {
|
|
3301
|
+
return featureColorMap(
|
|
3302
|
+
self.filteredAnnotations,
|
|
3303
|
+
this.featureFillEncoding,
|
|
3304
|
+
this.fillPalette,
|
|
3305
|
+
)
|
|
3306
|
+
},
|
|
3307
|
+
|
|
3308
|
+
/**
|
|
3309
|
+
* #getter
|
|
3310
|
+
* the text the `featureLabel` channel draws inside each span, undefined
|
|
3311
|
+
* when no encoding names the channel. A data channel, so it draws
|
|
3312
|
+
* whether or not the residue letters do
|
|
3313
|
+
*/
|
|
3314
|
+
get featureLabels(): Map<Annotation, string> | undefined {
|
|
3315
|
+
const encoding = this.resolvedEncodings.find(
|
|
3316
|
+
e => e.channel === 'featureLabel',
|
|
3317
|
+
)
|
|
3318
|
+
return encoding
|
|
3319
|
+
? featureLabelMap(self.filteredAnnotations, encoding.field)
|
|
3320
|
+
: undefined
|
|
3321
|
+
},
|
|
3322
|
+
|
|
2003
3323
|
/**
|
|
2004
3324
|
* #getter
|
|
2005
3325
|
* accession -> number drawn on each segment band: the trailing number of
|
|
@@ -2017,25 +3337,104 @@ function stateModelFactory() {
|
|
|
2017
3337
|
/**
|
|
2018
3338
|
* #getter
|
|
2019
3339
|
* the domain types currently drawn on the alignment (filtered-on), shared
|
|
2020
|
-
* by the on-screen legend and the SVG export legend
|
|
2021
|
-
*
|
|
2022
|
-
*
|
|
3340
|
+
* by the on-screen legend and the SVG export legend: the categorical types
|
|
3341
|
+
* ordered by sequence position. Ordinal segments (exons) are numbered on
|
|
3342
|
+
* the band instead
|
|
2023
3343
|
*/
|
|
2024
3344
|
get visibleDomainTypes() {
|
|
2025
3345
|
return this.categoricalDomainTypes
|
|
2026
|
-
.filter(d => self.
|
|
3346
|
+
.filter(d => !self.turnedOffFeatures.get(d.accession))
|
|
2027
3347
|
.toSorted((a, b) => a.start - b.start)
|
|
2028
3348
|
},
|
|
2029
3349
|
|
|
3350
|
+
/**
|
|
3351
|
+
* #getter
|
|
3352
|
+
* the categorical color keys drawn for this view, shared by the on-screen
|
|
3353
|
+
* legend overlay and the SVG export's reserved column. The domain overlay
|
|
3354
|
+
* produces the first, listing the `featureFill` scale where an encoding
|
|
3355
|
+
* names one. Every field a row-table encoding or a row panel reads
|
|
3356
|
+
* produces one more, so two channels over one field, or two strips over
|
|
3357
|
+
* it, list that field once. A strip's `legend` names the title it lists
|
|
3358
|
+
* under, so strips over eight fields with one set of colors list one
|
|
3359
|
+
* legend between them
|
|
3360
|
+
*/
|
|
3361
|
+
get legends(): Legend[] {
|
|
3362
|
+
const { featureFillEncoding, fillPalette, visibleDomainTypes } = this
|
|
3363
|
+
const byKey = new Map<string, Legend>()
|
|
3364
|
+
// the accession palette keys on no field, so it takes a key of its own
|
|
3365
|
+
const add = (
|
|
3366
|
+
field: string | undefined,
|
|
3367
|
+
entries: LegendEntry[],
|
|
3368
|
+
id?: string,
|
|
3369
|
+
) => {
|
|
3370
|
+
if (entries.length === 0) {
|
|
3371
|
+
return
|
|
3372
|
+
}
|
|
3373
|
+
const key = field === undefined ? 'domains' : `field:${field}`
|
|
3374
|
+
const legend = byKey.get(key)
|
|
3375
|
+
if (legend) {
|
|
3376
|
+
const seen = new Set(legend.entries.map(e => e.id))
|
|
3377
|
+
legend.entries.push(...entries.filter(e => !seen.has(e.id)))
|
|
3378
|
+
} else {
|
|
3379
|
+
byKey.set(key, {
|
|
3380
|
+
id: id ?? (field === undefined ? 'domains' : `rowData-${field}`),
|
|
3381
|
+
title: field ?? 'Domains',
|
|
3382
|
+
entries: [...entries],
|
|
3383
|
+
})
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
// the overlay draws the domain key; with no columns it draws nothing,
|
|
3387
|
+
// and a features panel taking the same colors lists them instead
|
|
3388
|
+
if (self.actuallyShowDomains && self.numColumns > 0) {
|
|
3389
|
+
add(
|
|
3390
|
+
featureFillEncoding?.field,
|
|
3391
|
+
featureFillEncoding
|
|
3392
|
+
? featureFillEncoding.legend
|
|
3393
|
+
: visibleDomainTypes.map(d => ({
|
|
3394
|
+
id: d.accession,
|
|
3395
|
+
label: d.name,
|
|
3396
|
+
color: fillPalette[d.accession]!,
|
|
3397
|
+
})),
|
|
3398
|
+
'domains',
|
|
3399
|
+
)
|
|
3400
|
+
}
|
|
3401
|
+
for (const { channel, field, legend } of this.resolvedEncodings) {
|
|
3402
|
+
if (!featureChannels.has(channel)) {
|
|
3403
|
+
add(field, legend)
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
for (const { legendTitle, legend } of this.resolvedRowPanels) {
|
|
3407
|
+
add(legendTitle, legend)
|
|
3408
|
+
}
|
|
3409
|
+
return [...byKey.values()]
|
|
3410
|
+
},
|
|
3411
|
+
|
|
3412
|
+
/**
|
|
3413
|
+
* #getter
|
|
3414
|
+
* whether the overlay marks each domain with a bar under its row instead
|
|
3415
|
+
* of filling the row behind the letters. Letter-color mode hands the
|
|
3416
|
+
* background to the color scheme, so a filled box would paint over it and
|
|
3417
|
+
* leave the setting with nothing to show. Sub-row layout already stacks
|
|
3418
|
+
* the boxes clear of the letters, and with the letters too small to draw
|
|
3419
|
+
* the filled box is the only thing left to read.
|
|
3420
|
+
*/
|
|
3421
|
+
get domainUnderline() {
|
|
3422
|
+
return (
|
|
3423
|
+
self.actuallyShowDomains &&
|
|
3424
|
+
!self.bgColor &&
|
|
3425
|
+
!self.subFeatureRows &&
|
|
3426
|
+
self.showMsaLetters
|
|
3427
|
+
)
|
|
3428
|
+
},
|
|
3429
|
+
|
|
2030
3430
|
/**
|
|
2031
3431
|
* #getter
|
|
2032
3432
|
* every filtered-on annotation resolved to the visible column span it is
|
|
2033
3433
|
* drawn across, keyed by row name. Each row is ordered longest-first so a
|
|
2034
|
-
* short domain
|
|
2035
|
-
*
|
|
2036
|
-
* block
|
|
2037
|
-
*
|
|
2038
|
-
* needs to keep residues readable on top of the boxes.
|
|
3434
|
+
* nested short domain draws on top, and each band carries the lane the
|
|
3435
|
+
* sub-row layout puts it in. Resolved once here instead of per canvas
|
|
3436
|
+
* block per redraw; the letter renderer also reads the band colors to
|
|
3437
|
+
* pick legible letter colors.
|
|
2039
3438
|
*/
|
|
2040
3439
|
get domainBands() {
|
|
2041
3440
|
const { blanks } = self
|
|
@@ -2046,32 +3445,27 @@ function stateModelFactory() {
|
|
|
2046
3445
|
const rowBands = annotations
|
|
2047
3446
|
.toSorted((a, b) => len(b) - len(a))
|
|
2048
3447
|
.map(annotation => {
|
|
2049
|
-
// annotation positions are 1-based and inclusive.
|
|
2050
|
-
//
|
|
2051
|
-
//
|
|
2052
|
-
//
|
|
2053
|
-
//
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
)
|
|
2061
|
-
const endCol = visibleColsBefore(
|
|
2062
|
-
blanks,
|
|
2063
|
-
self.seqPosToGlobalCol(name, annotation.end - 1) + 1,
|
|
2064
|
-
)
|
|
3448
|
+
// annotation positions are 1-based and inclusive. endCol is
|
|
3449
|
+
// exclusive, one past the last residue's column, so the band does
|
|
3450
|
+
// not extend over a following gap run. An endpoint in a hidden
|
|
3451
|
+
// column moves to the neighboring boundary. Bands with no visible
|
|
3452
|
+
// columns, or naming a missing row, are dropped.
|
|
3453
|
+
const start = self.seqPosToGlobalCol(name, annotation.start - 1)
|
|
3454
|
+
const end = self.seqPosToGlobalCol(name, annotation.end - 1)
|
|
3455
|
+
if (start === undefined || end === undefined) {
|
|
3456
|
+
return undefined
|
|
3457
|
+
}
|
|
3458
|
+
const startCol = visibleColsBefore(blanks, start)
|
|
3459
|
+
const endCol = visibleColsBefore(blanks, end + 1)
|
|
2065
3460
|
return endCol > startCol
|
|
2066
3461
|
? { annotation, startCol, endCol }
|
|
2067
3462
|
: undefined
|
|
2068
3463
|
})
|
|
2069
3464
|
.filter(notEmpty)
|
|
2070
|
-
// numbered after
|
|
2071
|
-
// not leave an empty sub-row behind it
|
|
3465
|
+
// numbered after filtering, so a dropped band leaves no gap
|
|
2072
3466
|
.map((band, stackIndex) => ({ ...band, stackIndex }))
|
|
2073
3467
|
if (rowBands.length > 0) {
|
|
2074
|
-
bands.set(name, rowBands)
|
|
3468
|
+
bands.set(name, packDomainLanes(rowBands))
|
|
2075
3469
|
}
|
|
2076
3470
|
}
|
|
2077
3471
|
return bands
|
|
@@ -2099,14 +3493,16 @@ function stateModelFactory() {
|
|
|
2099
3493
|
* domain annotations under the mouse, hit-tested against the exact visible
|
|
2100
3494
|
* column span each box is drawn at (so it matches the overlay across gaps)
|
|
2101
3495
|
*/
|
|
2102
|
-
get mouseOverDomains() {
|
|
3496
|
+
get mouseOverDomains(): Annotation[] {
|
|
2103
3497
|
const { mouseCol } = self
|
|
2104
3498
|
const name = self.mouseOverRowName
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
3499
|
+
if (name === undefined || mouseCol === undefined) {
|
|
3500
|
+
return noDomains
|
|
3501
|
+
}
|
|
3502
|
+
const hits = (this.domainBands.get(name) ?? [])
|
|
3503
|
+
.filter(b => mouseCol >= b.startCol && mouseCol < b.endCol)
|
|
3504
|
+
.map(b => b.annotation)
|
|
3505
|
+
return hits.length > 0 ? hits : noDomains
|
|
2110
3506
|
},
|
|
2111
3507
|
|
|
2112
3508
|
/**
|
|
@@ -2124,8 +3520,7 @@ function stateModelFactory() {
|
|
|
2124
3520
|
* #getter
|
|
2125
3521
|
* row indices highlighted by the current tree hover (a hovered internal
|
|
2126
3522
|
* node highlights every tip below it). Shared by the tree and MSA overlay
|
|
2127
|
-
* canvases
|
|
2128
|
-
* name->index map rather than rebuilding a lookup on each mouse move.
|
|
3523
|
+
* canvases, via the memoized name->index map.
|
|
2129
3524
|
*/
|
|
2130
3525
|
get hoveredRowIndices() {
|
|
2131
3526
|
const { hoveredTreeNode, rowNamesSet } = self
|
|
@@ -2139,8 +3534,8 @@ function stateModelFactory() {
|
|
|
2139
3534
|
/**
|
|
2140
3535
|
* #getter
|
|
2141
3536
|
* contiguous runs of `highlightedColumns`, so a run of highlighted columns
|
|
2142
|
-
* draws as one bordered band.
|
|
2143
|
-
*
|
|
3537
|
+
* draws as one bordered band. Memoized because the overlay canvas redraws
|
|
3538
|
+
* on every mouse move.
|
|
2144
3539
|
*/
|
|
2145
3540
|
get highlightedColumnRuns() {
|
|
2146
3541
|
const { highlightedColumns } = self
|
|
@@ -2166,89 +3561,289 @@ function stateModelFactory() {
|
|
|
2166
3561
|
* is dropped. Row names that match no row are ignored.
|
|
2167
3562
|
*/
|
|
2168
3563
|
get resolvedHighlights(): ResolvedHighlight[] {
|
|
2169
|
-
const {
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
3564
|
+
const { rowNamesSet, transientHighlights } = self
|
|
3565
|
+
// persisted highlights first, so transient ones draw on top
|
|
3566
|
+
const all = [
|
|
3567
|
+
...self.highlights,
|
|
3568
|
+
...Object.values(transientHighlights).flat(),
|
|
3569
|
+
]
|
|
3570
|
+
return all.flatMap(({ row, rows, start, end, label, color }) => {
|
|
3571
|
+
const base = { label, color }
|
|
3572
|
+
if (rows) {
|
|
3573
|
+
const rowIndices = rows
|
|
3574
|
+
.map(name => rowNamesSet.get(name))
|
|
3575
|
+
.filter(notEmpty)
|
|
3576
|
+
return rowIndices.length ? [{ ...base, rowIndices }] : []
|
|
3577
|
+
}
|
|
3578
|
+
if (start === undefined || end === undefined) {
|
|
3579
|
+
return []
|
|
3580
|
+
}
|
|
3581
|
+
const span = self.visibleSpan({ row, start, end })
|
|
3582
|
+
return span ? [{ ...base, ...span, rowIndices: [] }] : []
|
|
3583
|
+
})
|
|
3584
|
+
},
|
|
3585
|
+
|
|
3586
|
+
/**
|
|
3587
|
+
* #method
|
|
3588
|
+
* per-column summary statistics: consensus residue and its identity
|
|
3589
|
+
* fraction, both conservation scores, gap fraction, and the sorted non-gap
|
|
3590
|
+
* residue distribution. undefined past the end of the alignment or for an
|
|
3591
|
+
* all-gap column.
|
|
3592
|
+
*/
|
|
3593
|
+
columnStatsAt(col: number): ColumnStats | undefined {
|
|
3594
|
+
const { colStats, conservation, propertyConservation } = self
|
|
3595
|
+
return columnStats({
|
|
3596
|
+
col,
|
|
3597
|
+
colStats,
|
|
3598
|
+
conservation,
|
|
3599
|
+
propertyConservation,
|
|
3600
|
+
})
|
|
3601
|
+
},
|
|
3602
|
+
|
|
3603
|
+
/**
|
|
3604
|
+
* #getter
|
|
3605
|
+
* `columnStatsAt` for the hovered column, undefined when nothing is
|
|
3606
|
+
* hovered
|
|
3607
|
+
*/
|
|
3608
|
+
get mouseOverColumnStats(): ColumnStats | undefined {
|
|
3609
|
+
const { mouseCol, colStats, conservation, propertyConservation } = self
|
|
3610
|
+
return mouseCol === undefined
|
|
3611
|
+
? undefined
|
|
3612
|
+
: columnStats({
|
|
3613
|
+
col: mouseCol,
|
|
3614
|
+
colStats,
|
|
3615
|
+
conservation,
|
|
3616
|
+
propertyConservation,
|
|
3617
|
+
})
|
|
3618
|
+
},
|
|
3619
|
+
|
|
3620
|
+
/**
|
|
3621
|
+
* #method
|
|
3622
|
+
*/
|
|
3623
|
+
getRowData(name: string) {
|
|
3624
|
+
return {
|
|
3625
|
+
data: self.MSA?.getRowData(name),
|
|
3626
|
+
rowData: self.rowDataOf(name),
|
|
3627
|
+
}
|
|
3628
|
+
},
|
|
3629
|
+
|
|
3630
|
+
/**
|
|
3631
|
+
* #getter
|
|
3632
|
+
* each encoding with its scale resolved against the values its field
|
|
3633
|
+
* takes: a feature channel reads them across the features drawn, every
|
|
3634
|
+
* other channel across the row table. Resolved once per change of that
|
|
3635
|
+
* table or the encodings, never per row per frame.
|
|
3636
|
+
*/
|
|
3637
|
+
get resolvedEncodings(): ResolvedEncoding[] {
|
|
3638
|
+
const rows = Object.values(self.rowData)
|
|
3639
|
+
const features = self.filteredAnnotations
|
|
3640
|
+
return self.encodings.map(encoding => ({
|
|
3641
|
+
...encoding,
|
|
3642
|
+
...resolveScale(
|
|
3643
|
+
encoding.scale,
|
|
3644
|
+
featureChannels.has(encoding.channel)
|
|
3645
|
+
? features
|
|
3646
|
+
.map(a => featureField(a, encoding.field))
|
|
3647
|
+
.filter(notEmpty)
|
|
3648
|
+
: rows.map(row => row?.[encoding.field]).filter(notEmpty),
|
|
3649
|
+
),
|
|
3650
|
+
}))
|
|
3651
|
+
},
|
|
3652
|
+
|
|
3653
|
+
/**
|
|
3654
|
+
* #getter
|
|
3655
|
+
* each row panel with its scale resolved against the values its field
|
|
3656
|
+
* takes across the row table, giving the color per row name, the pixel
|
|
3657
|
+
* column it draws in, and the entries its legend lists. Resolved once
|
|
3658
|
+
* per change of that table or the panels, never per block per frame.
|
|
3659
|
+
*/
|
|
3660
|
+
get resolvedRowPanels(): ResolvedRowPanel[] {
|
|
3661
|
+
const rows = Object.entries(self.rowData)
|
|
3662
|
+
const { fillPalette, featureFillEncoding, featureLabels } = this
|
|
3663
|
+
const annotations = self.filteredAnnotations
|
|
3664
|
+
let offsetX = 0
|
|
3665
|
+
return self.rowPanels.map((panel, index) => {
|
|
3666
|
+
const width = rowPanelWidth(panel, self.rowHeight)
|
|
3667
|
+
const base = { id: `rowpanel-${index}`, width, offsetX }
|
|
3668
|
+
offsetX += width
|
|
3669
|
+
if (panel.kind === 'features') {
|
|
3670
|
+
const color = panel.encoding?.color
|
|
3671
|
+
const encoding = color
|
|
3672
|
+
? resolveFeatureScale(color.field, color.scale, annotations)
|
|
3673
|
+
: featureFillEncoding
|
|
3674
|
+
const label = panel.encoding?.label
|
|
3675
|
+
const align = panel.transform?.find(t => t.type === 'align')
|
|
3676
|
+
return {
|
|
3677
|
+
...base,
|
|
3678
|
+
kind: panel.kind,
|
|
3679
|
+
x: panel.x,
|
|
3680
|
+
header: panel.header ?? '',
|
|
3681
|
+
field: encoding?.field,
|
|
3682
|
+
legendTitle: encoding?.field,
|
|
3683
|
+
spans: featurePanelSpans({
|
|
3684
|
+
panel,
|
|
3685
|
+
width,
|
|
3686
|
+
rowHeight: self.rowHeight,
|
|
3687
|
+
colWidth: self.colWidth,
|
|
3688
|
+
domainBands: this.domainBands,
|
|
3689
|
+
annotationsByRow: self.annotationsByRow,
|
|
3690
|
+
shifts: align
|
|
3691
|
+
? this.featureAlignShifts.get(align.on)
|
|
3692
|
+
: undefined,
|
|
3693
|
+
}),
|
|
3694
|
+
colors: featureColorMap(annotations, encoding, fillPalette),
|
|
3695
|
+
labels: label
|
|
3696
|
+
? featureLabelMap(annotations, label)
|
|
3697
|
+
: featureLabels,
|
|
3698
|
+
legend:
|
|
3699
|
+
encoding?.legend ??
|
|
3700
|
+
this.visibleDomainTypes.map(d => ({
|
|
3701
|
+
id: d.accession,
|
|
3702
|
+
label: d.name,
|
|
3703
|
+
color: fillPalette[d.accession]!,
|
|
3704
|
+
})),
|
|
2185
3705
|
}
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
3706
|
+
}
|
|
3707
|
+
const { colorOf, legend } = resolveScale(
|
|
3708
|
+
panel.scale,
|
|
3709
|
+
rows.map(([, row]) => row?.[panel.field]).filter(notEmpty),
|
|
3710
|
+
)
|
|
3711
|
+
const colors = new Map<string, string>()
|
|
3712
|
+
for (const [name, row] of rows) {
|
|
3713
|
+
const value = row?.[panel.field]
|
|
3714
|
+
const color = value === undefined ? undefined : colorOf(value)
|
|
3715
|
+
if (color) {
|
|
3716
|
+
colors.set(name, color)
|
|
2194
3717
|
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
3718
|
+
}
|
|
3719
|
+
return {
|
|
3720
|
+
...base,
|
|
3721
|
+
kind: panel.kind,
|
|
3722
|
+
field: panel.field,
|
|
3723
|
+
header: panel.header ?? panel.field,
|
|
3724
|
+
legendTitle: panel.legend ?? panel.field,
|
|
3725
|
+
colors,
|
|
3726
|
+
legend,
|
|
3727
|
+
}
|
|
3728
|
+
})
|
|
3729
|
+
},
|
|
3730
|
+
|
|
3731
|
+
/**
|
|
3732
|
+
* #getter
|
|
3733
|
+
* the shift an `align` transform gives each row, keyed by the feature
|
|
3734
|
+
* name it aligns on: the offset putting the first feature of that name
|
|
3735
|
+
* at zero. A row carrying no such feature is absent, and keeps its own
|
|
3736
|
+
* origin.
|
|
3737
|
+
*/
|
|
3738
|
+
get featureAlignShifts(): Map<string, Map<string, number>> {
|
|
3739
|
+
const names = new Set(
|
|
3740
|
+
self.rowPanels.flatMap(panel =>
|
|
3741
|
+
panel.kind === 'features' && panel.x === 'position'
|
|
3742
|
+
? (panel.transform ?? [])
|
|
3743
|
+
.filter(t => t.type === 'align')
|
|
3744
|
+
.map(t => t.on)
|
|
3745
|
+
: [],
|
|
3746
|
+
),
|
|
3747
|
+
)
|
|
3748
|
+
return new Map(
|
|
3749
|
+
[...names].map(on => [
|
|
3750
|
+
on,
|
|
3751
|
+
new Map(
|
|
3752
|
+
Object.entries(self.annotationsByRow)
|
|
3753
|
+
.map(([name, annotations]) => {
|
|
3754
|
+
const hit = annotations.find(a => featureName(a) === on)
|
|
3755
|
+
return hit ? ([name, 1 - hit.start] as const) : undefined
|
|
3756
|
+
})
|
|
3757
|
+
.filter(notEmpty),
|
|
3758
|
+
),
|
|
3759
|
+
]),
|
|
2203
3760
|
)
|
|
2204
3761
|
},
|
|
2205
3762
|
|
|
2206
3763
|
/**
|
|
2207
3764
|
* #getter
|
|
2208
|
-
*
|
|
2209
|
-
*
|
|
2210
|
-
*
|
|
3765
|
+
* the color the `tipLabel` channel gives each row, by row name. Undefined
|
|
3766
|
+
* when no encoding names the channel, which leaves the labels the theme's
|
|
3767
|
+
* text color.
|
|
2211
3768
|
*/
|
|
2212
|
-
get
|
|
2213
|
-
const
|
|
2214
|
-
|
|
3769
|
+
get tipLabelColors(): Map<string, string> | undefined {
|
|
3770
|
+
const encoding = this.resolvedEncodings.find(
|
|
3771
|
+
e => e.channel === 'tipLabel',
|
|
3772
|
+
)
|
|
3773
|
+
if (!encoding) {
|
|
2215
3774
|
return undefined
|
|
2216
3775
|
}
|
|
2217
|
-
const
|
|
2218
|
-
|
|
2219
|
-
|
|
3776
|
+
const colors = new Map<string, string>()
|
|
3777
|
+
for (const [name, row] of Object.entries(self.rowData)) {
|
|
3778
|
+
const value = row?.[encoding.field]
|
|
3779
|
+
const color =
|
|
3780
|
+
value === undefined ? undefined : encoding.colorOf(value)
|
|
3781
|
+
if (color) {
|
|
3782
|
+
colors.set(name, color)
|
|
3783
|
+
}
|
|
2220
3784
|
}
|
|
2221
|
-
|
|
2222
|
-
|
|
3785
|
+
return colors
|
|
3786
|
+
},
|
|
3787
|
+
|
|
3788
|
+
/**
|
|
3789
|
+
* #getter
|
|
3790
|
+
* the wash the `rowTint` channel draws over each row, indexed by row, or
|
|
3791
|
+
* undefined when no encoding names the channel. The overlay draws these,
|
|
3792
|
+
* so a tint stays out of the raster tile cache and its keys.
|
|
3793
|
+
*/
|
|
3794
|
+
get rowTints(): (string | undefined)[] | undefined {
|
|
3795
|
+
const encoding = this.resolvedEncodings.find(
|
|
3796
|
+
e => e.channel === 'rowTint',
|
|
3797
|
+
)
|
|
3798
|
+
if (!encoding) {
|
|
2223
3799
|
return undefined
|
|
2224
3800
|
}
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
gapFraction: gaps / total,
|
|
2235
|
-
conservation: self.conservation[mouseCol] ?? 0,
|
|
2236
|
-
propertyConservation: self.propertyConservation[mouseCol],
|
|
2237
|
-
consensusLetter: consensus?.[0] ?? '',
|
|
2238
|
-
consensusCount: consensus?.[1] ?? 0,
|
|
2239
|
-
consensusFraction: consensus ? consensus[1] / total : 0,
|
|
2240
|
-
distribution,
|
|
2241
|
-
}
|
|
3801
|
+
return self.rowNames.map(name => {
|
|
3802
|
+
const value = self.rowDataOf(name)?.[encoding.field]
|
|
3803
|
+
const color =
|
|
3804
|
+
value === undefined ? undefined : encoding.colorOf(value)
|
|
3805
|
+
if (!color) {
|
|
3806
|
+
return undefined
|
|
3807
|
+
}
|
|
3808
|
+
return withAlpha(color, rowTintAlpha)
|
|
3809
|
+
})
|
|
2242
3810
|
},
|
|
2243
3811
|
|
|
2244
3812
|
/**
|
|
2245
|
-
* #
|
|
3813
|
+
* #getter
|
|
3814
|
+
* the color the `branch` channel gives each tree edge, by the node id at
|
|
3815
|
+
* the edge's far end, or undefined when no encoding names the channel. A
|
|
3816
|
+
* node takes the field value its tips agree on, so a clade of one value
|
|
3817
|
+
* colors down from where it splits off, and a node whose tips disagree or
|
|
3818
|
+
* whose value has no color is absent and draws in the default color.
|
|
3819
|
+
*
|
|
3820
|
+
* The pass runs over the whole tree, never `root`, so a collapsed or
|
|
3821
|
+
* focused clade keeps the color the full tree gives it.
|
|
2246
3822
|
*/
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
3823
|
+
get branchColors(): Map<string, string> | undefined {
|
|
3824
|
+
const encoding = this.resolvedEncodings.find(
|
|
3825
|
+
e => e.channel === 'branch',
|
|
3826
|
+
)
|
|
3827
|
+
if (!encoding) {
|
|
3828
|
+
return undefined
|
|
3829
|
+
}
|
|
3830
|
+
const order = preorder(self.tree)
|
|
3831
|
+
const values = new Map<NodeWithIds, string | undefined>()
|
|
3832
|
+
const colors = new Map<string, string>()
|
|
3833
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
3834
|
+
const node = order[i]!
|
|
3835
|
+
const value =
|
|
3836
|
+
node.children.length > 0
|
|
3837
|
+
? sharedValue(node.children.map(child => values.get(child)))
|
|
3838
|
+
: self.rowDataOf(node.name)?.[encoding.field]
|
|
3839
|
+
values.set(node, value)
|
|
3840
|
+
const color =
|
|
3841
|
+
value === undefined ? undefined : encoding.colorOf(value)
|
|
3842
|
+
if (color) {
|
|
3843
|
+
colors.set(node.id, color)
|
|
3844
|
+
}
|
|
2251
3845
|
}
|
|
3846
|
+
return colors
|
|
2252
3847
|
},
|
|
2253
3848
|
}))
|
|
2254
3849
|
.actions(self => ({
|
|
@@ -2261,23 +3856,39 @@ function stateModelFactory() {
|
|
|
2261
3856
|
/**
|
|
2262
3857
|
* #action
|
|
2263
3858
|
*/
|
|
2264
|
-
|
|
2265
|
-
self.
|
|
3859
|
+
setHideHeader(arg: boolean) {
|
|
3860
|
+
self.hideHeader = arg
|
|
3861
|
+
},
|
|
3862
|
+
/**
|
|
3863
|
+
* #action
|
|
3864
|
+
* focus the subtree a click `y` pixels down the tree overview lands on.
|
|
3865
|
+
* A click inside the box already drawn there clears the focus, the way
|
|
3866
|
+
* clicking the focused branch again does.
|
|
3867
|
+
*/
|
|
3868
|
+
treeOverviewClick(y: number) {
|
|
3869
|
+
const hit = self.treeOverviewHit(y)
|
|
3870
|
+
const focus = self.treeOverviewFocusRows
|
|
3871
|
+
const inside =
|
|
3872
|
+
!!focus && !!hit && hit.rows[0] >= focus[0] && hit.rows[1] <= focus[1]
|
|
3873
|
+
self.setShowOnly(inside || !hit ? undefined : hit.id)
|
|
2266
3874
|
},
|
|
2267
3875
|
/**
|
|
2268
3876
|
* #action
|
|
3877
|
+
* resize every track sharing a `heightKey`; see `trackHeights`
|
|
2269
3878
|
*/
|
|
2270
|
-
|
|
2271
|
-
self.
|
|
3879
|
+
setTrackHeight(heightKey: string, height: number) {
|
|
3880
|
+
self.trackHeights.set(heightKey, height)
|
|
2272
3881
|
},
|
|
2273
3882
|
/**
|
|
2274
3883
|
* #action
|
|
2275
|
-
* Return to the import form: every property
|
|
2276
|
-
*
|
|
2277
|
-
*
|
|
2278
|
-
* reach are cleared by hand.
|
|
3884
|
+
* Return to the import form: reset every property not in
|
|
3885
|
+
* `preservedOnReset` to its default, then clear the file-derived
|
|
3886
|
+
* volatiles applySnapshot does not touch.
|
|
2279
3887
|
*/
|
|
2280
3888
|
reset() {
|
|
3889
|
+
self.resetCount++
|
|
3890
|
+
self.clearWarnings()
|
|
3891
|
+
self.setStatus(undefined)
|
|
2281
3892
|
applySnapshot(
|
|
2282
3893
|
self,
|
|
2283
3894
|
Object.fromEntries(
|
|
@@ -2289,6 +3900,7 @@ function stateModelFactory() {
|
|
|
2289
3900
|
self.setError(undefined)
|
|
2290
3901
|
self.setAnnotations([])
|
|
2291
3902
|
self.setHighlightedColumns(undefined)
|
|
3903
|
+
self.transientHighlights = {}
|
|
2292
3904
|
self.setMousePos(undefined, undefined)
|
|
2293
3905
|
self.setMouseClickPos(undefined, undefined)
|
|
2294
3906
|
self.setHoveredTreeNode(undefined)
|
|
@@ -2302,21 +3914,31 @@ function stateModelFactory() {
|
|
|
2302
3914
|
const blob = new Blob([html], { type: 'image/svg+xml' })
|
|
2303
3915
|
saveAs(blob, exportFileName(self.msaFilehandle, 'svg'))
|
|
2304
3916
|
},
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
3917
|
+
/**
|
|
3918
|
+
* #action
|
|
3919
|
+
* show or hide an annotation type. Only hidden types are recorded; see
|
|
3920
|
+
* `turnedOffFeatures`
|
|
3921
|
+
*/
|
|
3922
|
+
setFilter(accession: string, shown: boolean) {
|
|
3923
|
+
if (shown) {
|
|
3924
|
+
self.turnedOffFeatures.delete(accession)
|
|
3925
|
+
} else {
|
|
3926
|
+
self.turnedOffFeatures.set(accession, true)
|
|
2308
3927
|
}
|
|
2309
3928
|
},
|
|
2310
|
-
setFilter(arg: string, flag: boolean) {
|
|
2311
|
-
self.featureFilters.set(arg, flag)
|
|
2312
|
-
},
|
|
2313
3929
|
|
|
2314
3930
|
/**
|
|
2315
3931
|
* #action
|
|
2316
3932
|
*/
|
|
2317
3933
|
fit() {
|
|
2318
|
-
|
|
2319
|
-
|
|
3934
|
+
// two passes: each direction's viewport depends on whether the other
|
|
3935
|
+
// overflows (the minimap takes height, the vertical scrollbar width)
|
|
3936
|
+
transaction(() => {
|
|
3937
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
3938
|
+
this.fitHorizontally()
|
|
3939
|
+
this.fitVertically()
|
|
3940
|
+
}
|
|
3941
|
+
})
|
|
2320
3942
|
},
|
|
2321
3943
|
/**
|
|
2322
3944
|
* #action
|
|
@@ -2331,14 +3953,36 @@ function stateModelFactory() {
|
|
|
2331
3953
|
}
|
|
2332
3954
|
self.scrollY = 0
|
|
2333
3955
|
},
|
|
3956
|
+
/**
|
|
3957
|
+
* #action
|
|
3958
|
+
* zoom and scroll so a span fills the alignment's width, in highlight
|
|
3959
|
+
* coordinates (see visibleSpan). Does nothing before the viewer knows
|
|
3960
|
+
* its width, or for a span that resolves to no visible column.
|
|
3961
|
+
*/
|
|
3962
|
+
zoomToRegion(region: Region) {
|
|
3963
|
+
const span = self.visibleSpan(region)
|
|
3964
|
+
if (!span || !self.viewInitialized) {
|
|
3965
|
+
return
|
|
3966
|
+
}
|
|
3967
|
+
transaction(() => {
|
|
3968
|
+
self.colWidth = clamp(
|
|
3969
|
+
self.msaCanvasWidth / (span.endCol - span.startCol + 1),
|
|
3970
|
+
minColWidth,
|
|
3971
|
+
maxCellSize,
|
|
3972
|
+
)
|
|
3973
|
+
self.scrollX = clamp(
|
|
3974
|
+
-span.startCol * self.colWidth,
|
|
3975
|
+
self.maxScrollX,
|
|
3976
|
+
0,
|
|
3977
|
+
)
|
|
3978
|
+
})
|
|
3979
|
+
},
|
|
2334
3980
|
/**
|
|
2335
3981
|
* #action
|
|
2336
3982
|
*/
|
|
2337
3983
|
fitHorizontally() {
|
|
2338
3984
|
if (self.numColumns > 0) {
|
|
2339
|
-
//
|
|
2340
|
-
// the right edge -- and short of the width that shows a minimap, so
|
|
2341
|
-
// nothing on screen said they were there
|
|
3985
|
+
// msaCanvasWidth excludes the vertical scrollbar's 20px
|
|
2342
3986
|
self.colWidth = clamp(
|
|
2343
3987
|
self.msaCanvasWidth / self.numColumns,
|
|
2344
3988
|
minColWidth,
|
|
@@ -2349,26 +3993,43 @@ function stateModelFactory() {
|
|
|
2349
3993
|
},
|
|
2350
3994
|
|
|
2351
3995
|
afterCreate() {
|
|
2352
|
-
// seed the highlighted-columns overlay from the declarative property so
|
|
2353
|
-
// a shared snapshot/URL opens with those columns highlighted (the
|
|
2354
|
-
// volatile highlightedColumns can later be driven by genome-hover sync)
|
|
2355
3996
|
if (self.highlightColumns?.length) {
|
|
2356
3997
|
self.setHighlightedColumns(self.highlightColumns)
|
|
2357
3998
|
}
|
|
2358
3999
|
|
|
4000
|
+
// The `collapse` and `focus` clade marks seed the collapsed list and
|
|
4001
|
+
// the subtree in focus, which the tree, `hideGapsEffective` and the
|
|
4002
|
+
// alignment all read. The tree arrives with the model for inline data
|
|
4003
|
+
// and later for a filehandle, so the seeding waits for it and then runs
|
|
4004
|
+
// once: expanding a seeded clade sticks, and the record collapses it
|
|
4005
|
+
// again only on reload.
|
|
4006
|
+
let cladesSeeded = false
|
|
2359
4007
|
addDisposer(
|
|
2360
4008
|
self,
|
|
2361
4009
|
autorun(() => {
|
|
2362
|
-
|
|
2363
|
-
|
|
4010
|
+
if (
|
|
4011
|
+
cladesSeeded ||
|
|
4012
|
+
!self.dataInitialized ||
|
|
4013
|
+
self.clades.length === 0
|
|
4014
|
+
) {
|
|
4015
|
+
return
|
|
4016
|
+
}
|
|
4017
|
+
cladesSeeded = true
|
|
4018
|
+
for (const { mark, nodeId } of self.resolvedClades) {
|
|
4019
|
+
if (nodeId === undefined) {
|
|
4020
|
+
continue
|
|
4021
|
+
}
|
|
4022
|
+
if (mark === 'collapse' && !self.collapsed.includes(nodeId)) {
|
|
4023
|
+
self.toggleCollapsed(nodeId)
|
|
4024
|
+
} else if (mark === 'focus') {
|
|
4025
|
+
self.setShowOnly(nodeId)
|
|
4026
|
+
}
|
|
2364
4027
|
}
|
|
2365
4028
|
}),
|
|
2366
4029
|
)
|
|
2367
4030
|
|
|
2368
|
-
//
|
|
2369
|
-
//
|
|
2370
|
-
// matchMedia query is pinned to the current ratio, so each change
|
|
2371
|
-
// re-registers against the new one to keep tracking further moves.
|
|
4031
|
+
// the matchMedia query is pinned to the current device pixel ratio, so
|
|
4032
|
+
// each change re-registers against the new one
|
|
2372
4033
|
if (
|
|
2373
4034
|
typeof window !== 'undefined' &&
|
|
2374
4035
|
typeof window.matchMedia === 'function'
|
|
@@ -2392,44 +4053,57 @@ function stateModelFactory() {
|
|
|
2392
4053
|
* Fetch a filehandle whenever it changes, and hand the text to
|
|
2393
4054
|
* `onLoad`.
|
|
2394
4055
|
*
|
|
2395
|
-
*
|
|
2396
|
-
*
|
|
2397
|
-
*
|
|
2398
|
-
* loading flag belonging to the newer one.
|
|
4056
|
+
* A generation guard keeps a slower superseded request from overwriting
|
|
4057
|
+
* the newer one's data, status or loading flag. A superseded or cleared
|
|
4058
|
+
* request is aborted and clears its status line.
|
|
2399
4059
|
*
|
|
2400
|
-
* `clearFilehandle`
|
|
2401
|
-
*
|
|
2402
|
-
*
|
|
2403
|
-
*
|
|
2404
|
-
*
|
|
4060
|
+
* `clearFilehandle` runs after a local file loads, since a blob has no
|
|
4061
|
+
* URL to refetch, and when the user cancels, which returns the view to
|
|
4062
|
+
* the import form.
|
|
4063
|
+
*
|
|
4064
|
+
* `what` names the layer in a failure message. An `optional` layer
|
|
4065
|
+
* (annotations, row metadata) that fails adds a warning instead of an
|
|
4066
|
+
* error.
|
|
2405
4067
|
*/
|
|
2406
4068
|
const loadOnFilehandleChange = ({
|
|
4069
|
+
what,
|
|
2407
4070
|
getFilehandle,
|
|
2408
4071
|
onLoad,
|
|
2409
4072
|
setLoading,
|
|
2410
4073
|
clearFilehandle,
|
|
4074
|
+
optional = false,
|
|
2411
4075
|
}: {
|
|
4076
|
+
what: string
|
|
2412
4077
|
getFilehandle: () => FileLocationType | undefined
|
|
2413
4078
|
onLoad: (text: string) => void
|
|
2414
4079
|
setLoading?: (arg: boolean) => void
|
|
2415
4080
|
clearFilehandle?: () => void
|
|
4081
|
+
optional?: boolean
|
|
2416
4082
|
}) => {
|
|
2417
4083
|
let generation = 0
|
|
4084
|
+
let cancelInFlight: (() => void) | undefined
|
|
4085
|
+
addDisposer(self, () => {
|
|
4086
|
+
cancelInFlight?.()
|
|
4087
|
+
})
|
|
2418
4088
|
addDisposer(
|
|
2419
4089
|
self,
|
|
2420
4090
|
autorun(async () => {
|
|
2421
4091
|
const filehandle = getFilehandle()
|
|
2422
|
-
// a cleared filehandle bumps the generation too, so a reset()
|
|
2423
|
-
// mid-download invalidates the in-flight fetch instead of letting
|
|
2424
|
-
// its onLoad land on the emptied model. That also orphans the
|
|
2425
|
-
// invalidated run's `finally`, so the loading flag is cleared
|
|
2426
|
-
// here on its behalf
|
|
2427
4092
|
const current = ++generation
|
|
4093
|
+
cancelInFlight?.()
|
|
4094
|
+
cancelInFlight = undefined
|
|
2428
4095
|
if (!filehandle) {
|
|
2429
4096
|
setLoading?.(false)
|
|
2430
4097
|
return
|
|
2431
4098
|
}
|
|
2432
|
-
const isCurrent = () => current === generation
|
|
4099
|
+
const isCurrent = () => current === generation && isAlive(self)
|
|
4100
|
+
const controller = new AbortController()
|
|
4101
|
+
cancelInFlight = () => {
|
|
4102
|
+
controller.abort()
|
|
4103
|
+
if (isAlive(self)) {
|
|
4104
|
+
self.setStatus(undefined)
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
2433
4107
|
try {
|
|
2434
4108
|
setLoading?.(true)
|
|
2435
4109
|
self.setError(undefined)
|
|
@@ -2440,6 +4114,7 @@ function stateModelFactory() {
|
|
|
2440
4114
|
self.setStatus(status)
|
|
2441
4115
|
}
|
|
2442
4116
|
},
|
|
4117
|
+
{ controller },
|
|
2443
4118
|
)
|
|
2444
4119
|
if (isCurrent()) {
|
|
2445
4120
|
transaction(() => {
|
|
@@ -2455,11 +4130,16 @@ function stateModelFactory() {
|
|
|
2455
4130
|
clearFilehandle?.()
|
|
2456
4131
|
} else {
|
|
2457
4132
|
console.error(e)
|
|
2458
|
-
|
|
4133
|
+
if (optional) {
|
|
4134
|
+
self.addWarning(`The ${what} did not load: ${e}`)
|
|
4135
|
+
} else {
|
|
4136
|
+
self.setError(e)
|
|
4137
|
+
}
|
|
2459
4138
|
}
|
|
2460
4139
|
}
|
|
2461
4140
|
} finally {
|
|
2462
4141
|
if (isCurrent()) {
|
|
4142
|
+
cancelInFlight = undefined
|
|
2463
4143
|
setLoading?.(false)
|
|
2464
4144
|
}
|
|
2465
4145
|
}
|
|
@@ -2468,6 +4148,7 @@ function stateModelFactory() {
|
|
|
2468
4148
|
}
|
|
2469
4149
|
|
|
2470
4150
|
loadOnFilehandleChange({
|
|
4151
|
+
what: 'tree',
|
|
2471
4152
|
getFilehandle: () => self.treeFilehandle,
|
|
2472
4153
|
onLoad: text => {
|
|
2473
4154
|
self.setTree(text)
|
|
@@ -2480,16 +4161,19 @@ function stateModelFactory() {
|
|
|
2480
4161
|
},
|
|
2481
4162
|
})
|
|
2482
4163
|
|
|
2483
|
-
// treeMetadata
|
|
2484
|
-
// it keeps no loading flag and nothing to return to on cancel
|
|
4164
|
+
// treeMetadata has no import-form step, so no loading flag or cancel
|
|
2485
4165
|
loadOnFilehandleChange({
|
|
4166
|
+
what: 'row metadata',
|
|
4167
|
+
optional: true,
|
|
2486
4168
|
getFilehandle: () => self.treeMetadataFilehandle,
|
|
2487
4169
|
onLoad: text => {
|
|
2488
4170
|
self.setTreeMetadata(text)
|
|
2489
4171
|
},
|
|
2490
4172
|
})
|
|
2491
4173
|
|
|
2492
|
-
//
|
|
4174
|
+
// parses data.gff into annotations. Clearing the text clears only the
|
|
4175
|
+
// annotations this autorun applied, not ones a host set directly
|
|
4176
|
+
let appliedGFF = false
|
|
2493
4177
|
addDisposer(
|
|
2494
4178
|
self,
|
|
2495
4179
|
autorun(() => {
|
|
@@ -2497,19 +4181,25 @@ function stateModelFactory() {
|
|
|
2497
4181
|
if (gffText) {
|
|
2498
4182
|
try {
|
|
2499
4183
|
self.applyGFFText(gffText)
|
|
4184
|
+
appliedGFF = true
|
|
2500
4185
|
} catch (e) {
|
|
2501
4186
|
console.error(e)
|
|
2502
|
-
self.
|
|
4187
|
+
self.addWarning(`The annotations did not parse: ${e}`)
|
|
2503
4188
|
}
|
|
4189
|
+
} else if (appliedGFF) {
|
|
4190
|
+
appliedGFF = false
|
|
4191
|
+
self.setAnnotations([])
|
|
2504
4192
|
}
|
|
2505
4193
|
}),
|
|
2506
4194
|
)
|
|
2507
4195
|
|
|
2508
|
-
// gffFilehandle
|
|
4196
|
+
// gffFilehandle loads into data.gff, which the autorun above parses
|
|
2509
4197
|
loadOnFilehandleChange({
|
|
4198
|
+
what: 'annotations',
|
|
4199
|
+
optional: true,
|
|
2510
4200
|
getFilehandle: () => self.gffFilehandle,
|
|
2511
4201
|
onLoad: text => {
|
|
2512
|
-
self.
|
|
4202
|
+
self.setGFF(text)
|
|
2513
4203
|
},
|
|
2514
4204
|
clearFilehandle: () => {
|
|
2515
4205
|
self.setGFFFilehandle(undefined)
|
|
@@ -2517,6 +4207,7 @@ function stateModelFactory() {
|
|
|
2517
4207
|
})
|
|
2518
4208
|
|
|
2519
4209
|
loadOnFilehandleChange({
|
|
4210
|
+
what: 'alignment',
|
|
2520
4211
|
getFilehandle: () => self.msaFilehandle,
|
|
2521
4212
|
onLoad: text => {
|
|
2522
4213
|
self.setMSA(text)
|
|
@@ -2529,19 +4220,12 @@ function stateModelFactory() {
|
|
|
2529
4220
|
},
|
|
2530
4221
|
})
|
|
2531
4222
|
|
|
2532
|
-
// Keep
|
|
2533
|
-
// self.
|
|
2534
|
-
//
|
|
2535
|
-
//
|
|
4223
|
+
// Keep computeds alive that are read outside reactions, so they are not
|
|
4224
|
+
// recomputed on every access: self.columns (and through it the
|
|
4225
|
+
// parseMSA result), and the column statistics that dynamic color
|
|
4226
|
+
// schemes and the mousemove-driven hover tooltip read.
|
|
2536
4227
|
// xref solution https://github.com/mobxjs/mobx/issues/266#issuecomment-222007278
|
|
2537
4228
|
// xref problem https://github.com/GMOD/react-msaview/issues/75
|
|
2538
|
-
//
|
|
2539
|
-
// The column statistics are held for the same reason whenever something
|
|
2540
|
-
// reads them off the reactive path: dynamic color schemes, and the hover
|
|
2541
|
-
// tooltip, which is read from a mousemove handler. Without this the
|
|
2542
|
-
// tooltip's cost depends on whether some visible track happens to be
|
|
2543
|
-
// observing them, so closing the conservation track would silently turn
|
|
2544
|
-
// every mouse move into a full-alignment recount.
|
|
2545
4229
|
addDisposer(
|
|
2546
4230
|
self,
|
|
2547
4231
|
autorun(() => {
|
|
@@ -2563,10 +4247,9 @@ function stateModelFactory() {
|
|
|
2563
4247
|
}),
|
|
2564
4248
|
)
|
|
2565
4249
|
|
|
2566
|
-
//
|
|
2567
|
-
//
|
|
2568
|
-
//
|
|
2569
|
-
// below (treeAreaWidth here depends only on labelsWidth, not treeWidth).
|
|
4250
|
+
// with autoTreeAreaWidth and no tree drawn, size the tree area to the
|
|
4251
|
+
// labels. Gated on noTree/!drawTree so it does not loop with the
|
|
4252
|
+
// treeWidth autorun below
|
|
2570
4253
|
addDisposer(
|
|
2571
4254
|
self,
|
|
2572
4255
|
autorun(() => {
|
|
@@ -2575,19 +4258,36 @@ function stateModelFactory() {
|
|
|
2575
4258
|
(self.noTree || !self.drawTree) &&
|
|
2576
4259
|
self.labelsWidth
|
|
2577
4260
|
) {
|
|
2578
|
-
self.setTreeAreaWidth(
|
|
4261
|
+
self.setTreeAreaWidth(
|
|
4262
|
+
self.labelsWidth + self.marginLeft + 12 + self.cladeGutterWidth,
|
|
4263
|
+
)
|
|
2579
4264
|
}
|
|
2580
4265
|
}),
|
|
2581
4266
|
)
|
|
2582
4267
|
|
|
2583
|
-
//
|
|
4268
|
+
// treeWidth follows the tree area less the labels. A non-default
|
|
4269
|
+
// snapshot treeWidth (jbrowse-plugin-msaview opens at 100 in a 200px
|
|
4270
|
+
// area) is kept until the tree area changes. Not a getter, because
|
|
4271
|
+
// labelsWidth is measured off leaves laid out against treeWidth.
|
|
4272
|
+
let pinnedAreaWidth =
|
|
4273
|
+
self.treeWidth === defaultTreeWidth ? undefined : self.treeAreaWidth
|
|
2584
4274
|
addDisposer(
|
|
2585
4275
|
self,
|
|
2586
4276
|
autorun(() => {
|
|
4277
|
+
const areaWidth = self.treeAreaWidth
|
|
4278
|
+
const labelsWidth = self.labelsWidth
|
|
4279
|
+
if (pinnedAreaWidth === areaWidth) {
|
|
4280
|
+
return
|
|
4281
|
+
}
|
|
4282
|
+
pinnedAreaWidth = undefined
|
|
2587
4283
|
self.setTreeWidth(
|
|
2588
4284
|
Math.max(
|
|
2589
4285
|
50,
|
|
2590
|
-
|
|
4286
|
+
areaWidth -
|
|
4287
|
+
labelsWidth -
|
|
4288
|
+
10 -
|
|
4289
|
+
self.marginLeft -
|
|
4290
|
+
self.cladeGutterWidth,
|
|
2591
4291
|
),
|
|
2592
4292
|
)
|
|
2593
4293
|
}),
|
|
@@ -2595,9 +4295,8 @@ function stateModelFactory() {
|
|
|
2595
4295
|
},
|
|
2596
4296
|
}))
|
|
2597
4297
|
.postProcessSnapshot(({ data, columnTracks, ...rest }) => ({
|
|
2598
|
-
// per-property defaults
|
|
2599
|
-
//
|
|
2600
|
-
// when a sibling filehandle can refetch them, keeping sessions/URLs small
|
|
4298
|
+
// stripDefault handles per-property defaults; this drops inline documents
|
|
4299
|
+
// whose filehandle can refetch them
|
|
2601
4300
|
...rest,
|
|
2602
4301
|
...smallColumnTracks(columnTracks),
|
|
2603
4302
|
data: {
|