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/dist/model.js
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
import { clamp, groupBy, notEmpty, sum } from '@jbrowse/core/util';
|
|
2
2
|
import { openLocation } from '@jbrowse/core/util/io';
|
|
3
3
|
import { ElementId, FileLocation } from '@jbrowse/core/util/types/mst';
|
|
4
|
-
import { addDisposer, applySnapshot, cast, getSnapshot, types, } from '@jbrowse/mobx-state-tree';
|
|
5
|
-
import { colord } from 'colord';
|
|
4
|
+
import { addDisposer, applySnapshot, cast, getSnapshot, isAlive, types, } from '@jbrowse/mobx-state-tree';
|
|
6
5
|
import { autorun, transaction } from 'mobx';
|
|
7
6
|
import { generateNodeIds, gffToAnnotations, interProScanToAnnotations, parseEmfTree, parseGFF, parseMSA, parseNewick, } from 'msa-parsers';
|
|
8
7
|
import { calculateBlocks } from './calculateBlocks.js';
|
|
9
8
|
import { clustalXColumnColors } from './clustalX.js';
|
|
10
9
|
import colorSchemes from './colorSchemes.js';
|
|
11
10
|
import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.js';
|
|
11
|
+
import { columnStats } from './columnStats.js';
|
|
12
|
+
import { packDomainLanes } from './components/msa/packDomainLanes.js';
|
|
13
|
+
import { visibleColRange } from './components/msa/visibleColRange.js';
|
|
12
14
|
import TrackBlocks from './components/tracks/TrackBlocks.js';
|
|
13
|
-
import {
|
|
15
|
+
import { cladeGutterWidth } from './components/tree/cladeBrackets.js';
|
|
16
|
+
import { scaleBarLength } from './components/tree/scaleBar.js';
|
|
17
|
+
import { cladeHighlightAlpha, cladeHighlightColor, defaultAllowedGappyness, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawMsaLetters, defaultFeaturePanelWidth, defaultHeight, defaultHideGaps, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultScrollZoomAxis, defaultShowDomainLegend, defaultShowDomains, defaultSubFeatureRows, defaultTreeWidth, labelReferenceFontSize, maxCellSize, maxInlineSnapshotBytes, maxNeighborJoiningRows, minColWidth, minLetterColWidth, minLetterRowHeight, minRowHeight, rowPanelHeaderHeight, rowTintAlpha, scrollZoomAxes, segmentFeatureTypes, segmentShades, treeScaleBarHeight, } from './constants.js';
|
|
14
18
|
import { createPaletteMap } from './createPaletteMap.js';
|
|
15
19
|
import { exportFileName } from './exportFileName.js';
|
|
20
|
+
import { featureField, featureName } from './featureFields.js';
|
|
16
21
|
import { fetchTextWithProgress, isAbortError } from './fetchUtils.js';
|
|
17
22
|
import { flatToTree } from './flatToTree.js';
|
|
18
|
-
import { calcDepthToLeaf, clusterLayout, collapse, collapsedSubtreeMaxLength, find,
|
|
23
|
+
import { calcDepthToLeaf, clusterLayout, collapse, collapsedSubtreeMaxLength, find, forEachDescendant, hierarchy, leafIndex, leaves, maxLength, mrca, nodeCoveringRows, setBrLength, sort, sum as hierarchySum, } from './hierarchy.js';
|
|
19
24
|
import { measureTextCanvas } from './measureTextCanvas.js';
|
|
20
25
|
import { DataModelF } from './model/DataModel.js';
|
|
21
26
|
import { DialogQueueSessionMixin } from './model/DialogQueue.js';
|
|
@@ -24,44 +29,176 @@ import { TreeModelF } from './model/treeModel.js';
|
|
|
24
29
|
import { calculateNeighborJoiningTree } from './neighborJoining.js';
|
|
25
30
|
import { parseAsn1 } from './parseAsn1.js';
|
|
26
31
|
import { calculatePropertyConservation } from './propertyConservation.js';
|
|
27
|
-
import { globalColToVisibleCol,
|
|
32
|
+
import { globalColToVisibleCol, seqPosOfGlobalCol, visibleColToGlobalCol, visibleColsBefore, } from './rowCoordinateCalculations.js';
|
|
33
|
+
import { resolveScale } from './scales.js';
|
|
28
34
|
import { buildSeqPosIndex } from './seqPosToGlobalCol.js';
|
|
29
35
|
import { maxBitsFor } from './sequenceLogo.js';
|
|
30
36
|
import { stripDefault } from './stripDefault.js';
|
|
31
|
-
import { computeRowInsertions, dropBlanks, len, skipBlanks, transform, } from './util.js';
|
|
37
|
+
import { computeRowInsertions, dropBlanks, len, outlineColor, skipBlanks, transform, withAlpha, } from './util.js';
|
|
32
38
|
import { saveAs } from './vendor/fileSaver.js';
|
|
39
|
+
import { parseWuss } from './wuss.js';
|
|
33
40
|
function parseTreeText(text) {
|
|
34
41
|
if (text.startsWith('BioTreeContainer')) {
|
|
35
42
|
return flatToTree(parseAsn1(text));
|
|
36
43
|
}
|
|
37
44
|
return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text);
|
|
38
45
|
}
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
const
|
|
46
|
+
// The height each kind draws at before the user drags a divider. A text track
|
|
47
|
+
// is absent: it is one alignment row tall and follows rowHeight, so the zoom
|
|
48
|
+
// controls already size it.
|
|
49
|
+
export const defaultTrackHeights = {
|
|
50
|
+
bar: 40,
|
|
51
|
+
// taller than a bar track: in a 40px stack of four residues each glyph is
|
|
52
|
+
// too short to identify. Twice the height of the tracks above it, so it
|
|
53
|
+
// starts hidden
|
|
54
|
+
logo: 80,
|
|
55
|
+
arc: 50,
|
|
56
|
+
ruler: 20,
|
|
57
|
+
};
|
|
58
|
+
// the kinds a divider resizes. The ruler is a fixed scale, and a text track
|
|
59
|
+
// follows the vertical zoom.
|
|
60
|
+
const resizableKinds = new Set(['bar', 'logo', 'arc']);
|
|
61
|
+
const defaultOffTracks = new Set(['sequence-logo', 'position-ruler']);
|
|
62
|
+
// a data track carries its own height rather than its kind's, so its divider
|
|
63
|
+
// resizes it alone
|
|
64
|
+
const ownHeightKey = (id) => `own:${id}`;
|
|
65
|
+
// base-pair arcs: one color for nested helices, one for pseudoknots, which cross
|
|
66
|
+
// them
|
|
67
|
+
const HELIX_ARC = '#4e79a7';
|
|
68
|
+
const PSEUDOKNOT_ARC = '#e15759';
|
|
69
|
+
// the fill of a span under a scale that gives its value no color, such as a
|
|
70
|
+
// gene a `map` over gene names leaves out
|
|
71
|
+
const UNSCALED_FEATURE = '#d9d9d9';
|
|
72
|
+
// the fraction of a span a following one may cover before it takes a lane of
|
|
73
|
+
// its own in a `features` panel
|
|
74
|
+
const laneOverlap = 0.1;
|
|
75
|
+
// a row panel's own width, or a default: the row height, which makes a strip
|
|
76
|
+
// cell square, and a gene neighborhood's width for a features panel
|
|
77
|
+
function rowPanelWidth(panel, rowHeight) {
|
|
78
|
+
return (panel.width ??
|
|
79
|
+
(panel.kind === 'features' ? defaultFeaturePanelWidth : rowHeight));
|
|
80
|
+
}
|
|
81
|
+
// the fill and outline of every feature in a list: its own GFF `color=` first,
|
|
82
|
+
// then the scale an encoding resolves over one of its fields, then the
|
|
83
|
+
// accession palette
|
|
84
|
+
function featureColorMap(annotations, encoding, fillPalette) {
|
|
85
|
+
const strokes = new Map();
|
|
86
|
+
const strokeOf = (fill) => {
|
|
87
|
+
const hit = strokes.get(fill);
|
|
88
|
+
if (hit !== undefined) {
|
|
89
|
+
return hit;
|
|
90
|
+
}
|
|
91
|
+
const stroke = outlineColor(fill);
|
|
92
|
+
strokes.set(fill, stroke);
|
|
93
|
+
return stroke;
|
|
94
|
+
};
|
|
95
|
+
return new Map(annotations.map(annotation => {
|
|
96
|
+
const value = encoding
|
|
97
|
+
? featureField(annotation, encoding.field)
|
|
98
|
+
: undefined;
|
|
99
|
+
const fill = annotation.color ??
|
|
100
|
+
(value === undefined ? undefined : encoding.colorOf(value)) ??
|
|
101
|
+
(encoding ? UNSCALED_FEATURE : fillPalette[annotation.accession]);
|
|
102
|
+
return [annotation, { fill, stroke: strokeOf(fill) }];
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
// the text a label channel draws inside each span, for the features carrying
|
|
106
|
+
// the field it names
|
|
107
|
+
function featureLabelMap(annotations, field) {
|
|
108
|
+
const labels = new Map();
|
|
109
|
+
for (const annotation of annotations) {
|
|
110
|
+
const value = featureField(annotation, field);
|
|
111
|
+
if (value !== undefined) {
|
|
112
|
+
labels.set(annotation, value);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return labels;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The spans a `features` panel draws, keyed by row name and measured in the
|
|
119
|
+
* panel's own pixels. `column` takes the bands the overlay draws, at the
|
|
120
|
+
* alignment's column width. `position` packs each row's features in its own
|
|
121
|
+
* residue positions, shifted by an `align` transform, and maps the extent they
|
|
122
|
+
* cover across every row onto the panel width, so a row with no alignment
|
|
123
|
+
* still has an x.
|
|
124
|
+
*/
|
|
125
|
+
function featurePanelSpans({ panel, width, rowHeight, colWidth, domainBands, annotationsByRow, shifts, }) {
|
|
126
|
+
if (panel.x === 'column') {
|
|
127
|
+
return new Map([...domainBands].map(([name, bands]) => [
|
|
128
|
+
name,
|
|
129
|
+
bands.map(band => ({
|
|
130
|
+
...band,
|
|
131
|
+
xStart: band.startCol * colWidth,
|
|
132
|
+
xEnd: band.endCol * colWidth,
|
|
133
|
+
})),
|
|
134
|
+
]));
|
|
135
|
+
}
|
|
136
|
+
const shifted = Object.entries(annotationsByRow).map(([name, annotations]) => {
|
|
137
|
+
const shift = shifts?.get(name) ?? 0;
|
|
138
|
+
return [
|
|
139
|
+
name,
|
|
140
|
+
annotations.map(annotation => ({
|
|
141
|
+
annotation,
|
|
142
|
+
start: annotation.start - 1 + shift,
|
|
143
|
+
end: annotation.end + shift,
|
|
144
|
+
})),
|
|
145
|
+
];
|
|
146
|
+
});
|
|
147
|
+
let min = Infinity;
|
|
148
|
+
let max = -Infinity;
|
|
149
|
+
for (const [, features] of shifted) {
|
|
150
|
+
for (const { start, end } of features) {
|
|
151
|
+
min = Math.min(min, start);
|
|
152
|
+
max = Math.max(max, end);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// the arrowhead on the rightmost feature reaches a row height past its end,
|
|
156
|
+
// so the extent maps onto the panel less that much
|
|
157
|
+
const drawable = Math.max(1, width - rowHeight);
|
|
158
|
+
const scale = max > min ? drawable / (max - min) : 0;
|
|
159
|
+
// a lane opens where a span covers more than a tenth of the one before it,
|
|
160
|
+
// which keeps the genes of an operon on one lane: adjacent genes commonly
|
|
161
|
+
// share a few bases, and a stop codon overlapping the next start reads as a
|
|
162
|
+
// second lane over the whole row
|
|
163
|
+
return new Map(shifted.map(([name, features]) => [
|
|
164
|
+
name,
|
|
165
|
+
packDomainLanes(features.map(({ annotation, start, end }) => ({
|
|
166
|
+
annotation,
|
|
167
|
+
xStart: (start - min) * scale,
|
|
168
|
+
xEnd: (end - min) * scale,
|
|
169
|
+
startCol: start,
|
|
170
|
+
endCol: end - (end - start) * laneOverlap,
|
|
171
|
+
}))),
|
|
172
|
+
]));
|
|
173
|
+
}
|
|
174
|
+
// a scale over the values a field takes across the features drawn
|
|
175
|
+
function resolveFeatureScale(field, scale, annotations) {
|
|
176
|
+
return {
|
|
177
|
+
field,
|
|
178
|
+
...resolveScale(scale, annotations.map(a => featureField(a, field)).filter(notEmpty)),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
43
181
|
// a data track over this size stays in the live model but leaves the snapshot,
|
|
44
182
|
// the same rule DataModel applies to an inline document
|
|
45
|
-
|
|
183
|
+
function columnTrackSizes(tracks) {
|
|
184
|
+
return (tracks ?? []).map(t => JSON.stringify(t).length);
|
|
185
|
+
}
|
|
46
186
|
function smallColumnTracks(tracks) {
|
|
47
|
-
const
|
|
187
|
+
const sizes = columnTrackSizes(tracks);
|
|
188
|
+
const kept = tracks?.filter((_t, i) => sizes[i] <= maxInlineSnapshotBytes);
|
|
48
189
|
return kept?.length ? { columnTracks: kept } : {};
|
|
49
190
|
}
|
|
50
191
|
/**
|
|
51
192
|
* The snapshot properties reset() carries across a return to the import form:
|
|
52
193
|
* display preferences and layout, nothing derived from the loaded file.
|
|
53
194
|
*
|
|
54
|
-
* reset() applies a default snapshot filtered to this list, so
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* are path-derived (node-0-0-1), a stale `collapsed` or `showOnly` id matched
|
|
60
|
-
* a real node in the new tree and folded it. Downstream composed properties
|
|
61
|
-
* (e.g. the jbrowse plugin's) are not on the list, so they reset too.
|
|
195
|
+
* reset() applies a default snapshot filtered to this list, so any property
|
|
196
|
+
* left off it resets, including downstream composed ones (e.g. the jbrowse
|
|
197
|
+
* plugin's). Node ids are path-derived (node-0-0-1), so a carried-over
|
|
198
|
+
* `collapsed` or `showOnly` id would match a real node in the next tree and
|
|
199
|
+
* fold it.
|
|
62
200
|
*
|
|
63
|
-
* Exported for modelReset.test.ts
|
|
64
|
-
* matches a freshly created model after reset().
|
|
201
|
+
* Exported for modelReset.test.ts.
|
|
65
202
|
*/
|
|
66
203
|
export const preservedOnReset = new Set([
|
|
67
204
|
'id',
|
|
@@ -69,6 +206,7 @@ export const preservedOnReset = new Set([
|
|
|
69
206
|
'height',
|
|
70
207
|
'drawMsaLetters',
|
|
71
208
|
'scrollZoom',
|
|
209
|
+
'scrollZoomAxis',
|
|
72
210
|
'bgColor',
|
|
73
211
|
'colorSchemeName',
|
|
74
212
|
'showColumnStats',
|
|
@@ -79,19 +217,166 @@ export const preservedOnReset = new Set([
|
|
|
79
217
|
'showBranchLen',
|
|
80
218
|
'drawTree',
|
|
81
219
|
'drawNodeBubbles',
|
|
220
|
+
'drawNodeLabels',
|
|
221
|
+
'showTreeOverview',
|
|
222
|
+
'overviewHeight',
|
|
82
223
|
'autoTreeAreaWidth',
|
|
83
224
|
'turnedOffTracks',
|
|
225
|
+
'trackHeights',
|
|
84
226
|
'hideGaps',
|
|
85
227
|
'allowedGappyness',
|
|
86
228
|
'subFeatureRows',
|
|
87
229
|
'showDomainLegend',
|
|
88
230
|
]);
|
|
89
|
-
// `turnedOffTracks`
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
function trackIsOff(turnedOffTracks, id) {
|
|
94
|
-
return turnedOffTracks.get(id) ?? defaultOffTracks.has(id);
|
|
231
|
+
// `turnedOffTracks` holds only the user's explicit choices, so a hidden-by-default
|
|
232
|
+
// track writes nothing into the snapshot. A file-supplied track passes its own
|
|
233
|
+
// `defaultOff`, since the count varies by file: a Pfam seed has a couple of #=GR
|
|
234
|
+
// lines, an Rfam family one per row.
|
|
235
|
+
function trackIsOff(turnedOffTracks, id, defaultOff) {
|
|
236
|
+
return turnedOffTracks.get(id) ?? (defaultOff || defaultOffTracks.has(id));
|
|
237
|
+
}
|
|
238
|
+
// shared empty results, so observers don't see a fresh [] as a change
|
|
239
|
+
const noDomains = [];
|
|
240
|
+
const noClades = [];
|
|
241
|
+
/**
|
|
242
|
+
* The rows a clade covers and the node an `mrca` names, or undefined when the
|
|
243
|
+
* clade does not resolve: a tip name the tree does not have or has twice, or a
|
|
244
|
+
* leaf count `tips` disagrees with. `index` and `rowNamesSet` are the memoized
|
|
245
|
+
* passes over the tree.
|
|
246
|
+
*/
|
|
247
|
+
function cladeRows(clade, root, index, rowNamesSet) {
|
|
248
|
+
if (clade.range) {
|
|
249
|
+
const [a, b] = clade.range.map(name => index.get(name) ? rowNamesSet.get(name) : undefined);
|
|
250
|
+
if (a === undefined || b === undefined) {
|
|
251
|
+
return undefined;
|
|
252
|
+
}
|
|
253
|
+
const rows = a <= b ? [a, b] : [b, a];
|
|
254
|
+
return rows[1] - rows[0] + 1 === clade.tips ? { rows } : undefined;
|
|
255
|
+
}
|
|
256
|
+
const node = clade.mrca ? mrca(root, clade.mrca, index) : undefined;
|
|
257
|
+
if (!node) {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
const tips = leaves(node);
|
|
261
|
+
if (tips.length !== clade.tips) {
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
let first = Infinity;
|
|
265
|
+
let last = -Infinity;
|
|
266
|
+
for (const tip of tips) {
|
|
267
|
+
const row = rowNamesSet.get(tip.data.name);
|
|
268
|
+
if (row !== undefined) {
|
|
269
|
+
first = Math.min(first, row);
|
|
270
|
+
last = Math.max(last, row);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return first <= last
|
|
274
|
+
? { rows: [first, last], nodeId: node.data.id }
|
|
275
|
+
: undefined;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Every clade that resolves, with its fill color settled, against the leaf
|
|
279
|
+
* order `rowNamesSet` gives. The tree panel resolves against the displayed
|
|
280
|
+
* rows and the overview against its own, so both take the tree they draw. A
|
|
281
|
+
* `range` record names no node, so `collapse` and `focus`, which need one,
|
|
282
|
+
* drop it.
|
|
283
|
+
*/
|
|
284
|
+
function resolveClades(clades, root, rowNamesSet) {
|
|
285
|
+
const index = leafIndex(root);
|
|
286
|
+
return clades.flatMap(clade => {
|
|
287
|
+
const resolved = cladeRows(clade, root, index, rowNamesSet);
|
|
288
|
+
const seeding = clade.mark === 'collapse' || clade.mark === 'focus';
|
|
289
|
+
return resolved && !(seeding && resolved.nodeId === undefined)
|
|
290
|
+
? [
|
|
291
|
+
{
|
|
292
|
+
...resolved,
|
|
293
|
+
mark: clade.mark,
|
|
294
|
+
color: withAlpha(clade.color ?? cladeHighlightColor, cladeHighlightAlpha),
|
|
295
|
+
markColor: clade.color,
|
|
296
|
+
label: clade.label,
|
|
297
|
+
},
|
|
298
|
+
]
|
|
299
|
+
: [];
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* The tree with the display transforms applied: leaf counts summed, children
|
|
304
|
+
* sorted by branch length, `showOnly` taken as the new root, and each
|
|
305
|
+
* `collapsed` clade folded. The overview builds the same tree without the
|
|
306
|
+
* focus, so it shows the focused subtree inside the whole.
|
|
307
|
+
*/
|
|
308
|
+
function buildTreeRoot(tree, collapsed, showOnly) {
|
|
309
|
+
let hier = hierarchy(tree, d => d.children);
|
|
310
|
+
hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1));
|
|
311
|
+
sort(hier, (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1));
|
|
312
|
+
if (showOnly) {
|
|
313
|
+
const res = find(hier, n => n.data.id === showOnly);
|
|
314
|
+
if (res) {
|
|
315
|
+
hier = res;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (const collapsedId of collapsed) {
|
|
319
|
+
const node = find(hier, n => n.data.id === collapsedId);
|
|
320
|
+
if (node) {
|
|
321
|
+
if (node.children) {
|
|
322
|
+
collapse(node);
|
|
323
|
+
}
|
|
324
|
+
else if (node.parent?.children) {
|
|
325
|
+
node.parent.children = node.parent.children.filter(c => c.data.id !== collapsedId);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return hier;
|
|
330
|
+
}
|
|
331
|
+
// the inclusive tip indices a node covers, from the row-space extent
|
|
332
|
+
// clusterLayout writes to xMin/xMax as tip centers
|
|
333
|
+
function tipRange(node) {
|
|
334
|
+
return [Math.round(node.xMin - 0.5), Math.round(node.xMax - 0.5)];
|
|
335
|
+
}
|
|
336
|
+
// the channels reading the feature table; every other channel reads rowData
|
|
337
|
+
const featureChannels = new Set([
|
|
338
|
+
'featureFill',
|
|
339
|
+
'featureLabel',
|
|
340
|
+
]);
|
|
341
|
+
// seqPos -> column indexes per row, keyed on the parse so they are garbage
|
|
342
|
+
// collected with it. A computed would rebuild every row's index when read
|
|
343
|
+
// outside a reactive context.
|
|
344
|
+
const seqPosIndexCache = new WeakMap();
|
|
345
|
+
// A segment is a 1:1 run. A segment whose sides differ in length is malformed,
|
|
346
|
+
// and the lookups treat it like an uncovered position.
|
|
347
|
+
function sameLength(segment) {
|
|
348
|
+
return (segment.rowEnd - segment.rowStart ===
|
|
349
|
+
segment.structEnd - segment.structStart);
|
|
350
|
+
}
|
|
351
|
+
// fit() divides the viewport by the row or column count and multiplies back, so
|
|
352
|
+
// an exact fit can land a fraction of a pixel over. A plain `>` would then show
|
|
353
|
+
// a scrollbar that shrinks the viewport and leaves a gap.
|
|
354
|
+
function overflows(content, viewport) {
|
|
355
|
+
return content - viewport > 0.5;
|
|
356
|
+
}
|
|
357
|
+
function inRanges(ranges, position) {
|
|
358
|
+
return !!ranges?.some(([start, end]) => position >= start && position <= end);
|
|
359
|
+
}
|
|
360
|
+
// the value every one of a node's children reports, or undefined where one of
|
|
361
|
+
// them has no value or they disagree
|
|
362
|
+
function sharedValue(values) {
|
|
363
|
+
const [first] = values;
|
|
364
|
+
return first !== undefined && values.every(v => v === first)
|
|
365
|
+
? first
|
|
366
|
+
: undefined;
|
|
367
|
+
}
|
|
368
|
+
// preorder, so reversing the list puts every node after its descendants
|
|
369
|
+
function preorder(tree) {
|
|
370
|
+
const order = [];
|
|
371
|
+
const stack = [tree];
|
|
372
|
+
while (stack.length > 0) {
|
|
373
|
+
const node = stack.pop();
|
|
374
|
+
order.push(node);
|
|
375
|
+
for (const child of node.children) {
|
|
376
|
+
stack.push(child);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return order;
|
|
95
380
|
}
|
|
96
381
|
/**
|
|
97
382
|
* #stateModel MsaView
|
|
@@ -128,10 +413,8 @@ function stateModelFactory() {
|
|
|
128
413
|
/**
|
|
129
414
|
* #property
|
|
130
415
|
* whether the domain legend is expanded. The legend floats over the
|
|
131
|
-
* top-right of the alignment, so
|
|
132
|
-
*
|
|
133
|
-
* and keep it collapsed, and what lets a session or a figure open with
|
|
134
|
-
* it already out of the way.
|
|
416
|
+
* top-right of the alignment and covers residues, so a session or
|
|
417
|
+
* figure can open with it collapsed.
|
|
135
418
|
*/
|
|
136
419
|
showDomainLegend: stripDefault(types.boolean, defaultShowDomainLegend),
|
|
137
420
|
/**
|
|
@@ -160,6 +443,11 @@ function stateModelFactory() {
|
|
|
160
443
|
* zoom in/out on plain mouse-wheel without holding ctrl
|
|
161
444
|
*/
|
|
162
445
|
scrollZoom: stripDefault(types.boolean, defaultScrollZoom),
|
|
446
|
+
/**
|
|
447
|
+
* #property
|
|
448
|
+
* which cell dimensions a wheel zoom scales, while `scrollZoom` is on
|
|
449
|
+
*/
|
|
450
|
+
scrollZoomAxis: stripDefault(types.enumeration('ScrollZoomAxis', [...scrollZoomAxes]), defaultScrollZoomAxis),
|
|
163
451
|
/**
|
|
164
452
|
* #property
|
|
165
453
|
* height of the div containing the view, px
|
|
@@ -231,11 +519,27 @@ function stateModelFactory() {
|
|
|
231
519
|
turnedOffTracks: stripDefault(types.map(types.boolean), {}),
|
|
232
520
|
/**
|
|
233
521
|
* #property
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
522
|
+
* the height of every track one divider resizes, keyed by `heightKey`:
|
|
523
|
+
* the `kind` for the computed tracks, `own:<id>` for a data track. A
|
|
524
|
+
* key is absent until the user drags that divider, and
|
|
525
|
+
* `defaultTrackHeights` answers for it until then, so an untouched
|
|
526
|
+
* viewer adds nothing to the shared URL.
|
|
527
|
+
*/
|
|
528
|
+
trackHeights: stripDefault(types.map(types.number), {}),
|
|
529
|
+
/**
|
|
530
|
+
* #property
|
|
531
|
+
* tracks supplied as data: per-column values drawn as bars, or a
|
|
532
|
+
* per-column string drawn as a text track. See docs/layers.md
|
|
237
533
|
*/
|
|
238
534
|
columnTracks: stripDefault(types.array(types.frozen()), []),
|
|
535
|
+
/**
|
|
536
|
+
* #property
|
|
537
|
+
* row-to-structure residue correspondence, computed outside the viewer
|
|
538
|
+
* (e.g. from SIFTS). Matching by sequence equality places a tagged
|
|
539
|
+
* construct, a truncation or a subsequence row on the wrong residue.
|
|
540
|
+
* See docs/layers.md
|
|
541
|
+
*/
|
|
542
|
+
residueMappings: stripDefault(types.array(types.frozen()), []),
|
|
239
543
|
/**
|
|
240
544
|
* #property
|
|
241
545
|
* data from the loaded tree/msa/treeMetadata, generally loaded by
|
|
@@ -248,8 +552,12 @@ function stateModelFactory() {
|
|
|
248
552
|
}),
|
|
249
553
|
/**
|
|
250
554
|
* #property
|
|
555
|
+
* the user's explicit hide choices per annotation accession, keyed by
|
|
556
|
+
* accession with the value meaning "off", like `turnedOffTracks`. An
|
|
557
|
+
* untouched accession is absent and drawn, so the shared URL grows
|
|
558
|
+
* only with the user's filters
|
|
251
559
|
*/
|
|
252
|
-
|
|
560
|
+
turnedOffFeatures: stripDefault(types.map(types.boolean), {}),
|
|
253
561
|
/**
|
|
254
562
|
* #property
|
|
255
563
|
*/
|
|
@@ -268,24 +576,53 @@ function stateModelFactory() {
|
|
|
268
576
|
* labeled highlights in 1-based inclusive coordinates: a column span
|
|
269
577
|
* `{start, end}`, a residue span `{row, start, end}` of a named row,
|
|
270
578
|
* or a row set `{rows}`, each with an optional `label` and `color`.
|
|
271
|
-
* Persists in the snapshot
|
|
579
|
+
* Persists in the snapshot and the URL.
|
|
272
580
|
*/
|
|
273
581
|
highlights: stripDefault(types.array(types.frozen()), []),
|
|
582
|
+
/**
|
|
583
|
+
* #property
|
|
584
|
+
* clades of the tree with a mark drawn over them. `mrca` names tips
|
|
585
|
+
* whose common ancestor is the clade, or `range` its first and last
|
|
586
|
+
* tip in display order, and `tips` is the leaf count the producer
|
|
587
|
+
* measured. `mark` is `highlight`, `bracket`, `collapse` or `focus`.
|
|
588
|
+
* See docs/layers.md
|
|
589
|
+
*/
|
|
590
|
+
clades: stripDefault(types.array(types.frozen()), []),
|
|
591
|
+
/**
|
|
592
|
+
* #property
|
|
593
|
+
* what the viewer's own marks read from `rowData`:
|
|
594
|
+
* `{channel, field, scale?}` per channel, where `channel` is
|
|
595
|
+
* `tipLabel` or `rowTint`. See docs/layers.md
|
|
596
|
+
*/
|
|
597
|
+
encodings: stripDefault(types.array(types.frozen()), []),
|
|
598
|
+
/**
|
|
599
|
+
* #property
|
|
600
|
+
* panels drawn between the tree and the alignment:
|
|
601
|
+
* `{kind: "strip", field, scale?, width?, header?}` colors one cell
|
|
602
|
+
* per row from a `rowData` field, and
|
|
603
|
+
* `{kind: "features", x, encoding?, transform?, width?, header?}`
|
|
604
|
+
* draws the spans the GFF carries. See docs/layers.md
|
|
605
|
+
*/
|
|
606
|
+
rowPanels: stripDefault(types.array(types.frozen()), []),
|
|
274
607
|
}))
|
|
275
608
|
.volatile(() => ({
|
|
276
609
|
/**
|
|
277
610
|
* #volatile
|
|
278
611
|
*/
|
|
279
612
|
headerHeight: 0,
|
|
613
|
+
/**
|
|
614
|
+
* #volatile
|
|
615
|
+
* leaves the toolbar out, for a host drawing its own controls. Kept out
|
|
616
|
+
* of the snapshot so a link opened in the full app shows the toolbar.
|
|
617
|
+
*/
|
|
618
|
+
hideHeader: false,
|
|
280
619
|
/**
|
|
281
620
|
* #volatile
|
|
282
621
|
*/
|
|
283
622
|
status: undefined,
|
|
284
623
|
/**
|
|
285
624
|
* #volatile
|
|
286
|
-
*
|
|
287
|
-
* screens. derived from the device pixel ratio so canvases are crisp on
|
|
288
|
-
* retina/4k displays and not needlessly oversized on standard ones
|
|
625
|
+
* canvas scale factor, from the device pixel ratio
|
|
289
626
|
*/
|
|
290
627
|
highResScaleFactor: typeof window === 'undefined' ? 1 : window.devicePixelRatio,
|
|
291
628
|
/**
|
|
@@ -340,6 +677,12 @@ function stateModelFactory() {
|
|
|
340
677
|
* array of column indices to highlight
|
|
341
678
|
*/
|
|
342
679
|
highlightedColumns: undefined,
|
|
680
|
+
/**
|
|
681
|
+
* #volatile
|
|
682
|
+
* transient highlights keyed by owner, so a structure viewer's hover and
|
|
683
|
+
* a genome view's hover each clear only their own. Not persisted.
|
|
684
|
+
*/
|
|
685
|
+
transientHighlights: {},
|
|
343
686
|
/**
|
|
344
687
|
* #volatile
|
|
345
688
|
*/
|
|
@@ -347,29 +690,36 @@ function stateModelFactory() {
|
|
|
347
690
|
/**
|
|
348
691
|
* #volatile
|
|
349
692
|
*/
|
|
350
|
-
|
|
693
|
+
marginLeft: 20,
|
|
694
|
+
/**
|
|
695
|
+
* #volatile
|
|
696
|
+
*/
|
|
697
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
|
|
698
|
+
error: undefined,
|
|
351
699
|
/**
|
|
352
700
|
* #volatile
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
701
|
+
* non-fatal load problems: an optional layer that failed to load, an
|
|
702
|
+
* overlay that failed to parse. `error` replaces the view and is for the
|
|
703
|
+
* alignment itself
|
|
356
704
|
*/
|
|
357
|
-
|
|
705
|
+
warnings: [],
|
|
358
706
|
/**
|
|
359
707
|
* #volatile
|
|
708
|
+
* bumped by reset(). The error boundary above the view uses it as its key,
|
|
709
|
+
* since the boundary keeps its caught error until remounted
|
|
360
710
|
*/
|
|
361
|
-
|
|
711
|
+
resetCount: 0,
|
|
362
712
|
/**
|
|
363
713
|
* #volatile
|
|
714
|
+
* set by a host that restores the loaded documents itself, such as a
|
|
715
|
+
* jbrowse session or a page that refetches them. `unshareableData` then
|
|
716
|
+
* reports nothing
|
|
364
717
|
*/
|
|
365
|
-
|
|
366
|
-
error: undefined,
|
|
718
|
+
hostCarriesData: false,
|
|
367
719
|
/**
|
|
368
720
|
* #volatile
|
|
369
|
-
* overlay annotations drawn on the alignment,
|
|
370
|
-
*
|
|
371
|
-
* flat list before it reaches the model, so nothing downstream of here
|
|
372
|
-
* knows which one it came from
|
|
721
|
+
* overlay annotations drawn on the alignment. InterProScan JSON, GFF and
|
|
722
|
+
* user uploads all convert to this flat list
|
|
373
723
|
*/
|
|
374
724
|
annotations: [],
|
|
375
725
|
}))
|
|
@@ -415,8 +765,8 @@ function stateModelFactory() {
|
|
|
415
765
|
},
|
|
416
766
|
/**
|
|
417
767
|
* #action
|
|
418
|
-
*
|
|
419
|
-
*
|
|
768
|
+
* update the canvas scale factor when the device pixel ratio changes
|
|
769
|
+
* (moving between monitors, browser zoom)
|
|
420
770
|
*/
|
|
421
771
|
setHighResScaleFactor(arg) {
|
|
422
772
|
self.highResScaleFactor = arg;
|
|
@@ -435,13 +785,35 @@ function stateModelFactory() {
|
|
|
435
785
|
setError(error) {
|
|
436
786
|
self.error = error;
|
|
437
787
|
},
|
|
788
|
+
/**
|
|
789
|
+
* #action
|
|
790
|
+
* record a non-fatal load problem: a layer that failed to load, a file
|
|
791
|
+
* that failed to parse
|
|
792
|
+
*/
|
|
793
|
+
addWarning(warning) {
|
|
794
|
+
self.warnings = [...self.warnings, warning];
|
|
795
|
+
},
|
|
796
|
+
/**
|
|
797
|
+
* #action
|
|
798
|
+
*/
|
|
799
|
+
clearWarnings() {
|
|
800
|
+
self.warnings = [];
|
|
801
|
+
},
|
|
802
|
+
/**
|
|
803
|
+
* #action
|
|
804
|
+
* declare that this host restores the loaded documents itself, which
|
|
805
|
+
* hides the "Not in the link" warning. See `hostCarriesData`
|
|
806
|
+
*/
|
|
807
|
+
setHostCarriesData(arg) {
|
|
808
|
+
self.hostCarriesData = arg;
|
|
809
|
+
},
|
|
438
810
|
/**
|
|
439
811
|
* #action
|
|
440
812
|
* set mouse position (row, column) in the MSA
|
|
441
813
|
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* signature stable
|
|
814
|
+
* Public API: a host calls this (and reads `mouseCol`) to sync hover with
|
|
815
|
+
* its own view, such as a genome view or 3D structure. Keep the name and
|
|
816
|
+
* signature stable.
|
|
445
817
|
*/
|
|
446
818
|
setMousePos(col, row) {
|
|
447
819
|
self.mouseCol = col;
|
|
@@ -451,9 +823,9 @@ function stateModelFactory() {
|
|
|
451
823
|
* #action
|
|
452
824
|
* set highlighted columns
|
|
453
825
|
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
826
|
+
* Public API: jbrowse-plugin-msaview calls this from its
|
|
827
|
+
* afterCreateAutoruns, and MSAViewer passes its `highlightColumns` prop
|
|
828
|
+
* through it.
|
|
457
829
|
*/
|
|
458
830
|
setHighlightedColumns(columns) {
|
|
459
831
|
self.highlightedColumns = columns;
|
|
@@ -464,6 +836,34 @@ function stateModelFactory() {
|
|
|
464
836
|
setHighlights(highlights) {
|
|
465
837
|
self.highlights.replace(highlights);
|
|
466
838
|
},
|
|
839
|
+
/**
|
|
840
|
+
* #action
|
|
841
|
+
* replace the clades the viewer marks (see docs/layers.md)
|
|
842
|
+
*/
|
|
843
|
+
setClades(clades) {
|
|
844
|
+
self.clades.replace(clades);
|
|
845
|
+
},
|
|
846
|
+
/**
|
|
847
|
+
* #action
|
|
848
|
+
* show `highlights` for `owner`, replacing that owner's previous ones and
|
|
849
|
+
* leaving other owners' in place
|
|
850
|
+
*/
|
|
851
|
+
applyHighlight(owner, highlights) {
|
|
852
|
+
self.transientHighlights = {
|
|
853
|
+
...self.transientHighlights,
|
|
854
|
+
[owner]: highlights,
|
|
855
|
+
};
|
|
856
|
+
},
|
|
857
|
+
/**
|
|
858
|
+
* #action
|
|
859
|
+
* remove `owner`'s highlights, leaving other owners' in place
|
|
860
|
+
*/
|
|
861
|
+
clearHighlight(owner) {
|
|
862
|
+
if (owner in self.transientHighlights) {
|
|
863
|
+
const { [owner]: _dropped, ...rest } = self.transientHighlights;
|
|
864
|
+
self.transientHighlights = rest;
|
|
865
|
+
}
|
|
866
|
+
},
|
|
467
867
|
/**
|
|
468
868
|
* #action
|
|
469
869
|
* toggle the annotation overlay on the alignment
|
|
@@ -508,10 +908,22 @@ function stateModelFactory() {
|
|
|
508
908
|
},
|
|
509
909
|
/**
|
|
510
910
|
* #action
|
|
511
|
-
*
|
|
911
|
+
* switch to another alignment of a multi-alignment file (Stockholm).
|
|
912
|
+
* Clears the collapsed node ids, the subtree in focus, the reference row
|
|
913
|
+
* and the scroll position, which all refer to the previous alignment
|
|
512
914
|
*/
|
|
513
915
|
setCurrentAlignment(n) {
|
|
514
|
-
self.currentAlignment
|
|
916
|
+
if (n === self.currentAlignment) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
transaction(() => {
|
|
920
|
+
self.currentAlignment = n;
|
|
921
|
+
self.collapsed.clear();
|
|
922
|
+
this.setShowOnly(undefined);
|
|
923
|
+
this.drawRelativeTo(undefined);
|
|
924
|
+
self.scrollX = 0;
|
|
925
|
+
self.scrollY = 0;
|
|
926
|
+
});
|
|
515
927
|
},
|
|
516
928
|
/**
|
|
517
929
|
* #action
|
|
@@ -583,6 +995,16 @@ function stateModelFactory() {
|
|
|
583
995
|
setTreeMetadata(result) {
|
|
584
996
|
self.data.setTreeMetadata(result);
|
|
585
997
|
},
|
|
998
|
+
/**
|
|
999
|
+
* #action
|
|
1000
|
+
* store the GFF text in the snapshot like the alignment and tree. The
|
|
1001
|
+
* parsed annotations are volatile and a blob filehandle is cleared once
|
|
1002
|
+
* read, so the text is the only persisted copy. An autorun parses it
|
|
1003
|
+
* into annotations.
|
|
1004
|
+
*/
|
|
1005
|
+
setGFF(result) {
|
|
1006
|
+
self.data.setGFF(result);
|
|
1007
|
+
},
|
|
586
1008
|
}))
|
|
587
1009
|
.views(self => ({
|
|
588
1010
|
/**
|
|
@@ -605,6 +1027,20 @@ function stateModelFactory() {
|
|
|
605
1027
|
get actuallyShowDomains() {
|
|
606
1028
|
return self.showDomains && self.annotations.length > 0;
|
|
607
1029
|
},
|
|
1030
|
+
/**
|
|
1031
|
+
* #getter
|
|
1032
|
+
* whether the host restores the loaded documents outside the snapshot.
|
|
1033
|
+
* When true, `unshareableData` is empty.
|
|
1034
|
+
*
|
|
1035
|
+
* A simple host sets `hostCarriesData`. A host where this depends on how
|
|
1036
|
+
* the view was opened overrides the getter in its own composed model's
|
|
1037
|
+
* `.views` block; jbrowse-plugin-msaview's indexed-location views refetch
|
|
1038
|
+
* from a URL the session holds, while its data-store views do not.
|
|
1039
|
+
* `unshareableData` reads it off `self`, so an override takes effect.
|
|
1040
|
+
*/
|
|
1041
|
+
get hostRestoresData() {
|
|
1042
|
+
return self.hostCarriesData;
|
|
1043
|
+
},
|
|
608
1044
|
get viewInitialized() {
|
|
609
1045
|
return self.volatileWidth !== undefined;
|
|
610
1046
|
},
|
|
@@ -619,19 +1055,12 @@ function stateModelFactory() {
|
|
|
619
1055
|
},
|
|
620
1056
|
}))
|
|
621
1057
|
.views(self => ({
|
|
622
|
-
/**
|
|
623
|
-
* #method
|
|
624
|
-
* unused here, but can be used by derived classes to add extra items
|
|
625
|
-
*/
|
|
626
|
-
extraViewMenuItems() {
|
|
627
|
-
return [];
|
|
628
|
-
},
|
|
629
1058
|
/**
|
|
630
1059
|
* #getter
|
|
631
1060
|
*/
|
|
632
1061
|
get colorScheme() {
|
|
633
|
-
// colorSchemeName is a free string (menus, snapshots, URL params)
|
|
634
|
-
// back to the default
|
|
1062
|
+
// colorSchemeName is a free string (menus, snapshots, URL params), so a
|
|
1063
|
+
// stale name falls back to the default
|
|
635
1064
|
return (colorSchemes[self.colorSchemeName] ??
|
|
636
1065
|
colorSchemes[defaultColorSchemeName]);
|
|
637
1066
|
},
|
|
@@ -656,18 +1085,53 @@ function stateModelFactory() {
|
|
|
656
1085
|
get noDomains() {
|
|
657
1086
|
return self.annotations.length === 0;
|
|
658
1087
|
},
|
|
1088
|
+
/**
|
|
1089
|
+
* #getter
|
|
1090
|
+
* loaded documents left out of the snapshot, largest first. A file opened
|
|
1091
|
+
* from disk or pasted in becomes inline text, and DataModel drops an
|
|
1092
|
+
* inline document past `maxInlineSnapshotBytes`.
|
|
1093
|
+
*
|
|
1094
|
+
* The header lists these, and the standalone app stops rewriting the
|
|
1095
|
+
* address bar while the list is non-empty, so a copied link does not
|
|
1096
|
+
* open an empty viewer unannounced. A document fetched from a URL never
|
|
1097
|
+
* appears here, since the snapshot keeps its filehandle.
|
|
1098
|
+
*
|
|
1099
|
+
* Empty when `hostRestoresData` is true.
|
|
1100
|
+
*/
|
|
1101
|
+
get unshareableData() {
|
|
1102
|
+
if (self.hostRestoresData) {
|
|
1103
|
+
return [];
|
|
1104
|
+
}
|
|
1105
|
+
const { data } = self;
|
|
1106
|
+
// data tracks past the limit also leave the snapshot
|
|
1107
|
+
const trackBytes = columnTrackSizes(self.columnTracks)
|
|
1108
|
+
.filter(bytes => bytes > maxInlineSnapshotBytes)
|
|
1109
|
+
.reduce((a, b) => a + b, 0);
|
|
1110
|
+
return [
|
|
1111
|
+
['alignment', data.msa, self.msaFilehandle],
|
|
1112
|
+
['tree', data.tree, self.treeFilehandle],
|
|
1113
|
+
['annotations', data.gff, self.gffFilehandle],
|
|
1114
|
+
['row metadata', data.treeMetadata, self.treeMetadataFilehandle],
|
|
1115
|
+
]
|
|
1116
|
+
.flatMap(([what, text, filehandle]) => !filehandle && text && text.length > maxInlineSnapshotBytes
|
|
1117
|
+
? [{ what, bytes: text.length }]
|
|
1118
|
+
: [])
|
|
1119
|
+
.concat(trackBytes ? [{ what: 'data tracks', bytes: trackBytes }] : [])
|
|
1120
|
+
.sort((a, b) => b.bytes - a.bytes);
|
|
1121
|
+
},
|
|
659
1122
|
menuItems() {
|
|
660
1123
|
return [];
|
|
661
1124
|
},
|
|
662
1125
|
/**
|
|
663
1126
|
* #getter
|
|
664
|
-
* extra per
|
|
665
|
-
*
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
1127
|
+
* the row table: extra fields per row, keyed by row name, which the
|
|
1128
|
+
* `encodings` channels read. It is stored as the JSON string
|
|
1129
|
+
* `data.treeMetadata`, the name that travels in existing links, so the
|
|
1130
|
+
* inline size limit and `treeMetadataFilehandle` cover it. labelWidthMap
|
|
1131
|
+
* reads it on every layout, so a malformed user-supplied file returns {}
|
|
1132
|
+
* instead of throwing out of rendering.
|
|
669
1133
|
*/
|
|
670
|
-
get
|
|
1134
|
+
get rowData() {
|
|
671
1135
|
const text = self.data.treeMetadata;
|
|
672
1136
|
if (!text) {
|
|
673
1137
|
return {};
|
|
@@ -679,10 +1143,31 @@ function stateModelFactory() {
|
|
|
679
1143
|
: {};
|
|
680
1144
|
}
|
|
681
1145
|
catch (e) {
|
|
682
|
-
console.error('failed to parse
|
|
1146
|
+
console.error('failed to parse rowData', e);
|
|
683
1147
|
return {};
|
|
684
1148
|
}
|
|
685
1149
|
},
|
|
1150
|
+
/**
|
|
1151
|
+
* #method
|
|
1152
|
+
* one row's fields, the single reader of the row table
|
|
1153
|
+
*/
|
|
1154
|
+
rowDataOf(name) {
|
|
1155
|
+
return this.rowData[name];
|
|
1156
|
+
},
|
|
1157
|
+
/**
|
|
1158
|
+
* #getter
|
|
1159
|
+
* the field names the row table carries, sorted, for a producer or a UI
|
|
1160
|
+
* choosing one to encode
|
|
1161
|
+
*/
|
|
1162
|
+
get rowFields() {
|
|
1163
|
+
const fields = new Set();
|
|
1164
|
+
for (const row of Object.values(this.rowData)) {
|
|
1165
|
+
for (const field of Object.keys(row ?? {})) {
|
|
1166
|
+
fields.add(field);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
return [...fields].sort((a, b) => a.localeCompare(b));
|
|
1170
|
+
},
|
|
686
1171
|
/**
|
|
687
1172
|
* #getter
|
|
688
1173
|
*/
|
|
@@ -728,6 +1213,33 @@ function stateModelFactory() {
|
|
|
728
1213
|
get rowNamesSet() {
|
|
729
1214
|
return new Map(this.leaves.map((leaf, index) => [leaf.data.name, index]));
|
|
730
1215
|
},
|
|
1216
|
+
/**
|
|
1217
|
+
* #getter
|
|
1218
|
+
* `clades` resolved to the rows each one covers. The tip names resolve
|
|
1219
|
+
* against `tree` rather than `root`, so a clade whose ancestor the user
|
|
1220
|
+
* collapsed keeps its rows. One leaf pass over the tree serves every
|
|
1221
|
+
* clade. A `range` record names no node, so `collapse` and `focus`, which
|
|
1222
|
+
* need one, drop it.
|
|
1223
|
+
*/
|
|
1224
|
+
get resolvedClades() {
|
|
1225
|
+
if (self.clades.length === 0) {
|
|
1226
|
+
return noClades;
|
|
1227
|
+
}
|
|
1228
|
+
return resolveClades(self.clades, hierarchy(this.tree, d => d.children), this.rowNamesSet);
|
|
1229
|
+
},
|
|
1230
|
+
/**
|
|
1231
|
+
* #getter
|
|
1232
|
+
* the pixel column reserved at the right of the tree area for the bracket
|
|
1233
|
+
* mark, which the tip labels and the tree itself stay clear of. Zero
|
|
1234
|
+
* where no clade draws a bar or a label.
|
|
1235
|
+
*/
|
|
1236
|
+
get cladeGutterWidth() {
|
|
1237
|
+
return cladeGutterWidth({
|
|
1238
|
+
clades: this.resolvedClades,
|
|
1239
|
+
rowHeight: self.rowHeight,
|
|
1240
|
+
fontSize: this.fontSize,
|
|
1241
|
+
});
|
|
1242
|
+
},
|
|
731
1243
|
/**
|
|
732
1244
|
* #getter
|
|
733
1245
|
*/
|
|
@@ -760,40 +1272,128 @@ function stateModelFactory() {
|
|
|
760
1272
|
* #getter
|
|
761
1273
|
*/
|
|
762
1274
|
get root() {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1275
|
+
return buildTreeRoot(this.tree, self.collapsed, self.showOnly);
|
|
1276
|
+
},
|
|
1277
|
+
/**
|
|
1278
|
+
* #getter
|
|
1279
|
+
* height of the band the tree overview draws in, zero when it is off
|
|
1280
|
+
*/
|
|
1281
|
+
get treeOverviewHeight() {
|
|
1282
|
+
return self.showTreeOverview ? self.overviewHeight : 0;
|
|
1283
|
+
},
|
|
1284
|
+
/**
|
|
1285
|
+
* #getter
|
|
1286
|
+
* the whole tree laid out for the overview, or undefined when the
|
|
1287
|
+
* overview is off. The focus is left out, so the focused subtree draws
|
|
1288
|
+
* inside the whole tree, and the collapsed clades are folded, since
|
|
1289
|
+
* those are rows the view no longer has. `x` is in tip-index space and
|
|
1290
|
+
* `len` is a fraction of the root-to-tip length, so one layout serves
|
|
1291
|
+
* any band size.
|
|
1292
|
+
*/
|
|
1293
|
+
get treeOverviewLayout() {
|
|
1294
|
+
if (!self.showTreeOverview) {
|
|
1295
|
+
return undefined;
|
|
771
1296
|
}
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
1297
|
+
const root = buildTreeRoot(this.tree, self.collapsed);
|
|
1298
|
+
const numTips = leaves(root).length;
|
|
1299
|
+
clusterLayout(root, numTips, 1);
|
|
1300
|
+
const rootLen = Math.max(root.data.length || 0, 0);
|
|
1301
|
+
const extent = maxLength(root) - rootLen;
|
|
1302
|
+
setBrLength(root, -rootLen, extent ? 1 / extent : 0);
|
|
1303
|
+
return {
|
|
1304
|
+
root,
|
|
1305
|
+
numTips,
|
|
1306
|
+
maxDepthToLeaf: calcDepthToLeaf(root),
|
|
1307
|
+
showBranchLen: self.showBranchLen && extent > 0,
|
|
1308
|
+
};
|
|
1309
|
+
},
|
|
1310
|
+
/**
|
|
1311
|
+
* #getter
|
|
1312
|
+
* the `clades` highlights in the overview's own row space, which the
|
|
1313
|
+
* focus does not narrow
|
|
1314
|
+
*/
|
|
1315
|
+
get treeOverviewClades() {
|
|
1316
|
+
const layout = this.treeOverviewLayout;
|
|
1317
|
+
if (!layout || self.clades.length === 0) {
|
|
1318
|
+
return noClades;
|
|
1319
|
+
}
|
|
1320
|
+
const rowNames = new Map(leaves(layout.root).map((leaf, index) => [leaf.data.name, index]));
|
|
1321
|
+
return resolveClades(self.clades, layout.root, rowNames);
|
|
1322
|
+
},
|
|
1323
|
+
/**
|
|
1324
|
+
* #getter
|
|
1325
|
+
* the inclusive tip rows the focused subtree covers in the overview,
|
|
1326
|
+
* which is the box drawn on it. undefined with no focus
|
|
1327
|
+
*/
|
|
1328
|
+
get treeOverviewFocusRows() {
|
|
1329
|
+
const layout = this.treeOverviewLayout;
|
|
1330
|
+
if (!layout || !self.showOnly) {
|
|
1331
|
+
return undefined;
|
|
1332
|
+
}
|
|
1333
|
+
const node = find(layout.root, n => n.data.id === self.showOnly);
|
|
1334
|
+
return node ? tipRange(node) : undefined;
|
|
1335
|
+
},
|
|
1336
|
+
/**
|
|
1337
|
+
* #method
|
|
1338
|
+
* the subtree a point `y` pixels down the tree overview picks: the
|
|
1339
|
+
* deepest one whose tip range covers every row under that pixel, with
|
|
1340
|
+
* the rows it covers. A pixel stands for several tips on a large tree,
|
|
1341
|
+
* which is what keeps the pick off the individual tips. undefined when
|
|
1342
|
+
* the overview is off or the point picks the whole tree.
|
|
1343
|
+
*/
|
|
1344
|
+
treeOverviewHit(y) {
|
|
1345
|
+
const layout = this.treeOverviewLayout;
|
|
1346
|
+
if (!layout) {
|
|
1347
|
+
return undefined;
|
|
782
1348
|
}
|
|
783
|
-
|
|
1349
|
+
const { root, numTips } = layout;
|
|
1350
|
+
const perPixel = numTips / self.overviewHeight;
|
|
1351
|
+
const first = clamp(Math.floor(y * perPixel), 0, numTips - 1);
|
|
1352
|
+
const last = clamp(Math.ceil((y + 1) * perPixel) - 1, first, numTips - 1);
|
|
1353
|
+
let node = nodeCoveringRows(root, first + 0.5, last + 0.5);
|
|
1354
|
+
// focusing one tip leaves a single row on screen, so the pick lifts to
|
|
1355
|
+
// the subtree that tip sits in
|
|
1356
|
+
while (!node.children && node.parent) {
|
|
1357
|
+
node = node.parent;
|
|
1358
|
+
}
|
|
1359
|
+
return node === root
|
|
1360
|
+
? undefined
|
|
1361
|
+
: { id: node.data.id, rows: tipRange(node) };
|
|
1362
|
+
},
|
|
1363
|
+
/**
|
|
1364
|
+
* #getter
|
|
1365
|
+
* the pixel column the row panels occupy between the tree and the
|
|
1366
|
+
* alignment, the sum of each record's width
|
|
1367
|
+
*/
|
|
1368
|
+
get rowPanelsWidth() {
|
|
1369
|
+
return sum(self.rowPanels.map(p => rowPanelWidth(p, self.rowHeight)));
|
|
1370
|
+
},
|
|
1371
|
+
/**
|
|
1372
|
+
* #getter
|
|
1373
|
+
* height of the band the row panel headers draw in, which is zero with
|
|
1374
|
+
* no row panels and leaves the top area as it was
|
|
1375
|
+
*/
|
|
1376
|
+
get rowPanelsHeaderHeight() {
|
|
1377
|
+
return self.rowPanels.length > 0 ? rowPanelHeaderHeight : 0;
|
|
784
1378
|
},
|
|
785
1379
|
/**
|
|
786
1380
|
* #getter
|
|
787
|
-
* widget width minus the tree area
|
|
1381
|
+
* widget width minus the tree area and the row panels gives the space
|
|
1382
|
+
* for the MSA
|
|
788
1383
|
*/
|
|
789
1384
|
get msaAreaWidth() {
|
|
790
|
-
return self.width -
|
|
1385
|
+
return (self.width -
|
|
1386
|
+
self.treeAreaWidth -
|
|
1387
|
+
this.rowPanelsWidth -
|
|
1388
|
+
self.resizeHandleWidth);
|
|
791
1389
|
},
|
|
792
1390
|
/**
|
|
793
1391
|
* #getter
|
|
1392
|
+
* the right edge the tip labels end at, which is the tree area less the
|
|
1393
|
+
* margin and the bracket gutter
|
|
794
1394
|
*/
|
|
795
1395
|
get treeAreaWidthMinusMargin() {
|
|
796
|
-
return self.treeAreaWidth - self.marginLeft;
|
|
1396
|
+
return self.treeAreaWidth - self.marginLeft - this.cladeGutterWidth;
|
|
797
1397
|
},
|
|
798
1398
|
/**
|
|
799
1399
|
* #getter
|
|
@@ -863,28 +1463,50 @@ function stateModelFactory() {
|
|
|
863
1463
|
},
|
|
864
1464
|
/**
|
|
865
1465
|
* #getter
|
|
866
|
-
* number of rows
|
|
867
|
-
*
|
|
868
|
-
* row of vertical space (drawn blank), so row hit-testing and fit-to-height
|
|
869
|
-
* must count it.
|
|
1466
|
+
* number of rows on screen: the leaf count, which includes tree leaves
|
|
1467
|
+
* with no matching MSA row (drawn blank), unlike `rows.length`.
|
|
870
1468
|
*/
|
|
871
1469
|
get numRows() {
|
|
872
1470
|
return this.leaves.length;
|
|
873
1471
|
},
|
|
874
1472
|
/**
|
|
875
|
-
* #
|
|
876
|
-
*
|
|
877
|
-
*
|
|
878
|
-
*
|
|
1473
|
+
* #method
|
|
1474
|
+
* index of the global column holding each ungapped sequence position of a
|
|
1475
|
+
* row. The domain overlay resolves thousands of these per redraw. Built
|
|
1476
|
+
* lazily per row and cached on the parse.
|
|
879
1477
|
*/
|
|
880
|
-
|
|
881
|
-
|
|
1478
|
+
seqPosIndex(rowName) {
|
|
1479
|
+
const MSA = this.MSA;
|
|
1480
|
+
if (!MSA) {
|
|
1481
|
+
return undefined;
|
|
1482
|
+
}
|
|
1483
|
+
let cache = seqPosIndexCache.get(MSA);
|
|
1484
|
+
if (!cache) {
|
|
1485
|
+
cache = new Map();
|
|
1486
|
+
seqPosIndexCache.set(MSA, cache);
|
|
1487
|
+
}
|
|
1488
|
+
let index = cache.get(rowName);
|
|
1489
|
+
if (!index) {
|
|
1490
|
+
const seq = MSA.getRow(rowName);
|
|
1491
|
+
if (!seq) {
|
|
1492
|
+
return undefined;
|
|
1493
|
+
}
|
|
1494
|
+
index = buildSeqPosIndex(seq);
|
|
1495
|
+
cache.set(rowName, index);
|
|
1496
|
+
}
|
|
1497
|
+
return index;
|
|
882
1498
|
},
|
|
883
1499
|
/**
|
|
884
1500
|
* #getter
|
|
1501
|
+
* every sequence in the alignment, keyed by row name, including rows a
|
|
1502
|
+
* collapsed clade hides. `rows` holds only the rows on screen; lookups
|
|
1503
|
+
* by row name use this
|
|
885
1504
|
*/
|
|
886
1505
|
get rowMap() {
|
|
887
|
-
|
|
1506
|
+
const MSA = this.MSA;
|
|
1507
|
+
return new Map(MSA?.getNames()
|
|
1508
|
+
.map(name => [name, MSA.getRow(name)])
|
|
1509
|
+
.filter(([, seq]) => !!seq));
|
|
888
1510
|
},
|
|
889
1511
|
/**
|
|
890
1512
|
* #getter
|
|
@@ -1009,10 +1631,11 @@ function stateModelFactory() {
|
|
|
1009
1631
|
get hierarchy() {
|
|
1010
1632
|
const r = this.root;
|
|
1011
1633
|
clusterLayout(r, this.totalHeight, self.treeWidth);
|
|
1012
|
-
|
|
1013
|
-
const max = maxLength(r);
|
|
1634
|
+
const max = this.rootToTipLength;
|
|
1014
1635
|
const k = max ? self.treeWidth / max : 0;
|
|
1015
|
-
|
|
1636
|
+
// the displayed root starts at x=0, so subtract its length here; `root`
|
|
1637
|
+
// returns the cached parse, which must not be mutated
|
|
1638
|
+
setBrLength(r, -Math.max(r.data.length || 0, 0), k);
|
|
1016
1639
|
// for each collapsed clade, record the pixel x-position of its farthest
|
|
1017
1640
|
// tip so the renderer can draw a triangle spanning the branch-length
|
|
1018
1641
|
// extent of the hidden subtree
|
|
@@ -1038,10 +1661,29 @@ function stateModelFactory() {
|
|
|
1038
1661
|
},
|
|
1039
1662
|
/**
|
|
1040
1663
|
* #getter
|
|
1041
|
-
*
|
|
1664
|
+
* branch-length extent of the displayed tree, root to farthest tip, in
|
|
1665
|
+
* the tree's own units
|
|
1666
|
+
*/
|
|
1667
|
+
get rootToTipLength() {
|
|
1668
|
+
const r = this.root;
|
|
1669
|
+
return maxLength(r) - Math.max(r.data.length || 0, 0);
|
|
1670
|
+
},
|
|
1671
|
+
/**
|
|
1672
|
+
* #getter
|
|
1673
|
+
* x-position of the farthest tip in a phylogram, px: treeWidth, or 0 for
|
|
1674
|
+
* a tree with no branch lengths (drawn as a cladogram)
|
|
1042
1675
|
*/
|
|
1043
1676
|
get maxBranchLength() {
|
|
1044
|
-
return
|
|
1677
|
+
return this.rootToTipLength ? self.treeWidth : 0;
|
|
1678
|
+
},
|
|
1679
|
+
/**
|
|
1680
|
+
* #getter
|
|
1681
|
+
* pixels per unit of branch length in the phylogram layout, 0 in
|
|
1682
|
+
* cladogram mode. The tree's scale bar uses it.
|
|
1683
|
+
*/
|
|
1684
|
+
get pxPerBranchLength() {
|
|
1685
|
+
const max = maxLength(this.root);
|
|
1686
|
+
return this.showBranchLenEffective && max ? self.treeWidth / max : 0;
|
|
1045
1687
|
},
|
|
1046
1688
|
/**
|
|
1047
1689
|
* #getter
|
|
@@ -1054,7 +1696,7 @@ function stateModelFactory() {
|
|
|
1054
1696
|
* #getter
|
|
1055
1697
|
*/
|
|
1056
1698
|
get allBranchesLength0() {
|
|
1057
|
-
return
|
|
1699
|
+
return this.rootToTipLength === 0;
|
|
1058
1700
|
},
|
|
1059
1701
|
/**
|
|
1060
1702
|
* #getter
|
|
@@ -1076,88 +1718,711 @@ function stateModelFactory() {
|
|
|
1076
1718
|
/**
|
|
1077
1719
|
* #getter
|
|
1078
1720
|
*/
|
|
1079
|
-
get
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
// the import form
|
|
1085
|
-
return !!(self.data.msa || self.data.tree) && !self.error;
|
|
1086
|
-
},
|
|
1087
|
-
/**
|
|
1088
|
-
* #getter
|
|
1089
|
-
*/
|
|
1090
|
-
get blocksX() {
|
|
1091
|
-
return calculateBlocks({
|
|
1092
|
-
viewportSize: self.msaAreaWidth,
|
|
1093
|
-
viewportPos: -self.scrollX,
|
|
1094
|
-
blockSize: self.blockSize,
|
|
1095
|
-
mapSize: self.totalWidth,
|
|
1096
|
-
});
|
|
1097
|
-
},
|
|
1098
|
-
/**
|
|
1099
|
-
* #getter
|
|
1100
|
-
*/
|
|
1101
|
-
get blocksY() {
|
|
1102
|
-
return calculateBlocks({
|
|
1103
|
-
viewportSize: this.visibleMsaHeight,
|
|
1104
|
-
viewportPos: -self.scrollY,
|
|
1105
|
-
blockSize: self.blockSize,
|
|
1106
|
-
mapSize: self.totalHeight,
|
|
1107
|
-
});
|
|
1721
|
+
get showMsaLetters() {
|
|
1722
|
+
return (self.drawMsaLetters &&
|
|
1723
|
+
self.rowHeight >= minLetterRowHeight &&
|
|
1724
|
+
self.colWidth >= minLetterColWidth &&
|
|
1725
|
+
self.colWidth > self.rowHeight / 2);
|
|
1108
1726
|
},
|
|
1109
1727
|
/**
|
|
1110
1728
|
* #getter
|
|
1111
|
-
* height of the alignment viewport, px. The same subtraction as
|
|
1112
|
-
* msaAreaHeight, which is defined later in the views chain
|
|
1113
1729
|
*/
|
|
1114
|
-
get
|
|
1115
|
-
return
|
|
1116
|
-
self.headerHeight -
|
|
1117
|
-
(self.msaAreaWidth < self.totalWidth ? self.minimapHeight : 0));
|
|
1730
|
+
get showTreeText() {
|
|
1731
|
+
return self.drawLabels && self.rowHeight >= minLetterRowHeight;
|
|
1118
1732
|
},
|
|
1119
1733
|
}))
|
|
1120
1734
|
.views(self => ({
|
|
1121
1735
|
/**
|
|
1122
1736
|
* #getter
|
|
1123
1737
|
*/
|
|
1124
|
-
get
|
|
1125
|
-
|
|
1738
|
+
get labelWidthMap() {
|
|
1739
|
+
const { showTreeText, leaves } = self;
|
|
1740
|
+
// gated on the renderer's label condition, so hidden labels reserve no
|
|
1741
|
+
// gutter. Measured once at a reference size and scaled by
|
|
1742
|
+
// labelWidthScale: re-measuring per vertical-zoom frame cost ~200ms on a
|
|
1743
|
+
// 50k-leaf tree
|
|
1744
|
+
return showTreeText
|
|
1745
|
+
? new Map(leaves.map(node => {
|
|
1746
|
+
const { name } = node.data;
|
|
1747
|
+
// `||`, matching renderTreeLabels: an empty genome falls back
|
|
1748
|
+
// to the row name
|
|
1749
|
+
const displayName = self.rowDataOf(name)?.genome || name;
|
|
1750
|
+
return [
|
|
1751
|
+
name,
|
|
1752
|
+
measureTextCanvas(displayName, labelReferenceFontSize),
|
|
1753
|
+
];
|
|
1754
|
+
}))
|
|
1755
|
+
: new Map();
|
|
1126
1756
|
},
|
|
1127
1757
|
/**
|
|
1128
1758
|
* #getter
|
|
1759
|
+
* factor turning a labelWidthMap entry into its width at the current
|
|
1760
|
+
* font size
|
|
1129
1761
|
*/
|
|
1130
|
-
get
|
|
1131
|
-
return self.
|
|
1762
|
+
get labelWidthScale() {
|
|
1763
|
+
return self.fontSize / labelReferenceFontSize;
|
|
1764
|
+
},
|
|
1765
|
+
get labelsWidth() {
|
|
1766
|
+
// a loop, not Math.max(...): spreading passes one argument per leaf,
|
|
1767
|
+
// and the ~125k argument limit throws RangeError on the 230k-tip COVID
|
|
1768
|
+
// tree
|
|
1769
|
+
let max = 0;
|
|
1770
|
+
for (const width of this.labelWidthMap.values()) {
|
|
1771
|
+
if (width > max) {
|
|
1772
|
+
max = width;
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
return max * this.labelWidthScale;
|
|
1132
1776
|
},
|
|
1133
1777
|
/**
|
|
1134
1778
|
* #getter
|
|
1135
1779
|
*/
|
|
1136
|
-
get
|
|
1137
|
-
return
|
|
1780
|
+
get secondaryStructureConsensus() {
|
|
1781
|
+
return self.MSA?.secondaryStructureConsensus;
|
|
1138
1782
|
},
|
|
1139
1783
|
/**
|
|
1140
1784
|
* #getter
|
|
1141
|
-
* most-negative allowed scrollY, keeping the last row in view rather than
|
|
1142
|
-
* letting the whole alignment scroll off the top.
|
|
1143
1785
|
*/
|
|
1144
|
-
get
|
|
1145
|
-
return
|
|
1786
|
+
get seqConsensus() {
|
|
1787
|
+
return self.MSA?.seqConsensus;
|
|
1146
1788
|
},
|
|
1147
1789
|
/**
|
|
1148
1790
|
* #getter
|
|
1791
|
+
* the base pairs of the consensus secondary structure, as arcs, in
|
|
1792
|
+
* visible column space (hidden columns are removed before parsing)
|
|
1149
1793
|
*/
|
|
1150
|
-
get
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1794
|
+
get secondaryStructureArcs() {
|
|
1795
|
+
const { blanks, hideGapsEffective } = self;
|
|
1796
|
+
const ss = this.secondaryStructureConsensus;
|
|
1797
|
+
if (!ss) {
|
|
1798
|
+
return undefined;
|
|
1799
|
+
}
|
|
1800
|
+
return parseWuss(hideGapsEffective ? skipBlanks(blanks, ss) : ss).map(({ start, end, pseudoknot }) => ({
|
|
1801
|
+
start,
|
|
1802
|
+
end,
|
|
1803
|
+
color: pseudoknot ? PSEUDOKNOT_ARC : HELIX_ARC,
|
|
1804
|
+
}));
|
|
1155
1805
|
},
|
|
1156
1806
|
/**
|
|
1157
1807
|
* #getter
|
|
1158
1808
|
*/
|
|
1159
|
-
get
|
|
1160
|
-
|
|
1809
|
+
get adapterTrackModels() {
|
|
1810
|
+
const { MSA, hideGapsEffective, blanks } = self;
|
|
1811
|
+
const tracks = (MSA?.tracks ?? []).filter(t => !!t.data);
|
|
1812
|
+
if (tracks.length === 0) {
|
|
1813
|
+
// return before reading rowHeight, so zooming does not rebuild the
|
|
1814
|
+
// list and redraw every track canvas
|
|
1815
|
+
return [];
|
|
1816
|
+
}
|
|
1817
|
+
const { rowHeight } = self;
|
|
1818
|
+
return tracks.map(t => ({
|
|
1819
|
+
model: {
|
|
1820
|
+
...t,
|
|
1821
|
+
kind: 'text',
|
|
1822
|
+
data: hideGapsEffective ? skipBlanks(blanks, t.data) : t.data,
|
|
1823
|
+
height: rowHeight,
|
|
1824
|
+
},
|
|
1825
|
+
ReactComponent: TrackBlocks,
|
|
1826
|
+
}));
|
|
1827
|
+
},
|
|
1828
|
+
/**
|
|
1829
|
+
* #getter
|
|
1830
|
+
* a data track's values or string, projected from its row's residues
|
|
1831
|
+
* onto alignment columns when it names a row
|
|
1832
|
+
*/
|
|
1833
|
+
get columnTrackContent() {
|
|
1834
|
+
const { MSA, blanks, hideGapsEffective } = self;
|
|
1835
|
+
const width = MSA?.getWidth() ?? 0;
|
|
1836
|
+
const project = (track, items, fill) => {
|
|
1837
|
+
if (!track.row) {
|
|
1838
|
+
return items;
|
|
1839
|
+
}
|
|
1840
|
+
const out = Array.from({ length: width }, () => fill);
|
|
1841
|
+
const index = self.seqPosIndex(track.row);
|
|
1842
|
+
items.forEach((item, seqPos) => {
|
|
1843
|
+
const col = index?.[seqPos];
|
|
1844
|
+
if (col !== undefined) {
|
|
1845
|
+
out[col] = item;
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
return out;
|
|
1849
|
+
};
|
|
1850
|
+
const skip = (items) => hideGapsEffective ? dropBlanks(blanks, items) : items;
|
|
1851
|
+
// an arc endpoint maps row residue -> column -> visible column.
|
|
1852
|
+
// visibleColsBefore, not globalColToVisibleCol, so an endpoint in a
|
|
1853
|
+
// hidden column moves to the neighboring visible one and the arc stays
|
|
1854
|
+
const resolve = (track, pos) => {
|
|
1855
|
+
const col = track.row
|
|
1856
|
+
? self.seqPosIndex(track.row)?.[pos - 1]
|
|
1857
|
+
: pos - 1;
|
|
1858
|
+
if (col === undefined || col < 0 || col >= width) {
|
|
1859
|
+
return undefined;
|
|
1860
|
+
}
|
|
1861
|
+
return hideGapsEffective ? visibleColsBefore(blanks, col) : col;
|
|
1862
|
+
};
|
|
1863
|
+
return new Map(self.columnTracks.map(track => {
|
|
1864
|
+
if (track.kind === 'arc') {
|
|
1865
|
+
const arcs = (track.arcs ?? [])
|
|
1866
|
+
.map(arc => {
|
|
1867
|
+
const start = resolve(track, Math.min(arc.start, arc.end));
|
|
1868
|
+
const end = resolve(track, Math.max(arc.start, arc.end));
|
|
1869
|
+
return start !== undefined && end !== undefined && start < end
|
|
1870
|
+
? { start, end, color: arc.color }
|
|
1871
|
+
: undefined;
|
|
1872
|
+
})
|
|
1873
|
+
.filter(notEmpty);
|
|
1874
|
+
return [track.id, { arcs }];
|
|
1875
|
+
}
|
|
1876
|
+
if (track.kind === 'bar') {
|
|
1877
|
+
const max = track.max ?? 1;
|
|
1878
|
+
const values = skip(project(track, track.values ?? [], 0)).map(v => Math.min(1, Math.max(0, v / max)));
|
|
1879
|
+
return [track.id, { values }];
|
|
1880
|
+
}
|
|
1881
|
+
const data = skip(project(track, (track.data ?? '').split(''), ' '));
|
|
1882
|
+
return [track.id, { data: data.join('') }];
|
|
1883
|
+
}));
|
|
1884
|
+
},
|
|
1885
|
+
/**
|
|
1886
|
+
* #method
|
|
1887
|
+
* the height a track draws at: what the user dragged its divider to,
|
|
1888
|
+
* then the height its snapshot asked for, then its kind's default. Only
|
|
1889
|
+
* a text track falls through to rowHeight, and `??` short-circuits
|
|
1890
|
+
* before reading it, so vertical zoom does not rebuild the other tracks
|
|
1891
|
+
*/
|
|
1892
|
+
trackHeight(kind, heightKey = kind, given) {
|
|
1893
|
+
return (self.trackHeights.get(heightKey) ??
|
|
1894
|
+
given ??
|
|
1895
|
+
defaultTrackHeights[kind] ??
|
|
1896
|
+
self.rowHeight);
|
|
1897
|
+
},
|
|
1898
|
+
/**
|
|
1899
|
+
* #getter
|
|
1900
|
+
*/
|
|
1901
|
+
get columnTrackModels() {
|
|
1902
|
+
return self.columnTracks.map(track => {
|
|
1903
|
+
const heightKey = resizableKinds.has(track.kind)
|
|
1904
|
+
? ownHeightKey(track.id)
|
|
1905
|
+
: undefined;
|
|
1906
|
+
return {
|
|
1907
|
+
model: {
|
|
1908
|
+
id: track.id,
|
|
1909
|
+
name: track.name,
|
|
1910
|
+
kind: track.kind,
|
|
1911
|
+
heightKey,
|
|
1912
|
+
height: this.trackHeight(track.kind, heightKey, track.height),
|
|
1913
|
+
barColor: track.color,
|
|
1914
|
+
arcColor: track.color,
|
|
1915
|
+
customColorScheme: track.colors,
|
|
1916
|
+
data: this.columnTrackContent.get(track.id)?.data,
|
|
1917
|
+
arcs: this.columnTrackContent.get(track.id)?.arcs,
|
|
1918
|
+
},
|
|
1919
|
+
ReactComponent: TrackBlocks,
|
|
1920
|
+
};
|
|
1921
|
+
});
|
|
1922
|
+
},
|
|
1923
|
+
/**
|
|
1924
|
+
* #getter
|
|
1925
|
+
* the consensus secondary structure as a track, when there is one. A
|
|
1926
|
+
* separate getter keeps the object stable across zoom, so its canvas
|
|
1927
|
+
* does not redraw
|
|
1928
|
+
*/
|
|
1929
|
+
get basePairTrackModels() {
|
|
1930
|
+
const arcs = this.secondaryStructureArcs;
|
|
1931
|
+
return arcs?.length
|
|
1932
|
+
? [
|
|
1933
|
+
{
|
|
1934
|
+
model: {
|
|
1935
|
+
id: 'base-pairs',
|
|
1936
|
+
name: 'Base pairs',
|
|
1937
|
+
kind: 'arc',
|
|
1938
|
+
heightKey: 'arc',
|
|
1939
|
+
height: this.trackHeight('arc'),
|
|
1940
|
+
arcs,
|
|
1941
|
+
},
|
|
1942
|
+
ReactComponent: TrackBlocks,
|
|
1943
|
+
},
|
|
1944
|
+
]
|
|
1945
|
+
: [];
|
|
1946
|
+
},
|
|
1947
|
+
/**
|
|
1948
|
+
* #getter
|
|
1949
|
+
* the tracks computed from the alignment; they depend on their heights
|
|
1950
|
+
* and the alphabet, not on zoom
|
|
1951
|
+
*/
|
|
1952
|
+
get computedTrackModels() {
|
|
1953
|
+
return [
|
|
1954
|
+
{
|
|
1955
|
+
id: 'conservation',
|
|
1956
|
+
name: 'Conservation',
|
|
1957
|
+
kind: 'bar',
|
|
1958
|
+
barColor: 'gray',
|
|
1959
|
+
},
|
|
1960
|
+
...(self.sequenceType === 'amino'
|
|
1961
|
+
? [
|
|
1962
|
+
{
|
|
1963
|
+
id: 'property-conservation',
|
|
1964
|
+
name: 'Property conservation',
|
|
1965
|
+
kind: 'bar',
|
|
1966
|
+
barColor: '#6a51a3',
|
|
1967
|
+
},
|
|
1968
|
+
]
|
|
1969
|
+
: []),
|
|
1970
|
+
{
|
|
1971
|
+
id: 'sequence-logo',
|
|
1972
|
+
name: 'Sequence logo',
|
|
1973
|
+
kind: 'logo',
|
|
1974
|
+
},
|
|
1975
|
+
// last, so it sits against the alignment it numbers
|
|
1976
|
+
{
|
|
1977
|
+
id: 'position-ruler',
|
|
1978
|
+
name: 'Position',
|
|
1979
|
+
kind: 'ruler',
|
|
1980
|
+
},
|
|
1981
|
+
].map(model => ({
|
|
1982
|
+
// every computed track of a kind shares that kind's height, so the
|
|
1983
|
+
// kind is its key
|
|
1984
|
+
model: {
|
|
1985
|
+
...model,
|
|
1986
|
+
heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
|
|
1987
|
+
height: this.trackHeight(model.kind),
|
|
1988
|
+
},
|
|
1989
|
+
ReactComponent: TrackBlocks,
|
|
1990
|
+
}));
|
|
1991
|
+
},
|
|
1992
|
+
get tracks() {
|
|
1993
|
+
return [
|
|
1994
|
+
...this.adapterTrackModels,
|
|
1995
|
+
...this.basePairTrackModels,
|
|
1996
|
+
...this.columnTrackModels,
|
|
1997
|
+
// every computed track reads the alignment's columns, and a tree, a
|
|
1998
|
+
// GFF and a features panel draw a figure with none
|
|
1999
|
+
...(self.numColumns > 0 ? this.computedTrackModels : []),
|
|
2000
|
+
];
|
|
2001
|
+
},
|
|
2002
|
+
/**
|
|
2003
|
+
* #getter
|
|
2004
|
+
*/
|
|
2005
|
+
get turnedOnTracks() {
|
|
2006
|
+
return this.tracks.filter(f => !trackIsOff(self.turnedOffTracks, f.model.id, f.model.defaultOff));
|
|
2007
|
+
},
|
|
2008
|
+
/**
|
|
2009
|
+
* #getter
|
|
2010
|
+
*/
|
|
2011
|
+
get showHorizontalScrollbar() {
|
|
2012
|
+
return overflows(self.totalWidth, self.msaAreaWidth);
|
|
2013
|
+
},
|
|
2014
|
+
/**
|
|
2015
|
+
* #method
|
|
2016
|
+
* Return a row-specific letter at a visible column, or undefined if gap.
|
|
2017
|
+
*
|
|
2018
|
+
* @param rowName - The name of the row
|
|
2019
|
+
* @param visibleCol - The visible column index (what the user sees on screen)
|
|
2020
|
+
* @returns The letter at that position, or undefined if it's a gap
|
|
2021
|
+
*/
|
|
2022
|
+
visibleColToRowLetter(rowName, visibleCol) {
|
|
2023
|
+
return self.rowMap.get(rowName)?.[this.visibleColToGlobalCol(visibleCol)];
|
|
2024
|
+
},
|
|
2025
|
+
/**
|
|
2026
|
+
* #method
|
|
2027
|
+
* Convert a visible column to a row-specific sequence position (0-based).
|
|
2028
|
+
* Returns undefined if the position is a gap in the sequence.
|
|
2029
|
+
*
|
|
2030
|
+
* Public API, like the sibling converters (visibleColToGlobalCol,
|
|
2031
|
+
* seqPosToVisibleCol, globalColToVisibleCol, seqPosToGlobalCol) hosts
|
|
2032
|
+
* use to translate between columns and residue positions. Keep them
|
|
2033
|
+
* stable.
|
|
2034
|
+
*
|
|
2035
|
+
* @param rowName - The name of the row
|
|
2036
|
+
* @param visibleCol - The visible column index
|
|
2037
|
+
* @returns The sequence position (0-based), or undefined if it's a gap
|
|
2038
|
+
*/
|
|
2039
|
+
visibleColToSeqPos(rowName, visibleCol) {
|
|
2040
|
+
// binary search: this runs on every mouse move, and scanning a
|
|
2041
|
+
// 30k-column row per event takes the whole frame
|
|
2042
|
+
return seqPosOfGlobalCol(self.seqPosIndex(rowName), this.visibleColToGlobalCol(visibleCol));
|
|
2043
|
+
},
|
|
2044
|
+
/**
|
|
2045
|
+
* #method
|
|
2046
|
+
* Convert a visible column to a row-specific sequence position (1-based).
|
|
2047
|
+
* Returns undefined if the position is a gap in the sequence.
|
|
2048
|
+
*
|
|
2049
|
+
* @param rowName - The name of the row
|
|
2050
|
+
* @param visibleCol - The visible column index
|
|
2051
|
+
* @returns The sequence position (1-based), or undefined if it's a gap
|
|
2052
|
+
*/
|
|
2053
|
+
visibleColToSeqPosOneBased(rowName, visibleCol) {
|
|
2054
|
+
const val = this.visibleColToSeqPos(rowName, visibleCol);
|
|
2055
|
+
return val !== undefined ? val + 1 : undefined;
|
|
2056
|
+
},
|
|
2057
|
+
/**
|
|
2058
|
+
* #method
|
|
2059
|
+
* Convert a global column index to a visible column index.
|
|
2060
|
+
* Returns undefined if the column is hidden (in blanks).
|
|
2061
|
+
* This is the inverse of visibleColToGlobalCol.
|
|
2062
|
+
*
|
|
2063
|
+
* @param globalCol - The global column index in the full MSA
|
|
2064
|
+
* @returns The visible column index, or undefined if the column is hidden
|
|
2065
|
+
*/
|
|
2066
|
+
globalColToVisibleCol(globalCol) {
|
|
2067
|
+
const { blanks, hideGapsEffective } = self;
|
|
2068
|
+
if (!hideGapsEffective) {
|
|
2069
|
+
return globalCol;
|
|
2070
|
+
}
|
|
2071
|
+
return globalColToVisibleCol(blanks, globalCol);
|
|
2072
|
+
},
|
|
2073
|
+
/**
|
|
2074
|
+
* #method
|
|
2075
|
+
* Convert a visible column index (what a mouse handler reports) to a
|
|
2076
|
+
* column of the full alignment. A host indexing its own per-column data
|
|
2077
|
+
* needs this when columns are hidden.
|
|
2078
|
+
*
|
|
2079
|
+
* @param visibleCol - The visible column index
|
|
2080
|
+
* @returns The global column index in the full MSA
|
|
2081
|
+
*/
|
|
2082
|
+
visibleColToGlobalCol(visibleCol) {
|
|
2083
|
+
const { blanks, hideGapsEffective } = self;
|
|
2084
|
+
return hideGapsEffective
|
|
2085
|
+
? visibleColToGlobalCol(blanks, visibleCol)
|
|
2086
|
+
: visibleCol;
|
|
2087
|
+
},
|
|
2088
|
+
/**
|
|
2089
|
+
* #method
|
|
2090
|
+
* Convert a sequence position (ungapped) to a global column index.
|
|
2091
|
+
* Returns undefined for a row name the alignment does not have.
|
|
2092
|
+
*
|
|
2093
|
+
* @param rowName - The name of the row
|
|
2094
|
+
* @param seqPos - The sequence position (0-based, ungapped)
|
|
2095
|
+
* @returns The global column index in the full MSA, or undefined
|
|
2096
|
+
*/
|
|
2097
|
+
seqPosToGlobalCol(rowName, seqPos) {
|
|
2098
|
+
const seq = self.rowMap.get(rowName);
|
|
2099
|
+
if (seq === undefined) {
|
|
2100
|
+
return undefined;
|
|
2101
|
+
}
|
|
2102
|
+
const col = self.seqPosIndex(rowName)?.[seqPos];
|
|
2103
|
+
// past the end of the ungapped sequence: 0 for the degenerate all-gap
|
|
2104
|
+
// row, otherwise one past the last column
|
|
2105
|
+
return col ?? (seqPos === 0 ? 0 : seq.length);
|
|
2106
|
+
},
|
|
2107
|
+
/**
|
|
2108
|
+
* #method
|
|
2109
|
+
* Convert a sequence position (ungapped) directly to a visible column index.
|
|
2110
|
+
* This combines seqPosToGlobalCol and globalColToVisibleCol.
|
|
2111
|
+
*
|
|
2112
|
+
* @param rowName - The name of the row
|
|
2113
|
+
* @param seqPos - The sequence position (0-based, ungapped)
|
|
2114
|
+
* @returns The visible column index, or undefined if the column is hidden
|
|
2115
|
+
*/
|
|
2116
|
+
seqPosToVisibleCol(rowName, seqPos) {
|
|
2117
|
+
const globalCol = this.seqPosToGlobalCol(rowName, seqPos);
|
|
2118
|
+
return globalCol === undefined
|
|
2119
|
+
? undefined
|
|
2120
|
+
: this.globalColToVisibleCol(globalCol);
|
|
2121
|
+
},
|
|
2122
|
+
/**
|
|
2123
|
+
* #method
|
|
2124
|
+
* the visible columns a span covers, in highlight coordinates: `start`
|
|
2125
|
+
* and `end` are 1-based residues of `row`, or columns of the file
|
|
2126
|
+
* without it. A fractional position, as a zoom gesture reports one,
|
|
2127
|
+
* widens to the whole residue or column it falls in. A span entirely on
|
|
2128
|
+
* hidden columns, or naming a row the alignment lacks, gives undefined.
|
|
2129
|
+
*/
|
|
2130
|
+
visibleSpan({ row, start: rawStart, end: rawEnd }) {
|
|
2131
|
+
const { blanks } = self;
|
|
2132
|
+
const start = Math.max(1, Math.floor(rawStart));
|
|
2133
|
+
const end = Math.ceil(rawEnd);
|
|
2134
|
+
let startGlobal = start - 1;
|
|
2135
|
+
let endGlobal = end - 1;
|
|
2136
|
+
if (row !== undefined) {
|
|
2137
|
+
const rowStart = this.seqPosToGlobalCol(row, start - 1);
|
|
2138
|
+
const rowEnd = this.seqPosToGlobalCol(row, end - 1);
|
|
2139
|
+
if (rowStart === undefined || rowEnd === undefined) {
|
|
2140
|
+
return undefined;
|
|
2141
|
+
}
|
|
2142
|
+
startGlobal = rowStart;
|
|
2143
|
+
endGlobal = rowEnd;
|
|
2144
|
+
}
|
|
2145
|
+
const startCol = this.globalColToVisibleCol(startGlobal) ??
|
|
2146
|
+
visibleColsBefore(blanks, startGlobal);
|
|
2147
|
+
const endCol = this.globalColToVisibleCol(endGlobal) ??
|
|
2148
|
+
visibleColsBefore(blanks, endGlobal) - 1;
|
|
2149
|
+
return startCol <= endCol ? { startCol, endCol } : undefined;
|
|
2150
|
+
},
|
|
2151
|
+
/**
|
|
2152
|
+
* #getter
|
|
2153
|
+
* why each ignored residue mapping is ignored, so a host can tell a
|
|
2154
|
+
* missing structure from a mapping made against a different alignment.
|
|
2155
|
+
*/
|
|
2156
|
+
get residueMappingProblems() {
|
|
2157
|
+
const problems = [];
|
|
2158
|
+
for (const mapping of self.residueMappings) {
|
|
2159
|
+
const where = { row: mapping.row, structureId: mapping.structure.id };
|
|
2160
|
+
const residues = self.seqPosIndex(mapping.row)?.length;
|
|
2161
|
+
if (residues === undefined) {
|
|
2162
|
+
problems.push({
|
|
2163
|
+
...where,
|
|
2164
|
+
scope: 'mapping',
|
|
2165
|
+
reason: 'no such row in the alignment',
|
|
2166
|
+
});
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
if (mapping.rowLength !== undefined &&
|
|
2170
|
+
mapping.rowLength !== residues) {
|
|
2171
|
+
problems.push({
|
|
2172
|
+
...where,
|
|
2173
|
+
scope: 'mapping',
|
|
2174
|
+
reason: `computed against a ${mapping.rowLength}-residue row; this one has ${residues}`,
|
|
2175
|
+
});
|
|
2176
|
+
continue;
|
|
2177
|
+
}
|
|
2178
|
+
const overrun = mapping.segments.find(segment => segment.rowEnd > residues || segment.rowStart < 1);
|
|
2179
|
+
if (overrun) {
|
|
2180
|
+
problems.push({
|
|
2181
|
+
...where,
|
|
2182
|
+
scope: 'mapping',
|
|
2183
|
+
reason: `segment ${overrun.rowStart}-${overrun.rowEnd} does not fit a ${residues}-residue row`,
|
|
2184
|
+
});
|
|
2185
|
+
continue;
|
|
2186
|
+
}
|
|
2187
|
+
for (const segment of mapping.segments) {
|
|
2188
|
+
if (!sameLength(segment)) {
|
|
2189
|
+
problems.push({
|
|
2190
|
+
...where,
|
|
2191
|
+
scope: 'segment',
|
|
2192
|
+
reason: `segment ${segment.rowStart}-${segment.rowEnd} maps to ${segment.structStart}-${segment.structEnd}, which is a different length`,
|
|
2193
|
+
});
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
return problems;
|
|
2198
|
+
},
|
|
2199
|
+
/**
|
|
2200
|
+
* #getter
|
|
2201
|
+
* the mappings that fit the loaded alignment. A row-level problem drops
|
|
2202
|
+
* the whole mapping; a malformed segment drops only that segment.
|
|
2203
|
+
*/
|
|
2204
|
+
get usableResidueMappings() {
|
|
2205
|
+
const unusable = new Set(this.residueMappingProblems
|
|
2206
|
+
.filter(problem => problem.scope === 'mapping')
|
|
2207
|
+
.map(problem => `${problem.row}\u0000${problem.structureId}`));
|
|
2208
|
+
return self.residueMappings
|
|
2209
|
+
.filter(m => !unusable.has(`${m.row}\u0000${m.structure.id}`))
|
|
2210
|
+
.map(m => ({
|
|
2211
|
+
...m,
|
|
2212
|
+
segments: m.segments.filter(sameLength),
|
|
2213
|
+
}));
|
|
2214
|
+
},
|
|
2215
|
+
/**
|
|
2216
|
+
* #getter
|
|
2217
|
+
* the structures with usable mappings. A row can map onto several, such
|
|
2218
|
+
* as an experimental entry and a predicted model.
|
|
2219
|
+
*/
|
|
2220
|
+
get mappedStructures() {
|
|
2221
|
+
return this.usableResidueMappings.map(m => ({
|
|
2222
|
+
row: m.row,
|
|
2223
|
+
structure: m.structure,
|
|
2224
|
+
}));
|
|
2225
|
+
},
|
|
2226
|
+
/**
|
|
2227
|
+
* #method
|
|
2228
|
+
* The structure residue for a row residue. Returns undefined when no
|
|
2229
|
+
* segment covers `seqPos`, or when the row maps onto several structures
|
|
2230
|
+
* and `structureId` does not pick one (see `mappedStructures`).
|
|
2231
|
+
*
|
|
2232
|
+
* Positions are 1-based, like `residueMappings` and `highlights`; the
|
|
2233
|
+
* column helpers above are 0-based.
|
|
2234
|
+
*
|
|
2235
|
+
* @param rowName - The alignment row
|
|
2236
|
+
* @param seqPos - Residue of that row, 1-based
|
|
2237
|
+
* @param structureId - Which structure, when the row maps onto several
|
|
2238
|
+
*/
|
|
2239
|
+
structureResidue(rowName, seqPos, structureId) {
|
|
2240
|
+
const hits = [];
|
|
2241
|
+
for (const mapping of this.usableResidueMappings) {
|
|
2242
|
+
if (mapping.row !== rowName) {
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
if (structureId !== undefined &&
|
|
2246
|
+
mapping.structure.id !== structureId) {
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
const segment = mapping.segments.find(seg => seqPos >= seg.rowStart && seqPos <= seg.rowEnd);
|
|
2250
|
+
if (segment) {
|
|
2251
|
+
const position = segment.structStart + (seqPos - segment.rowStart);
|
|
2252
|
+
hits.push({
|
|
2253
|
+
structure: mapping.structure,
|
|
2254
|
+
position,
|
|
2255
|
+
observed: !inRanges(mapping.unobserved, position),
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
return hits.length === 1 ? hits[0] : undefined;
|
|
2260
|
+
},
|
|
2261
|
+
/**
|
|
2262
|
+
* #method
|
|
2263
|
+
* The row residue for a structure residue; the inverse of
|
|
2264
|
+
* `structureResidue`, returning undefined in the same cases. `asymId`
|
|
2265
|
+
* picks a chain when several mappings share an entry id, as in a
|
|
2266
|
+
* homodimer.
|
|
2267
|
+
*
|
|
2268
|
+
* @param structureId - The structure's id, as the mapping names it
|
|
2269
|
+
* @param position - Residue of that structure, 1-based label_seq_id
|
|
2270
|
+
* @param asymId - Which chain, when the id alone is ambiguous
|
|
2271
|
+
*/
|
|
2272
|
+
rowResidue(structureId, position, asymId) {
|
|
2273
|
+
const hits = [];
|
|
2274
|
+
for (const mapping of this.usableResidueMappings) {
|
|
2275
|
+
if (mapping.structure.id !== structureId) {
|
|
2276
|
+
continue;
|
|
2277
|
+
}
|
|
2278
|
+
if (asymId !== undefined && mapping.structure.asymId !== asymId) {
|
|
2279
|
+
continue;
|
|
2280
|
+
}
|
|
2281
|
+
const segment = mapping.segments.find(seg => position >= seg.structStart && position <= seg.structEnd);
|
|
2282
|
+
if (segment) {
|
|
2283
|
+
hits.push({
|
|
2284
|
+
rowName: mapping.row,
|
|
2285
|
+
seqPos: segment.rowStart + (position - segment.structStart),
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
return hits.length === 1 ? hits[0] : undefined;
|
|
2290
|
+
},
|
|
2291
|
+
}))
|
|
2292
|
+
.views(self => ({
|
|
2293
|
+
/**
|
|
2294
|
+
* #getter
|
|
2295
|
+
* the branch-length scale bar over the tree, undefined in cladogram mode
|
|
2296
|
+
* or when the tree area is too narrow for one
|
|
2297
|
+
*/
|
|
2298
|
+
get treeScaleBar() {
|
|
2299
|
+
return scaleBarLength(self.pxPerBranchLength, self.treeAreaWidth - self.marginLeft * 2);
|
|
2300
|
+
},
|
|
2301
|
+
/**
|
|
2302
|
+
* #getter
|
|
2303
|
+
* the band across the top: the minimap over the alignment, the row panel
|
|
2304
|
+
* headers, and the tree overview stacked on the scale bar, as tall as the
|
|
2305
|
+
* tallest of the three
|
|
2306
|
+
*/
|
|
2307
|
+
get topBandHeight() {
|
|
2308
|
+
return Math.max(self.showHorizontalScrollbar ? self.minimapHeight : 0, self.rowPanelsHeaderHeight, self.treeOverviewHeight +
|
|
2309
|
+
(this.treeScaleBar ? treeScaleBarHeight : 0));
|
|
2310
|
+
},
|
|
2311
|
+
/**
|
|
2312
|
+
* #getter
|
|
2313
|
+
* the vertical space for alignment rows: the widget height less the
|
|
2314
|
+
* header, the top band and the tracks. Shared by blocksY, maxScrollY, the
|
|
2315
|
+
* vertical scrollbar and fitVertically.
|
|
2316
|
+
*/
|
|
2317
|
+
get msaAreaHeight() {
|
|
2318
|
+
return (self.height -
|
|
2319
|
+
this.topBandHeight -
|
|
2320
|
+
self.headerHeight -
|
|
2321
|
+
this.totalTrackAreaHeight);
|
|
2322
|
+
},
|
|
2323
|
+
/**
|
|
2324
|
+
* #getter
|
|
2325
|
+
* total height of track area (px)
|
|
2326
|
+
*/
|
|
2327
|
+
get totalTrackAreaHeight() {
|
|
2328
|
+
return sum(self.turnedOnTracks.map(r => r.model.height));
|
|
2329
|
+
},
|
|
2330
|
+
/**
|
|
2331
|
+
* one representative annotation per accession, for the legend, the
|
|
2332
|
+
* filter dialog and the palettes
|
|
2333
|
+
*/
|
|
2334
|
+
get annotationTypes() {
|
|
2335
|
+
// first occurrence wins; only name, description and segment start are
|
|
2336
|
+
// read from it
|
|
2337
|
+
const types = new Map();
|
|
2338
|
+
for (const annot of self.annotations) {
|
|
2339
|
+
if (!types.has(annot.accession)) {
|
|
2340
|
+
types.set(annot.accession, annot);
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
return types;
|
|
2344
|
+
},
|
|
2345
|
+
get filteredAnnotations() {
|
|
2346
|
+
return self.annotations.filter(r => !self.turnedOffFeatures.get(r.accession));
|
|
2347
|
+
},
|
|
2348
|
+
get annotationsByRow() {
|
|
2349
|
+
return groupBy(this.filteredAnnotations, r => r.id);
|
|
2350
|
+
},
|
|
2351
|
+
}))
|
|
2352
|
+
.views(self => ({
|
|
2353
|
+
/**
|
|
2354
|
+
* #getter
|
|
2355
|
+
*/
|
|
2356
|
+
get showVerticalScrollbar() {
|
|
2357
|
+
return overflows(self.totalHeight, self.msaAreaHeight);
|
|
2358
|
+
},
|
|
2359
|
+
}))
|
|
2360
|
+
.views(self => ({
|
|
2361
|
+
/**
|
|
2362
|
+
* #getter
|
|
2363
|
+
*/
|
|
2364
|
+
get dataInitialized() {
|
|
2365
|
+
// truthiness, not `!== ''`: DataModel drops an inline document over
|
|
2366
|
+
// 50kb from the snapshot, so a restored session can hold `undefined`
|
|
2367
|
+
return !!(self.data.msa || self.data.tree) && !self.error;
|
|
2368
|
+
},
|
|
2369
|
+
/**
|
|
2370
|
+
* #getter
|
|
2371
|
+
*/
|
|
2372
|
+
get blocksX() {
|
|
2373
|
+
return calculateBlocks({
|
|
2374
|
+
viewportSize: self.msaAreaWidth,
|
|
2375
|
+
viewportPos: -self.scrollX,
|
|
2376
|
+
blockSize: self.blockSize,
|
|
2377
|
+
mapSize: self.totalWidth,
|
|
2378
|
+
});
|
|
2379
|
+
},
|
|
2380
|
+
/**
|
|
2381
|
+
* #getter
|
|
2382
|
+
*/
|
|
2383
|
+
get blocksY() {
|
|
2384
|
+
return calculateBlocks({
|
|
2385
|
+
viewportSize: self.msaAreaHeight,
|
|
2386
|
+
viewportPos: -self.scrollY,
|
|
2387
|
+
blockSize: self.blockSize,
|
|
2388
|
+
mapSize: self.totalHeight,
|
|
2389
|
+
});
|
|
2390
|
+
},
|
|
2391
|
+
}))
|
|
2392
|
+
.views(self => ({
|
|
2393
|
+
/**
|
|
2394
|
+
* #getter
|
|
2395
|
+
*/
|
|
2396
|
+
get blocks2d() {
|
|
2397
|
+
return self.blocksY.flatMap(by => self.blocksX.map(bx => [bx, by]));
|
|
2398
|
+
},
|
|
2399
|
+
/**
|
|
2400
|
+
* #getter
|
|
2401
|
+
*/
|
|
2402
|
+
get isLoading() {
|
|
2403
|
+
return self.loadingMSA || self.loadingTree;
|
|
2404
|
+
},
|
|
2405
|
+
/**
|
|
2406
|
+
* #getter
|
|
2407
|
+
*/
|
|
2408
|
+
get maxScrollX() {
|
|
2409
|
+
return Math.min(-self.totalWidth + (self.msaAreaWidth - 100), 0);
|
|
2410
|
+
},
|
|
2411
|
+
/**
|
|
2412
|
+
* #getter
|
|
2413
|
+
* most-negative allowed scrollY, which keeps the last row in view
|
|
2414
|
+
*/
|
|
2415
|
+
get maxScrollY() {
|
|
2416
|
+
return Math.min(-self.totalHeight + self.msaAreaHeight, 0);
|
|
2417
|
+
},
|
|
2418
|
+
/**
|
|
2419
|
+
* #getter
|
|
2420
|
+
* axis a wheel zoom scales, for ctrl+wheel as much as for scroll-zoom.
|
|
2421
|
+
* With scroll-zoom off the toolbar shows no axis, so ctrl+wheel takes
|
|
2422
|
+
* both.
|
|
2423
|
+
*/
|
|
2424
|
+
get wheelZoomAxis() {
|
|
2425
|
+
return self.scrollZoom ? self.scrollZoomAxis : 'both';
|
|
1161
2426
|
},
|
|
1162
2427
|
}))
|
|
1163
2428
|
.actions(self => ({
|
|
@@ -1173,15 +2438,19 @@ function stateModelFactory() {
|
|
|
1173
2438
|
setScrollZoom(arg) {
|
|
1174
2439
|
self.scrollZoom = arg;
|
|
1175
2440
|
},
|
|
2441
|
+
/**
|
|
2442
|
+
* #action
|
|
2443
|
+
*/
|
|
2444
|
+
setScrollZoomAxis(arg) {
|
|
2445
|
+
self.scrollZoomAxis = arg;
|
|
2446
|
+
},
|
|
1176
2447
|
/**
|
|
1177
2448
|
* #action
|
|
1178
2449
|
* set hovered tree node and its descendants
|
|
1179
2450
|
*/
|
|
1180
2451
|
setHoveredTreeNode(nodeId) {
|
|
1181
|
-
//
|
|
1182
|
-
//
|
|
1183
|
-
// and a fresh object here invalidates hoveredRowIndices and redraws the
|
|
1184
|
-
// tree and MSA overlays. Re-hovering the same node is the common case
|
|
2452
|
+
// called on every tree mousemove; `find` walks the whole hierarchy and a
|
|
2453
|
+
// new object redraws the tree and MSA overlays
|
|
1185
2454
|
if (nodeId === self.hoveredTreeNode?.nodeId) {
|
|
1186
2455
|
return;
|
|
1187
2456
|
}
|
|
@@ -1199,14 +2468,34 @@ function stateModelFactory() {
|
|
|
1199
2468
|
},
|
|
1200
2469
|
/**
|
|
1201
2470
|
* #action
|
|
1202
|
-
* Calculate a neighbor joining tree from the current MSA using BLOSUM62
|
|
2471
|
+
* Calculate a neighbor joining tree from the current MSA using BLOSUM62
|
|
2472
|
+
* distances. Throws above `maxNeighborJoiningRows`: the join loop is
|
|
2473
|
+
* cubic and runs on the main thread, and 800 rows freeze the tab for ten
|
|
2474
|
+
* seconds with no cancel.
|
|
1203
2475
|
*/
|
|
1204
2476
|
calculateNeighborJoiningTreeFromMSA() {
|
|
1205
|
-
|
|
2477
|
+
// every sequence, including rows in collapsed clades
|
|
2478
|
+
const rows = [...self.rowMap];
|
|
2479
|
+
if (rows.length < 2) {
|
|
1206
2480
|
throw new Error('Need at least 2 sequences to build a tree');
|
|
1207
2481
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
2482
|
+
if (rows.length > maxNeighborJoiningRows) {
|
|
2483
|
+
throw new Error(`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`);
|
|
2484
|
+
}
|
|
2485
|
+
this.replaceTree(calculateNeighborJoiningTree(rows));
|
|
2486
|
+
},
|
|
2487
|
+
/**
|
|
2488
|
+
* #action
|
|
2489
|
+
* swap in a different tree over the same alignment. Clears `collapsed`
|
|
2490
|
+
* and `showOnly`, since path-derived node ids (node-0-0-1) from the old
|
|
2491
|
+
* tree would match unrelated nodes in the new one.
|
|
2492
|
+
*/
|
|
2493
|
+
replaceTree(newick) {
|
|
2494
|
+
transaction(() => {
|
|
2495
|
+
self.collapsed.clear();
|
|
2496
|
+
self.setShowOnly(undefined);
|
|
2497
|
+
self.setTree(newick);
|
|
2498
|
+
});
|
|
1210
2499
|
},
|
|
1211
2500
|
/**
|
|
1212
2501
|
* #action
|
|
@@ -1264,23 +2553,26 @@ function stateModelFactory() {
|
|
|
1264
2553
|
* #action
|
|
1265
2554
|
* Smoothly zoom by a continuous scaleFactor. The column under the cursor
|
|
1266
2555
|
* (offsetX/offsetY, px relative to the MSA area) stays anchored
|
|
1267
|
-
* horizontally. Vertically the anchor is biased toward
|
|
1268
|
-
* alignment nearly fits the viewport,
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1272
|
-
|
|
1273
|
-
|
|
2556
|
+
* horizontally. Vertically the anchor is biased toward y=0 when the
|
|
2557
|
+
* alignment nearly fits the viewport, fading to cursor-anchoring as the
|
|
2558
|
+
* alignment grows taller than the viewport.
|
|
2559
|
+
* Drives wheel/trackpad-pinch zoom. `axis` holds one cell dimension
|
|
2560
|
+
* fixed; the held axis still re-anchors its scroll offset, since the
|
|
2561
|
+
* other one can change how much of the alignment fits.
|
|
2562
|
+
*/
|
|
2563
|
+
zoomToPos(scaleFactor, offsetX, offsetY, axis = 'both') {
|
|
1274
2564
|
transaction(() => {
|
|
1275
2565
|
const colInView = (-self.scrollX + offsetX) / self.colWidth;
|
|
1276
2566
|
const rowInView = (-self.scrollY + offsetY) / self.rowHeight;
|
|
1277
|
-
|
|
1278
|
-
|
|
2567
|
+
if (axis !== 'vertical') {
|
|
2568
|
+
self.colWidth = clamp(self.colWidth * scaleFactor, minColWidth, maxCellSize);
|
|
2569
|
+
}
|
|
2570
|
+
if (axis !== 'horizontal') {
|
|
2571
|
+
self.rowHeight = clamp(self.rowHeight * scaleFactor, minRowHeight, maxCellSize);
|
|
2572
|
+
}
|
|
1279
2573
|
self.scrollX = clamp(offsetX - colInView * self.colWidth, self.maxScrollX, 0);
|
|
1280
2574
|
const anchoredScrollY = offsetY - rowInView * self.rowHeight;
|
|
1281
|
-
// maxScrollY is
|
|
1282
|
-
// overflows, so -maxScrollY is exactly that overflow past the
|
|
1283
|
-
// scrollable MSA viewport (0 when it fits)
|
|
2575
|
+
// -maxScrollY is the overflow past the viewport, 0 when it fits
|
|
1284
2576
|
const overflow = Math.max(0, -self.maxScrollY);
|
|
1285
2577
|
const visibleHeight = self.totalHeight - overflow;
|
|
1286
2578
|
const topBias = visibleHeight > 0 ? clamp(1 - overflow / visibleHeight, 0, 1) : 1;
|
|
@@ -1302,13 +2594,14 @@ function stateModelFactory() {
|
|
|
1302
2594
|
},
|
|
1303
2595
|
/**
|
|
1304
2596
|
* #action
|
|
1305
|
-
* Set the overlay annotations
|
|
1306
|
-
*
|
|
1307
|
-
*
|
|
2597
|
+
* Set the overlay annotations (an empty list clears them). InterProScan,
|
|
2598
|
+
* GFF, user uploads and NCBI CDD all arrive here as Annotation[].
|
|
2599
|
+
*
|
|
2600
|
+
* Leaves `showDomains` alone, because a restored snapshot reloads its
|
|
2601
|
+
* GFF and must keep a hidden overlay hidden.
|
|
1308
2602
|
*/
|
|
1309
2603
|
setAnnotations(annotations) {
|
|
1310
2604
|
self.annotations = annotations;
|
|
1311
|
-
self.setShowDomains(annotations.length > 0);
|
|
1312
2605
|
},
|
|
1313
2606
|
/**
|
|
1314
2607
|
* #action
|
|
@@ -1322,382 +2615,147 @@ function stateModelFactory() {
|
|
|
1322
2615
|
applyGFFText(gffText) {
|
|
1323
2616
|
this.setAnnotations(gffToAnnotations(parseGFF(gffText)));
|
|
1324
2617
|
},
|
|
1325
|
-
/**
|
|
1326
|
-
* #action
|
|
1327
|
-
*/
|
|
1328
|
-
doScrollX(deltaX) {
|
|
1329
|
-
this.setScrollX(self.scrollX + deltaX);
|
|
1330
|
-
},
|
|
1331
|
-
/**
|
|
1332
|
-
* #action
|
|
1333
|
-
*/
|
|
1334
|
-
setScrollX(n) {
|
|
1335
|
-
self.scrollX = clamp(n, self.maxScrollX, 0);
|
|
1336
|
-
},
|
|
1337
|
-
/**
|
|
1338
|
-
* #action
|
|
1339
|
-
*/
|
|
1340
|
-
setColumnTracks(tracks) {
|
|
1341
|
-
self.columnTracks.replace(tracks);
|
|
1342
|
-
},
|
|
1343
|
-
/**
|
|
1344
|
-
* #action
|
|
1345
|
-
*/
|
|
1346
|
-
toggleTrack(id) {
|
|
1347
|
-
// the stored value is "is off", so the current shown state is exactly
|
|
1348
|
-
// what the flipped entry should hold
|
|
1349
|
-
self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id));
|
|
1350
|
-
},
|
|
1351
|
-
/**
|
|
1352
|
-
* #action
|
|
1353
|
-
*/
|
|
1354
|
-
setStatus(status) {
|
|
1355
|
-
self.status = status;
|
|
1356
|
-
},
|
|
1357
|
-
}))
|
|
1358
|
-
.views(self => ({
|
|
1359
|
-
/**
|
|
1360
|
-
* #getter
|
|
1361
|
-
*/
|
|
1362
|
-
get labelWidthMap() {
|
|
1363
|
-
const { showTreeText, leaves, treeMetadata } = self;
|
|
1364
|
-
// gated on the same condition the renderer draws labels under, so the
|
|
1365
|
-
// gutter labelsWidth reserves and the labels actually drawn cannot
|
|
1366
|
-
// disagree -- and so turning labels off hands their space to the tree.
|
|
1367
|
-
// Measured at a fixed reference size and scaled by labelWidthScale:
|
|
1368
|
-
// re-measuring every leaf on every vertical-zoom frame cost ~200ms on a
|
|
1369
|
-
// 50k-leaf tree
|
|
1370
|
-
return showTreeText
|
|
1371
|
-
? new Map(leaves.map(node => {
|
|
1372
|
-
const { name } = node.data;
|
|
1373
|
-
// `||`, matching renderTreeLabels: an empty genome falls back
|
|
1374
|
-
// to the row name, and measuring '' would size the gutter (and
|
|
1375
|
-
// the label's click target) to nothing
|
|
1376
|
-
const displayName = treeMetadata[name]?.genome || name;
|
|
1377
|
-
return [
|
|
1378
|
-
name,
|
|
1379
|
-
measureTextCanvas(displayName, labelReferenceFontSize),
|
|
1380
|
-
];
|
|
1381
|
-
}))
|
|
1382
|
-
: new Map();
|
|
1383
|
-
},
|
|
1384
|
-
/**
|
|
1385
|
-
* #getter
|
|
1386
|
-
* factor turning a labelWidthMap entry into its width at the current
|
|
1387
|
-
* font size
|
|
1388
|
-
*/
|
|
1389
|
-
get labelWidthScale() {
|
|
1390
|
-
return self.fontSize / labelReferenceFontSize;
|
|
1391
|
-
},
|
|
1392
|
-
get labelsWidth() {
|
|
1393
|
-
// a loop, not Math.max(...widths.values()): spreading a map of every
|
|
1394
|
-
// leaf passes one argument per row, and the argument limit is somewhere
|
|
1395
|
-
// around 125k -- so the bundled 230k-tip COVID tree threw
|
|
1396
|
-
// "RangeError: Maximum call stack size exceeded" out of a getter the
|
|
1397
|
-
// treeWidth autorun reads on load
|
|
1398
|
-
let max = 0;
|
|
1399
|
-
for (const width of this.labelWidthMap.values()) {
|
|
1400
|
-
if (width > max) {
|
|
1401
|
-
max = width;
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
return max * this.labelWidthScale;
|
|
1405
|
-
},
|
|
1406
|
-
/**
|
|
1407
|
-
* #getter
|
|
1408
|
-
*/
|
|
1409
|
-
get secondaryStructureConsensus() {
|
|
1410
|
-
return self.MSA?.secondaryStructureConsensus;
|
|
1411
|
-
},
|
|
1412
|
-
/**
|
|
1413
|
-
* #getter
|
|
1414
|
-
*/
|
|
1415
|
-
get seqConsensus() {
|
|
1416
|
-
return self.MSA?.seqConsensus;
|
|
1417
|
-
},
|
|
1418
|
-
/**
|
|
1419
|
-
* #getter
|
|
1420
|
-
*/
|
|
1421
|
-
get adapterTrackModels() {
|
|
1422
|
-
const { rowHeight, MSA, hideGapsEffective, blanks } = self;
|
|
1423
|
-
const tracks = MSA?.tracks ?? [];
|
|
1424
|
-
return tracks
|
|
1425
|
-
.filter(t => !!t.data)
|
|
1426
|
-
.map(t => ({
|
|
1427
|
-
model: {
|
|
1428
|
-
...t,
|
|
1429
|
-
kind: 'text',
|
|
1430
|
-
data: hideGapsEffective ? skipBlanks(blanks, t.data) : t.data,
|
|
1431
|
-
height: rowHeight,
|
|
1432
|
-
},
|
|
1433
|
-
ReactComponent: TrackBlocks,
|
|
1434
|
-
}));
|
|
1435
|
-
},
|
|
1436
|
-
/**
|
|
1437
|
-
* #getter
|
|
1438
|
-
* a data track's values or string, projected from its row's residues
|
|
1439
|
-
* onto alignment columns when it names a row
|
|
1440
|
-
*/
|
|
1441
|
-
get columnTrackContent() {
|
|
1442
|
-
const { MSA, blanks, hideGapsEffective } = self;
|
|
1443
|
-
const width = MSA?.getWidth() ?? 0;
|
|
1444
|
-
const project = (track, items, fill) => {
|
|
1445
|
-
if (!track.row) {
|
|
1446
|
-
return items;
|
|
1447
|
-
}
|
|
1448
|
-
const out = Array.from({ length: width }, () => fill);
|
|
1449
|
-
const index = self.seqPosGlobalColIndex.get(track.row);
|
|
1450
|
-
items.forEach((item, seqPos) => {
|
|
1451
|
-
const col = index?.[seqPos];
|
|
1452
|
-
if (col !== undefined) {
|
|
1453
|
-
out[col] = item;
|
|
1454
|
-
}
|
|
1455
|
-
});
|
|
1456
|
-
return out;
|
|
1457
|
-
};
|
|
1458
|
-
const skip = (items) => hideGapsEffective ? dropBlanks(blanks, items) : items;
|
|
1459
|
-
return new Map(self.columnTracks.map(track => {
|
|
1460
|
-
if (track.kind === 'bar') {
|
|
1461
|
-
const max = track.max ?? 1;
|
|
1462
|
-
const values = skip(project(track, track.values ?? [], 0)).map(v => Math.min(1, Math.max(0, v / max)));
|
|
1463
|
-
return [track.id, { values }];
|
|
1464
|
-
}
|
|
1465
|
-
const data = skip(project(track, (track.data ?? '').split(''), ' '));
|
|
1466
|
-
return [track.id, { data: data.join('') }];
|
|
1467
|
-
}));
|
|
1468
|
-
},
|
|
1469
|
-
/**
|
|
1470
|
-
* #getter
|
|
1471
|
-
*/
|
|
1472
|
-
get columnTrackModels() {
|
|
1473
|
-
return self.columnTracks.map(track => ({
|
|
1474
|
-
model: {
|
|
1475
|
-
id: track.id,
|
|
1476
|
-
name: track.name,
|
|
1477
|
-
kind: track.kind,
|
|
1478
|
-
height: track.height ??
|
|
1479
|
-
(track.kind === 'bar'
|
|
1480
|
-
? self.conservationTrackHeight
|
|
1481
|
-
: self.rowHeight),
|
|
1482
|
-
barColor: track.color,
|
|
1483
|
-
customColorScheme: track.colors,
|
|
1484
|
-
data: this.columnTrackContent.get(track.id)?.data,
|
|
1485
|
-
},
|
|
1486
|
-
ReactComponent: TrackBlocks,
|
|
1487
|
-
}));
|
|
1488
|
-
},
|
|
1489
|
-
/**
|
|
1490
|
-
* #getter
|
|
1491
|
-
*/
|
|
1492
|
-
get tracks() {
|
|
1493
|
-
const conservationTrack = {
|
|
1494
|
-
id: 'conservation',
|
|
1495
|
-
name: 'Conservation',
|
|
1496
|
-
kind: 'bar',
|
|
1497
|
-
height: self.conservationTrackHeight,
|
|
1498
|
-
barColor: 'gray',
|
|
1499
|
-
};
|
|
1500
|
-
const propertyConservationTrack = {
|
|
1501
|
-
id: 'property-conservation',
|
|
1502
|
-
name: 'Property conservation',
|
|
1503
|
-
kind: 'bar',
|
|
1504
|
-
height: self.conservationTrackHeight,
|
|
1505
|
-
barColor: '#6a51a3',
|
|
1506
|
-
};
|
|
1507
|
-
const sequenceLogoTrack = {
|
|
1508
|
-
id: 'sequence-logo',
|
|
1509
|
-
name: 'Sequence logo',
|
|
1510
|
-
kind: 'logo',
|
|
1511
|
-
height: self.sequenceLogoTrackHeight,
|
|
1512
|
-
};
|
|
1513
|
-
return [
|
|
1514
|
-
...this.adapterTrackModels,
|
|
1515
|
-
...this.columnTrackModels,
|
|
1516
|
-
...[
|
|
1517
|
-
conservationTrack,
|
|
1518
|
-
...(self.sequenceType === 'amino'
|
|
1519
|
-
? [propertyConservationTrack]
|
|
1520
|
-
: []),
|
|
1521
|
-
sequenceLogoTrack,
|
|
1522
|
-
].map(model => ({ model, ReactComponent: TrackBlocks })),
|
|
1523
|
-
];
|
|
2618
|
+
/**
|
|
2619
|
+
* #action
|
|
2620
|
+
*/
|
|
2621
|
+
doScrollX(deltaX) {
|
|
2622
|
+
this.setScrollX(self.scrollX + deltaX);
|
|
1524
2623
|
},
|
|
1525
2624
|
/**
|
|
1526
|
-
* #
|
|
2625
|
+
* #action
|
|
1527
2626
|
*/
|
|
1528
|
-
|
|
1529
|
-
|
|
2627
|
+
setScrollX(n) {
|
|
2628
|
+
self.scrollX = clamp(n, self.maxScrollX, 0);
|
|
1530
2629
|
},
|
|
1531
2630
|
/**
|
|
1532
|
-
* #
|
|
2631
|
+
* #action
|
|
1533
2632
|
*/
|
|
1534
|
-
|
|
1535
|
-
|
|
2633
|
+
setColumnTracks(tracks) {
|
|
2634
|
+
self.columnTracks.replace(tracks);
|
|
1536
2635
|
},
|
|
1537
2636
|
/**
|
|
1538
|
-
* #
|
|
1539
|
-
*
|
|
1540
|
-
*
|
|
1541
|
-
* @param rowName - The name of the row
|
|
1542
|
-
* @param visibleCol - The visible column index (what the user sees on screen)
|
|
1543
|
-
* @returns The letter at that position, or undefined if it's a gap
|
|
2637
|
+
* #action
|
|
2638
|
+
* replace the row table, which the model keeps as the JSON string
|
|
2639
|
+
* `data.treeMetadata` (see docs/layers.md)
|
|
1544
2640
|
*/
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
return rowMap.get(rowName)?.[visibleColToGlobalCol(blanks, visibleCol)];
|
|
2641
|
+
setRowData(rowData) {
|
|
2642
|
+
self.data.setTreeMetadata(JSON.stringify(rowData));
|
|
1548
2643
|
},
|
|
1549
2644
|
/**
|
|
1550
|
-
* #
|
|
1551
|
-
*
|
|
1552
|
-
* Returns undefined if the position is a gap in the sequence.
|
|
1553
|
-
*
|
|
1554
|
-
* CROSS-REPO CONTRACT: this and the sibling coordinate converters
|
|
1555
|
-
* (seqPosToVisibleCol, globalColToVisibleCol, seqPosToGlobalCol) are used
|
|
1556
|
-
* by jbrowse-plugin-protein3d to translate between alignment columns and
|
|
1557
|
-
* structure/sequence residue positions across gaps. Keep them stable.
|
|
1558
|
-
*
|
|
1559
|
-
* @param rowName - The name of the row
|
|
1560
|
-
* @param visibleCol - The visible column index
|
|
1561
|
-
* @returns The sequence position (0-based), or undefined if it's a gap
|
|
2645
|
+
* #action
|
|
2646
|
+
* replace what the viewer's marks read from the row table
|
|
1562
2647
|
*/
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
rowName,
|
|
1566
|
-
visibleCol,
|
|
1567
|
-
rowMap: self.rowMap,
|
|
1568
|
-
blanks: self.blanks,
|
|
1569
|
-
});
|
|
2648
|
+
setEncodings(encodings) {
|
|
2649
|
+
self.encodings.replace(encodings);
|
|
1570
2650
|
},
|
|
1571
2651
|
/**
|
|
1572
|
-
* #
|
|
1573
|
-
*
|
|
1574
|
-
* Returns undefined if the position is a gap in the sequence.
|
|
1575
|
-
*
|
|
1576
|
-
* @param rowName - The name of the row
|
|
1577
|
-
* @param visibleCol - The visible column index
|
|
1578
|
-
* @returns The sequence position (1-based), or undefined if it's a gap
|
|
2652
|
+
* #action
|
|
2653
|
+
* replace the panels drawn between the tree and the alignment
|
|
1579
2654
|
*/
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
return val !== undefined ? val + 1 : undefined;
|
|
2655
|
+
setRowPanels(panels) {
|
|
2656
|
+
self.rowPanels.replace(panels);
|
|
1583
2657
|
},
|
|
1584
2658
|
/**
|
|
1585
|
-
* #
|
|
1586
|
-
*
|
|
1587
|
-
* Returns undefined if the column is hidden (in blanks).
|
|
1588
|
-
* This is the inverse of visibleColToGlobalCol.
|
|
1589
|
-
*
|
|
1590
|
-
* @param globalCol - The global column index in the full MSA
|
|
1591
|
-
* @returns The visible column index, or undefined if the column is hidden
|
|
2659
|
+
* #action
|
|
2660
|
+
* replace the alignment<->structure correspondence (see docs/layers.md)
|
|
1592
2661
|
*/
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
if (!hideGapsEffective) {
|
|
1596
|
-
return globalCol;
|
|
1597
|
-
}
|
|
1598
|
-
return globalColToVisibleCol(blanks, globalCol);
|
|
2662
|
+
setResidueMappings(mappings) {
|
|
2663
|
+
self.residueMappings.replace(mappings);
|
|
1599
2664
|
},
|
|
1600
2665
|
/**
|
|
1601
|
-
* #
|
|
1602
|
-
* Convert a sequence position (ungapped) to a global column index.
|
|
1603
|
-
*
|
|
1604
|
-
* @param rowName - The name of the row
|
|
1605
|
-
* @param seqPos - The sequence position (0-based, ungapped)
|
|
1606
|
-
* @returns The global column index in the full MSA
|
|
2666
|
+
* #action
|
|
1607
2667
|
*/
|
|
1608
|
-
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1611
|
-
return 0;
|
|
1612
|
-
}
|
|
1613
|
-
const col = self.seqPosGlobalColIndex.get(rowName)?.[seqPos];
|
|
1614
|
-
// past the end of the ungapped sequence: 0 for the degenerate all-gap
|
|
1615
|
-
// row, otherwise one past the last column
|
|
1616
|
-
return col ?? (seqPos === 0 ? 0 : seq.length);
|
|
2668
|
+
toggleTrack(id) {
|
|
2669
|
+
const defaultOff = self.MSA?.tracks.find(t => t.id === id)?.defaultOff;
|
|
2670
|
+
self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id, defaultOff));
|
|
1617
2671
|
},
|
|
1618
2672
|
/**
|
|
1619
|
-
* #
|
|
1620
|
-
* Convert a sequence position (ungapped) directly to a visible column index.
|
|
1621
|
-
* This combines seqPosToGlobalCol and globalColToVisibleCol.
|
|
1622
|
-
*
|
|
1623
|
-
* @param rowName - The name of the row
|
|
1624
|
-
* @param seqPos - The sequence position (0-based, ungapped)
|
|
1625
|
-
* @returns The visible column index, or undefined if the column is hidden
|
|
2673
|
+
* #action
|
|
1626
2674
|
*/
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
return this.globalColToVisibleCol(globalCol);
|
|
2675
|
+
setStatus(status) {
|
|
2676
|
+
self.status = status;
|
|
1630
2677
|
},
|
|
1631
2678
|
}))
|
|
1632
2679
|
.views(self => ({
|
|
1633
2680
|
/**
|
|
1634
2681
|
* #getter
|
|
1635
|
-
* widget width minus the tree area gives the space for the MSA
|
|
1636
2682
|
*/
|
|
1637
|
-
get
|
|
1638
|
-
return
|
|
1639
|
-
(self.showHorizontalScrollbar ? self.minimapHeight : 0) -
|
|
1640
|
-
self.headerHeight);
|
|
2683
|
+
get verticalScrollbarWidth() {
|
|
2684
|
+
return self.showVerticalScrollbar ? 20 : 0;
|
|
1641
2685
|
},
|
|
1642
2686
|
/**
|
|
1643
2687
|
* #getter
|
|
1644
|
-
*
|
|
2688
|
+
* width of the alignment canvas: the msa area less the vertical
|
|
2689
|
+
* scrollbar. showHorizontalScrollbar must not read it, since that feeds
|
|
2690
|
+
* msaAreaHeight -> showVerticalScrollbar and would form a cycle
|
|
1645
2691
|
*/
|
|
1646
|
-
get
|
|
1647
|
-
return
|
|
2692
|
+
get msaCanvasWidth() {
|
|
2693
|
+
return self.msaAreaWidth - this.verticalScrollbarWidth;
|
|
1648
2694
|
},
|
|
1649
2695
|
/**
|
|
1650
|
-
*
|
|
1651
|
-
* the
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
get filteredAnnotations() {
|
|
1666
|
-
return self.annotations.filter(r => self.featureFilters.get(r.accession));
|
|
1667
|
-
},
|
|
1668
|
-
get annotationsByRow() {
|
|
1669
|
-
return groupBy(this.filteredAnnotations, r => r.id);
|
|
2696
|
+
* #method
|
|
2697
|
+
* the cell at a visible column and row index, in the coordinates a host
|
|
2698
|
+
* writes highlights in
|
|
2699
|
+
*/
|
|
2700
|
+
cellAt(visibleCol, rowIndex) {
|
|
2701
|
+
const column = self.visibleColToGlobalCol(visibleCol);
|
|
2702
|
+
const row = rowIndex === undefined ? undefined : self.leaves[rowIndex]?.data.name;
|
|
2703
|
+
return row === undefined
|
|
2704
|
+
? { column: column + 1 }
|
|
2705
|
+
: {
|
|
2706
|
+
column: column + 1,
|
|
2707
|
+
row,
|
|
2708
|
+
residue: self.visibleColToSeqPosOneBased(row, visibleCol),
|
|
2709
|
+
letter: self.rowMap.get(row)?.[column],
|
|
2710
|
+
};
|
|
1670
2711
|
},
|
|
1671
|
-
}))
|
|
1672
|
-
.views(self => ({
|
|
1673
2712
|
/**
|
|
1674
2713
|
* #getter
|
|
2714
|
+
* the cell under the pointer. Public API: MSAViewer's onCellHover
|
|
2715
|
+
* reports it.
|
|
1675
2716
|
*/
|
|
1676
|
-
get
|
|
1677
|
-
|
|
2717
|
+
get hoveredCell() {
|
|
2718
|
+
const { mouseCol, mouseRow } = self;
|
|
2719
|
+
return mouseCol === undefined
|
|
2720
|
+
? undefined
|
|
2721
|
+
: this.cellAt(mouseCol, mouseRow);
|
|
1678
2722
|
},
|
|
1679
|
-
}))
|
|
1680
|
-
.views(self => ({
|
|
1681
2723
|
/**
|
|
1682
2724
|
* #getter
|
|
2725
|
+
* the cell a click pinned. Public API: MSAViewer's onCellClick reports it.
|
|
1683
2726
|
*/
|
|
1684
|
-
get
|
|
1685
|
-
|
|
2727
|
+
get clickedCell() {
|
|
2728
|
+
const { mouseClickCol, mouseClickRow } = self;
|
|
2729
|
+
return mouseClickCol === undefined
|
|
2730
|
+
? undefined
|
|
2731
|
+
: this.cellAt(mouseClickCol, mouseClickRow);
|
|
1686
2732
|
},
|
|
1687
2733
|
/**
|
|
1688
2734
|
* #getter
|
|
1689
|
-
*
|
|
1690
|
-
*
|
|
1691
|
-
* feeds msaAreaHeight -> showVerticalScrollbar and would close a cycle
|
|
2735
|
+
* the columns on screen. Public API: MSAViewer's onViewportChange reports
|
|
2736
|
+
* it.
|
|
1692
2737
|
*/
|
|
1693
|
-
get
|
|
1694
|
-
|
|
2738
|
+
get viewport() {
|
|
2739
|
+
const { scrollX, colWidth, numColumns, viewInitialized } = self;
|
|
2740
|
+
if (numColumns === 0 || !viewInitialized) {
|
|
2741
|
+
return undefined;
|
|
2742
|
+
}
|
|
2743
|
+
const { xStart, xEnd } = visibleColRange({
|
|
2744
|
+
offsetX: -scrollX,
|
|
2745
|
+
blockWidth: this.msaCanvasWidth,
|
|
2746
|
+
colWidth,
|
|
2747
|
+
});
|
|
2748
|
+
const last = Math.max(0, Math.min(xEnd, numColumns) - 1);
|
|
2749
|
+
return {
|
|
2750
|
+
startColumn: self.visibleColToGlobalCol(Math.min(xStart, last)) + 1,
|
|
2751
|
+
endColumn: self.visibleColToGlobalCol(last) + 1,
|
|
2752
|
+
};
|
|
1695
2753
|
},
|
|
1696
2754
|
/**
|
|
1697
2755
|
* #getter
|
|
1698
2756
|
* ordinal segment types (exons etc.), ordered by sequence position so
|
|
1699
|
-
* exon-1..exon-14
|
|
1700
|
-
* labeled by number
|
|
2757
|
+
* exon-1..exon-14 run left-to-right; colored by alternating shade and
|
|
2758
|
+
* labeled by number, with no legend row
|
|
1701
2759
|
*/
|
|
1702
2760
|
get segmentDomainTypes() {
|
|
1703
2761
|
return [...self.annotationTypes.values()]
|
|
@@ -1723,9 +2781,38 @@ function stateModelFactory() {
|
|
|
1723
2781
|
get strokePalette() {
|
|
1724
2782
|
return transform(this.fillPalette, ([key, val]) => [
|
|
1725
2783
|
key,
|
|
1726
|
-
|
|
2784
|
+
outlineColor(val),
|
|
1727
2785
|
]);
|
|
1728
2786
|
},
|
|
2787
|
+
/**
|
|
2788
|
+
* #getter
|
|
2789
|
+
* the encoding coloring the overlay's spans, undefined when none does,
|
|
2790
|
+
* which leaves each span the color its accession takes in `fillPalette`
|
|
2791
|
+
*/
|
|
2792
|
+
get featureFillEncoding() {
|
|
2793
|
+
return this.resolvedEncodings.find(e => e.channel === 'featureFill');
|
|
2794
|
+
},
|
|
2795
|
+
/**
|
|
2796
|
+
* #getter
|
|
2797
|
+
* the fill and outline of every feature's span: its own GFF `color=`
|
|
2798
|
+
* first, then the `featureFill` scale, then the accession palette.
|
|
2799
|
+
* Computed once per change of the features, the encodings or the palette
|
|
2800
|
+
*/
|
|
2801
|
+
get featureColors() {
|
|
2802
|
+
return featureColorMap(self.filteredAnnotations, this.featureFillEncoding, this.fillPalette);
|
|
2803
|
+
},
|
|
2804
|
+
/**
|
|
2805
|
+
* #getter
|
|
2806
|
+
* the text the `featureLabel` channel draws inside each span, undefined
|
|
2807
|
+
* when no encoding names the channel. A data channel, so it draws
|
|
2808
|
+
* whether or not the residue letters do
|
|
2809
|
+
*/
|
|
2810
|
+
get featureLabels() {
|
|
2811
|
+
const encoding = this.resolvedEncodings.find(e => e.channel === 'featureLabel');
|
|
2812
|
+
return encoding
|
|
2813
|
+
? featureLabelMap(self.filteredAnnotations, encoding.field)
|
|
2814
|
+
: undefined;
|
|
2815
|
+
},
|
|
1729
2816
|
/**
|
|
1730
2817
|
* #getter
|
|
1731
2818
|
* accession -> number drawn on each segment band: the trailing number of
|
|
@@ -1740,24 +2827,92 @@ function stateModelFactory() {
|
|
|
1740
2827
|
/**
|
|
1741
2828
|
* #getter
|
|
1742
2829
|
* the domain types currently drawn on the alignment (filtered-on), shared
|
|
1743
|
-
* by the on-screen legend and the SVG export legend
|
|
1744
|
-
*
|
|
1745
|
-
*
|
|
2830
|
+
* by the on-screen legend and the SVG export legend: the categorical types
|
|
2831
|
+
* ordered by sequence position. Ordinal segments (exons) are numbered on
|
|
2832
|
+
* the band instead
|
|
1746
2833
|
*/
|
|
1747
2834
|
get visibleDomainTypes() {
|
|
1748
2835
|
return this.categoricalDomainTypes
|
|
1749
|
-
.filter(d => self.
|
|
2836
|
+
.filter(d => !self.turnedOffFeatures.get(d.accession))
|
|
1750
2837
|
.toSorted((a, b) => a.start - b.start);
|
|
1751
2838
|
},
|
|
2839
|
+
/**
|
|
2840
|
+
* #getter
|
|
2841
|
+
* the categorical color keys drawn for this view, shared by the on-screen
|
|
2842
|
+
* legend overlay and the SVG export's reserved column. The domain overlay
|
|
2843
|
+
* produces the first, listing the `featureFill` scale where an encoding
|
|
2844
|
+
* names one. Every field a row-table encoding or a row panel reads
|
|
2845
|
+
* produces one more, so two channels over one field, or two strips over
|
|
2846
|
+
* it, list that field once. A strip's `legend` names the title it lists
|
|
2847
|
+
* under, so strips over eight fields with one set of colors list one
|
|
2848
|
+
* legend between them
|
|
2849
|
+
*/
|
|
2850
|
+
get legends() {
|
|
2851
|
+
const { featureFillEncoding, fillPalette, visibleDomainTypes } = this;
|
|
2852
|
+
const byKey = new Map();
|
|
2853
|
+
// the accession palette keys on no field, so it takes a key of its own
|
|
2854
|
+
const add = (field, entries, id) => {
|
|
2855
|
+
if (entries.length === 0) {
|
|
2856
|
+
return;
|
|
2857
|
+
}
|
|
2858
|
+
const key = field === undefined ? 'domains' : `field:${field}`;
|
|
2859
|
+
const legend = byKey.get(key);
|
|
2860
|
+
if (legend) {
|
|
2861
|
+
const seen = new Set(legend.entries.map(e => e.id));
|
|
2862
|
+
legend.entries.push(...entries.filter(e => !seen.has(e.id)));
|
|
2863
|
+
}
|
|
2864
|
+
else {
|
|
2865
|
+
byKey.set(key, {
|
|
2866
|
+
id: id ?? (field === undefined ? 'domains' : `rowData-${field}`),
|
|
2867
|
+
title: field ?? 'Domains',
|
|
2868
|
+
entries: [...entries],
|
|
2869
|
+
});
|
|
2870
|
+
}
|
|
2871
|
+
};
|
|
2872
|
+
// the overlay draws the domain key; with no columns it draws nothing,
|
|
2873
|
+
// and a features panel taking the same colors lists them instead
|
|
2874
|
+
if (self.actuallyShowDomains && self.numColumns > 0) {
|
|
2875
|
+
add(featureFillEncoding?.field, featureFillEncoding
|
|
2876
|
+
? featureFillEncoding.legend
|
|
2877
|
+
: visibleDomainTypes.map(d => ({
|
|
2878
|
+
id: d.accession,
|
|
2879
|
+
label: d.name,
|
|
2880
|
+
color: fillPalette[d.accession],
|
|
2881
|
+
})), 'domains');
|
|
2882
|
+
}
|
|
2883
|
+
for (const { channel, field, legend } of this.resolvedEncodings) {
|
|
2884
|
+
if (!featureChannels.has(channel)) {
|
|
2885
|
+
add(field, legend);
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
for (const { legendTitle, legend } of this.resolvedRowPanels) {
|
|
2889
|
+
add(legendTitle, legend);
|
|
2890
|
+
}
|
|
2891
|
+
return [...byKey.values()];
|
|
2892
|
+
},
|
|
2893
|
+
/**
|
|
2894
|
+
* #getter
|
|
2895
|
+
* whether the overlay marks each domain with a bar under its row instead
|
|
2896
|
+
* of filling the row behind the letters. Letter-color mode hands the
|
|
2897
|
+
* background to the color scheme, so a filled box would paint over it and
|
|
2898
|
+
* leave the setting with nothing to show. Sub-row layout already stacks
|
|
2899
|
+
* the boxes clear of the letters, and with the letters too small to draw
|
|
2900
|
+
* the filled box is the only thing left to read.
|
|
2901
|
+
*/
|
|
2902
|
+
get domainUnderline() {
|
|
2903
|
+
return (self.actuallyShowDomains &&
|
|
2904
|
+
!self.bgColor &&
|
|
2905
|
+
!self.subFeatureRows &&
|
|
2906
|
+
self.showMsaLetters);
|
|
2907
|
+
},
|
|
1752
2908
|
/**
|
|
1753
2909
|
* #getter
|
|
1754
2910
|
* every filtered-on annotation resolved to the visible column span it is
|
|
1755
2911
|
* drawn across, keyed by row name. Each row is ordered longest-first so a
|
|
1756
|
-
* short domain
|
|
1757
|
-
*
|
|
1758
|
-
* block
|
|
1759
|
-
*
|
|
1760
|
-
* needs to keep residues readable on top of the boxes.
|
|
2912
|
+
* nested short domain draws on top, and each band carries the lane the
|
|
2913
|
+
* sub-row layout puts it in. Resolved once here instead of per canvas
|
|
2914
|
+
* block per redraw; the letter renderer also reads the band colors to
|
|
2915
|
+
* pick legible letter colors.
|
|
1761
2916
|
*/
|
|
1762
2917
|
get domainBands() {
|
|
1763
2918
|
const { blanks } = self;
|
|
@@ -1766,26 +2921,27 @@ function stateModelFactory() {
|
|
|
1766
2921
|
const rowBands = annotations
|
|
1767
2922
|
.toSorted((a, b) => len(b) - len(a))
|
|
1768
2923
|
.map(annotation => {
|
|
1769
|
-
// annotation positions are 1-based and inclusive.
|
|
1770
|
-
//
|
|
1771
|
-
//
|
|
1772
|
-
//
|
|
1773
|
-
//
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2924
|
+
// annotation positions are 1-based and inclusive. endCol is
|
|
2925
|
+
// exclusive, one past the last residue's column, so the band does
|
|
2926
|
+
// not extend over a following gap run. An endpoint in a hidden
|
|
2927
|
+
// column moves to the neighboring boundary. Bands with no visible
|
|
2928
|
+
// columns, or naming a missing row, are dropped.
|
|
2929
|
+
const start = self.seqPosToGlobalCol(name, annotation.start - 1);
|
|
2930
|
+
const end = self.seqPosToGlobalCol(name, annotation.end - 1);
|
|
2931
|
+
if (start === undefined || end === undefined) {
|
|
2932
|
+
return undefined;
|
|
2933
|
+
}
|
|
2934
|
+
const startCol = visibleColsBefore(blanks, start);
|
|
2935
|
+
const endCol = visibleColsBefore(blanks, end + 1);
|
|
1779
2936
|
return endCol > startCol
|
|
1780
2937
|
? { annotation, startCol, endCol }
|
|
1781
2938
|
: undefined;
|
|
1782
2939
|
})
|
|
1783
2940
|
.filter(notEmpty)
|
|
1784
|
-
// numbered after
|
|
1785
|
-
// not leave an empty sub-row behind it
|
|
2941
|
+
// numbered after filtering, so a dropped band leaves no gap
|
|
1786
2942
|
.map((band, stackIndex) => ({ ...band, stackIndex }));
|
|
1787
2943
|
if (rowBands.length > 0) {
|
|
1788
|
-
bands.set(name, rowBands);
|
|
2944
|
+
bands.set(name, packDomainLanes(rowBands));
|
|
1789
2945
|
}
|
|
1790
2946
|
}
|
|
1791
2947
|
return bands;
|
|
@@ -1809,11 +2965,13 @@ function stateModelFactory() {
|
|
|
1809
2965
|
get mouseOverDomains() {
|
|
1810
2966
|
const { mouseCol } = self;
|
|
1811
2967
|
const name = self.mouseOverRowName;
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
2968
|
+
if (name === undefined || mouseCol === undefined) {
|
|
2969
|
+
return noDomains;
|
|
2970
|
+
}
|
|
2971
|
+
const hits = (this.domainBands.get(name) ?? [])
|
|
2972
|
+
.filter(b => mouseCol >= b.startCol && mouseCol < b.endCol)
|
|
2973
|
+
.map(b => b.annotation);
|
|
2974
|
+
return hits.length > 0 ? hits : noDomains;
|
|
1817
2975
|
},
|
|
1818
2976
|
/**
|
|
1819
2977
|
* #getter
|
|
@@ -1829,8 +2987,7 @@ function stateModelFactory() {
|
|
|
1829
2987
|
* #getter
|
|
1830
2988
|
* row indices highlighted by the current tree hover (a hovered internal
|
|
1831
2989
|
* node highlights every tip below it). Shared by the tree and MSA overlay
|
|
1832
|
-
* canvases
|
|
1833
|
-
* name->index map rather than rebuilding a lookup on each mouse move.
|
|
2990
|
+
* canvases, via the memoized name->index map.
|
|
1834
2991
|
*/
|
|
1835
2992
|
get hoveredRowIndices() {
|
|
1836
2993
|
const { hoveredTreeNode, rowNamesSet } = self;
|
|
@@ -1843,8 +3000,8 @@ function stateModelFactory() {
|
|
|
1843
3000
|
/**
|
|
1844
3001
|
* #getter
|
|
1845
3002
|
* contiguous runs of `highlightedColumns`, so a run of highlighted columns
|
|
1846
|
-
* draws as one bordered band.
|
|
1847
|
-
*
|
|
3003
|
+
* draws as one bordered band. Memoized because the overlay canvas redraws
|
|
3004
|
+
* on every mouse move.
|
|
1848
3005
|
*/
|
|
1849
3006
|
get highlightedColumnRuns() {
|
|
1850
3007
|
const { highlightedColumns } = self;
|
|
@@ -1868,12 +3025,13 @@ function stateModelFactory() {
|
|
|
1868
3025
|
* is dropped. Row names that match no row are ignored.
|
|
1869
3026
|
*/
|
|
1870
3027
|
get resolvedHighlights() {
|
|
1871
|
-
const {
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
3028
|
+
const { rowNamesSet, transientHighlights } = self;
|
|
3029
|
+
// persisted highlights first, so transient ones draw on top
|
|
3030
|
+
const all = [
|
|
3031
|
+
...self.highlights,
|
|
3032
|
+
...Object.values(transientHighlights).flat(),
|
|
3033
|
+
];
|
|
3034
|
+
return all.flatMap(({ row, rows, start, end, label, color }) => {
|
|
1877
3035
|
const base = { label, color };
|
|
1878
3036
|
if (rows) {
|
|
1879
3037
|
const rowIndices = rows
|
|
@@ -1884,68 +3042,238 @@ function stateModelFactory() {
|
|
|
1884
3042
|
if (start === undefined || end === undefined) {
|
|
1885
3043
|
return [];
|
|
1886
3044
|
}
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
3045
|
+
const span = self.visibleSpan({ row, start, end });
|
|
3046
|
+
return span ? [{ ...base, ...span, rowIndices: [] }] : [];
|
|
3047
|
+
});
|
|
3048
|
+
},
|
|
3049
|
+
/**
|
|
3050
|
+
* #method
|
|
3051
|
+
* per-column summary statistics: consensus residue and its identity
|
|
3052
|
+
* fraction, both conservation scores, gap fraction, and the sorted non-gap
|
|
3053
|
+
* residue distribution. undefined past the end of the alignment or for an
|
|
3054
|
+
* all-gap column.
|
|
3055
|
+
*/
|
|
3056
|
+
columnStatsAt(col) {
|
|
3057
|
+
const { colStats, conservation, propertyConservation } = self;
|
|
3058
|
+
return columnStats({
|
|
3059
|
+
col,
|
|
3060
|
+
colStats,
|
|
3061
|
+
conservation,
|
|
3062
|
+
propertyConservation,
|
|
3063
|
+
});
|
|
3064
|
+
},
|
|
3065
|
+
/**
|
|
3066
|
+
* #getter
|
|
3067
|
+
* `columnStatsAt` for the hovered column, undefined when nothing is
|
|
3068
|
+
* hovered
|
|
3069
|
+
*/
|
|
3070
|
+
get mouseOverColumnStats() {
|
|
3071
|
+
const { mouseCol, colStats, conservation, propertyConservation } = self;
|
|
3072
|
+
return mouseCol === undefined
|
|
3073
|
+
? undefined
|
|
3074
|
+
: columnStats({
|
|
3075
|
+
col: mouseCol,
|
|
3076
|
+
colStats,
|
|
3077
|
+
conservation,
|
|
3078
|
+
propertyConservation,
|
|
3079
|
+
});
|
|
3080
|
+
},
|
|
3081
|
+
/**
|
|
3082
|
+
* #method
|
|
3083
|
+
*/
|
|
3084
|
+
getRowData(name) {
|
|
3085
|
+
return {
|
|
3086
|
+
data: self.MSA?.getRowData(name),
|
|
3087
|
+
rowData: self.rowDataOf(name),
|
|
3088
|
+
};
|
|
3089
|
+
},
|
|
3090
|
+
/**
|
|
3091
|
+
* #getter
|
|
3092
|
+
* each encoding with its scale resolved against the values its field
|
|
3093
|
+
* takes: a feature channel reads them across the features drawn, every
|
|
3094
|
+
* other channel across the row table. Resolved once per change of that
|
|
3095
|
+
* table or the encodings, never per row per frame.
|
|
3096
|
+
*/
|
|
3097
|
+
get resolvedEncodings() {
|
|
3098
|
+
const rows = Object.values(self.rowData);
|
|
3099
|
+
const features = self.filteredAnnotations;
|
|
3100
|
+
return self.encodings.map(encoding => ({
|
|
3101
|
+
...encoding,
|
|
3102
|
+
...resolveScale(encoding.scale, featureChannels.has(encoding.channel)
|
|
3103
|
+
? features
|
|
3104
|
+
.map(a => featureField(a, encoding.field))
|
|
3105
|
+
.filter(notEmpty)
|
|
3106
|
+
: rows.map(row => row?.[encoding.field]).filter(notEmpty)),
|
|
3107
|
+
}));
|
|
3108
|
+
},
|
|
3109
|
+
/**
|
|
3110
|
+
* #getter
|
|
3111
|
+
* each row panel with its scale resolved against the values its field
|
|
3112
|
+
* takes across the row table, giving the color per row name, the pixel
|
|
3113
|
+
* column it draws in, and the entries its legend lists. Resolved once
|
|
3114
|
+
* per change of that table or the panels, never per block per frame.
|
|
3115
|
+
*/
|
|
3116
|
+
get resolvedRowPanels() {
|
|
3117
|
+
const rows = Object.entries(self.rowData);
|
|
3118
|
+
const { fillPalette, featureFillEncoding, featureLabels } = this;
|
|
3119
|
+
const annotations = self.filteredAnnotations;
|
|
3120
|
+
let offsetX = 0;
|
|
3121
|
+
return self.rowPanels.map((panel, index) => {
|
|
3122
|
+
const width = rowPanelWidth(panel, self.rowHeight);
|
|
3123
|
+
const base = { id: `rowpanel-${index}`, width, offsetX };
|
|
3124
|
+
offsetX += width;
|
|
3125
|
+
if (panel.kind === 'features') {
|
|
3126
|
+
const color = panel.encoding?.color;
|
|
3127
|
+
const encoding = color
|
|
3128
|
+
? resolveFeatureScale(color.field, color.scale, annotations)
|
|
3129
|
+
: featureFillEncoding;
|
|
3130
|
+
const label = panel.encoding?.label;
|
|
3131
|
+
const align = panel.transform?.find(t => t.type === 'align');
|
|
3132
|
+
return {
|
|
3133
|
+
...base,
|
|
3134
|
+
kind: panel.kind,
|
|
3135
|
+
x: panel.x,
|
|
3136
|
+
header: panel.header ?? '',
|
|
3137
|
+
field: encoding?.field,
|
|
3138
|
+
legendTitle: encoding?.field,
|
|
3139
|
+
spans: featurePanelSpans({
|
|
3140
|
+
panel,
|
|
3141
|
+
width,
|
|
3142
|
+
rowHeight: self.rowHeight,
|
|
3143
|
+
colWidth: self.colWidth,
|
|
3144
|
+
domainBands: this.domainBands,
|
|
3145
|
+
annotationsByRow: self.annotationsByRow,
|
|
3146
|
+
shifts: align
|
|
3147
|
+
? this.featureAlignShifts.get(align.on)
|
|
3148
|
+
: undefined,
|
|
3149
|
+
}),
|
|
3150
|
+
colors: featureColorMap(annotations, encoding, fillPalette),
|
|
3151
|
+
labels: label
|
|
3152
|
+
? featureLabelMap(annotations, label)
|
|
3153
|
+
: featureLabels,
|
|
3154
|
+
legend: encoding?.legend ??
|
|
3155
|
+
this.visibleDomainTypes.map(d => ({
|
|
3156
|
+
id: d.accession,
|
|
3157
|
+
label: d.name,
|
|
3158
|
+
color: fillPalette[d.accession],
|
|
3159
|
+
})),
|
|
3160
|
+
};
|
|
3161
|
+
}
|
|
3162
|
+
const { colorOf, legend } = resolveScale(panel.scale, rows.map(([, row]) => row?.[panel.field]).filter(notEmpty));
|
|
3163
|
+
const colors = new Map();
|
|
3164
|
+
for (const [name, row] of rows) {
|
|
3165
|
+
const value = row?.[panel.field];
|
|
3166
|
+
const color = value === undefined ? undefined : colorOf(value);
|
|
3167
|
+
if (color) {
|
|
3168
|
+
colors.set(name, color);
|
|
1892
3169
|
}
|
|
1893
|
-
startGlobal = self.seqPosToGlobalCol(row, start - 1);
|
|
1894
|
-
endGlobal = self.seqPosToGlobalCol(row, end - 1);
|
|
1895
3170
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
:
|
|
3171
|
+
return {
|
|
3172
|
+
...base,
|
|
3173
|
+
kind: panel.kind,
|
|
3174
|
+
field: panel.field,
|
|
3175
|
+
header: panel.header ?? panel.field,
|
|
3176
|
+
legendTitle: panel.legend ?? panel.field,
|
|
3177
|
+
colors,
|
|
3178
|
+
legend,
|
|
3179
|
+
};
|
|
1902
3180
|
});
|
|
1903
3181
|
},
|
|
1904
3182
|
/**
|
|
1905
3183
|
* #getter
|
|
1906
|
-
*
|
|
1907
|
-
*
|
|
1908
|
-
*
|
|
3184
|
+
* the shift an `align` transform gives each row, keyed by the feature
|
|
3185
|
+
* name it aligns on: the offset putting the first feature of that name
|
|
3186
|
+
* at zero. A row carrying no such feature is absent, and keeps its own
|
|
3187
|
+
* origin.
|
|
3188
|
+
*/
|
|
3189
|
+
get featureAlignShifts() {
|
|
3190
|
+
const names = new Set(self.rowPanels.flatMap(panel => panel.kind === 'features' && panel.x === 'position'
|
|
3191
|
+
? (panel.transform ?? [])
|
|
3192
|
+
.filter(t => t.type === 'align')
|
|
3193
|
+
.map(t => t.on)
|
|
3194
|
+
: []));
|
|
3195
|
+
return new Map([...names].map(on => [
|
|
3196
|
+
on,
|
|
3197
|
+
new Map(Object.entries(self.annotationsByRow)
|
|
3198
|
+
.map(([name, annotations]) => {
|
|
3199
|
+
const hit = annotations.find(a => featureName(a) === on);
|
|
3200
|
+
return hit ? [name, 1 - hit.start] : undefined;
|
|
3201
|
+
})
|
|
3202
|
+
.filter(notEmpty)),
|
|
3203
|
+
]));
|
|
3204
|
+
},
|
|
3205
|
+
/**
|
|
3206
|
+
* #getter
|
|
3207
|
+
* the color the `tipLabel` channel gives each row, by row name. Undefined
|
|
3208
|
+
* when no encoding names the channel, which leaves the labels the theme's
|
|
3209
|
+
* text color.
|
|
1909
3210
|
*/
|
|
1910
|
-
get
|
|
1911
|
-
const
|
|
1912
|
-
if (
|
|
3211
|
+
get tipLabelColors() {
|
|
3212
|
+
const encoding = this.resolvedEncodings.find(e => e.channel === 'tipLabel');
|
|
3213
|
+
if (!encoding) {
|
|
1913
3214
|
return undefined;
|
|
1914
3215
|
}
|
|
1915
|
-
const
|
|
1916
|
-
|
|
1917
|
-
|
|
3216
|
+
const colors = new Map();
|
|
3217
|
+
for (const [name, row] of Object.entries(self.rowData)) {
|
|
3218
|
+
const value = row?.[encoding.field];
|
|
3219
|
+
const color = value === undefined ? undefined : encoding.colorOf(value);
|
|
3220
|
+
if (color) {
|
|
3221
|
+
colors.set(name, color);
|
|
3222
|
+
}
|
|
1918
3223
|
}
|
|
1919
|
-
|
|
1920
|
-
|
|
3224
|
+
return colors;
|
|
3225
|
+
},
|
|
3226
|
+
/**
|
|
3227
|
+
* #getter
|
|
3228
|
+
* the wash the `rowTint` channel draws over each row, indexed by row, or
|
|
3229
|
+
* undefined when no encoding names the channel. The overlay draws these,
|
|
3230
|
+
* so a tint stays out of the raster tile cache and its keys.
|
|
3231
|
+
*/
|
|
3232
|
+
get rowTints() {
|
|
3233
|
+
const encoding = this.resolvedEncodings.find(e => e.channel === 'rowTint');
|
|
3234
|
+
if (!encoding) {
|
|
1921
3235
|
return undefined;
|
|
1922
3236
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
.
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
gaps,
|
|
1932
|
-
gapFraction: gaps / total,
|
|
1933
|
-
conservation: self.conservation[mouseCol] ?? 0,
|
|
1934
|
-
propertyConservation: self.propertyConservation[mouseCol],
|
|
1935
|
-
consensusLetter: consensus?.[0] ?? '',
|
|
1936
|
-
consensusCount: consensus?.[1] ?? 0,
|
|
1937
|
-
consensusFraction: consensus ? consensus[1] / total : 0,
|
|
1938
|
-
distribution,
|
|
1939
|
-
};
|
|
3237
|
+
return self.rowNames.map(name => {
|
|
3238
|
+
const value = self.rowDataOf(name)?.[encoding.field];
|
|
3239
|
+
const color = value === undefined ? undefined : encoding.colorOf(value);
|
|
3240
|
+
if (!color) {
|
|
3241
|
+
return undefined;
|
|
3242
|
+
}
|
|
3243
|
+
return withAlpha(color, rowTintAlpha);
|
|
3244
|
+
});
|
|
1940
3245
|
},
|
|
1941
3246
|
/**
|
|
1942
|
-
* #
|
|
3247
|
+
* #getter
|
|
3248
|
+
* the color the `branch` channel gives each tree edge, by the node id at
|
|
3249
|
+
* the edge's far end, or undefined when no encoding names the channel. A
|
|
3250
|
+
* node takes the field value its tips agree on, so a clade of one value
|
|
3251
|
+
* colors down from where it splits off, and a node whose tips disagree or
|
|
3252
|
+
* whose value has no color is absent and draws in the default color.
|
|
3253
|
+
*
|
|
3254
|
+
* The pass runs over the whole tree, never `root`, so a collapsed or
|
|
3255
|
+
* focused clade keeps the color the full tree gives it.
|
|
1943
3256
|
*/
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
}
|
|
3257
|
+
get branchColors() {
|
|
3258
|
+
const encoding = this.resolvedEncodings.find(e => e.channel === 'branch');
|
|
3259
|
+
if (!encoding) {
|
|
3260
|
+
return undefined;
|
|
3261
|
+
}
|
|
3262
|
+
const order = preorder(self.tree);
|
|
3263
|
+
const values = new Map();
|
|
3264
|
+
const colors = new Map();
|
|
3265
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
3266
|
+
const node = order[i];
|
|
3267
|
+
const value = node.children.length > 0
|
|
3268
|
+
? sharedValue(node.children.map(child => values.get(child)))
|
|
3269
|
+
: self.rowDataOf(node.name)?.[encoding.field];
|
|
3270
|
+
values.set(node, value);
|
|
3271
|
+
const color = value === undefined ? undefined : encoding.colorOf(value);
|
|
3272
|
+
if (color) {
|
|
3273
|
+
colors.set(node.id, color);
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
return colors;
|
|
1949
3277
|
},
|
|
1950
3278
|
}))
|
|
1951
3279
|
.actions(self => ({
|
|
@@ -1958,27 +3286,43 @@ function stateModelFactory() {
|
|
|
1958
3286
|
/**
|
|
1959
3287
|
* #action
|
|
1960
3288
|
*/
|
|
1961
|
-
|
|
1962
|
-
self.
|
|
3289
|
+
setHideHeader(arg) {
|
|
3290
|
+
self.hideHeader = arg;
|
|
3291
|
+
},
|
|
3292
|
+
/**
|
|
3293
|
+
* #action
|
|
3294
|
+
* focus the subtree a click `y` pixels down the tree overview lands on.
|
|
3295
|
+
* A click inside the box already drawn there clears the focus, the way
|
|
3296
|
+
* clicking the focused branch again does.
|
|
3297
|
+
*/
|
|
3298
|
+
treeOverviewClick(y) {
|
|
3299
|
+
const hit = self.treeOverviewHit(y);
|
|
3300
|
+
const focus = self.treeOverviewFocusRows;
|
|
3301
|
+
const inside = !!focus && !!hit && hit.rows[0] >= focus[0] && hit.rows[1] <= focus[1];
|
|
3302
|
+
self.setShowOnly(inside || !hit ? undefined : hit.id);
|
|
1963
3303
|
},
|
|
1964
3304
|
/**
|
|
1965
3305
|
* #action
|
|
3306
|
+
* resize every track sharing a `heightKey`; see `trackHeights`
|
|
1966
3307
|
*/
|
|
1967
|
-
|
|
1968
|
-
self.
|
|
3308
|
+
setTrackHeight(heightKey, height) {
|
|
3309
|
+
self.trackHeights.set(heightKey, height);
|
|
1969
3310
|
},
|
|
1970
3311
|
/**
|
|
1971
3312
|
* #action
|
|
1972
|
-
* Return to the import form: every property
|
|
1973
|
-
*
|
|
1974
|
-
*
|
|
1975
|
-
* reach are cleared by hand.
|
|
3313
|
+
* Return to the import form: reset every property not in
|
|
3314
|
+
* `preservedOnReset` to its default, then clear the file-derived
|
|
3315
|
+
* volatiles applySnapshot does not touch.
|
|
1976
3316
|
*/
|
|
1977
3317
|
reset() {
|
|
3318
|
+
self.resetCount++;
|
|
3319
|
+
self.clearWarnings();
|
|
3320
|
+
self.setStatus(undefined);
|
|
1978
3321
|
applySnapshot(self, Object.fromEntries(Object.entries(getSnapshot(self)).filter(([key]) => preservedOnReset.has(key))));
|
|
1979
3322
|
self.setError(undefined);
|
|
1980
3323
|
self.setAnnotations([]);
|
|
1981
3324
|
self.setHighlightedColumns(undefined);
|
|
3325
|
+
self.transientHighlights = {};
|
|
1982
3326
|
self.setMousePos(undefined, undefined);
|
|
1983
3327
|
self.setMouseClickPos(undefined, undefined);
|
|
1984
3328
|
self.setHoveredTreeNode(undefined);
|
|
@@ -1992,20 +3336,31 @@ function stateModelFactory() {
|
|
|
1992
3336
|
const blob = new Blob([html], { type: 'image/svg+xml' });
|
|
1993
3337
|
saveAs(blob, exportFileName(self.msaFilehandle, 'svg'));
|
|
1994
3338
|
},
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
3339
|
+
/**
|
|
3340
|
+
* #action
|
|
3341
|
+
* show or hide an annotation type. Only hidden types are recorded; see
|
|
3342
|
+
* `turnedOffFeatures`
|
|
3343
|
+
*/
|
|
3344
|
+
setFilter(accession, shown) {
|
|
3345
|
+
if (shown) {
|
|
3346
|
+
self.turnedOffFeatures.delete(accession);
|
|
3347
|
+
}
|
|
3348
|
+
else {
|
|
3349
|
+
self.turnedOffFeatures.set(accession, true);
|
|
1998
3350
|
}
|
|
1999
|
-
},
|
|
2000
|
-
setFilter(arg, flag) {
|
|
2001
|
-
self.featureFilters.set(arg, flag);
|
|
2002
3351
|
},
|
|
2003
3352
|
/**
|
|
2004
3353
|
* #action
|
|
2005
3354
|
*/
|
|
2006
3355
|
fit() {
|
|
2007
|
-
|
|
2008
|
-
|
|
3356
|
+
// two passes: each direction's viewport depends on whether the other
|
|
3357
|
+
// overflows (the minimap takes height, the vertical scrollbar width)
|
|
3358
|
+
transaction(() => {
|
|
3359
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
3360
|
+
this.fitHorizontally();
|
|
3361
|
+
this.fitVertically();
|
|
3362
|
+
}
|
|
3363
|
+
});
|
|
2009
3364
|
},
|
|
2010
3365
|
/**
|
|
2011
3366
|
* #action
|
|
@@ -2016,34 +3371,64 @@ function stateModelFactory() {
|
|
|
2016
3371
|
}
|
|
2017
3372
|
self.scrollY = 0;
|
|
2018
3373
|
},
|
|
3374
|
+
/**
|
|
3375
|
+
* #action
|
|
3376
|
+
* zoom and scroll so a span fills the alignment's width, in highlight
|
|
3377
|
+
* coordinates (see visibleSpan). Does nothing before the viewer knows
|
|
3378
|
+
* its width, or for a span that resolves to no visible column.
|
|
3379
|
+
*/
|
|
3380
|
+
zoomToRegion(region) {
|
|
3381
|
+
const span = self.visibleSpan(region);
|
|
3382
|
+
if (!span || !self.viewInitialized) {
|
|
3383
|
+
return;
|
|
3384
|
+
}
|
|
3385
|
+
transaction(() => {
|
|
3386
|
+
self.colWidth = clamp(self.msaCanvasWidth / (span.endCol - span.startCol + 1), minColWidth, maxCellSize);
|
|
3387
|
+
self.scrollX = clamp(-span.startCol * self.colWidth, self.maxScrollX, 0);
|
|
3388
|
+
});
|
|
3389
|
+
},
|
|
2019
3390
|
/**
|
|
2020
3391
|
* #action
|
|
2021
3392
|
*/
|
|
2022
3393
|
fitHorizontally() {
|
|
2023
3394
|
if (self.numColumns > 0) {
|
|
2024
|
-
//
|
|
2025
|
-
// the right edge -- and short of the width that shows a minimap, so
|
|
2026
|
-
// nothing on screen said they were there
|
|
3395
|
+
// msaCanvasWidth excludes the vertical scrollbar's 20px
|
|
2027
3396
|
self.colWidth = clamp(self.msaCanvasWidth / self.numColumns, minColWidth, maxCellSize);
|
|
2028
3397
|
}
|
|
2029
3398
|
self.scrollX = 0;
|
|
2030
3399
|
},
|
|
2031
3400
|
afterCreate() {
|
|
2032
|
-
// seed the highlighted-columns overlay from the declarative property so
|
|
2033
|
-
// a shared snapshot/URL opens with those columns highlighted (the
|
|
2034
|
-
// volatile highlightedColumns can later be driven by genome-hover sync)
|
|
2035
3401
|
if (self.highlightColumns?.length) {
|
|
2036
3402
|
self.setHighlightedColumns(self.highlightColumns);
|
|
2037
3403
|
}
|
|
3404
|
+
// The `collapse` and `focus` clade marks seed the collapsed list and
|
|
3405
|
+
// the subtree in focus, which the tree, `hideGapsEffective` and the
|
|
3406
|
+
// alignment all read. The tree arrives with the model for inline data
|
|
3407
|
+
// and later for a filehandle, so the seeding waits for it and then runs
|
|
3408
|
+
// once: expanding a seeded clade sticks, and the record collapses it
|
|
3409
|
+
// again only on reload.
|
|
3410
|
+
let cladesSeeded = false;
|
|
2038
3411
|
addDisposer(self, autorun(() => {
|
|
2039
|
-
|
|
2040
|
-
|
|
3412
|
+
if (cladesSeeded ||
|
|
3413
|
+
!self.dataInitialized ||
|
|
3414
|
+
self.clades.length === 0) {
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
cladesSeeded = true;
|
|
3418
|
+
for (const { mark, nodeId } of self.resolvedClades) {
|
|
3419
|
+
if (nodeId === undefined) {
|
|
3420
|
+
continue;
|
|
3421
|
+
}
|
|
3422
|
+
if (mark === 'collapse' && !self.collapsed.includes(nodeId)) {
|
|
3423
|
+
self.toggleCollapsed(nodeId);
|
|
3424
|
+
}
|
|
3425
|
+
else if (mark === 'focus') {
|
|
3426
|
+
self.setShowOnly(nodeId);
|
|
3427
|
+
}
|
|
2041
3428
|
}
|
|
2042
3429
|
}));
|
|
2043
|
-
//
|
|
2044
|
-
//
|
|
2045
|
-
// matchMedia query is pinned to the current ratio, so each change
|
|
2046
|
-
// re-registers against the new one to keep tracking further moves.
|
|
3430
|
+
// the matchMedia query is pinned to the current device pixel ratio, so
|
|
3431
|
+
// each change re-registers against the new one
|
|
2047
3432
|
if (typeof window !== 'undefined' &&
|
|
2048
3433
|
typeof window.matchMedia === 'function') {
|
|
2049
3434
|
const query = () => window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
|
|
@@ -2063,32 +3448,41 @@ function stateModelFactory() {
|
|
|
2063
3448
|
* Fetch a filehandle whenever it changes, and hand the text to
|
|
2064
3449
|
* `onLoad`.
|
|
2065
3450
|
*
|
|
2066
|
-
*
|
|
2067
|
-
*
|
|
2068
|
-
*
|
|
2069
|
-
*
|
|
3451
|
+
* A generation guard keeps a slower superseded request from overwriting
|
|
3452
|
+
* the newer one's data, status or loading flag. A superseded or cleared
|
|
3453
|
+
* request is aborted and clears its status line.
|
|
3454
|
+
*
|
|
3455
|
+
* `clearFilehandle` runs after a local file loads, since a blob has no
|
|
3456
|
+
* URL to refetch, and when the user cancels, which returns the view to
|
|
3457
|
+
* the import form.
|
|
2070
3458
|
*
|
|
2071
|
-
* `
|
|
2072
|
-
*
|
|
2073
|
-
*
|
|
2074
|
-
* it too, returning the view to the import form rather than leaving a
|
|
2075
|
-
* stuck spinner.
|
|
3459
|
+
* `what` names the layer in a failure message. An `optional` layer
|
|
3460
|
+
* (annotations, row metadata) that fails adds a warning instead of an
|
|
3461
|
+
* error.
|
|
2076
3462
|
*/
|
|
2077
|
-
const loadOnFilehandleChange = ({ getFilehandle, onLoad, setLoading, clearFilehandle, }) => {
|
|
3463
|
+
const loadOnFilehandleChange = ({ what, getFilehandle, onLoad, setLoading, clearFilehandle, optional = false, }) => {
|
|
2078
3464
|
let generation = 0;
|
|
3465
|
+
let cancelInFlight;
|
|
3466
|
+
addDisposer(self, () => {
|
|
3467
|
+
cancelInFlight?.();
|
|
3468
|
+
});
|
|
2079
3469
|
addDisposer(self, autorun(async () => {
|
|
2080
3470
|
const filehandle = getFilehandle();
|
|
2081
|
-
// a cleared filehandle bumps the generation too, so a reset()
|
|
2082
|
-
// mid-download invalidates the in-flight fetch instead of letting
|
|
2083
|
-
// its onLoad land on the emptied model. That also orphans the
|
|
2084
|
-
// invalidated run's `finally`, so the loading flag is cleared
|
|
2085
|
-
// here on its behalf
|
|
2086
3471
|
const current = ++generation;
|
|
3472
|
+
cancelInFlight?.();
|
|
3473
|
+
cancelInFlight = undefined;
|
|
2087
3474
|
if (!filehandle) {
|
|
2088
3475
|
setLoading?.(false);
|
|
2089
3476
|
return;
|
|
2090
3477
|
}
|
|
2091
|
-
const isCurrent = () => current === generation;
|
|
3478
|
+
const isCurrent = () => current === generation && isAlive(self);
|
|
3479
|
+
const controller = new AbortController();
|
|
3480
|
+
cancelInFlight = () => {
|
|
3481
|
+
controller.abort();
|
|
3482
|
+
if (isAlive(self)) {
|
|
3483
|
+
self.setStatus(undefined);
|
|
3484
|
+
}
|
|
3485
|
+
};
|
|
2092
3486
|
try {
|
|
2093
3487
|
setLoading?.(true);
|
|
2094
3488
|
self.setError(undefined);
|
|
@@ -2096,7 +3490,7 @@ function stateModelFactory() {
|
|
|
2096
3490
|
if (isCurrent()) {
|
|
2097
3491
|
self.setStatus(status);
|
|
2098
3492
|
}
|
|
2099
|
-
});
|
|
3493
|
+
}, { controller });
|
|
2100
3494
|
if (isCurrent()) {
|
|
2101
3495
|
transaction(() => {
|
|
2102
3496
|
onLoad(text);
|
|
@@ -2113,18 +3507,25 @@ function stateModelFactory() {
|
|
|
2113
3507
|
}
|
|
2114
3508
|
else {
|
|
2115
3509
|
console.error(e);
|
|
2116
|
-
|
|
3510
|
+
if (optional) {
|
|
3511
|
+
self.addWarning(`The ${what} did not load: ${e}`);
|
|
3512
|
+
}
|
|
3513
|
+
else {
|
|
3514
|
+
self.setError(e);
|
|
3515
|
+
}
|
|
2117
3516
|
}
|
|
2118
3517
|
}
|
|
2119
3518
|
}
|
|
2120
3519
|
finally {
|
|
2121
3520
|
if (isCurrent()) {
|
|
3521
|
+
cancelInFlight = undefined;
|
|
2122
3522
|
setLoading?.(false);
|
|
2123
3523
|
}
|
|
2124
3524
|
}
|
|
2125
3525
|
}));
|
|
2126
3526
|
};
|
|
2127
3527
|
loadOnFilehandleChange({
|
|
3528
|
+
what: 'tree',
|
|
2128
3529
|
getFilehandle: () => self.treeFilehandle,
|
|
2129
3530
|
onLoad: text => {
|
|
2130
3531
|
self.setTree(text);
|
|
@@ -2136,38 +3537,49 @@ function stateModelFactory() {
|
|
|
2136
3537
|
self.setTreeFilehandle(undefined);
|
|
2137
3538
|
},
|
|
2138
3539
|
});
|
|
2139
|
-
// treeMetadata
|
|
2140
|
-
// it keeps no loading flag and nothing to return to on cancel
|
|
3540
|
+
// treeMetadata has no import-form step, so no loading flag or cancel
|
|
2141
3541
|
loadOnFilehandleChange({
|
|
3542
|
+
what: 'row metadata',
|
|
3543
|
+
optional: true,
|
|
2142
3544
|
getFilehandle: () => self.treeMetadataFilehandle,
|
|
2143
3545
|
onLoad: text => {
|
|
2144
3546
|
self.setTreeMetadata(text);
|
|
2145
3547
|
},
|
|
2146
3548
|
});
|
|
2147
|
-
//
|
|
3549
|
+
// parses data.gff into annotations. Clearing the text clears only the
|
|
3550
|
+
// annotations this autorun applied, not ones a host set directly
|
|
3551
|
+
let appliedGFF = false;
|
|
2148
3552
|
addDisposer(self, autorun(() => {
|
|
2149
3553
|
const gffText = self.data.gff;
|
|
2150
3554
|
if (gffText) {
|
|
2151
3555
|
try {
|
|
2152
3556
|
self.applyGFFText(gffText);
|
|
3557
|
+
appliedGFF = true;
|
|
2153
3558
|
}
|
|
2154
3559
|
catch (e) {
|
|
2155
3560
|
console.error(e);
|
|
2156
|
-
self.
|
|
3561
|
+
self.addWarning(`The annotations did not parse: ${e}`);
|
|
2157
3562
|
}
|
|
2158
3563
|
}
|
|
3564
|
+
else if (appliedGFF) {
|
|
3565
|
+
appliedGFF = false;
|
|
3566
|
+
self.setAnnotations([]);
|
|
3567
|
+
}
|
|
2159
3568
|
}));
|
|
2160
|
-
// gffFilehandle
|
|
3569
|
+
// gffFilehandle loads into data.gff, which the autorun above parses
|
|
2161
3570
|
loadOnFilehandleChange({
|
|
3571
|
+
what: 'annotations',
|
|
3572
|
+
optional: true,
|
|
2162
3573
|
getFilehandle: () => self.gffFilehandle,
|
|
2163
3574
|
onLoad: text => {
|
|
2164
|
-
self.
|
|
3575
|
+
self.setGFF(text);
|
|
2165
3576
|
},
|
|
2166
3577
|
clearFilehandle: () => {
|
|
2167
3578
|
self.setGFFFilehandle(undefined);
|
|
2168
3579
|
},
|
|
2169
3580
|
});
|
|
2170
3581
|
loadOnFilehandleChange({
|
|
3582
|
+
what: 'alignment',
|
|
2171
3583
|
getFilehandle: () => self.msaFilehandle,
|
|
2172
3584
|
onLoad: text => {
|
|
2173
3585
|
self.setMSA(text);
|
|
@@ -2179,19 +3591,12 @@ function stateModelFactory() {
|
|
|
2179
3591
|
self.setMSAFilehandle(undefined);
|
|
2180
3592
|
},
|
|
2181
3593
|
});
|
|
2182
|
-
// Keep
|
|
2183
|
-
// self.
|
|
2184
|
-
//
|
|
2185
|
-
//
|
|
3594
|
+
// Keep computeds alive that are read outside reactions, so they are not
|
|
3595
|
+
// recomputed on every access: self.columns (and through it the
|
|
3596
|
+
// parseMSA result), and the column statistics that dynamic color
|
|
3597
|
+
// schemes and the mousemove-driven hover tooltip read.
|
|
2186
3598
|
// xref solution https://github.com/mobxjs/mobx/issues/266#issuecomment-222007278
|
|
2187
3599
|
// xref problem https://github.com/GMOD/react-msaview/issues/75
|
|
2188
|
-
//
|
|
2189
|
-
// The column statistics are held for the same reason whenever something
|
|
2190
|
-
// reads them off the reactive path: dynamic color schemes, and the hover
|
|
2191
|
-
// tooltip, which is read from a mousemove handler. Without this the
|
|
2192
|
-
// tooltip's cost depends on whether some visible track happens to be
|
|
2193
|
-
// observing them, so closing the conservation track would silently turn
|
|
2194
|
-
// every mouse move into a full-alignment recount.
|
|
2195
3600
|
addDisposer(self, autorun(() => {
|
|
2196
3601
|
if (self.colorSchemeName.includes('dynamic')) {
|
|
2197
3602
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
@@ -2209,27 +3614,39 @@ function stateModelFactory() {
|
|
|
2209
3614
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
2210
3615
|
self.columns;
|
|
2211
3616
|
}));
|
|
2212
|
-
//
|
|
2213
|
-
//
|
|
2214
|
-
//
|
|
2215
|
-
// below (treeAreaWidth here depends only on labelsWidth, not treeWidth).
|
|
3617
|
+
// with autoTreeAreaWidth and no tree drawn, size the tree area to the
|
|
3618
|
+
// labels. Gated on noTree/!drawTree so it does not loop with the
|
|
3619
|
+
// treeWidth autorun below
|
|
2216
3620
|
addDisposer(self, autorun(() => {
|
|
2217
3621
|
if (self.autoTreeAreaWidth &&
|
|
2218
3622
|
(self.noTree || !self.drawTree) &&
|
|
2219
3623
|
self.labelsWidth) {
|
|
2220
|
-
self.setTreeAreaWidth(self.labelsWidth + self.marginLeft + 12);
|
|
3624
|
+
self.setTreeAreaWidth(self.labelsWidth + self.marginLeft + 12 + self.cladeGutterWidth);
|
|
2221
3625
|
}
|
|
2222
3626
|
}));
|
|
2223
|
-
//
|
|
3627
|
+
// treeWidth follows the tree area less the labels. A non-default
|
|
3628
|
+
// snapshot treeWidth (jbrowse-plugin-msaview opens at 100 in a 200px
|
|
3629
|
+
// area) is kept until the tree area changes. Not a getter, because
|
|
3630
|
+
// labelsWidth is measured off leaves laid out against treeWidth.
|
|
3631
|
+
let pinnedAreaWidth = self.treeWidth === defaultTreeWidth ? undefined : self.treeAreaWidth;
|
|
2224
3632
|
addDisposer(self, autorun(() => {
|
|
2225
|
-
|
|
3633
|
+
const areaWidth = self.treeAreaWidth;
|
|
3634
|
+
const labelsWidth = self.labelsWidth;
|
|
3635
|
+
if (pinnedAreaWidth === areaWidth) {
|
|
3636
|
+
return;
|
|
3637
|
+
}
|
|
3638
|
+
pinnedAreaWidth = undefined;
|
|
3639
|
+
self.setTreeWidth(Math.max(50, areaWidth -
|
|
3640
|
+
labelsWidth -
|
|
3641
|
+
10 -
|
|
3642
|
+
self.marginLeft -
|
|
3643
|
+
self.cladeGutterWidth));
|
|
2226
3644
|
}));
|
|
2227
3645
|
},
|
|
2228
3646
|
}))
|
|
2229
3647
|
.postProcessSnapshot(({ data, columnTracks, ...rest }) => ({
|
|
2230
|
-
// per-property defaults
|
|
2231
|
-
//
|
|
2232
|
-
// when a sibling filehandle can refetch them, keeping sessions/URLs small
|
|
3648
|
+
// stripDefault handles per-property defaults; this drops inline documents
|
|
3649
|
+
// whose filehandle can refetch them
|
|
2233
3650
|
...rest,
|
|
2234
3651
|
...smallColumnTracks(columnTracks),
|
|
2235
3652
|
data: {
|