jbrowse-plugin-graphgenomeviewer 7.4.4 → 7.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/dist/chunks/{GbzBaseSyntenyAdapter-A5MNPCKX.js → GbzBaseSyntenyAdapter-4KVWVUN6.js} +2 -2
  2. package/dist/chunks/{GbzBaseSyntenyAdapter-A5MNPCKX.js.map → GbzBaseSyntenyAdapter-4KVWVUN6.js.map} +1 -1
  3. package/dist/chunks/{GraphGenomeView-SEAPDRXS.js → GraphGenomeView-KI4FQ5A7.js} +2 -2
  4. package/dist/chunks/GraphTrackSettingsDialog-UQNMDXTE.js +4 -0
  5. package/dist/chunks/GraphTrackSettingsDialog-UQNMDXTE.js.map +7 -0
  6. package/dist/chunks/{HighlightColorDialog-QAYWLO5I.js → HighlightColorDialog-D4RCUCUF.js} +2 -2
  7. package/dist/chunks/LinearGraphDisplay-RPZUZSM2.js +2 -0
  8. package/dist/chunks/{LinearGraphDisplay-ZC4BQ27J.js.map → LinearGraphDisplay-RPZUZSM2.js.map} +2 -2
  9. package/dist/chunks/{MinigraphBubbleAdapter-3QM4XV47.js → MinigraphBubbleAdapter-TJYI7KEN.js} +2 -2
  10. package/dist/chunks/RgfaTabixAdapter-5ZMJLFZP.js +2 -0
  11. package/dist/chunks/RgfaTabixAdapter-5ZMJLFZP.js.map +7 -0
  12. package/dist/chunks/{WalkFormsAdapter-FT2SW4H6.js → WalkFormsAdapter-CAJOWOEX.js} +2 -2
  13. package/dist/chunks/WalkTabixSyntenyAdapter-VI3KA2GX.js +2 -0
  14. package/dist/chunks/{WalkTabixSyntenyAdapter-3WRVNDLB.js.map → WalkTabixSyntenyAdapter-VI3KA2GX.js.map} +2 -2
  15. package/dist/chunks/{chunk-6WUWQX2P.js → chunk-22ANI2KL.js} +2 -2
  16. package/dist/chunks/{chunk-6WUWQX2P.js.map → chunk-22ANI2KL.js.map} +1 -1
  17. package/dist/chunks/chunk-42KD4AVY.js +2 -0
  18. package/dist/chunks/{chunk-HCDKP3IP.js.map → chunk-42KD4AVY.js.map} +4 -4
  19. package/dist/chunks/chunk-4NIWMRKH.js +2 -0
  20. package/dist/chunks/{chunk-S333HOG5.js.map → chunk-4NIWMRKH.js.map} +4 -4
  21. package/dist/chunks/chunk-BCOXRUNQ.js +3 -0
  22. package/dist/chunks/chunk-BCOXRUNQ.js.map +7 -0
  23. package/dist/chunks/chunk-DAPI7M4M.js +4 -0
  24. package/dist/chunks/chunk-DAPI7M4M.js.map +7 -0
  25. package/dist/chunks/{chunk-G53BI7VK.js → chunk-DEWZOJSC.js} +2 -2
  26. package/dist/chunks/{chunk-G53BI7VK.js.map → chunk-DEWZOJSC.js.map} +1 -1
  27. package/dist/chunks/{chunk-6SWXMRAV.js → chunk-FCMXEFJA.js} +1 -1
  28. package/dist/chunks/{chunk-6SWXMRAV.js.map → chunk-FCMXEFJA.js.map} +1 -1
  29. package/dist/chunks/{chunk-P3FBI5ML.js → chunk-GYY6LF5L.js} +1 -1
  30. package/dist/chunks/{chunk-P3FBI5ML.js.map → chunk-GYY6LF5L.js.map} +1 -1
  31. package/dist/chunks/{chunk-HF4H6LQI.js → chunk-HHMJNAA3.js} +2 -2
  32. package/dist/chunks/{chunk-XXBE4IFZ.js → chunk-HTTONSO4.js} +2 -2
  33. package/dist/chunks/{chunk-XXBE4IFZ.js.map → chunk-HTTONSO4.js.map} +1 -1
  34. package/dist/chunks/{chunk-MUONTMYZ.js → chunk-K4JL57NR.js} +2 -2
  35. package/dist/chunks/chunk-KOC23YNN.js +4 -0
  36. package/dist/chunks/chunk-KOC23YNN.js.map +7 -0
  37. package/dist/chunks/{chunk-OJWBLPPH.js → chunk-LTNFVL2F.js} +2 -2
  38. package/dist/chunks/chunk-OZEKEYT3.js +2 -0
  39. package/dist/chunks/{chunk-6VNU7SD4.js.map → chunk-OZEKEYT3.js.map} +4 -4
  40. package/dist/chunks/{chunk-KIYRMZWP.js → chunk-POUV7OPY.js} +2 -2
  41. package/dist/chunks/{chunk-KIYRMZWP.js.map → chunk-POUV7OPY.js.map} +1 -1
  42. package/dist/chunks/{chunk-LE7FZM6Y.js → chunk-SPL3UO2D.js} +2 -2
  43. package/dist/chunks/{chunk-LE7FZM6Y.js.map → chunk-SPL3UO2D.js.map} +1 -1
  44. package/dist/chunks/chunk-UR2MJJUP.js +2 -0
  45. package/dist/chunks/chunk-VTMJQFN5.js +2 -0
  46. package/dist/chunks/chunk-VTMJQFN5.js.map +7 -0
  47. package/dist/chunks/{chunk-2KVE7YAS.js → chunk-XZGYJ3JC.js} +1 -1
  48. package/dist/chunks/{chunk-2KVE7YAS.js.map → chunk-XZGYJ3JC.js.map} +1 -1
  49. package/dist/chunks/{chunk-7RJMG6I2.js → chunk-Z3IKZAYC.js} +1 -1
  50. package/dist/chunks/{chunk-7RJMG6I2.js.map → chunk-Z3IKZAYC.js.map} +1 -1
  51. package/dist/chunks/{chunk-FTXDTU27.js → chunk-ZV5FNOHS.js} +3 -3
  52. package/dist/chunks/{chunk-FTXDTU27.js.map → chunk-ZV5FNOHS.js.map} +2 -2
  53. package/dist/chunks/{renderSvg-ZIUOPWJQ.js → renderSvg-3PPAEMBT.js} +2 -2
  54. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +4 -3
  55. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
  56. package/package.json +2 -2
  57. package/src/GraphComputeLayout.test.ts +24 -0
  58. package/src/GraphComputeLayout.ts +4 -2
  59. package/src/GraphGenomeView/components/GraphSettingsDialog.tsx +16 -0
  60. package/src/GraphGenomeView/components/WalkRowsOverlay.tsx +83 -0
  61. package/src/GraphGenomeView/components/usePaneGestures.test.tsx +82 -5
  62. package/src/GraphGenomeView/components/usePaneGestures.ts +10 -4
  63. package/src/GraphGenomeView/graphLayers.ts +14 -1
  64. package/src/GraphGenomeView/legendSections.ts +32 -0
  65. package/src/GraphGenomeView/model.test.ts +292 -4
  66. package/src/GraphGenomeView/model.ts +3 -10
  67. package/src/GraphGenomeView/nodeSize.test.ts +21 -5
  68. package/src/GraphGenomeView/nodeSize.ts +24 -8
  69. package/src/GraphGenomeView/pane/fitViews.ts +5 -1
  70. package/src/GraphGenomeView/pane/graphViews.ts +96 -16
  71. package/src/GraphGenomeView/pane/launchActions.ts +55 -11
  72. package/src/GraphGenomeView/pane/launchViews.ts +46 -0
  73. package/src/GraphGenomeView/pane/loadActions.ts +85 -25
  74. package/src/GraphGenomeView/pane/morph.test.ts +25 -1
  75. package/src/GraphGenomeView/pane/morph.ts +3 -1
  76. package/src/GraphGenomeView/pane/paneBase.ts +5 -0
  77. package/src/GraphGenomeView/pane/renderingActions.ts +61 -2
  78. package/src/GraphGenomeView/pane/settingActions.ts +62 -4
  79. package/src/GraphGenomeView/pane/walkRowViews.ts +17 -6
  80. package/src/GraphGenomeView/tubeMapPanels.ts +2 -1
  81. package/src/GraphGenomeView/viewModel.ts +4 -6
  82. package/src/GraphGenomeView/walkRowBlocks.ts +50 -0
  83. package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +35 -5
  84. package/src/LinearGraphDisplay/components/GraphStatusChrome.test.tsx +18 -0
  85. package/src/LinearGraphDisplay/components/GraphStatusChrome.tsx +4 -2
  86. package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.test.tsx +41 -1
  87. package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.tsx +33 -19
  88. package/src/LinearGraphDisplay/configSchema.ts +4 -3
  89. package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +2 -9
  90. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +3 -6
  91. package/src/hoverSync/graphViewHighlights.test.ts +53 -0
  92. package/src/hoverSync/graphViewHighlights.ts +25 -0
  93. package/src/hoverSync/lgvHover.test.ts +100 -1
  94. package/src/hoverSync/lgvHover.ts +77 -6
  95. package/src/launchFromGraph/contributors.test.ts +52 -0
  96. package/src/launchFromGraph/contributors.ts +105 -1
  97. package/src/launchFromGraph/launchFromGraph.ts +2 -2
  98. package/src/panSNTabix.test.ts +28 -0
  99. package/src/panSNTabix.ts +26 -8
  100. package/src/version.ts +1 -1
  101. package/dist/chunks/GraphTrackSettingsDialog-EVIIGC5G.js +0 -6
  102. package/dist/chunks/GraphTrackSettingsDialog-EVIIGC5G.js.map +0 -7
  103. package/dist/chunks/LinearGraphDisplay-ZC4BQ27J.js +0 -2
  104. package/dist/chunks/RgfaTabixAdapter-XSOIEWBK.js +0 -2
  105. package/dist/chunks/RgfaTabixAdapter-XSOIEWBK.js.map +0 -7
  106. package/dist/chunks/WalkTabixSyntenyAdapter-3WRVNDLB.js +0 -2
  107. package/dist/chunks/chunk-6VNU7SD4.js +0 -2
  108. package/dist/chunks/chunk-GRTN25B6.js +0 -3
  109. package/dist/chunks/chunk-GRTN25B6.js.map +0 -7
  110. package/dist/chunks/chunk-HCDKP3IP.js +0 -2
  111. package/dist/chunks/chunk-LE43I4PT.js +0 -4
  112. package/dist/chunks/chunk-LE43I4PT.js.map +0 -7
  113. package/dist/chunks/chunk-QV7SSOMJ.js +0 -2
  114. package/dist/chunks/chunk-S333HOG5.js +0 -2
  115. package/dist/chunks/chunk-VZQ6VOUP.js +0 -2
  116. package/dist/chunks/chunk-VZQ6VOUP.js.map +0 -7
  117. package/dist/chunks/chunk-WLRRI35A.js +0 -3
  118. package/dist/chunks/chunk-WLRRI35A.js.map +0 -7
  119. package/src/launchFromGraph/launchTracks.test.ts +0 -95
  120. package/src/launchFromGraph/launchTracks.ts +0 -49
  121. /package/dist/chunks/{GraphGenomeView-SEAPDRXS.js.map → GraphGenomeView-KI4FQ5A7.js.map} +0 -0
  122. /package/dist/chunks/{HighlightColorDialog-QAYWLO5I.js.map → HighlightColorDialog-D4RCUCUF.js.map} +0 -0
  123. /package/dist/chunks/{MinigraphBubbleAdapter-3QM4XV47.js.map → MinigraphBubbleAdapter-TJYI7KEN.js.map} +0 -0
  124. /package/dist/chunks/{WalkFormsAdapter-FT2SW4H6.js.map → WalkFormsAdapter-CAJOWOEX.js.map} +0 -0
  125. /package/dist/chunks/{chunk-HF4H6LQI.js.map → chunk-HHMJNAA3.js.map} +0 -0
  126. /package/dist/chunks/{chunk-MUONTMYZ.js.map → chunk-K4JL57NR.js.map} +0 -0
  127. /package/dist/chunks/{chunk-OJWBLPPH.js.map → chunk-LTNFVL2F.js.map} +0 -0
  128. /package/dist/chunks/{chunk-QV7SSOMJ.js.map → chunk-UR2MJJUP.js.map} +0 -0
  129. /package/dist/chunks/{renderSvg-ZIUOPWJQ.js.map → renderSvg-3PPAEMBT.js.map} +0 -0
@@ -156,6 +156,7 @@ vi.mock('@jbrowse/core/util/io', () => ({
156
156
  }))
157
157
 
158
158
  const SIMPLE_GFA = 'H\tVN:Z:1.0\nS\t1\tACGT\nS\t2\tGGCC\nL\t1\t+\t2\t+\t0M\n'
159
+ const WALKED_GFA = `${SIMPLE_GFA}W\tGRCh38\t0\tchr1\t0\t8\t>1>2\nW\tA\t1\tctg\t0\t8\t>1>2\n`
159
160
 
160
161
  const MOCK_LAYOUT = {
161
162
  nodePositions: {
@@ -218,7 +219,9 @@ function createAnchoredModel(props: { layers?: GraphLayers } = {}) {
218
219
 
219
220
  // A view attached to a tree, so afterAttach installs its reactions. A root
220
221
  // model never attaches.
221
- function attachedModel(props: { layoutMode?: LayoutModeValue } = {}) {
222
+ function attachedModel(
223
+ props: { layoutMode?: LayoutModeValue; [key: string]: unknown } = {},
224
+ ) {
222
225
  return types
223
226
  .model({ view: stateModelFactory() })
224
227
  .create({ view: { type: 'GraphGenomeView', ...props } }).view
@@ -366,6 +369,30 @@ describe('restoring a stated cut', () => {
366
369
  )
367
370
  })
368
371
 
372
+ // Only a GBZ cut follows snarls past the window; a walk file cuts the same
373
+ // graph either way, so the strip's toggle has nothing to fetch.
374
+ test.each([
375
+ ['RgfaTabixAdapter', 0],
376
+ ['GbzBaseSyntenyAdapter', 1],
377
+ ])('the walk strip on a %s track cuts again %i times', async (type, n) => {
378
+ rpcRespond()
379
+ mockSession.tracks = [{ trackId: 'rgfa-track', adapter: { type } }]
380
+ const model = attachedModel({
381
+ layoutMode: 'force',
382
+ loadedTrackId: 'rgfa-track',
383
+ loadedRegion: TEST_REGION,
384
+ })
385
+ await vi.waitFor(() => {
386
+ expect(model.graph).toBeDefined()
387
+ })
388
+ const cuts = () =>
389
+ mockRpcCall.mock.calls.filter(c => c[1] === 'GetSubgraph').length
390
+ const before = cuts()
391
+ model.setWalkStrip(!model.walkStrip)
392
+ await layoutsSettled(model)
393
+ expect(cuts()).toBe(before + n)
394
+ })
395
+
369
396
  test('a stated track the session lacks is reported without a fetch', async () => {
370
397
  mockSession.tracks = []
371
398
  const model = restoredView()
@@ -1136,7 +1163,7 @@ describe('layoutMode', () => {
1136
1163
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
1137
1164
  rpcRespond()
1138
1165
  const model = attachedModel()
1139
- await model.loadGFA(SIMPLE_GFA, 'plain')
1166
+ await model.loadGFA(WALKED_GFA, 'plain')
1140
1167
 
1141
1168
  model.setMaxGraphNodes(1)
1142
1169
  await layoutsSettled(model)
@@ -1434,7 +1461,7 @@ describe('node budget', () => {
1434
1461
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
1435
1462
  rpcRespond()
1436
1463
  const model = withLimit(1)
1437
- await model.loadGFA(SIMPLE_GFA, 'two nodes')
1464
+ await model.loadGFA(WALKED_GFA, 'two nodes')
1438
1465
  expect(String(model.error)).toMatch(/too large to draw/i)
1439
1466
 
1440
1467
  await model.switchLayout('walkrows')
@@ -1443,6 +1470,21 @@ describe('node budget', () => {
1443
1470
  consoleSpy.mockRestore()
1444
1471
  })
1445
1472
 
1473
+ // walk rows hand a graph with no walks to the force engine, which draws
1474
+ // every node
1475
+ test('walk rows of a graph with no walks keep the budget', async () => {
1476
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
1477
+ rpcRespond()
1478
+ const model = withLimit(1)
1479
+ model.setLayoutMode('walkrows')
1480
+ await model.loadGFA(SIMPLE_GFA, 'two nodes')
1481
+ expect(String(model.error)).toMatch(/too large to draw/i)
1482
+ expect(
1483
+ mockRpcCall.mock.calls.filter(c => c[1] === 'GraphComputeLayout'),
1484
+ ).toHaveLength(0)
1485
+ consoleSpy.mockRestore()
1486
+ })
1487
+
1446
1488
  test('a declined graph does not keep the last graph drawn under it', async () => {
1447
1489
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
1448
1490
  rpcRespond()
@@ -1466,7 +1508,7 @@ describe('node budget', () => {
1466
1508
  rpcRespond()
1467
1509
  const model = withLimit(1)
1468
1510
  model.setLayoutMode('walkrows')
1469
- await model.loadGFA(SIMPLE_GFA, 'two nodes')
1511
+ await model.loadGFA(WALKED_GFA, 'two nodes')
1470
1512
  expect(model.error).toBeUndefined()
1471
1513
  expect(model.layoutResult).toBeDefined()
1472
1514
  })
@@ -2279,6 +2321,61 @@ describe('launching out of the graph', () => {
2279
2321
  // Where every contributor is a loaded assembly the graph offers one panel per
2280
2322
  // strain, and a synteny view of all of them — the item is disabled while no
2281
2323
  // synteny track aligns them, rather than absent.
2324
+ // Sakai's allele is drawn bare unless Sakai's own annotation is read onto
2325
+ // it: the reference's genes only reach the backbone
2326
+ test("a loaded strain's own genes are read onto its nodes off the reference", async () => {
2327
+ mockSession.assemblyNames = ['K12', 'Sakai']
2328
+ mockSession.tracks = [
2329
+ {
2330
+ trackId: 'Sakai_genes',
2331
+ name: 'Sakai genes',
2332
+ assemblyNames: ['Sakai'],
2333
+ adapter: { type: 'Gff3TabixAdapter' },
2334
+ },
2335
+ ]
2336
+ const model = await loadedGraph(ECOLI_RGFA, {
2337
+ refName: 'chr',
2338
+ assemblyName: 'K12',
2339
+ start: 1000,
2340
+ end: 1020,
2341
+ })
2342
+ expect(model.ownGeneReads).toEqual([
2343
+ {
2344
+ stableRefName: 'Sakai#1#chr',
2345
+ trackId: 'Sakai_genes',
2346
+ region: {
2347
+ assemblyName: 'Sakai',
2348
+ refName: 'chr',
2349
+ start: 90000,
2350
+ end: 90004,
2351
+ },
2352
+ },
2353
+ ])
2354
+ mockRpcCall.mockImplementation((_sid: unknown, method: string) =>
2355
+ method === 'CoreGetFeatures'
2356
+ ? Promise.resolve([
2357
+ {
2358
+ name: 'stx2A',
2359
+ refName: 'chr',
2360
+ start: 90001,
2361
+ end: 90003,
2362
+ strand: 1,
2363
+ subfeatures: [{ type: 'exon', start: 90001, end: 90003 }],
2364
+ },
2365
+ ])
2366
+ : Promise.reject(new Error(`Unexpected RPC: ${method}`)),
2367
+ )
2368
+ await model.loadOwnGenes()
2369
+ expect(model.ownGeneFeatures).toEqual([
2370
+ expect.objectContaining({
2371
+ name: 'stx2A · Sakai#1',
2372
+ refName: 'Sakai#1#chr',
2373
+ start: 90001,
2374
+ end: 90003,
2375
+ }),
2376
+ ])
2377
+ })
2378
+
2282
2379
  test('loaded strains offer a per-strain linear view and a synteny item', async () => {
2283
2380
  mockSession.assemblyNames = ['K12', 'Sakai']
2284
2381
  const model = await loadedGraph(ECOLI_RGFA, {
@@ -2345,6 +2442,68 @@ describe('launching out of the graph', () => {
2345
2442
  expect(model.connectedViewId).toBe('view-1')
2346
2443
  })
2347
2444
 
2445
+ test('an opened view carries one gene track, not every feature track', async () => {
2446
+ mockSession.assemblyNames = ['hg38']
2447
+ const feature = (trackId: string, name: string, type: string) => ({
2448
+ trackId,
2449
+ name,
2450
+ type: 'FeatureTrack',
2451
+ assemblyNames: ['hg38'],
2452
+ adapter: { type },
2453
+ })
2454
+ mockSession.tracks = [
2455
+ feature('rgfa-track', 'graph', 'RgfaTabixAdapter'),
2456
+ feature('clinvar', 'ClinVar', 'BigBedAdapter'),
2457
+ feature('ncbi', 'NCBI RefSeq genes', 'Gff3TabixAdapter'),
2458
+ feature('gwas', 'GWAS catalog', 'BedTabixAdapter'),
2459
+ ]
2460
+ const model = await loadedGraph(HPRC_RGFA)
2461
+ const { reference } = model.nodeLaunchTargets('1+')
2462
+ model.showInLinearView(reference!)
2463
+
2464
+ expect(mockSession.addedViews).toEqual([
2465
+ [
2466
+ 'LinearGenomeView',
2467
+ expect.objectContaining({
2468
+ init: expect.objectContaining({ tracks: ['rgfa-track', 'ncbi'] }),
2469
+ }),
2470
+ ],
2471
+ ])
2472
+ })
2473
+
2474
+ // a BED named for no annotation is not picked as genes, but beats `No
2475
+ // tracks active`
2476
+ test('a strain with no gene-looking track opens on its first feature track', async () => {
2477
+ mockSession.assemblyNames = ['K12', 'Sakai']
2478
+ mockSession.tracks = [
2479
+ {
2480
+ trackId: 'Sakai_segments',
2481
+ name: 'Sakai segments',
2482
+ type: 'FeatureTrack',
2483
+ assemblyNames: ['Sakai'],
2484
+ adapter: { type: 'BedAdapter' },
2485
+ },
2486
+ ]
2487
+ const model = await loadedGraph(ECOLI_RGFA, {
2488
+ refName: 'chr',
2489
+ assemblyName: 'K12',
2490
+ start: 1000,
2491
+ end: 1020,
2492
+ })
2493
+ model.showInLinearView(model.nodeLaunchTargets('3+').own!)
2494
+ expect(mockSession.addedViews).toEqual([
2495
+ [
2496
+ 'LinearGenomeView',
2497
+ expect.objectContaining({
2498
+ init: expect.objectContaining({
2499
+ assembly: 'Sakai',
2500
+ tracks: ['Sakai_segments'],
2501
+ }),
2502
+ }),
2503
+ ],
2504
+ ])
2505
+ })
2506
+
2348
2507
  // A pairing is not stolen from a view still on screen: a graph launched from
2349
2508
  // a linear view came in on that sync. One whose view has been closed is held
2350
2509
  // by nothing, and kept, the view the graph opens next never got its hover.
@@ -3244,6 +3403,71 @@ describe('walk rows', () => {
3244
3403
  expect(model.walkStripRows).toBeUndefined()
3245
3404
  })
3246
3405
 
3406
+ // node 2 is off GRCh38's path and both A#1 and B#1 walk it
3407
+ test('the block two rows walk off the reference is lettered on both', async () => {
3408
+ rpcRespond()
3409
+ const model = stateModelFactory().create({
3410
+ type: 'GraphGenomeView',
3411
+ layoutMode: 'walkrows',
3412
+ })
3413
+ await model.loadGFA(WALKS_GFA, 'walks')
3414
+ expect(model.sharedBlocks).toBeUndefined()
3415
+ model.setLayer('walkBlocks', true)
3416
+ const rows = [model.walkRowBars!.reference, ...model.walkRowBars!.rows]
3417
+ const lettered = model.sharedBlocks!.map(b => ({
3418
+ label: rows[b.row]!.label,
3419
+ start: b.start,
3420
+ bp: b.bp,
3421
+ block: b.block,
3422
+ }))
3423
+ expect(lettered.sort((a, b) => a.label.localeCompare(b.label))).toEqual([
3424
+ { label: 'A#1', start: 4, bp: 8, block: 0 },
3425
+ { label: 'B#1', start: 4, bp: 8, block: 0 },
3426
+ ])
3427
+ })
3428
+
3429
+ test('the shared order stacks rows that walk the same nodes together', async () => {
3430
+ rpcRespond()
3431
+ const model = stateModelFactory().create({
3432
+ type: 'GraphGenomeView',
3433
+ layoutMode: 'walkrows',
3434
+ })
3435
+ await model.loadGFA(WALKS_GFA, 'walks')
3436
+ model.setWalkRowSamples(['A', 'B'])
3437
+ const before = model.walkRowBars!.rows.map(r => r.label)
3438
+ model.setWalkRowOrder('shared')
3439
+ const labels = model.walkRowBars!.rows.map(r => r.label)
3440
+ expect(labels).not.toEqual(before)
3441
+ // A#1 and B#1 share node 2, so B#1 stacks under A#1 rather than after
3442
+ // A#2, which walks GRCh38's own route
3443
+ expect(labels).toEqual(['A#1', 'B#1', 'A#2'])
3444
+ // the sample filter and the order sit side by side in the grammar
3445
+ expect(model.walkRowSamples).toEqual(['A', 'B'])
3446
+ model.setWalkRowSamples(undefined)
3447
+ expect(model.walkRowOrder).toBe('shared')
3448
+ })
3449
+
3450
+ test('ribbons join neighbouring walk rows where they walk the same nodes', async () => {
3451
+ rpcRespond()
3452
+ const model = stateModelFactory().create({
3453
+ type: 'GraphGenomeView',
3454
+ layoutMode: 'walkrows',
3455
+ })
3456
+ await model.loadGFA(WALKS_GFA, 'walks')
3457
+ expect(model.walkRowRibbons).toBeUndefined()
3458
+ model.setLayer('walkRibbons', true)
3459
+ const ribbons = model.walkRowRibbons!
3460
+ expect(ribbons.length).toBeGreaterThan(0)
3461
+ // every row passes nodes 1 and 3, so each pair of neighbours shares a
3462
+ // stretch that starts at both bars' left ends
3463
+ const rows = [model.walkRowBars!.reference, ...model.walkRowBars!.rows]
3464
+ for (let row = 0; row < rows.length - 1; row++) {
3465
+ expect(ribbons.some(r => r.row === row && r.a0 === 0 && r.b0 === 0)).toBe(
3466
+ true,
3467
+ )
3468
+ }
3469
+ })
3470
+
3247
3471
  test('walk rows and tube maps take no strip', async () => {
3248
3472
  rpcRespond()
3249
3473
  const model = stateModelFactory().create({
@@ -3801,6 +4025,33 @@ describe('walk rows', () => {
3801
4025
  model.setHoveredRowWalks([])
3802
4026
  expect(model.walkLift).toBeUndefined()
3803
4027
  })
4028
+
4029
+ // each lift rebuilds the drawing, and a sweep down a lane crosses many rows
4030
+ test('moving between lane rows lifts the next once the pointer settles', async () => {
4031
+ rpcRespond()
4032
+ const model = stateModelFactory().create({
4033
+ type: 'GraphGenomeView',
4034
+ layoutMode: 'auto',
4035
+ })
4036
+ await model.loadGFA(WALKS_GFA, 'walks')
4037
+ vi.useFakeTimers()
4038
+ try {
4039
+ model.setHoveredRowWalks(['A#1#ctg'])
4040
+ expect(model.hoveredRowWalks).toEqual(['A#1#ctg'])
4041
+ model.setHoveredRowWalks(['A#2#ctg'])
4042
+ model.setHoveredRowWalks(['B#1#ctg'])
4043
+ vi.advanceTimersByTime(40)
4044
+ // more moves within the row do not put the lift off
4045
+ model.setHoveredRowWalks(['B#1#ctg'])
4046
+ expect(model.hoveredRowWalks).toEqual(['A#1#ctg'])
4047
+ vi.advanceTimersByTime(60)
4048
+ expect(model.hoveredRowWalks).toEqual(['B#1#ctg'])
4049
+ model.setHoveredRowWalks([])
4050
+ expect(model.hoveredRowWalks).toEqual([])
4051
+ } finally {
4052
+ vi.useRealTimers()
4053
+ }
4054
+ })
3804
4055
  })
3805
4056
 
3806
4057
  describe('annotation reads beside a cut', () => {
@@ -3900,6 +4151,43 @@ describe('annotation reads beside a cut', () => {
3900
4151
  expect(model.error).toBeUndefined()
3901
4152
  })
3902
4153
 
4154
+ // most graphs have no bubble index, and a pan re-cuts many times
4155
+ test('a bubble index that failed to read is not asked for again', async () => {
4156
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
4157
+ const bubbleReads = () =>
4158
+ mockRpcCall.mock.calls.filter(
4159
+ c =>
4160
+ (c[2] as { adapterConfig?: { type?: string } }).adapterConfig
4161
+ ?.type === 'MinigraphBubbleAdapter',
4162
+ ).length
4163
+ const respond = mockRpcCall.getMockImplementation()!
4164
+ mockRpcCall.mockImplementation(
4165
+ (sid: unknown, method: string, args: Record<string, unknown>) =>
4166
+ (args.adapterConfig as { type?: string } | undefined)?.type ===
4167
+ 'MinigraphBubbleAdapter'
4168
+ ? Promise.reject(new Error('404'))
4169
+ : respond(sid, method, args),
4170
+ )
4171
+ const model = attachedModel()
4172
+ const settleAll = async () => {
4173
+ await vi.waitFor(() => {
4174
+ expect(reads.length).toBeGreaterThanOrEqual(2)
4175
+ })
4176
+ for (const read of reads.splice(0)) {
4177
+ read.settle([])
4178
+ }
4179
+ }
4180
+ let loaded = cut(model)
4181
+ await settleAll()
4182
+ await loaded
4183
+ expect(bubbleReads()).toBe(1)
4184
+ loaded = cut(model)
4185
+ await settleAll()
4186
+ await loaded
4187
+ expect(bubbleReads()).toBe(1)
4188
+ warn.mockRestore()
4189
+ })
4190
+
3903
4191
  test('a cut too large to draw still reads its genes, for when the limit is raised', async () => {
3904
4192
  vi.spyOn(console, 'error').mockImplementation(() => {})
3905
4193
  const model = attachedModel()
@@ -1,5 +1,3 @@
1
- import { cutsWholeWalks as wholeWalksRule } from '@jbrowse/bandage-core/layout/walkStrip'
2
- import { layoutModeByValue } from '@jbrowse/bandage-core/layoutModes'
3
1
  import { getSession } from '@jbrowse/core/util'
4
2
 
5
3
  import {
@@ -46,16 +44,11 @@ export const GraphPane = withLaunchActions
46
44
  canonicalAssemblyName(session, region.assemblyName),
47
45
  )
48
46
  },
49
- // Walk rows, and the strip of them under a node layout, measure whole
50
- // walks, which a GBZ cut only follows when asked, so a layout change
51
- // that changes whether the drawing needs them cuts the track again.
47
+ // A layout change that changes the cut options cuts the track again:
48
+ // whole walks (cutsWholeWalksIn), or the walk-row step budget.
52
49
  cutChangedFrom(from: LayoutModeValue) {
53
- const before = wholeWalksRule(layoutModeByValue(from), {
54
- walkStrip: self.walkStrip,
55
- host: !!self.host,
56
- })
57
50
  return (
58
- self.cutsWholeWalks !== before ||
51
+ self.cutsWholeWalks !== self.cutsWholeWalksIn(from) ||
59
52
  (from === 'walkrows') !== (self.chosenLayoutMode === 'walkrows')
60
53
  )
61
54
  },
@@ -1,14 +1,30 @@
1
- import { nodeSizeOf, sizeOfNodeWidth } from './nodeSize'
1
+ import { nodeSizeOf, sizeOf } from './nodeSize'
2
2
 
3
3
  test('a number is a uniform thickness, an object scales by its field', () => {
4
- expect(nodeSizeOf(9)).toEqual({ width: 'uniform', px: 9 })
4
+ expect(nodeSizeOf(9)).toEqual({ width: 'uniform', px: 9, links: 'uniform' })
5
5
  expect(nodeSizeOf({ field: 'depth', value: 4 })).toEqual({
6
6
  width: 'depth',
7
7
  px: 4,
8
+ links: 'uniform',
9
+ })
10
+ expect(nodeSizeOf(undefined)).toEqual({
11
+ width: 'depth',
12
+ px: 6,
13
+ links: 'uniform',
14
+ })
15
+ expect(nodeSizeOf({ value: 5, links: 'walks' })).toEqual({
16
+ width: 'uniform',
17
+ px: 5,
18
+ links: 'walks',
8
19
  })
9
- expect(nodeSizeOf(undefined)).toEqual({ width: 'depth', px: 6 })
10
20
  })
11
21
 
12
- test.each(['depth', 'uniform'] as const)('%s survives the trip', width => {
13
- expect(nodeSizeOf(sizeOfNodeWidth(width, 5))).toEqual({ width, px: 5 })
22
+ test.each([
23
+ ['depth', 'uniform'],
24
+ ['uniform', 'uniform'],
25
+ ['depth', 'walks'],
26
+ ['uniform', 'walks'],
27
+ ] as const)('nodes %s, links %s survive the trip', (width, links) => {
28
+ const size = { width, px: 5, links }
29
+ expect(nodeSizeOf(sizeOf(size))).toEqual(size)
14
30
  })
@@ -1,25 +1,41 @@
1
1
  import { isRecord } from '../isRecord'
2
2
 
3
+ import type { EdgeWidth } from '@jbrowse/bandage-core/edgeWidths'
3
4
  import type { NodeWidth } from '@jbrowse/bandage-core/nodeWidths'
4
5
 
5
- // The node thickness as a config or session writes it: a px number draws every
6
+ // The mark thickness as a config or session writes it: a px number draws every
6
7
  // node that thick; `{ field: 'depth', value }` thickens a node the more paths
7
- // carry it, around `value` px. Unset is by depth around 6.
8
- export type NodeSize = number | { field?: 'depth'; value?: number }
8
+ // carry it, around `value` px; `links: 'walks'` thickens a link the more walks
9
+ // take it. Unset is nodes by depth around 6, links uniform.
10
+ export type NodeSize =
11
+ number | { field?: 'depth'; value?: number; links?: 'walks' }
9
12
 
10
13
  const DEFAULT_PX = 6
11
14
 
12
- export function nodeSizeOf(size: unknown): { width: NodeWidth; px: number } {
15
+ export interface MarkSize {
16
+ width: NodeWidth
17
+ px: number
18
+ links: EdgeWidth
19
+ }
20
+
21
+ export function nodeSizeOf(size: unknown): MarkSize {
13
22
  return typeof size === 'number'
14
- ? { width: 'uniform', px: size }
23
+ ? { width: 'uniform', px: size, links: 'uniform' }
15
24
  : isRecord(size)
16
25
  ? {
17
26
  width: size.field === 'depth' ? 'depth' : 'uniform',
18
27
  px: typeof size.value === 'number' ? size.value : DEFAULT_PX,
28
+ links: size.links === 'walks' ? 'walks' : 'uniform',
19
29
  }
20
- : { width: 'depth', px: DEFAULT_PX }
30
+ : { width: 'depth', px: DEFAULT_PX, links: 'uniform' }
21
31
  }
22
32
 
23
- export function sizeOfNodeWidth(width: NodeWidth, px: number): NodeSize {
24
- return width === 'depth' ? { field: 'depth', value: px } : px
33
+ export function sizeOf({ width, px, links }: MarkSize): NodeSize {
34
+ return width === 'uniform' && links === 'uniform'
35
+ ? px
36
+ : {
37
+ ...(width === 'depth' ? { field: 'depth' as const } : {}),
38
+ value: px,
39
+ ...(links === 'walks' ? { links } : {}),
40
+ }
25
41
  }
@@ -520,6 +520,7 @@ export const withFitViews = withHostViews
520
520
  connectorThickness: self.connectorThickness,
521
521
  drawPaths,
522
522
  nodeWidth: self.nodeWidth,
523
+ edgeWidth: self.linkWidth,
523
524
  highlight,
524
525
  // Untracked, so a zoom does not eagerly rebuild geometry — the
525
526
  // debounced viewportDirty bump drives the scale-dependent rebuild
@@ -573,13 +574,16 @@ export const withFitViews = withHostViews
573
574
  facet: self.facetSpec,
574
575
  colorScheme: self.effectiveColorScheme,
575
576
  nodeWidth: self.nodeWidth,
577
+ edgeWidth: self.linkWidth,
576
578
  showDeletionEdges: self.showDeletionEdges,
577
579
  linearLayout: self.linearLayout,
578
580
  contigThickness: self.contigThickness,
579
581
  connectorThickness: self.connectorThickness,
580
582
  region: self.graphRegion,
581
583
  colorDomain: self.statedColorDomain,
582
- genes: self.showGenes ? self.backboneGenes : undefined,
584
+ genes: self.showGenes
585
+ ? [...(self.backboneGenes ?? []), ...(self.ownGeneFeatures ?? [])]
586
+ : undefined,
583
587
  walkRows: self.walkRowBars,
584
588
  walkRowPitch: self.walkRowPitch,
585
589
  rowGenes: self.walkRowGenes,