jbrowse-plugin-graphgenomeviewer 7.1.0 → 7.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -8
- package/dist/chunks/AddTrackWorkflow-RPH4ZFSX.js +2 -0
- package/dist/chunks/{AddTrackWorkflow-Q6RY23LF.js.map → AddTrackWorkflow-RPH4ZFSX.js.map} +2 -2
- package/dist/chunks/{ChooseSamplesDialog-BNVYBGJQ.js → ChooseSamplesDialog-JRN4YLBY.js} +2 -2
- package/dist/chunks/{ChooseWalksDialog-EN3C6AQA.js → ChooseWalksDialog-UXLFZTZJ.js} +2 -2
- package/dist/chunks/GbzBaseSyntenyAdapter-LRECKL74.js +2 -0
- package/dist/chunks/GbzBaseSyntenyAdapter-LRECKL74.js.map +7 -0
- package/dist/chunks/{GraphGenomeView-Z2WZKEEO.js → GraphGenomeView-NJ5XCREB.js} +2 -2
- package/dist/chunks/GraphTrackSettingsDialog-47PQ6LM7.js +6 -0
- package/dist/chunks/GraphTrackSettingsDialog-47PQ6LM7.js.map +7 -0
- package/dist/chunks/{HighlightColorDialog-EYBKFSJU.js → HighlightColorDialog-UK5VDDV6.js} +2 -2
- package/dist/chunks/{LinearGraphDisplay-WIBVWRQE.js → LinearGraphDisplay-TXTKAACH.js} +2 -2
- package/dist/chunks/MinigraphBubbleAdapter-HW7XQMAI.js +2 -0
- package/dist/chunks/MinigraphBubbleAdapter-HW7XQMAI.js.map +7 -0
- package/dist/chunks/RgfaTabixAdapter-PSKYWD2X.js +3 -0
- package/dist/chunks/RgfaTabixAdapter-PSKYWD2X.js.map +7 -0
- package/dist/chunks/{TrackWarningsDialog-QT4YEXGD.js → TrackWarningsDialog-TRYBOIHX.js} +2 -2
- package/dist/chunks/WalkTabixSyntenyAdapter-U53JLYVU.js +2 -0
- package/dist/chunks/WalkTabixSyntenyAdapter-U53JLYVU.js.map +7 -0
- package/dist/chunks/chunk-2MCOY3YG.js +3 -0
- package/dist/chunks/chunk-2MCOY3YG.js.map +7 -0
- package/dist/chunks/{chunk-EWFJBQDV.js → chunk-5O6YRJHL.js} +2 -2
- package/dist/chunks/{chunk-EWFJBQDV.js.map → chunk-5O6YRJHL.js.map} +1 -1
- package/dist/chunks/chunk-BUY2PV7D.js +2 -0
- package/dist/chunks/chunk-BUY2PV7D.js.map +7 -0
- package/dist/chunks/chunk-DVCZ3BOC.js +2 -0
- package/dist/chunks/chunk-DVCZ3BOC.js.map +7 -0
- package/dist/chunks/{chunk-SAMJE6GH.js → chunk-EGDLEQ47.js} +3 -3
- package/dist/chunks/{chunk-SAMJE6GH.js.map → chunk-EGDLEQ47.js.map} +4 -4
- package/dist/chunks/chunk-FHZXW7IG.js +2 -0
- package/dist/chunks/chunk-FHZXW7IG.js.map +7 -0
- package/dist/chunks/{chunk-DZ4OUKQV.js → chunk-FITPB5X6.js} +2 -2
- package/dist/chunks/{chunk-DZ4OUKQV.js.map → chunk-FITPB5X6.js.map} +1 -1
- package/dist/chunks/chunk-FVK2THE4.js +2 -0
- package/dist/chunks/chunk-FVK2THE4.js.map +7 -0
- package/dist/chunks/{chunk-AHFQHYJI.js → chunk-IBWORBGY.js} +2 -2
- package/dist/chunks/chunk-MND7GAOG.js +2 -0
- package/dist/chunks/chunk-MND7GAOG.js.map +7 -0
- package/dist/chunks/chunk-MURN3JPK.js +2 -0
- package/dist/chunks/chunk-MURN3JPK.js.map +7 -0
- package/dist/chunks/{chunk-3Q4RBDCL.js → chunk-N6PQZSAX.js} +1 -1
- package/dist/chunks/{chunk-3Q4RBDCL.js.map → chunk-N6PQZSAX.js.map} +1 -1
- package/dist/chunks/chunk-NWJYLP6Z.js +4 -0
- package/dist/chunks/chunk-NWJYLP6Z.js.map +7 -0
- package/dist/chunks/{chunk-YJHYBDYF.js → chunk-S75W27NT.js} +2 -2
- package/dist/chunks/{chunk-YJHYBDYF.js.map → chunk-S75W27NT.js.map} +1 -1
- package/dist/chunks/{chunk-IXQK7JAW.js → chunk-T4HOC6O3.js} +1 -1
- package/dist/chunks/{chunk-IXQK7JAW.js.map → chunk-T4HOC6O3.js.map} +1 -1
- package/dist/chunks/{chunk-AU73K7LX.js → chunk-TUKJRVBS.js} +1 -1
- package/dist/chunks/{chunk-AU73K7LX.js.map → chunk-TUKJRVBS.js.map} +1 -1
- package/dist/chunks/chunk-UGCNPRKC.js +2 -0
- package/dist/chunks/{chunk-E3WYW7Z6.js.map → chunk-UGCNPRKC.js.map} +2 -2
- package/dist/chunks/{chunk-YSTMDXXX.js → chunk-VJ3SO4CH.js} +1 -1
- package/dist/chunks/{chunk-YSTMDXXX.js.map → chunk-VJ3SO4CH.js.map} +1 -1
- package/dist/chunks/{chunk-XT766CZ5.js → chunk-W6EQLQ6H.js} +1 -1
- package/dist/chunks/{chunk-XT766CZ5.js.map → chunk-W6EQLQ6H.js.map} +1 -1
- package/dist/chunks/chunk-WAMBI7OX.js +2 -0
- package/dist/chunks/chunk-WAMBI7OX.js.map +7 -0
- package/dist/chunks/chunk-WCGVUZWP.js +2 -0
- package/dist/chunks/chunk-WCGVUZWP.js.map +7 -0
- package/dist/chunks/chunk-WEGWTC52.js +2 -0
- package/dist/chunks/chunk-WEGWTC52.js.map +7 -0
- package/dist/chunks/chunk-YU7T5WOQ.js +2 -0
- package/dist/chunks/{chunk-6OPB7FOJ.js.map → chunk-YU7T5WOQ.js.map} +2 -2
- package/dist/chunks/{chunk-446RWIA3.js → chunk-ZMOMFU2B.js} +1 -1
- package/dist/chunks/{chunk-446RWIA3.js.map → chunk-ZMOMFU2B.js.map} +1 -1
- package/dist/chunks/renderSvg-KSQI5DH6.js +2 -0
- package/dist/chunks/renderSvg-KSQI5DH6.js.map +7 -0
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +4 -4
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
- package/package.json +8 -9
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.test.ts +12 -10
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.ts +31 -39
- package/src/GbzBaseSyntenyAdapter/configSchema.ts +5 -29
- package/src/GetGraphHaplotypes.ts +6 -15
- package/src/GetGraphReads.ts +7 -14
- package/src/GetSubgraph.ts +11 -20
- package/src/GraphAddTrackWorkflow/AddTrackWorkflow.test.tsx +73 -0
- package/src/GraphAddTrackWorkflow/AddTrackWorkflow.tsx +1 -0
- package/src/GraphAddTrackWorkflow/buildTrackConfig.test.ts +3 -2
- package/src/GraphAddTrackWorkflow/buildTrackConfig.ts +2 -11
- package/src/GraphAddTrackWorkflow/graphManifest.ts +15 -3
- package/src/GraphGenomeView/components/BubbleHalos.tsx +5 -24
- package/src/GraphGenomeView/components/ElTree.tsx +6 -1
- package/src/GraphGenomeView/components/FacetPanels.test.tsx +65 -0
- package/src/GraphGenomeView/components/FacetPanels.tsx +17 -12
- package/src/GraphGenomeView/components/GenePins.tsx +5 -22
- package/src/GraphGenomeView/components/GraphCanvas.tsx +40 -234
- package/src/GraphGenomeView/components/GraphSettingsDialog.tsx +102 -108
- package/src/GraphGenomeView/components/GraphToolbar.tsx +5 -6
- package/src/GraphGenomeView/components/HoverLayer.tsx +3 -7
- package/src/GraphGenomeView/components/HoverTooltip.test.tsx +39 -0
- package/src/GraphGenomeView/components/HoverTooltip.tsx +122 -0
- package/src/GraphGenomeView/components/LabelChip.tsx +3 -5
- package/src/GraphGenomeView/components/LabelLayer.tsx +4 -17
- package/src/GraphGenomeView/components/LabelledSelect.tsx +1 -1
- package/src/GraphGenomeView/components/Legend.tsx +14 -23
- package/src/GraphGenomeView/components/PaneContextMenu.test.tsx +48 -0
- package/src/GraphGenomeView/components/PaneContextMenu.tsx +144 -0
- package/src/GraphGenomeView/components/PaneLayer.tsx +91 -0
- package/src/GraphGenomeView/components/PaneSvg.tsx +32 -50
- package/src/GraphGenomeView/components/ReferenceStripOverlay.test.tsx +48 -0
- package/src/GraphGenomeView/components/ReferenceStripOverlay.tsx +13 -42
- package/src/GraphGenomeView/components/SettingsMenu.test.tsx +29 -0
- package/src/GraphGenomeView/components/SettingsMenu.tsx +56 -65
- package/src/GraphGenomeView/components/TubeMapOverlay.tsx +15 -46
- package/src/GraphGenomeView/components/WalkRowsOverlay.tsx +58 -132
- package/src/GraphGenomeView/components/WalkStrip.tsx +17 -20
- package/src/GraphGenomeView/components/WalkStripLocator.tsx +8 -15
- package/src/GraphGenomeView/components/usePaneGestures.test.tsx +35 -0
- package/src/GraphGenomeView/components/usePaneGestures.ts +88 -91
- package/src/GraphGenomeView/genes/geneFeatures.ts +27 -45
- package/src/GraphGenomeView/graphLayers.ts +4 -4
- package/src/GraphGenomeView/host.ts +1 -1
- package/src/GraphGenomeView/legendSections.test.ts +23 -1
- package/src/GraphGenomeView/legendSections.ts +68 -74
- package/src/GraphGenomeView/model.test.ts +373 -39
- package/src/GraphGenomeView/model.ts +67 -11
- package/src/GraphGenomeView/pane/fitViews.ts +57 -514
- package/src/GraphGenomeView/pane/graphViews.ts +219 -160
- package/src/GraphGenomeView/pane/hostViews.ts +2 -10
- package/src/GraphGenomeView/pane/keepInView.ts +5 -4
- package/src/GraphGenomeView/pane/launchActions.ts +117 -139
- package/src/GraphGenomeView/pane/launchViews.ts +8 -27
- package/src/GraphGenomeView/pane/loadActions.ts +208 -187
- package/src/GraphGenomeView/pane/morph.ts +3 -9
- package/src/GraphGenomeView/pane/paneBase.ts +22 -11
- package/src/GraphGenomeView/pane/renderingActions.ts +65 -11
- package/src/GraphGenomeView/pane/settingActions.ts +17 -42
- package/src/GraphGenomeView/pane/tubeMapViews.ts +445 -0
- package/src/GraphGenomeView/pane/walkRowViews.ts +29 -37
- package/src/GraphGenomeView/renderPipeline.test.ts +1 -1
- package/src/GraphGenomeView/repeats/repeatFeatures.test.ts +23 -0
- package/src/GraphGenomeView/repeats/repeatFeatures.ts +16 -23
- package/src/GraphGenomeView/repeats/walkCalls.test.ts +18 -0
- package/src/GraphGenomeView/repeats/walkCalls.ts +46 -38
- package/src/GraphGenomeView/subgraphLoad.test.ts +67 -3
- package/src/GraphGenomeView/trackFeatures.ts +24 -0
- package/src/GraphGenomeView/tubeMapPanels.ts +28 -36
- package/src/GraphGenomeView/viewModel.ts +8 -56
- package/src/GraphGenomeView/walkRowGroups.ts +104 -30
- package/src/GraphGenomeView/walkRowSpans.test.ts +6 -0
- package/src/GraphGenomeView/walkRowSpans.ts +22 -31
- package/src/GraphGenomeView/walkRowsCut.ts +8 -17
- package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +173 -26
- package/src/LinearGraphDisplay/components/SubgraphContextSelect.test.tsx +1 -1
- package/src/LinearGraphDisplay/components/SubgraphContextSelect.tsx +3 -3
- package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.test.tsx +3 -3
- package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.tsx +7 -10
- package/src/LinearGraphDisplay/configSchema.ts +0 -3
- package/src/LinearGraphDisplay/denseWindow.ts +8 -11
- package/src/LinearGraphDisplay/model.ts +89 -82
- package/src/MinigraphBubbleAdapter/MinigraphBubbleAdapter.test.ts +21 -0
- package/src/MinigraphBubbleAdapter/MinigraphBubbleAdapter.ts +6 -14
- package/src/MinigraphBubbleAdapter/configSchema.ts +3 -9
- package/src/MinigraphBubbleAdapter/index.ts +1 -2
- package/src/RgfaTabixAdapter/RgfaTabixAdapter.test.ts +45 -1
- package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +111 -276
- package/src/RgfaTabixAdapter/configSchema.ts +15 -49
- package/src/RgfaTabixAdapter/index.ts +1 -2
- package/src/RgfaTabixAdapter/rgfaBed.ts +25 -15
- package/src/RgfaTabixAdapter/rgfaCut.ts +139 -0
- package/src/RgfaTabixAdapter/test_data/walks2_twopart.GRCh38.walks.bed.gz +0 -0
- package/src/RgfaTabixAdapter/test_data/walks2_twopart.GRCh38.walks.bed.gz.tbi +0 -0
- package/src/RgfaTabixAdapter/walkFiles.test.ts +33 -0
- package/src/RgfaTabixAdapter/walkReader.ts +103 -27
- package/src/RgfaTabixAdapter/walkRowRuns.test.ts +53 -0
- package/src/RgfaTabixAdapter/walkRowRuns.ts +42 -24
- package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.test.ts +37 -6
- package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +23 -45
- package/src/WalkTabixSyntenyAdapter/configSchema.ts +11 -24
- package/src/WalkTabixSyntenyAdapter/index.ts +1 -2
- package/src/WalkTabixSyntenyAdapter/walkLanes.ts +35 -43
- package/src/adapterImplementing.ts +27 -0
- package/src/graphTrackDefaults/index.ts +14 -20
- package/src/hoverSync/lgvHover.ts +2 -4
- package/src/launchFromGraph/contributors.ts +1 -5
- package/src/launchFromGraph/graphMenuItems.ts +3 -4
- package/src/launchFromGraph/launchFromGraph.test.ts +0 -1
- package/src/launchFromGraph/launchFromGraph.ts +8 -13
- package/src/panSNTabix.test.ts +1 -26
- package/src/panSNTabix.ts +54 -51
- package/src/synteny/lanePairs.ts +8 -0
- package/src/synteny/matchFeature.ts +52 -0
- package/src/tabixRange.ts +13 -10
- package/src/version.ts +1 -1
- package/dist/chunks/AddTrackWorkflow-Q6RY23LF.js +0 -2
- package/dist/chunks/GbzBaseSyntenyAdapter-TIXR4Y3J.js +0 -2
- package/dist/chunks/GbzBaseSyntenyAdapter-TIXR4Y3J.js.map +0 -7
- package/dist/chunks/GraphTrackSettingsDialog-TAM6PMXD.js +0 -6
- package/dist/chunks/GraphTrackSettingsDialog-TAM6PMXD.js.map +0 -7
- package/dist/chunks/MinigraphBubbleAdapter-AGI2UYQR.js +0 -2
- package/dist/chunks/MinigraphBubbleAdapter-AGI2UYQR.js.map +0 -7
- package/dist/chunks/RgfaTabixAdapter-2OL5X3HF.js +0 -3
- package/dist/chunks/RgfaTabixAdapter-2OL5X3HF.js.map +0 -7
- package/dist/chunks/WalkTabixSyntenyAdapter-UKFHCFOF.js +0 -2
- package/dist/chunks/WalkTabixSyntenyAdapter-UKFHCFOF.js.map +0 -7
- package/dist/chunks/chunk-5SFGHDQA.js +0 -2
- package/dist/chunks/chunk-5SFGHDQA.js.map +0 -7
- package/dist/chunks/chunk-6OPB7FOJ.js +0 -2
- package/dist/chunks/chunk-AMFIBQVR.js +0 -2
- package/dist/chunks/chunk-AMFIBQVR.js.map +0 -7
- package/dist/chunks/chunk-BJMRBVG3.js +0 -2
- package/dist/chunks/chunk-BJMRBVG3.js.map +0 -7
- package/dist/chunks/chunk-E3WYW7Z6.js +0 -2
- package/dist/chunks/chunk-EHCF6AXH.js +0 -2
- package/dist/chunks/chunk-EHCF6AXH.js.map +0 -7
- package/dist/chunks/chunk-GOFS7BXK.js +0 -3
- package/dist/chunks/chunk-GOFS7BXK.js.map +0 -7
- package/dist/chunks/chunk-IJQTSJGT.js +0 -4
- package/dist/chunks/chunk-IJQTSJGT.js.map +0 -7
- package/dist/chunks/chunk-J6NZIAZF.js +0 -2
- package/dist/chunks/chunk-J6NZIAZF.js.map +0 -7
- package/dist/chunks/chunk-LUTMKRXY.js +0 -2
- package/dist/chunks/chunk-LUTMKRXY.js.map +0 -7
- package/dist/chunks/chunk-MHPJB22Z.js +0 -2
- package/dist/chunks/chunk-MHPJB22Z.js.map +0 -7
- package/dist/chunks/chunk-TXI43PEN.js +0 -2
- package/dist/chunks/chunk-TXI43PEN.js.map +0 -7
- package/dist/chunks/chunk-WRH2EJKX.js +0 -2
- package/dist/chunks/chunk-WRH2EJKX.js.map +0 -7
- package/dist/chunks/chunk-WX26KTZM.js +0 -2
- package/dist/chunks/chunk-WX26KTZM.js.map +0 -7
- package/dist/chunks/renderSvg-ST2GBBOC.js +0 -2
- package/dist/chunks/renderSvg-ST2GBBOC.js.map +0 -7
- package/src/GraphGenomeView/components/WalkRowContextMenu.tsx +0 -105
- /package/dist/chunks/{ChooseSamplesDialog-BNVYBGJQ.js.map → ChooseSamplesDialog-JRN4YLBY.js.map} +0 -0
- /package/dist/chunks/{ChooseWalksDialog-EN3C6AQA.js.map → ChooseWalksDialog-UXLFZTZJ.js.map} +0 -0
- /package/dist/chunks/{GraphGenomeView-Z2WZKEEO.js.map → GraphGenomeView-NJ5XCREB.js.map} +0 -0
- /package/dist/chunks/{HighlightColorDialog-EYBKFSJU.js.map → HighlightColorDialog-UK5VDDV6.js.map} +0 -0
- /package/dist/chunks/{LinearGraphDisplay-WIBVWRQE.js.map → LinearGraphDisplay-TXTKAACH.js.map} +0 -0
- /package/dist/chunks/{TrackWarningsDialog-QT4YEXGD.js.map → TrackWarningsDialog-TRYBOIHX.js.map} +0 -0
- /package/dist/chunks/{chunk-AHFQHYJI.js.map → chunk-IBWORBGY.js.map} +0 -0
|
@@ -143,3 +143,21 @@ test('a phased allele pairs with its own haplotype, not by length', () => {
|
|
|
143
143
|
})
|
|
144
144
|
expect(lone[0]!.call).toBeUndefined()
|
|
145
145
|
})
|
|
146
|
+
|
|
147
|
+
// A haplotype's walk broken across two contigs makes two rows of one
|
|
148
|
+
// haplotype, and its phased allele goes to the one walk across the array.
|
|
149
|
+
test("a phased allele goes to one of its haplotype's walks", () => {
|
|
150
|
+
const piece = (bp: number, complete: boolean) => ({
|
|
151
|
+
...walk('HG1', bp, complete),
|
|
152
|
+
haplotype: 1,
|
|
153
|
+
})
|
|
154
|
+
const call = { bp: 3100, spanningReads: 4, haplotype: 1 }
|
|
155
|
+
const rows = withCalls([piece(900, false), piece(3161, true)], {
|
|
156
|
+
HG1: calls(call),
|
|
157
|
+
})
|
|
158
|
+
expect(rows.map(r => r.call?.bp)).toEqual([undefined, 3100])
|
|
159
|
+
const both = withCalls([piece(400, true), piece(3161, true)], {
|
|
160
|
+
HG1: calls(call),
|
|
161
|
+
})
|
|
162
|
+
expect(both.map(r => r.call?.agrees)).toEqual([undefined, true])
|
|
163
|
+
})
|
|
@@ -14,7 +14,7 @@ import type { RepeatCall } from './repeatFeatures'
|
|
|
14
14
|
// the array take the allele that fit one spanning it, and the spanning walk
|
|
15
15
|
// then read as a disagreement.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
interface WalkCall extends RepeatCall {
|
|
18
18
|
// undefined where the walk and the allele are not both measurements
|
|
19
19
|
agrees?: boolean
|
|
20
20
|
}
|
|
@@ -74,50 +74,53 @@ function scored(row: { bp: number; complete: boolean }, call: RepeatCall) {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// A phased genotype names its walk's haplotype outright
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
77
|
+
// A phased genotype names its walk's haplotype outright, though a contig
|
|
78
|
+
// break can give that haplotype two walks. Within a haplotype, and among the
|
|
79
|
+
// unphased alleles, pairing goes by length: the walks that span the array
|
|
80
|
+
// against the alleles reads spanned, then everything else against what those
|
|
81
|
+
// two left. That decides the ambiguous cases on merit: which walk takes the
|
|
82
|
+
// one allele a lone spanning read supports, and which allele a walk that stops
|
|
83
|
+
// inside the array is drawn against.
|
|
83
84
|
function pairSample<
|
|
84
85
|
R extends { bp: number; complete: boolean; haplotype?: number },
|
|
85
86
|
>(rows: R[], calls: RepeatCall[]) {
|
|
86
87
|
const out = new Map<number, WalkCall>()
|
|
87
88
|
const taken = new Set<RepeatCall>()
|
|
88
|
-
rows.forEach((row, i) => {
|
|
89
|
-
const call = calls.find(
|
|
90
|
-
c => c.haplotype !== undefined && c.haplotype === row.haplotype,
|
|
91
|
-
)
|
|
92
|
-
if (call) {
|
|
93
|
-
taken.add(call)
|
|
94
|
-
out.set(i, scored(row, call))
|
|
95
|
-
}
|
|
96
|
-
})
|
|
97
|
-
const unphased = calls.filter(call => call.haplotype === undefined)
|
|
98
89
|
const ranked = byLength(rows, r => r.bp)
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
90
|
+
const pair = (pool: typeof ranked, group: RepeatCall[]) => {
|
|
91
|
+
const stages = [
|
|
92
|
+
[pool.filter(r => r.item.complete), group.filter(backed)],
|
|
93
|
+
[pool, group],
|
|
94
|
+
] as const
|
|
95
|
+
for (const [walks, alleles] of stages) {
|
|
96
|
+
const free = walks.filter(r => !out.has(r.i))
|
|
97
|
+
const open = byLength(
|
|
98
|
+
alleles.filter(call => !taken.has(call)),
|
|
99
|
+
call => call.bp,
|
|
100
|
+
)
|
|
101
|
+
nearest(
|
|
102
|
+
free.map(r => r.item.bp),
|
|
103
|
+
open.map(c => c.item.bp),
|
|
104
|
+
).forEach((pick, k) => {
|
|
105
|
+
if (pick !== undefined) {
|
|
106
|
+
const row = free[k]!
|
|
107
|
+
const call = open[pick]!.item
|
|
108
|
+
taken.add(call)
|
|
109
|
+
out.set(row.i, scored(row.item, call))
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const haplotype of new Set(calls.flatMap(c => c.haplotype ?? []))) {
|
|
115
|
+
pair(
|
|
116
|
+
ranked.filter(r => r.item.haplotype === haplotype),
|
|
117
|
+
calls.filter(c => c.haplotype === haplotype),
|
|
108
118
|
)
|
|
109
|
-
nearest(
|
|
110
|
-
free.map(r => r.item.bp),
|
|
111
|
-
open.map(c => c.item.bp),
|
|
112
|
-
).forEach((pick, k) => {
|
|
113
|
-
if (pick !== undefined) {
|
|
114
|
-
const row = free[k]!
|
|
115
|
-
const call = open[pick]!.item
|
|
116
|
-
taken.add(call)
|
|
117
|
-
out.set(row.i, scored(row.item, call))
|
|
118
|
-
}
|
|
119
|
-
})
|
|
120
119
|
}
|
|
120
|
+
pair(
|
|
121
|
+
ranked,
|
|
122
|
+
calls.filter(c => c.haplotype === undefined),
|
|
123
|
+
)
|
|
121
124
|
return out
|
|
122
125
|
}
|
|
123
126
|
|
|
@@ -135,7 +138,12 @@ export function withCalls<
|
|
|
135
138
|
const paired = new Map<R, WalkCall>()
|
|
136
139
|
const bySample = new Map<string, R[]>()
|
|
137
140
|
for (const row of rows) {
|
|
138
|
-
|
|
141
|
+
const group = bySample.get(row.sample)
|
|
142
|
+
if (group) {
|
|
143
|
+
group.push(row)
|
|
144
|
+
} else {
|
|
145
|
+
bySample.set(row.sample, [row])
|
|
146
|
+
}
|
|
139
147
|
}
|
|
140
148
|
for (const [sample, group] of bySample) {
|
|
141
149
|
const called = calls[sample]
|
|
@@ -7,7 +7,8 @@ const mockSession = {
|
|
|
7
7
|
tracks: [] as Record<string, unknown>[],
|
|
8
8
|
rpcManager: { call: mockRpcCall },
|
|
9
9
|
assemblyManager: {
|
|
10
|
-
has: () => false,
|
|
10
|
+
has: (_name: string) => false,
|
|
11
|
+
get: (_name: string) => undefined as { name: string } | undefined,
|
|
11
12
|
assemblyList: [] as { name: string; aliases: string[] }[],
|
|
12
13
|
},
|
|
13
14
|
}
|
|
@@ -41,6 +42,11 @@ vi.mock(import('@jbrowse/core/configuration'), async importOriginal => ({
|
|
|
41
42
|
),
|
|
42
43
|
}))
|
|
43
44
|
|
|
45
|
+
const mockReadFile = vi.hoisted(() => vi.fn())
|
|
46
|
+
vi.mock('@jbrowse/core/util/io', () => ({
|
|
47
|
+
openLocation: () => ({ readFile: mockReadFile }),
|
|
48
|
+
}))
|
|
49
|
+
|
|
44
50
|
// pangenome_hprc's segments track once NA20809 haplotype 2 is loaded
|
|
45
51
|
const TRACK = {
|
|
46
52
|
trackId: 'segments',
|
|
@@ -98,6 +104,8 @@ async function cut(region: typeof ON_HG38, gfa = GFA) {
|
|
|
98
104
|
beforeEach(() => {
|
|
99
105
|
mockRpcCall.mockReset()
|
|
100
106
|
mockSession.tracks = [TRACK]
|
|
107
|
+
mockSession.assemblyManager.has = () => false
|
|
108
|
+
mockSession.assemblyManager.get = () => undefined
|
|
101
109
|
mockSession.assemblyManager.assemblyList = []
|
|
102
110
|
})
|
|
103
111
|
|
|
@@ -116,6 +124,17 @@ test('a cut on another assembly the track names is refused, naming the reference
|
|
|
116
124
|
expect(mockRpcCall).not.toHaveBeenCalled()
|
|
117
125
|
})
|
|
118
126
|
|
|
127
|
+
test("a track naming its reference by an alias of the region's assembly cuts it", async () => {
|
|
128
|
+
mockSession.tracks = [{ ...TRACK, assemblyNames: ['GRCh38', 'NA20809.2'] }]
|
|
129
|
+
const hg38 = { name: 'hg38', aliases: ['GRCh38'] }
|
|
130
|
+
const known = (name: string) => name === 'hg38' || name === 'GRCh38'
|
|
131
|
+
mockSession.assemblyManager.has = known
|
|
132
|
+
mockSession.assemblyManager.get = name => (known(name) ? hg38 : undefined)
|
|
133
|
+
const model = await cut(ON_HG38)
|
|
134
|
+
expect(model.error).toBeUndefined()
|
|
135
|
+
expect(model.nodeCount).toBe(3)
|
|
136
|
+
})
|
|
137
|
+
|
|
119
138
|
test('a cut that finds no segments is an error, not an empty graph', async () => {
|
|
120
139
|
const model = await cut(ON_HG38, 'H\tVN:Z:1.0')
|
|
121
140
|
expect(String(model.error)).toMatch(/No graph segments/)
|
|
@@ -300,11 +319,11 @@ describe('reads', () => {
|
|
|
300
319
|
secondary: false,
|
|
301
320
|
}
|
|
302
321
|
|
|
303
|
-
async function cutWithReads(reads: () => Promise<unknown
|
|
322
|
+
async function cutWithReads(reads: () => Promise<unknown>, gfa = PATHS) {
|
|
304
323
|
mockSession.tracks = [READS_TRACK]
|
|
305
324
|
mockRpcCall.mockImplementation((_sid: unknown, method: string) =>
|
|
306
325
|
method === 'GetSubgraph'
|
|
307
|
-
? Promise.resolve(
|
|
326
|
+
? Promise.resolve(gfa)
|
|
308
327
|
: method === 'GetGraphReads'
|
|
309
328
|
? reads()
|
|
310
329
|
: Promise.reject(new Error(`Unexpected RPC: ${method}`)),
|
|
@@ -343,6 +362,51 @@ describe('reads', () => {
|
|
|
343
362
|
expect(model.colorSchemeLock?.value).toBe('By strand')
|
|
344
363
|
})
|
|
345
364
|
|
|
365
|
+
// the panels draw no reads, so the boxes take the scheme picked
|
|
366
|
+
test('a tube map split into panels colours its boxes beside reads', async () => {
|
|
367
|
+
const model = await cutWithReads(
|
|
368
|
+
() => Promise.resolve({ records: [READ], total: 1 }),
|
|
369
|
+
PATHS.replace('P\talt', 'P\tA#1#c').concat('\nP\tB#1#c\t1+,2+,4+\t*'),
|
|
370
|
+
)
|
|
371
|
+
expect(model.tubeMapReads).toBe(true)
|
|
372
|
+
model.setFacet('sample')
|
|
373
|
+
await model.recomputeLayout()
|
|
374
|
+
model.setColorScheme('reference-position')
|
|
375
|
+
expect(model.layoutResult?.tubeMapPanels).toHaveLength(2)
|
|
376
|
+
expect(model.tubeMapReads).toBe(false)
|
|
377
|
+
expect(model.colorSchemeLock).toBeUndefined()
|
|
378
|
+
expect(model.effectiveColorScheme).toBe('reference-position')
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
// the reads take the reds and blues, so the tubes keep their greys, which
|
|
382
|
+
// the key names walk by walk
|
|
383
|
+
test('beside reads a tube colour column leaves the key naming each walk', async () => {
|
|
384
|
+
const model = await cutWithReads(() =>
|
|
385
|
+
Promise.resolve({ records: [READ], total: 1 }),
|
|
386
|
+
)
|
|
387
|
+
mockSession.tracks = [
|
|
388
|
+
{
|
|
389
|
+
...READS_TRACK,
|
|
390
|
+
adapter: {
|
|
391
|
+
...READS_TRACK.adapter,
|
|
392
|
+
samplesTsvLocation: {
|
|
393
|
+
uri: 'samples.tsv',
|
|
394
|
+
locationType: 'UriLocation',
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
},
|
|
398
|
+
]
|
|
399
|
+
mockReadFile.mockResolvedValue('name\tpopulation\nalt\tYRI\n')
|
|
400
|
+
await model.loadWalkRowSampleTable()
|
|
401
|
+
model.setTubeMapColorBy('population')
|
|
402
|
+
expect(model.tubeMapColoring?.field).toBe('population')
|
|
403
|
+
expect(model.tubeMapKeys.values).toBeUndefined()
|
|
404
|
+
expect(model.pathLegend.map(e => [e.name, e.color])).toEqual([
|
|
405
|
+
['hg38', model.tubeMapTubeColors![0]],
|
|
406
|
+
['alt', model.tubeMapTubeColors![1]],
|
|
407
|
+
])
|
|
408
|
+
})
|
|
409
|
+
|
|
346
410
|
// A tube map draws the window alone, so its ramp key speaks for its boxes:
|
|
347
411
|
// h0 is an allele before the window that the tube map trims away
|
|
348
412
|
test("a tube map's ramp key names no node the window trimmed away", async () => {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Feature } from '@jbrowse/core/util'
|
|
2
|
+
|
|
3
|
+
export type FeatureLike = Feature | Record<string, unknown>
|
|
4
|
+
|
|
5
|
+
export function ownField(f: FeatureLike, name: string): unknown {
|
|
6
|
+
return typeof (f as Feature).get === 'function'
|
|
7
|
+
? (f as Feature).get(name)
|
|
8
|
+
: (f as Record<string, unknown>)[name]
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TrackChoice {
|
|
12
|
+
trackId: string
|
|
13
|
+
name?: string
|
|
14
|
+
adapterType: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function namedTrack<T extends TrackChoice>(tracks: T[], named: string) {
|
|
18
|
+
return tracks.find(t => t.trackId === named)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// what a track-name hint such as /repeat|tandem/ is matched against
|
|
22
|
+
export function trackLabel(t: TrackChoice) {
|
|
23
|
+
return `${t.trackId} ${t.name ?? ''}`
|
|
24
|
+
}
|
|
@@ -4,6 +4,8 @@ import { pathOrigin } from '@jbrowse/bandage-core/pathAnchoring'
|
|
|
4
4
|
import { drawTubeMapHighlight } from '@jbrowse/bandage-core/tubeMap/draw'
|
|
5
5
|
import { groupKeyComparator } from '@jbrowse/core/util/groupKeys'
|
|
6
6
|
|
|
7
|
+
import { columnValueOf, groupsByColumn, plural } from './walkRowGroups'
|
|
8
|
+
|
|
7
9
|
import type { SampleRow } from './walkRowGroups'
|
|
8
10
|
import type { FacetBy } from '@jbrowse/bandage-core/facetGrid'
|
|
9
11
|
import type { TubeMapDrawing } from '@jbrowse/bandage-core/layout/tubeMapLayout'
|
|
@@ -115,57 +117,47 @@ export function valueTubeColors(
|
|
|
115
117
|
})
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
// A walk's value in a column
|
|
119
|
-
// sample's, as groupWalkRows reads it
|
|
120
|
+
// A PanSN walk's value in a column
|
|
120
121
|
export function columnKeyOf(field: string, table: SampleRow[]) {
|
|
121
|
-
const valueOf =
|
|
122
|
+
const valueOf = columnValueOf(field, table)
|
|
122
123
|
return (walk: string) => {
|
|
123
124
|
const { sample, haplotype } = parsePanSN(walk)
|
|
124
|
-
return valueOf
|
|
125
|
+
return valueOf(`${sample}#${haplotype}`, sample)
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
|
|
128
|
-
function plural(n: number, noun: string) {
|
|
129
|
-
return `${n} ${noun}${n === 1 ? '' : 's'}`
|
|
130
|
-
}
|
|
131
|
-
|
|
132
129
|
// Each non-reference walk's group
|
|
133
130
|
export function tubeMapPanelGroups(
|
|
134
131
|
graph: Graph,
|
|
135
132
|
split: TubeMapPanelSplit,
|
|
136
133
|
): TubeMapPanelGroup[] {
|
|
134
|
+
const names = (graph.paths ?? [])
|
|
135
|
+
.map(p => p.name)
|
|
136
|
+
.filter(name => walkOf(name) !== graph.referencePath)
|
|
137
|
+
if (split.by === 'column') {
|
|
138
|
+
const keyOf = columnKeyOf(split.field, split.table)
|
|
139
|
+
return groupsByColumn(
|
|
140
|
+
names,
|
|
141
|
+
name => keyOf(walkOf(name)),
|
|
142
|
+
split,
|
|
143
|
+
'sample',
|
|
144
|
+
name => parsePanSN(walkOf(name)).sample,
|
|
145
|
+
).map(({ key, members, title }) => ({ key, paths: members, label: title }))
|
|
146
|
+
}
|
|
137
147
|
const keyOf =
|
|
138
|
-
split.by === '
|
|
139
|
-
?
|
|
140
|
-
:
|
|
141
|
-
? (walk: string) => parsePanSN(walk).sample
|
|
142
|
-
: (walk: string) => walk
|
|
148
|
+
split.by === 'sample'
|
|
149
|
+
? (walk: string) => parsePanSN(walk).sample
|
|
150
|
+
: (walk: string) => walk
|
|
143
151
|
const groups = new Map<string, string[]>()
|
|
144
|
-
for (const
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
} else {
|
|
152
|
-
groups.set(key, [name])
|
|
153
|
-
}
|
|
152
|
+
for (const name of names) {
|
|
153
|
+
const key = keyOf(walkOf(name))
|
|
154
|
+
const list = groups.get(key)
|
|
155
|
+
if (list) {
|
|
156
|
+
list.push(name)
|
|
157
|
+
} else {
|
|
158
|
+
groups.set(key, [name])
|
|
154
159
|
}
|
|
155
160
|
}
|
|
156
|
-
if (split.by === 'column') {
|
|
157
|
-
return [...groups.keys()]
|
|
158
|
-
.sort(groupKeyComparator(split.domain))
|
|
159
|
-
.map(key => {
|
|
160
|
-
const paths = groups.get(key)!
|
|
161
|
-
const samples = new Set(paths.map(p => parsePanSN(walkOf(p)).sample))
|
|
162
|
-
return {
|
|
163
|
-
key,
|
|
164
|
-
paths,
|
|
165
|
-
label: `${key || `${split.field}: none`} · ${plural(samples.size, 'sample')}`,
|
|
166
|
-
}
|
|
167
|
-
})
|
|
168
|
-
}
|
|
169
161
|
const domain = split.domain ?? []
|
|
170
162
|
const rank = new Map(domain.map((key, i) => [key, i]))
|
|
171
163
|
const listed = (key: string) => rank.get(key) ?? domain.length
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { cutsWholeWalks as wholeWalksRule } from '@jbrowse/bandage-core/layout/walkStrip'
|
|
2
1
|
import {
|
|
3
2
|
LAYOUT_MODE_VALUES,
|
|
4
3
|
layoutModeByValue,
|
|
@@ -8,20 +7,9 @@ import { getSession } from '@jbrowse/core/util'
|
|
|
8
7
|
import { addDisposer, types } from '@jbrowse/mobx-state-tree'
|
|
9
8
|
import { reaction } from 'mobx'
|
|
10
9
|
|
|
11
|
-
import {
|
|
12
|
-
GraphPaneMixin,
|
|
13
|
-
MAX_GRAPH_REGION_BP,
|
|
14
|
-
fileName,
|
|
15
|
-
formatSpanBp,
|
|
16
|
-
} from './model'
|
|
17
|
-
import {
|
|
18
|
-
configHaplotypes,
|
|
19
|
-
graphReferenceAssembly,
|
|
20
|
-
offReferenceProblem,
|
|
21
|
-
} from '../graphTrackConfig'
|
|
22
10
|
import { HOVER_HIGHLIGHT_VALUES } from './hoverHighlight'
|
|
23
11
|
import { lenientMaybeEnum } from './lenientEnum'
|
|
24
|
-
import {
|
|
12
|
+
import { GraphPane, fileName, formatSpanBp } from './model'
|
|
25
13
|
|
|
26
14
|
import type { GraphLayers } from './graphLayers'
|
|
27
15
|
import type { NodeColor } from './nodeColor'
|
|
@@ -39,7 +27,7 @@ export default function stateModelFactory() {
|
|
|
39
27
|
.compose(
|
|
40
28
|
'GraphGenomeView',
|
|
41
29
|
BaseViewModel,
|
|
42
|
-
|
|
30
|
+
GraphPane,
|
|
43
31
|
types.model({
|
|
44
32
|
type: types.literal('GraphGenomeView'),
|
|
45
33
|
// a whole GFA the view loads on attach, kept so the source round-trips
|
|
@@ -51,9 +39,6 @@ export default function stateModelFactory() {
|
|
|
51
39
|
// sessions and the docs' figure specs state it. Following the linear
|
|
52
40
|
// view is the track's job (LinearGraphDisplay).
|
|
53
41
|
loadedTrackId: types.optional(types.string, ''),
|
|
54
|
-
subgraphContext: types.optional(types.number, 1),
|
|
55
|
-
subgraphHaplotypes: types.maybe(types.frozen<string[]>()),
|
|
56
|
-
maxRegionBp: types.optional(types.number, MAX_GRAPH_REGION_BP),
|
|
57
42
|
// unset is force
|
|
58
43
|
layoutMode: lenientMaybeEnum(LAYOUT_MODE_VALUES),
|
|
59
44
|
color: types.maybe(types.frozen<NodeColor>()),
|
|
@@ -107,16 +92,6 @@ export default function stateModelFactory() {
|
|
|
107
92
|
get paneMeasured() {
|
|
108
93
|
return self.widthMeasured
|
|
109
94
|
},
|
|
110
|
-
// the set stated, else the source track's defaultHaplotypes
|
|
111
|
-
get cutHaplotypes() {
|
|
112
|
-
const track = this.sourceTrack
|
|
113
|
-
return (
|
|
114
|
-
self.subgraphHaplotypes ??
|
|
115
|
-
(track
|
|
116
|
-
? configHaplotypes(trackAdapterConfig(self, track))
|
|
117
|
-
: undefined)
|
|
118
|
-
)
|
|
119
|
-
},
|
|
120
95
|
get sourceGfaLocation() {
|
|
121
96
|
return self.gfaLocation
|
|
122
97
|
},
|
|
@@ -163,15 +138,10 @@ export default function stateModelFactory() {
|
|
|
163
138
|
if (!self.loadedTrackId || !region) {
|
|
164
139
|
return
|
|
165
140
|
}
|
|
166
|
-
const
|
|
141
|
+
const adapter = self.sourceAdapter
|
|
167
142
|
const span = region.end - region.start
|
|
168
|
-
const problem =
|
|
169
|
-
|
|
170
|
-
graphReferenceAssembly(track),
|
|
171
|
-
region.assemblyName,
|
|
172
|
-
)
|
|
173
|
-
: undefined
|
|
174
|
-
if (!track) {
|
|
143
|
+
const problem = self.offReference(region)
|
|
144
|
+
if (!adapter) {
|
|
175
145
|
self.setError(
|
|
176
146
|
new Error(
|
|
177
147
|
`The track this graph was cut from, "${self.loadedTrackId}", is not in this session`,
|
|
@@ -186,16 +156,7 @@ export default function stateModelFactory() {
|
|
|
186
156
|
),
|
|
187
157
|
)
|
|
188
158
|
} else {
|
|
189
|
-
return self.cutSubgraph(
|
|
190
|
-
hops: self.subgraphContext,
|
|
191
|
-
haplotypes: self.cutHaplotypes,
|
|
192
|
-
...(self.chosenLayoutMode === 'walkrows'
|
|
193
|
-
? { walkRows: true }
|
|
194
|
-
: {}),
|
|
195
|
-
...(self.cutsWholeWalks
|
|
196
|
-
? { snarls: 'overlapping' as const }
|
|
197
|
-
: {}),
|
|
198
|
-
})
|
|
159
|
+
return self.cutSubgraph(adapter, region, self.cutOptions)
|
|
199
160
|
}
|
|
200
161
|
return undefined
|
|
201
162
|
},
|
|
@@ -205,18 +166,9 @@ export default function stateModelFactory() {
|
|
|
205
166
|
retryLoad() {
|
|
206
167
|
void self.load()
|
|
207
168
|
},
|
|
208
|
-
//
|
|
209
|
-
// walks, which a GBZ cut only follows when asked, so a change in whether
|
|
210
|
-
// the drawing needs them cuts the track again; the strip's toggle does
|
|
211
|
-
// the same (afterAttach)
|
|
169
|
+
// the strip's toggle cuts the track again too (afterAttach)
|
|
212
170
|
relayoutFrom(from: LayoutModeValue) {
|
|
213
|
-
|
|
214
|
-
walkStrip: self.walkStrip,
|
|
215
|
-
host: !!self.host,
|
|
216
|
-
})
|
|
217
|
-
return self.loadedTrackId &&
|
|
218
|
-
(self.cutsWholeWalks !== before ||
|
|
219
|
-
(from === 'walkrows') !== (self.chosenLayoutMode === 'walkrows'))
|
|
171
|
+
return self.loadedTrackId && self.cutChangedFrom(from)
|
|
220
172
|
? self.load()
|
|
221
173
|
: self.recomputeLayout()
|
|
222
174
|
},
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import { walkRowsTree } from '@jbrowse/bandage-core/layout/walkRowDraw'
|
|
1
2
|
import { groupKeyComparator } from '@jbrowse/core/util/groupKeys'
|
|
2
3
|
|
|
4
|
+
import type { El } from '@jbrowse/bandage-core/el'
|
|
5
|
+
import type {
|
|
6
|
+
WalkRowsFrame,
|
|
7
|
+
WalkRowsWithCalls,
|
|
8
|
+
} from '@jbrowse/bandage-core/layout/walkRowDraw'
|
|
9
|
+
|
|
3
10
|
// Walk rows stacked into one section per value of a sample table's column,
|
|
4
11
|
// as jbrowse-plugin-tandem-repeat's "Group by…" stacks its alleles. Placement
|
|
5
12
|
// here, not in bandage-core's walkRowsTree, which puts row i at i * rowPx.
|
|
@@ -71,46 +78,71 @@ export function groupByOf(value: unknown): WalkRowGroupBy | undefined {
|
|
|
71
78
|
: { field }
|
|
72
79
|
}
|
|
73
80
|
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
return `${n} haplotype${n === 1 ? '' : 's'}`
|
|
81
|
+
export function plural(n: number, noun: string) {
|
|
82
|
+
return `${n} ${noun}${n === 1 ? '' : 's'}`
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
export function groupWalkRows<R extends { sample: string; label: string }>(
|
|
83
|
-
rows: R[],
|
|
84
|
-
table: SampleRow[],
|
|
85
|
-
{ field, domain }: WalkRowGroupBy,
|
|
86
|
-
) {
|
|
85
|
+
// A walk's value in a column: its haplotype's row (`HG00097#1`), else its
|
|
86
|
+
// sample's (`HG00097`), else ''
|
|
87
|
+
export function columnValueOf(field: string, table: SampleRow[]) {
|
|
87
88
|
const valueOf = new Map(table.map(row => [row.name, row[field] ?? '']))
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
return (haplotype: string, sample: string) =>
|
|
90
|
+
valueOf.get(haplotype) || valueOf.get(sample) || ''
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Items grouped by their value in a column, each group in the order its items
|
|
94
|
+
// came in, the groups in the order a JBrowse Group by sorts them, and each
|
|
95
|
+
// titled with its value and how many distinct `noun`s its items are
|
|
96
|
+
export function groupsByColumn<T>(
|
|
97
|
+
items: readonly T[],
|
|
98
|
+
valueOf: (item: T) => string,
|
|
99
|
+
{ field, domain }: { field: string; domain?: readonly string[] },
|
|
100
|
+
noun: string,
|
|
101
|
+
nounOf: (item: T) => string,
|
|
102
|
+
) {
|
|
103
|
+
const groups = new Map<string, T[]>()
|
|
104
|
+
for (const item of items) {
|
|
105
|
+
const key = valueOf(item)
|
|
91
106
|
const group = groups.get(key)
|
|
92
107
|
if (group) {
|
|
93
|
-
group.push(
|
|
108
|
+
group.push(item)
|
|
94
109
|
} else {
|
|
95
|
-
groups.set(key, [
|
|
110
|
+
groups.set(key, [item])
|
|
96
111
|
}
|
|
97
112
|
}
|
|
113
|
+
return [...groups.keys()].sort(groupKeyComparator(domain)).map(key => {
|
|
114
|
+
const members = groups.get(key)!
|
|
115
|
+
const n = new Set(members.map(nounOf)).size
|
|
116
|
+
return {
|
|
117
|
+
key,
|
|
118
|
+
members,
|
|
119
|
+
title: `${key || `${field}: none`} · ${plural(n, noun)}`,
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The rows reordered section by section; rows with no value stack last
|
|
125
|
+
export function groupWalkRows<R extends { sample: string; label: string }>(
|
|
126
|
+
rows: R[],
|
|
127
|
+
table: SampleRow[],
|
|
128
|
+
groupBy: WalkRowGroupBy,
|
|
129
|
+
) {
|
|
130
|
+
const valueOf = columnValueOf(groupBy.field, table)
|
|
131
|
+
const groups = groupsByColumn(
|
|
132
|
+
rows,
|
|
133
|
+
row => valueOf(row.label, row.sample),
|
|
134
|
+
groupBy,
|
|
135
|
+
'haplotype',
|
|
136
|
+
row => row.label,
|
|
137
|
+
)
|
|
98
138
|
let first = 1
|
|
99
|
-
const sections: WalkRowSection[] =
|
|
100
|
-
.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
key,
|
|
105
|
-
title: `${key || `${field}: none`} · ${haplotypes(members)}`,
|
|
106
|
-
first,
|
|
107
|
-
count: members.length,
|
|
108
|
-
}
|
|
109
|
-
first += members.length
|
|
110
|
-
return section
|
|
111
|
-
})
|
|
139
|
+
const sections: WalkRowSection[] = groups.map(({ key, members, title }) => {
|
|
140
|
+
const section = { key, title, first, count: members.length }
|
|
141
|
+
first += members.length
|
|
142
|
+
return section
|
|
143
|
+
})
|
|
112
144
|
return {
|
|
113
|
-
rows:
|
|
145
|
+
rows: groups.flatMap(g => g.members),
|
|
114
146
|
sections,
|
|
115
147
|
}
|
|
116
148
|
}
|
|
@@ -156,3 +188,45 @@ export function sectionPlacement(sections: WalkRowSection[], rowPx: number) {
|
|
|
156
188
|
},
|
|
157
189
|
}
|
|
158
190
|
}
|
|
191
|
+
|
|
192
|
+
// The rows' trees, a section at a time while grouped. walkRowsTree puts its
|
|
193
|
+
// i-th row at i * rowPx below its reference row and draws that row too, so
|
|
194
|
+
// each section is drawn shifted down to its place with its reference row
|
|
195
|
+
// dropped.
|
|
196
|
+
export function walkRowTrees(
|
|
197
|
+
bars: WalkRowsWithCalls,
|
|
198
|
+
sections: WalkRowSection[],
|
|
199
|
+
rowY: (i: number) => number,
|
|
200
|
+
frame: WalkRowsFrame & { rowPx: number },
|
|
201
|
+
o: Parameters<typeof walkRowsTree>[2] & { idPrefix: string },
|
|
202
|
+
): El[] {
|
|
203
|
+
if (sections.length === 0) {
|
|
204
|
+
return [walkRowsTree(bars, frame, o)]
|
|
205
|
+
}
|
|
206
|
+
return [
|
|
207
|
+
walkRowsTree({ ...bars, rows: [] }, frame, o),
|
|
208
|
+
...sections.flatMap((section, k) => {
|
|
209
|
+
const tree = walkRowsTree(
|
|
210
|
+
{
|
|
211
|
+
...bars,
|
|
212
|
+
rows: bars.rows.slice(
|
|
213
|
+
section.first - 1,
|
|
214
|
+
section.first - 1 + section.count,
|
|
215
|
+
),
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
...frame,
|
|
219
|
+
translateY:
|
|
220
|
+
frame.translateY +
|
|
221
|
+
(rowY(section.first) - frame.rowPx) * frame.scaleY,
|
|
222
|
+
},
|
|
223
|
+
{ ...o, idPrefix: `${o.idPrefix}-s${k}` },
|
|
224
|
+
)
|
|
225
|
+
return tree.children.filter(
|
|
226
|
+
(c): c is El =>
|
|
227
|
+
typeof c !== 'string' &&
|
|
228
|
+
c.attrs['data-testid'] !== 'graph-walk-reference',
|
|
229
|
+
)
|
|
230
|
+
}),
|
|
231
|
+
]
|
|
232
|
+
}
|
|
@@ -121,3 +121,9 @@ test('a solid run under the ramp is one piece in its own hue', () => {
|
|
|
121
121
|
const off = bars.rows[0]!.runs[1]!
|
|
122
122
|
expect(runPieces(off, ramp, frame.scaleX)).toHaveLength(1)
|
|
123
123
|
})
|
|
124
|
+
|
|
125
|
+
test('the spans state how far the longest row reaches', () => {
|
|
126
|
+
const all = [bars.reference, ...bars.rows]
|
|
127
|
+
expect(walkRowSpans(all, frame.scaleX).longest).toBe(3500)
|
|
128
|
+
expect(walkRowSpans([], frame.scaleX).longest).toBe(1)
|
|
129
|
+
})
|