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,84 @@
|
|
|
1
|
+
import { convertGFAToGraph } from './gfa/gfaConverter'
|
|
2
|
+
import { buildNeighbors, nodeReferenceSpan } from './referenceSpan'
|
|
3
|
+
import { parseGFA } from '../gfa-core/index'
|
|
4
|
+
|
|
5
|
+
// The worked example from gfatools/doc/rGFA.md, the same fixture the anchored
|
|
6
|
+
// layout tests use: chr1 runs v1->v2->v3->v4, and foo carries an alternate
|
|
7
|
+
// allele (v5,v6) leaving after v2 and rejoining at v4.
|
|
8
|
+
const RGFA = `S\tv1\tAAAAA\tLN:i:5\tSN:Z:chr1\tSO:i:0\tSR:i:0
|
|
9
|
+
S\tv2\tCCC\tLN:i:3\tSN:Z:chr1\tSO:i:5\tSR:i:0
|
|
10
|
+
S\tv3\tGG\tLN:i:2\tSN:Z:chr1\tSO:i:8\tSR:i:0
|
|
11
|
+
S\tv4\tTTTTTTT\tLN:i:7\tSN:Z:chr1\tSO:i:10\tSR:i:0
|
|
12
|
+
S\tv5\tAC\tLN:i:2\tSN:Z:foo\tSO:i:8\tSR:i:1
|
|
13
|
+
S\tv6\tGTA\tLN:i:3\tSN:Z:foo\tSO:i:10\tSR:i:1
|
|
14
|
+
L\tv1\t+\tv2\t+\t0M
|
|
15
|
+
L\tv2\t+\tv3\t+\t0M
|
|
16
|
+
L\tv3\t+\tv4\t+\t0M
|
|
17
|
+
L\tv2\t+\tv5\t+\t0M
|
|
18
|
+
L\tv5\t+\tv6\t+\t0M
|
|
19
|
+
L\tv6\t+\tv4\t+\t0M`
|
|
20
|
+
|
|
21
|
+
function setup(gfa: string) {
|
|
22
|
+
const graph = convertGFAToGraph(parseGFA(gfa))
|
|
23
|
+
const nodeById = new Map(graph.nodes.map(n => [n.id, n]))
|
|
24
|
+
const neighbors = buildNeighbors(graph)
|
|
25
|
+
return (nodeId: string) => nodeReferenceSpan({ nodeId, nodeById, neighbors })
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
test('a backbone node states its own reference span', () => {
|
|
29
|
+
const span = setup(RGFA)
|
|
30
|
+
expect(span('v1+')).toEqual({ start: 0, end: 5 })
|
|
31
|
+
expect(span('v2+')).toEqual({ start: 5, end: 8 })
|
|
32
|
+
expect(span('v4+')).toEqual({ start: 10, end: 17 })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
// v5/v6's SO is an offset on `foo`, not on chr1, so it has no reference span of
|
|
36
|
+
// its own. The allele leaves after v2 (ends at 8) and rejoins at v4 (starts at
|
|
37
|
+
// 10), so what it replaces on chr1 is 8-10 — which is v3's span, the reference
|
|
38
|
+
// allele of the same bubble.
|
|
39
|
+
test('an off-reference allele resolves to the interval it branches into', () => {
|
|
40
|
+
const span = setup(RGFA)
|
|
41
|
+
expect(span('v5+')).toEqual({ start: 8, end: 10 })
|
|
42
|
+
expect(span('v6+')).toEqual({ start: 8, end: 10 })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// A pure insertion's flanks abut, so there is exactly one reference point to
|
|
46
|
+
// mark. The span collapses to it rather than covering either flank; the linear
|
|
47
|
+
// view floors the drawn width so it stays visible.
|
|
48
|
+
test('an insertion collapses to the reference point it is inserted at', () => {
|
|
49
|
+
const insertion = `S\tv1\tAAAAA\tLN:i:5\tSN:Z:chr1\tSO:i:0\tSR:i:0
|
|
50
|
+
S\tv2\tCCCCC\tLN:i:5\tSN:Z:chr1\tSO:i:5\tSR:i:0
|
|
51
|
+
S\tins\tGGG\tLN:i:3\tSN:Z:alt\tSO:i:0\tSR:i:1
|
|
52
|
+
L\tv1\t+\tv2\t+\t0M
|
|
53
|
+
L\tv1\t+\tins\t+\t0M
|
|
54
|
+
L\tins\t+\tv2\t+\t0M`
|
|
55
|
+
expect(setup(insertion)('ins+')).toEqual({ start: 5, end: 5 })
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
// At the edge of a fetched window a bubble can have only one flank present.
|
|
59
|
+
// Falling back to that flank's own span keeps the highlight in the right place
|
|
60
|
+
// instead of dropping it.
|
|
61
|
+
test('an allele with one flank in the window takes that flank span', () => {
|
|
62
|
+
const clipped = `S\tv1\tAAAAA\tLN:i:5\tSN:Z:chr1\tSO:i:0\tSR:i:0
|
|
63
|
+
S\talt\tGGG\tLN:i:3\tSN:Z:alt\tSO:i:0\tSR:i:1
|
|
64
|
+
L\tv1\t+\talt\t+\t0M`
|
|
65
|
+
expect(setup(clipped)('alt+')).toEqual({ start: 0, end: 5 })
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test('a node with no backbone anywhere in reach has no reference span', () => {
|
|
69
|
+
const plain = `S\t1\tACGT
|
|
70
|
+
S\t2\tGGCC
|
|
71
|
+
L\t1\t+\t2\t+\t0M`
|
|
72
|
+
expect(setup(plain)('1+')).toBeUndefined()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('an unknown node id has no reference span', () => {
|
|
76
|
+
expect(setup(RGFA)('nope+')).toBeUndefined()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('neighbors are undirected, so a bubble is reachable from either flank', () => {
|
|
80
|
+
const neighbors = buildNeighbors(convertGFAToGraph(parseGFA(RGFA)))
|
|
81
|
+
expect(neighbors.get('v2+')).toContain('v1+')
|
|
82
|
+
expect(neighbors.get('v2+')).toContain('v5+')
|
|
83
|
+
expect(neighbors.get('v4+')).toContain('v6+')
|
|
84
|
+
})
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { isBackbone } from './anchoredNodes'
|
|
2
|
+
|
|
3
|
+
import type { Graph, GraphNode } from './types'
|
|
4
|
+
|
|
5
|
+
// How long an allele chain can be and still be located from both ends. An
|
|
6
|
+
// allele is a run of off-reference segments between two backbone flanks, so the
|
|
7
|
+
// two flanks sit at *different* hop depths from any segment in the middle and
|
|
8
|
+
// the walk has to keep going after it finds the first one. Minigraph alleles are
|
|
9
|
+
// short runs, so this reaches both flanks throughout the fixtures; a longer run
|
|
10
|
+
// degrades to the flanks it did reach rather than to nothing.
|
|
11
|
+
const MAX_BACKBONE_HOPS = 4
|
|
12
|
+
|
|
13
|
+
export interface ReferenceSpan {
|
|
14
|
+
start: number
|
|
15
|
+
end: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Undirected adjacency over node ids. Edges carry the topology in both
|
|
19
|
+
// directions for this purpose: finding the backbone a node hangs off doesn't
|
|
20
|
+
// care which way the L line was written.
|
|
21
|
+
export function buildNeighbors(graph: Graph) {
|
|
22
|
+
const neighbors = new Map<string, string[]>()
|
|
23
|
+
function add(from: string, to: string) {
|
|
24
|
+
const list = neighbors.get(from)
|
|
25
|
+
if (list) {
|
|
26
|
+
list.push(to)
|
|
27
|
+
} else {
|
|
28
|
+
neighbors.set(from, [to])
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const edge of graph.edges) {
|
|
32
|
+
add(edge.from, edge.to)
|
|
33
|
+
add(edge.to, edge.from)
|
|
34
|
+
}
|
|
35
|
+
return neighbors
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The reference interval an allele branches into: between the end of the
|
|
39
|
+
// backbone segment upstream of it and the start of the one downstream.
|
|
40
|
+
// `[min(ends), max(starts)]` picks those two out of the reached flanks without
|
|
41
|
+
// needing to know which is which, and it degrades usefully in both directions —
|
|
42
|
+
// a pure insertion, whose flanks abut, collapses to the single reference point
|
|
43
|
+
// it is inserted at, and a bubble with only one flank inside the window yields
|
|
44
|
+
// that flank's own span.
|
|
45
|
+
function flankingSpan({
|
|
46
|
+
nodeId,
|
|
47
|
+
nodeById,
|
|
48
|
+
neighbors,
|
|
49
|
+
}: {
|
|
50
|
+
nodeId: string
|
|
51
|
+
nodeById: Map<string, GraphNode>
|
|
52
|
+
neighbors: Map<string, string[]>
|
|
53
|
+
}) {
|
|
54
|
+
const seen = new Set([nodeId])
|
|
55
|
+
let frontier = [nodeId]
|
|
56
|
+
let minEnd = Infinity
|
|
57
|
+
let maxStart = -Infinity
|
|
58
|
+
|
|
59
|
+
// Backbone nodes are terminal — they are collected, never queued — so the walk
|
|
60
|
+
// cannot cross the reference into a neighbouring bubble. It stays inside the
|
|
61
|
+
// allele it started in and stops on its own at the flanks.
|
|
62
|
+
for (let hop = 0; hop < MAX_BACKBONE_HOPS && frontier.length > 0; hop++) {
|
|
63
|
+
const next: string[] = []
|
|
64
|
+
for (const id of frontier) {
|
|
65
|
+
const adjacent = neighbors.get(id)
|
|
66
|
+
if (adjacent) {
|
|
67
|
+
for (const neighborId of adjacent) {
|
|
68
|
+
if (!seen.has(neighborId)) {
|
|
69
|
+
seen.add(neighborId)
|
|
70
|
+
const neighbor = nodeById.get(neighborId)
|
|
71
|
+
if (neighbor && isBackbone(neighbor)) {
|
|
72
|
+
minEnd = Math.min(minEnd, neighbor.stable.start + neighbor.length)
|
|
73
|
+
maxStart = Math.max(maxStart, neighbor.stable.start)
|
|
74
|
+
} else {
|
|
75
|
+
next.push(neighborId)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
frontier = next
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return minEnd === Infinity
|
|
85
|
+
? undefined
|
|
86
|
+
: {
|
|
87
|
+
start: Math.min(minEnd, maxStart),
|
|
88
|
+
end: Math.max(minEnd, maxStart),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The reference interval to highlight for a node, in the coordinates of the
|
|
93
|
+
// stable sequence the subgraph was cut from.
|
|
94
|
+
//
|
|
95
|
+
// A rank-0 node states its own, so the interval is exact. A rank>0 allele has
|
|
96
|
+
// none: its SO is an offset on a *different* stable sequence and is not
|
|
97
|
+
// comparable to the reference axis (see the axis note in
|
|
98
|
+
// layout/anchoredLayout.ts), so it is located by the backbone it branches from
|
|
99
|
+
// instead.
|
|
100
|
+
export function nodeReferenceSpan({
|
|
101
|
+
nodeId,
|
|
102
|
+
nodeById,
|
|
103
|
+
neighbors,
|
|
104
|
+
}: {
|
|
105
|
+
nodeId: string
|
|
106
|
+
nodeById: Map<string, GraphNode>
|
|
107
|
+
neighbors: Map<string, string[]>
|
|
108
|
+
}): ReferenceSpan | undefined {
|
|
109
|
+
const node = nodeById.get(nodeId)
|
|
110
|
+
return node === undefined
|
|
111
|
+
? undefined
|
|
112
|
+
: isBackbone(node)
|
|
113
|
+
? { start: node.stable.start, end: node.stable.start + node.length }
|
|
114
|
+
: flankingSpan({ nodeId, nodeById, neighbors })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Every node's midpoint on the reference, keyed by id: what a position-based
|
|
118
|
+
// colour scheme ramps over. Alleles come through the same flanking walk hover
|
|
119
|
+
// sync uses, so a bubble takes the colour of the reference interval it branches
|
|
120
|
+
// from instead of dropping out of the ramp — which is the whole point, since an
|
|
121
|
+
// allele is exactly what a linear view cannot draw at a position of its own.
|
|
122
|
+
export function referenceMidpoints(graph: Graph) {
|
|
123
|
+
const nodeById = new Map(graph.nodes.map(node => [node.id, node]))
|
|
124
|
+
const neighbors = buildNeighbors(graph)
|
|
125
|
+
const midpoints = new Map<string, number>()
|
|
126
|
+
for (const node of graph.nodes) {
|
|
127
|
+
const span = nodeReferenceSpan({ nodeId: node.id, nodeById, neighbors })
|
|
128
|
+
if (span) {
|
|
129
|
+
midpoints.set(node.id, (span.start + span.end) / 2)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return midpoints
|
|
133
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { describe, test } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { deletionEdges } from './deletionEdges'
|
|
4
|
+
import { graphLabels } from './graphLabels'
|
|
5
|
+
import { buildGeometry, computeReferenceRamp } from './renderer/GeometryBuilder'
|
|
6
|
+
import { EdgeSpatialIndex, SpatialIndex } from './util/SpatialIndex'
|
|
7
|
+
|
|
8
|
+
import type { Graph, GraphNode, NodeSegment } from './types'
|
|
9
|
+
|
|
10
|
+
// `pnpm bench`. Not part of `pnpm test`: these assert nothing, they report the
|
|
11
|
+
// cost of the four passes that run while a user is holding the mouse down.
|
|
12
|
+
//
|
|
13
|
+
// Which pass runs when, because the difference is the whole point of the
|
|
14
|
+
// numbers:
|
|
15
|
+
//
|
|
16
|
+
// graphLabels every mousemove of a pan, undebounced — it places labels in
|
|
17
|
+
// screen space, so a translate genuinely moves them
|
|
18
|
+
// buildGeometry once per debounced pan/zoom, and once per frame of a drag
|
|
19
|
+
// the two indexes lazily, on the first mousemove after the positions move
|
|
20
|
+
//
|
|
21
|
+
// So a millisecond in `graphLabels` costs about ten times what a millisecond in
|
|
22
|
+
// `buildGeometry` does, and both are budgeted against the ~10 ms that
|
|
23
|
+
// agent-docs/GRAPH_SCALE_AND_LOD.md measures a redraw of a 1-2k node cut at.
|
|
24
|
+
//
|
|
25
|
+
// Vitest 5 moved `bench` from a top-level export to a test-context fixture:
|
|
26
|
+
// each measurement is a `test()` that awaits `bench(name, fn).run()`.
|
|
27
|
+
|
|
28
|
+
// A bubble-chain rGFA: a rank-0 backbone with an alt allele and a bare deletion
|
|
29
|
+
// edge hanging off every other segment. Both anchored layouts draw this shape,
|
|
30
|
+
// and it is what the scale notes above were measured on.
|
|
31
|
+
function benchGraph(backboneCount: number) {
|
|
32
|
+
const nodes: GraphNode[] = []
|
|
33
|
+
const edges: Graph['edges'] = []
|
|
34
|
+
let pos = 0
|
|
35
|
+
for (let i = 0; i < backboneCount; i++) {
|
|
36
|
+
const length = 300 + (i % 17) * 40
|
|
37
|
+
nodes.push({
|
|
38
|
+
id: `b${i}+`,
|
|
39
|
+
name: `b${i}`,
|
|
40
|
+
length,
|
|
41
|
+
depth: 5,
|
|
42
|
+
stable: { refName: 'chr1', start: pos, rank: 0 },
|
|
43
|
+
})
|
|
44
|
+
pos += length
|
|
45
|
+
if (i > 0) {
|
|
46
|
+
edges.push({ from: `b${i - 1}+`, to: `b${i}+` })
|
|
47
|
+
}
|
|
48
|
+
if (i % 2 === 1 && i > 1) {
|
|
49
|
+
nodes.push({
|
|
50
|
+
id: `a${i}+`,
|
|
51
|
+
name: `a${i}`,
|
|
52
|
+
length: 40 + (i % 7) * 11,
|
|
53
|
+
depth: 2,
|
|
54
|
+
stable: { refName: `HG${i % 40}#1#chr1`, start: i * 100, rank: 1 },
|
|
55
|
+
})
|
|
56
|
+
edges.push({ from: `b${i - 2}+`, to: `a${i}+` })
|
|
57
|
+
edges.push({ from: `a${i}+`, to: `b${i}+` })
|
|
58
|
+
edges.push({ from: `b${i - 2}+`, to: `b${i}+` })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { name: 'bench', nodes, edges, anchoredBy: 'tags' } satisfies Graph
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The anchored layout's shape: one row per rank, y already in screen px, so the
|
|
65
|
+
// two axes are in different units — the case every `yToX` conversion is for.
|
|
66
|
+
function benchLayout(graph: Graph) {
|
|
67
|
+
const nodePositions: Record<string, NodeSegment[]> = {}
|
|
68
|
+
for (const node of graph.nodes) {
|
|
69
|
+
const { start, rank } = node.stable!
|
|
70
|
+
nodePositions[node.id] = [
|
|
71
|
+
{ x: start, y: rank * 20 },
|
|
72
|
+
{ x: start + node.length, y: rank * 20 },
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
return nodePositions
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const AXIS = { scaleX: 0.01, scaleY: 1 }
|
|
79
|
+
const VIEWPORT = { translateX: 40, translateY: 40, width: 900, height: 600 }
|
|
80
|
+
|
|
81
|
+
for (const backbone of [1000, 5000]) {
|
|
82
|
+
const graph = benchGraph(backbone)
|
|
83
|
+
const nodePositions = benchLayout(graph)
|
|
84
|
+
const nodeById = new Map(graph.nodes.map(n => [n.id, n]))
|
|
85
|
+
const nodeLengths = new Map(graph.nodes.map(n => [n.id, n.length]))
|
|
86
|
+
const deletions = deletionEdges(graph)
|
|
87
|
+
const bypassed = new Map(deletions.map(d => [d.edgeIndex, d.bypassed]))
|
|
88
|
+
const referenceRamp = computeReferenceRamp(graph, {
|
|
89
|
+
start: 0,
|
|
90
|
+
end: pos(graph),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
describe(`${graph.nodes.length} nodes / ${graph.edges.length} edges`, () => {
|
|
94
|
+
// The pan case: same layout, same zoom, a new translate. Everything a frame
|
|
95
|
+
// can reuse is reused, so this is the number that has to stay small.
|
|
96
|
+
let tx = VIEWPORT.translateX
|
|
97
|
+
test('graphLabels (pan)', async ({ bench }) => {
|
|
98
|
+
await bench('graphLabels (pan)', () => {
|
|
99
|
+
graphLabels({
|
|
100
|
+
nodePositions,
|
|
101
|
+
nodeLengths,
|
|
102
|
+
deletions,
|
|
103
|
+
axis: AXIS,
|
|
104
|
+
...VIEWPORT,
|
|
105
|
+
translateX: tx++ % 80,
|
|
106
|
+
})
|
|
107
|
+
}).run()
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
// The drag case: the positions moved in place, so nothing carries over.
|
|
111
|
+
let version = 0
|
|
112
|
+
test('graphLabels (drag)', async ({ bench }) => {
|
|
113
|
+
await bench('graphLabels (drag)', () => {
|
|
114
|
+
graphLabels({
|
|
115
|
+
nodePositions,
|
|
116
|
+
nodeLengths,
|
|
117
|
+
deletions,
|
|
118
|
+
axis: AXIS,
|
|
119
|
+
...VIEWPORT,
|
|
120
|
+
version: ++version,
|
|
121
|
+
})
|
|
122
|
+
}).run()
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('buildGeometry', async ({ bench }) => {
|
|
126
|
+
await bench('buildGeometry', () => {
|
|
127
|
+
buildGeometry({
|
|
128
|
+
nodePositions,
|
|
129
|
+
graph,
|
|
130
|
+
nodeById,
|
|
131
|
+
colorScheme: 'reference-position',
|
|
132
|
+
contigThickness: 10,
|
|
133
|
+
connectorThickness: 4,
|
|
134
|
+
drawPaths: false,
|
|
135
|
+
axis: AXIS,
|
|
136
|
+
referenceRamp,
|
|
137
|
+
deletions: bypassed,
|
|
138
|
+
})
|
|
139
|
+
}).run()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Once per graph, feeding the pass above.
|
|
143
|
+
test('computeReferenceRamp', async ({ bench }) => {
|
|
144
|
+
await bench('computeReferenceRamp', () => {
|
|
145
|
+
computeReferenceRamp(graph, { start: 0, end: pos(graph) })
|
|
146
|
+
}).run()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('SpatialIndex (nodes)', async ({ bench }) => {
|
|
150
|
+
await bench('SpatialIndex (nodes)', () => {
|
|
151
|
+
new SpatialIndex(nodePositions)
|
|
152
|
+
}).run()
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
test('EdgeSpatialIndex', async ({ bench }) => {
|
|
156
|
+
await bench('EdgeSpatialIndex', () => {
|
|
157
|
+
new EdgeSpatialIndex(
|
|
158
|
+
nodePositions,
|
|
159
|
+
graph,
|
|
160
|
+
false,
|
|
161
|
+
AXIS,
|
|
162
|
+
undefined,
|
|
163
|
+
bypassed,
|
|
164
|
+
)
|
|
165
|
+
}).run()
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function pos(graph: Graph) {
|
|
171
|
+
return graph.nodes.reduce((sum, n) => sum + n.length, 0)
|
|
172
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import stateModelFactory from './model'
|
|
2
|
+
import { MAX_PATH_COLORS } from './pathColors'
|
|
3
|
+
|
|
4
|
+
import type { RenderBatch, Renderer } from './renderer/types'
|
|
5
|
+
|
|
6
|
+
const mockRpcCall = vi.fn()
|
|
7
|
+
const mockSession = {
|
|
8
|
+
tracks: [] as { trackId: string; [key: string]: unknown }[],
|
|
9
|
+
rpcManager: { call: mockRpcCall },
|
|
10
|
+
assemblyNames: [] as string[],
|
|
11
|
+
views: [] as unknown[],
|
|
12
|
+
addView() {
|
|
13
|
+
return { id: 'view-1' }
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
vi.mock('@jbrowse/core/util', () => ({
|
|
18
|
+
getSession: () => mockSession,
|
|
19
|
+
isSessionModelWithWidgets: () => false,
|
|
20
|
+
parseLocString: () => ({}),
|
|
21
|
+
getEnv: () => ({}),
|
|
22
|
+
useWidthSetter: () => {},
|
|
23
|
+
measureText: () => 0,
|
|
24
|
+
IntervalTree: class {},
|
|
25
|
+
getSnapshot: () => ({}),
|
|
26
|
+
applySnapshot: () => {},
|
|
27
|
+
objectHash: () => '',
|
|
28
|
+
}))
|
|
29
|
+
|
|
30
|
+
// Partial, not wholesale: core modules pulled in transitively (BaseFeatureWidget's
|
|
31
|
+
// config schema) call ConfigurationSchema at module-eval time.
|
|
32
|
+
vi.mock(import('@jbrowse/core/configuration'), async importOriginal => ({
|
|
33
|
+
...(await importOriginal()),
|
|
34
|
+
readConfObject: vi.fn(),
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
const GFA = 'H\tVN:Z:1.0\nS\t1\tACGT\nS\t2\tGGCC\nL\t1\t+\t2\t+\t0M\n'
|
|
38
|
+
|
|
39
|
+
// A layout whose extent dwarfs the pane, which is what every real force layout
|
|
40
|
+
// produces: bandageAutoScale sizes a drawing in tens of thousands of FMMM units
|
|
41
|
+
// and zoom-to-fit is what brings it on screen.
|
|
42
|
+
const WIDE_LAYOUT = {
|
|
43
|
+
nodePositions: {
|
|
44
|
+
'1+': [
|
|
45
|
+
{ x: 0, y: 0 },
|
|
46
|
+
{ x: 40000, y: 0 },
|
|
47
|
+
],
|
|
48
|
+
'1-': [
|
|
49
|
+
{ x: 0, y: 30000 },
|
|
50
|
+
{ x: 40000, y: 30000 },
|
|
51
|
+
],
|
|
52
|
+
'2+': [
|
|
53
|
+
{ x: 50000, y: 0 },
|
|
54
|
+
{ x: 90000, y: 0 },
|
|
55
|
+
],
|
|
56
|
+
'2-': [
|
|
57
|
+
{ x: 50000, y: 30000 },
|
|
58
|
+
{ x: 90000, y: 30000 },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function fakeRenderer(uploads: RenderBatch[]) {
|
|
64
|
+
const renderer: Renderer = {
|
|
65
|
+
resize: () => {},
|
|
66
|
+
uploadGeometry: (batch: RenderBatch) => {
|
|
67
|
+
uploads.push(batch)
|
|
68
|
+
},
|
|
69
|
+
updateTransform: () => {},
|
|
70
|
+
render: () => {},
|
|
71
|
+
setNodeHighlights: () => {},
|
|
72
|
+
setEdgeHighlight: () => {},
|
|
73
|
+
destroy: () => {},
|
|
74
|
+
} as unknown as Renderer
|
|
75
|
+
return renderer
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Regression: a graph whose layout is much larger than the pane drew a blank
|
|
79
|
+
// canvas on the whole-file import path. buildGeometry culls to a viewport
|
|
80
|
+
// derived from the *current* transform, read untracked, so the build that runs
|
|
81
|
+
// before zoom-to-fit lands sees a pane-sized window of a 90,000-unit drawing
|
|
82
|
+
// and discards every node. Whether anything rebuilt afterwards decided whether
|
|
83
|
+
// the user saw the graph or an empty white pane, and on the 4,749-node HPRC
|
|
84
|
+
// chrM pggb graph they saw the pane.
|
|
85
|
+
test('a layout larger than the pane still uploads geometry', async () => {
|
|
86
|
+
mockRpcCall.mockImplementation((_id: string, method: string) =>
|
|
87
|
+
method === 'GraphComputeLayout'
|
|
88
|
+
? Promise.resolve({ result: WIDE_LAYOUT, duration: 5 })
|
|
89
|
+
: Promise.reject(new Error(`Unexpected RPC: ${method}`)),
|
|
90
|
+
)
|
|
91
|
+
const model = stateModelFactory().create({ type: 'GraphGenomeView' })
|
|
92
|
+
const uploads: RenderBatch[] = []
|
|
93
|
+
// The mount order the app actually has: the canvas is rendered only once
|
|
94
|
+
// `hasGraph` is true, so the graph and its layout exist *before* any autorun
|
|
95
|
+
// is installed, and the width lands after the canvas is measured.
|
|
96
|
+
await model.loadGFA(GFA, 'wide')
|
|
97
|
+
model.startRenderingBackend(fakeRenderer(uploads))
|
|
98
|
+
model.setWidth(1000)
|
|
99
|
+
// past the pan/zoom debounce, so any rebuild it would have scheduled has run
|
|
100
|
+
await new Promise(resolve => setTimeout(resolve, 400))
|
|
101
|
+
|
|
102
|
+
expect(uploads.length).toBeGreaterThan(0)
|
|
103
|
+
expect(uploads.at(-1)!.nodeStrokes.length).toBeGreaterThan(0)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
107
|
+
|
|
108
|
+
async function drawnModel() {
|
|
109
|
+
mockRpcCall.mockImplementation((_id: string, method: string) =>
|
|
110
|
+
method === 'GraphComputeLayout'
|
|
111
|
+
? Promise.resolve({ result: WIDE_LAYOUT, duration: 5 })
|
|
112
|
+
: Promise.reject(new Error(`Unexpected RPC: ${method}`)),
|
|
113
|
+
)
|
|
114
|
+
const model = stateModelFactory().create({ type: 'GraphGenomeView' })
|
|
115
|
+
await model.loadGFA(GFA, 'wide')
|
|
116
|
+
model.startRenderingBackend(fakeRenderer([]))
|
|
117
|
+
model.setWidth(1000)
|
|
118
|
+
// past the zoom-to-fit autorun and the pan/zoom debounce, so the transform
|
|
119
|
+
// has stopped moving on its own before a test moves it
|
|
120
|
+
await sleep(400)
|
|
121
|
+
return model
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// jbrowse-components' `installClearHoverOnViewportChange` rule, on this view's
|
|
125
|
+
// own three axes. A hover is set by a mousemove; a wheel zoom, a toolbar zoom
|
|
126
|
+
// and a pan all move the drawing under a stationary cursor and fire no pointer
|
|
127
|
+
// event, so without this the tooltip goes on naming a node that has moved away
|
|
128
|
+
// and `hoverHighlight` goes on publishing its span to the paired linear view.
|
|
129
|
+
test('a zoom drops a hover the pointer can no longer be over', async () => {
|
|
130
|
+
const model = await drawnModel()
|
|
131
|
+
model.setHoveredNode('1+')
|
|
132
|
+
model.setHoveredEdge(0)
|
|
133
|
+
model.setSelectedNode('1+')
|
|
134
|
+
|
|
135
|
+
model.zoom(1.5, 100, 100)
|
|
136
|
+
|
|
137
|
+
expect(model.hoveredNode).toBeNull()
|
|
138
|
+
expect(model.hoveredEdge).toBeNull()
|
|
139
|
+
// A click is a choice, and the content moving does not unmake it.
|
|
140
|
+
expect(model.selectedNode).toBe('1+')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('a pan drops it too', async () => {
|
|
144
|
+
const model = await drawnModel()
|
|
145
|
+
model.setHoveredNode('1+')
|
|
146
|
+
model.setTransform(model.scale, model.translateX + 40, model.translateY)
|
|
147
|
+
expect(model.hoveredNode).toBeNull()
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
// The two signals name different events and are read by different consumers:
|
|
151
|
+
// `viewportDirty` says the window onto the drawing moved past what was built,
|
|
152
|
+
// `positionsVersion` says the drawing itself did. Conflating them rebuilt a
|
|
153
|
+
// 12k-edge hit index on the first mousemove after every pan, for an index that
|
|
154
|
+
// is in layout units and had not changed.
|
|
155
|
+
test('a pan past the built window moves the viewport signal and a node drag moves the positions one', async () => {
|
|
156
|
+
const model = await drawnModel()
|
|
157
|
+
const dirty = model.viewportDirty
|
|
158
|
+
const positions = model.positionsVersion
|
|
159
|
+
|
|
160
|
+
model.setTransform(model.scale, model.translateX + 5 * model.width, 0)
|
|
161
|
+
await sleep(300)
|
|
162
|
+
expect(model.viewportDirty).toBeGreaterThan(dirty)
|
|
163
|
+
expect(model.positionsVersion).toBe(positions)
|
|
164
|
+
|
|
165
|
+
model.moveNode('1+', 10, 10)
|
|
166
|
+
await sleep(100)
|
|
167
|
+
expect(model.positionsVersion).toBe(positions + 1)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// A build covers a pane on every side of the one shown, so an ordinary pan is
|
|
171
|
+
// a repaint of strokes already uploaded: no debounce, no blank margin, no
|
|
172
|
+
// rebuild. A zoom changes what the strokes are and rebuilds however small.
|
|
173
|
+
test('a pan inside the built window rebuilds nothing, a zoom always does', async () => {
|
|
174
|
+
const model = await drawnModel()
|
|
175
|
+
const dirty = model.viewportDirty
|
|
176
|
+
|
|
177
|
+
model.setTransform(model.scale, model.translateX + 40, model.translateY + 40)
|
|
178
|
+
await sleep(300)
|
|
179
|
+
expect(model.viewportDirty).toBe(dirty)
|
|
180
|
+
|
|
181
|
+
model.zoom(1.01, 100, 100)
|
|
182
|
+
await sleep(300)
|
|
183
|
+
expect(model.viewportDirty).toBe(dirty + 1)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// The three consumers of a path colour disagreed about how many paths is too
|
|
187
|
+
// many: only the node stripes stopped. A Minigraph-Cactus cut writes one W
|
|
188
|
+
// record per haplotype and walks every backbone link with all of them, so the
|
|
189
|
+
// two that did not stop drew a stroke per path per edge — measured at 99,800
|
|
190
|
+
// strokes and 64 ms of geometry for 200 paths over 499 links — under a key
|
|
191
|
+
// hundreds of rows tall, naming stripes that were not drawn.
|
|
192
|
+
function pathGFA(count: number) {
|
|
193
|
+
const paths = Array.from(
|
|
194
|
+
{ length: count },
|
|
195
|
+
(_, i) => `P\tHG${i}#1#chr\t1+,2+\t*`,
|
|
196
|
+
).join('\n')
|
|
197
|
+
return `H\tVN:Z:1.0\nS\t1\tACGT\nS\t2\tGGCC\nL\t1\t+\t2\t+\t0M\n${paths}\n`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function pathModel(count: number) {
|
|
201
|
+
mockRpcCall.mockImplementation((_id: string, method: string) =>
|
|
202
|
+
method === 'GraphComputeLayout'
|
|
203
|
+
? Promise.resolve({ result: WIDE_LAYOUT, duration: 5 })
|
|
204
|
+
: Promise.reject(new Error(`Unexpected RPC: ${method}`)),
|
|
205
|
+
)
|
|
206
|
+
const model = stateModelFactory().create({ type: 'GraphGenomeView' })
|
|
207
|
+
await model.loadGFA(pathGFA(count), 'paths')
|
|
208
|
+
model.setDrawPaths(true)
|
|
209
|
+
return model
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
test('at the colour cap the paths are drawn and named', async () => {
|
|
213
|
+
const model = await pathModel(MAX_PATH_COLORS)
|
|
214
|
+
expect(model.pathCount).toBe(MAX_PATH_COLORS)
|
|
215
|
+
expect(model.effectiveDrawPaths).toBe(true)
|
|
216
|
+
expect(model.pathLegend).toHaveLength(MAX_PATH_COLORS)
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
test('past it the whole of drawPaths resolves off, key included', async () => {
|
|
220
|
+
const model = await pathModel(MAX_PATH_COLORS + 1)
|
|
221
|
+
expect(model.pathCount).toBe(MAX_PATH_COLORS + 1)
|
|
222
|
+
// the switch keeps the user's choice; what the drawing does with it is here
|
|
223
|
+
expect(model.drawPaths).toBe(true)
|
|
224
|
+
expect(model.effectiveDrawPaths).toBe(false)
|
|
225
|
+
expect(model.pathLegend).toEqual([])
|
|
226
|
+
})
|