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,723 @@
|
|
|
1
|
+
import { packAbgr } from '@jbrowse/core/util/colorBits'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
REFERENCE_RAMP_MAX_HUE,
|
|
5
|
+
buildGeometry,
|
|
6
|
+
computeReferenceRamp,
|
|
7
|
+
endTangent,
|
|
8
|
+
hslToRgb,
|
|
9
|
+
} from './GeometryBuilder'
|
|
10
|
+
|
|
11
|
+
import type { RenderBatch } from './types'
|
|
12
|
+
|
|
13
|
+
// the colour a node is drawn in, off its first stroke
|
|
14
|
+
function nodeColor(batch: RenderBatch, id: string) {
|
|
15
|
+
return batch.nodeStrokes[batch.nodeStrokeRuns.get(id)!.start]!.color
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// isotropic: one scale for both axes, which is every layout but the row ones
|
|
19
|
+
const iso = (scale = 1) => ({ scaleX: scale, scaleY: scale })
|
|
20
|
+
|
|
21
|
+
const simpleGraph = {
|
|
22
|
+
name: 'test',
|
|
23
|
+
nodes: [
|
|
24
|
+
{ id: 'A+', name: 'A', length: 100, depth: 1 },
|
|
25
|
+
{ id: 'B+', name: 'B', length: 200, depth: 2 },
|
|
26
|
+
],
|
|
27
|
+
edges: [{ from: 'A+', to: 'B+' }],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const simpleNodeById = new Map(simpleGraph.nodes.map(n => [n.id, n]))
|
|
31
|
+
|
|
32
|
+
const simplePositions = {
|
|
33
|
+
'A+': [
|
|
34
|
+
{ x: 0, y: 0 },
|
|
35
|
+
{ x: 10, y: 0 },
|
|
36
|
+
],
|
|
37
|
+
'B+': [
|
|
38
|
+
{ x: 20, y: 0 },
|
|
39
|
+
{ x: 30, y: 0 },
|
|
40
|
+
],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test('produces non-empty geometry for simple graph', () => {
|
|
44
|
+
const batch = buildGeometry({
|
|
45
|
+
axis: iso(),
|
|
46
|
+
nodePositions: simplePositions,
|
|
47
|
+
graph: simpleGraph,
|
|
48
|
+
nodeById: simpleNodeById,
|
|
49
|
+
colorScheme: 'uniform',
|
|
50
|
+
contigThickness: 5,
|
|
51
|
+
connectorThickness: 1.5,
|
|
52
|
+
drawPaths: false,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// one stroke per node, along the node's own polyline, at half its width
|
|
56
|
+
expect(batch.nodeStrokes).toHaveLength(2)
|
|
57
|
+
expect(batch.nodeStrokes[0]!.points).toBe(simplePositions['A+'])
|
|
58
|
+
expect(batch.nodeStrokes[0]!.thickness).toBe(2.5)
|
|
59
|
+
expect(batch.edgeCurves.length).toBeGreaterThan(0)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('produces different geometry for different color schemes', () => {
|
|
63
|
+
const opts = {
|
|
64
|
+
nodePositions: simplePositions,
|
|
65
|
+
graph: simpleGraph,
|
|
66
|
+
nodeById: simpleNodeById,
|
|
67
|
+
contigThickness: 5,
|
|
68
|
+
connectorThickness: 1.5,
|
|
69
|
+
drawPaths: false,
|
|
70
|
+
axis: iso(),
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const uniformBatch = buildGeometry({
|
|
74
|
+
...opts,
|
|
75
|
+
colorScheme: 'uniform' as const,
|
|
76
|
+
})
|
|
77
|
+
const depthBatch = buildGeometry({ ...opts, colorScheme: 'depth' as const })
|
|
78
|
+
|
|
79
|
+
expect(uniformBatch.nodeStrokes).toHaveLength(depthBatch.nodeStrokes.length)
|
|
80
|
+
expect(nodeColor(uniformBatch, 'B+')).not.toBe(nodeColor(depthBatch, 'B+'))
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('keys the strokes by node and by edge', () => {
|
|
84
|
+
const batch = buildGeometry({
|
|
85
|
+
nodePositions: simplePositions,
|
|
86
|
+
graph: simpleGraph,
|
|
87
|
+
nodeById: simpleNodeById,
|
|
88
|
+
colorScheme: 'uniform',
|
|
89
|
+
axis: iso(),
|
|
90
|
+
contigThickness: 5,
|
|
91
|
+
connectorThickness: 1.5,
|
|
92
|
+
drawPaths: false,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
expect(batch.nodeStrokeRuns.get('A+')).toEqual({ start: 0, count: 1 })
|
|
96
|
+
expect(batch.nodeStrokeRuns.get('B+')).toEqual({ start: 1, count: 1 })
|
|
97
|
+
expect(batch.edgeCurveRuns.get(0)).toEqual({ start: 0, count: 1 })
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('handles empty node positions gracefully', () => {
|
|
101
|
+
const batch = buildGeometry({
|
|
102
|
+
nodePositions: {},
|
|
103
|
+
axis: iso(),
|
|
104
|
+
graph: simpleGraph,
|
|
105
|
+
nodeById: simpleNodeById,
|
|
106
|
+
colorScheme: 'uniform',
|
|
107
|
+
contigThickness: 5,
|
|
108
|
+
connectorThickness: 1.5,
|
|
109
|
+
drawPaths: false,
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
expect(batch.nodeStrokes).toHaveLength(0)
|
|
113
|
+
expect(batch.edgeCurves).toHaveLength(0)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('handles graph with paths and drawPaths', () => {
|
|
117
|
+
const graphWithPaths = {
|
|
118
|
+
...simpleGraph,
|
|
119
|
+
edges: [{ from: 'A+', to: 'B+', pathIds: ['p1'] }],
|
|
120
|
+
paths: [{ name: 'p1', nodeIds: ['A+', 'B+'] }],
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const batch = buildGeometry({
|
|
124
|
+
nodePositions: simplePositions,
|
|
125
|
+
axis: iso(),
|
|
126
|
+
graph: graphWithPaths,
|
|
127
|
+
nodeById: simpleNodeById,
|
|
128
|
+
colorScheme: 'uniform',
|
|
129
|
+
contigThickness: 5,
|
|
130
|
+
connectorThickness: 1.5,
|
|
131
|
+
drawPaths: true,
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
expect(batch.nodeStrokes.length).toBeGreaterThan(0)
|
|
135
|
+
// one stroke per path crossing the edge, not one for the edge
|
|
136
|
+
expect(batch.edgeCurves).toHaveLength(1)
|
|
137
|
+
expect(batch.edgeCurveRuns.get(0)).toEqual({ start: 0, count: 1 })
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// A three-path graph where the middle node is the one B skips: A and C walk
|
|
141
|
+
// A+ -> B+ , B walks around it. That absence is what the stripes have to show.
|
|
142
|
+
const carriageGraph = {
|
|
143
|
+
...simpleGraph,
|
|
144
|
+
edges: [{ from: 'A+', to: 'B+', pathIds: ['pA', 'pB', 'pC'] }],
|
|
145
|
+
paths: [
|
|
146
|
+
{ name: 'pA', nodeIds: ['A+', 'B+'] },
|
|
147
|
+
{ name: 'pB', nodeIds: ['A+'] },
|
|
148
|
+
{ name: 'pC', nodeIds: ['A+', 'B+'] },
|
|
149
|
+
],
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const carriageOpts = {
|
|
153
|
+
nodePositions: simplePositions,
|
|
154
|
+
axis: iso(),
|
|
155
|
+
graph: carriageGraph,
|
|
156
|
+
nodeById: simpleNodeById,
|
|
157
|
+
colorScheme: 'uniform' as const,
|
|
158
|
+
contigThickness: 12,
|
|
159
|
+
connectorThickness: 1.5,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// distinct colors across a node's strokes
|
|
163
|
+
function nodeColors(batch: RenderBatch, nodeId: string) {
|
|
164
|
+
const run = batch.nodeStrokeRuns.get(nodeId)!
|
|
165
|
+
return new Set(
|
|
166
|
+
batch.nodeStrokes
|
|
167
|
+
.slice(run.start, run.start + run.count)
|
|
168
|
+
.map(stroke => stroke.color),
|
|
169
|
+
)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
test('drawPaths stripes a node once per path that visits it', () => {
|
|
173
|
+
const batch = buildGeometry({ ...carriageOpts, drawPaths: true })
|
|
174
|
+
|
|
175
|
+
// A+ is on all three paths, B+ on two: the count of colors down the node is
|
|
176
|
+
// its carriage, which is the whole point of striping the node rather than
|
|
177
|
+
// only the edge
|
|
178
|
+
expect(nodeColors(batch, 'A+').size).toBe(3)
|
|
179
|
+
expect(nodeColors(batch, 'B+').size).toBe(2)
|
|
180
|
+
// and B+'s two are the same two colors A+ has, not a re-numbered pair: the
|
|
181
|
+
// slot is fixed by the path's position in the legend, so a gap lands in the
|
|
182
|
+
// same place on every node
|
|
183
|
+
expect(
|
|
184
|
+
[...nodeColors(batch, 'B+')].every(c => nodeColors(batch, 'A+').has(c)),
|
|
185
|
+
).toBe(true)
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
test('stripes divide the node width rather than inflating it', () => {
|
|
189
|
+
const striped = buildGeometry({ ...carriageOpts, drawPaths: true })
|
|
190
|
+
const plain = buildGeometry({ ...carriageOpts, drawPaths: false })
|
|
191
|
+
|
|
192
|
+
const halfWidth = (batch: RenderBatch) =>
|
|
193
|
+
Math.max(...batch.nodeStrokes.map(stroke => stroke.thickness))
|
|
194
|
+
// three stripes of a third the width each, laid across the same 12 units
|
|
195
|
+
expect(halfWidth(striped)).toBe(halfWidth(plain) / 3)
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('a node keeps its scheme color when a stripe would be sub-pixel', () => {
|
|
199
|
+
// contigThickness is screen px, so a thin node over three paths gives a
|
|
200
|
+
// one-px stripe each: aliasing between neighbours rather than three colors
|
|
201
|
+
const thin = buildGeometry({
|
|
202
|
+
...carriageOpts,
|
|
203
|
+
contigThickness: 3,
|
|
204
|
+
drawPaths: true,
|
|
205
|
+
})
|
|
206
|
+
expect(nodeColors(thin, 'A+').size).toBe(1)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test('the stripe offset is in world units, so zoom does not fan them apart', () => {
|
|
210
|
+
const yAt = (batch: RenderBatch, nodeId: string) => {
|
|
211
|
+
const run = batch.nodeStrokeRuns.get(nodeId)!
|
|
212
|
+
const ys = batch.nodeStrokes
|
|
213
|
+
.slice(run.start, run.start + run.count)
|
|
214
|
+
.flatMap(stroke => stroke.points.map(p => p.y))
|
|
215
|
+
return Math.max(...ys) - Math.min(...ys)
|
|
216
|
+
}
|
|
217
|
+
// the stripes are laid across a width stated in screen px, so in world units
|
|
218
|
+
// they must spread twice as far when the view is drawn at half the zoom
|
|
219
|
+
const near = buildGeometry({ ...carriageOpts, drawPaths: true, axis: iso() })
|
|
220
|
+
const far = buildGeometry({
|
|
221
|
+
...carriageOpts,
|
|
222
|
+
drawPaths: true,
|
|
223
|
+
axis: iso(0.5),
|
|
224
|
+
})
|
|
225
|
+
expect(yAt(far, 'A+')).toBeCloseTo(yAt(near, 'A+') * 2, 5)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
test('a many-path graph is not striped at all', () => {
|
|
229
|
+
const manyPaths = {
|
|
230
|
+
...simpleGraph,
|
|
231
|
+
paths: Array.from({ length: 40 }, (_, i) => ({
|
|
232
|
+
name: `p${i}`,
|
|
233
|
+
nodeIds: ['A+', 'B+'],
|
|
234
|
+
})),
|
|
235
|
+
}
|
|
236
|
+
const batch = buildGeometry({
|
|
237
|
+
...carriageOpts,
|
|
238
|
+
graph: manyPaths,
|
|
239
|
+
drawPaths: true,
|
|
240
|
+
})
|
|
241
|
+
// forty colors down one node is not a reading, so the node keeps its own
|
|
242
|
+
expect(nodeColors(batch, 'A+').size).toBe(1)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
test('viewport culling skips off-screen nodes', () => {
|
|
246
|
+
const batch = buildGeometry({
|
|
247
|
+
axis: iso(),
|
|
248
|
+
nodePositions: simplePositions,
|
|
249
|
+
graph: simpleGraph,
|
|
250
|
+
nodeById: simpleNodeById,
|
|
251
|
+
colorScheme: 'uniform',
|
|
252
|
+
contigThickness: 5,
|
|
253
|
+
connectorThickness: 1.5,
|
|
254
|
+
drawPaths: false,
|
|
255
|
+
viewportBounds: { minX: -5, minY: -5, maxX: 15, maxY: 5 },
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
expect(batch.nodeStrokeRuns.has('A+')).toBe(true)
|
|
259
|
+
expect(batch.nodeStrokeRuns.has('B+')).toBe(false)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// The reference-anchored layouts put x in bp, so a backbone segment is routinely
|
|
263
|
+
// wider than the window with both of its endpoints outside it. Culling on
|
|
264
|
+
// endpoint containment dropped exactly the segment those layouts exist to show.
|
|
265
|
+
test('viewport culling keeps a node spanning the whole viewport', () => {
|
|
266
|
+
const nodes = [
|
|
267
|
+
{ id: 'backbone+', name: 'backbone', length: 50_000, depth: 1 },
|
|
268
|
+
]
|
|
269
|
+
const batch = buildGeometry({
|
|
270
|
+
nodePositions: {
|
|
271
|
+
'backbone+': [
|
|
272
|
+
{ x: 0, y: 0 },
|
|
273
|
+
{ x: 50_000, y: 0 },
|
|
274
|
+
],
|
|
275
|
+
},
|
|
276
|
+
graph: { name: 'test', nodes, edges: [] },
|
|
277
|
+
nodeById: new Map(nodes.map(n => [n.id, n])),
|
|
278
|
+
colorScheme: 'uniform',
|
|
279
|
+
axis: iso(),
|
|
280
|
+
contigThickness: 5,
|
|
281
|
+
connectorThickness: 1.5,
|
|
282
|
+
drawPaths: false,
|
|
283
|
+
viewportBounds: { minX: 20_000, minY: -100, maxX: 21_000, maxY: 100 },
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
expect(batch.nodeStrokeRuns.has('backbone+')).toBe(true)
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test('node-length color scheme produces distinct colors for different lengths', () => {
|
|
290
|
+
const nodes = [
|
|
291
|
+
{ id: 'short+', name: 'short', length: 10, depth: 1 },
|
|
292
|
+
{ id: 'long+', name: 'long', length: 10000, depth: 1 },
|
|
293
|
+
]
|
|
294
|
+
const positions = {
|
|
295
|
+
'short+': [
|
|
296
|
+
{ x: 0, y: 0 },
|
|
297
|
+
{ x: 10, y: 0 },
|
|
298
|
+
],
|
|
299
|
+
'long+': [
|
|
300
|
+
{ x: 20, y: 0 },
|
|
301
|
+
{ x: 30, y: 0 },
|
|
302
|
+
],
|
|
303
|
+
}
|
|
304
|
+
const graph = { name: 'test', nodes, edges: [] }
|
|
305
|
+
const nodeById = new Map(nodes.map(n => [n.id, n]))
|
|
306
|
+
|
|
307
|
+
const batch = buildGeometry({
|
|
308
|
+
nodePositions: positions,
|
|
309
|
+
graph,
|
|
310
|
+
nodeById,
|
|
311
|
+
colorScheme: 'node-length',
|
|
312
|
+
axis: iso(),
|
|
313
|
+
contigThickness: 5,
|
|
314
|
+
connectorThickness: 1.5,
|
|
315
|
+
drawPaths: false,
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
expect(nodeColor(batch, 'short+')).not.toBe(nodeColor(batch, 'long+'))
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
test('rainbow color scheme produces distinct colors for nodes at different indices', () => {
|
|
322
|
+
const nodes = [
|
|
323
|
+
{ id: 'A+', name: 'A', length: 100, depth: 1 },
|
|
324
|
+
{ id: 'B+', name: 'B', length: 100, depth: 1 },
|
|
325
|
+
{ id: 'C+', name: 'C', length: 100, depth: 1 },
|
|
326
|
+
]
|
|
327
|
+
const positions = {
|
|
328
|
+
'A+': [
|
|
329
|
+
{ x: 0, y: 0 },
|
|
330
|
+
{ x: 10, y: 0 },
|
|
331
|
+
],
|
|
332
|
+
'B+': [
|
|
333
|
+
{ x: 20, y: 0 },
|
|
334
|
+
{ x: 30, y: 0 },
|
|
335
|
+
],
|
|
336
|
+
'C+': [
|
|
337
|
+
{ x: 40, y: 0 },
|
|
338
|
+
{ x: 50, y: 0 },
|
|
339
|
+
],
|
|
340
|
+
}
|
|
341
|
+
const graph = { name: 'test', nodes, edges: [] }
|
|
342
|
+
const nodeById = new Map(nodes.map(n => [n.id, n]))
|
|
343
|
+
|
|
344
|
+
const batch = buildGeometry({
|
|
345
|
+
nodePositions: positions,
|
|
346
|
+
graph,
|
|
347
|
+
nodeById,
|
|
348
|
+
colorScheme: 'rainbow',
|
|
349
|
+
axis: iso(),
|
|
350
|
+
contigThickness: 5,
|
|
351
|
+
connectorThickness: 1.5,
|
|
352
|
+
drawPaths: false,
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
const colors = ['A+', 'B+', 'C+'].map(id => nodeColor(batch, id))
|
|
356
|
+
expect(new Set(colors).size).toBe(3)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
test('builds geometry for a self-loop edge', () => {
|
|
360
|
+
const graph = {
|
|
361
|
+
name: 'test',
|
|
362
|
+
nodes: [{ id: 'A+', name: 'A', length: 100, depth: 1 }],
|
|
363
|
+
edges: [{ from: 'A+', to: 'A+' }],
|
|
364
|
+
}
|
|
365
|
+
const batch = buildGeometry({
|
|
366
|
+
nodePositions: {
|
|
367
|
+
'A+': [
|
|
368
|
+
{ x: 0, y: 0 },
|
|
369
|
+
{ x: 10, y: 0 },
|
|
370
|
+
],
|
|
371
|
+
},
|
|
372
|
+
graph,
|
|
373
|
+
nodeById: new Map(graph.nodes.map(n => [n.id, n])),
|
|
374
|
+
colorScheme: 'uniform',
|
|
375
|
+
contigThickness: 5,
|
|
376
|
+
connectorThickness: 1.5,
|
|
377
|
+
drawPaths: false,
|
|
378
|
+
axis: iso(),
|
|
379
|
+
})
|
|
380
|
+
expect(batch.nodeStrokes).toHaveLength(1)
|
|
381
|
+
// a self loop is two curves, so it strokes as one two-segment path
|
|
382
|
+
expect(batch.edgeCurves[0]!.curves).toHaveLength(2)
|
|
383
|
+
expect(batch.edgeCurveRuns.has(0)).toBe(true)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
test('skips edges that reference missing node positions', () => {
|
|
387
|
+
const graph = {
|
|
388
|
+
name: 'test',
|
|
389
|
+
nodes: [{ id: 'A+', name: 'A', length: 100, depth: 1 }],
|
|
390
|
+
edges: [{ from: 'A+', to: 'missing+' }],
|
|
391
|
+
}
|
|
392
|
+
const batch = buildGeometry({
|
|
393
|
+
nodePositions: {
|
|
394
|
+
'A+': [
|
|
395
|
+
{ x: 0, y: 0 },
|
|
396
|
+
{ x: 10, y: 0 },
|
|
397
|
+
],
|
|
398
|
+
},
|
|
399
|
+
graph,
|
|
400
|
+
nodeById: new Map(graph.nodes.map(n => [n.id, n])),
|
|
401
|
+
colorScheme: 'uniform',
|
|
402
|
+
contigThickness: 5,
|
|
403
|
+
connectorThickness: 1.5,
|
|
404
|
+
drawPaths: false,
|
|
405
|
+
axis: iso(),
|
|
406
|
+
})
|
|
407
|
+
// node still builds; the dangling edge is silently dropped
|
|
408
|
+
expect(batch.nodeStrokes).toHaveLength(1)
|
|
409
|
+
expect(batch.edgeCurves).toHaveLength(0)
|
|
410
|
+
expect(batch.edgeCurveRuns.size).toBe(0)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
// Arrowheads used to take their angle from the last two points of the
|
|
414
|
+
// tessellated edge mesh. The mesh is gone, so the angle comes from the curve's
|
|
415
|
+
// analytic end tangent instead — which has to agree with the direction the
|
|
416
|
+
// tessellation was reporting, or every arrowhead points somewhere else.
|
|
417
|
+
describe('endTangent', () => {
|
|
418
|
+
// direction of a cubic just short of its endpoint, i.e. what walking the
|
|
419
|
+
// tessellated points to the end and taking the last step measured
|
|
420
|
+
function numericEndAngle(c: {
|
|
421
|
+
x0: number
|
|
422
|
+
y0: number
|
|
423
|
+
cx0: number
|
|
424
|
+
cy0: number
|
|
425
|
+
cx1: number
|
|
426
|
+
cy1: number
|
|
427
|
+
x1: number
|
|
428
|
+
y1: number
|
|
429
|
+
}) {
|
|
430
|
+
const at = (t: number) => {
|
|
431
|
+
const u = 1 - t
|
|
432
|
+
return {
|
|
433
|
+
x:
|
|
434
|
+
u ** 3 * c.x0 +
|
|
435
|
+
3 * u * u * t * c.cx0 +
|
|
436
|
+
3 * u * t * t * c.cx1 +
|
|
437
|
+
t ** 3 * c.x1,
|
|
438
|
+
y:
|
|
439
|
+
u ** 3 * c.y0 +
|
|
440
|
+
3 * u * u * t * c.cy0 +
|
|
441
|
+
3 * u * t * t * c.cy1 +
|
|
442
|
+
t ** 3 * c.y1,
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// a short secant, which is what the last tessellated step was; small enough
|
|
446
|
+
// that its own error stays well under the tolerance below
|
|
447
|
+
const a = at(1 - 1e-6)
|
|
448
|
+
const b = at(1)
|
|
449
|
+
return Math.atan2(b.y - a.y, b.x - a.x)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const curves = [
|
|
453
|
+
{ x0: 0, y0: 0, cx0: 30, cy0: 0, cx1: 70, cy1: 40, x1: 100, y1: 40 },
|
|
454
|
+
{ x0: 0, y0: 0, cx0: 0, cy0: 80, cx1: 100, cy1: 80, x1: 100, y1: 0 },
|
|
455
|
+
{ x0: 5, y0: 5, cx0: -20, cy0: 60, cx1: -60, cy1: -30, x1: -100, y1: 10 },
|
|
456
|
+
]
|
|
457
|
+
|
|
458
|
+
test.each(curves)('matches the numeric tangent of %j', curve => {
|
|
459
|
+
expect(endTangent(curve)).toBeCloseTo(numericEndAngle(curve), 4)
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
// Degenerate control point: the derivative at t=1 vanishes, so there is no
|
|
463
|
+
// tangent to read and the chord is the only direction available.
|
|
464
|
+
test('falls back to the chord when the control point is on the endpoint', () => {
|
|
465
|
+
expect(
|
|
466
|
+
endTangent({
|
|
467
|
+
x0: 0,
|
|
468
|
+
y0: 0,
|
|
469
|
+
cx0: 0,
|
|
470
|
+
cy0: 0,
|
|
471
|
+
cx1: 10,
|
|
472
|
+
cy1: 10,
|
|
473
|
+
x1: 10,
|
|
474
|
+
y1: 10,
|
|
475
|
+
}),
|
|
476
|
+
).toBeCloseTo(Math.atan2(10, 10), 6)
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
describe('the reference-position ramp', () => {
|
|
481
|
+
// Two backbone segments at either end of a 1000 bp window and one allele
|
|
482
|
+
// bridging them, which is the shape every graph-over-linear figure has.
|
|
483
|
+
const nodes = [
|
|
484
|
+
{
|
|
485
|
+
id: 'left+',
|
|
486
|
+
name: 'left',
|
|
487
|
+
length: 100,
|
|
488
|
+
depth: 2,
|
|
489
|
+
stable: { refName: 'chr', start: 0, rank: 0 },
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
id: 'right+',
|
|
493
|
+
name: 'right',
|
|
494
|
+
length: 100,
|
|
495
|
+
depth: 2,
|
|
496
|
+
stable: { refName: 'chr', start: 900, rank: 0 },
|
|
497
|
+
},
|
|
498
|
+
{
|
|
499
|
+
id: 'alt+',
|
|
500
|
+
name: 'alt',
|
|
501
|
+
length: 5000,
|
|
502
|
+
depth: 1,
|
|
503
|
+
stable: { refName: 'other', start: 7, rank: 1 },
|
|
504
|
+
},
|
|
505
|
+
// a second allele, branching from the far end of the window rather than the
|
|
506
|
+
// middle of it, so a position-dependent colouring would paint it differently
|
|
507
|
+
{
|
|
508
|
+
id: 'alt2+',
|
|
509
|
+
name: 'alt2',
|
|
510
|
+
length: 400,
|
|
511
|
+
depth: 1,
|
|
512
|
+
stable: { refName: 'other', start: 900, rank: 1 },
|
|
513
|
+
},
|
|
514
|
+
{ id: 'loose+', name: 'loose', length: 10, depth: 1 },
|
|
515
|
+
]
|
|
516
|
+
const graph = {
|
|
517
|
+
name: 'test',
|
|
518
|
+
nodes,
|
|
519
|
+
edges: [
|
|
520
|
+
{ from: 'left+', to: 'alt+' },
|
|
521
|
+
{ from: 'alt+', to: 'right+' },
|
|
522
|
+
{ from: 'right+', to: 'alt2+' },
|
|
523
|
+
],
|
|
524
|
+
}
|
|
525
|
+
const positions = Object.fromEntries(
|
|
526
|
+
nodes.map((n, i) => [
|
|
527
|
+
n.id,
|
|
528
|
+
[
|
|
529
|
+
{ x: i * 20, y: 0 },
|
|
530
|
+
{ x: i * 20 + 10, y: 0 },
|
|
531
|
+
],
|
|
532
|
+
]),
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
function colorsFor(colorDomain?: { start: number; end: number }) {
|
|
536
|
+
const batch = buildGeometry({
|
|
537
|
+
nodePositions: positions,
|
|
538
|
+
graph,
|
|
539
|
+
nodeById: new Map(nodes.map(n => [n.id, n])),
|
|
540
|
+
colorScheme: 'reference-position',
|
|
541
|
+
// what the model holds against the graph and hands over; see BuildOptions
|
|
542
|
+
referenceRamp: computeReferenceRamp(graph, colorDomain),
|
|
543
|
+
axis: iso(),
|
|
544
|
+
contigThickness: 5,
|
|
545
|
+
connectorThickness: 1.5,
|
|
546
|
+
drawPaths: false,
|
|
547
|
+
})
|
|
548
|
+
return Object.fromEntries(nodes.map(n => [n.id, nodeColor(batch, n.id)]))
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// The contract a linear track's `color` jexl reproduces. Written out as the
|
|
552
|
+
// arithmetic rather than as a captured constant, so an edit to the ramp fails
|
|
553
|
+
// here instead of silently desynchronising the two panels.
|
|
554
|
+
function expectedColor(mid: number, start: number, span: number) {
|
|
555
|
+
const frac = Math.max(0, Math.min(1, (mid - start) / span))
|
|
556
|
+
const [r, g, b] = hslToRgb(frac * REFERENCE_RAMP_MAX_HUE, 0.7, 0.5)
|
|
557
|
+
return packAbgr(
|
|
558
|
+
Math.round(r * 255),
|
|
559
|
+
Math.round(g * 255),
|
|
560
|
+
Math.round(b * 255),
|
|
561
|
+
255,
|
|
562
|
+
)
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
test('paints a backbone segment the hue of its own midpoint', () => {
|
|
566
|
+
const colors = colorsFor({ start: 0, end: 1000 })
|
|
567
|
+
expect(colors['left+']).toBe(expectedColor(50, 0, 1000))
|
|
568
|
+
expect(colors['right+']).toBe(expectedColor(950, 0, 1000))
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
// The ramp is the reference path's colouring, so an allele is taken off it
|
|
572
|
+
// entirely: a hue here — even a pale one — says the allele IS the reference at
|
|
573
|
+
// that position, which is the opposite of what it is.
|
|
574
|
+
test('paints an allele off the ramp, in the flat alternative colour', () => {
|
|
575
|
+
const colors = colorsFor({ start: 0, end: 1000 })
|
|
576
|
+
expect(colors['alt+']).toBe(packAbgr(60, 65, 72, 255))
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
// Every allele the same colour, wherever it branches: the ramp is not what
|
|
580
|
+
// distinguishes them, and a reader looking for one is reading position off a
|
|
581
|
+
// node that has none.
|
|
582
|
+
test('paints two alleles at different positions the same colour', () => {
|
|
583
|
+
const colors = colorsFor({ start: 0, end: 1000 })
|
|
584
|
+
expect(colors['alt+']).toBe(colors['alt2+'])
|
|
585
|
+
expect(colors['alt+']).not.toBe(expectedColor(500, 0, 1000))
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
test('greys a node with no reference position at all', () => {
|
|
589
|
+
expect(colorsFor({ start: 0, end: 1000 })['loose+']).toBe(
|
|
590
|
+
packAbgr(160, 160, 160, 255),
|
|
591
|
+
)
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
// A whole-file import states no region, so the ramp spans what it drew. The
|
|
595
|
+
// two backbone segments then take the ends of the ramp rather than sitting in
|
|
596
|
+
// the middle of a window nothing declared.
|
|
597
|
+
test('falls back to the drawn extent with no region', () => {
|
|
598
|
+
const colors = colorsFor()
|
|
599
|
+
expect(colors['left+']).toBe(expectedColor(50, 50, 900))
|
|
600
|
+
expect(colors['right+']).toBe(expectedColor(950, 50, 900))
|
|
601
|
+
})
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
// In both anchored layouts consecutive segments meet at exactly one point, so
|
|
605
|
+
// every backbone link has a zero-length chord. The ribbon fan used to be
|
|
606
|
+
// perpendicular to that chord and gave up when there was none, which dropped the
|
|
607
|
+
// edge outright: no curve, no arrowhead, nothing to hover, along the whole
|
|
608
|
+
// backbone, and the abutting nodes hid it. Only reachable with drawPaths on,
|
|
609
|
+
// which is why it survived — the plain path draws the edge either way.
|
|
610
|
+
describe('abutting nodes under drawPaths', () => {
|
|
611
|
+
const abutting = {
|
|
612
|
+
name: 'abutting',
|
|
613
|
+
nodes: [
|
|
614
|
+
{ id: 'A+', name: 'A', length: 100, depth: 1 },
|
|
615
|
+
{ id: 'B+', name: 'B', length: 100, depth: 1 },
|
|
616
|
+
],
|
|
617
|
+
edges: [{ from: 'A+', to: 'B+', pathIds: ['p1', 'p2'] }],
|
|
618
|
+
paths: [
|
|
619
|
+
{ name: 'p1', nodeIds: ['A+', 'B+'] },
|
|
620
|
+
{ name: 'p2', nodeIds: ['A+', 'B+'] },
|
|
621
|
+
],
|
|
622
|
+
}
|
|
623
|
+
const nodeById = new Map(abutting.nodes.map(n => [n.id, n]))
|
|
624
|
+
const positions = {
|
|
625
|
+
'A+': [
|
|
626
|
+
{ x: 0, y: 0 },
|
|
627
|
+
{ x: 100, y: 0 },
|
|
628
|
+
],
|
|
629
|
+
'B+': [
|
|
630
|
+
{ x: 100, y: 0 },
|
|
631
|
+
{ x: 200, y: 0 },
|
|
632
|
+
],
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function build(drawPaths: boolean) {
|
|
636
|
+
return buildGeometry({
|
|
637
|
+
axis: iso(),
|
|
638
|
+
nodePositions: positions,
|
|
639
|
+
graph: abutting,
|
|
640
|
+
nodeById,
|
|
641
|
+
colorScheme: 'uniform',
|
|
642
|
+
contigThickness: 10,
|
|
643
|
+
connectorThickness: 4,
|
|
644
|
+
drawPaths,
|
|
645
|
+
})
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
test('draws one ribbon per path where a plain edge draws one curve', () => {
|
|
649
|
+
expect(build(false).edgeCurves).toHaveLength(1)
|
|
650
|
+
expect(build(true).edgeCurves).toHaveLength(2)
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
test('fans them across the row, since the nodes run along it', () => {
|
|
654
|
+
const ys = build(true).edgeCurves.map(c => c.curves[0]!.y0)
|
|
655
|
+
// the node's own direction stands in for the missing chord, so the two
|
|
656
|
+
// ribbons straddle y = 0 rather than landing on top of each other
|
|
657
|
+
expect(ys).toEqual([-1.5, 1.5])
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
test('keys the ribbons under the edge, so they stay hoverable', () => {
|
|
661
|
+
expect(build(true).edgeCurveRuns.get(0)).toEqual({ start: 0, count: 2 })
|
|
662
|
+
})
|
|
663
|
+
})
|
|
664
|
+
|
|
665
|
+
// An arrowhead states which way a link is read, which belongs to the edge and
|
|
666
|
+
// not to each path crossing it. One head per ribbon said it once per haplotype:
|
|
667
|
+
// five stacked heads in five colours at each joint, which is the confetti that
|
|
668
|
+
// made drawPaths unusable on a force layout and reached the anchored ones the
|
|
669
|
+
// moment their abutting edges started drawing again.
|
|
670
|
+
describe('one arrowhead per edge, whatever crosses it', () => {
|
|
671
|
+
const graph = {
|
|
672
|
+
name: 'ribbons',
|
|
673
|
+
nodes: [
|
|
674
|
+
{ id: 'A+', name: 'A', length: 100, depth: 1 },
|
|
675
|
+
{ id: 'B+', name: 'B', length: 100, depth: 1 },
|
|
676
|
+
],
|
|
677
|
+
edges: [{ from: 'A+', to: 'B+', pathIds: ['p1', 'p2', 'p3'] }],
|
|
678
|
+
paths: [
|
|
679
|
+
{ name: 'p1', nodeIds: ['A+', 'B+'] },
|
|
680
|
+
{ name: 'p2', nodeIds: ['A+', 'B+'] },
|
|
681
|
+
{ name: 'p3', nodeIds: ['A+', 'B+'] },
|
|
682
|
+
],
|
|
683
|
+
}
|
|
684
|
+
const nodeById = new Map(graph.nodes.map(n => [n.id, n]))
|
|
685
|
+
const positions = {
|
|
686
|
+
'A+': [
|
|
687
|
+
{ x: 0, y: 0 },
|
|
688
|
+
{ x: 100, y: 0 },
|
|
689
|
+
],
|
|
690
|
+
'B+': [
|
|
691
|
+
{ x: 200, y: 0 },
|
|
692
|
+
{ x: 300, y: 0 },
|
|
693
|
+
],
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function build(drawPaths: boolean) {
|
|
697
|
+
return buildGeometry({
|
|
698
|
+
axis: iso(),
|
|
699
|
+
nodePositions: positions,
|
|
700
|
+
graph,
|
|
701
|
+
nodeById,
|
|
702
|
+
colorScheme: 'uniform',
|
|
703
|
+
contigThickness: 10,
|
|
704
|
+
connectorThickness: 4,
|
|
705
|
+
drawPaths,
|
|
706
|
+
})
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
const heads = (b: ReturnType<typeof build>) => b.arrows.length
|
|
710
|
+
|
|
711
|
+
test('three ribbons still leave one head', () => {
|
|
712
|
+
expect(heads(build(false))).toBe(1)
|
|
713
|
+
expect(build(true).edgeCurves).toHaveLength(3)
|
|
714
|
+
expect(heads(build(true))).toBe(1)
|
|
715
|
+
})
|
|
716
|
+
|
|
717
|
+
test('and it is the edge colour, not a haplotype it would have to pick', () => {
|
|
718
|
+
const plain = build(false)
|
|
719
|
+
const ribboned = build(true)
|
|
720
|
+
const colorOf = (b: ReturnType<typeof build>) => b.arrows[0]!.color
|
|
721
|
+
expect(colorOf(ribboned)).toBe(colorOf(plain))
|
|
722
|
+
})
|
|
723
|
+
})
|