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
@@ -9,6 +9,7 @@ import {
9
9
  import { walkRowLayoutOf } from '@jbrowse/bandage-core/layout/walkRowLayout'
10
10
  import { walkRowsOf } from '@jbrowse/bandage-core/layout/walkRowRuns'
11
11
  import { layoutModeByValue } from '@jbrowse/bandage-core/layoutModes'
12
+ import { panSNHaplotype, panSNSample } from '@jbrowse/bandage-core/pansn'
12
13
  import {
13
14
  anchorFromPaths,
14
15
  chooseReferencePath,
@@ -93,6 +94,15 @@ export const withLoadActions = withEngineSettings.actions(self => {
93
94
  }) as Promise<{ result: LayoutResult; duration: number }>
94
95
  }
95
96
 
97
+ // the node cap is a drawing's cost
98
+ function assertDrawable(graph: Graph) {
99
+ if (graph.nodes.length > self.maxGraphNodes) {
100
+ throw new Error(
101
+ `Graph too large to draw: ${graph.nodes.length.toLocaleString()} nodes (limit ${self.maxGraphNodes.toLocaleString()}). Pick Layout → Walk rows, zoom in to a smaller region, or raise the node limit in Graph settings.`,
102
+ )
103
+ }
104
+ }
105
+
96
106
  // Single dispatch point for every layout mode. A mode that returns a
97
107
  // result computed it locally; one that returns undefined can't draw this
98
108
  // graph and hands off to the remote FMMM engine, which is also how
@@ -150,6 +160,8 @@ export const withLoadActions = withEngineSettings.actions(self => {
150
160
  mode: self.chosenLayoutMode,
151
161
  }
152
162
  }
163
+ // walk rows and tube maps fall back here for a graph with no walks
164
+ assertDrawable(graph)
153
165
  const settings = self.engineSettings
154
166
  const hit = forceLayouts.ready(graph, settings)
155
167
  if (hit) {
@@ -209,11 +221,9 @@ export const withLoadActions = withEngineSettings.actions(self => {
209
221
  const signal = loadController?.signal
210
222
  let computed
211
223
  try {
212
- // the node cap is a drawing's cost, and walk rows draw a bar per walk
213
- if (self.modeDrawsNodes && graph.nodes.length > self.maxGraphNodes) {
214
- throw new Error(
215
- `Graph too large to draw: ${graph.nodes.length.toLocaleString()} nodes (limit ${self.maxGraphNodes.toLocaleString()}). Pick Layout → Walk rows, zoom in to a smaller region, or raise the node limit in Graph settings.`,
216
- )
224
+ // walk rows draw a bar per walk, not a node each
225
+ if (self.modeDrawsNodes) {
226
+ assertDrawable(graph)
217
227
  }
218
228
  computed = yield* computeLayout(graph)
219
229
  } catch (e) {
@@ -314,6 +324,7 @@ export const withLoadActions = withEngineSettings.actions(self => {
314
324
  self.indexBubbles = undefined
315
325
  self.geneTrackFeatures = undefined
316
326
  self.walkGeneFeatures = undefined
327
+ self.ownGeneFeatures = undefined
317
328
  walkGeneCache.clear()
318
329
  self.repeatArrays = undefined
319
330
  self.clearInteractionState()
@@ -478,11 +489,18 @@ export const withLoadActions = withEngineSettings.actions(self => {
478
489
  { adapterConfig, regions: [region], signal },
479
490
  )) as Feature[]
480
491
  } catch (e) {
481
- console.warn(`[GraphGenomeView] no ${what} for this graph`, e)
492
+ // a cut the next one replaced aborts its reads, which is not a failure
493
+ if (!signal?.aborted) {
494
+ console.warn(`[GraphGenomeView] no ${what} for this graph`, e)
495
+ }
482
496
  return undefined
483
497
  }
484
498
  }
485
499
 
500
+ // Bubble indexes a read failed on, so a graph without one, the ordinary
501
+ // case, is not asked again on every re-cut of a pan
502
+ const missingBubbleIndexes = new Set<string>()
503
+
486
504
  // The bubble index that the hosted HPRC build keeps beside its segments,
487
505
  // `<prefix>.bubbles.bed.gz`, read through the bubble adapter over the same
488
506
  // window. A graph whose source has no such file, the ordinary case for a
@@ -499,13 +517,18 @@ export const withLoadActions = withEngineSettings.actions(self => {
499
517
  // The track config arrives as written, so the prefix is either the
500
518
  // `uri` shorthand or the segments location it expands to.
501
519
  const prefix = bubblePrefix(adapterConfig)
502
- if (adapterConfig.type !== 'RgfaTabixAdapter' || prefix === undefined) {
520
+ const uri = `${prefix}.bubbles.bed.gz`
521
+ if (
522
+ adapterConfig.type !== 'RgfaTabixAdapter' ||
523
+ prefix === undefined ||
524
+ missingBubbleIndexes.has(uri)
525
+ ) {
503
526
  return
504
527
  }
505
528
  const features = yield* readFeatures(
506
529
  {
507
530
  type: 'MinigraphBubbleAdapter',
508
- uri: `${prefix}.bubbles.bed.gz`,
531
+ uri,
509
532
  baseUri: adapterConfig.baseUri,
510
533
  assemblyNameToPanSN: adapterConfig.assemblyNameToPanSN,
511
534
  },
@@ -513,6 +536,9 @@ export const withLoadActions = withEngineSettings.actions(self => {
513
536
  'bubble index',
514
537
  signal,
515
538
  )
539
+ if (features === undefined && !signal?.aborted) {
540
+ missingBubbleIndexes.add(uri)
541
+ }
516
542
  if (features && isLive()) {
517
543
  self.indexBubbles = features.flatMap(f => {
518
544
  const start = f.get('start')
@@ -559,6 +585,25 @@ export const withLoadActions = withEngineSettings.actions(self => {
559
585
  )
560
586
  }
561
587
 
588
+ // A haplotype assembly's genes over a span of its own contig, named for
589
+ // that contig whatever the track calls it, read once per cut
590
+ function* haplotypeGenes(trackId: string, region: SubgraphRegion) {
591
+ const cacheKey = `${trackId} ${region.refName}:${region.start}-${region.end}`
592
+ let genes = walkGeneCache.get(cacheKey)
593
+ if (!genes) {
594
+ const features = yield* trackFeatures(trackId, region, 'haplotype genes')
595
+ if (!features) {
596
+ return undefined
597
+ }
598
+ genes = geneModelsFrom(features).map(g => ({
599
+ ...g,
600
+ refName: region.refName,
601
+ }))
602
+ walkGeneCache.set(cacheKey, genes)
603
+ }
604
+ return genes
605
+ }
606
+
562
607
  // The annotation tracks read over the cut: genes, which `geneFeatures`
563
608
  // makes genes of for the backbone to carry, and tandem repeat arrays, for
564
609
  // the walk rows to measure between and tile by
@@ -860,24 +905,10 @@ export const withLoadActions = withEngineSettings.actions(self => {
860
905
  yield Promise.all(
861
906
  reads.map(({ row, trackId, region }) =>
862
907
  flow(function* () {
863
- const cacheKey = `${trackId} ${region.refName}:${region.start}-${region.end}`
864
- let genes = walkGeneCache.get(cacheKey)
865
- if (!genes) {
866
- const features = yield* trackFeatures(
867
- trackId,
868
- region,
869
- 'haplotype genes',
870
- )
871
- if (!features) {
872
- return
873
- }
874
- genes = geneModelsFrom(features).map(g => ({
875
- ...g,
876
- refName: region.refName,
877
- }))
878
- walkGeneCache.set(cacheKey, genes)
908
+ const genes = yield* haplotypeGenes(trackId, region)
909
+ if (genes) {
910
+ fetched.set(row, genes)
879
911
  }
880
- fetched.set(row, genes)
881
912
  })(),
882
913
  ),
883
914
  )
@@ -889,6 +920,35 @@ export const withLoadActions = withEngineSettings.actions(self => {
889
920
  self.walkGeneFeatures = reads.length ? fetched : undefined
890
921
  }
891
922
  }),
923
+ // Each loaded haplotype's genes on its own off-reference nodes, renamed
924
+ // to those nodes' stable sequence so genePins lands them there
925
+ loadOwnGenes: flow(function* () {
926
+ const reads = self.ownGeneReads
927
+ const key = JSON.stringify(reads)
928
+ const graph = self.graph
929
+ const fetched = (yield Promise.all(
930
+ reads.map(({ stableRefName, trackId, region }) =>
931
+ flow(function* () {
932
+ const genes = yield* haplotypeGenes(trackId, region)
933
+ // a chip reading LPA twice would not say whose copy each is
934
+ const whose =
935
+ panSNHaplotype(stableRefName) ?? panSNSample(stableRefName)
936
+ return (genes ?? []).map(g => ({
937
+ ...g,
938
+ name: `${g.name} · ${whose}`,
939
+ refName: stableRefName,
940
+ }))
941
+ })(),
942
+ ),
943
+ )) as GeneModel[][]
944
+ if (
945
+ isAlive(self) &&
946
+ self.graph === graph &&
947
+ JSON.stringify(self.ownGeneReads) === key
948
+ ) {
949
+ self.ownGeneFeatures = reads.length ? fetched.flat() : undefined
950
+ }
951
+ }),
892
952
  // Open a bubble in place: it keeps its colour, the rest of the drawing
893
953
  // greys, and the bubbles inside it get labels of their own. Opening an
894
954
  // open one closes it and those inside it.
@@ -1,5 +1,5 @@
1
1
  import { abgrAlpha, packAbgr } from '@jbrowse/bandage-core/renderer/colorBits'
2
- import { expect, test } from 'vitest'
2
+ import { expect, test, vi } from 'vitest'
3
3
 
4
4
  import {
5
5
  blendInto,
@@ -7,6 +7,7 @@ import {
7
7
  fadeEntering,
8
8
  morphStarts,
9
9
  routeStarts,
10
+ runMorph,
10
11
  } from './morph'
11
12
 
12
13
  import type { RenderBatch } from '@jbrowse/bandage-core/renderer/types'
@@ -248,3 +249,26 @@ test('a graph past the frame budget snaps', () => {
248
249
  expect(canMorph(free, free, 3000, false)).toBe(true)
249
250
  expect(canMorph(free, free, 3001, false)).toBe(false)
250
251
  })
252
+
253
+ // The layout cache holds the positions a morph blends, so a morph abandoned
254
+ // half way would leave a cached layout half blended for the next cache hit
255
+ test('a morph cut short lands its last frame', () => {
256
+ const frames: FrameRequestCallback[] = []
257
+ vi.stubGlobal('requestAnimationFrame', (cb: FrameRequestCallback) =>
258
+ frames.push(cb),
259
+ )
260
+ const blends: number[] = []
261
+ let live = true
262
+ const ended = vi.fn()
263
+ runMorph(
264
+ t => blends.push(t),
265
+ () => live,
266
+ ended,
267
+ )
268
+ frames.shift()!(0)
269
+ live = false
270
+ frames.shift()!(0)
271
+ expect(blends.at(-1)).toBe(1)
272
+ expect(ended).toHaveBeenCalledTimes(1)
273
+ vi.unstubAllGlobals()
274
+ })
@@ -314,7 +314,8 @@ export function motionAllowed() {
314
314
  }
315
315
 
316
316
  // Calls `blend` with the eased progress once a frame for MORPH_MS while
317
- // `isLive` holds, then `onEnd`. `finish` jumps to the last frame.
317
+ // `isLive` holds, then `onEnd`. `finish` jumps to the last frame, as does a
318
+ // morph cut short, since the layout cache keeps the positions it blends.
318
319
  export function runMorph(
319
320
  blend: (t: number) => void,
320
321
  isLive: () => boolean,
@@ -330,6 +331,7 @@ export function runMorph(
330
331
  let last = performance.now()
331
332
  const step = () => {
332
333
  if (!isLive()) {
334
+ blend(1)
333
335
  onEnd()
334
336
  return
335
337
  }
@@ -95,6 +95,8 @@ export const VIEWPORT_DEBOUNCE_MS = 150
95
95
  export const VIEWPORT_PANES_BUILT = 1
96
96
  // How many walk rows read their haplotype's genes, the first rows down
97
97
  export const WALK_GENE_ROWS = 40
98
+ // How many off-reference contigs read their haplotype's genes
99
+ export const OWN_GENE_CONTIGS = 40
98
100
 
99
101
  // What the canvas draws under the tube map, whose ink is all TubeMapOverlay's
100
102
  export type TubeMapRoutes = 'each' | 'grouped' | 'bundled'
@@ -177,13 +179,6 @@ export function fileName(location: FileLocation) {
177
179
  return path.split(/[\\/]/).at(-1) ?? ''
178
180
  }
179
181
 
180
- // A url bandage-figure can read again, or undefined for a local file
181
- export function uriOf(location: FileLocation | undefined) {
182
- return location && 'uri' in location && location.uri
183
- ? new URL(location.uri, location.baseUri ?? window.location.href).href
184
- : undefined
185
- }
186
-
187
182
  type ViewportOwner = 'fit' | 'user' | 'host'
188
183
 
189
184
  interface BuiltViewport {
@@ -366,6 +361,9 @@ export const paneBase = types
366
361
  // each walk row's genes, read from its haplotype's own assembly, by walk
367
362
  // name
368
363
  walkGeneFeatures: undefined as Map<string, GeneModel[]> | undefined,
364
+ // each loaded haplotype's genes on its own nodes off the reference, under
365
+ // those nodes' stable names (ownGeneReads)
366
+ ownGeneFeatures: undefined as GeneModel[] | undefined,
369
367
  // the source track's samples TSV, which walk rows group by
370
368
  walkRowSampleTable: undefined as SampleRow[] | undefined,
371
369
  // the tandem repeat arrays over the cut window, from the repeat track
@@ -1,3 +1,4 @@
1
+ import { sameBubble } from '@jbrowse/bandage-core/bubbles/bubbleLine'
1
2
  import { contains } from '@jbrowse/bandage-core/viewport'
2
3
  import { getSession } from '@jbrowse/core/util'
3
4
  import { addDisposer } from '@jbrowse/mobx-state-tree'
@@ -11,11 +12,16 @@ import {
11
12
  paneViewportOf,
12
13
  } from './paneBase'
13
14
  import {
15
+ bubbleForLgvHover,
16
+ hoverFromPair,
14
17
  hoverInRegion,
15
18
  nodeForLgvHover,
16
19
  readLgvHover,
20
+ readSelectedFeature,
17
21
  } from '../../hoverSync/lgvHover'
18
22
  import { walksForRow } from '../../hoverSync/rowWalks'
23
+ import { isRecord } from '../../isRecord'
24
+ import { withRows } from '../../launchFromGraph/linearViewTarget'
19
25
  import { trackedDpr } from '../trackedDpr'
20
26
 
21
27
  import type { LayoutEngineKind } from '@jbrowse/bandage-core/layoutEngines'
@@ -88,6 +94,23 @@ export const withRenderingActions = withLoadActions
88
94
  },
89
95
  ),
90
96
  )
97
+ // and the haplotypes' own genes on their nodes off the reference
98
+ addDisposer(
99
+ self,
100
+ reaction(
101
+ () => ({
102
+ graph: self.graph,
103
+ reads: JSON.stringify(self.ownGeneReads),
104
+ }),
105
+ () => {
106
+ void self.loadOwnGenes()
107
+ },
108
+ {
109
+ fireImmediately: true,
110
+ equals: (a, b) => a.graph === b.graph && a.reads === b.reads,
111
+ },
112
+ ),
113
+ )
91
114
  // Walk rows and tube map panels read the source track's samples
92
115
  // TSV, to group by its columns, again for a graph after a dropped one
93
116
  addDisposer(
@@ -145,8 +168,16 @@ export const withRenderingActions = withLoadActions
145
168
  untracked(() => {
146
169
  const region = self.graphRegion
147
170
  const graph = self.graph
171
+ const liveViewIds = new Set(
172
+ withRows([...getSession(self).views]).flatMap(v =>
173
+ isRecord(v) && typeof v.id === 'string' ? [v.id] : [],
174
+ ),
175
+ )
148
176
  const inRegion =
149
- hover && region && hoverInRegion(hover, region)
177
+ hover &&
178
+ region &&
179
+ hoverInRegion(hover, region) &&
180
+ hoverFromPair(hover, self.pairedViewId, liveViewIds)
150
181
  ? hover
151
182
  : undefined
152
183
  // a row naming a walk is the hover, not the node at its bp,
@@ -155,9 +186,14 @@ export const withRenderingActions = withLoadActions
155
186
  inRegion?.featureName && graph?.paths
156
187
  ? walksForRow(inRegion.featureName, graph.paths)
157
188
  : []
189
+ // a bubble track's feature, or a graph-derived VCF record,
190
+ // names a whole bubble, which lights whatever the setting
191
+ const bubble =
192
+ inRegion && bubbleForLgvHover(inRegion, self.bubbles)
158
193
  if (region && graph && !self.pointerInPane) {
194
+ self.setHoveredBubble(bubble ?? null)
159
195
  self.setHoveredNode(
160
- inRegion && lightsNode && rowWalks.length === 0
196
+ inRegion && lightsNode && rowWalks.length === 0 && !bubble
161
197
  ? nodeForLgvHover({ hover: inRegion, nodes: graph.nodes })
162
198
  : null,
163
199
  )
@@ -167,6 +203,29 @@ export const withRenderingActions = withLoadActions
167
203
  }),
168
204
  )
169
205
 
206
+ // A click on a bubble feature or graph-derived variant in a linear
207
+ // view opens its bubble here, as clicking its label would
208
+ addDisposer(
209
+ self,
210
+ reaction(
211
+ () => readSelectedFeature(getSession(self).selection),
212
+ selected => {
213
+ const region = self.graphRegion
214
+ const bubble =
215
+ selected &&
216
+ region &&
217
+ hoverInRegion(selected, region) &&
218
+ bubbleForLgvHover(selected, self.bubbles)
219
+ if (
220
+ bubble &&
221
+ !self.openBubbles.some(open => sameBubble(open, bubble))
222
+ ) {
223
+ self.toggleBubble(bubble)
224
+ }
225
+ },
226
+ ),
227
+ )
228
+
170
229
  // Reaction: drop a hover the pointer cannot still be over.
171
230
  //
172
231
  // A hover is set by a mousemove on the canvas, and here the drawing
@@ -20,7 +20,7 @@ import { nodeOwnLocation } from '../../launchFromGraph/contributors'
20
20
  import { withRows } from '../../launchFromGraph/linearViewTarget'
21
21
  import { withLayer } from '../graphLayers'
22
22
  import { colorOfScheme } from '../nodeColor'
23
- import { sizeOfNodeWidth } from '../nodeSize'
23
+ import { sizeOf } from '../nodeSize'
24
24
 
25
25
  import type { GraphGrammar } from './graphViews'
26
26
  import type { ScreenTransform } from './keepInView'
@@ -32,6 +32,7 @@ import type { WalkRowGroupBy } from '../walkRowGroups'
32
32
  import type { BubbleSpread } from '@jbrowse/bandage-core/bubbleSpreads'
33
33
  import type { MinigraphBubble } from '@jbrowse/bandage-core/bubbles/bubbleLine'
34
34
  import type { ColorScheme } from '@jbrowse/bandage-core/colorSchemes'
35
+ import type { EdgeWidth } from '@jbrowse/bandage-core/edgeWidths'
35
36
  import type { FacetSetting } from '@jbrowse/bandage-core/facetGrid'
36
37
  import type { LayoutEngineKind } from '@jbrowse/bandage-core/layoutEngines'
37
38
  import type { LayoutModeValue } from '@jbrowse/bandage-core/layoutModes'
@@ -44,6 +45,8 @@ import type {
44
45
  WalkLayer,
45
46
  } from '@jbrowse/bandage-core/walkEncoding'
46
47
 
48
+ const ROW_HOVER_SETTLE_MS = 80
49
+
47
50
  export const withSettingActions = withTubeMapViews
48
51
  .actions(() => ({
49
52
  // each host overrides: a view assigns its props, a track display writes
@@ -166,7 +169,20 @@ export const withSettingActions = withTubeMapViews
166
169
  },
167
170
  setNodeWidth(width: NodeWidth) {
168
171
  self.writeGrammar({
169
- size: sizeOfNodeWidth(width, self.contigThickness),
172
+ size: sizeOf({
173
+ width,
174
+ px: self.contigThickness,
175
+ links: self.linkWidth,
176
+ }),
177
+ })
178
+ },
179
+ setLinkWidth(links: EdgeWidth) {
180
+ self.writeGrammar({
181
+ size: sizeOf({
182
+ width: self.nodeWidth,
183
+ px: self.contigThickness,
184
+ links,
185
+ }),
170
186
  })
171
187
  },
172
188
  setTubeMapFold(bp: number) {
@@ -237,7 +253,19 @@ export const withSettingActions = withTubeMapViews
237
253
  )
238
254
  },
239
255
  setWalkRowSamples(samples: string[] | undefined) {
240
- self.writeGrammar({ rows: samples ? { kept: samples } : {} })
256
+ const order = self.walkRowOrder
257
+ self.writeGrammar({
258
+ rows: {
259
+ ...(samples ? { kept: samples } : {}),
260
+ ...(order ? { order } : {}),
261
+ },
262
+ })
263
+ },
264
+ setWalkRowOrder(order: 'shared' | undefined) {
265
+ const kept = self.walkRowSamples
266
+ self.writeGrammar({
267
+ rows: { ...(kept ? { kept } : {}), ...(order ? { order } : {}) },
268
+ })
241
269
  },
242
270
  setWalkRowGroupBy(groupBy: WalkRowGroupBy | undefined) {
243
271
  if (groupBy || self.walkRowGroupBy) {
@@ -276,7 +304,7 @@ export const withSettingActions = withTubeMapViews
276
304
  self.hoveredTube = tube
277
305
  }
278
306
  },
279
- setHoveredRowWalks(walks: string[]) {
307
+ applyHoveredRowWalks(walks: string[]) {
280
308
  if (walks.join('\n') !== self.hoveredRowWalks.join('\n')) {
281
309
  self.hoveredRowWalks = walks
282
310
  }
@@ -469,6 +497,36 @@ export const withSettingActions = withTubeMapViews
469
497
  self.builtViewport = undefined
470
498
  },
471
499
  }))
500
+ .actions(self => {
501
+ let settle: ReturnType<typeof setTimeout> | undefined
502
+ let pending: string | undefined
503
+ return {
504
+ // A lane row's lift rebuilds the drawing (43 ms at 10k nodes), and a
505
+ // sweep down a lane crosses a row every few px, so a move from one row
506
+ // to the next lifts once the pointer has stayed on a row a moment.
507
+ // Entering and leaving the rows apply at once.
508
+ setHoveredRowWalks(walks: string[]) {
509
+ const key = walks.join('\n')
510
+ if (key === pending) {
511
+ return
512
+ }
513
+ clearTimeout(settle)
514
+ pending = undefined
515
+ if (walks.length > 0 && self.hoveredRowWalks.length > 0) {
516
+ pending = key
517
+ settle = setTimeout(() => {
518
+ pending = undefined
519
+ // a hover cleared meanwhile, by a pan, stays cleared
520
+ if (isAlive(self) && self.hoveredRowWalks.length > 0) {
521
+ self.applyHoveredRowWalks(walks)
522
+ }
523
+ }, ROW_HOVER_SETTLE_MS)
524
+ } else {
525
+ self.applyHoveredRowWalks(walks)
526
+ }
527
+ },
528
+ }
529
+ })
472
530
  .actions(self => ({
473
531
  // The frame clock. Unclamped, since clampZoom's floor is there to keep a
474
532
  // scale finite and 1 / bpPerPx already is; clamped, a whole-chromosome
@@ -18,6 +18,7 @@ import { resolveLocationAssembly } from '../../launchFromGraph/contributors'
18
18
 
19
19
  import type { SubgraphRegion } from '../../GetSubgraph'
20
20
  import type { WalkAxis, WalkRow } from '@jbrowse/bandage-core/layout/walkRows'
21
+ import type { LayoutModeValue } from '@jbrowse/bandage-core/layoutModes'
21
22
 
22
23
  export const withWalkRowViews = withLaunchViews
23
24
  .views(self => ({
@@ -47,13 +48,23 @@ export const withWalkRowViews = withLaunchViews
47
48
  }
48
49
  return walkStripFrame(bars, { width: self.paneWidth })
49
50
  },
50
- // Whether a cut must follow every snarl a walk leaves the window by, so
51
- // walks come back whole: walk rows measure them, and so does the strip
51
+ // Whether a cut under `mode` must follow every snarl a walk leaves the
52
+ // window by, so walks come back whole: walk rows measure them, and so does
53
+ // the strip under a layout that draws nodes. Only a GBZ cut follows
54
+ // snarls; a walk file cuts the same either way, so asking would re-fetch
55
+ // an identical graph.
56
+ cutsWholeWalksIn(mode: LayoutModeValue, walkStrip = self.walkStrip) {
57
+ const m = layoutModeByValue(mode)
58
+ return (
59
+ self.sourceAdapter?.type === 'GbzBaseSyntenyAdapter' &&
60
+ wholeWalksRule(m, {
61
+ walkStrip: walkStrip && m.drawsNodes,
62
+ host: !!self.host,
63
+ })
64
+ )
65
+ },
52
66
  get cutsWholeWalks() {
53
- return wholeWalksRule(layoutModeByValue(self.chosenLayoutMode), {
54
- walkStrip: self.walkStrip,
55
- host: !!self.host,
56
- })
67
+ return this.cutsWholeWalksIn(self.chosenLayoutMode)
57
68
  },
58
69
  // Where each walk passes the hovered node, or the selected one
59
70
  get walkStripMarks() {
@@ -215,9 +215,10 @@ export function walksThrough(graph: Graph, box: string) {
215
215
  for (const p of graph.paths ?? []) {
216
216
  const walk = walkOf(p.name)
217
217
  if (walk !== graph.referencePath) {
218
+ const through = p.nodeIds.includes(box)
218
219
  for (const member of p.members ?? [walk]) {
219
220
  all.add(member)
220
- if (p.nodeIds.includes(box)) {
221
+ if (through) {
221
222
  here.add(member)
222
223
  }
223
224
  }
@@ -1,7 +1,4 @@
1
- import {
2
- LAYOUT_MODE_VALUES,
3
- layoutModeByValue,
4
- } from '@jbrowse/bandage-core/layoutModes'
1
+ import { LAYOUT_MODE_VALUES } from '@jbrowse/bandage-core/layoutModes'
5
2
  import { BaseViewModel } from '@jbrowse/core/pluggableElementTypes/models'
6
3
  import { getSession } from '@jbrowse/core/util'
7
4
  import { addDisposer, isAlive, types } from '@jbrowse/mobx-state-tree'
@@ -93,9 +90,6 @@ export default function stateModelFactory() {
93
90
  get paneMeasured() {
94
91
  return self.widthMeasured
95
92
  },
96
- get sourceGfaLocation() {
97
- return self.gfaLocation
98
- },
99
93
  get sourceTrack(): AnyConfigurationModel | undefined {
100
94
  return self.loadedTrackId
101
95
  ? self.sessionTrack(self.loadedTrackId)
@@ -225,10 +219,11 @@ export default function stateModelFactory() {
225
219
  self,
226
220
  reaction(
227
221
  () => self.walkStrip,
228
- () => {
222
+ on => {
229
223
  if (
230
224
  self.loadedTrackId &&
231
- !layoutModeByValue(self.chosenLayoutMode).wholeWalks
225
+ self.cutsWholeWalks !==
226
+ self.cutsWholeWalksIn(self.chosenLayoutMode, !on)
232
227
  ) {
233
228
  void self.load()
234
229
  }
@@ -0,0 +1,50 @@
1
+ import type { WalkRowBlock } from '@jbrowse/bandage-core/layout/walkRowLinks'
2
+
3
+ // ggbandage's: the three biggest blocks in colour, the rest a pale grey that
4
+ // still carries its letter
5
+ const BLOCK_COLORS = ['#d4735e', '#d8a94a', '#c98aa4']
6
+ const OTHER_BLOCK = '#e6e6e6'
7
+ const BLOCK_EDGE = '#555'
8
+ const LETTER_FONT = 'bold 11px sans-serif'
9
+ const MIN_LETTER_PX = 12
10
+ const MIN_LETTER_BAR_PX = 9
11
+
12
+ export const blockFill = (block: number) => BLOCK_COLORS[block] ?? OTHER_BLOCK
13
+
14
+ // A, B, ... Z, then AA, AB, ...
15
+ export function blockLetter(block: number): string {
16
+ const letter = String.fromCharCode(65 + (block % 26))
17
+ return block < 26 ? letter : blockLetter(Math.floor(block / 26) - 1) + letter
18
+ }
19
+
20
+ // How many blocks get a colour of their own, for the key
21
+ export const COLORED_BLOCKS = BLOCK_COLORS.length
22
+
23
+ // Each stretch as a box over its row's bar, lettered where it is wide enough
24
+ export function paintWalkRowBlocks(
25
+ ctx: CanvasRenderingContext2D,
26
+ blocks: WalkRowBlock[],
27
+ f: {
28
+ x: (offset: number) => number
29
+ rowCentre: (row: number) => number
30
+ barPx: number
31
+ },
32
+ ) {
33
+ ctx.font = LETTER_FONT
34
+ ctx.textAlign = 'center'
35
+ ctx.textBaseline = 'middle'
36
+ for (const { row, start, bp, block } of blocks) {
37
+ const left = f.x(start)
38
+ const width = f.x(start + bp) - left
39
+ const top = f.rowCentre(row) - f.barPx / 2
40
+ ctx.fillStyle = blockFill(block)
41
+ ctx.fillRect(left, top, width, f.barPx)
42
+ ctx.strokeStyle = BLOCK_EDGE
43
+ ctx.lineWidth = 1
44
+ ctx.strokeRect(left + 0.5, top + 0.5, width - 1, f.barPx - 1)
45
+ if (width >= MIN_LETTER_PX && f.barPx >= MIN_LETTER_BAR_PX) {
46
+ ctx.fillStyle = '#111'
47
+ ctx.fillText(blockLetter(block), left + width / 2, f.rowCentre(row))
48
+ }
49
+ }
50
+ }