jbrowse-plugin-graphgenomeviewer 7.4.3 → 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 (170) hide show
  1. package/README.md +10 -7
  2. package/dist/chunks/{AddTrackWorkflow-LK25EC3H.js → AddTrackWorkflow-NLSZYK56.js} +2 -2
  3. package/dist/chunks/{GbzBaseSyntenyAdapter-EKLUEECW.js → GbzBaseSyntenyAdapter-4KVWVUN6.js} +2 -2
  4. package/dist/chunks/{GbzBaseSyntenyAdapter-EKLUEECW.js.map → GbzBaseSyntenyAdapter-4KVWVUN6.js.map} +1 -1
  5. package/dist/chunks/{GraphGenomeView-P3XMSKUQ.js → GraphGenomeView-KI4FQ5A7.js} +2 -2
  6. package/dist/chunks/GraphTrackSettingsDialog-UQNMDXTE.js +4 -0
  7. package/dist/chunks/GraphTrackSettingsDialog-UQNMDXTE.js.map +7 -0
  8. package/dist/chunks/{HighlightColorDialog-G6AFRYUD.js → HighlightColorDialog-D4RCUCUF.js} +2 -2
  9. package/dist/chunks/LinearGraphDisplay-RPZUZSM2.js +2 -0
  10. package/dist/chunks/{LinearGraphDisplay-O4SQSXJJ.js.map → LinearGraphDisplay-RPZUZSM2.js.map} +2 -2
  11. package/dist/chunks/MinigraphBubbleAdapter-TJYI7KEN.js +2 -0
  12. package/dist/chunks/{MinigraphBubbleAdapter-3NJ2VLN2.js.map → MinigraphBubbleAdapter-TJYI7KEN.js.map} +1 -1
  13. package/dist/chunks/RgfaTabixAdapter-5ZMJLFZP.js +2 -0
  14. package/dist/chunks/RgfaTabixAdapter-5ZMJLFZP.js.map +7 -0
  15. package/dist/chunks/WalkFormsAdapter-CAJOWOEX.js +2 -0
  16. package/dist/chunks/WalkFormsAdapter-CAJOWOEX.js.map +7 -0
  17. package/dist/chunks/WalkTabixSyntenyAdapter-VI3KA2GX.js +2 -0
  18. package/dist/chunks/{WalkTabixSyntenyAdapter-NHNSZZBX.js.map → WalkTabixSyntenyAdapter-VI3KA2GX.js.map} +2 -2
  19. package/dist/chunks/{chunk-MCE6STZR.js → chunk-22ANI2KL.js} +2 -2
  20. package/dist/chunks/{chunk-MCE6STZR.js.map → chunk-22ANI2KL.js.map} +1 -1
  21. package/dist/chunks/chunk-42KD4AVY.js +2 -0
  22. package/dist/chunks/{chunk-OKRGO7XA.js.map → chunk-42KD4AVY.js.map} +4 -4
  23. package/dist/chunks/chunk-4NIWMRKH.js +2 -0
  24. package/dist/chunks/chunk-4NIWMRKH.js.map +7 -0
  25. package/dist/chunks/chunk-BCOXRUNQ.js +3 -0
  26. package/dist/chunks/chunk-BCOXRUNQ.js.map +7 -0
  27. package/dist/chunks/chunk-DAPI7M4M.js +4 -0
  28. package/dist/chunks/chunk-DAPI7M4M.js.map +7 -0
  29. package/dist/chunks/chunk-DEWZOJSC.js +2 -0
  30. package/dist/chunks/{chunk-6OXXT2YP.js.map → chunk-DEWZOJSC.js.map} +4 -4
  31. package/dist/chunks/{chunk-K4TFAHGC.js → chunk-FCMXEFJA.js} +1 -1
  32. package/dist/chunks/{chunk-K4TFAHGC.js.map → chunk-FCMXEFJA.js.map} +1 -1
  33. package/dist/chunks/{chunk-UOFWYS6T.js → chunk-GYY6LF5L.js} +1 -1
  34. package/dist/chunks/{chunk-UOFWYS6T.js.map → chunk-GYY6LF5L.js.map} +1 -1
  35. package/dist/chunks/{chunk-F2DMEJEK.js → chunk-HHMJNAA3.js} +2 -2
  36. package/dist/chunks/{chunk-NPV7SRJD.js → chunk-HTTONSO4.js} +2 -2
  37. package/dist/chunks/{chunk-NPV7SRJD.js.map → chunk-HTTONSO4.js.map} +1 -1
  38. package/dist/chunks/{chunk-MVVXRZXV.js → chunk-JRJHXLGP.js} +2 -2
  39. package/dist/chunks/{chunk-MVVXRZXV.js.map → chunk-JRJHXLGP.js.map} +2 -2
  40. package/dist/chunks/chunk-K4JL57NR.js +2 -0
  41. package/dist/chunks/chunk-K4JL57NR.js.map +7 -0
  42. package/dist/chunks/chunk-KOC23YNN.js +4 -0
  43. package/dist/chunks/chunk-KOC23YNN.js.map +7 -0
  44. package/dist/chunks/{chunk-MNCPMOHJ.js → chunk-LTNFVL2F.js} +2 -2
  45. package/dist/chunks/chunk-OZEKEYT3.js +2 -0
  46. package/dist/chunks/{chunk-ZAYC5OMQ.js.map → chunk-OZEKEYT3.js.map} +4 -4
  47. package/dist/chunks/{chunk-DUPBXBYE.js → chunk-POUV7OPY.js} +2 -2
  48. package/dist/chunks/{chunk-DUPBXBYE.js.map → chunk-POUV7OPY.js.map} +1 -1
  49. package/dist/chunks/{chunk-CNNZDE6I.js → chunk-SPL3UO2D.js} +2 -2
  50. package/dist/chunks/{chunk-CNNZDE6I.js.map → chunk-SPL3UO2D.js.map} +1 -1
  51. package/dist/chunks/{chunk-QJ43NDYS.js → chunk-TYV6UXJA.js} +2 -2
  52. package/dist/chunks/chunk-UR2MJJUP.js +2 -0
  53. package/dist/chunks/chunk-VTMJQFN5.js +2 -0
  54. package/dist/chunks/chunk-VTMJQFN5.js.map +7 -0
  55. package/dist/chunks/{chunk-CXOMX2I6.js → chunk-XZGYJ3JC.js} +1 -1
  56. package/dist/chunks/{chunk-CXOMX2I6.js.map → chunk-XZGYJ3JC.js.map} +1 -1
  57. package/dist/chunks/{chunk-TGSDXZXI.js → chunk-Z3IKZAYC.js} +2 -2
  58. package/dist/chunks/{chunk-TGSDXZXI.js.map → chunk-Z3IKZAYC.js.map} +1 -1
  59. package/dist/chunks/chunk-ZV5FNOHS.js +3 -0
  60. package/dist/chunks/chunk-ZV5FNOHS.js.map +7 -0
  61. package/dist/chunks/{renderSvg-7G6NWBXF.js → renderSvg-3PPAEMBT.js} +2 -2
  62. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +4 -4
  63. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
  64. package/package.json +3 -3
  65. package/src/GraphComputeLayout.test.ts +24 -0
  66. package/src/GraphComputeLayout.ts +4 -2
  67. package/src/GraphGenomeView/components/GraphSettingsDialog.tsx +16 -0
  68. package/src/GraphGenomeView/components/WalkRowsOverlay.tsx +83 -0
  69. package/src/GraphGenomeView/components/usePaneGestures.test.tsx +82 -5
  70. package/src/GraphGenomeView/components/usePaneGestures.ts +10 -4
  71. package/src/GraphGenomeView/graphLayers.ts +14 -1
  72. package/src/GraphGenomeView/legendSections.ts +32 -0
  73. package/src/GraphGenomeView/model.test.ts +294 -113
  74. package/src/GraphGenomeView/model.ts +3 -10
  75. package/src/GraphGenomeView/nodeSize.test.ts +21 -5
  76. package/src/GraphGenomeView/nodeSize.ts +24 -8
  77. package/src/GraphGenomeView/pane/fitViews.ts +5 -110
  78. package/src/GraphGenomeView/pane/graphViews.ts +96 -20
  79. package/src/GraphGenomeView/pane/launchActions.ts +55 -33
  80. package/src/GraphGenomeView/pane/launchViews.ts +46 -0
  81. package/src/GraphGenomeView/pane/loadActions.ts +85 -25
  82. package/src/GraphGenomeView/pane/morph.test.ts +25 -1
  83. package/src/GraphGenomeView/pane/morph.ts +3 -1
  84. package/src/GraphGenomeView/pane/paneBase.ts +5 -7
  85. package/src/GraphGenomeView/pane/renderingActions.ts +61 -2
  86. package/src/GraphGenomeView/pane/settingActions.ts +62 -4
  87. package/src/GraphGenomeView/pane/walkRowViews.ts +17 -6
  88. package/src/GraphGenomeView/tubeMapPanels.ts +2 -1
  89. package/src/GraphGenomeView/viewModel.ts +4 -9
  90. package/src/GraphGenomeView/walkRowBlocks.ts +50 -0
  91. package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +35 -5
  92. package/src/LinearGraphDisplay/components/GraphStatusChrome.test.tsx +18 -0
  93. package/src/LinearGraphDisplay/components/GraphStatusChrome.tsx +4 -2
  94. package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.test.tsx +41 -1
  95. package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.tsx +33 -19
  96. package/src/LinearGraphDisplay/configSchema.ts +4 -3
  97. package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +2 -9
  98. package/src/RgfaTabixAdapter/configSchema.ts +3 -3
  99. package/src/RgfaTabixAdapter/walkFiles.test.ts +6 -6
  100. package/src/WalkFormsAdapter/WalkFormsAdapter.test.ts +204 -0
  101. package/src/WalkFormsAdapter/WalkFormsAdapter.ts +232 -0
  102. package/src/WalkFormsAdapter/configSchema.ts +127 -0
  103. package/src/WalkFormsAdapter/formRows.test.ts +258 -0
  104. package/src/WalkFormsAdapter/formRows.ts +231 -0
  105. package/src/WalkFormsAdapter/formsMenu.test.ts +237 -0
  106. package/src/WalkFormsAdapter/formsMenu.ts +202 -0
  107. package/src/WalkFormsAdapter/index.ts +21 -0
  108. package/src/WalkFormsAdapter/pinned.ts +33 -0
  109. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.test.ts +5 -5
  110. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +3 -6
  111. package/src/WalkTabixSyntenyAdapter/configSchema.ts +2 -2
  112. package/src/hoverSync/graphViewHighlights.test.ts +53 -0
  113. package/src/hoverSync/graphViewHighlights.ts +25 -0
  114. package/src/hoverSync/lgvHover.test.ts +100 -1
  115. package/src/hoverSync/lgvHover.ts +77 -6
  116. package/src/index.ts +4 -0
  117. package/src/launchFromGraph/contributors.test.ts +52 -0
  118. package/src/launchFromGraph/contributors.ts +105 -1
  119. package/src/launchFromGraph/launchFromGraph.ts +2 -2
  120. package/src/panSNAliases/panSNAliases.test.ts +2 -2
  121. package/src/panSNTabix.test.ts +28 -0
  122. package/src/panSNTabix.ts +26 -8
  123. package/src/version.ts +1 -1
  124. package/dist/chunks/GraphTrackSettingsDialog-UTYH6HEC.js +0 -6
  125. package/dist/chunks/GraphTrackSettingsDialog-UTYH6HEC.js.map +0 -7
  126. package/dist/chunks/LinearGraphDisplay-O4SQSXJJ.js +0 -2
  127. package/dist/chunks/MinigraphBubbleAdapter-3NJ2VLN2.js +0 -2
  128. package/dist/chunks/RgfaTabixAdapter-55DGDID2.js +0 -2
  129. package/dist/chunks/RgfaTabixAdapter-55DGDID2.js.map +0 -7
  130. package/dist/chunks/WalkTabixSyntenyAdapter-NHNSZZBX.js +0 -2
  131. package/dist/chunks/chunk-4I3K4RHE.js +0 -2
  132. package/dist/chunks/chunk-4I3K4RHE.js.map +0 -7
  133. package/dist/chunks/chunk-6OXXT2YP.js +0 -2
  134. package/dist/chunks/chunk-NFH2U74B.js +0 -2
  135. package/dist/chunks/chunk-OKRGO7XA.js +0 -2
  136. package/dist/chunks/chunk-P7VMOTUM.js +0 -3
  137. package/dist/chunks/chunk-P7VMOTUM.js.map +0 -7
  138. package/dist/chunks/chunk-RU4WSKBO.js +0 -3
  139. package/dist/chunks/chunk-RU4WSKBO.js.map +0 -7
  140. package/dist/chunks/chunk-WGTLNVST.js +0 -2
  141. package/dist/chunks/chunk-WGTLNVST.js.map +0 -7
  142. package/dist/chunks/chunk-XQFBLJU5.js +0 -4
  143. package/dist/chunks/chunk-XQFBLJU5.js.map +0 -7
  144. package/dist/chunks/chunk-Y5DNBFDY.js +0 -3
  145. package/dist/chunks/chunk-Y5DNBFDY.js.map +0 -7
  146. package/dist/chunks/chunk-ZAYC5OMQ.js +0 -2
  147. package/src/launchFromGraph/launchTracks.test.ts +0 -95
  148. package/src/launchFromGraph/launchTracks.ts +0 -49
  149. /package/dist/chunks/{AddTrackWorkflow-LK25EC3H.js.map → AddTrackWorkflow-NLSZYK56.js.map} +0 -0
  150. /package/dist/chunks/{GraphGenomeView-P3XMSKUQ.js.map → GraphGenomeView-KI4FQ5A7.js.map} +0 -0
  151. /package/dist/chunks/{HighlightColorDialog-G6AFRYUD.js.map → HighlightColorDialog-D4RCUCUF.js.map} +0 -0
  152. /package/dist/chunks/{chunk-F2DMEJEK.js.map → chunk-HHMJNAA3.js.map} +0 -0
  153. /package/dist/chunks/{chunk-MNCPMOHJ.js.map → chunk-LTNFVL2F.js.map} +0 -0
  154. /package/dist/chunks/{chunk-QJ43NDYS.js.map → chunk-TYV6UXJA.js.map} +0 -0
  155. /package/dist/chunks/{chunk-NFH2U74B.js.map → chunk-UR2MJJUP.js.map} +0 -0
  156. /package/dist/chunks/{renderSvg-7G6NWBXF.js.map → renderSvg-3PPAEMBT.js.map} +0 -0
  157. /package/src/RgfaTabixAdapter/test_data/{walks4_follow.GRCh38.walks.bed.gz → walks5_follow.GRCh38.walks.bed.gz} +0 -0
  158. /package/src/RgfaTabixAdapter/test_data/{walks4_follow.GRCh38.walks.bed.gz.tbi → walks5_follow.GRCh38.walks.bed.gz.tbi} +0 -0
  159. /package/src/RgfaTabixAdapter/test_data/{walks4_header.GRCh38.walks.bed.gz → walks5_header.GRCh38.walks.bed.gz} +0 -0
  160. /package/src/RgfaTabixAdapter/test_data/{walks4_header.GRCh38.walks.bed.gz.tbi → walks5_header.GRCh38.walks.bed.gz.tbi} +0 -0
  161. /package/src/RgfaTabixAdapter/test_data/{walks4_inversion.GRCh38.walks.bed.gz → walks5_inversion.GRCh38.walks.bed.gz} +0 -0
  162. /package/src/RgfaTabixAdapter/test_data/{walks4_inversion.GRCh38.walks.bed.gz.tbi → walks5_inversion.GRCh38.walks.bed.gz.tbi} +0 -0
  163. /package/src/RgfaTabixAdapter/test_data/{walks4_lanepairs.GRCh38.walks.bed.gz → walks5_lanepairs.GRCh38.walks.bed.gz} +0 -0
  164. /package/src/RgfaTabixAdapter/test_data/{walks4_lanepairs.GRCh38.walks.bed.gz.tbi → walks5_lanepairs.GRCh38.walks.bed.gz.tbi} +0 -0
  165. /package/src/RgfaTabixAdapter/test_data/{walks4_maxnode.GRCh38.walks.bed.gz → walks5_maxnode.GRCh38.walks.bed.gz} +0 -0
  166. /package/src/RgfaTabixAdapter/test_data/{walks4_maxnode.GRCh38.walks.bed.gz.tbi → walks5_maxnode.GRCh38.walks.bed.gz.tbi} +0 -0
  167. /package/src/RgfaTabixAdapter/test_data/{walks4_rejoin.GRCh38.walks.bed.gz → walks5_rejoin.GRCh38.walks.bed.gz} +0 -0
  168. /package/src/RgfaTabixAdapter/test_data/{walks4_rejoin.GRCh38.walks.bed.gz.tbi → walks5_rejoin.GRCh38.walks.bed.gz.tbi} +0 -0
  169. /package/src/RgfaTabixAdapter/test_data/{walks4_twopart.GRCh38.walks.bed.gz → walks5_twopart.GRCh38.walks.bed.gz} +0 -0
  170. /package/src/RgfaTabixAdapter/test_data/{walks4_twopart.GRCh38.walks.bed.gz.tbi → walks5_twopart.GRCh38.walks.bed.gz.tbi} +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()
@@ -874,27 +901,7 @@ describe('loadGFAFromLocation', () => {
874
901
  expect(model.graphRegion).toEqual(TEST_REGION)
875
902
  })
876
903
 
877
- test('a graph read from a url gives the figure spec that reads it again', async () => {
878
- rpcRespond()
879
- mockReadFile.mockResolvedValue(SIMPLE_GFA)
880
- const model = stateModelFactory().create({
881
- type: 'GraphGenomeView',
882
- gfaLocation: location,
883
- loadedRegion: TEST_REGION,
884
- layoutMode: 'auto',
885
- })
886
- await model.load()
887
- expect(model.figureSpec()).toMatchObject({
888
- gfa: location.uri,
889
- region: 'chr1:1000-5000',
890
- layout: 'auto',
891
- })
892
- expect(model.figure()).toContain(
893
- '"gfa":"https://example.com/graphs/small.gfa"',
894
- )
895
- })
896
-
897
- test('a gene track in tabix shorthand gives the figure spec its index', async () => {
904
+ test('a gene track in tabix shorthand turns genes on', async () => {
898
905
  rpcRespond()
899
906
  mockReadFile.mockResolvedValue(SIMPLE_GFA)
900
907
  mockSession.tracks = [
@@ -916,11 +923,7 @@ describe('loadGFAFromLocation', () => {
916
923
  })
917
924
  await model.load()
918
925
  expect(model.showGenes).toBe(true)
919
- expect(model.figureSpec()?.genes).toEqual({
920
- file: 'https://jbrowse.org/ucsc/hg38/ncbiRefSeq.gff.gz',
921
- index: 'https://jbrowse.org/ucsc/hg38/ncbiRefSeq.gff.gz.csi',
922
- format: 'gff3',
923
- })
926
+ expect(model.geneTrack?.trackId).toBe('ncbiRefSeq')
924
927
  mockSession.tracks = []
925
928
  })
926
929
 
@@ -1160,7 +1163,7 @@ describe('layoutMode', () => {
1160
1163
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
1161
1164
  rpcRespond()
1162
1165
  const model = attachedModel()
1163
- await model.loadGFA(SIMPLE_GFA, 'plain')
1166
+ await model.loadGFA(WALKED_GFA, 'plain')
1164
1167
 
1165
1168
  model.setMaxGraphNodes(1)
1166
1169
  await layoutsSettled(model)
@@ -1458,7 +1461,7 @@ describe('node budget', () => {
1458
1461
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
1459
1462
  rpcRespond()
1460
1463
  const model = withLimit(1)
1461
- await model.loadGFA(SIMPLE_GFA, 'two nodes')
1464
+ await model.loadGFA(WALKED_GFA, 'two nodes')
1462
1465
  expect(String(model.error)).toMatch(/too large to draw/i)
1463
1466
 
1464
1467
  await model.switchLayout('walkrows')
@@ -1467,6 +1470,21 @@ describe('node budget', () => {
1467
1470
  consoleSpy.mockRestore()
1468
1471
  })
1469
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
+
1470
1488
  test('a declined graph does not keep the last graph drawn under it', async () => {
1471
1489
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
1472
1490
  rpcRespond()
@@ -1490,7 +1508,7 @@ describe('node budget', () => {
1490
1508
  rpcRespond()
1491
1509
  const model = withLimit(1)
1492
1510
  model.setLayoutMode('walkrows')
1493
- await model.loadGFA(SIMPLE_GFA, 'two nodes')
1511
+ await model.loadGFA(WALKED_GFA, 'two nodes')
1494
1512
  expect(model.error).toBeUndefined()
1495
1513
  expect(model.layoutResult).toBeDefined()
1496
1514
  })
@@ -2303,6 +2321,61 @@ describe('launching out of the graph', () => {
2303
2321
  // Where every contributor is a loaded assembly the graph offers one panel per
2304
2322
  // strain, and a synteny view of all of them — the item is disabled while no
2305
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
+
2306
2379
  test('loaded strains offer a per-strain linear view and a synteny item', async () => {
2307
2380
  mockSession.assemblyNames = ['K12', 'Sakai']
2308
2381
  const model = await loadedGraph(ECOLI_RGFA, {
@@ -2369,6 +2442,68 @@ describe('launching out of the graph', () => {
2369
2442
  expect(model.connectedViewId).toBe('view-1')
2370
2443
  })
2371
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
+
2372
2507
  // A pairing is not stolen from a view still on screen: a graph launched from
2373
2508
  // a linear view came in on that sync. One whose view has been closed is held
2374
2509
  // by nothing, and kept, the view the graph opens next never got its hover.
@@ -3268,6 +3403,71 @@ describe('walk rows', () => {
3268
3403
  expect(model.walkStripRows).toBeUndefined()
3269
3404
  })
3270
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
+
3271
3471
  test('walk rows and tube maps take no strip', async () => {
3272
3472
  rpcRespond()
3273
3473
  const model = stateModelFactory().create({
@@ -3413,89 +3613,6 @@ describe('walk rows', () => {
3413
3613
  expect(svg).toContain("'s path")
3414
3614
  })
3415
3615
 
3416
- test("the figure spec keeps the strip's sample filter", async () => {
3417
- rpcRespond()
3418
- mockReadFile.mockResolvedValue(WALKS_GFA)
3419
- const model = stateModelFactory().create({
3420
- type: 'GraphGenomeView',
3421
- gfaLocation: {
3422
- uri: 'https://example.com/graphs/walks.gfa',
3423
- locationType: 'UriLocation',
3424
- },
3425
- loadedRegion: TEST_REGION,
3426
- layoutMode: 'ordered',
3427
- layers: { walkStrip: true },
3428
- rows: { kept: ['B'] },
3429
- })
3430
- await model.load()
3431
- expect(model.walkStripRows!.rows.map(r => r.label)).toEqual(['B#1'])
3432
- expect(model.figureSpec()).toMatchObject({
3433
- walkStrip: true,
3434
- walkRowSamples: ['B'],
3435
- })
3436
- model.setWalkStrip(false)
3437
- expect(model.figureSpec()?.walkRowSamples).toBeUndefined()
3438
- })
3439
-
3440
- // bandage-figure shows deletion edges unless a spec says otherwise
3441
- test('the figure spec states deletion edges only when they are off', async () => {
3442
- rpcRespond()
3443
- mockReadFile.mockResolvedValue(WALKS_GFA)
3444
- const model = stateModelFactory().create({
3445
- type: 'GraphGenomeView',
3446
- gfaLocation: {
3447
- uri: 'https://example.com/graphs/walks.gfa',
3448
- locationType: 'UriLocation',
3449
- },
3450
- loadedRegion: TEST_REGION,
3451
- layoutMode: 'ordered',
3452
- })
3453
- await model.load()
3454
- expect(model.figureSpec()).not.toHaveProperty('showDeletionEdges')
3455
- model.setLayer('deletions', false)
3456
- expect(model.figureSpec()?.showDeletionEdges).toBe(false)
3457
- })
3458
-
3459
- // bandage-figure laid a linear layout out as an ordinary one, and drew its
3460
- // own line widths, while the spec said nothing about either
3461
- test('the figure spec carries the settings that change the picture', async () => {
3462
- rpcRespond()
3463
- mockReadFile.mockResolvedValue(WALKS_GFA)
3464
- const model = stateModelFactory().create({
3465
- type: 'GraphGenomeView',
3466
- gfaLocation: {
3467
- uri: 'https://example.com/graphs/walks.gfa',
3468
- locationType: 'UriLocation',
3469
- },
3470
- loadedRegion: TEST_REGION,
3471
- layoutMode: 'ordered',
3472
- size: 9,
3473
- })
3474
- await model.load()
3475
- expect(model.figureSpec()).toMatchObject({ contigThickness: 9 })
3476
- expect(model.figureSpec()).not.toHaveProperty('connectorThickness')
3477
- expect(model.figureSpec()).not.toHaveProperty('linearLayout')
3478
- model.setLinearLayout(true)
3479
- expect(model.figureSpec()?.linearLayout).toBe(true)
3480
- })
3481
-
3482
- test('walk rows offer no figure spec, since bandage-figure draws no walk rows', async () => {
3483
- rpcRespond()
3484
- mockReadFile.mockResolvedValue(WALKS_GFA)
3485
- const model = stateModelFactory().create({
3486
- type: 'GraphGenomeView',
3487
- gfaLocation: {
3488
- uri: 'https://example.com/graphs/walks.gfa',
3489
- locationType: 'UriLocation',
3490
- },
3491
- loadedRegion: TEST_REGION,
3492
- layoutMode: 'walkrows',
3493
- })
3494
- await model.load()
3495
- expect(model.walkRowBars).toBeDefined()
3496
- expect(model.figureSpecUnavailable).toMatch(/^bandage-figure draws no /)
3497
- })
3498
-
3499
3616
  test("a picker's set keeps lifted walks this cut does not hold", async () => {
3500
3617
  rpcRespond()
3501
3618
  const model = stateModelFactory().create({
@@ -3908,6 +4025,33 @@ describe('walk rows', () => {
3908
4025
  model.setHoveredRowWalks([])
3909
4026
  expect(model.walkLift).toBeUndefined()
3910
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
+ })
3911
4055
  })
3912
4056
 
3913
4057
  describe('annotation reads beside a cut', () => {
@@ -4007,6 +4151,43 @@ describe('annotation reads beside a cut', () => {
4007
4151
  expect(model.error).toBeUndefined()
4008
4152
  })
4009
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
+
4010
4191
  test('a cut too large to draw still reads its genes, for when the limit is raised', async () => {
4011
4192
  vi.spyOn(console, 'error').mockImplementation(() => {})
4012
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
  }