jbrowse-plugin-graphgenomeviewer 8.3.4 → 8.3.5
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-RRLBONL5.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-4I32E62Y.js +2 -0
- package/dist/chunks/WalkTabixSyntenyAdapter-4I32E62Y.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-HMIJBEUE.js} +3 -3
- package/dist/chunks/{chunk-WANYNRGU.js.map → chunk-HMIJBEUE.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_skippair.GRCh38.walks.bed.gz +0 -0
- package/src/RgfaTabixAdapter/test_data/walks5_skippair.GRCh38.walks.bed.gz.tbi +0 -0
- package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.test.ts +127 -0
- package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +163 -46
- package/src/WalkTabixSyntenyAdapter/walkLanes.ts +88 -1
- package/src/readinessConformance.test.ts +66 -0
- package/src/synteny/matchFeature.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-RRLBONL5.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.5",
|
|
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",
|
|
@@ -247,6 +247,133 @@ test('a window on a haplotype lane answers nothing', async () => {
|
|
|
247
247
|
expect(found).toEqual([])
|
|
248
248
|
})
|
|
249
249
|
|
|
250
|
+
// gfa-to-tabix --walks --chunk 1000 of ten 1 kb reference nodes, whose chunks
|
|
251
|
+
// over 2000-8000 hold skip rows for HG002#1 and HG004#1:
|
|
252
|
+
// W GRCh38 0 chr1 >1>2>3>4>5>6>7>8>9>10
|
|
253
|
+
// W HG002 1 ctg >1>2>9>10 a deletion of 2000-8000
|
|
254
|
+
// W HG003 1 ctg >1>2>3>4>5>6>7>8>9>10
|
|
255
|
+
// W HG004 1 ctg <10<9<2<1 the same deletion, walked backwards
|
|
256
|
+
const skipWindow = { ...window, start: 3100, end: 6000 }
|
|
257
|
+
const skipFeats = (opts: Record<string, unknown> = {}) =>
|
|
258
|
+
firstValueFrom(
|
|
259
|
+
makeAdapter({}, 'walks5_skippair')
|
|
260
|
+
.getFeaturesInMultipleRegions([skipWindow], opts as never)
|
|
261
|
+
.pipe(toArray()),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
test('a haplotype whose deletion spans the window is a lane, its record the skipped stretch with the point it crosses the deletion as its mate', async () => {
|
|
265
|
+
const found = await skipFeats()
|
|
266
|
+
expect(found.map(f => ({ ...summary(f), strand: f.get('strand') }))).toEqual([
|
|
267
|
+
{
|
|
268
|
+
lane: 'HG003#1',
|
|
269
|
+
refName: 'chr1',
|
|
270
|
+
start: 2000,
|
|
271
|
+
end: 7000,
|
|
272
|
+
mate: 'ctg:2000-7000',
|
|
273
|
+
cigar: '5000=',
|
|
274
|
+
strand: 1,
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
lane: 'HG002#1',
|
|
278
|
+
refName: 'chr1',
|
|
279
|
+
start: 2000,
|
|
280
|
+
end: 8000,
|
|
281
|
+
mate: 'ctg:2000-2000',
|
|
282
|
+
cigar: undefined,
|
|
283
|
+
strand: 1,
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
lane: 'HG004#1',
|
|
287
|
+
refName: 'chr1',
|
|
288
|
+
start: 2000,
|
|
289
|
+
end: 8000,
|
|
290
|
+
mate: 'ctg:2000-2000',
|
|
291
|
+
cigar: undefined,
|
|
292
|
+
strand: -1,
|
|
293
|
+
},
|
|
294
|
+
])
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
test("the display's clip maps the whole window onto a skipping haplotype's crossing", async () => {
|
|
298
|
+
const found = await skipFeats({
|
|
299
|
+
haplotypes: ['HG002'],
|
|
300
|
+
clipToRegion: true,
|
|
301
|
+
splitAtGapBp: 10_000,
|
|
302
|
+
})
|
|
303
|
+
expect(found.map(summary)).toEqual([
|
|
304
|
+
{
|
|
305
|
+
lane: 'HG002#1',
|
|
306
|
+
refName: 'chr1',
|
|
307
|
+
start: 3100,
|
|
308
|
+
end: 6000,
|
|
309
|
+
mate: 'ctg:2000-2000',
|
|
310
|
+
cigar: undefined,
|
|
311
|
+
},
|
|
312
|
+
])
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
// RgfaTabixAdapter/walkFiles.test.ts's walks5_skip: HG002#1 >1>2>9>10 over
|
|
316
|
+
// ten 10 bp reference nodes in 10 bp chunks
|
|
317
|
+
test('a named haplotype skipping the window opens its lane at the crossing', async () => {
|
|
318
|
+
const found = await firstValueFrom(
|
|
319
|
+
makeAdapter({}, 'walks5_skip')
|
|
320
|
+
.getFeatures(
|
|
321
|
+
{ ...window, start: 31, end: 60 },
|
|
322
|
+
{ haplotypes: ['HG002#1'] },
|
|
323
|
+
)
|
|
324
|
+
.pipe(toArray()),
|
|
325
|
+
)
|
|
326
|
+
expect(found.map(summary)).toEqual([
|
|
327
|
+
{
|
|
328
|
+
lane: 'HG002#1',
|
|
329
|
+
refName: 'chr1',
|
|
330
|
+
start: 20,
|
|
331
|
+
end: 80,
|
|
332
|
+
mate: 'ctg:20-20',
|
|
333
|
+
cigar: undefined,
|
|
334
|
+
},
|
|
335
|
+
])
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
test('a skipping haplotype outside the lanes asked for opens no lane', async () => {
|
|
339
|
+
const found = await skipFeats({ haplotypes: ['HG003'] })
|
|
340
|
+
expect(found.map(f => mateOf(f).assemblyName)).toEqual(['HG003#1'])
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
test('a lane pair with one lane skipping the window joins the other lane to the crossing, either way round', async () => {
|
|
344
|
+
const found = await skipFeats({
|
|
345
|
+
lanePairs: [
|
|
346
|
+
{ queryAssemblyName: 'HG003#1', targetAssemblyName: 'HG002#1' },
|
|
347
|
+
{ queryAssemblyName: 'HG002#1', targetAssemblyName: 'HG003#1' },
|
|
348
|
+
{ queryAssemblyName: 'HG002#1', targetAssemblyName: 'HG004#1' },
|
|
349
|
+
],
|
|
350
|
+
clipToRegion: true,
|
|
351
|
+
splitAtGapBp: 10_000,
|
|
352
|
+
})
|
|
353
|
+
expect(
|
|
354
|
+
found.map(f => ({ assemblyName: f.get('assemblyName'), ...summary(f) })),
|
|
355
|
+
).toEqual([
|
|
356
|
+
{
|
|
357
|
+
assemblyName: 'HG003#1',
|
|
358
|
+
lane: 'HG002#1',
|
|
359
|
+
refName: 'ctg',
|
|
360
|
+
start: 2000,
|
|
361
|
+
end: 7000,
|
|
362
|
+
mate: 'ctg:2000-2000',
|
|
363
|
+
cigar: undefined,
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
assemblyName: 'HG002#1',
|
|
367
|
+
lane: 'HG003#1',
|
|
368
|
+
refName: 'ctg',
|
|
369
|
+
start: 2000,
|
|
370
|
+
end: 2000,
|
|
371
|
+
mate: 'ctg:2000-7000',
|
|
372
|
+
cigar: undefined,
|
|
373
|
+
},
|
|
374
|
+
])
|
|
375
|
+
})
|
|
376
|
+
|
|
250
377
|
test('clipToRegion cuts a record to the window on both axes', async () => {
|
|
251
378
|
const found = await firstValueFrom(
|
|
252
379
|
makeAdapter()
|
|
@@ -10,7 +10,15 @@ 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
|
+
crossingFeature,
|
|
17
|
+
crossingSide,
|
|
18
|
+
lanePairAligner,
|
|
19
|
+
skipCrossing,
|
|
20
|
+
walkSide,
|
|
21
|
+
} from './walkLanes.ts'
|
|
14
22
|
import { WalkIndex } from '../RgfaTabixAdapter/walkReader.ts'
|
|
15
23
|
import { PanSNRefNames, openTabixSlot } from '../panSNTabix.ts'
|
|
16
24
|
import { ComparativeAdapterBase } from '../synteny/ComparativeAdapterBase.ts'
|
|
@@ -24,13 +32,13 @@ import { laneZoomNotice } from '../synteny/laneZoomNotice.ts'
|
|
|
24
32
|
import type { WalkTabixSyntenyAdapterConfig } from './configSchema.ts'
|
|
25
33
|
import type { SubgraphAdapterOptions } from '../GetSubgraph.ts'
|
|
26
34
|
import type { LaneFeatureOptions, LanePair } from '../synteny/lanePairs.ts'
|
|
35
|
+
import type { MatchSide } from '../synteny/matchFeature.ts'
|
|
27
36
|
import type { BaseOptions } from '@jbrowse/core/data_adapters/BaseAdapter'
|
|
28
37
|
import type { Feature } from '@jbrowse/core/util'
|
|
29
38
|
import type { Region } from '@jbrowse/core/util/types'
|
|
30
|
-
import type {
|
|
39
|
+
import type { WalkSkip } from '@jbrowse/graph-walks'
|
|
31
40
|
|
|
32
|
-
const haplotypeOf = (
|
|
33
|
-
panSNHaplotype(fragment.name) ?? fragment.name
|
|
41
|
+
const haplotypeOf = ({ name }: { name: string }) => panSNHaplotype(name) ?? name
|
|
34
42
|
|
|
35
43
|
export default class WalkTabixSyntenyAdapter extends ComparativeAdapterBase<WalkTabixSyntenyAdapterConfig> {
|
|
36
44
|
private readonly walks: WalkIndex = new WalkIndex(
|
|
@@ -135,9 +143,62 @@ export default class WalkTabixSyntenyAdapter extends ComparativeAdapterBase<Walk
|
|
|
135
143
|
)
|
|
136
144
|
}
|
|
137
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Where each haplotype that skips the window crosses its deletion, read
|
|
148
|
+
* from cuts over the reference nodes they leave at, one cut per chunk those
|
|
149
|
+
* nodes fall in. A haplotype with a walk in the window draws that instead:
|
|
150
|
+
* NA19036#1 skips the IGL locus on one contig and walks it on another.
|
|
151
|
+
*/
|
|
152
|
+
private async skipCrossings(
|
|
153
|
+
refName: string,
|
|
154
|
+
{ skips, fragments }: { skips: WalkSkip[]; fragments: { name: string }[] },
|
|
155
|
+
opts: LaneFeatureOptions,
|
|
156
|
+
) {
|
|
157
|
+
const { signal } = opts
|
|
158
|
+
const walked = new Set(fragments.map(haplotypeOf))
|
|
159
|
+
const crossing = skips.filter(skip => !walked.has(haplotypeOf(skip)))
|
|
160
|
+
const { chunk } = await this.walks.reader.header({ signal })
|
|
161
|
+
const byChunk = new Map<
|
|
162
|
+
number,
|
|
163
|
+
{ group: WalkSkip[]; start: number; end: number }
|
|
164
|
+
>()
|
|
165
|
+
for (const skip of crossing) {
|
|
166
|
+
const cs = Math.floor((skip.refStart - 1) / chunk)
|
|
167
|
+
const held = byChunk.get(cs)
|
|
168
|
+
if (held) {
|
|
169
|
+
held.group.push(skip)
|
|
170
|
+
held.start = Math.min(held.start, skip.refStart - 1)
|
|
171
|
+
held.end = Math.max(held.end, skip.refStart)
|
|
172
|
+
} else {
|
|
173
|
+
byChunk.set(cs, {
|
|
174
|
+
group: [skip],
|
|
175
|
+
start: skip.refStart - 1,
|
|
176
|
+
end: skip.refStart,
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const found = await Promise.all(
|
|
181
|
+
[...byChunk.values()].map(async ({ group, start, end }) => {
|
|
182
|
+
const names = new Set(group.map(skip => skip.name))
|
|
183
|
+
const keep = (name: string) => names.has(name)
|
|
184
|
+
const { graph, fragments } = await this.walks.reader.cut(
|
|
185
|
+
refName,
|
|
186
|
+
{ start, end },
|
|
187
|
+
{ signal, keep, expand: keep, context: 0, links: false },
|
|
188
|
+
)
|
|
189
|
+
return group.map(skip =>
|
|
190
|
+
skipCrossing(skip, refName, fragments, graph.nodes),
|
|
191
|
+
)
|
|
192
|
+
}),
|
|
193
|
+
)
|
|
194
|
+
return found.flat().filter(crossing => crossing !== undefined)
|
|
195
|
+
}
|
|
196
|
+
|
|
138
197
|
/**
|
|
139
198
|
* One record per stretch a haplotype's walk shares with the reference's,
|
|
140
|
-
* on the reference with the haplotype as its mate
|
|
199
|
+
* on the reference with the haplotype as its mate. A haplotype whose
|
|
200
|
+
* deletion spans the window is one record of the skipped stretch, its mate
|
|
201
|
+
* the point where the haplotype crosses the deletion.
|
|
141
202
|
*/
|
|
142
203
|
private async anchorFeatures(
|
|
143
204
|
refName: string,
|
|
@@ -163,48 +224,79 @@ export default class WalkTabixSyntenyAdapter extends ComparativeAdapterBase<Walk
|
|
|
163
224
|
return []
|
|
164
225
|
}
|
|
165
226
|
const { start, end } = region
|
|
166
|
-
const
|
|
227
|
+
const status = `Reading walks ${region.refName}:${start.toLocaleString()}-${end.toLocaleString()}`
|
|
228
|
+
const { graph, fragments, skips } = await this.laneWalks(
|
|
167
229
|
refName,
|
|
168
230
|
region,
|
|
169
231
|
prefixes,
|
|
170
232
|
opts,
|
|
171
|
-
|
|
233
|
+
status,
|
|
172
234
|
)
|
|
173
|
-
const align = anchorAligner(graph.nodes)
|
|
174
|
-
const asmByPrefix = assemblyByPanSNPrefix(this)
|
|
175
|
-
const references = fragments.filter(f => f.name === refName)
|
|
176
235
|
// lanes come back in the order the fetch names them, which is the order
|
|
177
236
|
// the display stacks lanes of equal weight in
|
|
178
|
-
const rank = (
|
|
237
|
+
const rank = ({ name }: { name: string }) =>
|
|
179
238
|
prefixes === undefined
|
|
180
239
|
? 0
|
|
181
|
-
: prefixes.findIndex(prefix =>
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return shown.flatMap(query =>
|
|
188
|
-
references.flatMap(reference =>
|
|
189
|
-
align(query, reference).map(chain =>
|
|
190
|
-
chainFeature({
|
|
191
|
-
chain,
|
|
192
|
-
target: reference,
|
|
193
|
-
query,
|
|
194
|
-
assemblyName: region.assemblyName,
|
|
195
|
-
refName: region.refName,
|
|
196
|
-
mateAssemblyName: laneAssemblyName(asmByPrefix, haplotypeOf(query)),
|
|
197
|
-
}),
|
|
198
|
-
),
|
|
240
|
+
: prefixes.findIndex(prefix => panSNMatchesPrefix(name, prefix))
|
|
241
|
+
const crossings = await updateStatus(status, opts.statusCallback, () =>
|
|
242
|
+
this.skipCrossings(
|
|
243
|
+
refName,
|
|
244
|
+
{ skips: skips.filter(skip => rank(skip) >= 0), fragments },
|
|
245
|
+
opts,
|
|
199
246
|
),
|
|
200
247
|
)
|
|
248
|
+
const align = anchorAligner(graph.nodes)
|
|
249
|
+
const asmByPrefix = assemblyByPanSNPrefix(this)
|
|
250
|
+
const laneOf = (walk: { name: string }) =>
|
|
251
|
+
laneAssemblyName(asmByPrefix, haplotypeOf(walk))
|
|
252
|
+
const references = fragments.filter(f => f.name === refName)
|
|
253
|
+
const walked = fragments
|
|
254
|
+
.filter(fragment => fragment.name !== refName && rank(fragment) >= 0)
|
|
255
|
+
.map(query => ({
|
|
256
|
+
rank: rank(query),
|
|
257
|
+
features: references.flatMap(reference =>
|
|
258
|
+
align(query, reference).map(chain =>
|
|
259
|
+
chainFeature({
|
|
260
|
+
chain,
|
|
261
|
+
target: reference,
|
|
262
|
+
query,
|
|
263
|
+
assemblyName: region.assemblyName,
|
|
264
|
+
refName: region.refName,
|
|
265
|
+
mateAssemblyName: laneOf(query),
|
|
266
|
+
}),
|
|
267
|
+
),
|
|
268
|
+
),
|
|
269
|
+
}))
|
|
270
|
+
const skipped = crossings.map(crossing => {
|
|
271
|
+
const { skip } = crossing
|
|
272
|
+
return {
|
|
273
|
+
rank: rank(skip),
|
|
274
|
+
features: [
|
|
275
|
+
crossingFeature({
|
|
276
|
+
target: {
|
|
277
|
+
name: refName,
|
|
278
|
+
assemblyName: region.assemblyName,
|
|
279
|
+
refName: region.refName,
|
|
280
|
+
start: skip.refStart,
|
|
281
|
+
end: skip.refEnd,
|
|
282
|
+
},
|
|
283
|
+
query: crossingSide(crossing, laneOf(skip)),
|
|
284
|
+
strand: crossing.strand,
|
|
285
|
+
}),
|
|
286
|
+
],
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
return [...walked, ...skipped]
|
|
290
|
+
.sort((a, b) => a.rank - b.rank)
|
|
291
|
+
.flatMap(lane => lane.features)
|
|
201
292
|
}
|
|
202
293
|
|
|
203
294
|
/**
|
|
204
295
|
* Each pair's two lanes read out of the anchor window, all pairs in one
|
|
205
296
|
* read, and each walk of a pair's query lane aligned to each walk of its
|
|
206
297
|
* target lane. A pair's records sit on its query lane's contigs, the lane
|
|
207
|
-
* the display draws on top.
|
|
298
|
+
* the display draws on top. A lane that skips the window pairs as the
|
|
299
|
+
* point where it crosses its deletion.
|
|
208
300
|
*/
|
|
209
301
|
private async pairFeatures(
|
|
210
302
|
refName: string,
|
|
@@ -217,32 +309,57 @@ export default class WalkTabixSyntenyAdapter extends ComparativeAdapterBase<Walk
|
|
|
217
309
|
featurePrefix: resolvePanSNPrefix(this, pair.queryAssemblyName),
|
|
218
310
|
matePrefix: resolvePanSNPrefix(this, pair.targetAssemblyName),
|
|
219
311
|
}))
|
|
220
|
-
const
|
|
312
|
+
const status = lanePairsStatus(pairs)
|
|
313
|
+
const { graph, fragments, skips } = await this.laneWalks(
|
|
221
314
|
refName,
|
|
222
315
|
region,
|
|
223
316
|
sides.flatMap(side => [side.featurePrefix, side.matePrefix]),
|
|
224
317
|
opts,
|
|
225
|
-
|
|
318
|
+
status,
|
|
319
|
+
)
|
|
320
|
+
const crossings = await updateStatus(status, opts.statusCallback, () =>
|
|
321
|
+
this.skipCrossings(refName, { skips, fragments }, opts),
|
|
226
322
|
)
|
|
227
323
|
const align = lanePairAligner(graph.nodes)
|
|
228
324
|
const walksOf = (prefix: string) =>
|
|
229
325
|
fragments.filter(f => panSNMatchesPrefix(f.name, prefix))
|
|
230
|
-
|
|
231
|
-
walksOf(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
326
|
+
const walkSides = (prefix: string, assemblyName: string) =>
|
|
327
|
+
walksOf(prefix).map(walk => walkSide(walk, assemblyName))
|
|
328
|
+
const crossingSides = (prefix: string, assemblyName: string) =>
|
|
329
|
+
crossings
|
|
330
|
+
.filter(({ skip }) => panSNMatchesPrefix(skip.name, prefix))
|
|
331
|
+
.map(crossing => crossingSide(crossing, assemblyName))
|
|
332
|
+
const crossed = (own: MatchSide[], mates: MatchSide[]) =>
|
|
333
|
+
own.flatMap(target =>
|
|
334
|
+
mates.map(query => crossingFeature({ target, query, strand: '+' })),
|
|
335
|
+
)
|
|
336
|
+
return sides.flatMap(({ pair, featurePrefix, matePrefix }) => {
|
|
337
|
+
const { queryAssemblyName, targetAssemblyName } = pair
|
|
338
|
+
return [
|
|
339
|
+
...walksOf(featurePrefix).flatMap(target =>
|
|
340
|
+
walksOf(matePrefix).flatMap(query =>
|
|
341
|
+
align(query, target).map(chain =>
|
|
342
|
+
chainFeature({
|
|
343
|
+
chain,
|
|
344
|
+
target,
|
|
345
|
+
query,
|
|
346
|
+
assemblyName: queryAssemblyName,
|
|
347
|
+
refName: panSNContig(target.name),
|
|
348
|
+
mateAssemblyName: targetAssemblyName,
|
|
349
|
+
}),
|
|
350
|
+
),
|
|
242
351
|
),
|
|
243
352
|
),
|
|
244
|
-
|
|
245
|
-
|
|
353
|
+
...crossed(
|
|
354
|
+
walkSides(featurePrefix, queryAssemblyName),
|
|
355
|
+
crossingSides(matePrefix, targetAssemblyName),
|
|
356
|
+
),
|
|
357
|
+
...crossed(
|
|
358
|
+
crossingSides(featurePrefix, queryAssemblyName),
|
|
359
|
+
walkSides(matePrefix, targetAssemblyName),
|
|
360
|
+
),
|
|
361
|
+
]
|
|
362
|
+
})
|
|
246
363
|
}
|
|
247
364
|
|
|
248
365
|
// The files are filed under the reference alone, so a window on a
|
|
@@ -4,8 +4,9 @@ import { panSNContig } from '@jbrowse/bandage-core/pansn'
|
|
|
4
4
|
|
|
5
5
|
import { matchFeature } from '../synteny/matchFeature.ts'
|
|
6
6
|
|
|
7
|
+
import type { MatchSide } from '../synteny/matchFeature.ts'
|
|
7
8
|
import type { PairChain, PairEdit } from '@gmod/gbz-base/pairAlignment'
|
|
8
|
-
import type { WalkFragment, WalkNode } from '@jbrowse/graph-walks'
|
|
9
|
+
import type { WalkFragment, WalkNode, WalkSkip } from '@jbrowse/graph-walks'
|
|
9
10
|
|
|
10
11
|
// The longest bubble with equal bases on both sides that is written as
|
|
11
12
|
// mismatches; a longer one stays an insertion and a deletion
|
|
@@ -294,3 +295,89 @@ export function chainFeature({
|
|
|
294
295
|
columns: edits.reduce((sum, [, n]) => sum + n, 0),
|
|
295
296
|
})
|
|
296
297
|
}
|
|
298
|
+
|
|
299
|
+
/** The point on a haplotype's contig where it crosses a skipped stretch */
|
|
300
|
+
export interface SkipCrossing {
|
|
301
|
+
skip: WalkSkip
|
|
302
|
+
at: number
|
|
303
|
+
strand: '+' | '-'
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Where a haplotype that skips the window crosses its deletion, from a cut
|
|
308
|
+
* over the reference node it leaves at `skip.refStart`: past that node, or
|
|
309
|
+
* where it enters the node when it passes it backwards, arriving across the
|
|
310
|
+
* deletion
|
|
311
|
+
*/
|
|
312
|
+
export function skipCrossing(
|
|
313
|
+
skip: WalkSkip,
|
|
314
|
+
refName: string,
|
|
315
|
+
fragments: WalkFragment[],
|
|
316
|
+
nodes: Map<number, WalkNode>,
|
|
317
|
+
): SkipCrossing | undefined {
|
|
318
|
+
for (const { name, fragStart, hapStart, ids, rev } of fragments) {
|
|
319
|
+
if (name === skip.name && fragStart === skip.fragStart) {
|
|
320
|
+
let at = hapStart
|
|
321
|
+
for (let i = 0; i < ids.length; i++) {
|
|
322
|
+
const node = nodes.get(ids[i]!)
|
|
323
|
+
const length = node === undefined ? 0 : node.end - node.start
|
|
324
|
+
if (
|
|
325
|
+
node?.rank === 0 &&
|
|
326
|
+
node.refName === refName &&
|
|
327
|
+
node.end === skip.refStart
|
|
328
|
+
) {
|
|
329
|
+
return rev[i]
|
|
330
|
+
? { skip, at, strand: '-' }
|
|
331
|
+
: { skip, at: at + length, strand: '+' }
|
|
332
|
+
}
|
|
333
|
+
at += length
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return undefined
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** One side of a record: a walk fragment's extent on its contig */
|
|
341
|
+
export function walkSide(fragment: WalkFragment, assemblyName: string) {
|
|
342
|
+
return {
|
|
343
|
+
name: fragment.name,
|
|
344
|
+
assemblyName,
|
|
345
|
+
refName: panSNContig(fragment.name),
|
|
346
|
+
start: fragment.hapStart,
|
|
347
|
+
end: fragment.hapEnd,
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** One side of a record: the point where a haplotype crosses its deletion */
|
|
352
|
+
export function crossingSide({ skip, at }: SkipCrossing, assemblyName: string) {
|
|
353
|
+
return {
|
|
354
|
+
name: skip.name,
|
|
355
|
+
assemblyName,
|
|
356
|
+
refName: panSNContig(skip.name),
|
|
357
|
+
start: at,
|
|
358
|
+
end: at,
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* A record with one side a haplotype's deletion crossing. With no CIGAR, a
|
|
364
|
+
* display clipping it to a window maps the window onto that point, so the
|
|
365
|
+
* haplotype gets a lane that the window's ribbon narrows to.
|
|
366
|
+
*/
|
|
367
|
+
export function crossingFeature({
|
|
368
|
+
target,
|
|
369
|
+
query,
|
|
370
|
+
strand,
|
|
371
|
+
}: {
|
|
372
|
+
target: MatchSide
|
|
373
|
+
query: MatchSide
|
|
374
|
+
strand: string
|
|
375
|
+
}) {
|
|
376
|
+
return matchFeature({
|
|
377
|
+
target,
|
|
378
|
+
query,
|
|
379
|
+
strand,
|
|
380
|
+
matches: 0,
|
|
381
|
+
columns: Math.max(target.end - target.start, query.end - query.start),
|
|
382
|
+
})
|
|
383
|
+
}
|
|
@@ -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
|
+
})
|
|
@@ -12,6 +12,7 @@ export interface MatchSide {
|
|
|
12
12
|
* One record of a walk aligned to another, on the target walk's contig with
|
|
13
13
|
* the query walk as its mate and the CIGAR read along the target, a `D` being
|
|
14
14
|
* target bases the query lacks. Each side's walk name and extent make the id.
|
|
15
|
+
* A record without a CIGAR maps its two extents onto each other in proportion.
|
|
15
16
|
*/
|
|
16
17
|
export function matchFeature({
|
|
17
18
|
target,
|
|
@@ -24,7 +25,7 @@ export function matchFeature({
|
|
|
24
25
|
target: MatchSide
|
|
25
26
|
query: MatchSide
|
|
26
27
|
strand: string
|
|
27
|
-
cigar
|
|
28
|
+
cigar?: string
|
|
28
29
|
matches: number
|
|
29
30
|
columns: number
|
|
30
31
|
}) {
|
|
@@ -37,7 +38,7 @@ export function matchFeature({
|
|
|
37
38
|
end: target.end,
|
|
38
39
|
type: 'match',
|
|
39
40
|
strand: strand === '-' ? -1 : 1,
|
|
40
|
-
CIGAR: cigar,
|
|
41
|
+
...(cigar === undefined ? {} : { CIGAR: cigar }),
|
|
41
42
|
syntenyId: id,
|
|
42
43
|
identity: matches / Math.max(columns, 1),
|
|
43
44
|
numMatches: matches,
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '8.3.
|
|
1
|
+
export const version = '8.3.5'
|
|
@@ -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
|