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.
Files changed (35) hide show
  1. package/README.md +2 -2
  2. package/dist/chunks/{GbzBaseSyntenyAdapter-GX3NGJHG.js → GbzBaseSyntenyAdapter-BG6FNGHY.js} +2 -2
  3. package/dist/chunks/{MinigraphBubbleAdapter-XYAUUM35.js → MinigraphBubbleAdapter-P66QLENP.js} +2 -2
  4. package/dist/chunks/{RgfaTabixAdapter-SBIUUIRI.js → RgfaTabixAdapter-CQJROUIP.js} +2 -2
  5. package/dist/chunks/{WalkFormsAdapter-ANELMZU4.js → WalkFormsAdapter-YSYANJD5.js} +2 -2
  6. package/dist/chunks/WalkTabixSyntenyAdapter-7COETKFB.js +2 -0
  7. package/dist/chunks/WalkTabixSyntenyAdapter-7COETKFB.js.map +7 -0
  8. package/dist/chunks/chunk-DAF7EWRF.js +3 -0
  9. package/dist/chunks/{chunk-EENENDGZ.js.map → chunk-DAF7EWRF.js.map} +4 -4
  10. package/dist/chunks/{chunk-WANYNRGU.js → chunk-HLWIFWDZ.js} +3 -3
  11. package/dist/chunks/{chunk-WANYNRGU.js.map → chunk-HLWIFWDZ.js.map} +2 -2
  12. package/dist/chunks/{chunk-OXU7F4BR.js → chunk-KVZIGTH3.js} +2 -2
  13. package/dist/chunks/{chunk-OHQ3WIPI.js → chunk-RCJQCBBI.js} +2 -2
  14. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +2 -2
  15. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +1 -1
  16. package/package.json +2 -2
  17. package/src/RgfaTabixAdapter/test_data/walks5_skipfollow.GRCh38.walks.bed.gz +0 -0
  18. package/src/RgfaTabixAdapter/test_data/walks5_skipfollow.GRCh38.walks.bed.gz.tbi +0 -0
  19. package/src/RgfaTabixAdapter/test_data/walks5_skipinv.GRCh38.walks.bed.gz +0 -0
  20. package/src/RgfaTabixAdapter/test_data/walks5_skipinv.GRCh38.walks.bed.gz.tbi +0 -0
  21. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.test.ts +154 -0
  22. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +148 -28
  23. package/src/WalkTabixSyntenyAdapter/walkLanes.ts +111 -1
  24. package/src/readinessConformance.test.ts +66 -0
  25. package/src/synteny/clipFeatureToRegion.ts +3 -2
  26. package/src/version.ts +1 -1
  27. package/dist/chunks/WalkTabixSyntenyAdapter-I3MGWUQ5.js +0 -2
  28. package/dist/chunks/WalkTabixSyntenyAdapter-I3MGWUQ5.js.map +0 -7
  29. package/dist/chunks/chunk-EENENDGZ.js +0 -3
  30. /package/dist/chunks/{GbzBaseSyntenyAdapter-GX3NGJHG.js.map → GbzBaseSyntenyAdapter-BG6FNGHY.js.map} +0 -0
  31. /package/dist/chunks/{MinigraphBubbleAdapter-XYAUUM35.js.map → MinigraphBubbleAdapter-P66QLENP.js.map} +0 -0
  32. /package/dist/chunks/{RgfaTabixAdapter-SBIUUIRI.js.map → RgfaTabixAdapter-CQJROUIP.js.map} +0 -0
  33. /package/dist/chunks/{WalkFormsAdapter-ANELMZU4.js.map → WalkFormsAdapter-YSYANJD5.js.map} +0 -0
  34. /package/dist/chunks/{chunk-OXU7F4BR.js.map → chunk-KVZIGTH3.js.map} +0 -0
  35. /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.4",
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.0",
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 { anchorAligner, chainFeature, lanePairAligner } from './walkLanes.ts'
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 { WalkFragment } from '@jbrowse/graph-walks'
37
+ import type { WalkSkip } from '@jbrowse/graph-walks'
31
38
 
32
- const haplotypeOf = (fragment: WalkFragment) =>
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 { graph, fragments } = await this.laneWalks(
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
- `Reading walks ${region.refName}:${start.toLocaleString()}-${end.toLocaleString()}`,
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 = (fragment: WalkFragment) =>
266
+ const rank = ({ name }: { name: string }) =>
179
267
  prefixes === undefined
180
268
  ? 0
181
- : prefixes.findIndex(prefix =>
182
- panSNMatchesPrefix(fragment.name, prefix),
183
- )
184
- const shown = fragments
185
- .filter(fragment => fragment.name !== refName && rank(fragment) >= 0)
186
- .sort((a, b) => rank(a) - rank(b))
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
- }),
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 { WalkFragment, WalkNode } from '@jbrowse/graph-walks'
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 passes through whole.
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.4'
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