react-msaview 9.0.0 → 9.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -10
- package/bundle/index.js +111 -115
- package/bundle/index.js.map +4 -4
- package/dist/blosum62.d.ts +3 -0
- package/dist/blosum62.js +43 -0
- package/dist/blosum62.js.map +1 -0
- package/dist/colorSchemes.js +2 -3
- package/dist/colorSchemes.js.map +1 -1
- package/dist/columnCounts.d.ts +0 -1
- package/dist/columnCounts.js +1 -1
- package/dist/columnCounts.js.map +1 -1
- package/dist/components/Loading.d.ts +3 -2
- package/dist/components/Loading.js +7 -7
- package/dist/components/Loading.js.map +1 -1
- package/dist/components/MSAView.d.ts +3 -2
- package/dist/components/MSAView.js +7 -4
- package/dist/components/MSAView.js.map +1 -1
- package/dist/components/MSAViewer.d.ts +1941 -4
- package/dist/components/MSAViewer.js +144 -201
- package/dist/components/MSAViewer.js.map +1 -1
- package/dist/components/MetadataTable.d.ts +4 -1
- package/dist/components/MetadataTable.js +55 -16
- package/dist/components/MetadataTable.js.map +1 -1
- package/dist/components/ResizeHandles.js +3 -2
- package/dist/components/ResizeHandles.js.map +1 -1
- package/dist/components/Track.js +8 -1
- package/dist/components/Track.js.map +1 -1
- package/dist/components/controls.d.ts +11 -0
- package/dist/components/controls.js +49 -0
- package/dist/components/controls.js.map +1 -0
- package/dist/components/dialogs/AnnotationFileDialog.js +12 -18
- package/dist/components/dialogs/AnnotationFileDialog.js.map +1 -1
- package/dist/components/dialogs/ExportSVGDialog.js +5 -7
- package/dist/components/dialogs/ExportSVGDialog.js.map +1 -1
- package/dist/components/dialogs/FeatureDialog.js +2 -2
- package/dist/components/dialogs/FeatureDialog.js.map +1 -1
- package/dist/components/{header/TreeSettingsMenu.d.ts → dialogs/GapThresholdDialog.d.ts} +3 -2
- package/dist/components/dialogs/GapThresholdDialog.js +15 -0
- package/dist/components/dialogs/GapThresholdDialog.js.map +1 -0
- package/dist/components/{header → dialogs}/GappynessSlider.d.ts +1 -1
- package/dist/components/{header → dialogs}/GappynessSlider.js +3 -10
- package/dist/components/dialogs/GappynessSlider.js.map +1 -0
- package/dist/components/{header → dialogs}/GoToBox.d.ts +4 -2
- package/dist/components/dialogs/GoToBox.js +48 -0
- package/dist/components/dialogs/GoToBox.js.map +1 -0
- package/dist/components/dialogs/GoToDialog.d.ts +6 -0
- package/dist/components/dialogs/GoToDialog.js +10 -0
- package/dist/components/dialogs/GoToDialog.js.map +1 -0
- package/dist/components/dialogs/HelpDialog.d.ts +6 -0
- package/dist/components/dialogs/HelpDialog.js +30 -0
- package/dist/components/dialogs/HelpDialog.js.map +1 -0
- package/dist/components/dialogs/MetadataDialog.js +11 -3
- package/dist/components/dialogs/MetadataDialog.js.map +1 -1
- package/dist/components/dialogs/goTo.d.ts +5 -0
- package/dist/components/{header → dialogs}/goTo.js +1 -1
- package/dist/components/dialogs/goTo.js.map +1 -0
- package/dist/components/dialogs/metadataLabels.d.ts +4 -0
- package/dist/components/dialogs/metadataLabels.js +52 -0
- package/dist/components/dialogs/metadataLabels.js.map +1 -0
- package/dist/components/dialogs/metadataRecord.d.ts +1 -0
- package/dist/components/dialogs/metadataRecord.js +38 -0
- package/dist/components/dialogs/metadataRecord.js.map +1 -0
- package/dist/components/header/Header.d.ts +6 -2
- package/dist/components/header/Header.js +30 -34
- package/dist/components/header/Header.js.map +1 -1
- package/dist/components/header/HiddenColumnsChip.d.ts +6 -0
- package/dist/components/header/HiddenColumnsChip.js +15 -0
- package/dist/components/header/HiddenColumnsChip.js.map +1 -0
- package/dist/components/header/{FileMenu.d.ts → MenuBar.d.ts} +3 -3
- package/dist/components/header/MenuBar.js +33 -0
- package/dist/components/header/MenuBar.js.map +1 -0
- package/dist/components/header/ScrollZoomCheckbox.d.ts +7 -0
- package/dist/components/header/ScrollZoomCheckbox.js +19 -0
- package/dist/components/header/ScrollZoomCheckbox.js.map +1 -0
- package/dist/components/header/ZoomControls.js +12 -48
- package/dist/components/header/ZoomControls.js.map +1 -1
- package/dist/components/header/colorSchemeMenuItems.d.ts +4 -0
- package/dist/components/header/colorSchemeMenuItems.js +106 -0
- package/dist/components/header/colorSchemeMenuItems.js.map +1 -0
- package/dist/components/header/getAnnotationsMenu.d.ts +3 -30
- package/dist/components/header/getAnnotationsMenu.js +48 -54
- package/dist/components/header/getAnnotationsMenu.js.map +1 -1
- package/dist/components/header/menuBarItems.d.ts +11 -0
- package/dist/components/header/menuBarItems.js +277 -0
- package/dist/components/header/menuBarItems.js.map +1 -0
- package/dist/components/import/ImportForm.js +12 -6
- package/dist/components/import/ImportForm.js.map +1 -1
- package/dist/components/import/data/seq2.d.ts +0 -1
- package/dist/components/import/data/seq2.js +0 -17
- package/dist/components/import/data/seq2.js.map +1 -1
- package/dist/components/minimap/Minimap.js +7 -3
- package/dist/components/minimap/Minimap.js.map +1 -1
- package/dist/components/minimap/MinimapSVG.js +2 -1
- package/dist/components/minimap/MinimapSVG.js.map +1 -1
- package/dist/components/minimap/minimapLayout.d.ts +12 -0
- package/dist/components/minimap/minimapLayout.js +20 -0
- package/dist/components/minimap/minimapLayout.js.map +1 -1
- package/dist/components/msa/MSACanvas.js +28 -10
- package/dist/components/msa/MSACanvas.js.map +1 -1
- package/dist/components/msa/MSACanvasBlock.js +17 -16
- package/dist/components/msa/MSACanvasBlock.js.map +1 -1
- package/dist/components/msa/MSAMouseoverCanvas.js +2 -2
- package/dist/components/msa/MSAMouseoverCanvas.js.map +1 -1
- package/dist/components/msa/WheelZoomHint.d.ts +7 -0
- package/dist/components/msa/WheelZoomHint.js +38 -0
- package/dist/components/msa/WheelZoomHint.js.map +1 -0
- package/dist/components/msa/drawFeatureSpans.d.ts +50 -11
- package/dist/components/msa/drawFeatureSpans.js +99 -56
- package/dist/components/msa/drawFeatureSpans.js.map +1 -1
- package/dist/components/msa/msaKeys.js +3 -0
- package/dist/components/msa/msaKeys.js.map +1 -1
- package/dist/components/msa/msaRaster.d.ts +14 -2
- package/dist/components/msa/msaRaster.js +20 -10
- package/dist/components/msa/msaRaster.js.map +1 -1
- package/dist/components/msa/renderBoxFeatureCanvasBlock.d.ts +9 -0
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js +39 -8
- package/dist/components/msa/renderBoxFeatureCanvasBlock.js.map +1 -1
- package/dist/components/msa/renderHighlights.d.ts +2 -5
- package/dist/components/msa/renderHighlights.js +5 -6
- package/dist/components/msa/renderHighlights.js.map +1 -1
- package/dist/components/msa/renderMSABlock.d.ts +6 -1
- package/dist/components/msa/renderMSABlock.js +44 -17
- package/dist/components/msa/renderMSABlock.js.map +1 -1
- package/dist/components/msa/renderMSAMouseover.js +2 -1
- package/dist/components/msa/renderMSAMouseover.js.map +1 -1
- package/dist/components/msa/tileColor.js +7 -0
- package/dist/components/msa/tileColor.js.map +1 -1
- package/dist/components/renderCtx.d.ts +1 -1
- package/dist/components/rowpanels/RowPanelBlock.js +2 -2
- package/dist/components/rowpanels/RowPanelBlock.js.map +1 -1
- package/dist/components/rowpanels/renderFeaturePanel.d.ts +5 -1
- package/dist/components/rowpanels/renderFeaturePanel.js +12 -5
- package/dist/components/rowpanels/renderFeaturePanel.js.map +1 -1
- package/dist/components/settings/MenuItemsForm.d.ts +10 -0
- package/dist/components/settings/MenuItemsForm.js +80 -0
- package/dist/components/settings/MenuItemsForm.js.map +1 -0
- package/dist/components/settings/SettingsPanel.d.ts +12 -0
- package/dist/components/settings/SettingsPanel.js +92 -0
- package/dist/components/settings/SettingsPanel.js.map +1 -0
- package/dist/components/{header/ZoomMenu.d.ts → settings/TrackList.d.ts} +2 -2
- package/dist/components/settings/TrackList.js +55 -0
- package/dist/components/settings/TrackList.js.map +1 -0
- package/dist/components/tracks/TrackBlocks.js +2 -2
- package/dist/components/tracks/TrackBlocks.js.map +1 -1
- package/dist/components/tracks/drawTracks.d.ts +1 -53
- package/dist/components/tracks/drawTracks.js +5 -5
- package/dist/components/tracks/drawTracks.js.map +1 -1
- package/dist/components/tree/CladeLabels.d.ts +1 -1
- package/dist/components/tree/CladeLabels.js +22 -34
- package/dist/components/tree/CladeLabels.js.map +1 -1
- package/dist/components/tree/TreeCanvasBlock.js +2 -2
- package/dist/components/tree/TreeCanvasBlock.js.map +1 -1
- package/dist/components/tree/TreeMenu.js +4 -6
- package/dist/components/tree/TreeMenu.js.map +1 -1
- package/dist/components/tree/TreeOverview.js +2 -2
- package/dist/components/tree/TreeOverview.js.map +1 -1
- package/dist/components/tree/cladeBrackets.d.ts +17 -10
- package/dist/components/tree/cladeBrackets.js +33 -3
- package/dist/components/tree/cladeBrackets.js.map +1 -1
- package/dist/components/tree/renderTreeCanvas.js +2 -8
- package/dist/components/tree/renderTreeCanvas.js.map +1 -1
- package/dist/components/tree/renderTreeOverview.js +4 -11
- package/dist/components/tree/renderTreeOverview.js.map +1 -1
- package/dist/constants.d.ts +4 -1
- package/dist/constants.js +7 -1
- package/dist/constants.js.map +1 -1
- package/dist/darkReader.d.ts +7 -0
- package/dist/darkReader.js +42 -0
- package/dist/darkReader.js.map +1 -0
- package/dist/element.js +25 -53
- package/dist/element.js.map +1 -1
- package/dist/exportLayout.d.ts +39 -0
- package/dist/exportLayout.js +78 -0
- package/dist/exportLayout.js.map +1 -0
- package/dist/featurePanels.d.ts +37 -0
- package/dist/featurePanels.js +187 -0
- package/dist/featurePanels.js.map +1 -0
- package/dist/filehandleLoader.d.ts +41 -0
- package/dist/filehandleLoader.js +99 -0
- package/dist/filehandleLoader.js.map +1 -0
- package/dist/headlessRenderEnv.d.ts +12 -39
- package/dist/headlessRenderEnv.js +18 -12
- package/dist/headlessRenderEnv.js.map +1 -1
- package/dist/hierarchy.d.ts +5 -8
- package/dist/hierarchy.js +5 -79
- package/dist/hierarchy.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/measureTextCanvas.d.ts +7 -0
- package/dist/measureTextCanvas.js +8 -6
- package/dist/measureTextCanvas.js.map +1 -1
- package/dist/model/treeModel.d.ts +6 -2
- package/dist/model/treeModel.js +8 -4
- package/dist/model/treeModel.js.map +1 -1
- package/dist/model.d.ts +180 -76
- package/dist/model.js +358 -618
- package/dist/model.js.map +1 -1
- package/dist/neighborJoining.js +1 -40
- package/dist/neighborJoining.js.map +1 -1
- package/dist/renderToSvg.d.ts +2 -7
- package/dist/renderToSvg.js +33 -133
- package/dist/renderToSvg.js.map +1 -1
- package/dist/rerootTree.d.ts +1 -0
- package/dist/rerootTree.js +7 -6
- package/dist/rerootTree.js.map +1 -1
- package/dist/schemeResidueColor.d.ts +12 -0
- package/dist/schemeResidueColor.js +33 -0
- package/dist/schemeResidueColor.js.map +1 -0
- package/dist/similarityColors.d.ts +11 -0
- package/dist/similarityColors.js +90 -0
- package/dist/similarityColors.js.map +1 -0
- package/dist/svgTestUtil.d.ts +74 -32
- package/dist/treeDisplay.d.ts +37 -0
- package/dist/treeDisplay.js +209 -0
- package/dist/treeDisplay.js.map +1 -0
- package/dist/types.d.ts +6 -3
- package/dist/useWheelScroll.d.ts +3 -1
- package/dist/useWheelScroll.js +36 -17
- package/dist/useWheelScroll.js.map +1 -1
- package/dist/util.d.ts +2 -0
- package/dist/util.js +2 -0
- package/dist/util.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/viewerSnapshot.d.ts +169 -0
- package/dist/viewerSnapshot.js +100 -0
- package/dist/viewerSnapshot.js.map +1 -0
- package/dist/wheelZoom.d.ts +8 -3
- package/dist/wheelZoom.js +44 -32
- package/dist/wheelZoom.js.map +1 -1
- package/dist/zoomSnapshot.d.ts +29 -0
- package/dist/zoomSnapshot.js +47 -0
- package/dist/zoomSnapshot.js.map +1 -0
- package/package.json +11 -11
- package/src/blosum62.ts +45 -0
- package/src/colorSchemes.ts +2 -4
- package/src/columnCounts.ts +1 -1
- package/src/components/Loading.tsx +25 -14
- package/src/components/MSAView.tsx +22 -5
- package/src/components/MSAViewer.tsx +275 -251
- package/src/components/MetadataTable.tsx +83 -26
- package/src/components/ResizeHandles.tsx +9 -2
- package/src/components/Track.tsx +15 -1
- package/src/components/controls.ts +62 -0
- package/src/components/dialogs/AnnotationFileDialog.tsx +16 -21
- package/src/components/dialogs/ExportSVGDialog.tsx +8 -19
- package/src/components/dialogs/FeatureDialog.tsx +2 -5
- package/src/components/dialogs/GapThresholdDialog.tsx +36 -0
- package/src/components/{header → dialogs}/GappynessSlider.tsx +3 -10
- package/src/components/{header → dialogs}/GoToBox.tsx +36 -10
- package/src/components/dialogs/GoToDialog.tsx +30 -0
- package/src/components/dialogs/HelpDialog.tsx +79 -0
- package/src/components/dialogs/MetadataDialog.tsx +32 -4
- package/src/components/{header → dialogs}/goTo.ts +1 -1
- package/src/components/dialogs/metadataLabels.ts +54 -0
- package/src/components/dialogs/metadataRecord.tsx +61 -0
- package/src/components/header/Header.tsx +75 -44
- package/src/components/header/HiddenColumnsChip.tsx +39 -0
- package/src/components/header/MenuBar.tsx +64 -0
- package/src/components/header/ScrollZoomCheckbox.tsx +46 -0
- package/src/components/header/ZoomControls.tsx +23 -74
- package/src/components/header/colorSchemeMenuItems.ts +121 -0
- package/src/components/header/getAnnotationsMenu.ts +52 -56
- package/src/components/header/menuBarItems.ts +357 -0
- package/src/components/import/ImportForm.tsx +19 -7
- package/src/components/import/data/seq2.ts +0 -18
- package/src/components/minimap/Minimap.tsx +11 -3
- package/src/components/minimap/MinimapSVG.tsx +17 -1
- package/src/components/minimap/minimapLayout.ts +27 -0
- package/src/components/msa/MSACanvas.tsx +24 -4
- package/src/components/msa/MSACanvasBlock.tsx +20 -17
- package/src/components/msa/MSAMouseoverCanvas.tsx +2 -2
- package/src/components/msa/WheelZoomHint.tsx +56 -0
- package/src/components/msa/drawFeatureSpans.ts +160 -79
- package/src/components/msa/msaKeys.ts +3 -0
- package/src/components/msa/msaRaster.ts +26 -10
- package/src/components/msa/renderBoxFeatureCanvasBlock.ts +62 -24
- package/src/components/msa/renderHighlights.ts +5 -6
- package/src/components/msa/renderMSABlock.ts +51 -15
- package/src/components/msa/renderMSAMouseover.ts +5 -1
- package/src/components/msa/tileColor.ts +7 -0
- package/src/components/renderCtx.ts +0 -1
- package/src/components/rowpanels/RowPanelBlock.tsx +2 -2
- package/src/components/rowpanels/renderFeaturePanel.ts +26 -7
- package/src/components/settings/MenuItemsForm.tsx +225 -0
- package/src/components/settings/SettingsPanel.tsx +156 -0
- package/src/components/settings/TrackList.tsx +119 -0
- package/src/components/tracks/TrackBlocks.tsx +2 -2
- package/src/components/tracks/drawTracks.ts +5 -5
- package/src/components/tree/CladeLabels.tsx +30 -54
- package/src/components/tree/TreeCanvasBlock.tsx +2 -2
- package/src/components/tree/TreeMenu.tsx +4 -6
- package/src/components/tree/TreeOverview.tsx +2 -2
- package/src/components/tree/cladeBrackets.ts +50 -3
- package/src/components/tree/renderTreeCanvas.ts +2 -13
- package/src/components/tree/renderTreeOverview.ts +7 -14
- package/src/constants.ts +9 -1
- package/src/darkReader.ts +57 -0
- package/src/element.ts +37 -64
- package/src/exportLayout.ts +134 -0
- package/src/featurePanels.ts +269 -0
- package/src/filehandleLoader.ts +136 -0
- package/src/headlessRenderEnv.ts +24 -11
- package/src/hierarchy.ts +5 -88
- package/src/index.ts +9 -5
- package/src/measureTextCanvas.ts +8 -6
- package/src/model/treeModel.ts +9 -3
- package/src/model.ts +452 -807
- package/src/neighborJoining.ts +1 -42
- package/src/renderToSvg.tsx +56 -207
- package/src/rerootTree.ts +7 -6
- package/src/schemeResidueColor.ts +37 -0
- package/src/similarityColors.ts +112 -0
- package/src/treeDisplay.ts +278 -0
- package/src/types.ts +6 -4
- package/src/useWheelScroll.ts +43 -22
- package/src/util.ts +3 -0
- package/src/version.ts +1 -1
- package/src/viewerSnapshot.ts +147 -0
- package/src/wheelZoom.ts +55 -33
- package/src/zoomSnapshot.ts +80 -0
- package/dist/components/header/ColorSchemeMenu.d.ts +0 -12
- package/dist/components/header/ColorSchemeMenu.js +0 -37
- package/dist/components/header/ColorSchemeMenu.js.map +0 -1
- package/dist/components/header/FileMenu.js +0 -66
- package/dist/components/header/FileMenu.js.map +0 -1
- package/dist/components/header/GappynessSlider.js.map +0 -1
- package/dist/components/header/GoToBox.js +0 -31
- package/dist/components/header/GoToBox.js.map +0 -1
- package/dist/components/header/MSASettingsMenu.d.ts +0 -6
- package/dist/components/header/MSASettingsMenu.js +0 -11
- package/dist/components/header/MSASettingsMenu.js.map +0 -1
- package/dist/components/header/ScrollZoomIcons.d.ts +0 -6
- package/dist/components/header/ScrollZoomIcons.js +0 -35
- package/dist/components/header/ScrollZoomIcons.js.map +0 -1
- package/dist/components/header/TreeSettingsMenu.js +0 -42
- package/dist/components/header/TreeSettingsMenu.js.map +0 -1
- package/dist/components/header/ZoomMenu.js +0 -37
- package/dist/components/header/ZoomMenu.js.map +0 -1
- package/dist/components/header/goTo.d.ts +0 -20
- package/dist/components/header/goTo.js.map +0 -1
- package/dist/components/header/settingsMenuItems.d.ts +0 -4
- package/dist/components/header/settingsMenuItems.js +0 -127
- package/dist/components/header/settingsMenuItems.js.map +0 -1
- package/src/components/header/ColorSchemeMenu.tsx +0 -56
- package/src/components/header/FileMenu.tsx +0 -81
- package/src/components/header/MSASettingsMenu.tsx +0 -26
- package/src/components/header/ScrollZoomIcons.tsx +0 -52
- package/src/components/header/TreeSettingsMenu.tsx +0 -60
- package/src/components/header/ZoomMenu.tsx +0 -48
- package/src/components/header/settingsMenuItems.ts +0 -157
package/dist/model.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { clamp, groupBy, notEmpty, sum } from '@jbrowse/core/util';
|
|
2
|
-
import { openLocation } from '@jbrowse/core/util/io';
|
|
3
2
|
import { ElementId, FileLocation } from '@jbrowse/core/util/types/mst';
|
|
4
|
-
import { addDisposer, applySnapshot, cast, getSnapshot,
|
|
3
|
+
import { addDisposer, applySnapshot, cast, getSnapshot, types, } from '@jbrowse/mobx-state-tree';
|
|
5
4
|
import { autorun, compareStructural, computed, reaction, transaction, } from 'mobx';
|
|
6
|
-
import { generateNodeIds, gffToAnnotations,
|
|
5
|
+
import { generateNodeIds, gffToAnnotations, parseGFF, parseMSA, } from 'msa-parsers';
|
|
7
6
|
import { blockFasta } from './blockFasta.js';
|
|
8
7
|
import { calculateBlocks } from './calculateBlocks.js';
|
|
9
8
|
import { clustalXColumnColors } from './clustalX.js';
|
|
@@ -12,41 +11,36 @@ import { columnCountsFromRows, letterOfResidueSlot } from './columnCounts.js';
|
|
|
12
11
|
import { columnStats } from './columnStats.js';
|
|
13
12
|
import { packDomainLanes } from './components/msa/packDomainLanes.js';
|
|
14
13
|
import { visibleColRange } from './components/msa/visibleColRange.js';
|
|
15
|
-
import TrackBlocks from './components/tracks/TrackBlocks.js';
|
|
16
14
|
import { cladeGutterWidth } from './components/tree/cladeBrackets.js';
|
|
17
15
|
import { scaleBarLength } from './components/tree/scaleBar.js';
|
|
18
|
-
import {
|
|
16
|
+
import { defaultAllowedGappyness, defaultColWidth, defaultColorSchemeName, defaultCurrentAlignment, defaultDrawMsaLetters, defaultHeight, defaultHideGaps, defaultRowHeight, defaultScrollX, defaultScrollY, defaultScrollZoom, defaultScrollZoomAxis, defaultShowDomainLegend, defaultShowDomains, defaultSubFeatureRows, fontSizeFor, labelFitFontSize, maxCellSize, maxFitTreeWidth, maxTreeAreaShare, maxInlineSnapshotBytes as defaultMaxInlineSnapshotBytes, maxNeighborJoiningRows, minColWidth, minLetterColWidth, minLetterRowHeight, minRowHeight, minTreeWidth, pxPerTreeDepth, rowPanelHeaderHeight, rowTintAlpha, scrollZoomAxes, segmentFeatureTypes, segmentShades, settingsPanelWidth, treeScaleBarHeight, } from './constants.js';
|
|
19
17
|
import { createPaletteMap } from './createPaletteMap.js';
|
|
20
18
|
import { exportFileName } from './exportFileName.js';
|
|
21
|
-
import {
|
|
19
|
+
import { featureName } from './featureFields.js';
|
|
20
|
+
import { colorByRow, featureColorMap, featureLabelMap, featurePanelSpans, resolveFeatureScale, rowPanelWidth, stringFields, } from './featurePanels.js';
|
|
22
21
|
import { featuresToAnnotations } from './featuresLayer.js';
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { calcDepthToLeaf, clusterLayout, collapse, find, forEachDescendant, hierarchy, layoutTree, leafIndex, leaves, maxLength, mrca, nodeCoveringRows, setBrLength, sort, sum as hierarchySum, } from './hierarchy.js';
|
|
22
|
+
import { installFilehandleLoader } from './filehandleLoader.js';
|
|
23
|
+
import { find, hierarchy, layoutTree, leaves, nodeCoveringRows, } from './hierarchy.js';
|
|
26
24
|
import { DataModelF } from './model/DataModel.js';
|
|
27
25
|
import { DialogQueueSessionMixin } from './model/DialogQueue.js';
|
|
28
26
|
import { MSAModelF } from './model/msaModel.js';
|
|
29
27
|
import { TreeModelF } from './model/treeModel.js';
|
|
30
28
|
import { calculateNeighborJoiningTree } from './neighborJoining.js';
|
|
31
|
-
import { parseAsn1 } from './parseAsn1.js';
|
|
32
29
|
import { calculatePropertyConservation } from './propertyConservation.js';
|
|
33
|
-
import {
|
|
30
|
+
import { preorder } from './rerootTree.js';
|
|
34
31
|
import { globalColToVisibleCol, seqPosOfGlobalCol, visibleColToGlobalCol, visibleColsBefore, } from './rowCoordinateCalculations.js';
|
|
35
32
|
import { resolveScale } from './scales.js';
|
|
36
33
|
import { buildSeqPosIndex } from './seqPosToGlobalCol.js';
|
|
37
34
|
import { maxBitsFor } from './sequenceLogo.js';
|
|
35
|
+
import { blosum62ColumnColors, identityColumnColors, } from './similarityColors.js';
|
|
38
36
|
import { stripDefault } from './stripDefault.js';
|
|
39
37
|
import { treeTips, widestLabel } from './tipLabels.js';
|
|
40
|
-
import {
|
|
38
|
+
import { buildTreeRoot, outgroupNode, parseTreeText, resolveClades, rootTree, tipRange, } from './treeDisplay.js';
|
|
39
|
+
import { computeRowInsertions, dropBlanks, len, readComputed, skipBlanks, withAlpha, } from './util.js';
|
|
41
40
|
import { saveAs } from './vendor/fileSaver.js';
|
|
42
41
|
import { writeNewick } from './writeNewick.js';
|
|
43
42
|
import { parseWuss } from './wuss.js';
|
|
44
|
-
|
|
45
|
-
if (text.startsWith('BioTreeContainer')) {
|
|
46
|
-
return flatToTree(parseAsn1(text));
|
|
47
|
-
}
|
|
48
|
-
return parseNewick(text.startsWith('SEQ') ? parseEmfTree(text).tree : text);
|
|
49
|
-
}
|
|
43
|
+
import { takeZoomSnapshot, zoomSnapshotCovers } from './zoomSnapshot.js';
|
|
50
44
|
// The height each kind draws at before the user drags a divider. A text track
|
|
51
45
|
// is absent: it is one alignment row tall and follows rowHeight, so the zoom
|
|
52
46
|
// controls already size it.
|
|
@@ -70,186 +64,6 @@ const ownHeightKey = (id) => `own:${id}`;
|
|
|
70
64
|
// them
|
|
71
65
|
const HELIX_ARC = '#4e79a7';
|
|
72
66
|
const PSEUDOKNOT_ARC = '#e15759';
|
|
73
|
-
// the fill of a span under a scale that gives its value no color, such as a
|
|
74
|
-
// gene a `map` over gene names leaves out
|
|
75
|
-
const UNSCALED_FEATURE = '#d9d9d9';
|
|
76
|
-
// the fraction of a span a following one may cover before it takes a lane of
|
|
77
|
-
// its own in a `features` panel
|
|
78
|
-
const laneOverlap = 0.1;
|
|
79
|
-
// a row panel's own width, or a default: the row height, which makes a strip
|
|
80
|
-
// cell square, and a gene neighborhood's width for a features panel. The row
|
|
81
|
-
// height is read only when used, so a panel with a width of its own does not
|
|
82
|
-
// recompute on a vertical zoom
|
|
83
|
-
function rowPanelWidth(panel, view) {
|
|
84
|
-
return (panel.width ??
|
|
85
|
-
(panel.kind === 'features' ? defaultFeaturePanelWidth : view.rowHeight));
|
|
86
|
-
}
|
|
87
|
-
// one row of the row table as the channels read it. A number or a boolean
|
|
88
|
-
// reads as its string, the way a GFF value does, and null or an object drops
|
|
89
|
-
function stringFields(row) {
|
|
90
|
-
if (typeof row !== 'object' || row === null) {
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
const fields = {};
|
|
94
|
-
for (const [field, value] of Object.entries(row)) {
|
|
95
|
-
if (typeof value === 'string') {
|
|
96
|
-
fields[field] = value;
|
|
97
|
-
}
|
|
98
|
-
else if (typeof value === 'number' || typeof value === 'boolean') {
|
|
99
|
-
fields[field] = String(value);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return fields;
|
|
103
|
-
}
|
|
104
|
-
// the color each row takes from a field of the row table under a scale
|
|
105
|
-
function colorByRow(rowData, field, colorOf) {
|
|
106
|
-
const colors = new Map();
|
|
107
|
-
for (const [name, row] of Object.entries(rowData)) {
|
|
108
|
-
const value = row?.[field];
|
|
109
|
-
const color = value === undefined ? undefined : colorOf(value);
|
|
110
|
-
if (color) {
|
|
111
|
-
colors.set(name, color);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return colors;
|
|
115
|
-
}
|
|
116
|
-
// the fill and outline of every feature in a list: its own GFF `color=` first,
|
|
117
|
-
// then the scale an encoding resolves over one of its fields, then the
|
|
118
|
-
// accession palette
|
|
119
|
-
function featureColorMap(annotations, encoding, fillPalette) {
|
|
120
|
-
const strokes = new Map();
|
|
121
|
-
const strokeOf = (fill) => {
|
|
122
|
-
const hit = strokes.get(fill);
|
|
123
|
-
if (hit !== undefined) {
|
|
124
|
-
return hit;
|
|
125
|
-
}
|
|
126
|
-
const stroke = outlineColor(fill);
|
|
127
|
-
strokes.set(fill, stroke);
|
|
128
|
-
return stroke;
|
|
129
|
-
};
|
|
130
|
-
return new Map(annotations.map(annotation => {
|
|
131
|
-
const value = encoding
|
|
132
|
-
? featureField(annotation, encoding.field)
|
|
133
|
-
: undefined;
|
|
134
|
-
const fill = annotation.color ??
|
|
135
|
-
(value === undefined ? undefined : encoding.colorOf(value)) ??
|
|
136
|
-
(encoding ? UNSCALED_FEATURE : fillPalette[annotation.accession]);
|
|
137
|
-
return [annotation, { fill, stroke: strokeOf(fill) }];
|
|
138
|
-
}));
|
|
139
|
-
}
|
|
140
|
-
// the text a label channel draws inside each span, for the features carrying
|
|
141
|
-
// the field it names
|
|
142
|
-
function featureLabelMap(annotations, field) {
|
|
143
|
-
const labels = new Map();
|
|
144
|
-
for (const annotation of annotations) {
|
|
145
|
-
const value = featureField(annotation, field);
|
|
146
|
-
if (value !== undefined) {
|
|
147
|
-
labels.set(annotation, value);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return labels;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Lanes for `position: "strandpile"`, gggenomes' `position_strandpile`: the
|
|
154
|
-
* forward features stack above the line and the reverse below it, each strand
|
|
155
|
-
* packing on its own. The deepest row on each side sets the grid every row lays
|
|
156
|
-
* out on, so the line between the strands sits at one height down the panel and
|
|
157
|
-
* a reader can scan it. A feature with no strand piles with the forward ones.
|
|
158
|
-
*/
|
|
159
|
-
function strandpileLanes(rows) {
|
|
160
|
-
const piled = rows.map(([name, bands]) => {
|
|
161
|
-
const reverse = packDomainLanes(bands.filter(b => b.annotation.strand === -1));
|
|
162
|
-
const forward = packDomainLanes(bands.filter(b => b.annotation.strand !== -1));
|
|
163
|
-
return { name, forward, reverse };
|
|
164
|
-
});
|
|
165
|
-
const up = Math.max(0, ...piled.map(p => p.forward[0]?.laneCount ?? 0));
|
|
166
|
-
const down = Math.max(0, ...piled.map(p => p.reverse[0]?.laneCount ?? 0));
|
|
167
|
-
const laneCount = up + down;
|
|
168
|
-
return new Map(piled.map(({ name, forward, reverse }) => [
|
|
169
|
-
name,
|
|
170
|
-
[
|
|
171
|
-
// forward level 0 is the lane just above the line, stacking upward
|
|
172
|
-
...forward.map(b => ({ ...b, lane: up - 1 - b.lane, laneCount })),
|
|
173
|
-
...reverse.map(b => ({ ...b, lane: up + b.lane, laneCount })),
|
|
174
|
-
],
|
|
175
|
-
]));
|
|
176
|
-
}
|
|
177
|
-
/** one row's spans laid out by the panel's `position` */
|
|
178
|
-
function panelLanes(panel, rows) {
|
|
179
|
-
return panel.position === 'strandpile'
|
|
180
|
-
? strandpileLanes(rows)
|
|
181
|
-
: new Map(rows.map(([name, bands]) => [name, packDomainLanes(bands)]));
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* The spans a `features` panel draws, keyed by row name and measured in the
|
|
185
|
-
* panel's own pixels. `column` takes the bands the overlay draws, at the
|
|
186
|
-
* alignment's column width. `position` packs each row's features in its own
|
|
187
|
-
* residue positions, shifted by an `align` transform, and maps the extent they
|
|
188
|
-
* cover across every row onto the panel width, so a row with no alignment
|
|
189
|
-
* still has an x.
|
|
190
|
-
*/
|
|
191
|
-
function featurePanelSpans({ panel, width, colWidth, domainBands, annotationsByRow, shifts, }) {
|
|
192
|
-
if (panel.x === 'column') {
|
|
193
|
-
return panelLanes(panel, [...domainBands].map(([name, bands]) => [
|
|
194
|
-
name,
|
|
195
|
-
bands.map(band => ({
|
|
196
|
-
...band,
|
|
197
|
-
xStart: band.startCol * colWidth,
|
|
198
|
-
xEnd: band.endCol * colWidth,
|
|
199
|
-
})),
|
|
200
|
-
]));
|
|
201
|
-
}
|
|
202
|
-
const shifted = Object.entries(annotationsByRow).map(([name, annotations]) => {
|
|
203
|
-
const shift = shifts?.get(name) ?? 0;
|
|
204
|
-
return [
|
|
205
|
-
name,
|
|
206
|
-
annotations.map(annotation => ({
|
|
207
|
-
annotation,
|
|
208
|
-
start: annotation.start - 1 + shift,
|
|
209
|
-
end: annotation.end + shift,
|
|
210
|
-
})),
|
|
211
|
-
];
|
|
212
|
-
});
|
|
213
|
-
let min = Infinity;
|
|
214
|
-
let max = -Infinity;
|
|
215
|
-
for (const [, features] of shifted) {
|
|
216
|
-
for (const { start, end } of features) {
|
|
217
|
-
min = Math.min(min, start);
|
|
218
|
-
max = Math.max(max, end);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
// a gene arrow ends where its feature does, so the extent maps onto the whole
|
|
222
|
-
// panel less the pixel the rightmost stroke needs
|
|
223
|
-
const drawable = Math.max(1, width - 1);
|
|
224
|
-
const scale = max > min ? drawable / (max - min) : 0;
|
|
225
|
-
// a lane opens where a span covers more than a tenth of the one before it,
|
|
226
|
-
// which keeps the genes of an operon on one lane: adjacent genes commonly
|
|
227
|
-
// share a few bases, and a stop codon overlapping the next start reads as a
|
|
228
|
-
// second lane over the whole row
|
|
229
|
-
return panelLanes(panel, shifted.map(([name, features]) => [
|
|
230
|
-
name,
|
|
231
|
-
features.map(({ annotation, start, end }) => ({
|
|
232
|
-
annotation,
|
|
233
|
-
xStart: (start - min) * scale,
|
|
234
|
-
xEnd: (end - min) * scale,
|
|
235
|
-
startCol: start,
|
|
236
|
-
endCol: end - (end - start) * laneOverlap,
|
|
237
|
-
})),
|
|
238
|
-
]));
|
|
239
|
-
}
|
|
240
|
-
// a scale over the values a field takes across every feature loaded, so hiding
|
|
241
|
-
// a type in the filter dialog leaves the rest in their colors, and a legend of
|
|
242
|
-
// the values the features drawn carry
|
|
243
|
-
function resolveFeatureScale(field, scale, annotations, drawn) {
|
|
244
|
-
const valuesOf = (list) => list.map(a => featureField(a, field)).filter(notEmpty);
|
|
245
|
-
const resolved = resolveScale(scale, valuesOf(annotations));
|
|
246
|
-
const shown = new Set(valuesOf(drawn));
|
|
247
|
-
return {
|
|
248
|
-
field,
|
|
249
|
-
...resolved,
|
|
250
|
-
legend: resolved.legend.filter(entry => shown.has(entry.id)),
|
|
251
|
-
};
|
|
252
|
-
}
|
|
253
67
|
const columnTrackBytes = new WeakMap();
|
|
254
68
|
function columnTrackSize(track) {
|
|
255
69
|
let bytes = columnTrackBytes.get(track);
|
|
@@ -302,6 +116,7 @@ export const preservedOnReset = new Set([
|
|
|
302
116
|
'drawNodeBubbles',
|
|
303
117
|
'drawNodeLabels',
|
|
304
118
|
'showTreeOverview',
|
|
119
|
+
'showTreeScaleBar',
|
|
305
120
|
'overviewHeight',
|
|
306
121
|
'turnedOffTracks',
|
|
307
122
|
'trackHeights',
|
|
@@ -309,172 +124,17 @@ export const preservedOnReset = new Set([
|
|
|
309
124
|
'allowedGappyness',
|
|
310
125
|
'subFeatureRows',
|
|
311
126
|
'showDomainLegend',
|
|
127
|
+
'domainFill',
|
|
128
|
+
'embeddedMode',
|
|
312
129
|
]);
|
|
313
130
|
// `turnedOffTracks` holds only the user's explicit choices, so a hidden-by-default
|
|
314
|
-
// track writes nothing into the snapshot
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
function trackIsOff(turnedOffTracks, id, defaultOff) {
|
|
318
|
-
return turnedOffTracks.get(id) ?? (defaultOff || defaultOffTracks.has(id));
|
|
131
|
+
// track writes nothing into the snapshot
|
|
132
|
+
function trackIsOff(turnedOffTracks, id) {
|
|
133
|
+
return turnedOffTracks.get(id) ?? defaultOffTracks.has(id);
|
|
319
134
|
}
|
|
320
|
-
// the marks that set tree state from a node rather than drawing
|
|
321
|
-
const seedingMarks = new Set(['collapse', 'focus', 'rotate']);
|
|
322
135
|
// shared empty results, so observers don't see a fresh [] as a change
|
|
323
136
|
const noDomains = [];
|
|
324
137
|
const noClades = [];
|
|
325
|
-
/**
|
|
326
|
-
* The rows a clade covers and the node an `mrca` names, or undefined when the
|
|
327
|
-
* clade does not resolve: a tip name the tree does not have or has twice, or a
|
|
328
|
-
* leaf count `tips` disagrees with. `index` and `rowNamesSet` are the memoized
|
|
329
|
-
* passes over the tree.
|
|
330
|
-
*/
|
|
331
|
-
function cladeRows(clade, root, index, rowNamesSet) {
|
|
332
|
-
if (clade.range) {
|
|
333
|
-
const [a, b] = clade.range.map(name => index.get(name) ? rowNamesSet.get(name) : undefined);
|
|
334
|
-
if (a === undefined || b === undefined) {
|
|
335
|
-
return undefined;
|
|
336
|
-
}
|
|
337
|
-
const rows = a <= b ? [a, b] : [b, a];
|
|
338
|
-
return rows[1] - rows[0] + 1 === clade.tips ? { rows } : undefined;
|
|
339
|
-
}
|
|
340
|
-
const node = clade.mrca ? mrca(root, clade.mrca, index) : undefined;
|
|
341
|
-
if (!node) {
|
|
342
|
-
return undefined;
|
|
343
|
-
}
|
|
344
|
-
const tips = leaves(node);
|
|
345
|
-
if (tips.length !== clade.tips) {
|
|
346
|
-
return undefined;
|
|
347
|
-
}
|
|
348
|
-
let first = Infinity;
|
|
349
|
-
let last = -Infinity;
|
|
350
|
-
for (const tip of tips) {
|
|
351
|
-
const row = rowNamesSet.get(tip.data.name);
|
|
352
|
-
if (row !== undefined) {
|
|
353
|
-
first = Math.min(first, row);
|
|
354
|
-
last = Math.max(last, row);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
return first <= last
|
|
358
|
-
? { rows: [first, last], nodeId: node.data.id }
|
|
359
|
-
: undefined;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Every clade that resolves, with its fill color settled, against the leaf
|
|
363
|
-
* order `rowNamesSet` gives. The tree panel resolves against the displayed
|
|
364
|
-
* rows and the overview against its own, so both take the tree they draw. A
|
|
365
|
-
* `range` record names no node, so the seeding marks, which need one, drop it.
|
|
366
|
-
*/
|
|
367
|
-
function resolveClades(clades, root, rowNamesSet) {
|
|
368
|
-
const index = leafIndex(root);
|
|
369
|
-
return clades.flatMap(clade => {
|
|
370
|
-
const resolved = cladeRows(clade, root, index, rowNamesSet);
|
|
371
|
-
const seeding = seedingMarks.has(clade.mark);
|
|
372
|
-
return resolved && !(seeding && resolved.nodeId === undefined)
|
|
373
|
-
? [
|
|
374
|
-
{
|
|
375
|
-
...resolved,
|
|
376
|
-
mark: clade.mark,
|
|
377
|
-
color: withAlpha(clade.color ?? cladeHighlightColor, cladeHighlightAlpha),
|
|
378
|
-
markColor: clade.color,
|
|
379
|
-
label: clade.label,
|
|
380
|
-
},
|
|
381
|
-
]
|
|
382
|
-
: [];
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
const childOrders = {
|
|
386
|
-
input: undefined,
|
|
387
|
-
branchLength: (a, b) => (a.data.length ?? 1) - (b.data.length ?? 1),
|
|
388
|
-
ladderize: (a, b) => a.value - b.value,
|
|
389
|
-
ladderizeReverse: (a, b) => b.value - a.value,
|
|
390
|
-
};
|
|
391
|
-
/**
|
|
392
|
-
* The tree with the display transforms applied: leaf counts summed, children
|
|
393
|
-
* put in `order` and each `rotated` node's reversed, `showOnly` taken as the
|
|
394
|
-
* new root, and each `collapsed` clade folded. The overview builds the same
|
|
395
|
-
* tree without the focus, so it shows the focused subtree inside the whole.
|
|
396
|
-
*/
|
|
397
|
-
function buildTreeRoot(tree, { collapsed, rotated, order, showOnly, }) {
|
|
398
|
-
let hier = hierarchy(tree, d => d.children);
|
|
399
|
-
hierarchySum(hier, d => (d.children.length > 0 ? 0 : 1));
|
|
400
|
-
const compare = childOrders[order];
|
|
401
|
-
if (compare) {
|
|
402
|
-
sort(hier, compare);
|
|
403
|
-
}
|
|
404
|
-
if (rotated.length > 0) {
|
|
405
|
-
const flip = new Set(rotated);
|
|
406
|
-
forEachDescendant(hier, n => {
|
|
407
|
-
if (flip.has(n.data.id)) {
|
|
408
|
-
n.children?.reverse();
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
if (showOnly) {
|
|
413
|
-
const res = find(hier, n => n.data.id === showOnly);
|
|
414
|
-
if (res) {
|
|
415
|
-
hier = res;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
for (const collapsedId of collapsed) {
|
|
419
|
-
const node = find(hier, n => n.data.id === collapsedId);
|
|
420
|
-
if (node) {
|
|
421
|
-
if (node.children) {
|
|
422
|
-
collapse(node);
|
|
423
|
-
}
|
|
424
|
-
else if (node.parent?.children) {
|
|
425
|
-
node.parent.children = node.parent.children.filter(c => c.data.id !== collapsedId);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
return hier;
|
|
430
|
-
}
|
|
431
|
-
/**
|
|
432
|
-
* The node whose branch an outgroup roots the tree on: the outgroup's most
|
|
433
|
-
* recent common ancestor, or the rest of the tips' where that ancestor is the
|
|
434
|
-
* root, since the outgroup then straddles the root and the branch above the
|
|
435
|
-
* other side is the same branch of the unrooted tree.
|
|
436
|
-
*/
|
|
437
|
-
function outgroupNode(root, outgroup) {
|
|
438
|
-
const index = leafIndex(root);
|
|
439
|
-
const node = mrca(root, outgroup, index);
|
|
440
|
-
if (node !== root) {
|
|
441
|
-
return node;
|
|
442
|
-
}
|
|
443
|
-
const out = new Set(outgroup);
|
|
444
|
-
const rest = leaves(root)
|
|
445
|
-
.map(n => n.data.name)
|
|
446
|
-
.filter(name => !out.has(name));
|
|
447
|
-
const other = mrca(root, rest, index);
|
|
448
|
-
return other === root ? undefined : other;
|
|
449
|
-
}
|
|
450
|
-
/**
|
|
451
|
-
* `tree` rerooted as `treeRoot` asks, with fresh path ids. The ids of the tree
|
|
452
|
-
* as written become names on unnamed nodes, and a rerooting would carry them
|
|
453
|
-
* along as labels, so they come off first.
|
|
454
|
-
*/
|
|
455
|
-
function rootTree(tree, treeRoot) {
|
|
456
|
-
const plain = new Map();
|
|
457
|
-
for (const node of preorder(tree).reverse()) {
|
|
458
|
-
plain.set(node, {
|
|
459
|
-
name: node.name === node.id ? undefined : node.name,
|
|
460
|
-
length: node.length,
|
|
461
|
-
children: node.children.map(c => plain.get(c)),
|
|
462
|
-
});
|
|
463
|
-
}
|
|
464
|
-
const input = plain.get(tree);
|
|
465
|
-
if (treeRoot === 'midpoint') {
|
|
466
|
-
return generateNodeIds(midpointRoot(input));
|
|
467
|
-
}
|
|
468
|
-
const target = outgroupNode(hierarchy(tree, d => d.children), treeRoot.outgroup);
|
|
469
|
-
return target
|
|
470
|
-
? generateNodeIds(rerootTree(input, plain.get(target.data)))
|
|
471
|
-
: tree;
|
|
472
|
-
}
|
|
473
|
-
// the inclusive tip indices a node covers, from the row-space extent
|
|
474
|
-
// clusterLayout writes to xMin/xMax as tip centers
|
|
475
|
-
function tipRange(node) {
|
|
476
|
-
return [Math.round(node.xMin - 0.5), Math.round(node.xMax - 0.5)];
|
|
477
|
-
}
|
|
478
138
|
// the channels reading the feature table; every other channel reads rowData
|
|
479
139
|
const featureChannels = new Set([
|
|
480
140
|
'featureFill',
|
|
@@ -483,6 +143,15 @@ const featureChannels = new Set([
|
|
|
483
143
|
// seqPos -> column indexes per row, keyed on the parse so they are garbage
|
|
484
144
|
// collected with it. A computed would rebuild every row's index when read
|
|
485
145
|
// outside a reactive context.
|
|
146
|
+
// Newick tools write a colon in a name as an underscore, so a tree's row
|
|
147
|
+
// `chr1_100-200` is the GFF's `chr1:100-200`, as FastaMSA.getRow resolves it
|
|
148
|
+
function annotationRowName(id, rowNames) {
|
|
149
|
+
if (rowNames.has(id)) {
|
|
150
|
+
return id;
|
|
151
|
+
}
|
|
152
|
+
const underscored = id.replaceAll(':', '_');
|
|
153
|
+
return rowNames.has(underscored) ? underscored : id;
|
|
154
|
+
}
|
|
486
155
|
const seqPosIndexCache = new WeakMap();
|
|
487
156
|
// A segment is a 1:1 run. A segment whose sides differ in length is malformed,
|
|
488
157
|
// and the lookups treat it like an uncovered position.
|
|
@@ -490,6 +159,22 @@ function sameLength(segment) {
|
|
|
490
159
|
return (segment.rowEnd - segment.rowStart ===
|
|
491
160
|
segment.structEnd - segment.structStart);
|
|
492
161
|
}
|
|
162
|
+
// A zoom that passes the default cell size stops on it, and one that starts on
|
|
163
|
+
// it holds there until the wheel has gathered `zoomDetent` worth of scale, so
|
|
164
|
+
// a zoom in or out finds its way back to the default
|
|
165
|
+
const zoomDetent = Math.log(1.3);
|
|
166
|
+
function detentZoom(value, scale, home, held, min, canHold) {
|
|
167
|
+
if (value === home && canHold) {
|
|
168
|
+
const total = held + Math.log(scale);
|
|
169
|
+
return Math.abs(total) < zoomDetent
|
|
170
|
+
? { value, held: total }
|
|
171
|
+
: { value: clamp(home * Math.exp(total), min, maxCellSize), held: 0 };
|
|
172
|
+
}
|
|
173
|
+
const next = clamp(value * scale, min, maxCellSize);
|
|
174
|
+
return (value - home) * (next - home) < 0
|
|
175
|
+
? { value: home, held: 0 }
|
|
176
|
+
: { value: next, held: 0 };
|
|
177
|
+
}
|
|
493
178
|
// fit() divides the viewport by the row or column count and multiplies back, so
|
|
494
179
|
// an exact fit can land a fraction of a pixel over. A plain `>` would then show
|
|
495
180
|
// a scrollbar that shrinks the viewport and leaves a gap.
|
|
@@ -521,19 +206,6 @@ function sharedValue(values) {
|
|
|
521
206
|
? first
|
|
522
207
|
: undefined;
|
|
523
208
|
}
|
|
524
|
-
// preorder, so reversing the list puts every node after its descendants
|
|
525
|
-
function preorder(tree) {
|
|
526
|
-
const order = [];
|
|
527
|
-
const stack = [tree];
|
|
528
|
-
while (stack.length > 0) {
|
|
529
|
-
const node = stack.pop();
|
|
530
|
-
order.push(node);
|
|
531
|
-
for (const child of node.children) {
|
|
532
|
-
stack.push(child);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
return order;
|
|
536
|
-
}
|
|
537
209
|
/**
|
|
538
210
|
* #stateModel MsaView
|
|
539
211
|
*
|
|
@@ -571,6 +243,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
571
243
|
* #property
|
|
572
244
|
*/
|
|
573
245
|
showDomains: stripDefault(types.boolean, defaultShowDomains),
|
|
246
|
+
/**
|
|
247
|
+
* #property
|
|
248
|
+
* whether shown domains fill the cells they cover. Off, the residue
|
|
249
|
+
* scheme keeps the cells and each domain draws as a bar under its
|
|
250
|
+
* row. See `cellFill`.
|
|
251
|
+
*/
|
|
252
|
+
domainFill: stripDefault(types.boolean, true),
|
|
574
253
|
/**
|
|
575
254
|
* #property
|
|
576
255
|
* whether the domain legend is expanded. The legend floats over the
|
|
@@ -599,6 +278,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
599
278
|
* #property
|
|
600
279
|
*/
|
|
601
280
|
drawMsaLetters: stripDefault(types.boolean, defaultDrawMsaLetters),
|
|
281
|
+
/**
|
|
282
|
+
* #property
|
|
283
|
+
* draws the toolbar's menus and Settings as icons without text, for a
|
|
284
|
+
* host with little room, such as a JBrowse view
|
|
285
|
+
*/
|
|
286
|
+
embeddedMode: stripDefault(types.boolean, false),
|
|
602
287
|
/**
|
|
603
288
|
* #property
|
|
604
289
|
* zoom in/out on plain mouse-wheel without holding ctrl
|
|
@@ -723,10 +408,10 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
723
408
|
data: types.optional(DataModelF(maxInlineSnapshotBytes), {}),
|
|
724
409
|
/**
|
|
725
410
|
* #property
|
|
726
|
-
* the user's
|
|
727
|
-
* accession with the value meaning "off", like
|
|
728
|
-
* untouched accession is absent and
|
|
729
|
-
* only with the user's filters
|
|
411
|
+
* the user's departures from each annotation accession's default,
|
|
412
|
+
* keyed by accession with the value meaning "off", like
|
|
413
|
+
* `turnedOffTracks`. An untouched accession is absent and takes its
|
|
414
|
+
* default, so the shared URL grows only with the user's filters
|
|
730
415
|
*/
|
|
731
416
|
turnedOffFeatures: stripDefault(types.map(types.boolean), {}),
|
|
732
417
|
/**
|
|
@@ -768,9 +453,10 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
768
453
|
/**
|
|
769
454
|
* #property
|
|
770
455
|
* where the view opens, in `highlights` coordinates: `{row, start,
|
|
771
|
-
* end}` zooms onto residues of that row
|
|
772
|
-
*
|
|
773
|
-
* then cleared, so a reloaded
|
|
456
|
+
* end}` zooms onto residues of that row and scrolls the row to the
|
|
457
|
+
* middle, `{start, end}` zooms onto alignment columns. Applied once the
|
|
458
|
+
* alignment and any tree file have loaded, then cleared, so a reloaded
|
|
459
|
+
* session keeps the reader's own scroll.
|
|
774
460
|
*/
|
|
775
461
|
region: types.frozen(),
|
|
776
462
|
/**
|
|
@@ -805,12 +491,30 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
805
491
|
* #volatile
|
|
806
492
|
*/
|
|
807
493
|
headerHeight: 0,
|
|
494
|
+
/**
|
|
495
|
+
* #volatile
|
|
496
|
+
* whether the settings panel docks beside the alignment, taking
|
|
497
|
+
* `settingsPanelWidth` out of the width the view lays out in
|
|
498
|
+
*/
|
|
499
|
+
settingsOpen: false,
|
|
500
|
+
/**
|
|
501
|
+
* #volatile
|
|
502
|
+
* the log scale a wheel zoom has gathered against the detent at the
|
|
503
|
+
* default column width and row height, per axis
|
|
504
|
+
*/
|
|
505
|
+
zoomHeld: { col: 0, row: 0 },
|
|
808
506
|
/**
|
|
809
507
|
* #volatile
|
|
810
508
|
* leaves the toolbar out, for a host drawing its own controls. Kept out
|
|
811
509
|
* of the snapshot so a link opened in the full app shows the toolbar.
|
|
812
510
|
*/
|
|
813
511
|
hideHeader: false,
|
|
512
|
+
/**
|
|
513
|
+
* #volatile
|
|
514
|
+
* shows the import form while the view has no data. MSAViewer turns it
|
|
515
|
+
* off, since its host supplies the data and the form is the demo app's
|
|
516
|
+
*/
|
|
517
|
+
importForm: true,
|
|
814
518
|
/**
|
|
815
519
|
* #volatile
|
|
816
520
|
*/
|
|
@@ -820,6 +524,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
820
524
|
* canvas scale factor, from the device pixel ratio
|
|
821
525
|
*/
|
|
822
526
|
highResScaleFactor: typeof window === 'undefined' ? 1 : window.devicePixelRatio,
|
|
527
|
+
/**
|
|
528
|
+
* #volatile
|
|
529
|
+
* the cell size and block grid the alignment last drew at, held from a
|
|
530
|
+
* wheel-zoom frame until the gesture settles. MSACanvas stretches those
|
|
531
|
+
* blocks to the current cell size with one CSS transform
|
|
532
|
+
*/
|
|
533
|
+
zoomSnapshot: undefined,
|
|
823
534
|
/**
|
|
824
535
|
* #volatile
|
|
825
536
|
*/
|
|
@@ -964,6 +675,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
964
675
|
setWidth(arg) {
|
|
965
676
|
self.volatileWidth = arg;
|
|
966
677
|
},
|
|
678
|
+
/**
|
|
679
|
+
* #action
|
|
680
|
+
*/
|
|
681
|
+
setSettingsOpen(arg) {
|
|
682
|
+
self.settingsOpen = arg;
|
|
683
|
+
},
|
|
967
684
|
/**
|
|
968
685
|
* #action
|
|
969
686
|
* update the canvas scale factor when the device pixel ratio changes
|
|
@@ -1101,6 +818,23 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
1101
818
|
setShowDomains(arg) {
|
|
1102
819
|
self.showDomains = arg;
|
|
1103
820
|
},
|
|
821
|
+
/**
|
|
822
|
+
* #action
|
|
823
|
+
*/
|
|
824
|
+
setDomainFill(arg) {
|
|
825
|
+
self.domainFill = arg;
|
|
826
|
+
},
|
|
827
|
+
/**
|
|
828
|
+
* #action
|
|
829
|
+
* hand the cell fill to the residue scheme, the domains, or neither,
|
|
830
|
+
* which colors the letters instead
|
|
831
|
+
*/
|
|
832
|
+
setCellFill(mode) {
|
|
833
|
+
self.setBgColor(mode !== 'none');
|
|
834
|
+
if (mode !== 'none') {
|
|
835
|
+
self.domainFill = mode === 'domain';
|
|
836
|
+
}
|
|
837
|
+
},
|
|
1104
838
|
/**
|
|
1105
839
|
* #action
|
|
1106
840
|
* expand or collapse the domain legend that floats over the alignment
|
|
@@ -1128,6 +862,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
1128
862
|
*/
|
|
1129
863
|
setRowHeight(n) {
|
|
1130
864
|
self.rowHeight = n;
|
|
865
|
+
self.zoomHeld = { ...self.zoomHeld, row: 0 };
|
|
1131
866
|
},
|
|
1132
867
|
/**
|
|
1133
868
|
* #action
|
|
@@ -1135,6 +870,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
1135
870
|
*/
|
|
1136
871
|
setColWidth(n) {
|
|
1137
872
|
self.colWidth = n;
|
|
873
|
+
self.zoomHeld = { ...self.zoomHeld, col: 0 };
|
|
1138
874
|
},
|
|
1139
875
|
/**
|
|
1140
876
|
* #action
|
|
@@ -1275,33 +1011,15 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
1275
1011
|
.views(self => ({
|
|
1276
1012
|
/**
|
|
1277
1013
|
* #getter
|
|
1278
|
-
* hideGaps takes effect when
|
|
1014
|
+
* hideGaps takes effect when a collapse or a focus hides rows, whose
|
|
1015
|
+
* residues leave columns all gaps among the rest, or when
|
|
1016
|
+
* allowedGappyness is under 100
|
|
1279
1017
|
*/
|
|
1280
1018
|
get hideGapsEffective() {
|
|
1281
1019
|
return (self.hideGaps &&
|
|
1282
|
-
(self.collapsed.length > 0 ||
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
* #getter
|
|
1286
|
-
*/
|
|
1287
|
-
get realAllowedGappyness() {
|
|
1288
|
-
return this.hideGapsEffective ? self.allowedGappyness : 100;
|
|
1289
|
-
},
|
|
1290
|
-
/**
|
|
1291
|
-
* #getter
|
|
1292
|
-
* the annotations the overlay, the legend and the encodings draw: the
|
|
1293
|
-
* GFF's, then the `features` layer's
|
|
1294
|
-
*/
|
|
1295
|
-
get allAnnotations() {
|
|
1296
|
-
return self.features.length > 0
|
|
1297
|
-
? [...self.annotations, ...featuresToAnnotations(self.features)]
|
|
1298
|
-
: self.annotations;
|
|
1299
|
-
},
|
|
1300
|
-
/**
|
|
1301
|
-
* #getter
|
|
1302
|
-
*/
|
|
1303
|
-
get actuallyShowDomains() {
|
|
1304
|
-
return self.showDomains && this.allAnnotations.length > 0;
|
|
1020
|
+
(self.collapsed.length > 0 ||
|
|
1021
|
+
self.showOnly !== undefined ||
|
|
1022
|
+
self.allowedGappyness < 100));
|
|
1305
1023
|
},
|
|
1306
1024
|
/**
|
|
1307
1025
|
* #getter
|
|
@@ -1327,7 +1045,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
1327
1045
|
if (self.volatileWidth === undefined) {
|
|
1328
1046
|
throw new Error('not initialized');
|
|
1329
1047
|
}
|
|
1330
|
-
return self.volatileWidth;
|
|
1048
|
+
return self.volatileWidth - (self.settingsOpen ? settingsPanelWidth : 0);
|
|
1331
1049
|
},
|
|
1332
1050
|
}))
|
|
1333
1051
|
.views(self => ({
|
|
@@ -1374,7 +1092,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
1374
1092
|
return !!this.tree.noTree;
|
|
1375
1093
|
},
|
|
1376
1094
|
get noDomains() {
|
|
1377
|
-
return
|
|
1095
|
+
return this.allAnnotations.length === 0;
|
|
1378
1096
|
},
|
|
1379
1097
|
/**
|
|
1380
1098
|
* #getter
|
|
@@ -1475,6 +1193,27 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
1475
1193
|
? parseMSA(text, self.currentAlignment, self.msaFormat)
|
|
1476
1194
|
: null;
|
|
1477
1195
|
},
|
|
1196
|
+
/**
|
|
1197
|
+
* #getter
|
|
1198
|
+
* the annotations the overlay, the legend and the encodings draw: the
|
|
1199
|
+
* GFF's, the `features` layer's, then the file's own per-row lines
|
|
1200
|
+
*/
|
|
1201
|
+
get allAnnotations() {
|
|
1202
|
+
const inline = this.MSA?.annotations ?? [];
|
|
1203
|
+
return self.features.length > 0 || inline.length > 0
|
|
1204
|
+
? [
|
|
1205
|
+
...self.annotations,
|
|
1206
|
+
...featuresToAnnotations(self.features),
|
|
1207
|
+
...inline,
|
|
1208
|
+
]
|
|
1209
|
+
: self.annotations;
|
|
1210
|
+
},
|
|
1211
|
+
/**
|
|
1212
|
+
* #getter
|
|
1213
|
+
*/
|
|
1214
|
+
get actuallyShowDomains() {
|
|
1215
|
+
return self.showDomains && this.allAnnotations.length > 0;
|
|
1216
|
+
},
|
|
1478
1217
|
/**
|
|
1479
1218
|
* #getter
|
|
1480
1219
|
*/
|
|
@@ -1534,7 +1273,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
1534
1273
|
if (self.clades.length === 0) {
|
|
1535
1274
|
return noClades;
|
|
1536
1275
|
}
|
|
1537
|
-
return resolveClades(self.clades, hierarchy(this.tree, d => d.children), this.
|
|
1276
|
+
return resolveClades(self.clades, hierarchy(this.tree, d => d.children), this.root);
|
|
1538
1277
|
},
|
|
1539
1278
|
/**
|
|
1540
1279
|
* #getter
|
|
@@ -1702,21 +1441,16 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
1702
1441
|
if (!self.showTreeOverview) {
|
|
1703
1442
|
return undefined;
|
|
1704
1443
|
}
|
|
1705
|
-
const
|
|
1444
|
+
const layout = layoutTree(buildTreeRoot(this.tree, {
|
|
1706
1445
|
collapsed: self.collapsed,
|
|
1707
1446
|
rotated: self.rotated,
|
|
1708
1447
|
order: self.treeOrder,
|
|
1709
|
-
});
|
|
1710
|
-
const numTips = leaves(root).length;
|
|
1711
|
-
clusterLayout(root, numTips, 1);
|
|
1712
|
-
const rootLen = Math.max(root.data.length || 0, 0);
|
|
1713
|
-
const extent = maxLength(root) - rootLen;
|
|
1714
|
-
setBrLength(root, -rootLen, extent ? 1 / extent : 0);
|
|
1448
|
+
}), { rowHeight: 1, treeWidth: 1 });
|
|
1715
1449
|
return {
|
|
1716
|
-
root,
|
|
1717
|
-
numTips,
|
|
1718
|
-
maxDepthToLeaf:
|
|
1719
|
-
showBranchLen: self.showBranchLen &&
|
|
1450
|
+
root: layout.root,
|
|
1451
|
+
numTips: layout.leaves.length,
|
|
1452
|
+
maxDepthToLeaf: layout.root.depthToLeaf,
|
|
1453
|
+
showBranchLen: self.showBranchLen && layout.rootToTipLength > 0,
|
|
1720
1454
|
};
|
|
1721
1455
|
},
|
|
1722
1456
|
/**
|
|
@@ -1729,8 +1463,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
1729
1463
|
if (!layout || self.clades.length === 0) {
|
|
1730
1464
|
return noClades;
|
|
1731
1465
|
}
|
|
1732
|
-
|
|
1733
|
-
return resolveClades(self.clades, layout.root, rowNames);
|
|
1466
|
+
return resolveClades(self.clades, hierarchy(this.tree, d => d.children), layout.root);
|
|
1734
1467
|
},
|
|
1735
1468
|
/**
|
|
1736
1469
|
* #getter
|
|
@@ -1811,7 +1544,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
1811
1544
|
* #getter
|
|
1812
1545
|
*/
|
|
1813
1546
|
get blanks() {
|
|
1814
|
-
const { hideGapsEffective,
|
|
1547
|
+
const { hideGapsEffective, allowedGappyness } = self;
|
|
1815
1548
|
if (!hideGapsEffective) {
|
|
1816
1549
|
return [];
|
|
1817
1550
|
}
|
|
@@ -1827,7 +1560,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
1827
1560
|
// row that stops early counts as gapped for the remainder
|
|
1828
1561
|
const numCols = this.MSA?.getWidth() ?? 0;
|
|
1829
1562
|
const numRows = strs.length;
|
|
1830
|
-
const threshold = Math.ceil((
|
|
1563
|
+
const threshold = Math.ceil((allowedGappyness / 100) * numRows);
|
|
1831
1564
|
const blankCounts = new Uint32Array(numCols);
|
|
1832
1565
|
for (let j = 0; j < numRows; j++) {
|
|
1833
1566
|
const str = strs[j];
|
|
@@ -1909,11 +1642,19 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
1909
1642
|
}
|
|
1910
1643
|
return index;
|
|
1911
1644
|
},
|
|
1645
|
+
/**
|
|
1646
|
+
* #method
|
|
1647
|
+
* the row's aligned sequence by the name the tree gives it, including
|
|
1648
|
+
* rows a collapsed clade hides. `MSA.getRow` resolves a FASTA name whose
|
|
1649
|
+
* colon a Newick tool wrote as an underscore, which `rowMap` does not
|
|
1650
|
+
*/
|
|
1651
|
+
rowSequence(name) {
|
|
1652
|
+
return this.MSA?.getRow(name) || undefined;
|
|
1653
|
+
},
|
|
1912
1654
|
/**
|
|
1913
1655
|
* #getter
|
|
1914
|
-
* every sequence in the alignment, keyed by
|
|
1915
|
-
*
|
|
1916
|
-
* by row name use this
|
|
1656
|
+
* every sequence in the alignment, keyed by the alignment's own row
|
|
1657
|
+
* names, including rows a collapsed clade hides
|
|
1917
1658
|
*/
|
|
1918
1659
|
get rowMap() {
|
|
1919
1660
|
const MSA = this.MSA;
|
|
@@ -1956,10 +1697,11 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
1956
1697
|
*/
|
|
1957
1698
|
get sequenceType() {
|
|
1958
1699
|
const letters = this.colStats.lettersPresent;
|
|
1959
|
-
//
|
|
1960
|
-
//
|
|
1961
|
-
const
|
|
1962
|
-
const
|
|
1700
|
+
// the two-base IUPAC codes keep an ambiguous read nucleotide; the
|
|
1701
|
+
// three-base ones (B, D, H, V) are common amino acids, so they don't
|
|
1702
|
+
const ambiguous = ['N', 'R', 'Y', 'K', 'M', 'S', 'W'];
|
|
1703
|
+
const dna = new Set(['A', 'C', 'G', 'T', ...ambiguous]);
|
|
1704
|
+
const rna = new Set(['A', 'C', 'G', 'U', ...ambiguous]);
|
|
1963
1705
|
const isDna = letters.size > 0 && [...letters].every(l => dna.has(l));
|
|
1964
1706
|
const isRna = letters.size > 0 && [...letters].every(l => rna.has(l));
|
|
1965
1707
|
return isDna ? 'dna' : isRna ? 'rna' : 'amino';
|
|
@@ -2000,6 +1742,18 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2000
1742
|
const { colStats } = this;
|
|
2001
1743
|
return Array.from({ length: colStats.numColumns }, (_, col) => clustalXColumnColors(colStats, col));
|
|
2002
1744
|
},
|
|
1745
|
+
/**
|
|
1746
|
+
* #getter
|
|
1747
|
+
* Pre-computed similarity shades per column, for blosum62_dynamic.
|
|
1748
|
+
* Returns a map of letter -> color for each column. Amino acids shade by
|
|
1749
|
+
* BLOSUM62 similarity to the rest of the column, and an alignment of
|
|
1750
|
+
* nucleotides by identity, since the matrix names amino acids.
|
|
1751
|
+
*/
|
|
1752
|
+
get colSimilarity() {
|
|
1753
|
+
const { colStats, sequenceType } = this;
|
|
1754
|
+
const columnColors = sequenceType === 'amino' ? blosum62ColumnColors : identityColumnColors;
|
|
1755
|
+
return Array.from({ length: colStats.numColumns }, (_, col) => columnColors(colStats, col));
|
|
1756
|
+
},
|
|
2003
1757
|
/**
|
|
2004
1758
|
* #getter
|
|
2005
1759
|
* Conservation score per column using Shannon entropy (biojs-msa style).
|
|
@@ -2082,14 +1836,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2082
1836
|
get rootToTipLength() {
|
|
2083
1837
|
return this.treeLayout.rootToTipLength;
|
|
2084
1838
|
},
|
|
2085
|
-
/**
|
|
2086
|
-
* #getter
|
|
2087
|
-
* x-position of the farthest tip in a phylogram, px: effectiveTreeWidth,
|
|
2088
|
-
* or 0 for a tree with no branch lengths (drawn as a cladogram)
|
|
2089
|
-
*/
|
|
2090
|
-
get maxBranchLength() {
|
|
2091
|
-
return this.rootToTipLength ? this.effectiveTreeWidth : 0;
|
|
2092
|
-
},
|
|
2093
1839
|
/**
|
|
2094
1840
|
* #getter
|
|
2095
1841
|
* pixels per unit of branch length in the phylogram layout, 0 in
|
|
@@ -2245,7 +1991,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2245
1991
|
data: content?.data,
|
|
2246
1992
|
arcs: content?.arcs,
|
|
2247
1993
|
},
|
|
2248
|
-
ReactComponent: TrackBlocks,
|
|
2249
1994
|
};
|
|
2250
1995
|
});
|
|
2251
1996
|
columnTrackModels.set(track, computedModel);
|
|
@@ -2303,7 +2048,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2303
2048
|
data: hideGapsEffective ? skipBlanks(blanks, t.data) : t.data,
|
|
2304
2049
|
height: rowHeight,
|
|
2305
2050
|
},
|
|
2306
|
-
ReactComponent: TrackBlocks,
|
|
2307
2051
|
}));
|
|
2308
2052
|
},
|
|
2309
2053
|
/**
|
|
@@ -2325,7 +2069,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2325
2069
|
height: self.trackHeight('arc'),
|
|
2326
2070
|
arcs,
|
|
2327
2071
|
},
|
|
2328
|
-
ReactComponent: TrackBlocks,
|
|
2329
2072
|
},
|
|
2330
2073
|
]
|
|
2331
2074
|
: [];
|
|
@@ -2372,7 +2115,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2372
2115
|
heightKey: resizableKinds.has(model.kind) ? model.kind : undefined,
|
|
2373
2116
|
height: self.trackHeight(model.kind),
|
|
2374
2117
|
},
|
|
2375
|
-
ReactComponent: TrackBlocks,
|
|
2376
2118
|
}));
|
|
2377
2119
|
},
|
|
2378
2120
|
get tracks() {
|
|
@@ -2389,7 +2131,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2389
2131
|
* #getter
|
|
2390
2132
|
*/
|
|
2391
2133
|
get turnedOnTracks() {
|
|
2392
|
-
return this.tracks.filter(f => !trackIsOff(self.turnedOffTracks, f.model.id
|
|
2134
|
+
return this.tracks.filter(f => !trackIsOff(self.turnedOffTracks, f.model.id));
|
|
2393
2135
|
},
|
|
2394
2136
|
/**
|
|
2395
2137
|
* #getter
|
|
@@ -2408,7 +2150,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2408
2150
|
* @returns The character at that position
|
|
2409
2151
|
*/
|
|
2410
2152
|
visibleColToRowLetter(rowName, visibleCol) {
|
|
2411
|
-
return self.
|
|
2153
|
+
return self.rowSequence(rowName)?.[this.visibleColToGlobalCol(visibleCol)];
|
|
2412
2154
|
},
|
|
2413
2155
|
/**
|
|
2414
2156
|
* #method
|
|
@@ -2483,7 +2225,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2483
2225
|
* @returns The global column index in the full MSA, or undefined
|
|
2484
2226
|
*/
|
|
2485
2227
|
seqPosToGlobalCol(rowName, seqPos) {
|
|
2486
|
-
const seq = self.
|
|
2228
|
+
const seq = self.rowSequence(rowName);
|
|
2487
2229
|
if (seq === undefined) {
|
|
2488
2230
|
return undefined;
|
|
2489
2231
|
}
|
|
@@ -2678,7 +2420,9 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2678
2420
|
if (mapping.structure.id !== structureId) {
|
|
2679
2421
|
continue;
|
|
2680
2422
|
}
|
|
2681
|
-
if (asymId !== undefined &&
|
|
2423
|
+
if (asymId !== undefined &&
|
|
2424
|
+
mapping.structure.asymId !== undefined &&
|
|
2425
|
+
mapping.structure.asymId !== asymId) {
|
|
2682
2426
|
continue;
|
|
2683
2427
|
}
|
|
2684
2428
|
const segment = mapping.segments.find(seg => position >= seg.structStart && position <= seg.structEnd);
|
|
@@ -2695,10 +2439,14 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2695
2439
|
.views(self => ({
|
|
2696
2440
|
/**
|
|
2697
2441
|
* #getter
|
|
2698
|
-
* the branch-length scale bar over the tree, undefined
|
|
2699
|
-
* or when the tree area is
|
|
2442
|
+
* the branch-length scale bar over the tree, undefined while
|
|
2443
|
+
* `showTreeScaleBar` is off, in cladogram mode, or when the tree area is
|
|
2444
|
+
* too narrow for one
|
|
2700
2445
|
*/
|
|
2701
2446
|
get treeScaleBar() {
|
|
2447
|
+
if (!self.showTreeScaleBar) {
|
|
2448
|
+
return undefined;
|
|
2449
|
+
}
|
|
2702
2450
|
return scaleBarLength(self.pxPerBranchLength, self.effectiveTreeAreaWidth - self.marginLeft * 2);
|
|
2703
2451
|
},
|
|
2704
2452
|
/**
|
|
@@ -2745,11 +2493,42 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2745
2493
|
}
|
|
2746
2494
|
return types;
|
|
2747
2495
|
},
|
|
2496
|
+
/**
|
|
2497
|
+
* accessions hidden until the user turns them on, such as a Stockholm
|
|
2498
|
+
* file's #=GR lines
|
|
2499
|
+
*/
|
|
2500
|
+
get defaultOffAccessions() {
|
|
2501
|
+
return new Set(self.allAnnotations.filter(a => a.defaultOff).map(a => a.accession));
|
|
2502
|
+
},
|
|
2503
|
+
/**
|
|
2504
|
+
* #method
|
|
2505
|
+
*/
|
|
2506
|
+
featureOff(accession) {
|
|
2507
|
+
return (self.turnedOffFeatures.get(accession) ??
|
|
2508
|
+
this.defaultOffAccessions.has(accession));
|
|
2509
|
+
},
|
|
2748
2510
|
get filteredAnnotations() {
|
|
2749
|
-
return self.allAnnotations.filter(r => !
|
|
2511
|
+
return self.allAnnotations.filter(r => !this.featureOff(r.accession));
|
|
2512
|
+
},
|
|
2513
|
+
/**
|
|
2514
|
+
* #getter
|
|
2515
|
+
* what colors the alignment's cells: the residue scheme, the domains
|
|
2516
|
+
* covering them, or nothing, which leaves the scheme to the letters.
|
|
2517
|
+
* Domains own the fill only while some are drawn, so a file whose
|
|
2518
|
+
* annotations all start hidden keeps its residue colors
|
|
2519
|
+
*/
|
|
2520
|
+
get cellFill() {
|
|
2521
|
+
return !self.bgColor
|
|
2522
|
+
? 'none'
|
|
2523
|
+
: self.actuallyShowDomains &&
|
|
2524
|
+
self.domainFill &&
|
|
2525
|
+
this.filteredAnnotations.length > 0
|
|
2526
|
+
? 'domain'
|
|
2527
|
+
: 'residue';
|
|
2750
2528
|
},
|
|
2751
2529
|
get annotationsByRow() {
|
|
2752
|
-
|
|
2530
|
+
const { rowNamesSet } = self;
|
|
2531
|
+
return groupBy(this.filteredAnnotations, r => annotationRowName(r.id, rowNamesSet));
|
|
2753
2532
|
},
|
|
2754
2533
|
}))
|
|
2755
2534
|
.views(self => ({
|
|
@@ -2821,6 +2600,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2821
2600
|
get blocks2d() {
|
|
2822
2601
|
return self.blocksY.flatMap(by => self.blocksX.map(bx => [bx, by]));
|
|
2823
2602
|
},
|
|
2603
|
+
/**
|
|
2604
|
+
* #getter
|
|
2605
|
+
*/
|
|
2606
|
+
get wheelZooming() {
|
|
2607
|
+
return self.zoomSnapshot !== undefined;
|
|
2608
|
+
},
|
|
2824
2609
|
/**
|
|
2825
2610
|
* #getter
|
|
2826
2611
|
*/
|
|
@@ -2837,7 +2622,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2837
2622
|
const { annotations, rowNamesSet } = self;
|
|
2838
2623
|
return self.dataInitialized &&
|
|
2839
2624
|
annotations.length > 0 &&
|
|
2840
|
-
!annotations.some(a => rowNamesSet.has(a.id))
|
|
2625
|
+
!annotations.some(a => rowNamesSet.has(annotationRowName(a.id, rowNamesSet)))
|
|
2841
2626
|
? [
|
|
2842
2627
|
`0 of ${annotations.length} annotations name a row in this alignment`,
|
|
2843
2628
|
]
|
|
@@ -2962,10 +2747,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2962
2747
|
* #action
|
|
2963
2748
|
* swap in a different tree over the same alignment. Clears `collapsed`,
|
|
2964
2749
|
* `rotated` and `showOnly`, since path-derived node ids (node-0-0-1) from the old
|
|
2965
|
-
* tree would match unrelated nodes in the new one
|
|
2750
|
+
* tree would match unrelated nodes in the new one, and the tree's
|
|
2751
|
+
* filehandle, which the snapshot would otherwise reopen in place of
|
|
2752
|
+
* the new tree
|
|
2966
2753
|
*/
|
|
2967
2754
|
replaceTree(newick) {
|
|
2968
2755
|
transaction(() => {
|
|
2756
|
+
self.setTreeFilehandle(undefined);
|
|
2969
2757
|
self.collapsed.clear();
|
|
2970
2758
|
self.rotated.clear();
|
|
2971
2759
|
self.setShowOnly(undefined);
|
|
@@ -2987,30 +2775,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
2987
2775
|
zoomAtCenter(scaleFactor, axis = 'both') {
|
|
2988
2776
|
this.zoomToPos(scaleFactor, self.msaCanvasWidth / 2, self.msaAreaHeight / 2, axis);
|
|
2989
2777
|
},
|
|
2990
|
-
/**
|
|
2991
|
-
* #action
|
|
2992
|
-
*/
|
|
2993
|
-
zoomOutHorizontal() {
|
|
2994
|
-
this.zoomAtCenter(0.75, 'horizontal');
|
|
2995
|
-
},
|
|
2996
|
-
/**
|
|
2997
|
-
* #action
|
|
2998
|
-
*/
|
|
2999
|
-
zoomInHorizontal() {
|
|
3000
|
-
this.zoomAtCenter(1.5, 'horizontal');
|
|
3001
|
-
},
|
|
3002
|
-
/**
|
|
3003
|
-
* #action
|
|
3004
|
-
*/
|
|
3005
|
-
zoomInVertical() {
|
|
3006
|
-
this.zoomAtCenter(1.5, 'vertical');
|
|
3007
|
-
},
|
|
3008
|
-
/**
|
|
3009
|
-
* #action
|
|
3010
|
-
*/
|
|
3011
|
-
zoomOutVertical() {
|
|
3012
|
-
this.zoomAtCenter(0.75, 'vertical');
|
|
3013
|
-
},
|
|
3014
2778
|
/**
|
|
3015
2779
|
* #action
|
|
3016
2780
|
*/
|
|
@@ -3038,11 +2802,20 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3038
2802
|
transaction(() => {
|
|
3039
2803
|
const colInView = (-self.scrollX + offsetX) / self.colWidth;
|
|
3040
2804
|
const rowInView = (-self.scrollY + offsetY) / self.rowHeight;
|
|
2805
|
+
// zooming both axes, an axis holds at the default only while the
|
|
2806
|
+
// other sits there too, or one would hold while the other grows
|
|
2807
|
+
const canHold = axis !== 'both' ||
|
|
2808
|
+
(self.colWidth === defaultColWidth &&
|
|
2809
|
+
self.rowHeight === defaultRowHeight);
|
|
3041
2810
|
if (axis !== 'vertical') {
|
|
3042
|
-
|
|
2811
|
+
const { value, held } = detentZoom(self.colWidth, scaleFactor, defaultColWidth, self.zoomHeld.col, minColWidth, canHold);
|
|
2812
|
+
self.colWidth = value;
|
|
2813
|
+
self.zoomHeld = { ...self.zoomHeld, col: held };
|
|
3043
2814
|
}
|
|
3044
2815
|
if (axis !== 'horizontal') {
|
|
3045
|
-
|
|
2816
|
+
const { value, held } = detentZoom(self.rowHeight, scaleFactor, defaultRowHeight, self.zoomHeld.row, minRowHeight, canHold);
|
|
2817
|
+
self.rowHeight = value;
|
|
2818
|
+
self.zoomHeld = { ...self.zoomHeld, row: held };
|
|
3046
2819
|
}
|
|
3047
2820
|
self.scrollX = clamp(offsetX - colInView * self.colWidth, self.maxScrollX, 0);
|
|
3048
2821
|
const anchoredScrollY = offsetY - rowInView * self.rowHeight;
|
|
@@ -3053,6 +2826,25 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3053
2826
|
self.scrollY = clamp(anchoredScrollY * (1 - topBias), self.maxScrollY, 0);
|
|
3054
2827
|
});
|
|
3055
2828
|
},
|
|
2829
|
+
/**
|
|
2830
|
+
* #action
|
|
2831
|
+
* zoomToPos for one wheel or pinch frame. The alignment keeps the blocks
|
|
2832
|
+
* it drew at the start of the gesture, scaled, and redraws them only once
|
|
2833
|
+
* they stop covering the viewport; endWheelZoom redraws them sharp
|
|
2834
|
+
*/
|
|
2835
|
+
wheelZoomToPos(scaleFactor, offsetX, offsetY) {
|
|
2836
|
+
self.zoomSnapshot ??= takeZoomSnapshot(self, false);
|
|
2837
|
+
this.zoomToPos(scaleFactor, offsetX, offsetY, self.wheelZoomAxis);
|
|
2838
|
+
if (!zoomSnapshotCovers(self, self.zoomSnapshot)) {
|
|
2839
|
+
self.zoomSnapshot = takeZoomSnapshot(self, true);
|
|
2840
|
+
}
|
|
2841
|
+
},
|
|
2842
|
+
/**
|
|
2843
|
+
* #action
|
|
2844
|
+
*/
|
|
2845
|
+
endWheelZoom() {
|
|
2846
|
+
self.zoomSnapshot = undefined;
|
|
2847
|
+
},
|
|
3056
2848
|
/**
|
|
3057
2849
|
* #action
|
|
3058
2850
|
*/
|
|
@@ -3077,15 +2869,6 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3077
2869
|
setAnnotations(annotations) {
|
|
3078
2870
|
self.annotations = annotations;
|
|
3079
2871
|
},
|
|
3080
|
-
/**
|
|
3081
|
-
* #action
|
|
3082
|
-
* set the overlay from raw InterProScan results keyed by row name. Kept
|
|
3083
|
-
* for downstream plugins that hold the EBI wire format; new code should
|
|
3084
|
-
* adapt to Annotation[] and call setAnnotations.
|
|
3085
|
-
*/
|
|
3086
|
-
setDomains(data) {
|
|
3087
|
-
this.setAnnotations(data ? interProScanToAnnotations(data) : []);
|
|
3088
|
-
},
|
|
3089
2872
|
applyGFFText(gffText) {
|
|
3090
2873
|
this.setAnnotations(gffToAnnotations(parseGFF(gffText)));
|
|
3091
2874
|
},
|
|
@@ -3140,8 +2923,23 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3140
2923
|
* #action
|
|
3141
2924
|
*/
|
|
3142
2925
|
toggleTrack(id) {
|
|
3143
|
-
|
|
3144
|
-
|
|
2926
|
+
self.turnedOffTracks.set(id, !trackIsOff(self.turnedOffTracks, id));
|
|
2927
|
+
},
|
|
2928
|
+
/**
|
|
2929
|
+
* #action
|
|
2930
|
+
* show or hide several tracks at once, as the track selector's Show all
|
|
2931
|
+
* and Hide all do. A choice matching the track's default is dropped, so
|
|
2932
|
+
* it stays out of the shared URL
|
|
2933
|
+
*/
|
|
2934
|
+
setTracksShown(ids, shown) {
|
|
2935
|
+
for (const id of ids) {
|
|
2936
|
+
if (defaultOffTracks.has(id) === !shown) {
|
|
2937
|
+
self.turnedOffTracks.delete(id);
|
|
2938
|
+
}
|
|
2939
|
+
else {
|
|
2940
|
+
self.turnedOffTracks.set(id, !shown);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
3145
2943
|
},
|
|
3146
2944
|
/**
|
|
3147
2945
|
* #action
|
|
@@ -3166,7 +2964,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3166
2964
|
column: column + 1,
|
|
3167
2965
|
row,
|
|
3168
2966
|
residue: self.visibleColToSeqPosOneBased(row, visibleCol),
|
|
3169
|
-
letter: self.
|
|
2967
|
+
letter: self.rowSequence(row)?.[column],
|
|
3170
2968
|
};
|
|
3171
2969
|
},
|
|
3172
2970
|
/**
|
|
@@ -3287,7 +3085,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3287
3085
|
*/
|
|
3288
3086
|
get visibleDomainTypes() {
|
|
3289
3087
|
return this.categoricalDomainTypes
|
|
3290
|
-
.filter(d => !self.
|
|
3088
|
+
.filter(d => !self.featureOff(d.accession))
|
|
3291
3089
|
.toSorted((a, b) => a.start - b.start);
|
|
3292
3090
|
},
|
|
3293
3091
|
/**
|
|
@@ -3331,7 +3129,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3331
3129
|
: visibleDomainTypes.map(d => ({
|
|
3332
3130
|
id: d.accession,
|
|
3333
3131
|
label: d.name,
|
|
3334
|
-
color: fillPalette[d.accession],
|
|
3132
|
+
color: d.color ?? fillPalette[d.accession],
|
|
3335
3133
|
})), 'domains');
|
|
3336
3134
|
}
|
|
3337
3135
|
for (const { channel, field, legend } of this.resolvedEncodings) {
|
|
@@ -3347,15 +3145,15 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3347
3145
|
/**
|
|
3348
3146
|
* #getter
|
|
3349
3147
|
* whether the overlay marks each domain with a bar under its row instead
|
|
3350
|
-
* of filling the row behind the letters
|
|
3351
|
-
*
|
|
3352
|
-
*
|
|
3148
|
+
* of filling the row behind the letters: whenever the domains do not own
|
|
3149
|
+
* the cell fill (`cellFill`), a filled box would paint over the residue
|
|
3150
|
+
* colors the user picked. Sub-row layout already stacks
|
|
3353
3151
|
* the boxes clear of the letters, and with the letters too small to draw
|
|
3354
3152
|
* the filled box is the only thing left to read.
|
|
3355
3153
|
*/
|
|
3356
3154
|
get domainUnderline() {
|
|
3357
3155
|
return (self.actuallyShowDomains &&
|
|
3358
|
-
|
|
3156
|
+
self.cellFill !== 'domain' &&
|
|
3359
3157
|
!self.subFeatureRows &&
|
|
3360
3158
|
self.showMsaLetters);
|
|
3361
3159
|
},
|
|
@@ -3567,7 +3365,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3567
3365
|
* to `end`, gaps included
|
|
3568
3366
|
*/
|
|
3569
3367
|
get selectionFasta() {
|
|
3570
|
-
const { rowNames
|
|
3368
|
+
const { rowNames } = self;
|
|
3571
3369
|
const resolved = this.resolvedSelection;
|
|
3572
3370
|
const span = this.selectionFileSpan;
|
|
3573
3371
|
if (!resolved || !span) {
|
|
@@ -3576,7 +3374,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3576
3374
|
const rows = resolved.rowRuns
|
|
3577
3375
|
.flatMap(([first, last]) => rowNames.slice(first, last + 1))
|
|
3578
3376
|
.flatMap(name => {
|
|
3579
|
-
const seq =
|
|
3377
|
+
const seq = self.rowSequence(name);
|
|
3580
3378
|
return seq === undefined ? [] : [[name, seq]];
|
|
3581
3379
|
});
|
|
3582
3380
|
return blockFasta(rows, span.start, span.end);
|
|
@@ -3655,7 +3453,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3655
3453
|
this.visibleDomainTypes.map(d => ({
|
|
3656
3454
|
id: d.accession,
|
|
3657
3455
|
label: d.name,
|
|
3658
|
-
color: fillPalette[d.accession],
|
|
3456
|
+
color: d.color ?? fillPalette[d.accession],
|
|
3659
3457
|
})),
|
|
3660
3458
|
};
|
|
3661
3459
|
}
|
|
@@ -3804,6 +3602,18 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3804
3602
|
setHideHeader(arg) {
|
|
3805
3603
|
self.hideHeader = arg;
|
|
3806
3604
|
},
|
|
3605
|
+
/**
|
|
3606
|
+
* #action
|
|
3607
|
+
*/
|
|
3608
|
+
setEmbeddedMode(arg) {
|
|
3609
|
+
self.embeddedMode = arg;
|
|
3610
|
+
},
|
|
3611
|
+
/**
|
|
3612
|
+
* #action
|
|
3613
|
+
*/
|
|
3614
|
+
setImportForm(arg) {
|
|
3615
|
+
self.importForm = arg;
|
|
3616
|
+
},
|
|
3807
3617
|
/**
|
|
3808
3618
|
* #action
|
|
3809
3619
|
* focus the subtree a click `y` pixels down the tree overview lands on.
|
|
@@ -3862,15 +3672,16 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3862
3672
|
},
|
|
3863
3673
|
/**
|
|
3864
3674
|
* #action
|
|
3865
|
-
* show or hide an annotation type. Only
|
|
3866
|
-
* `turnedOffFeatures`
|
|
3675
|
+
* show or hide an annotation type. Only a choice that differs from the
|
|
3676
|
+
* type's default is recorded; see `turnedOffFeatures`
|
|
3867
3677
|
*/
|
|
3868
3678
|
setFilter(accession, shown) {
|
|
3869
|
-
|
|
3870
|
-
|
|
3679
|
+
// only a departure from the accession's default is recorded
|
|
3680
|
+
if (shown === self.defaultOffAccessions.has(accession)) {
|
|
3681
|
+
self.turnedOffFeatures.set(accession, !shown);
|
|
3871
3682
|
}
|
|
3872
3683
|
else {
|
|
3873
|
-
self.turnedOffFeatures.
|
|
3684
|
+
self.turnedOffFeatures.delete(accession);
|
|
3874
3685
|
}
|
|
3875
3686
|
},
|
|
3876
3687
|
/**
|
|
@@ -3878,20 +3689,23 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3878
3689
|
*/
|
|
3879
3690
|
fit() {
|
|
3880
3691
|
// two passes: each direction's viewport depends on whether the other
|
|
3881
|
-
// overflows (the minimap takes height, the vertical scrollbar width)
|
|
3692
|
+
// overflows (the minimap takes height, the vertical scrollbar width).
|
|
3693
|
+
// Fit shrinks an alignment into the window but never grows one past
|
|
3694
|
+
// the default cell size, which would stretch a small alignment's rows
|
|
3695
|
+
// and columns out of proportion
|
|
3882
3696
|
transaction(() => {
|
|
3883
3697
|
for (let pass = 0; pass < 2; pass++) {
|
|
3884
|
-
this.fitHorizontally();
|
|
3885
|
-
this.fitVertically();
|
|
3698
|
+
this.fitHorizontally(defaultColWidth);
|
|
3699
|
+
this.fitVertically(defaultRowHeight);
|
|
3886
3700
|
}
|
|
3887
3701
|
});
|
|
3888
3702
|
},
|
|
3889
3703
|
/**
|
|
3890
3704
|
* #action
|
|
3891
3705
|
*/
|
|
3892
|
-
fitVertically() {
|
|
3706
|
+
fitVertically(maxRowHeight = maxCellSize) {
|
|
3893
3707
|
if (self.numRows > 0) {
|
|
3894
|
-
self.
|
|
3708
|
+
self.setRowHeight(clamp(self.msaAreaHeight / self.numRows, minRowHeight, maxRowHeight));
|
|
3895
3709
|
}
|
|
3896
3710
|
self.scrollY = 0;
|
|
3897
3711
|
},
|
|
@@ -3907,10 +3721,28 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3907
3721
|
return;
|
|
3908
3722
|
}
|
|
3909
3723
|
transaction(() => {
|
|
3910
|
-
self.
|
|
3724
|
+
self.setColWidth(clamp(self.msaCanvasWidth / (span.endCol - span.startCol + 1), minColWidth, maxCellSize));
|
|
3911
3725
|
self.scrollX = clamp(-span.startCol * self.colWidth, self.maxScrollX, 0);
|
|
3912
3726
|
});
|
|
3913
3727
|
},
|
|
3728
|
+
/**
|
|
3729
|
+
* #action
|
|
3730
|
+
* zoomToRegion, and for a region naming a row, also scroll that row to
|
|
3731
|
+
* the middle of the alignment, as Go to does. What `region` opens on; a
|
|
3732
|
+
* caller following a range as it moves wants zoomToRegion alone, which
|
|
3733
|
+
* leaves the reader's vertical scroll where they put it.
|
|
3734
|
+
*/
|
|
3735
|
+
revealRegion(region) {
|
|
3736
|
+
transaction(() => {
|
|
3737
|
+
this.zoomToRegion(region);
|
|
3738
|
+
const row = region.row === undefined
|
|
3739
|
+
? undefined
|
|
3740
|
+
: self.rowNamesSet.get(region.row);
|
|
3741
|
+
if (row !== undefined && self.viewInitialized) {
|
|
3742
|
+
self.setScrollY(-(row + 0.5) * self.rowHeight + self.msaAreaHeight / 2);
|
|
3743
|
+
}
|
|
3744
|
+
});
|
|
3745
|
+
},
|
|
3914
3746
|
/**
|
|
3915
3747
|
* #action
|
|
3916
3748
|
* zoom and scroll so the selected columns fill the alignment's width,
|
|
@@ -3927,7 +3759,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3927
3759
|
const { rowRuns } = resolvedSelection;
|
|
3928
3760
|
const first = rowRuns[0][0];
|
|
3929
3761
|
const last = rowRuns.at(-1)[1];
|
|
3930
|
-
self.
|
|
3762
|
+
self.setRowHeight(clamp(self.msaAreaHeight / (last - first + 1), minRowHeight, maxCellSize));
|
|
3931
3763
|
self.scrollY = clamp(-first * self.rowHeight, self.maxScrollY, 0);
|
|
3932
3764
|
}
|
|
3933
3765
|
});
|
|
@@ -3959,10 +3791,10 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3959
3791
|
/**
|
|
3960
3792
|
* #action
|
|
3961
3793
|
*/
|
|
3962
|
-
fitHorizontally() {
|
|
3794
|
+
fitHorizontally(maxColWidth = maxCellSize) {
|
|
3963
3795
|
if (self.numColumns > 0) {
|
|
3964
3796
|
// msaCanvasWidth excludes the vertical scrollbar's 20px
|
|
3965
|
-
self.
|
|
3797
|
+
self.setColWidth(clamp(self.msaCanvasWidth / self.numColumns, minColWidth, maxColWidth));
|
|
3966
3798
|
}
|
|
3967
3799
|
self.scrollX = 0;
|
|
3968
3800
|
},
|
|
@@ -3970,17 +3802,13 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
3970
3802
|
if (self.highlightColumns?.length) {
|
|
3971
3803
|
self.setHighlightedColumns(self.highlightColumns);
|
|
3972
3804
|
}
|
|
3973
|
-
|
|
3974
|
-
//
|
|
3975
|
-
// `
|
|
3976
|
-
//
|
|
3977
|
-
//
|
|
3978
|
-
//
|
|
3979
|
-
|
|
3980
|
-
// `dataInitialized` is true once the MSA alone has loaded, when the
|
|
3981
|
-
// tree is still the flat stub, so a tree filehandle holds the seeding
|
|
3982
|
-
// until its text lands.
|
|
3983
|
-
addDisposer(self, reaction(() => self.dataInitialized && !(self.treeFilehandle && !self.data.tree)
|
|
3805
|
+
const awaitingTree = () => !!self.treeFilehandle && !self.data.tree;
|
|
3806
|
+
// `collapse`, `focus` and `rotate` marks seed `collapsed`, `showOnly`
|
|
3807
|
+
// and `rotated`. Seeding runs once per data load, `clades` change and
|
|
3808
|
+
// reroot, so expanding a seeded clade sticks until one of those. A tree
|
|
3809
|
+
// filehandle holds it back, since `dataInitialized` is true while the
|
|
3810
|
+
// tree is still the flat stub.
|
|
3811
|
+
addDisposer(self, reaction(() => self.dataInitialized && !awaitingTree()
|
|
3984
3812
|
? { clades: getSnapshot(self.clades), treeRoot: self.treeRoot }
|
|
3985
3813
|
: undefined, seed => {
|
|
3986
3814
|
if (!seed?.clades.length) {
|
|
@@ -4010,8 +3838,8 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
4010
3838
|
if (region &&
|
|
4011
3839
|
self.viewInitialized &&
|
|
4012
3840
|
self.numColumns > 0 &&
|
|
4013
|
-
!(
|
|
4014
|
-
this.
|
|
3841
|
+
!awaitingTree()) {
|
|
3842
|
+
this.revealRegion(region);
|
|
4015
3843
|
self.setRegion(undefined);
|
|
4016
3844
|
}
|
|
4017
3845
|
}));
|
|
@@ -4032,87 +3860,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
4032
3860
|
mql.removeEventListener('change', onChange);
|
|
4033
3861
|
});
|
|
4034
3862
|
}
|
|
4035
|
-
|
|
4036
|
-
* Fetch a filehandle whenever it changes, and hand the text to
|
|
4037
|
-
* `onLoad`.
|
|
4038
|
-
*
|
|
4039
|
-
* A generation guard keeps a slower superseded request from overwriting
|
|
4040
|
-
* the newer one's data, status or loading flag. A superseded or cleared
|
|
4041
|
-
* request is aborted and clears its status line.
|
|
4042
|
-
*
|
|
4043
|
-
* `clearFilehandle` runs after a local file loads, since a blob has no
|
|
4044
|
-
* URL to refetch, and when the user cancels, which returns the view to
|
|
4045
|
-
* the import form.
|
|
4046
|
-
*
|
|
4047
|
-
* `what` names the layer in a failure message. An `optional` layer
|
|
4048
|
-
* (annotations, row metadata) that fails adds a warning instead of an
|
|
4049
|
-
* error.
|
|
4050
|
-
*/
|
|
4051
|
-
const loadOnFilehandleChange = ({ what, getFilehandle, onLoad, setLoading, clearFilehandle, optional = false, }) => {
|
|
4052
|
-
let generation = 0;
|
|
4053
|
-
let cancelInFlight;
|
|
4054
|
-
addDisposer(self, () => {
|
|
4055
|
-
cancelInFlight?.();
|
|
4056
|
-
});
|
|
4057
|
-
addDisposer(self, autorun(async () => {
|
|
4058
|
-
const filehandle = getFilehandle();
|
|
4059
|
-
const current = ++generation;
|
|
4060
|
-
cancelInFlight?.();
|
|
4061
|
-
cancelInFlight = undefined;
|
|
4062
|
-
if (!filehandle) {
|
|
4063
|
-
setLoading?.(false);
|
|
4064
|
-
return;
|
|
4065
|
-
}
|
|
4066
|
-
const isCurrent = () => current === generation && isAlive(self);
|
|
4067
|
-
const controller = new AbortController();
|
|
4068
|
-
cancelInFlight = () => {
|
|
4069
|
-
controller.abort();
|
|
4070
|
-
if (isAlive(self)) {
|
|
4071
|
-
self.setStatus(undefined);
|
|
4072
|
-
}
|
|
4073
|
-
};
|
|
4074
|
-
try {
|
|
4075
|
-
setLoading?.(true);
|
|
4076
|
-
self.setError(undefined);
|
|
4077
|
-
const text = await fetchTextWithProgress(openLocation(filehandle), status => {
|
|
4078
|
-
if (isCurrent()) {
|
|
4079
|
-
self.setStatus(status);
|
|
4080
|
-
}
|
|
4081
|
-
}, { controller });
|
|
4082
|
-
if (isCurrent()) {
|
|
4083
|
-
transaction(() => {
|
|
4084
|
-
onLoad(text);
|
|
4085
|
-
if (filehandle.locationType === 'BlobLocation') {
|
|
4086
|
-
clearFilehandle?.();
|
|
4087
|
-
}
|
|
4088
|
-
});
|
|
4089
|
-
}
|
|
4090
|
-
}
|
|
4091
|
-
catch (e) {
|
|
4092
|
-
if (isCurrent()) {
|
|
4093
|
-
if (isAbortError(e)) {
|
|
4094
|
-
clearFilehandle?.();
|
|
4095
|
-
}
|
|
4096
|
-
else {
|
|
4097
|
-
console.error(e);
|
|
4098
|
-
if (optional) {
|
|
4099
|
-
self.addWarning(`The ${what} did not load: ${e}`);
|
|
4100
|
-
}
|
|
4101
|
-
else {
|
|
4102
|
-
self.setError(e);
|
|
4103
|
-
}
|
|
4104
|
-
}
|
|
4105
|
-
}
|
|
4106
|
-
}
|
|
4107
|
-
finally {
|
|
4108
|
-
if (isCurrent()) {
|
|
4109
|
-
cancelInFlight = undefined;
|
|
4110
|
-
setLoading?.(false);
|
|
4111
|
-
}
|
|
4112
|
-
}
|
|
4113
|
-
}));
|
|
4114
|
-
};
|
|
4115
|
-
loadOnFilehandleChange({
|
|
3863
|
+
installFilehandleLoader(self, {
|
|
4116
3864
|
what: 'tree',
|
|
4117
3865
|
getFilehandle: () => self.treeFilehandle,
|
|
4118
3866
|
onLoad: text => {
|
|
@@ -4126,7 +3874,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
4126
3874
|
},
|
|
4127
3875
|
});
|
|
4128
3876
|
// treeMetadata has no import-form step, so no loading flag or cancel
|
|
4129
|
-
|
|
3877
|
+
installFilehandleLoader(self, {
|
|
4130
3878
|
what: 'row metadata',
|
|
4131
3879
|
optional: true,
|
|
4132
3880
|
getFilehandle: () => self.treeMetadataFilehandle,
|
|
@@ -4155,7 +3903,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
4155
3903
|
}
|
|
4156
3904
|
}));
|
|
4157
3905
|
// gffFilehandle loads into data.gff, which the autorun above parses
|
|
4158
|
-
|
|
3906
|
+
installFilehandleLoader(self, {
|
|
4159
3907
|
what: 'annotations',
|
|
4160
3908
|
optional: true,
|
|
4161
3909
|
getFilehandle: () => self.gffFilehandle,
|
|
@@ -4166,7 +3914,7 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
4166
3914
|
self.setGFFFilehandle(undefined);
|
|
4167
3915
|
},
|
|
4168
3916
|
});
|
|
4169
|
-
|
|
3917
|
+
installFilehandleLoader(self, {
|
|
4170
3918
|
what: 'alignment',
|
|
4171
3919
|
getFilehandle: () => self.msaFilehandle,
|
|
4172
3920
|
onLoad: text => {
|
|
@@ -4187,20 +3935,12 @@ function stateModelFactory({ maxInlineSnapshotBytes = defaultMaxInlineSnapshotBy
|
|
|
4187
3935
|
// xref problem https://github.com/GMOD/react-msaview/issues/75
|
|
4188
3936
|
addDisposer(self, autorun(() => {
|
|
4189
3937
|
if (self.dynamicColorSchemeName) {
|
|
4190
|
-
|
|
4191
|
-
self.colStats;
|
|
3938
|
+
readComputed(self.colStats);
|
|
4192
3939
|
}
|
|
4193
3940
|
if (self.showColumnStats) {
|
|
4194
|
-
|
|
4195
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
4196
|
-
self.colStats;
|
|
4197
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
4198
|
-
self.conservation;
|
|
4199
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
4200
|
-
self.propertyConservation;
|
|
3941
|
+
readComputed(self.colStats, self.conservation, self.propertyConservation);
|
|
4201
3942
|
}
|
|
4202
|
-
|
|
4203
|
-
self.columns;
|
|
3943
|
+
readComputed(self.columns);
|
|
4204
3944
|
}));
|
|
4205
3945
|
},
|
|
4206
3946
|
}))
|