jbrowse-plugin-graphgenomeviewer 6.1.4 → 6.2.0
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/dist/chunks/GbzBaseSyntenyAdapter-TLI5IUU7.js +8 -0
- package/dist/chunks/GbzBaseSyntenyAdapter-TLI5IUU7.js.map +7 -0
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +2 -2
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +2 -2
- package/package.json +2 -2
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.test.ts +78 -1
- package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.ts +132 -51
- package/src/GbzBaseSyntenyAdapter/index.ts +3 -1
- package/src/version.ts +1 -1
- package/dist/chunks/GbzBaseSyntenyAdapter-KQMN6UCE.js +0 -8
- package/dist/chunks/GbzBaseSyntenyAdapter-KQMN6UCE.js.map +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jbrowse-plugin-graphgenomeviewer",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"description": "JBrowse 2 graph genome (pangenome) view: rGFA reference-anchored layouts, the Bandage force-directed engine, and haplotype lanes read from a gbz-base database",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"private": false,
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@gmod/bgzf-filehandle": "^6.7.0",
|
|
29
|
-
"@gmod/gbz-base": "^7.
|
|
29
|
+
"@gmod/gbz-base": "^7.1.0",
|
|
30
30
|
"@gmod/tabix": "^3.9.1",
|
|
31
31
|
"@gmod/tubemap-core": "^0.1.0",
|
|
32
32
|
"@jbrowse/cigar-utils": "5.0.0-beta.11",
|
|
@@ -15,6 +15,7 @@ import Adapter, {
|
|
|
15
15
|
PairTargetError,
|
|
16
16
|
laneAssemblyName,
|
|
17
17
|
resolveReferenceSample,
|
|
18
|
+
widestWalkGap,
|
|
18
19
|
} from './GbzBaseSyntenyAdapter.ts'
|
|
19
20
|
import configSchema from './configSchema.ts'
|
|
20
21
|
import GbzBaseSyntenyAdapterF from './index.ts'
|
|
@@ -518,7 +519,7 @@ test('the adapter type declares that a window of its anchor answers any lane pai
|
|
|
518
519
|
pluginManager.createPluggableElements()
|
|
519
520
|
expect(
|
|
520
521
|
pluginManager.getAdapterType('GbzBaseSyntenyAdapter').adapterCapabilities,
|
|
521
|
-
).
|
|
522
|
+
).toEqual(expect.arrayContaining(['lanePairsOnAnchor', 'lanePairBatches']))
|
|
522
523
|
})
|
|
523
524
|
|
|
524
525
|
// HG00673#1's insertion is 16 bp longer than HG01361#2's, so that pair holds
|
|
@@ -692,6 +693,82 @@ test('a lane pair walks only its two lanes', async () => {
|
|
|
692
693
|
cut.mockRestore()
|
|
693
694
|
})
|
|
694
695
|
|
|
696
|
+
test('a batch of lane pairs answers each pair what it answers alone, from one cut', async () => {
|
|
697
|
+
const adapter = anchoredAdapter()
|
|
698
|
+
const pairs = [
|
|
699
|
+
{ queryAssemblyName: 'HG01361#2', targetAssemblyName: 'HG02145#2' },
|
|
700
|
+
{ queryAssemblyName: 'HG02145#2', targetAssemblyName: 'HG00673#1' },
|
|
701
|
+
{ queryAssemblyName: 'HG00673#1', targetAssemblyName: 'HG00438#1' },
|
|
702
|
+
]
|
|
703
|
+
const alone = await Promise.all(
|
|
704
|
+
pairs.map(async p =>
|
|
705
|
+
(await feats(adapter, insertionWindow, p)).map(f => f.toJSON()),
|
|
706
|
+
),
|
|
707
|
+
)
|
|
708
|
+
const { db } = await (
|
|
709
|
+
adapter as unknown as {
|
|
710
|
+
graph: () => Promise<{ db: { getSubgraphs: unknown } }>
|
|
711
|
+
}
|
|
712
|
+
).graph()
|
|
713
|
+
const cut = vi.spyOn(db, 'getSubgraphs')
|
|
714
|
+
const batch = await feats(adapter, insertionWindow, { lanePairs: pairs })
|
|
715
|
+
expect(cut).toHaveBeenCalledTimes(1)
|
|
716
|
+
cut.mockRestore()
|
|
717
|
+
pairs.forEach((p, i) => {
|
|
718
|
+
expect(alone[i]!.length).toBeGreaterThan(0)
|
|
719
|
+
expect(
|
|
720
|
+
batch
|
|
721
|
+
.filter(
|
|
722
|
+
f =>
|
|
723
|
+
f.get('assemblyName') === p.queryAssemblyName &&
|
|
724
|
+
mateOf(f).assemblyName === p.targetAssemblyName,
|
|
725
|
+
)
|
|
726
|
+
.map(f => f.toJSON()),
|
|
727
|
+
).toEqual(alone[i])
|
|
728
|
+
})
|
|
729
|
+
})
|
|
730
|
+
|
|
731
|
+
test('a kept walk in two pieces is the gap a pair cut widens its context for', () => {
|
|
732
|
+
const name = (sample: string, haplotype: number, contig = 'c') => ({
|
|
733
|
+
sample,
|
|
734
|
+
haplotype,
|
|
735
|
+
contig,
|
|
736
|
+
fragment: 0,
|
|
737
|
+
})
|
|
738
|
+
const subgraph = (
|
|
739
|
+
spans: { name: ReturnType<typeof name>; start: number; end: number }[],
|
|
740
|
+
) => ({ walkSpans: () => spans }) as unknown as Subgraph
|
|
741
|
+
const kept = new Set(['A#1', 'B#1'])
|
|
742
|
+
expect(
|
|
743
|
+
widestWalkGap(
|
|
744
|
+
[
|
|
745
|
+
subgraph([
|
|
746
|
+
{ name: name('A', 1), start: 0, end: 100 },
|
|
747
|
+
{ name: name('A', 1), start: 54_100, end: 60_000 },
|
|
748
|
+
{ name: name('B', 1), start: 0, end: 500 },
|
|
749
|
+
{ name: name('B', 1), start: 2500, end: 3000 },
|
|
750
|
+
{ name: name('C', 1), start: 0, end: 10 },
|
|
751
|
+
{ name: name('C', 1), start: 90_000, end: 90_010 },
|
|
752
|
+
]),
|
|
753
|
+
],
|
|
754
|
+
kept,
|
|
755
|
+
),
|
|
756
|
+
).toBe(54_000)
|
|
757
|
+
expect(
|
|
758
|
+
widestWalkGap(
|
|
759
|
+
[
|
|
760
|
+
subgraph([
|
|
761
|
+
{ name: name('A', 1), start: 0, end: 100 },
|
|
762
|
+
{ name: name('A', 1), start: 500_000, end: 500_100 },
|
|
763
|
+
{ name: name('B', 1, 'c1'), start: 0, end: 100 },
|
|
764
|
+
{ name: name('B', 1, 'c2'), start: 5000, end: 5100 },
|
|
765
|
+
]),
|
|
766
|
+
],
|
|
767
|
+
kept,
|
|
768
|
+
),
|
|
769
|
+
).toBe(0)
|
|
770
|
+
})
|
|
771
|
+
|
|
695
772
|
test('lane pair ids are the same across two fetches of one window', async () => {
|
|
696
773
|
const adapter = anchoredAdapter()
|
|
697
774
|
const ids = async () =>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GBZBase } from '@gmod/gbz-base'
|
|
1
|
+
import { GBZBase, SubgraphLimitError } from '@gmod/gbz-base'
|
|
2
2
|
import {
|
|
3
3
|
NodeLimitError,
|
|
4
4
|
cutWindowGFA,
|
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
PairAlignment,
|
|
35
35
|
PathName,
|
|
36
36
|
PathQuery,
|
|
37
|
+
Subgraph,
|
|
37
38
|
} from '@gmod/gbz-base'
|
|
38
39
|
import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter'
|
|
39
40
|
import type { Feature, SimpleFeatureSerialized } from '@jbrowse/core/util'
|
|
@@ -91,6 +92,16 @@ export interface GbzHeaderLane {
|
|
|
91
92
|
export interface GbzFeatureOptions extends ComparativeOptions {
|
|
92
93
|
haplotypes?: string[]
|
|
93
94
|
queryAssemblyName?: string
|
|
95
|
+
lanePairs?: LanePair[]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Two lanes aligned to each other inside an anchor window: records on the
|
|
100
|
+
* query lane's contigs, with the target lane's walk as the mate
|
|
101
|
+
*/
|
|
102
|
+
export interface LanePair {
|
|
103
|
+
queryAssemblyName: string
|
|
104
|
+
targetAssemblyName: string
|
|
94
105
|
}
|
|
95
106
|
|
|
96
107
|
export class PairTargetError extends Error {
|
|
@@ -103,6 +114,16 @@ export class PairTargetError extends Error {
|
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
116
|
|
|
117
|
+
function onePair(
|
|
118
|
+
queryAssemblyName: string,
|
|
119
|
+
targetAssemblyName: string | undefined,
|
|
120
|
+
): LanePair {
|
|
121
|
+
if (targetAssemblyName === undefined) {
|
|
122
|
+
throw new PairTargetError(queryAssemblyName)
|
|
123
|
+
}
|
|
124
|
+
return { queryAssemblyName, targetAssemblyName }
|
|
125
|
+
}
|
|
126
|
+
|
|
106
127
|
export class HaplotypeWindowError extends Error {
|
|
107
128
|
override name = 'HaplotypeWindowError'
|
|
108
129
|
|
|
@@ -213,6 +234,39 @@ export function pairFeature({
|
|
|
213
234
|
})
|
|
214
235
|
}
|
|
215
236
|
|
|
237
|
+
// The longest detour a pair cut widens its context to take in
|
|
238
|
+
const BRIDGE_MAX_BP = 120_000
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The widest gap, up to `BRIDGE_MAX_BP`, between two pieces of one kept walk:
|
|
242
|
+
* a haplotype that left the cut's nodes for sequence the reference lacks and
|
|
243
|
+
* came back. Two kept haplotypes that share that sequence only align across it
|
|
244
|
+
* once its nodes are in the cut.
|
|
245
|
+
*/
|
|
246
|
+
export function widestWalkGap(subgraphs: Subgraph[], kept: Set<string>) {
|
|
247
|
+
const byPath = new Map<string, [number, number][]>()
|
|
248
|
+
for (const subgraph of subgraphs) {
|
|
249
|
+
for (const { name, start, end } of subgraph.walkSpans()) {
|
|
250
|
+
const prefix = haplotypePrefix(name)
|
|
251
|
+
if (kept.has(prefix)) {
|
|
252
|
+
const key = `${prefix}#${name.contig}`
|
|
253
|
+
byPath.set(key, [...(byPath.get(key) ?? []), [start, end]])
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
let widest = 0
|
|
258
|
+
for (const spans of byPath.values()) {
|
|
259
|
+
spans.sort((a, b) => a[0] - b[0])
|
|
260
|
+
for (let i = 1; i < spans.length; i++) {
|
|
261
|
+
const gap = spans[i]![0] - spans[i - 1]![1]
|
|
262
|
+
if (gap > 0 && gap <= BRIDGE_MAX_BP) {
|
|
263
|
+
widest = Math.max(widest, gap)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return widest
|
|
268
|
+
}
|
|
269
|
+
|
|
216
270
|
// A window past the node limit fails as a zoom-in notice naming a span that
|
|
217
271
|
// fits
|
|
218
272
|
function zoomInNotice(error: unknown, what: string) {
|
|
@@ -495,71 +549,93 @@ export default class GbzBaseSyntenyAdapter extends ComparativeAdapterBase<GbzBas
|
|
|
495
549
|
}
|
|
496
550
|
|
|
497
551
|
/**
|
|
498
|
-
*
|
|
499
|
-
* of
|
|
500
|
-
* both visit, with no base compared.
|
|
501
|
-
* contigs, the lane the display draws on top.
|
|
552
|
+
* Each pair's two lanes cut out of the anchor window alone, all pairs in one
|
|
553
|
+
* cut, and each walk of a pair's query lane aligned to each walk of its
|
|
554
|
+
* target lane on the nodes both visit, with no base compared. A pair's
|
|
555
|
+
* records sit on its query lane's contigs, the lane the display draws on top.
|
|
502
556
|
*
|
|
503
557
|
* Whether the index has anchor rows changes only how fast gbz-base finds
|
|
504
558
|
* the walks: both routes cut the same pieces.
|
|
505
559
|
*/
|
|
506
560
|
private async pairFeatures(
|
|
507
561
|
region: Region,
|
|
508
|
-
|
|
562
|
+
pairs: LanePair[],
|
|
509
563
|
opts: GbzFeatureOptions,
|
|
510
564
|
) {
|
|
511
|
-
const { targetAssemblyName } = opts
|
|
512
|
-
if (targetAssemblyName === undefined) {
|
|
513
|
-
throw new PairTargetError(queryAssemblyName)
|
|
514
|
-
}
|
|
515
565
|
const { db } = await this.graph(opts)
|
|
516
566
|
const { refName, start, end } = region
|
|
517
|
-
const
|
|
518
|
-
|
|
519
|
-
|
|
567
|
+
const sides = await Promise.all(
|
|
568
|
+
pairs.map(async pair => ({
|
|
569
|
+
pair,
|
|
570
|
+
featureSide: await this.laneHaplotypes(pair.queryAssemblyName, opts),
|
|
571
|
+
mateSide: await this.laneHaplotypes(pair.targetAssemblyName, opts),
|
|
572
|
+
})),
|
|
573
|
+
)
|
|
574
|
+
const answerable = sides.filter(
|
|
575
|
+
({ featureSide, mateSide }) =>
|
|
576
|
+
featureSide.length > 0 && mateSide.length > 0,
|
|
577
|
+
)
|
|
578
|
+
const kept = new Set(
|
|
579
|
+
answerable.flatMap(({ featureSide, mateSide }) =>
|
|
580
|
+
[...featureSide, ...mateSide].map(haplotypePrefix),
|
|
581
|
+
),
|
|
582
|
+
)
|
|
520
583
|
const query = await this.referenceQuery(refName, opts)
|
|
521
584
|
const nodeLimit: number = this.getConf('nodeLimit')
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
585
|
+
const baseContext: number = this.getConf('context')
|
|
586
|
+
const [first] = pairs
|
|
587
|
+
const cut = (path: PathQuery, context: number) =>
|
|
588
|
+
db.getSubgraphs({
|
|
589
|
+
path,
|
|
590
|
+
start,
|
|
591
|
+
end,
|
|
592
|
+
context,
|
|
593
|
+
snarls: this.getConf('subgraphSnarls'),
|
|
594
|
+
haplotypes: 'all',
|
|
595
|
+
limit: nodeLimit,
|
|
596
|
+
signal: opts.signal,
|
|
597
|
+
keep: name => kept.has(haplotypePrefix(name)),
|
|
598
|
+
})
|
|
599
|
+
// a cut past the node limit for the wider context keeps the first one
|
|
600
|
+
const bridged = async (path: PathQuery) => {
|
|
601
|
+
const subgraphs = await cut(path, baseContext).catch((error: unknown) => {
|
|
602
|
+
throw nodeLimitError(error, nodeLimit, end - start) ?? error
|
|
603
|
+
})
|
|
604
|
+
const gap = widestWalkGap(subgraphs, kept)
|
|
605
|
+
const context = Math.ceil(gap / 2) + 2000
|
|
606
|
+
return gap > 0 && context > baseContext
|
|
607
|
+
? cut(path, context).catch((error: unknown) => {
|
|
608
|
+
if (error instanceof SubgraphLimitError) {
|
|
609
|
+
return subgraphs
|
|
610
|
+
}
|
|
611
|
+
throw error
|
|
612
|
+
})
|
|
613
|
+
: subgraphs
|
|
614
|
+
}
|
|
543
615
|
const subgraphs =
|
|
544
|
-
query &&
|
|
616
|
+
query && first && answerable.length > 0
|
|
545
617
|
? await updateStatus(
|
|
546
|
-
|
|
618
|
+
pairs.length === 1
|
|
619
|
+
? `Reading ${first.queryAssemblyName} against ${first.targetAssemblyName}`
|
|
620
|
+
: `Reading ${pairs.length} lane pairs`,
|
|
547
621
|
opts.statusCallback,
|
|
548
|
-
() =>
|
|
622
|
+
() => bridged(query),
|
|
549
623
|
)
|
|
550
624
|
: []
|
|
551
625
|
return subgraphs.flatMap(subgraph =>
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
626
|
+
answerable.flatMap(({ pair, featureSide, mateSide }) =>
|
|
627
|
+
featureSide.flatMap(target =>
|
|
628
|
+
mateSide.flatMap(mate =>
|
|
629
|
+
subgraph
|
|
630
|
+
.pairAlignments({ target, query: mate, bases: false })
|
|
631
|
+
.map(alignment =>
|
|
632
|
+
pairFeature({
|
|
633
|
+
pair: alignment,
|
|
634
|
+
lane: pair.queryAssemblyName,
|
|
635
|
+
mateLane: pair.targetAssemblyName,
|
|
636
|
+
}),
|
|
637
|
+
),
|
|
638
|
+
),
|
|
563
639
|
),
|
|
564
640
|
),
|
|
565
641
|
)
|
|
@@ -574,10 +650,15 @@ export default class GbzBaseSyntenyAdapter extends ComparativeAdapterBase<GbzBas
|
|
|
574
650
|
// the graph is indexed on its reference alone, so a window on a
|
|
575
651
|
// haplotype lane has no answer: a lane pair is read inside the anchor's
|
|
576
652
|
if (region.assemblyName === anchor) {
|
|
577
|
-
const features =
|
|
578
|
-
opts.
|
|
653
|
+
const features = opts.lanePairs?.length
|
|
654
|
+
? await this.pairFeatures(region, opts.lanePairs, opts)
|
|
655
|
+
: opts.queryAssemblyName === undefined
|
|
579
656
|
? await this.anchorFeatures(region, opts)
|
|
580
|
-
: await this.pairFeatures(
|
|
657
|
+
: await this.pairFeatures(
|
|
658
|
+
region,
|
|
659
|
+
[onePair(opts.queryAssemblyName, opts.targetAssemblyName)],
|
|
660
|
+
opts,
|
|
661
|
+
)
|
|
581
662
|
for (const feature of features) {
|
|
582
663
|
observer.next(feature)
|
|
583
664
|
}
|
|
@@ -17,11 +17,13 @@ export default function GbzBaseSyntenyAdapterF(pluginManager: PluginManager) {
|
|
|
17
17
|
// adapter and offer its lane picker over the whole graph.
|
|
18
18
|
// lanePairsOnAnchor: a window of the anchor answers any two lanes'
|
|
19
19
|
// alignment to each other, so the display fetches each adjacent pair
|
|
20
|
-
// rather than composing it through the reference
|
|
20
|
+
// rather than composing it through the reference.
|
|
21
|
+
// lanePairBatches: every pair on a window comes from one cut
|
|
21
22
|
adapterCapabilities: [
|
|
22
23
|
'getSubgraph',
|
|
23
24
|
'headerLanes',
|
|
24
25
|
'lanePairsOnAnchor',
|
|
26
|
+
'lanePairBatches',
|
|
25
27
|
],
|
|
26
28
|
adapterMetadata: {
|
|
27
29
|
category: 'Synteny adapters',
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '6.
|
|
1
|
+
export const version = '6.2.0'
|