jbrowse-plugin-graphgenomeviewer 8.3.4 → 8.3.6
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 +2 -2
- package/dist/chunks/{GbzBaseSyntenyAdapter-GX3NGJHG.js → GbzBaseSyntenyAdapter-BG6FNGHY.js} +2 -2
- package/dist/chunks/{MinigraphBubbleAdapter-XYAUUM35.js → MinigraphBubbleAdapter-P66QLENP.js} +2 -2
- package/dist/chunks/{RgfaTabixAdapter-SBIUUIRI.js → RgfaTabixAdapter-CQJROUIP.js} +2 -2
- package/dist/chunks/{WalkFormsAdapter-ANELMZU4.js → WalkFormsAdapter-YSYANJD5.js} +2 -2
- package/dist/chunks/WalkTabixSyntenyAdapter-7COETKFB.js +2 -0
- package/dist/chunks/WalkTabixSyntenyAdapter-7COETKFB.js.map +7 -0
- package/dist/chunks/chunk-DAF7EWRF.js +3 -0
- package/dist/chunks/{chunk-EENENDGZ.js.map → chunk-DAF7EWRF.js.map} +4 -4
- package/dist/chunks/{chunk-WANYNRGU.js → chunk-HLWIFWDZ.js} +3 -3
- package/dist/chunks/{chunk-WANYNRGU.js.map → chunk-HLWIFWDZ.js.map} +2 -2
- package/dist/chunks/{chunk-OXU7F4BR.js → chunk-KVZIGTH3.js} +2 -2
- package/dist/chunks/{chunk-OHQ3WIPI.js → chunk-RCJQCBBI.js} +2 -2
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +2 -2
- package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/RgfaTabixAdapter/test_data/walks5_skipfollow.GRCh38.walks.bed.gz +0 -0
- package/src/RgfaTabixAdapter/test_data/walks5_skipfollow.GRCh38.walks.bed.gz.tbi +0 -0
- package/src/RgfaTabixAdapter/test_data/walks5_skipinv.GRCh38.walks.bed.gz +0 -0
- package/src/RgfaTabixAdapter/test_data/walks5_skipinv.GRCh38.walks.bed.gz.tbi +0 -0
- package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.test.ts +154 -0
- package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +148 -28
- package/src/WalkTabixSyntenyAdapter/walkLanes.ts +111 -1
- package/src/readinessConformance.test.ts +66 -0
- package/src/synteny/clipFeatureToRegion.ts +3 -2
- package/src/version.ts +1 -1
- package/dist/chunks/WalkTabixSyntenyAdapter-I3MGWUQ5.js +0 -2
- package/dist/chunks/WalkTabixSyntenyAdapter-I3MGWUQ5.js.map +0 -7
- package/dist/chunks/chunk-EENENDGZ.js +0 -3
- /package/dist/chunks/{GbzBaseSyntenyAdapter-GX3NGJHG.js.map → GbzBaseSyntenyAdapter-BG6FNGHY.js.map} +0 -0
- /package/dist/chunks/{MinigraphBubbleAdapter-XYAUUM35.js.map → MinigraphBubbleAdapter-P66QLENP.js.map} +0 -0
- /package/dist/chunks/{RgfaTabixAdapter-SBIUUIRI.js.map → RgfaTabixAdapter-CQJROUIP.js.map} +0 -0
- /package/dist/chunks/{WalkFormsAdapter-ANELMZU4.js.map → WalkFormsAdapter-YSYANJD5.js.map} +0 -0
- /package/dist/chunks/{chunk-OXU7F4BR.js.map → chunk-KVZIGTH3.js.map} +0 -0
- /package/dist/chunks/{chunk-OHQ3WIPI.js.map → chunk-RCJQCBBI.js.map} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jbrowse-plugin-graphgenomeviewer",
|
|
3
|
-
"version": "8.3.
|
|
3
|
+
"version": "8.3.6",
|
|
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": {
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@gmod/bgzf-filehandle": "^6.8.2",
|
|
29
29
|
"@gmod/gbz-base": "^7.4.1",
|
|
30
|
-
"@gmod/tabix": "^3.13.
|
|
30
|
+
"@gmod/tabix": "^3.13.1",
|
|
31
31
|
"@jbrowse/cigar-utils": "5.0.0-beta.18",
|
|
32
32
|
"@jbrowse/core": "5.0.0-beta.18",
|
|
33
33
|
"@jbrowse/display-kit": "5.0.0-beta.18",
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { TabixIndexedFile } from '@gmod/tabix'
|
|
1
2
|
import { graphTablesGFA } from '@jbrowse/bandage-core/gfa/graphTables'
|
|
2
3
|
import { walkRowsOf } from '@jbrowse/bandage-core/layout/walkRowRuns'
|
|
3
4
|
import { walkRows } from '@jbrowse/bandage-core/layout/walkRows'
|
|
4
5
|
import { loadGraph } from '@jbrowse/bandage-core/pipeline'
|
|
5
6
|
import PluginManager from '@jbrowse/core/PluginManager'
|
|
6
7
|
import { readConfObject } from '@jbrowse/core/configuration'
|
|
8
|
+
import { WalkReader } from '@jbrowse/graph-walks'
|
|
7
9
|
import { firstValueFrom } from 'rxjs'
|
|
8
10
|
import { toArray } from 'rxjs/operators'
|
|
9
11
|
|
|
@@ -247,6 +249,158 @@ test('a window on a haplotype lane answers nothing', async () => {
|
|
|
247
249
|
expect(found).toEqual([])
|
|
248
250
|
})
|
|
249
251
|
|
|
252
|
+
// gfa-to-tabix --walks --chunk 1000 of twenty 1 kb reference nodes; every
|
|
253
|
+
// haplotype but HG003 skips 2000-5000, so chunks 2000-4000 hold skip rows:
|
|
254
|
+
// W GRCh38 0 chr1 >1>2>3>...>20
|
|
255
|
+
// W HG002 1 ctg >1>2>6>...>20
|
|
256
|
+
// W HG004 1 ctg <20<...<6<2<1 the same walk backwards
|
|
257
|
+
// W HG003 1 ctg >1>2>3>...>20
|
|
258
|
+
// W HG007 1 ctg >1>2>6>...>20 a W line starting at 100,000
|
|
259
|
+
// W HG005 1 ctg >1>2>99>6>...>20 500 bp of node 99 in the gap
|
|
260
|
+
const skipWindow = { ...window, start: 3100, end: 3900 }
|
|
261
|
+
const skipFeats = (
|
|
262
|
+
opts: Record<string, unknown> = {},
|
|
263
|
+
name = 'walks5_skipfollow',
|
|
264
|
+
) =>
|
|
265
|
+
firstValueFrom(
|
|
266
|
+
makeAdapter({}, name)
|
|
267
|
+
.getFeaturesInMultipleRegions([skipWindow], opts as never)
|
|
268
|
+
.pipe(toArray()),
|
|
269
|
+
)
|
|
270
|
+
const skipSummary = (f: { get: (k: string) => unknown }) => ({
|
|
271
|
+
...summary(f),
|
|
272
|
+
strand: f.get('strand'),
|
|
273
|
+
spansWindow: f.get('spansWindow'),
|
|
274
|
+
})
|
|
275
|
+
const deletion = (lane: string, mate: string, strand: number) => ({
|
|
276
|
+
lane,
|
|
277
|
+
refName: 'chr1',
|
|
278
|
+
start: 2000,
|
|
279
|
+
end: 5000,
|
|
280
|
+
mate,
|
|
281
|
+
cigar: undefined,
|
|
282
|
+
strand,
|
|
283
|
+
spansWindow: true,
|
|
284
|
+
})
|
|
285
|
+
const crossings = [
|
|
286
|
+
deletion('HG002#1', 'ctg:2000-2000', 1),
|
|
287
|
+
deletion('HG004#1', 'ctg:15000-15000', -1),
|
|
288
|
+
deletion('HG005#1', 'ctg:2000-2000', 1),
|
|
289
|
+
deletion('HG007#1', 'ctg:102000-102000', 1),
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
test('a haplotype whose deletion spans the window is one spansWindow record of the whole deletion, its mate where it crosses', async () => {
|
|
293
|
+
const found = await skipFeats()
|
|
294
|
+
expect(found.map(skipSummary)).toEqual([
|
|
295
|
+
{
|
|
296
|
+
lane: 'HG003#1',
|
|
297
|
+
refName: 'chr1',
|
|
298
|
+
start: 2000,
|
|
299
|
+
end: 5000,
|
|
300
|
+
mate: 'ctg:2000-5000',
|
|
301
|
+
cigar: '3000=',
|
|
302
|
+
strand: 1,
|
|
303
|
+
spansWindow: undefined,
|
|
304
|
+
},
|
|
305
|
+
...crossings,
|
|
306
|
+
])
|
|
307
|
+
expect(found[1]!.get('identity')).toBeUndefined()
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
test("the display's clip passes a deletion through whole, so its extent is the deletion's length", async () => {
|
|
311
|
+
const found = await skipFeats({
|
|
312
|
+
haplotypes: ['HG002'],
|
|
313
|
+
clipToRegion: true,
|
|
314
|
+
splitAtGapBp: 10_000,
|
|
315
|
+
})
|
|
316
|
+
expect(found.map(skipSummary)).toEqual([crossings[0]])
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
test('a walk passing the node it leaves at inverted still crosses after it', async () => {
|
|
320
|
+
const found = await skipFeats({ haplotypes: ['HG008'] }, 'walks5_skipinv')
|
|
321
|
+
expect(found.map(skipSummary)).toEqual([
|
|
322
|
+
deletion('HG008#1', 'ctg:2000-2000', 1),
|
|
323
|
+
])
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
test('a skipping haplotype outside the lanes asked for opens no lane', async () => {
|
|
327
|
+
const found = await skipFeats({ haplotypes: ['HG003'] })
|
|
328
|
+
expect(found.map(f => mateOf(f).assemblyName)).toEqual(['HG003#1'])
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
test('the crossing cut reads only the chunks before the node a haplotype leaves', async () => {
|
|
332
|
+
const getLines = vi.spyOn(TabixIndexedFile.prototype, 'getLines')
|
|
333
|
+
await skipFeats({ haplotypes: ['HG002'] })
|
|
334
|
+
const reads = getLines.mock.calls.map(call => `${call[1]}-${call[2]}`)
|
|
335
|
+
getLines.mockRestore()
|
|
336
|
+
expect(reads.at(-1)).toBe('0-2000')
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
test('a haplotype whose leaving node the cut lacks is crossed from the node it rejoins', async () => {
|
|
340
|
+
const cut = WalkReader.prototype.cut
|
|
341
|
+
const spy = vi
|
|
342
|
+
.spyOn(WalkReader.prototype, 'cut')
|
|
343
|
+
.mockImplementation(async function (
|
|
344
|
+
this: WalkReader,
|
|
345
|
+
refName,
|
|
346
|
+
region,
|
|
347
|
+
opts,
|
|
348
|
+
) {
|
|
349
|
+
const found = await cut.call(this, refName, region, opts)
|
|
350
|
+
return opts?.contained && region.end === 2000
|
|
351
|
+
? { ...found, fragments: [] }
|
|
352
|
+
: found
|
|
353
|
+
})
|
|
354
|
+
const found = await skipFeats()
|
|
355
|
+
spy.mockRestore()
|
|
356
|
+
// HG005 rejoins past the 500 bp it carries in the gap
|
|
357
|
+
expect(found.slice(1).map(skipSummary)).toEqual([
|
|
358
|
+
crossings[0],
|
|
359
|
+
crossings[1],
|
|
360
|
+
deletion('HG005#1', 'ctg:2500-2500', 1),
|
|
361
|
+
crossings[3],
|
|
362
|
+
])
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
test('a failed crossing cut drops only its own lanes', async () => {
|
|
366
|
+
const cut = WalkReader.prototype.cut
|
|
367
|
+
const spy = vi
|
|
368
|
+
.spyOn(WalkReader.prototype, 'cut')
|
|
369
|
+
.mockImplementation(function (this: WalkReader, refName, region, opts) {
|
|
370
|
+
return opts?.contained
|
|
371
|
+
? Promise.reject(new Error('no such chunk'))
|
|
372
|
+
: cut.call(this, refName, region, opts)
|
|
373
|
+
})
|
|
374
|
+
const found = await skipFeats()
|
|
375
|
+
spy.mockRestore()
|
|
376
|
+
expect(found.map(f => mateOf(f).assemblyName)).toEqual(['HG003#1'])
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
// RgfaTabixAdapter/walkFiles.test.ts's walks5_skip: HG002#1 >1>2>9>10 over
|
|
380
|
+
// ten 10 bp reference nodes in 10 bp chunks
|
|
381
|
+
test('a named haplotype skipping the window is a deletion of the skipped stretch', async () => {
|
|
382
|
+
const found = await firstValueFrom(
|
|
383
|
+
makeAdapter({}, 'walks5_skip')
|
|
384
|
+
.getFeatures(
|
|
385
|
+
{ ...window, start: 31, end: 60 },
|
|
386
|
+
{ haplotypes: ['HG002#1'] },
|
|
387
|
+
)
|
|
388
|
+
.pipe(toArray()),
|
|
389
|
+
)
|
|
390
|
+
expect(found.map(skipSummary)).toEqual([
|
|
391
|
+
{
|
|
392
|
+
lane: 'HG002#1',
|
|
393
|
+
refName: 'chr1',
|
|
394
|
+
start: 20,
|
|
395
|
+
end: 80,
|
|
396
|
+
mate: 'ctg:20-20',
|
|
397
|
+
cigar: undefined,
|
|
398
|
+
strand: 1,
|
|
399
|
+
spansWindow: true,
|
|
400
|
+
},
|
|
401
|
+
])
|
|
402
|
+
})
|
|
403
|
+
|
|
250
404
|
test('clipToRegion cuts a record to the window on both axes', async () => {
|
|
251
405
|
const found = await firstValueFrom(
|
|
252
406
|
makeAdapter()
|
|
@@ -10,7 +10,13 @@ import { updateStatus } from '@jbrowse/core/util'
|
|
|
10
10
|
import { ObservableCreate } from '@jbrowse/core/util/rxjs'
|
|
11
11
|
import { walksKept } from '@jbrowse/graph-walks'
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
anchorAligner,
|
|
15
|
+
chainFeature,
|
|
16
|
+
lanePairAligner,
|
|
17
|
+
skipCrossing,
|
|
18
|
+
skipFeature,
|
|
19
|
+
} from './walkLanes.ts'
|
|
14
20
|
import { WalkIndex } from '../RgfaTabixAdapter/walkReader.ts'
|
|
15
21
|
import { PanSNRefNames, openTabixSlot } from '../panSNTabix.ts'
|
|
16
22
|
import { ComparativeAdapterBase } from '../synteny/ComparativeAdapterBase.ts'
|
|
@@ -22,15 +28,15 @@ import {
|
|
|
22
28
|
import { laneZoomNotice } from '../synteny/laneZoomNotice.ts'
|
|
23
29
|
|
|
24
30
|
import type { WalkTabixSyntenyAdapterConfig } from './configSchema.ts'
|
|
31
|
+
import type { SkipCrossing, SkipSide } from './walkLanes.ts'
|
|
25
32
|
import type { SubgraphAdapterOptions } from '../GetSubgraph.ts'
|
|
26
33
|
import type { LaneFeatureOptions, LanePair } from '../synteny/lanePairs.ts'
|
|
27
34
|
import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter'
|
|
28
35
|
import type { Feature } from '@jbrowse/core/util'
|
|
29
36
|
import type { Region } from '@jbrowse/core/util/types'
|
|
30
|
-
import type {
|
|
37
|
+
import type { WalkSkip } from '@jbrowse/graph-walks'
|
|
31
38
|
|
|
32
|
-
const haplotypeOf = (
|
|
33
|
-
panSNHaplotype(fragment.name) ?? fragment.name
|
|
39
|
+
const haplotypeOf = ({ name }: { name: string }) => panSNHaplotype(name) ?? name
|
|
34
40
|
|
|
35
41
|
export default class WalkTabixSyntenyAdapter extends ComparativeAdapterBase<WalkTabixSyntenyAdapterConfig> {
|
|
36
42
|
private readonly walks: WalkIndex = new WalkIndex(
|
|
@@ -135,9 +141,93 @@ export default class WalkTabixSyntenyAdapter extends ComparativeAdapterBase<Walk
|
|
|
135
141
|
)
|
|
136
142
|
}
|
|
137
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Where each skipping haplotype crosses its deletion, read from contained
|
|
146
|
+
* cuts over the reference node it leaves, one cut per chunk those nodes
|
|
147
|
+
* fall in, and for any that cut misses, over the node it rejoins. A cut
|
|
148
|
+
* that fails drops only its own haplotypes' crossings.
|
|
149
|
+
*/
|
|
150
|
+
private async skipCrossings(
|
|
151
|
+
refName: string,
|
|
152
|
+
skips: WalkSkip[],
|
|
153
|
+
opts: LaneFeatureOptions,
|
|
154
|
+
) {
|
|
155
|
+
const found: SkipCrossing[] = []
|
|
156
|
+
let missed = skips
|
|
157
|
+
for (const side of ['leave', 'rejoin'] as const) {
|
|
158
|
+
const crossings = await this.sideCrossings(refName, missed, side, opts)
|
|
159
|
+
for (const crossing of crossings) {
|
|
160
|
+
found.push(crossing)
|
|
161
|
+
}
|
|
162
|
+
const placed = new Set(crossings.map(crossing => crossing.skip))
|
|
163
|
+
missed = missed.filter(skip => !placed.has(skip))
|
|
164
|
+
}
|
|
165
|
+
return found
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async sideCrossings(
|
|
169
|
+
refName: string,
|
|
170
|
+
skips: WalkSkip[],
|
|
171
|
+
side: SkipSide,
|
|
172
|
+
opts: LaneFeatureOptions,
|
|
173
|
+
) {
|
|
174
|
+
const { signal } = opts
|
|
175
|
+
const { chunk } = await this.walks.reader.header({ signal })
|
|
176
|
+
const boundary = (skip: WalkSkip) =>
|
|
177
|
+
side === 'leave' ? skip.refStart - 1 : skip.refEnd
|
|
178
|
+
const byChunk = new Map<
|
|
179
|
+
number,
|
|
180
|
+
{ group: WalkSkip[]; start: number; end: number }
|
|
181
|
+
>()
|
|
182
|
+
for (const skip of skips) {
|
|
183
|
+
const at = boundary(skip)
|
|
184
|
+
const cs = Math.floor(at / chunk)
|
|
185
|
+
const held = byChunk.get(cs)
|
|
186
|
+
if (held) {
|
|
187
|
+
held.group.push(skip)
|
|
188
|
+
held.start = Math.min(held.start, at)
|
|
189
|
+
held.end = Math.max(held.end, at + 1)
|
|
190
|
+
} else {
|
|
191
|
+
byChunk.set(cs, { group: [skip], start: at, end: at + 1 })
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const found = await Promise.all(
|
|
195
|
+
[...byChunk.values()].map(async ({ group, start, end }) => {
|
|
196
|
+
const names = new Set(group.map(skip => skip.name))
|
|
197
|
+
const keep = (name: string) => names.has(name)
|
|
198
|
+
try {
|
|
199
|
+
const { graph, fragments } = await this.walks.reader.cut(
|
|
200
|
+
refName,
|
|
201
|
+
{ start, end },
|
|
202
|
+
{
|
|
203
|
+
...this.walks.budgets(),
|
|
204
|
+
signal,
|
|
205
|
+
keep,
|
|
206
|
+
expand: keep,
|
|
207
|
+
context: 0,
|
|
208
|
+
contained: true,
|
|
209
|
+
links: false,
|
|
210
|
+
},
|
|
211
|
+
)
|
|
212
|
+
return group.map(skip =>
|
|
213
|
+
skipCrossing(skip, refName, fragments, graph.nodes, side),
|
|
214
|
+
)
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (signal?.aborted) {
|
|
217
|
+
throw error
|
|
218
|
+
}
|
|
219
|
+
return []
|
|
220
|
+
}
|
|
221
|
+
}),
|
|
222
|
+
)
|
|
223
|
+
return found.flat().filter(crossing => crossing !== undefined)
|
|
224
|
+
}
|
|
225
|
+
|
|
138
226
|
/**
|
|
139
227
|
* One record per stretch a haplotype's walk shares with the reference's,
|
|
140
|
-
* on the reference with the haplotype as its mate
|
|
228
|
+
* on the reference with the haplotype as its mate. A haplotype whose
|
|
229
|
+
* deletion spans the window, with no walk there on any contig, is one
|
|
230
|
+
* `spansWindow` record of the skipped stretch.
|
|
141
231
|
*/
|
|
142
232
|
private async anchorFeatures(
|
|
143
233
|
refName: string,
|
|
@@ -163,41 +253,71 @@ export default class WalkTabixSyntenyAdapter extends ComparativeAdapterBase<Walk
|
|
|
163
253
|
return []
|
|
164
254
|
}
|
|
165
255
|
const { start, end } = region
|
|
166
|
-
const
|
|
256
|
+
const status = `Reading walks ${region.refName}:${start.toLocaleString()}-${end.toLocaleString()}`
|
|
257
|
+
const { graph, fragments, skips } = await this.laneWalks(
|
|
167
258
|
refName,
|
|
168
259
|
region,
|
|
169
260
|
prefixes,
|
|
170
261
|
opts,
|
|
171
|
-
|
|
262
|
+
status,
|
|
172
263
|
)
|
|
173
|
-
const align = anchorAligner(graph.nodes)
|
|
174
|
-
const asmByPrefix = assemblyByPanSNPrefix(this)
|
|
175
|
-
const references = fragments.filter(f => f.name === refName)
|
|
176
264
|
// lanes come back in the order the fetch names them, which is the order
|
|
177
265
|
// the display stacks lanes of equal weight in
|
|
178
|
-
const rank = (
|
|
266
|
+
const rank = ({ name }: { name: string }) =>
|
|
179
267
|
prefixes === undefined
|
|
180
268
|
? 0
|
|
181
|
-
: prefixes.findIndex(prefix =>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
.
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
query,
|
|
194
|
-
assemblyName: region.assemblyName,
|
|
195
|
-
refName: region.refName,
|
|
196
|
-
mateAssemblyName: laneAssemblyName(asmByPrefix, haplotypeOf(query)),
|
|
197
|
-
}),
|
|
269
|
+
: prefixes.findIndex(prefix => panSNMatchesPrefix(name, prefix))
|
|
270
|
+
// NA19036#1 skips the IGL locus on one contig and walks it on another
|
|
271
|
+
const walking = new Set(fragments.map(haplotypeOf))
|
|
272
|
+
const crossings = await updateStatus(status, opts.statusCallback, () =>
|
|
273
|
+
this.skipCrossings(
|
|
274
|
+
refName,
|
|
275
|
+
skips.filter(
|
|
276
|
+
skip =>
|
|
277
|
+
rank(skip) >= 0 &&
|
|
278
|
+
!walking.has(haplotypeOf(skip)) &&
|
|
279
|
+
skip.refStart <= start &&
|
|
280
|
+
skip.refEnd >= end,
|
|
198
281
|
),
|
|
282
|
+
opts,
|
|
199
283
|
),
|
|
200
284
|
)
|
|
285
|
+
const align = anchorAligner(graph.nodes)
|
|
286
|
+
const asmByPrefix = assemblyByPanSNPrefix(this)
|
|
287
|
+
const laneOf = (walk: { name: string }) =>
|
|
288
|
+
laneAssemblyName(asmByPrefix, haplotypeOf(walk))
|
|
289
|
+
const references = fragments.filter(f => f.name === refName)
|
|
290
|
+
const walked = fragments
|
|
291
|
+
.filter(fragment => fragment.name !== refName && rank(fragment) >= 0)
|
|
292
|
+
.map(query => ({
|
|
293
|
+
rank: rank(query),
|
|
294
|
+
features: references.flatMap(reference =>
|
|
295
|
+
align(query, reference).map(chain =>
|
|
296
|
+
chainFeature({
|
|
297
|
+
chain,
|
|
298
|
+
target: reference,
|
|
299
|
+
query,
|
|
300
|
+
assemblyName: region.assemblyName,
|
|
301
|
+
refName: region.refName,
|
|
302
|
+
mateAssemblyName: laneOf(query),
|
|
303
|
+
}),
|
|
304
|
+
),
|
|
305
|
+
),
|
|
306
|
+
}))
|
|
307
|
+
const skipped = crossings.map(crossing => ({
|
|
308
|
+
rank: rank(crossing.skip),
|
|
309
|
+
features: [
|
|
310
|
+
skipFeature({
|
|
311
|
+
crossing,
|
|
312
|
+
refName: region.refName,
|
|
313
|
+
assemblyName: region.assemblyName,
|
|
314
|
+
mateAssemblyName: laneOf(crossing.skip),
|
|
315
|
+
}),
|
|
316
|
+
],
|
|
317
|
+
}))
|
|
318
|
+
return [...walked, ...skipped]
|
|
319
|
+
.sort((a, b) => a.rank - b.rank)
|
|
320
|
+
.flatMap(lane => lane.features)
|
|
201
321
|
}
|
|
202
322
|
|
|
203
323
|
/**
|
|
@@ -2,10 +2,16 @@ import { weightedLcs } from '@gmod/gbz-base/lcs'
|
|
|
2
2
|
import { pairAlignments, pairCigar } from '@gmod/gbz-base/pairAlignment'
|
|
3
3
|
import { panSNContig } from '@jbrowse/bandage-core/pansn'
|
|
4
4
|
|
|
5
|
+
import SyntenyFeature from '../synteny/SyntenyFeature.ts'
|
|
5
6
|
import { matchFeature } from '../synteny/matchFeature.ts'
|
|
6
7
|
|
|
7
8
|
import type { PairChain, PairEdit } from '@gmod/gbz-base/pairAlignment'
|
|
8
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
WalkFragment,
|
|
11
|
+
WalkNode,
|
|
12
|
+
WalkSkip,
|
|
13
|
+
WalkStop,
|
|
14
|
+
} from '@jbrowse/graph-walks'
|
|
9
15
|
|
|
10
16
|
// The longest bubble with equal bases on both sides that is written as
|
|
11
17
|
// mismatches; a longer one stays an insertion and a deletion
|
|
@@ -294,3 +300,107 @@ export function chainFeature({
|
|
|
294
300
|
columns: edits.reduce((sum, [, n]) => sum + n, 0),
|
|
295
301
|
})
|
|
296
302
|
}
|
|
303
|
+
|
|
304
|
+
/** The point on a haplotype's contig where it crosses a skipped stretch */
|
|
305
|
+
export interface SkipCrossing {
|
|
306
|
+
skip: WalkSkip
|
|
307
|
+
at: number
|
|
308
|
+
strand: '+' | '-'
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* The reference node either side of a skipped stretch: the one a haplotype
|
|
313
|
+
* leaves, ending at `refStart`, or the one it rejoins, starting at `refEnd`
|
|
314
|
+
*/
|
|
315
|
+
export type SkipSide = 'leave' | 'rejoin'
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Where a haplotype that skips the window crosses its deletion, from a cut
|
|
319
|
+
* over the reference node on one `side` of it. The jump lies on whichever end
|
|
320
|
+
* of the walk's fragment goes on to a chunk across the stretch; with neither
|
|
321
|
+
* or both, on the end the fragment's other steps leave free, and failing that
|
|
322
|
+
* on the end the node's own direction points to. A walk that passes the node
|
|
323
|
+
* inverted between two forward steps still crosses after it.
|
|
324
|
+
*/
|
|
325
|
+
export function skipCrossing(
|
|
326
|
+
skip: WalkSkip,
|
|
327
|
+
refName: string,
|
|
328
|
+
fragments: WalkFragment[],
|
|
329
|
+
nodes: Map<number, WalkNode>,
|
|
330
|
+
side: SkipSide,
|
|
331
|
+
): SkipCrossing | undefined {
|
|
332
|
+
const leaving = side === 'leave'
|
|
333
|
+
const across = (stop: WalkStop) =>
|
|
334
|
+
stop.kind === 'elsewhere' &&
|
|
335
|
+
(leaving
|
|
336
|
+
? stop.chunk.start >= skip.refStart
|
|
337
|
+
: stop.chunk.start < skip.refStart)
|
|
338
|
+
for (const fragment of fragments) {
|
|
339
|
+
const { name, fragStart, hapStart, ids, rev, before, after } = fragment
|
|
340
|
+
if (name === skip.name && fragStart === skip.fragStart) {
|
|
341
|
+
let at = hapStart
|
|
342
|
+
for (let i = 0; i < ids.length; i++) {
|
|
343
|
+
const node = nodes.get(ids[i]!)
|
|
344
|
+
const length = node === undefined ? 0 : node.end - node.start
|
|
345
|
+
if (
|
|
346
|
+
node?.rank === 0 &&
|
|
347
|
+
node.refName === refName &&
|
|
348
|
+
(leaving ? node.end === skip.refStart : node.start === skip.refEnd)
|
|
349
|
+
) {
|
|
350
|
+
const jumpsAfter =
|
|
351
|
+
across(after) !== across(before)
|
|
352
|
+
? across(after)
|
|
353
|
+
: i > 0 !== i < ids.length - 1
|
|
354
|
+
? i > 0
|
|
355
|
+
: leaving !== (rev[i] === 1)
|
|
356
|
+
return {
|
|
357
|
+
skip,
|
|
358
|
+
at: jumpsAfter ? at + length : at,
|
|
359
|
+
strand: jumpsAfter === leaving ? '+' : '-',
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
at += length
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return undefined
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* A haplotype whose deletion spans the whole window, as the record a
|
|
371
|
+
* multi-way display lifts out as a deletion: `spansWindow`, the skipped
|
|
372
|
+
* stretch whole so its length heads the lane, and the crossing as its mate.
|
|
373
|
+
* The record carries no CIGAR and no identity, so no ramp or identity filter
|
|
374
|
+
* reads it as an alignment.
|
|
375
|
+
*/
|
|
376
|
+
export function skipFeature({
|
|
377
|
+
crossing,
|
|
378
|
+
refName,
|
|
379
|
+
assemblyName,
|
|
380
|
+
mateAssemblyName,
|
|
381
|
+
}: {
|
|
382
|
+
crossing: SkipCrossing
|
|
383
|
+
refName: string
|
|
384
|
+
assemblyName: string
|
|
385
|
+
mateAssemblyName: string
|
|
386
|
+
}) {
|
|
387
|
+
const { skip, at, strand } = crossing
|
|
388
|
+
const id = `${skip.name}:${skip.fragStart}:${skip.piece}:skip`
|
|
389
|
+
return new SyntenyFeature({
|
|
390
|
+
uniqueId: id,
|
|
391
|
+
assemblyName,
|
|
392
|
+
refName,
|
|
393
|
+
start: skip.refStart,
|
|
394
|
+
end: skip.refEnd,
|
|
395
|
+
type: 'deletion',
|
|
396
|
+
strand: strand === '-' ? -1 : 1,
|
|
397
|
+
syntenyId: id,
|
|
398
|
+
spansWindow: true,
|
|
399
|
+
mate: {
|
|
400
|
+
refName: panSNContig(skip.name),
|
|
401
|
+
start: at,
|
|
402
|
+
end: at,
|
|
403
|
+
assemblyName: mateAssemblyName,
|
|
404
|
+
},
|
|
405
|
+
})
|
|
406
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import PluginManager from '@jbrowse/core/PluginManager'
|
|
2
|
+
import LinearGenomeViewPlugin from '@jbrowse/plugin-linear-genome-view'
|
|
3
|
+
import { expect, test } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import GraphGenomeViewPlugin from './index'
|
|
6
|
+
|
|
7
|
+
import type DisplayType from '@jbrowse/core/pluggableElementTypes/DisplayType'
|
|
8
|
+
import type ViewType from '@jbrowse/core/pluggableElementTypes/ViewType'
|
|
9
|
+
|
|
10
|
+
// jb.waitReady, the app's ready marker, screenshot tools and the session
|
|
11
|
+
// thumbnail read an absent displayPhase or showLoading as finished, so a type
|
|
12
|
+
// without one reads ready while it loads. The same check for jbrowse-components'
|
|
13
|
+
// own types is products/jbrowse-web/src/schemaTests/ElementsReportLoading.test.ts
|
|
14
|
+
// there.
|
|
15
|
+
|
|
16
|
+
async function setup() {
|
|
17
|
+
const pluginManager = new PluginManager([
|
|
18
|
+
new LinearGenomeViewPlugin(),
|
|
19
|
+
new GraphGenomeViewPlugin(),
|
|
20
|
+
])
|
|
21
|
+
pluginManager.createPluggableElements()
|
|
22
|
+
pluginManager.configure()
|
|
23
|
+
const displays = pluginManager.getElementTypesInGroup(
|
|
24
|
+
'display',
|
|
25
|
+
) as DisplayType[]
|
|
26
|
+
const views = pluginManager.getElementTypesInGroup('view') as ViewType[]
|
|
27
|
+
await Promise.all([...displays, ...views].map(t => t.loadStateModel()))
|
|
28
|
+
return { pluginManager, displays, views }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function missing(instance: object, keys: string[]) {
|
|
32
|
+
return keys.filter(k => !(k in instance))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
test('every display type publishes displayPhase and svgReady', async () => {
|
|
36
|
+
const { pluginManager, displays } = await setup()
|
|
37
|
+
expect(displays.length).toBeGreaterThan(0)
|
|
38
|
+
const gaps = displays.flatMap(t => {
|
|
39
|
+
const display = t.stateModel.create(
|
|
40
|
+
{
|
|
41
|
+
type: t.name,
|
|
42
|
+
configuration: t.configSchema.create({ displayId: 'x', type: t.name }),
|
|
43
|
+
},
|
|
44
|
+
{ pluginManager },
|
|
45
|
+
)
|
|
46
|
+
const keys = missing(display, ['displayPhase', 'svgReady'])
|
|
47
|
+
return keys.length ? [`${t.name}: ${keys.join(', ')}`] : []
|
|
48
|
+
})
|
|
49
|
+
expect(gaps).toEqual([])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('every view type publishes showLoading, and an exporting one its init state', async () => {
|
|
53
|
+
const { pluginManager, views } = await setup()
|
|
54
|
+
expect(views.length).toBeGreaterThan(0)
|
|
55
|
+
const gaps = views.flatMap(t => {
|
|
56
|
+
const view = t.stateModel.create({ type: t.name }, { pluginManager })
|
|
57
|
+
const keys = missing(
|
|
58
|
+
view,
|
|
59
|
+
'exportSvg' in view
|
|
60
|
+
? ['showLoading', 'initialized', 'error']
|
|
61
|
+
: ['showLoading'],
|
|
62
|
+
)
|
|
63
|
+
return keys.length ? [`${t.name}: ${keys.join(', ')}`] : []
|
|
64
|
+
})
|
|
65
|
+
expect(gaps).toEqual([])
|
|
66
|
+
})
|
|
@@ -151,7 +151,8 @@ function clippedFeature(
|
|
|
151
151
|
* record the piece was cut from, so a display can tell two runs of one record
|
|
152
152
|
* from two records; `keepAlignment` hands back its
|
|
153
153
|
* own stretch of them as packed ops in `alignmentOps`. A feature with no
|
|
154
|
-
* `mate` is not a pairwise record and
|
|
154
|
+
* `mate` is not a pairwise record, and a `spansWindow` one is a deletion
|
|
155
|
+
* whose length the display reads off its extent; both pass through whole.
|
|
155
156
|
*
|
|
156
157
|
* A copy of `@jbrowse/plugin-comparative-adapters`' own, which a runtime plugin
|
|
157
158
|
* cannot import: the worker serves that package as a stub too.
|
|
@@ -163,7 +164,7 @@ export function clipFeatureToRegion(
|
|
|
163
164
|
keepAlignment = false,
|
|
164
165
|
): Feature[] {
|
|
165
166
|
const mate = mateOf(feature)
|
|
166
|
-
if (mate === undefined) {
|
|
167
|
+
if (mate === undefined || feature.get('spansWindow')) {
|
|
167
168
|
return [feature]
|
|
168
169
|
} else {
|
|
169
170
|
const pieces = clipIntervals(
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '8.3.
|
|
1
|
+
export const version = '8.3.6'
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
import{a as K}from"./chunk-OXU7F4BR.js";import{b as $,d as U,e as X,f as O,g as z,h as G}from"./chunk-WANYNRGU.js";import"./chunk-SYKOZDP6.js";import{a as q}from"./chunk-KOB7YIMR.js";import{p as M,q as _,r as T}from"./chunk-M2D7NUOY.js";import"./chunk-YBLNN5FU.js";import"./chunk-3HS6N7WB.js";import"./chunk-HICKAMKK.js";import{E as D,F as H,i as j}from"./chunk-EENENDGZ.js";import"./chunk-YBLNN5FU.js";import"./chunk-ROZPEXN4.js";import{a as S,c as I,d as W,g as F,h as w,i as x}from"./chunk-L372SDUG.js";import"./chunk-OTBDKJEA.js";import"./chunk-3VGXCEMQ.js";import"./chunk-OGQ6OQDN.js";import"./chunk-3WG3GF2A.js";import{o as R}from"./chunk-J5RMHLFJ.js";var se=50;function re(r){let i=[];for(let e=0;e<r.length;e++){let[t,a]=r[e],[s,n]=r[e+1]??[];a===n&&a<=se&&(t==="I"&&s==="D"||t==="D"&&s==="I")?(i.push(["X",a]),e++):i.push([t,a])}return i}function E(r){return Array.from(r.ids,(i,e)=>2*i+r.rev[e])}function A(r,i){let e=r.get(i);return e===void 0?0:e.end-e.start}function V(r){let i=new Map,e=s=>{let n=A(r,s),o=i.get(n);return o===void 0&&(o="N".repeat(n),i.set(n,o)),o},t=new Map,a=s=>{let n=t.get(s);return n===void 0&&(n=E(s),t.set(s,n)),n};return(s,n)=>_({query:a(s),target:a(n),sequenceOf:e,bases:!1})}var B=100;function v(r,i,e){let t=r.at(-1);e>0&&(t?.[0]===i?t[1]+=e:r.push([i,e]))}function Z(r){let i=t=>A(r,t>>1),e=t=>{let a=new Float64Array(t.length+1);return t.forEach((s,n)=>{a[n+1]=a[n]+i(s)}),a};return(t,a)=>{let s=E(a),n=E(t),[o,l]=M(n,s,i),[u,f]=M(n.map(g=>g^1).reverse(),s,i),c=f>l,p=c?u:o,N=Math.max(l,f);if(N<B)return[];let h=e(s),d=e(n),y=g=>c?n.length-1-p[g][0]:p[g][0],m=[];p.forEach(([,g],k)=>{if(k>0){let[ee,te]=[y(k-1),y(k)].sort((ae,ne)=>ae-ne);v(m,"I",d[te]-d[ee+1]),v(m,"D",h[g]-h[p[k-1][1]+1])}v(m,"=",i(s[g]))});let[b,P]=[y(0),y(p.length-1)].sort((g,k)=>g-k);return[{queryStart:d[b],queryEnd:d[P+1],targetStart:h[p[0][1]],targetEnd:h[p.at(-1)[1]+1],strand:c?"-":"+",edits:m,sharedBases:N}]}}function oe(r){let i=new Map,e=new Set;r.ids.forEach((t,a)=>{i.has(t)?e.add(t):i.set(t,r.rev[a])});for(let t of e)i.delete(t);return i}function J(r){let i=new Map,e=t=>{let a=i.get(t);return a===void 0&&(a=oe(t),i.set(t,a)),a};return(t,a)=>{let s=e(a),n=0,o=0;for(let[l,u]of e(t)){let f=s.get(l);f===u?n+=A(r,l):f!==void 0&&(o+=A(r,l))}return{same:n,opposite:o}}}function Q(r){let i=Z(r),e=V(r),t=J(r);return(a,s)=>{let{same:n,opposite:o}=t(a,s);return n>=B&&o>=B?e(a,s):i(a,s)}}var ie=1e4;function Y(r){let i=Z(r),e=V(r),t=J(r);return(a,s)=>{let n=i(a,s),o=n[0]?.strand,{same:l,opposite:u}=t(a,s);return o===void 0||Math.min(l,u)<ie?n:[...n,...e(a,s).filter(f=>f.strand!==o)]}}function C({chain:r,target:i,query:e,assemblyName:t,refName:a,mateAssemblyName:s}){let n=re(r.edits);return G({target:{name:i.name,assemblyName:t,refName:a,start:i.hapStart+r.targetStart,end:i.hapStart+r.targetEnd},query:{name:e.name,assemblyName:s,refName:W(e.name),start:e.hapStart+r.queryStart,end:e.hapStart+r.queryEnd},strand:r.strand,cigar:T(n),matches:n.reduce((o,[l,u])=>o+(l==="="?u:0),0),columns:n.reduce((o,[,l])=>o+l,0)})}var me=r=>I(r.name)??r.name,L=class extends ${constructor(){super(...arguments);this.walks=new K(D(this,["walksLocation"],["walksIndex"]),this);this.refNames=new H(this.walks.file,this)}get anchor(){let e=this.getConf("assemblyNames")[0];if(e===void 0)throw new Error("WalkTabixSyntenyAdapter needs assemblyNames: its first entry is the assembly the reference sample is loaded as");return e}async getHaplotypeNames(e={}){return this.walks.haplotypeNames(e)}async getHeader(e={}){let{haplotypes:t}=await this.walks.reader.header(e),{anchor:a}=this,s=w(this,a),n=x(this);return{hasCoarseTier:!1,anchorAssemblyName:a,lanes:t.filter(o=>o!==s&&S(o)!==s).map(o=>({name:O(n,o),label:o,group:S(o)}))}}async getRefNames(e={}){let{assemblyName:t}=e;return t===void 0||t===this.anchor?this.refNames.assemblyRefNames({...e,assemblyName:this.anchor}):[]}async getSubgraph(e,t={}){let a=await this.refNames.resolveForCut(e,{signal:t.signal});return this.walks.subgraph(a,e,t)}async laneWalks(e,t,a,s,n){let o=j(a,e);return R(n,s.statusCallback,()=>this.walks.reader.cut(e,t,{...this.walks.budgets(),signal:s.signal,keep:o,expand:o,links:!1}).catch(l=>{throw z(l,t.end-t.start)}))}async anchorFeatures(e,t,a){let s=a.haplotypes?.length?a.haplotypes.map(m=>w(this,m)):void 0,n=w(this,a.targetAssemblyName),o=n===void 0?s:s===void 0||s.some(m=>F(n,m)||F(m,n))?[n]:[];if(o?.length===0)return[];let{start:l,end:u}=t,{graph:f,fragments:c}=await this.laneWalks(e,t,o,a,`Reading walks ${t.refName}:${l.toLocaleString()}-${u.toLocaleString()}`),p=Y(f.nodes),N=x(this),h=c.filter(m=>m.name===e),d=m=>o===void 0?0:o.findIndex(b=>F(m.name,b));return c.filter(m=>m.name!==e&&d(m)>=0).sort((m,b)=>d(m)-d(b)).flatMap(m=>h.flatMap(b=>p(m,b).map(P=>C({chain:P,target:b,query:m,assemblyName:t.assemblyName,refName:t.refName,mateAssemblyName:O(N,me(m))}))))}async pairFeatures(e,t,a,s){let n=a.map(c=>({pair:c,featurePrefix:w(this,c.queryAssemblyName),matePrefix:w(this,c.targetAssemblyName)})),{graph:o,fragments:l}=await this.laneWalks(e,t,n.flatMap(c=>[c.featurePrefix,c.matePrefix]),s,X(a)),u=Q(o.nodes),f=c=>l.filter(p=>F(p.name,c));return n.flatMap(({pair:c,featurePrefix:p,matePrefix:N})=>f(p).flatMap(h=>f(N).flatMap(d=>u(d,h).map(y=>C({chain:y,target:h,query:d,assemblyName:c.queryAssemblyName,refName:W(h.name),mateAssemblyName:c.targetAssemblyName})))))}getFeatures(e,t={}){return q(async a=>{let s=e.assemblyName===this.anchor?await this.refNames.resolve(e,t):void 0;if(s!==void 0){let n=U(t),o=n.length>0?await this.pairFeatures(s,e,n,t):await this.anchorFeatures(s,e,t);for(let l of o)a.next(l)}a.complete()},t.signal)}};export{L as default};
|
|
2
|
-
//# sourceMappingURL=WalkTabixSyntenyAdapter-I3MGWUQ5.js.map
|