jbrowse-plugin-graphgenomeviewer 1.0.0 → 3.0.3
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/LICENSE +674 -0
- package/README.md +202 -24
- package/dist/chunks/AddTrackWorkflow-Y3OBHM33.js +2 -0
- package/dist/chunks/AddTrackWorkflow-Y3OBHM33.js.map +7 -0
- package/dist/chunks/GbzBaseSyntenyAdapter-EFIOI2SN.js +8 -0
- package/dist/chunks/GbzBaseSyntenyAdapter-EFIOI2SN.js.map +7 -0
- package/dist/chunks/GraphGenomeView-UD5WCV3Q.js +14 -0
- package/dist/chunks/GraphGenomeView-UD5WCV3Q.js.map +7 -0
- package/dist/chunks/MinigraphBubbleAdapter-B75VQK7T.js +2 -0
- package/dist/chunks/MinigraphBubbleAdapter-B75VQK7T.js.map +7 -0
- package/dist/chunks/RgfaTabixAdapter-IT7I5RR3.js +3 -0
- package/dist/chunks/RgfaTabixAdapter-IT7I5RR3.js.map +7 -0
- package/dist/chunks/bandage-layout-FIOZ7B3E.js +1660 -0
- package/dist/chunks/bandage-layout-FIOZ7B3E.js.map +7 -0
- package/dist/chunks/chunk-D7XFR2IV.js +2 -0
- package/dist/chunks/chunk-D7XFR2IV.js.map +7 -0
- package/dist/chunks/chunk-F4C3BEPV.js +2 -0
- package/dist/chunks/chunk-F4C3BEPV.js.map +7 -0
- package/dist/chunks/chunk-FCWXYIFC.js +2 -0
- package/dist/chunks/chunk-FCWXYIFC.js.map +7 -0
- package/dist/chunks/chunk-FY3BTKY4.js +2 -0
- package/dist/chunks/chunk-FY3BTKY4.js.map +7 -0
- package/dist/chunks/chunk-J4OLMLS5.js +2 -0
- package/dist/chunks/chunk-J4OLMLS5.js.map +7 -0
- package/dist/chunks/chunk-JCG7RLMB.js +2 -0
- package/dist/chunks/chunk-JCG7RLMB.js.map +7 -0
- package/dist/chunks/chunk-KWAXLUPQ.js +2 -0
- package/dist/chunks/chunk-KWAXLUPQ.js.map +7 -0
- package/dist/chunks/chunk-LGKM2PZH.js +2 -0
- package/dist/chunks/chunk-LGKM2PZH.js.map +7 -0
- package/dist/chunks/chunk-TN6FK5UD.js +2 -0
- package/dist/chunks/chunk-TN6FK5UD.js.map +7 -0
- package/dist/chunks/chunk-X3X74OGB.js +2 -0
- package/dist/chunks/chunk-X3X74OGB.js.map +7 -0
- package/dist/chunks/chunk-XEJ47DLD.js +2 -0
- package/dist/chunks/chunk-XEJ47DLD.js.map +7 -0
- package/dist/chunks/chunk-ZNGDWZ6Z.js +2 -0
- package/dist/chunks/chunk-ZNGDWZ6Z.js.map +7 -0
- package/dist/chunks/executeDiagonalize-XMPEDHVZ.js +2 -0
- package/dist/chunks/executeDiagonalize-XMPEDHVZ.js.map +7 -0
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +3 -0
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +7 -0
- package/package.json +94 -49
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.test.ts +427 -0
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.ts +461 -0
- package/src/GbzBaseSyntenyAdapter/configSchema.ts +181 -0
- package/src/GbzBaseSyntenyAdapter/index.ts +27 -0
- package/src/GbzBaseSyntenyAdapter/test_data/micb-kir3dl1.gbz.db +0 -0
- package/src/GbzBaseSyntenyAdapter/test_data/micb-kir3dl1.haplotype-index.db +0 -0
- package/src/GetSubgraph.test.ts +193 -0
- package/src/GetSubgraph.ts +93 -0
- package/src/GraphAddTrackWorkflow/AddTrackWorkflow.tsx +167 -0
- package/src/GraphAddTrackWorkflow/buildTrackConfig.test.ts +150 -0
- package/src/GraphAddTrackWorkflow/buildTrackConfig.ts +114 -0
- package/src/GraphAddTrackWorkflow/index.ts +20 -0
- package/src/GraphComputeLayout.ts +46 -0
- package/src/GraphGenomeView/anchoredNodes.ts +101 -0
- package/src/GraphGenomeView/bubbleSpreads.ts +102 -0
- package/src/GraphGenomeView/bubbles/bubbleHalos.test.ts +58 -0
- package/src/GraphGenomeView/bubbles/bubbleHalos.ts +149 -0
- package/src/GraphGenomeView/bubbles/bubbleRoutes.test.ts +141 -0
- package/src/GraphGenomeView/bubbles/bubblesFromGraph.test.ts +239 -0
- package/src/GraphGenomeView/bubbles/bubblesFromGraph.ts +210 -0
- package/src/GraphGenomeView/bubbles/classifyBubble.test.ts +172 -0
- package/src/GraphGenomeView/bubbles/classifyBubble.ts +138 -0
- package/src/GraphGenomeView/bubbles/popBubble.test.ts +73 -0
- package/src/GraphGenomeView/bubbles/popBubble.ts +26 -0
- package/src/GraphGenomeView/colorSchemes.ts +36 -0
- package/src/GraphGenomeView/components/BubbleHalos.tsx +162 -0
- package/src/GraphGenomeView/components/BubbleOverlay.tsx +181 -0
- package/src/GraphGenomeView/components/ColorSchemeSelect.tsx +41 -0
- package/src/GraphGenomeView/components/GenePins.tsx +107 -0
- package/src/GraphGenomeView/components/GraphCanvas.tsx +759 -0
- package/src/GraphGenomeView/components/GraphGenomeView.test.tsx +77 -0
- package/src/GraphGenomeView/components/GraphGenomeView.tsx +18 -17
- package/src/GraphGenomeView/components/GraphLoadStatus.tsx +84 -0
- package/src/GraphGenomeView/components/GraphSettingsDialog.tsx +327 -0
- package/src/GraphGenomeView/components/GraphStats.tsx +82 -0
- package/src/GraphGenomeView/components/GraphToolbar.tsx +84 -0
- package/src/GraphGenomeView/components/ImportForm.tsx +124 -0
- package/src/GraphGenomeView/components/LabelChip.tsx +65 -0
- package/src/GraphGenomeView/components/LayoutSelect.tsx +56 -0
- package/src/GraphGenomeView/components/RepeatSelect.tsx +51 -0
- package/src/GraphGenomeView/components/SettingsMenu.tsx +82 -0
- package/src/GraphGenomeView/components/SubgraphContextSelect.tsx +75 -0
- package/src/GraphGenomeView/components/SubgraphHaplotypesField.tsx +86 -0
- package/src/GraphGenomeView/components/WalkRowsOverlay.tsx +252 -0
- package/src/GraphGenomeView/components/WalkSelect.tsx +51 -0
- package/src/GraphGenomeView/components/overlayLabels.test.ts +56 -0
- package/src/GraphGenomeView/components/overlayLabels.ts +75 -0
- package/src/GraphGenomeView/deletionEdges.test.ts +198 -0
- package/src/GraphGenomeView/deletionEdges.ts +217 -0
- package/src/GraphGenomeView/genes/geneFeatures.ts +107 -0
- package/src/GraphGenomeView/genes/genePins.ts +130 -0
- package/src/GraphGenomeView/genes/genes.test.ts +153 -0
- package/src/GraphGenomeView/gfa/gfaConverter.test.ts +394 -0
- package/src/GraphGenomeView/gfa/gfaConverter.ts +235 -0
- package/src/GraphGenomeView/graphLabels.budget.test.ts +36 -0
- package/src/GraphGenomeView/graphLabels.test.ts +548 -0
- package/src/GraphGenomeView/graphLabels.ts +640 -0
- package/src/GraphGenomeView/index.ts +29 -7
- package/src/GraphGenomeView/laneRamp.test.ts +97 -0
- package/src/GraphGenomeView/laneRamp.ts +87 -0
- package/src/GraphGenomeView/launchKeys.test.ts +63 -0
- package/src/GraphGenomeView/layout/anchoredLayout.test.ts +213 -0
- package/src/GraphGenomeView/layout/anchoredLayout.ts +80 -0
- package/src/GraphGenomeView/layout/bubbleCrossing.test.ts +190 -0
- package/src/GraphGenomeView/layout/drawnScale.ts +199 -0
- package/src/GraphGenomeView/layout/mergeRuns.test.ts +100 -0
- package/src/GraphGenomeView/layout/mergeRuns.ts +203 -0
- package/src/GraphGenomeView/layout/orderedLayout.test.ts +212 -0
- package/src/GraphGenomeView/layout/orderedLayout.ts +396 -0
- package/src/GraphGenomeView/layout/orientToReference.test.ts +110 -0
- package/src/GraphGenomeView/layout/orientToReference.ts +79 -0
- package/src/GraphGenomeView/layout/placeOffReference.ts +147 -0
- package/src/GraphGenomeView/layout/referenceSeeds.test.ts +79 -0
- package/src/GraphGenomeView/layout/referenceSeeds.ts +89 -0
- package/src/GraphGenomeView/layout/rowSpacing.ts +5 -0
- package/src/GraphGenomeView/layout/sampleRowLayout.test.ts +318 -0
- package/src/GraphGenomeView/layout/sampleRowLayout.ts +134 -0
- package/src/GraphGenomeView/layout/variantMapLayout.test.ts +59 -0
- package/src/GraphGenomeView/layout/variantMapLayout.ts +20 -0
- package/src/GraphGenomeView/layout/walkRowLayout.ts +54 -0
- package/src/GraphGenomeView/layout/walkRows.test.ts +178 -0
- package/src/GraphGenomeView/layout/walkRows.ts +177 -0
- package/src/GraphGenomeView/layoutModes.ts +131 -0
- package/src/GraphGenomeView/model.test.ts +2696 -0
- package/src/GraphGenomeView/model.ts +2672 -94
- package/src/GraphGenomeView/nodeWidths.ts +67 -0
- package/src/GraphGenomeView/pathAnchoring.test.ts +252 -0
- package/src/GraphGenomeView/pathAnchoring.ts +164 -0
- package/src/GraphGenomeView/pathColors.test.ts +83 -0
- package/src/GraphGenomeView/pathColors.ts +122 -0
- package/src/GraphGenomeView/referenceSpan.test.ts +84 -0
- package/src/GraphGenomeView/referenceSpan.ts +133 -0
- package/src/GraphGenomeView/renderPasses.bench.ts +172 -0
- package/src/GraphGenomeView/renderPipeline.test.ts +226 -0
- package/src/GraphGenomeView/renderer/Canvas2DRenderer.test.ts +205 -0
- package/src/GraphGenomeView/renderer/Canvas2DRenderer.ts +246 -0
- package/src/GraphGenomeView/renderer/GeometryBuilder.test.ts +723 -0
- package/src/GraphGenomeView/renderer/GeometryBuilder.ts +882 -0
- package/src/GraphGenomeView/renderer/GraphRenderer.ts +13 -0
- package/src/GraphGenomeView/renderer/colorBits.ts +43 -0
- package/src/GraphGenomeView/renderer/recordingCanvas.ts +64 -0
- package/src/GraphGenomeView/renderer/types.ts +84 -0
- package/src/GraphGenomeView/renderer/walkFade.test.ts +100 -0
- package/src/GraphGenomeView/repeats/repeatFeatures.test.ts +114 -0
- package/src/GraphGenomeView/repeats/repeatFeatures.ts +202 -0
- package/src/GraphGenomeView/repeats/test_data/abca7_trgt_walks.json +575 -0
- package/src/GraphGenomeView/repeats/walkCalls.test.ts +127 -0
- package/src/GraphGenomeView/repeats/walkCalls.ts +132 -0
- package/src/GraphGenomeView/stress.test.ts +214 -0
- package/src/GraphGenomeView/subgraphLoad.test.ts +245 -0
- package/src/GraphGenomeView/types.ts +141 -0
- package/src/GraphGenomeView/util/SpatialIndex.test.ts +73 -0
- package/src/GraphGenomeView/util/SpatialIndex.ts +312 -0
- package/src/GraphGenomeView/util/edgeCurves.test.ts +93 -0
- package/src/GraphGenomeView/util/edgeCurves.ts +94 -0
- package/src/GraphGenomeView/util/geometry.test.ts +447 -0
- package/src/GraphGenomeView/util/geometry.ts +656 -0
- package/src/GraphGenomeView/util/hitDetection.test.ts +307 -0
- package/src/GraphGenomeView/util/hitDetection.ts +330 -0
- package/src/GraphGenomeView/util/wheelZoom.test.ts +40 -0
- package/src/GraphGenomeView/util/wheelZoom.ts +26 -0
- package/src/GraphGenomeView/walkHighlight.test.ts +54 -0
- package/src/GraphGenomeView/walkHighlight.ts +56 -0
- package/src/LaunchGraphGenomeView.ts +46 -0
- package/src/MinigraphBubbleAdapter/MinigraphBubbleAdapter.test.ts +139 -0
- package/src/MinigraphBubbleAdapter/MinigraphBubbleAdapter.ts +86 -0
- package/src/MinigraphBubbleAdapter/bubbleLine.ts +92 -0
- package/src/MinigraphBubbleAdapter/configSchema.ts +109 -0
- package/src/MinigraphBubbleAdapter/index.ts +22 -0
- package/src/MinigraphBubbleAdapter/test_data/hprc_bubbles_mhc.bed.gz +0 -0
- package/src/MinigraphBubbleAdapter/test_data/hprc_bubbles_mhc.bed.gz.tbi +0 -0
- package/src/RgfaTabixAdapter/RgfaTabixAdapter.test.ts +285 -0
- package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +194 -0
- package/src/RgfaTabixAdapter/bubbleTier.test.ts +84 -0
- package/src/RgfaTabixAdapter/configSchema.ts +142 -0
- package/src/RgfaTabixAdapter/index.ts +24 -0
- package/src/RgfaTabixAdapter/rgfaBed.test.ts +158 -0
- package/src/RgfaTabixAdapter/rgfaBed.ts +156 -0
- package/src/RgfaTabixAdapter/test_data/bubble_tier_chrY.links.bed.gz +0 -0
- package/src/RgfaTabixAdapter/test_data/bubble_tier_chrY.links.bed.gz.tbi +0 -0
- package/src/RgfaTabixAdapter/test_data/bubble_tier_chrY.segs.bed.gz +0 -0
- package/src/RgfaTabixAdapter/test_data/bubble_tier_chrY.segs.bed.gz.tbi +0 -0
- package/src/RgfaTabixAdapter/test_data/pggb_ecoli.links.bed.gz +0 -0
- package/src/RgfaTabixAdapter/test_data/pggb_ecoli.links.bed.gz.tbi +0 -0
- package/src/RgfaTabixAdapter/test_data/pggb_ecoli.segs.bed.gz +0 -0
- package/src/RgfaTabixAdapter/test_data/pggb_ecoli.segs.bed.gz.tbi +0 -0
- package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.links.bed.gz +0 -0
- package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.links.bed.gz.tbi +0 -0
- package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.segs.bed.gz +0 -0
- package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.segs.bed.gz.tbi +0 -0
- package/src/alleleProjection/projectAlleles.test.ts +75 -0
- package/src/alleleProjection/projectAlleles.ts +190 -0
- package/src/bandage/README.md +121 -0
- package/src/bandage/bandage-layout.d.ts +14 -0
- package/src/bandage/bandage-layout.js +0 -0
- package/src/bandage/native/CMakeLists.txt +45 -0
- package/src/bandage/native/include/graph.h +188 -0
- package/src/bandage/native/include/graphlayout.h +74 -0
- package/src/bandage/native/include/settings.h +86 -0
- package/src/bandage/native/include/types.h +56 -0
- package/src/bandage/native/src/bindings.cpp +164 -0
- package/src/bandage/native/src/graphlayout.cpp +621 -0
- package/src/bandage/resizableDecode.test.ts +58 -0
- package/src/gfa-core/gfaParser.bench.ts +55 -0
- package/src/gfa-core/gfaParser.test.ts +168 -0
- package/src/gfa-core/gfaParser.ts +237 -0
- package/src/gfa-core/index.ts +10 -0
- package/src/hoverSync/GraphNodeHighlight.tsx +62 -0
- package/src/hoverSync/graphViewHighlights.test.ts +158 -0
- package/src/hoverSync/graphViewHighlights.ts +119 -0
- package/src/hoverSync/index.test.tsx +36 -0
- package/src/hoverSync/index.tsx +19 -0
- package/src/hoverSync/lgvHover.test.ts +202 -0
- package/src/hoverSync/lgvHover.ts +136 -0
- package/src/index.ts +46 -15
- package/src/launchFromGraph/contributors.test.ts +197 -0
- package/src/launchFromGraph/contributors.ts +0 -0
- package/src/launchFromGraph/graphMenuItems.ts +211 -0
- package/src/launchFromGraph/launchFromGraph.test.ts +489 -0
- package/src/launchFromGraph/launchFromGraph.ts +265 -0
- package/src/launchFromGraph/launchTracks.test.ts +72 -0
- package/src/launchFromGraph/launchTracks.ts +50 -0
- package/src/launchFromGraph/linearViewTarget.test.ts +82 -0
- package/src/launchFromGraph/linearViewTarget.ts +75 -0
- package/src/launchFromGraph/syntenyTracks.ts +47 -0
- package/src/launchSubgraph/index.ts +152 -0
- package/src/launchSubgraph/launchSubgraph.test.ts +264 -0
- package/src/launchSubgraph/launchSubgraphView.ts +128 -0
- package/src/launchSubgraph/linearViewMenuItems.test.ts +176 -0
- package/src/launchSubgraph/linearViewMenuItems.ts +107 -0
- package/src/launchSubgraph/multiWayLaunch.test.ts +289 -0
- package/src/launchSubgraph/subgraphMenuItems.ts +73 -0
- package/src/launchSubgraph/subgraphTracks.test.ts +165 -0
- package/src/launchSubgraph/subgraphTracks.ts +151 -0
- package/src/launchSubgraph/testEnv.ts +258 -0
- package/src/loadBandage.ts +22 -0
- package/src/panSNTabix.test.ts +72 -0
- package/src/panSNTabix.ts +149 -0
- package/src/pansn.ts +106 -0
- package/src/pluginName.ts +3 -0
- package/src/synteny/ComparativeAdapterBase.ts +77 -0
- package/src/synteny/SyntenyFeature.ts +116 -0
- package/src/synteny/clipFeatureToRegion.test.ts +329 -0
- package/src/synteny/clipFeatureToRegion.ts +167 -0
- package/src/version.ts +1 -0
- package/dist/GraphGenomeView/components/FeatureDialog.d.ts +0 -5
- package/dist/GraphGenomeView/components/FeatureDialog.js +0 -18
- package/dist/GraphGenomeView/components/FeatureDialog.js.map +0 -1
- package/dist/GraphGenomeView/components/GraphGenomeView.d.ts +0 -6
- package/dist/GraphGenomeView/components/GraphGenomeView.js +0 -17
- package/dist/GraphGenomeView/components/GraphGenomeView.js.map +0 -1
- package/dist/GraphGenomeView/components/GraphPanel.d.ts +0 -6
- package/dist/GraphGenomeView/components/GraphPanel.js +0 -17
- package/dist/GraphGenomeView/components/GraphPanel.js.map +0 -1
- package/dist/GraphGenomeView/components/Header.d.ts +0 -6
- package/dist/GraphGenomeView/components/Header.js +0 -59
- package/dist/GraphGenomeView/components/Header.js.map +0 -1
- package/dist/GraphGenomeView/components/SettingsDialog.d.ts +0 -7
- package/dist/GraphGenomeView/components/SettingsDialog.js +0 -24
- package/dist/GraphGenomeView/components/SettingsDialog.js.map +0 -1
- package/dist/GraphGenomeView/index.d.ts +0 -2
- package/dist/GraphGenomeView/index.js +0 -15
- package/dist/GraphGenomeView/index.js.map +0 -1
- package/dist/GraphGenomeView/model.d.ts +0 -80
- package/dist/GraphGenomeView/model.js +0 -96
- package/dist/GraphGenomeView/model.js.map +0 -1
- package/dist/GraphGenomeView/util.d.ts +0 -1
- package/dist/GraphGenomeView/util.js +0 -8
- package/dist/GraphGenomeView/util.js.map +0 -1
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -24
- package/dist/index.js.map +0 -1
- package/dist/jbrowse-plugin-graphgenomeviewer.umd.production.min.js +0 -67
- package/dist/jbrowse-plugin-graphgenomeviewer.umd.production.min.js.map +0 -7
- package/src/GraphGenomeView/components/FeatureDialog.tsx +0 -51
- package/src/GraphGenomeView/components/GraphPanel.tsx +0 -37
- package/src/GraphGenomeView/components/Header.tsx +0 -90
- package/src/GraphGenomeView/components/SettingsDialog.tsx +0 -71
- package/src/GraphGenomeView/util.ts +0 -7
- package/src/declare.d.ts +0 -1
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { orderedLayout, referenceOrder } from './orderedLayout'
|
|
2
|
+
import { ROW_HEIGHT_PX } from './rowSpacing'
|
|
3
|
+
import { parseGFA } from '../../gfa-core/index'
|
|
4
|
+
import { convertGFAToGraph } from '../gfa/gfaConverter'
|
|
5
|
+
|
|
6
|
+
// chr1 runs v1->v2->v3->v4; two single-segment alleles, a1 (foo) and a2 (bar),
|
|
7
|
+
// each replace v3 between the same anchors, so they share a layer and have to
|
|
8
|
+
// take different lanes. The file lists the L-lines in reference direction.
|
|
9
|
+
const BUBBLE = `S\tv1\tAAAAA\tLN:i:5\tSN:Z:chr1\tSO:i:0\tSR:i:0
|
|
10
|
+
S\tv2\tCCC\tLN:i:3\tSN:Z:chr1\tSO:i:5\tSR:i:0
|
|
11
|
+
S\tv3\tGG\tLN:i:2\tSN:Z:chr1\tSO:i:8\tSR:i:0
|
|
12
|
+
S\tv4\tTTTTTTT\tLN:i:7\tSN:Z:chr1\tSO:i:10\tSR:i:0
|
|
13
|
+
S\ta1\tAC\tLN:i:2\tSN:Z:foo\tSO:i:8\tSR:i:1
|
|
14
|
+
S\ta2\tT\tLN:i:1\tSN:Z:bar\tSO:i:8\tSR:i:2
|
|
15
|
+
L\tv1\t+\tv2\t+\t0M
|
|
16
|
+
L\tv2\t+\tv3\t+\t0M
|
|
17
|
+
L\tv3\t+\tv4\t+\t0M
|
|
18
|
+
L\tv2\t+\ta1\t+\t0M
|
|
19
|
+
L\ta1\t+\tv4\t+\t0M
|
|
20
|
+
L\tv2\t+\ta2\t+\t0M
|
|
21
|
+
L\ta2\t+\tv4\t+\t0M`
|
|
22
|
+
|
|
23
|
+
const BACKBONE = ['v1+', 'v2+', 'v3+', 'v4+']
|
|
24
|
+
|
|
25
|
+
function graphOf(gfa: string) {
|
|
26
|
+
return convertGFAToGraph(parseGFA(gfa))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function layout(gfa: string) {
|
|
30
|
+
return orderedLayout(graphOf(gfa))!.nodePositions
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const centre = (seg: { x: number }[]) => (seg[0]!.x + seg.at(-1)!.x) / 2
|
|
34
|
+
|
|
35
|
+
test('backbone x strictly increases in reference order', () => {
|
|
36
|
+
const pos = layout(BUBBLE)
|
|
37
|
+
const xs = BACKBONE.map(id => centre(pos[id]!))
|
|
38
|
+
for (let i = 1; i < xs.length; i++) {
|
|
39
|
+
expect(xs[i]!).toBeGreaterThan(xs[i - 1]!)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('no two nodes share a layer and a lane', () => {
|
|
44
|
+
const pos = layout(BUBBLE)
|
|
45
|
+
const cells = Object.values(pos).map(seg => `${centre(seg)},${seg[0]!.y}`)
|
|
46
|
+
expect(new Set(cells).size).toBe(cells.length)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('the backbone holds lane 0 and nothing else does', () => {
|
|
50
|
+
const pos = layout(BUBBLE)
|
|
51
|
+
for (const id of BACKBONE) {
|
|
52
|
+
expect(pos[id]!.every(p => p.y === 0)).toBe(true)
|
|
53
|
+
}
|
|
54
|
+
expect(pos['a1+']![0]!.y).not.toBe(0)
|
|
55
|
+
expect(pos['a2+']![0]!.y).not.toBe(0)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('every edge runs left to right', () => {
|
|
59
|
+
const graph = graphOf(BUBBLE)
|
|
60
|
+
const pos = orderedLayout(graph)!.nodePositions
|
|
61
|
+
for (const { from, to } of graph.edges) {
|
|
62
|
+
expect(centre(pos[from]!)).toBeLessThan(centre(pos[to]!))
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test("a bubble's two alleles share a layer on different lanes", () => {
|
|
67
|
+
const pos = layout(BUBBLE)
|
|
68
|
+
const a1 = pos['a1+']!
|
|
69
|
+
const a2 = pos['a2+']!
|
|
70
|
+
expect(centre(a1)).toBe(centre(a2))
|
|
71
|
+
expect(a1[0]!.y).not.toBe(a2[0]!.y)
|
|
72
|
+
// both one or more lane pitches off the reference line
|
|
73
|
+
expect(Math.abs(a1[0]!.y) % ROW_HEIGHT_PX).toBe(0)
|
|
74
|
+
expect(Math.abs(a2[0]!.y)).toBeGreaterThanOrEqual(ROW_HEIGHT_PX)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('an allele sits between its anchors, not under one of them', () => {
|
|
78
|
+
const pos = layout(BUBBLE)
|
|
79
|
+
expect(centre(pos['a1+']!)).toBeGreaterThan(centre(pos['v2+']!))
|
|
80
|
+
expect(centre(pos['a1+']!)).toBeLessThan(centre(pos['v4+']!))
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('a node is a two-point polyline as wide as the log of its bp', () => {
|
|
84
|
+
const pos = layout(BUBBLE)
|
|
85
|
+
const seg = pos['v4+']!
|
|
86
|
+
expect(seg).toHaveLength(2)
|
|
87
|
+
expect(seg[1]!.x - seg[0]!.x).toBeCloseTo(10 + 8 * Math.log2(8), 5)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('x is order, and y a lane pitch in screen px', () => {
|
|
91
|
+
const result = orderedLayout(graphOf(BUBBLE))!
|
|
92
|
+
expect(result.referenceAxis).toBe(false)
|
|
93
|
+
expect(result.pixelRows).toBe(true)
|
|
94
|
+
expect(result.rowLabels).toBeUndefined()
|
|
95
|
+
expect(result.alleleDeletions).toBeUndefined()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('a graph without a backbone gets no ordered layout', () => {
|
|
99
|
+
const plain = `S\t1\tACGT
|
|
100
|
+
S\t2\tGGCC
|
|
101
|
+
L\t1\t+\t2\t+\t0M`
|
|
102
|
+
expect(orderedLayout(graphOf(plain))).toBeUndefined()
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('the same graph always gets the same drawing', () => {
|
|
106
|
+
const graph = graphOf(BUBBLE)
|
|
107
|
+
expect(orderedLayout(graph)).toEqual(orderedLayout(graph))
|
|
108
|
+
expect(referenceOrder(graph)).toEqual(referenceOrder(graphOf(BUBBLE)))
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('reference order is backbone by offset, alleles just past their anchor', () => {
|
|
112
|
+
expect(referenceOrder(graphOf(BUBBLE))).toEqual([
|
|
113
|
+
'v1+',
|
|
114
|
+
'v2+',
|
|
115
|
+
'a1+',
|
|
116
|
+
'a2+',
|
|
117
|
+
'v3+',
|
|
118
|
+
'v4+',
|
|
119
|
+
])
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// r1 r2 r3 r4 along chr1 at 0, 1, 2, 3, a base apart as a base-level graph's
|
|
123
|
+
// backbone is, with whatever `extra` hangs off it.
|
|
124
|
+
function baseLevel(extra: string[]) {
|
|
125
|
+
return graphOf(
|
|
126
|
+
[
|
|
127
|
+
...[0, 1, 2, 3].map(
|
|
128
|
+
i => `S\tr${i + 1}\tA\tLN:i:1\tSN:Z:chr1\tSO:i:${i}\tSR:i:0`,
|
|
129
|
+
),
|
|
130
|
+
...[1, 2, 3].map(i => `L\tr${i}\t+\tr${i + 1}\t+\t0M`),
|
|
131
|
+
...extra,
|
|
132
|
+
].join('\n'),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const off = (name: string) => `S\t${name}\tA\tLN:i:1\tSN:Z:alt\tSO:i:0\tSR:i:1`
|
|
137
|
+
const link = (a: string, b: string) => `L\t${a}\t+\t${b}\t+\t0M`
|
|
138
|
+
|
|
139
|
+
// The search from the backbone meets itself in the middle of x1..x4, so x3 and
|
|
140
|
+
// x4 are claimed from r4. Sorted after it, their links into r4 turned round.
|
|
141
|
+
test('both halves of a multi-node allele sort between its anchors', () => {
|
|
142
|
+
const chain = ['r1', 'x1', 'x2', 'x3', 'x4', 'r4']
|
|
143
|
+
const graph = baseLevel([
|
|
144
|
+
...['x1', 'x2', 'x3', 'x4'].map(off),
|
|
145
|
+
...chain.slice(1).map((n, i) => link(chain[i]!, n)),
|
|
146
|
+
])
|
|
147
|
+
const order = referenceOrder(graph)
|
|
148
|
+
const places = chain.map(n => order.indexOf(`${n}+`))
|
|
149
|
+
expect(places).toEqual([...places].sort((a, b) => a - b))
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
// Six nodes off r1, whose next backbone node is a base away. Nudged half a bp
|
|
153
|
+
// per step, the chain's keys walked past r2, r3 and r4.
|
|
154
|
+
test('a long allele stays beside its anchor however close the next node is', () => {
|
|
155
|
+
const names = ['y1', 'y2', 'y3', 'y4', 'y5', 'y6']
|
|
156
|
+
const graph = baseLevel([
|
|
157
|
+
...names.map(off),
|
|
158
|
+
link('r1', 'y1'),
|
|
159
|
+
...names.slice(1).map((n, i) => link(names[i]!, n)),
|
|
160
|
+
])
|
|
161
|
+
expect(referenceOrder(graph)).toEqual([
|
|
162
|
+
'r1+',
|
|
163
|
+
...names.map(n => `${n}+`),
|
|
164
|
+
'r2+',
|
|
165
|
+
'r3+',
|
|
166
|
+
'r4+',
|
|
167
|
+
])
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// t1 and t2 are claimed from r4, and the left reaches them only at t1. t2 is a
|
|
171
|
+
// dead end of the search that the left never reached, so steps from r4 say
|
|
172
|
+
// nothing about where it lies: sorted first, as the far half of a chain is, it
|
|
173
|
+
// would have nothing before it. The run stays after its anchor.
|
|
174
|
+
test('a run with a dead end the left never reached stays after its anchor', () => {
|
|
175
|
+
const graph = baseLevel([
|
|
176
|
+
...['s1', 't1', 't2'].map(off),
|
|
177
|
+
link('r1', 's1'),
|
|
178
|
+
link('s1', 't1'),
|
|
179
|
+
link('t1', 'r4'),
|
|
180
|
+
link('t1', 't2'),
|
|
181
|
+
])
|
|
182
|
+
const order = referenceOrder(graph)
|
|
183
|
+
const at = (name: string) => order.indexOf(`${name}+`)
|
|
184
|
+
expect(at('t1')).toBeGreaterThan(at('r4'))
|
|
185
|
+
expect(at('t2')).toBeGreaterThan(at('t1'))
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
// A walk says where its nodes lie. w2 links only to r4, so the links alone put
|
|
189
|
+
// it after r4; the walk enters it from w1, off r1.
|
|
190
|
+
const WALKED = [
|
|
191
|
+
...['w1', 'w2'].map(off),
|
|
192
|
+
link('r1', 'w1'),
|
|
193
|
+
link('w2', 'r4'),
|
|
194
|
+
'W\tref\t0\tchr1\t0\t4\t>r1>r2>r3>r4',
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
test('a walk places the nodes it visits after the node it came from', () => {
|
|
198
|
+
const graph = baseLevel([...WALKED, 'W\thap\t1\tctg\t0\t4\t>r1>w1>w2>r4'])
|
|
199
|
+
expect(referenceOrder(graph)).toEqual([
|
|
200
|
+
'r1+',
|
|
201
|
+
'w1+',
|
|
202
|
+
'w2+',
|
|
203
|
+
'r2+',
|
|
204
|
+
'r3+',
|
|
205
|
+
'r4+',
|
|
206
|
+
])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('a walk on the reverse strand is read end-first', () => {
|
|
210
|
+
const graph = baseLevel([...WALKED, 'W\thap\t1\tctg\t0\t4\t<r4<w2<w1<r1'])
|
|
211
|
+
expect(referenceOrder(graph).slice(0, 3)).toEqual(['r1+', 'w1+', 'w2+'])
|
|
212
|
+
})
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { ROW_HEIGHT_PX } from './rowSpacing'
|
|
2
|
+
import { isBackbone } from '../anchoredNodes'
|
|
3
|
+
|
|
4
|
+
import type { AnchoredNode } from '../anchoredNodes'
|
|
5
|
+
import type { Graph, GraphNode, LayoutResult, NodeSegment } from '../types'
|
|
6
|
+
|
|
7
|
+
// Reference-ordered layered layout: x is reference ORDER, not bp. Backbone
|
|
8
|
+
// segments take increasing layers by stable offset, every other node the layer
|
|
9
|
+
// between the anchors it hangs off, and a node is as wide as the log of its bp,
|
|
10
|
+
// so a SNP allele gets the same room as a 10 kb segment and a bubble reads as a
|
|
11
|
+
// lens rather than a bar under a line. y is a lane in screen px with the
|
|
12
|
+
// reference pinned at lane 0 (docs/layout-experiments.md, experiment 3).
|
|
13
|
+
|
|
14
|
+
const GUTTER = 12
|
|
15
|
+
const LANE_PX = ROW_HEIGHT_PX
|
|
16
|
+
|
|
17
|
+
function nodeWidth(node: GraphNode) {
|
|
18
|
+
return 10 + 8 * Math.log2(1 + node.length)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function compare<T extends string | number>(a: T, b: T) {
|
|
22
|
+
return a < b ? -1 : a > b ? 1 : 0
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Where a node sorts: `at` is the rank of a placed node, the backbone and what
|
|
26
|
+
// the walks visit, and a node the search claims sorts before or after the one
|
|
27
|
+
// that claimed it, by its steps from it. A tuple rather than an offset nudged
|
|
28
|
+
// per step: on a base-level graph the backbone nodes are a bp apart, and a
|
|
29
|
+
// chain a few nodes long nudged its keys past the ones that follow.
|
|
30
|
+
interface OrderKey {
|
|
31
|
+
at: number
|
|
32
|
+
side: -1 | 0 | 1
|
|
33
|
+
hops: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function compareKeys(a: OrderKey, b: OrderKey) {
|
|
37
|
+
return (
|
|
38
|
+
compare(a.at, b.at) || compare(a.side, b.side) || compare(a.hops, b.hops)
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const UNREACHED: OrderKey = { at: Infinity, side: 0, hops: 0 }
|
|
43
|
+
|
|
44
|
+
// Every node id in reference order: backbone by stable offset, every other node
|
|
45
|
+
// beside a backbone node (orderKeys), nodes nothing reaches last. Name breaks
|
|
46
|
+
// ties, so the order is a strict total order and the same graph always gets
|
|
47
|
+
// the same one.
|
|
48
|
+
export function referenceOrder(graph: Graph) {
|
|
49
|
+
const byId = new Map(graph.nodes.map(n => [n.id, n]))
|
|
50
|
+
const key = orderKeys(graph)
|
|
51
|
+
return graph.nodes
|
|
52
|
+
.map(n => n.id)
|
|
53
|
+
.sort(
|
|
54
|
+
(a, b) =>
|
|
55
|
+
compareKeys(key.get(a) ?? UNREACHED, key.get(b) ?? UNREACHED) ||
|
|
56
|
+
compare(byId.get(a)!.name, byId.get(b)!.name) ||
|
|
57
|
+
compare(a, b),
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A walk states where its nodes lie, which the links alone do not. Starting
|
|
62
|
+
// from the backbone in offset order, each node a walk visits for the first time
|
|
63
|
+
// goes into the order immediately after the node that walk came from, and the
|
|
64
|
+
// nodes a walk visits before it meets a placed one go immediately before it.
|
|
65
|
+
//
|
|
66
|
+
// That keeps every true boundary. A walk's step joins two nodes on the same
|
|
67
|
+
// side of a node every haplotype crosses once, unless one of them is that
|
|
68
|
+
// node, so by induction the nodes between two such boundaries stay one
|
|
69
|
+
// unbroken stretch of the order and no link among them reaches across. On the
|
|
70
|
+
// eight-haplotype KIV-2 cut (15,808 nodes) the walks imply 28 bubbles and this
|
|
71
|
+
// derives those 28, with 5 of 65,776 walk steps against the order. Placing
|
|
72
|
+
// every node by the links alone derived 21, with 23,308 steps against it.
|
|
73
|
+
//
|
|
74
|
+
// A walk on the reverse strand is read end-first.
|
|
75
|
+
function placeByWalks(graph: Graph, backbone: AnchoredNode[]) {
|
|
76
|
+
const isNode = new Set(graph.nodes.map(n => n.id))
|
|
77
|
+
const startOf = new Map(backbone.map(n => [n.id, n.stable.start]))
|
|
78
|
+
const next = new Map<string, string | undefined>()
|
|
79
|
+
const prev = new Map<string, string | undefined>()
|
|
80
|
+
backbone.forEach((n, i) => {
|
|
81
|
+
prev.set(n.id, backbone[i - 1]?.id)
|
|
82
|
+
next.set(n.id, backbone[i + 1]?.id)
|
|
83
|
+
})
|
|
84
|
+
let head = backbone[0]?.id
|
|
85
|
+
const insertAfter = (at: string, id: string) => {
|
|
86
|
+
const after = next.get(at)
|
|
87
|
+
next.set(at, id)
|
|
88
|
+
prev.set(id, at)
|
|
89
|
+
next.set(id, after)
|
|
90
|
+
if (after !== undefined) {
|
|
91
|
+
prev.set(after, id)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const insertBefore = (at: string, id: string) => {
|
|
95
|
+
const before = prev.get(at)
|
|
96
|
+
if (before === undefined) {
|
|
97
|
+
head = id
|
|
98
|
+
prev.set(id, undefined)
|
|
99
|
+
next.set(id, at)
|
|
100
|
+
prev.set(at, id)
|
|
101
|
+
} else {
|
|
102
|
+
insertAfter(before, id)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const path of graph.paths ?? []) {
|
|
107
|
+
let rising = 0
|
|
108
|
+
let falling = 0
|
|
109
|
+
let last: number | undefined
|
|
110
|
+
for (const id of path.nodeIds) {
|
|
111
|
+
const start = startOf.get(id)
|
|
112
|
+
if (start !== undefined) {
|
|
113
|
+
if (last !== undefined && start !== last) {
|
|
114
|
+
if (start > last) {
|
|
115
|
+
rising++
|
|
116
|
+
} else {
|
|
117
|
+
falling++
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
last = start
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const ids = falling > rising ? [...path.nodeIds].reverse() : path.nodeIds
|
|
124
|
+
let from: string | undefined
|
|
125
|
+
const leading = new Set<string>()
|
|
126
|
+
for (const id of ids) {
|
|
127
|
+
if (next.has(id)) {
|
|
128
|
+
if (from === undefined) {
|
|
129
|
+
for (const lead of leading) {
|
|
130
|
+
insertBefore(id, lead)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
from = id
|
|
134
|
+
} else if (isNode.has(id)) {
|
|
135
|
+
if (from === undefined) {
|
|
136
|
+
leading.add(id)
|
|
137
|
+
} else {
|
|
138
|
+
insertAfter(from, id)
|
|
139
|
+
from = id
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const placed: string[] = []
|
|
146
|
+
for (let id = head; id !== undefined; id = next.get(id)) {
|
|
147
|
+
placed.push(id)
|
|
148
|
+
}
|
|
149
|
+
return placed
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The nodes no walk visits, which is every off-reference node of a graph with
|
|
153
|
+
// no walks: each goes beside the nearest placed node that reaches it (BFS over
|
|
154
|
+
// the links as undirected, seeded in order so a tie goes left).
|
|
155
|
+
//
|
|
156
|
+
// That search meets itself in the middle of an allele, so the far half of a
|
|
157
|
+
// chain A>X1>X2>B is claimed from B, its RIGHT anchor. Placed after B, as every
|
|
158
|
+
// claimed node once was, X2 sorted past the node it leads into, its link was
|
|
159
|
+
// turned round, and the insertion stopped being a bubble. So a run of nodes
|
|
160
|
+
// claimed from one placed node lies BEFORE it, farthest node first, when it is
|
|
161
|
+
// such a far half: it touches nothing right of that node, and every dead end of
|
|
162
|
+
// the search inside it is a place the search from the left arrived. Steps are
|
|
163
|
+
// a position only along a chain. In a tangle the farthest node is a dead end
|
|
164
|
+
// with nothing before it, and sorted first its links to its real neighbours
|
|
165
|
+
// sweep across every boundary between, so any other run stays after.
|
|
166
|
+
function placeBySearch(
|
|
167
|
+
adjacent: Map<string, string[]>,
|
|
168
|
+
key: Map<string, OrderKey>,
|
|
169
|
+
) {
|
|
170
|
+
const queue = [...key.entries()]
|
|
171
|
+
.sort(([a, ka], [b, kb]) => compareKeys(ka, kb) || compare(a, b))
|
|
172
|
+
.map(([id]) => id)
|
|
173
|
+
const placed = queue.length
|
|
174
|
+
// the queue grows while the loop walks it, which for-of follows
|
|
175
|
+
for (const id of queue) {
|
|
176
|
+
const from = key.get(id)!
|
|
177
|
+
for (const nb of adjacent.get(id) ?? []) {
|
|
178
|
+
if (!key.has(nb)) {
|
|
179
|
+
key.set(nb, { at: from.at, side: 1, hops: from.hops + 1 })
|
|
180
|
+
queue.push(nb)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const searched = new Set(queue.slice(placed))
|
|
186
|
+
const sided = new Set<string>()
|
|
187
|
+
for (const seed of searched) {
|
|
188
|
+
if (sided.has(seed)) {
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
const { at } = key.get(seed)!
|
|
192
|
+
const run = [seed]
|
|
193
|
+
sided.add(seed)
|
|
194
|
+
let farHalf = true
|
|
195
|
+
for (const id of run) {
|
|
196
|
+
const { hops } = key.get(id)!
|
|
197
|
+
let deadEnd = true
|
|
198
|
+
let touchesLeft = false
|
|
199
|
+
for (const nb of adjacent.get(id) ?? []) {
|
|
200
|
+
const other = key.get(nb)!
|
|
201
|
+
if (other.at === at && searched.has(nb)) {
|
|
202
|
+
deadEnd &&= other.hops <= hops
|
|
203
|
+
if (!sided.has(nb)) {
|
|
204
|
+
sided.add(nb)
|
|
205
|
+
run.push(nb)
|
|
206
|
+
}
|
|
207
|
+
} else if (other.at !== at) {
|
|
208
|
+
touchesLeft ||= other.at < at
|
|
209
|
+
farHalf &&= other.at < at
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
farHalf &&= !deadEnd || touchesLeft
|
|
213
|
+
}
|
|
214
|
+
if (farHalf) {
|
|
215
|
+
for (const id of run) {
|
|
216
|
+
const k = key.get(id)!
|
|
217
|
+
key.set(id, { ...k, side: -1, hops: -k.hops })
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function orderKeys(graph: Graph) {
|
|
224
|
+
const ids = new Set(graph.nodes.map(n => n.id))
|
|
225
|
+
const adjacent = new Map<string, string[]>()
|
|
226
|
+
for (const e of graph.edges) {
|
|
227
|
+
if (!ids.has(e.from) || !ids.has(e.to)) {
|
|
228
|
+
continue
|
|
229
|
+
}
|
|
230
|
+
;(adjacent.get(e.from) ?? adjacent.set(e.from, []).get(e.from)!).push(e.to)
|
|
231
|
+
;(adjacent.get(e.to) ?? adjacent.set(e.to, []).get(e.to)!).push(e.from)
|
|
232
|
+
}
|
|
233
|
+
const backbone = graph.nodes
|
|
234
|
+
.filter(isBackbone)
|
|
235
|
+
.sort(
|
|
236
|
+
(a, b) =>
|
|
237
|
+
compare(a.stable.start, b.stable.start) || compare(a.name, b.name),
|
|
238
|
+
)
|
|
239
|
+
const key = new Map<string, OrderKey>()
|
|
240
|
+
placeByWalks(graph, backbone).forEach((id, rank) => {
|
|
241
|
+
key.set(id, { at: rank, side: 0, hops: 0 })
|
|
242
|
+
})
|
|
243
|
+
placeBySearch(adjacent, key)
|
|
244
|
+
return key
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface LayeredGraph {
|
|
248
|
+
layers: string[][]
|
|
249
|
+
layerOf: Map<string, number>
|
|
250
|
+
// predecessors in layer order, for the y sweep
|
|
251
|
+
preds: Map<string, string[]>
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// The layered DAG the ordered layout draws and the bubble decomposition reads
|
|
255
|
+
// (bubbles/bubblesFromGraph.ts), from one function so the two cannot disagree
|
|
256
|
+
// about which nodes share a layer.
|
|
257
|
+
export function layerGraph(graph: Graph): LayeredGraph {
|
|
258
|
+
const byId = new Map(graph.nodes.map(n => [n.id, n]))
|
|
259
|
+
const order = referenceOrder(graph)
|
|
260
|
+
const key = new Map(order.map((id, i) => [id, i]))
|
|
261
|
+
const isRef = (id: string) => isBackbone(byId.get(id)!)
|
|
262
|
+
|
|
263
|
+
// Every edge directed from lower reference key to higher, so the graph is
|
|
264
|
+
// acyclic by construction and no cycle-removal heuristic can misplace the
|
|
265
|
+
// reference.
|
|
266
|
+
const preds = new Map<string, string[]>(order.map(id => [id, []]))
|
|
267
|
+
const succs = new Map<string, string[]>(order.map(id => [id, []]))
|
|
268
|
+
const link = (a: string, b: string) => {
|
|
269
|
+
preds.get(b)!.push(a)
|
|
270
|
+
succs.get(a)!.push(b)
|
|
271
|
+
}
|
|
272
|
+
for (const e of graph.edges) {
|
|
273
|
+
if (e.from === e.to || !key.has(e.from) || !key.has(e.to)) {
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
if (key.get(e.from)! < key.get(e.to)!) {
|
|
277
|
+
link(e.from, e.to)
|
|
278
|
+
} else {
|
|
279
|
+
link(e.to, e.from)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// The backbone chained by virtual edges, so the reference stays monotone even
|
|
283
|
+
// where adjacent segments are not linked inside the cut.
|
|
284
|
+
const backbone = order.filter(isRef)
|
|
285
|
+
for (let i = 1; i < backbone.length; i++) {
|
|
286
|
+
link(backbone[i - 1]!, backbone[i]!)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Longest-path layering; the key order is a topological order of this DAG.
|
|
290
|
+
const layerOf = new Map<string, number>()
|
|
291
|
+
for (const id of order) {
|
|
292
|
+
let l = 0
|
|
293
|
+
for (const p of preds.get(id)!) {
|
|
294
|
+
l = Math.max(l, layerOf.get(p)! + 1)
|
|
295
|
+
}
|
|
296
|
+
layerOf.set(id, l)
|
|
297
|
+
}
|
|
298
|
+
// Then each allele slides right to centre between the layer its predecessors
|
|
299
|
+
// force and the one its successors allow, so a short allele sits in the
|
|
300
|
+
// middle of its bubble rather than hugging the left anchor.
|
|
301
|
+
for (let i = order.length - 1; i >= 0; i--) {
|
|
302
|
+
const id = order[i]!
|
|
303
|
+
const s = succs.get(id)!
|
|
304
|
+
if (isRef(id) || s.length === 0) {
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
let minSucc = Infinity
|
|
308
|
+
for (const t of s) {
|
|
309
|
+
minSucc = Math.min(minSucc, layerOf.get(t)!)
|
|
310
|
+
}
|
|
311
|
+
const lo = layerOf.get(id)!
|
|
312
|
+
const hi = Math.max(lo, minSucc - 1)
|
|
313
|
+
layerOf.set(id, Math.floor((lo + hi) / 2))
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const layers: string[][] = []
|
|
317
|
+
for (const id of order) {
|
|
318
|
+
;(layers[layerOf.get(id)!] ??= []).push(id)
|
|
319
|
+
}
|
|
320
|
+
return { layers, layerOf, preds }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function orderedLayout(graph: Graph): LayoutResult | undefined {
|
|
324
|
+
if (!graph.nodes.some(isBackbone)) {
|
|
325
|
+
return undefined
|
|
326
|
+
}
|
|
327
|
+
const byId = new Map(graph.nodes.map(n => [n.id, n]))
|
|
328
|
+
const isRef = (id: string) => isBackbone(byId.get(id)!)
|
|
329
|
+
const { layers, layerOf: layer, preds } = layerGraph(graph)
|
|
330
|
+
|
|
331
|
+
const layerWidth = layers.map(ids => {
|
|
332
|
+
let w = 0
|
|
333
|
+
for (const id of ids) {
|
|
334
|
+
w = Math.max(w, nodeWidth(byId.get(id)!))
|
|
335
|
+
}
|
|
336
|
+
return w
|
|
337
|
+
})
|
|
338
|
+
const layerX: number[] = []
|
|
339
|
+
let x = 0
|
|
340
|
+
for (let l = 0; l < layers.length; l++) {
|
|
341
|
+
layerX[l] = x
|
|
342
|
+
x += layerWidth[l]! + GUTTER
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// y: sweep the layers left to right, each allele at the barycenter of its
|
|
346
|
+
// placed predecessors, then at the nearest FREE lane to it. Lane 0 is the
|
|
347
|
+
// reference line and stays reserved even in a layer with no backbone node,
|
|
348
|
+
// or an allele between two backbone segments reads as reference.
|
|
349
|
+
const y = new Map<string, number>()
|
|
350
|
+
for (const ids of layers) {
|
|
351
|
+
const free: { id: string; ideal: number }[] = []
|
|
352
|
+
for (const id of ids) {
|
|
353
|
+
if (isRef(id)) {
|
|
354
|
+
y.set(id, 0)
|
|
355
|
+
continue
|
|
356
|
+
}
|
|
357
|
+
const placed = preds.get(id)!.filter(p => y.has(p))
|
|
358
|
+
let ideal = 0
|
|
359
|
+
for (const p of placed) {
|
|
360
|
+
ideal += y.get(p)! / placed.length
|
|
361
|
+
}
|
|
362
|
+
// a node straight off the backbone wants to leave it, not sit on it
|
|
363
|
+
free.push({ id, ideal: ideal === 0 ? LANE_PX : ideal })
|
|
364
|
+
}
|
|
365
|
+
free.sort((a, b) => a.ideal - b.ideal)
|
|
366
|
+
const taken = new Set([0])
|
|
367
|
+
for (const { id, ideal } of free) {
|
|
368
|
+
let lane = Math.round(ideal / LANE_PX)
|
|
369
|
+
if (lane === 0) {
|
|
370
|
+
lane = ideal >= 0 ? 1 : -1
|
|
371
|
+
}
|
|
372
|
+
for (let d = 1; taken.has(lane); d++) {
|
|
373
|
+
if (!taken.has(lane + d)) {
|
|
374
|
+
lane += d
|
|
375
|
+
} else if (!taken.has(lane - d)) {
|
|
376
|
+
lane -= d
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
taken.add(lane)
|
|
380
|
+
y.set(id, lane * LANE_PX)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const nodePositions: Record<string, NodeSegment[]> = {}
|
|
385
|
+
for (const n of graph.nodes) {
|
|
386
|
+
const l = layer.get(n.id)!
|
|
387
|
+
const w = nodeWidth(n)
|
|
388
|
+
const cx = layerX[l]! + layerWidth[l]! / 2
|
|
389
|
+
const ny = y.get(n.id)!
|
|
390
|
+
nodePositions[n.id] = [
|
|
391
|
+
{ x: cx - w / 2, y: ny },
|
|
392
|
+
{ x: cx + w / 2, y: ny },
|
|
393
|
+
]
|
|
394
|
+
}
|
|
395
|
+
return { nodePositions, referenceAxis: false, pixelRows: true }
|
|
396
|
+
}
|