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.
Files changed (33) hide show
  1. package/README.md +2 -2
  2. package/dist/chunks/{GbzBaseSyntenyAdapter-GX3NGJHG.js → GbzBaseSyntenyAdapter-RRLBONL5.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-4I32E62Y.js +2 -0
  7. package/dist/chunks/WalkTabixSyntenyAdapter-4I32E62Y.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-HMIJBEUE.js} +3 -3
  11. package/dist/chunks/{chunk-WANYNRGU.js.map → chunk-HMIJBEUE.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_skippair.GRCh38.walks.bed.gz +0 -0
  18. package/src/RgfaTabixAdapter/test_data/walks5_skippair.GRCh38.walks.bed.gz.tbi +0 -0
  19. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.test.ts +127 -0
  20. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +163 -46
  21. package/src/WalkTabixSyntenyAdapter/walkLanes.ts +88 -1
  22. package/src/readinessConformance.test.ts +66 -0
  23. package/src/synteny/matchFeature.ts +3 -2
  24. package/src/version.ts +1 -1
  25. package/dist/chunks/WalkTabixSyntenyAdapter-I3MGWUQ5.js +0 -2
  26. package/dist/chunks/WalkTabixSyntenyAdapter-I3MGWUQ5.js.map +0 -7
  27. package/dist/chunks/chunk-EENENDGZ.js +0 -3
  28. /package/dist/chunks/{GbzBaseSyntenyAdapter-GX3NGJHG.js.map → GbzBaseSyntenyAdapter-RRLBONL5.js.map} +0 -0
  29. /package/dist/chunks/{MinigraphBubbleAdapter-XYAUUM35.js.map → MinigraphBubbleAdapter-P66QLENP.js.map} +0 -0
  30. /package/dist/chunks/{RgfaTabixAdapter-SBIUUIRI.js.map → RgfaTabixAdapter-CQJROUIP.js.map} +0 -0
  31. /package/dist/chunks/{WalkFormsAdapter-ANELMZU4.js.map → WalkFormsAdapter-YSYANJD5.js.map} +0 -0
  32. /package/dist/chunks/{chunk-OXU7F4BR.js.map → chunk-KVZIGTH3.js.map} +0 -0
  33. /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.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.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",
@@ -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 { anchorAligner, chainFeature, lanePairAligner } from './walkLanes.ts'
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 { WalkFragment } from '@jbrowse/graph-walks'
39
+ import type { WalkSkip } from '@jbrowse/graph-walks'
31
40
 
32
- const haplotypeOf = (fragment: WalkFragment) =>
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 { graph, fragments } = await this.laneWalks(
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
- `Reading walks ${region.refName}:${start.toLocaleString()}-${end.toLocaleString()}`,
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 = (fragment: WalkFragment) =>
237
+ const rank = ({ name }: { name: string }) =>
179
238
  prefixes === undefined
180
239
  ? 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
- }),
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 { graph, fragments } = await this.laneWalks(
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
- lanePairsStatus(pairs),
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
- return sides.flatMap(({ pair, featurePrefix, matePrefix }) =>
231
- walksOf(featurePrefix).flatMap(target =>
232
- walksOf(matePrefix).flatMap(query =>
233
- align(query, target).map(chain =>
234
- chainFeature({
235
- chain,
236
- target,
237
- query,
238
- assemblyName: pair.queryAssemblyName,
239
- refName: panSNContig(target.name),
240
- mateAssemblyName: pair.targetAssemblyName,
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: string
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.4'
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