jbrowse-plugin-graphgenomeviewer 7.1.0 → 7.1.2

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 (233) hide show
  1. package/README.md +17 -8
  2. package/dist/chunks/AddTrackWorkflow-RPH4ZFSX.js +2 -0
  3. package/dist/chunks/{AddTrackWorkflow-Q6RY23LF.js.map → AddTrackWorkflow-RPH4ZFSX.js.map} +2 -2
  4. package/dist/chunks/{ChooseSamplesDialog-BNVYBGJQ.js → ChooseSamplesDialog-JRN4YLBY.js} +2 -2
  5. package/dist/chunks/{ChooseWalksDialog-EN3C6AQA.js → ChooseWalksDialog-UXLFZTZJ.js} +2 -2
  6. package/dist/chunks/GbzBaseSyntenyAdapter-LRECKL74.js +2 -0
  7. package/dist/chunks/GbzBaseSyntenyAdapter-LRECKL74.js.map +7 -0
  8. package/dist/chunks/{GraphGenomeView-Z2WZKEEO.js → GraphGenomeView-NJ5XCREB.js} +2 -2
  9. package/dist/chunks/GraphTrackSettingsDialog-47PQ6LM7.js +6 -0
  10. package/dist/chunks/GraphTrackSettingsDialog-47PQ6LM7.js.map +7 -0
  11. package/dist/chunks/{HighlightColorDialog-EYBKFSJU.js → HighlightColorDialog-UK5VDDV6.js} +2 -2
  12. package/dist/chunks/{LinearGraphDisplay-WIBVWRQE.js → LinearGraphDisplay-TXTKAACH.js} +2 -2
  13. package/dist/chunks/MinigraphBubbleAdapter-HW7XQMAI.js +2 -0
  14. package/dist/chunks/MinigraphBubbleAdapter-HW7XQMAI.js.map +7 -0
  15. package/dist/chunks/RgfaTabixAdapter-PSKYWD2X.js +3 -0
  16. package/dist/chunks/RgfaTabixAdapter-PSKYWD2X.js.map +7 -0
  17. package/dist/chunks/{TrackWarningsDialog-QT4YEXGD.js → TrackWarningsDialog-TRYBOIHX.js} +2 -2
  18. package/dist/chunks/WalkTabixSyntenyAdapter-U53JLYVU.js +2 -0
  19. package/dist/chunks/WalkTabixSyntenyAdapter-U53JLYVU.js.map +7 -0
  20. package/dist/chunks/chunk-2MCOY3YG.js +3 -0
  21. package/dist/chunks/chunk-2MCOY3YG.js.map +7 -0
  22. package/dist/chunks/{chunk-EWFJBQDV.js → chunk-5O6YRJHL.js} +2 -2
  23. package/dist/chunks/{chunk-EWFJBQDV.js.map → chunk-5O6YRJHL.js.map} +1 -1
  24. package/dist/chunks/chunk-BUY2PV7D.js +2 -0
  25. package/dist/chunks/chunk-BUY2PV7D.js.map +7 -0
  26. package/dist/chunks/chunk-DVCZ3BOC.js +2 -0
  27. package/dist/chunks/chunk-DVCZ3BOC.js.map +7 -0
  28. package/dist/chunks/{chunk-SAMJE6GH.js → chunk-EGDLEQ47.js} +3 -3
  29. package/dist/chunks/{chunk-SAMJE6GH.js.map → chunk-EGDLEQ47.js.map} +4 -4
  30. package/dist/chunks/chunk-FHZXW7IG.js +2 -0
  31. package/dist/chunks/chunk-FHZXW7IG.js.map +7 -0
  32. package/dist/chunks/{chunk-DZ4OUKQV.js → chunk-FITPB5X6.js} +2 -2
  33. package/dist/chunks/{chunk-DZ4OUKQV.js.map → chunk-FITPB5X6.js.map} +1 -1
  34. package/dist/chunks/chunk-FVK2THE4.js +2 -0
  35. package/dist/chunks/chunk-FVK2THE4.js.map +7 -0
  36. package/dist/chunks/{chunk-AHFQHYJI.js → chunk-IBWORBGY.js} +2 -2
  37. package/dist/chunks/chunk-MND7GAOG.js +2 -0
  38. package/dist/chunks/chunk-MND7GAOG.js.map +7 -0
  39. package/dist/chunks/chunk-MURN3JPK.js +2 -0
  40. package/dist/chunks/chunk-MURN3JPK.js.map +7 -0
  41. package/dist/chunks/{chunk-3Q4RBDCL.js → chunk-N6PQZSAX.js} +1 -1
  42. package/dist/chunks/{chunk-3Q4RBDCL.js.map → chunk-N6PQZSAX.js.map} +1 -1
  43. package/dist/chunks/chunk-NWJYLP6Z.js +4 -0
  44. package/dist/chunks/chunk-NWJYLP6Z.js.map +7 -0
  45. package/dist/chunks/{chunk-YJHYBDYF.js → chunk-S75W27NT.js} +2 -2
  46. package/dist/chunks/{chunk-YJHYBDYF.js.map → chunk-S75W27NT.js.map} +1 -1
  47. package/dist/chunks/{chunk-IXQK7JAW.js → chunk-T4HOC6O3.js} +1 -1
  48. package/dist/chunks/{chunk-IXQK7JAW.js.map → chunk-T4HOC6O3.js.map} +1 -1
  49. package/dist/chunks/{chunk-AU73K7LX.js → chunk-TUKJRVBS.js} +1 -1
  50. package/dist/chunks/{chunk-AU73K7LX.js.map → chunk-TUKJRVBS.js.map} +1 -1
  51. package/dist/chunks/chunk-UGCNPRKC.js +2 -0
  52. package/dist/chunks/{chunk-E3WYW7Z6.js.map → chunk-UGCNPRKC.js.map} +2 -2
  53. package/dist/chunks/{chunk-YSTMDXXX.js → chunk-VJ3SO4CH.js} +1 -1
  54. package/dist/chunks/{chunk-YSTMDXXX.js.map → chunk-VJ3SO4CH.js.map} +1 -1
  55. package/dist/chunks/{chunk-XT766CZ5.js → chunk-W6EQLQ6H.js} +1 -1
  56. package/dist/chunks/{chunk-XT766CZ5.js.map → chunk-W6EQLQ6H.js.map} +1 -1
  57. package/dist/chunks/chunk-WAMBI7OX.js +2 -0
  58. package/dist/chunks/chunk-WAMBI7OX.js.map +7 -0
  59. package/dist/chunks/chunk-WCGVUZWP.js +2 -0
  60. package/dist/chunks/chunk-WCGVUZWP.js.map +7 -0
  61. package/dist/chunks/chunk-WEGWTC52.js +2 -0
  62. package/dist/chunks/chunk-WEGWTC52.js.map +7 -0
  63. package/dist/chunks/chunk-YU7T5WOQ.js +2 -0
  64. package/dist/chunks/{chunk-6OPB7FOJ.js.map → chunk-YU7T5WOQ.js.map} +2 -2
  65. package/dist/chunks/{chunk-446RWIA3.js → chunk-ZMOMFU2B.js} +1 -1
  66. package/dist/chunks/{chunk-446RWIA3.js.map → chunk-ZMOMFU2B.js.map} +1 -1
  67. package/dist/chunks/renderSvg-KSQI5DH6.js +2 -0
  68. package/dist/chunks/renderSvg-KSQI5DH6.js.map +7 -0
  69. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +4 -4
  70. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +4 -4
  71. package/package.json +8 -9
  72. package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.test.ts +12 -10
  73. package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.ts +31 -39
  74. package/src/GbzBaseSyntenyAdapter/configSchema.ts +5 -29
  75. package/src/GetGraphHaplotypes.ts +6 -15
  76. package/src/GetGraphReads.ts +7 -14
  77. package/src/GetSubgraph.ts +11 -20
  78. package/src/GraphAddTrackWorkflow/AddTrackWorkflow.test.tsx +73 -0
  79. package/src/GraphAddTrackWorkflow/AddTrackWorkflow.tsx +1 -0
  80. package/src/GraphAddTrackWorkflow/buildTrackConfig.test.ts +3 -2
  81. package/src/GraphAddTrackWorkflow/buildTrackConfig.ts +2 -11
  82. package/src/GraphAddTrackWorkflow/graphManifest.ts +15 -3
  83. package/src/GraphGenomeView/components/BubbleHalos.tsx +5 -24
  84. package/src/GraphGenomeView/components/ElTree.tsx +6 -1
  85. package/src/GraphGenomeView/components/FacetPanels.test.tsx +65 -0
  86. package/src/GraphGenomeView/components/FacetPanels.tsx +17 -12
  87. package/src/GraphGenomeView/components/GenePins.tsx +5 -22
  88. package/src/GraphGenomeView/components/GraphCanvas.tsx +40 -234
  89. package/src/GraphGenomeView/components/GraphSettingsDialog.tsx +102 -108
  90. package/src/GraphGenomeView/components/GraphToolbar.tsx +5 -6
  91. package/src/GraphGenomeView/components/HoverLayer.tsx +3 -7
  92. package/src/GraphGenomeView/components/HoverTooltip.test.tsx +39 -0
  93. package/src/GraphGenomeView/components/HoverTooltip.tsx +122 -0
  94. package/src/GraphGenomeView/components/LabelChip.tsx +3 -5
  95. package/src/GraphGenomeView/components/LabelLayer.tsx +4 -17
  96. package/src/GraphGenomeView/components/LabelledSelect.tsx +1 -1
  97. package/src/GraphGenomeView/components/Legend.tsx +14 -23
  98. package/src/GraphGenomeView/components/PaneContextMenu.test.tsx +48 -0
  99. package/src/GraphGenomeView/components/PaneContextMenu.tsx +144 -0
  100. package/src/GraphGenomeView/components/PaneLayer.tsx +91 -0
  101. package/src/GraphGenomeView/components/PaneSvg.tsx +32 -50
  102. package/src/GraphGenomeView/components/ReferenceStripOverlay.test.tsx +48 -0
  103. package/src/GraphGenomeView/components/ReferenceStripOverlay.tsx +13 -42
  104. package/src/GraphGenomeView/components/SettingsMenu.test.tsx +29 -0
  105. package/src/GraphGenomeView/components/SettingsMenu.tsx +56 -65
  106. package/src/GraphGenomeView/components/TubeMapOverlay.tsx +15 -46
  107. package/src/GraphGenomeView/components/WalkRowsOverlay.tsx +58 -132
  108. package/src/GraphGenomeView/components/WalkStrip.tsx +17 -20
  109. package/src/GraphGenomeView/components/WalkStripLocator.tsx +8 -15
  110. package/src/GraphGenomeView/components/usePaneGestures.test.tsx +35 -0
  111. package/src/GraphGenomeView/components/usePaneGestures.ts +88 -91
  112. package/src/GraphGenomeView/genes/geneFeatures.ts +27 -45
  113. package/src/GraphGenomeView/graphLayers.ts +4 -4
  114. package/src/GraphGenomeView/host.ts +1 -1
  115. package/src/GraphGenomeView/legendSections.test.ts +23 -1
  116. package/src/GraphGenomeView/legendSections.ts +68 -74
  117. package/src/GraphGenomeView/model.test.ts +373 -39
  118. package/src/GraphGenomeView/model.ts +67 -11
  119. package/src/GraphGenomeView/pane/fitViews.ts +57 -514
  120. package/src/GraphGenomeView/pane/graphViews.ts +219 -160
  121. package/src/GraphGenomeView/pane/hostViews.ts +2 -10
  122. package/src/GraphGenomeView/pane/keepInView.ts +5 -4
  123. package/src/GraphGenomeView/pane/launchActions.ts +117 -139
  124. package/src/GraphGenomeView/pane/launchViews.ts +8 -27
  125. package/src/GraphGenomeView/pane/loadActions.ts +208 -187
  126. package/src/GraphGenomeView/pane/morph.ts +3 -9
  127. package/src/GraphGenomeView/pane/paneBase.ts +22 -11
  128. package/src/GraphGenomeView/pane/renderingActions.ts +65 -11
  129. package/src/GraphGenomeView/pane/settingActions.ts +17 -42
  130. package/src/GraphGenomeView/pane/tubeMapViews.ts +445 -0
  131. package/src/GraphGenomeView/pane/walkRowViews.ts +29 -37
  132. package/src/GraphGenomeView/renderPipeline.test.ts +1 -1
  133. package/src/GraphGenomeView/repeats/repeatFeatures.test.ts +23 -0
  134. package/src/GraphGenomeView/repeats/repeatFeatures.ts +16 -23
  135. package/src/GraphGenomeView/repeats/walkCalls.test.ts +18 -0
  136. package/src/GraphGenomeView/repeats/walkCalls.ts +46 -38
  137. package/src/GraphGenomeView/subgraphLoad.test.ts +67 -3
  138. package/src/GraphGenomeView/trackFeatures.ts +24 -0
  139. package/src/GraphGenomeView/tubeMapPanels.ts +28 -36
  140. package/src/GraphGenomeView/viewModel.ts +8 -56
  141. package/src/GraphGenomeView/walkRowGroups.ts +104 -30
  142. package/src/GraphGenomeView/walkRowSpans.test.ts +6 -0
  143. package/src/GraphGenomeView/walkRowSpans.ts +22 -31
  144. package/src/GraphGenomeView/walkRowsCut.ts +8 -17
  145. package/src/LinearGraphDisplay/LinearGraphDisplay.test.ts +173 -26
  146. package/src/LinearGraphDisplay/components/SubgraphContextSelect.test.tsx +1 -1
  147. package/src/LinearGraphDisplay/components/SubgraphContextSelect.tsx +3 -3
  148. package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.test.tsx +3 -3
  149. package/src/LinearGraphDisplay/components/SubgraphHaplotypesField.tsx +7 -10
  150. package/src/LinearGraphDisplay/configSchema.ts +0 -3
  151. package/src/LinearGraphDisplay/denseWindow.ts +8 -11
  152. package/src/LinearGraphDisplay/model.ts +89 -82
  153. package/src/MinigraphBubbleAdapter/MinigraphBubbleAdapter.test.ts +21 -0
  154. package/src/MinigraphBubbleAdapter/MinigraphBubbleAdapter.ts +6 -14
  155. package/src/MinigraphBubbleAdapter/configSchema.ts +3 -9
  156. package/src/MinigraphBubbleAdapter/index.ts +1 -2
  157. package/src/RgfaTabixAdapter/RgfaTabixAdapter.test.ts +45 -1
  158. package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +111 -276
  159. package/src/RgfaTabixAdapter/configSchema.ts +15 -49
  160. package/src/RgfaTabixAdapter/index.ts +1 -2
  161. package/src/RgfaTabixAdapter/rgfaBed.ts +25 -15
  162. package/src/RgfaTabixAdapter/rgfaCut.ts +139 -0
  163. package/src/RgfaTabixAdapter/test_data/walks2_twopart.GRCh38.walks.bed.gz +0 -0
  164. package/src/RgfaTabixAdapter/test_data/walks2_twopart.GRCh38.walks.bed.gz.tbi +0 -0
  165. package/src/RgfaTabixAdapter/walkFiles.test.ts +33 -0
  166. package/src/RgfaTabixAdapter/walkReader.ts +103 -27
  167. package/src/RgfaTabixAdapter/walkRowRuns.test.ts +53 -0
  168. package/src/RgfaTabixAdapter/walkRowRuns.ts +42 -24
  169. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.test.ts +37 -6
  170. package/src/WalkTabixSyntenyAdapter/WalkTabixSyntenyAdapter.ts +23 -45
  171. package/src/WalkTabixSyntenyAdapter/configSchema.ts +11 -24
  172. package/src/WalkTabixSyntenyAdapter/index.ts +1 -2
  173. package/src/WalkTabixSyntenyAdapter/walkLanes.ts +35 -43
  174. package/src/adapterImplementing.ts +27 -0
  175. package/src/graphTrackDefaults/index.ts +14 -20
  176. package/src/hoverSync/lgvHover.ts +2 -4
  177. package/src/launchFromGraph/contributors.ts +1 -5
  178. package/src/launchFromGraph/graphMenuItems.ts +3 -4
  179. package/src/launchFromGraph/launchFromGraph.test.ts +0 -1
  180. package/src/launchFromGraph/launchFromGraph.ts +8 -13
  181. package/src/panSNTabix.test.ts +1 -26
  182. package/src/panSNTabix.ts +54 -51
  183. package/src/synteny/lanePairs.ts +8 -0
  184. package/src/synteny/matchFeature.ts +52 -0
  185. package/src/tabixRange.ts +13 -10
  186. package/src/version.ts +1 -1
  187. package/dist/chunks/AddTrackWorkflow-Q6RY23LF.js +0 -2
  188. package/dist/chunks/GbzBaseSyntenyAdapter-TIXR4Y3J.js +0 -2
  189. package/dist/chunks/GbzBaseSyntenyAdapter-TIXR4Y3J.js.map +0 -7
  190. package/dist/chunks/GraphTrackSettingsDialog-TAM6PMXD.js +0 -6
  191. package/dist/chunks/GraphTrackSettingsDialog-TAM6PMXD.js.map +0 -7
  192. package/dist/chunks/MinigraphBubbleAdapter-AGI2UYQR.js +0 -2
  193. package/dist/chunks/MinigraphBubbleAdapter-AGI2UYQR.js.map +0 -7
  194. package/dist/chunks/RgfaTabixAdapter-2OL5X3HF.js +0 -3
  195. package/dist/chunks/RgfaTabixAdapter-2OL5X3HF.js.map +0 -7
  196. package/dist/chunks/WalkTabixSyntenyAdapter-UKFHCFOF.js +0 -2
  197. package/dist/chunks/WalkTabixSyntenyAdapter-UKFHCFOF.js.map +0 -7
  198. package/dist/chunks/chunk-5SFGHDQA.js +0 -2
  199. package/dist/chunks/chunk-5SFGHDQA.js.map +0 -7
  200. package/dist/chunks/chunk-6OPB7FOJ.js +0 -2
  201. package/dist/chunks/chunk-AMFIBQVR.js +0 -2
  202. package/dist/chunks/chunk-AMFIBQVR.js.map +0 -7
  203. package/dist/chunks/chunk-BJMRBVG3.js +0 -2
  204. package/dist/chunks/chunk-BJMRBVG3.js.map +0 -7
  205. package/dist/chunks/chunk-E3WYW7Z6.js +0 -2
  206. package/dist/chunks/chunk-EHCF6AXH.js +0 -2
  207. package/dist/chunks/chunk-EHCF6AXH.js.map +0 -7
  208. package/dist/chunks/chunk-GOFS7BXK.js +0 -3
  209. package/dist/chunks/chunk-GOFS7BXK.js.map +0 -7
  210. package/dist/chunks/chunk-IJQTSJGT.js +0 -4
  211. package/dist/chunks/chunk-IJQTSJGT.js.map +0 -7
  212. package/dist/chunks/chunk-J6NZIAZF.js +0 -2
  213. package/dist/chunks/chunk-J6NZIAZF.js.map +0 -7
  214. package/dist/chunks/chunk-LUTMKRXY.js +0 -2
  215. package/dist/chunks/chunk-LUTMKRXY.js.map +0 -7
  216. package/dist/chunks/chunk-MHPJB22Z.js +0 -2
  217. package/dist/chunks/chunk-MHPJB22Z.js.map +0 -7
  218. package/dist/chunks/chunk-TXI43PEN.js +0 -2
  219. package/dist/chunks/chunk-TXI43PEN.js.map +0 -7
  220. package/dist/chunks/chunk-WRH2EJKX.js +0 -2
  221. package/dist/chunks/chunk-WRH2EJKX.js.map +0 -7
  222. package/dist/chunks/chunk-WX26KTZM.js +0 -2
  223. package/dist/chunks/chunk-WX26KTZM.js.map +0 -7
  224. package/dist/chunks/renderSvg-ST2GBBOC.js +0 -2
  225. package/dist/chunks/renderSvg-ST2GBBOC.js.map +0 -7
  226. package/src/GraphGenomeView/components/WalkRowContextMenu.tsx +0 -105
  227. /package/dist/chunks/{ChooseSamplesDialog-BNVYBGJQ.js.map → ChooseSamplesDialog-JRN4YLBY.js.map} +0 -0
  228. /package/dist/chunks/{ChooseWalksDialog-EN3C6AQA.js.map → ChooseWalksDialog-UXLFZTZJ.js.map} +0 -0
  229. /package/dist/chunks/{GraphGenomeView-Z2WZKEEO.js.map → GraphGenomeView-NJ5XCREB.js.map} +0 -0
  230. /package/dist/chunks/{HighlightColorDialog-EYBKFSJU.js.map → HighlightColorDialog-UK5VDDV6.js.map} +0 -0
  231. /package/dist/chunks/{LinearGraphDisplay-WIBVWRQE.js.map → LinearGraphDisplay-TXTKAACH.js.map} +0 -0
  232. /package/dist/chunks/{TrackWarningsDialog-QT4YEXGD.js.map → TrackWarningsDialog-TRYBOIHX.js.map} +0 -0
  233. /package/dist/chunks/{chunk-AHFQHYJI.js.map → chunk-IBWORBGY.js.map} +0 -0
@@ -120,6 +120,25 @@ export function linkKey(link: RgfaLink) {
120
120
  return `${link.source}${link.sourceStrand}${link.target}${link.targetStrand}`
121
121
  }
122
122
 
123
+ // gfa-to-tabix writes both endpoints of a link into every row and files the
124
+ // link under each endpoint's stable sequence, so a read covering both returns
125
+ // it twice; linkKey dedupes it. Returns the endpoints the cut lacked.
126
+ export function addLink(
127
+ segments: Map<string, RgfaSegment>,
128
+ links: Map<string, RgfaLink>,
129
+ link: RgfaLink,
130
+ ) {
131
+ links.set(linkKey(link), link)
132
+ const added: RgfaSegment[] = []
133
+ for (const segment of [link.sourceSegment, link.targetSegment]) {
134
+ if (!segments.has(segment.id)) {
135
+ segments.set(segment.id, segment)
136
+ added.push(segment)
137
+ }
138
+ }
139
+ return added
140
+ }
141
+
123
142
  // The cut an anchored index gives: `under` is what the segment file returned
124
143
  // for the window, every segment of every bubble under it, and `found` is what
125
144
  // the link file returned. gfa-to-tabix files a link widely enough that `found`
@@ -148,12 +167,7 @@ export function anchoredCut(
148
167
  under.has(link.target) ||
149
168
  side(link.sourceSegment) * side(link.targetSegment) === -1
150
169
  ) {
151
- links.set(linkKey(link), link)
152
- for (const segment of [link.sourceSegment, link.targetSegment]) {
153
- if (!segments.has(segment.id)) {
154
- segments.set(segment.id, segment)
155
- }
156
- }
170
+ addLink(segments, links, link)
157
171
  }
158
172
  }
159
173
  for (const link of found) {
@@ -164,10 +178,6 @@ export function anchoredCut(
164
178
  return { segments, links }
165
179
  }
166
180
 
167
- // Both endpoints of a link are written into every row, so the same link is
168
- // indexed under each endpoint's stable sequence and arrives twice for a region
169
- // covering both. Callers dedupe on linkKey.
170
-
171
181
  export interface StableSpan {
172
182
  refName: string
173
183
  start: number
@@ -195,12 +205,12 @@ export function closingSpans(
195
205
  return [
196
206
  ...mergedSpans(
197
207
  held.filter(segment => segment.rank > 0),
198
- unread,
208
+ segment => unread.has(segment.id),
199
209
  window.end - window.start,
200
210
  ),
201
211
  ...mergedSpans(
202
212
  held.filter(segment => segment.rank === 0 && outside(segment)),
203
- new Set(held.map(segment => segment.id)),
213
+ () => true,
204
214
  0,
205
215
  ),
206
216
  ]
@@ -208,7 +218,7 @@ export function closingSpans(
208
218
 
209
219
  function mergedSpans(
210
220
  segments: RgfaSegment[],
211
- unread: Set<string>,
221
+ isUnread: (segment: RgfaSegment) => boolean,
212
222
  maxGap: number,
213
223
  ) {
214
224
  const sorted = [...segments].sort(
@@ -221,14 +231,14 @@ function mergedSpans(
221
231
  span?.refName === segment.refName &&
222
232
  segment.start - span.end <= maxGap
223
233
  ) {
224
- span.needed ||= segment.start > span.end || unread.has(segment.id)
234
+ span.needed ||= segment.start > span.end || isUnread(segment)
225
235
  span.end = Math.max(span.end, segment.end)
226
236
  } else {
227
237
  spans.push({
228
238
  refName: segment.refName,
229
239
  start: segment.start,
230
240
  end: segment.end,
231
- needed: unread.has(segment.id),
241
+ needed: isUnread(segment),
232
242
  })
233
243
  }
234
244
  }
@@ -0,0 +1,139 @@
1
+ import {
2
+ addLink,
3
+ anchoredCut,
4
+ closingLinks,
5
+ closingSpans,
6
+ formatSubgraph,
7
+ parseLinkLine,
8
+ parseSegmentRow,
9
+ } from './rgfaBed.ts'
10
+ import { getLines } from '../tabixRange.ts'
11
+
12
+ import type { RgfaLink, RgfaSegment, StableSpan } from './rgfaBed.ts'
13
+ import type { TabixIndexedFile } from '@gmod/tabix'
14
+
15
+ // What a hop follows: alleles, never the backbone. A rank-0 segment reached
16
+ // through a link is flanking backbone, usually outside the window, so querying
17
+ // its links walks the reference away from the region, brings back more backbone,
18
+ // and closes nothing. Every segment that closes a bubble is an allele interior,
19
+ // and an allele interior is rank > 0 in both index formats (rGFA's SR tag, and
20
+ // pggb's rank 0-or-1 against the anchor path). Reached backbone still becomes a
21
+ // node and its edge still draws; it is only not walked through.
22
+ //
23
+ // This is gfabase's rule, arrived at from the other end. `gfabase sub` expands
24
+ // from a seed either to the whole connected component (`--connected`, which its
25
+ // own README calls overkill) or with `--cutpoints 1`, which takes the biconnected
26
+ // component by *stopping at cutpoints*, the segments every end-to-end walk of
27
+ // the chromosome must traverse. In a reference-anchored pangenome the cutpoints
28
+ // are the backbone, and rank is what the index already states about it, so
29
+ // `rank > 0` is that stopping rule without a connectivity index to build. The
30
+ // third tool is the same shape again: `gfatools view -R` maps the region onto its
31
+ // precomputed bubble decomposition and returns whole overlapping bubbles
32
+ // (gfa_query_by_reg), which is complete for the same reason. Bandage's own
33
+ // `--distance` is a node-step radius, but Bandage holds the entire graph in
34
+ // memory, so its scope is a drawing filter rather than a fetch strategy.
35
+ //
36
+ // Measured with this rule: the E. coli paa window converges at one hop (17
37
+ // segments at 1 and at 2), so the cut closes and stays closed. HPRC's amylase
38
+ // window keeps growing (63 at 0, 78 at 1, 92 at 2) because its alleles have
39
+ // alleles, and wall-clock is flat across all three, since a hop's queries go out
40
+ // together and the remote index dominates.
41
+ //
42
+ // The exact version, when a graph ships a bubble index beside its segments:
43
+ // gfatools' bubble rows carry the member segment ids of every bubble, so the
44
+ // bubbles overlapping the region state which segments a complete cut must
45
+ // contain. That turns the hop count into a termination condition rather than a
46
+ // setting, and MinigraphBubbleAdapter already parses those rows.
47
+ function offReference(segments: RgfaSegment[]) {
48
+ return segments.filter(segment => segment.rank > 0)
49
+ }
50
+
51
+ export interface RgfaFiles {
52
+ segments: TabixIndexedFile
53
+ links: TabixIndexedFile
54
+ }
55
+
56
+ async function linksOver(
57
+ file: TabixIndexedFile,
58
+ span: StableSpan,
59
+ signal?: AbortSignal,
60
+ ) {
61
+ const found: RgfaLink[] = []
62
+ await getLines(file, span.refName, span.start, span.end, {
63
+ signal,
64
+ lineCallback: line => {
65
+ found.push(parseLinkLine(line))
66
+ },
67
+ })
68
+ return found
69
+ }
70
+
71
+ /**
72
+ * The graph around a window of one stable sequence, as GFA text. The window's
73
+ * own segments come from the segment file; the link file supplies the edges
74
+ * incident to them *and* the coordinates of the segments on the other end,
75
+ * which typically sit on a different stable sequence (a rank>0 bubble) and so
76
+ * are not reachable by any coordinate query on this window.
77
+ *
78
+ * `hops` is extra rounds of link-following past the window's own segments and
79
+ * their immediate neighbours, each costing one tabix query per off-reference
80
+ * segment newly reached. After the last round, one more read per stretch of
81
+ * stable sequence the cut holds closes it (closingSpans), so an allele reached
82
+ * from both ends draws whole. The view asks for one hop; a hopless cut stays
83
+ * as cheap as it was. `signal` goes to every one of those queries, so a cut
84
+ * the view has replaced stops.
85
+ */
86
+ export async function rgfaCut(
87
+ files: RgfaFiles,
88
+ window: StableSpan,
89
+ { hops = 0, signal }: { hops?: number; signal?: AbortSignal } = {},
90
+ ) {
91
+ const segments = new Map<string, RgfaSegment>()
92
+ const links = new Map<string, RgfaLink>()
93
+ const layout = { anchored: false }
94
+ await getLines(files.segments, window.refName, window.start, window.end, {
95
+ signal,
96
+ lineCallback: line => {
97
+ const row = parseSegmentRow(line)
98
+ layout.anchored ||= row.anchored
99
+ segments.set(row.segment.id, row.segment)
100
+ },
101
+ })
102
+ // An anchored index files every allele and every link under the reference
103
+ // interval its bubble hangs from, so one read of each file holds the whole
104
+ // graph under the window and there is nothing to hop to or close.
105
+ if (layout.anchored) {
106
+ const cut = anchoredCut(
107
+ segments,
108
+ await linksOver(files.links, window, signal),
109
+ window,
110
+ )
111
+ return formatSubgraph(cut.segments, cut.links)
112
+ }
113
+ const addLinksOver = async (span: StableSpan) =>
114
+ (await linksOver(files.links, span, signal)).flatMap(link =>
115
+ addLink(segments, links, link),
116
+ )
117
+ let frontier = offReference(await addLinksOver(window))
118
+ for (let hop = 0; hop < hops; hop++) {
119
+ // One hop's queries are independent of each other, so they go out
120
+ // together rather than one round-trip at a time, and the output is sorted
121
+ // at the end regardless of the order they arrive in.
122
+ const reached = await Promise.all(frontier.map(addLinksOver))
123
+ frontier = offReference(reached.flat())
124
+ }
125
+ if (hops > 0) {
126
+ const spans = closingSpans(
127
+ [...segments.values()],
128
+ new Set(frontier.map(segment => segment.id)),
129
+ window,
130
+ )
131
+ const found = await Promise.all(
132
+ spans.map(span => linksOver(files.links, span, signal)),
133
+ )
134
+ for (const link of closingLinks(found.flat(), segments, spans)) {
135
+ addLink(segments, links, link)
136
+ }
137
+ }
138
+ return formatSubgraph(segments, links)
139
+ }
@@ -33,6 +33,10 @@ const fixture = (name: string) =>
33
33
  // W HG00097 1 chr1 >1>2>3>4>5>6
34
34
  const headerPrefix = fixture('walks2_header')
35
35
 
36
+ // walks2_header's walks with the reference named as pggb names a P-line,
37
+ // GRCh38#chr1
38
+ const twoPartPrefix = fixture('walks2_twopart')
39
+
36
40
  // An unchopped graph, node 1 spanning 0-2600 and filed under chunk 0 alone:
37
41
  // W GRCh38 0 chr1 >1>2>4
38
42
  // W HG002 1 chr1 >1>3>4 3 replaces 2
@@ -193,6 +197,13 @@ test("the haplotypes a walk file's header names", async () => {
193
197
  expect(gfa).not.toMatch(/^S\t8\t/m)
194
198
  })
195
199
 
200
+ test('a cut for some haplotypes keeps a reference walk named without a haplotype', async () => {
201
+ const cut = (await makeAdapter(twoPartPrefix).getSubgraph(chr1(1000, 2600), {
202
+ haplotypes: ['HG002#1'],
203
+ })) as GraphTables
204
+ expect(cut.walks.names).toEqual(['GRCh38#chr1', 'HG002#1#chr1'])
205
+ })
206
+
196
207
  test('a format 1 walk file is refused, with what to rebuild it with', async () => {
197
208
  const cut = makeAdapter(fixture('walks1_header')).getSubgraph(chr1(0, 600))
198
209
  await expect(cut).rejects.toThrow(/rebuild it with gfa-to-tabix 1.0/)
@@ -228,6 +239,9 @@ test('a cut or a feature read over a range that is not finite queries nothing',
228
239
  await expect(
229
240
  firstValueFrom(adapter.getFeatures(region).pipe(toArray())),
230
241
  ).rejects.toThrow(/not a finite range/)
242
+ await expect(adapter.getRegionByteSize([region])).rejects.toThrow(
243
+ /not a finite range/,
244
+ )
231
245
  expect(getLines).not.toHaveBeenCalled()
232
246
  expect(bytes).not.toHaveBeenCalled()
233
247
  getLines.mockRestore()
@@ -247,6 +261,25 @@ test("a node longer than a chunk is read back as far as the header's maxnode rea
247
261
  expect(walkNames(gfa)).toEqual(['GRCh38#0#chr1', 'HG002#1#chr1'])
248
262
  })
249
263
 
264
+ test("a walk-indexed graph's features are the reference nodes overlapping the window", async () => {
265
+ const features = await firstValueFrom(
266
+ makeAdapter(headerPrefix).getFeatures(chr1(1000, 2300)).pipe(toArray()),
267
+ )
268
+ expect(features.map(f => f.get('name'))).toEqual(['2', '3', '4'])
269
+ })
270
+
271
+ test('the byte gate measures a walk file from the chunk before the window', async () => {
272
+ const bytes = vi.spyOn(TabixIndexedFile.prototype, 'bytesForRegions')
273
+ const size = await makeAdapter(headerPrefix).getRegionByteSize([
274
+ chr1(2100, 2600),
275
+ ])
276
+ expect(size).toBeGreaterThan(0)
277
+ expect(bytes.mock.calls.map(call => call[0])).toEqual([
278
+ [{ refName: 'GRCh38#0#chr1', start: 1000, end: 2600 }],
279
+ ])
280
+ bytes.mockRestore()
281
+ })
282
+
250
283
  test('getFeatures reads from the chunk before the window', async () => {
251
284
  const getLines = vi.spyOn(TabixIndexedFile.prototype, 'getLines')
252
285
  await firstValueFrom(
@@ -1,9 +1,15 @@
1
- import { panSNHaplotype } from '@jbrowse/bandage-core/pansn'
2
- import { walkNameFilter } from '@jbrowse/graph-walks'
1
+ import { resolvePanSNPrefix } from '@jbrowse/bandage-core/pansn'
2
+ import { WalkReader, referenceNodes, walksKept } from '@jbrowse/graph-walks'
3
3
 
4
+ import { getLines } from '../tabixRange.ts'
4
5
  import { walkCutFor } from './walkRowRuns.ts'
5
6
 
6
- import type { WalkReader } from '@jbrowse/graph-walks'
7
+ import type { SubgraphAdapterOptions } from '../GetSubgraph.ts'
8
+ import type { TabixIndexedFile } from '@gmod/tabix'
9
+ import type {
10
+ BaseFeatureDataAdapter,
11
+ BaseOptions,
12
+ } from '@jbrowse/core/data_adapters/BaseAdapter'
7
13
 
8
14
  // The zoom-in notices name the config slots that set the budgets
9
15
  export const BUDGET_SLOTS = {
@@ -12,29 +18,99 @@ export const BUDGET_SLOTS = {
12
18
  }
13
19
 
14
20
  /**
15
- * The graph display's cut of a walk-indexed window: the reference walk, the
16
- * walks of `prefixes` (every walk when undefined), and the nodes and links
17
- * they visit. Walk rows draw runs, so the step budget spares them.
18
- * @jbrowse/graph-walks reads and cuts the window.
21
+ * A walk file and the answers both walk-indexed adapters give from it,
22
+ * RgfaTabixAdapter and WalkTabixSyntenyAdapter, whose schemas declare the
23
+ * budget slots. @jbrowse/graph-walks reads and cuts the file.
19
24
  */
20
- export async function walkSubgraph(
21
- reader: WalkReader,
22
- refName: string,
23
- region: { start: number; end: number },
24
- opts: {
25
- signal?: AbortSignal
26
- prefixes?: string[]
27
- walkRows?: boolean
28
- stepBudget: number
29
- byteBudget: number
30
- },
31
- ) {
32
- const { graph, kept, fragments } = await reader.cut(refName, region, {
33
- signal: opts.signal,
34
- keep: walkNameFilter(opts.prefixes, panSNHaplotype(refName)),
35
- stepBudget: opts.walkRows ? Infinity : opts.stepBudget,
36
- byteBudget: opts.byteBudget,
37
- budgetNames: BUDGET_SLOTS,
38
- })
39
- return walkCutFor(graph.tables(kept, fragments), region, opts.walkRows)
25
+ export class WalkIndex {
26
+ readonly reader: WalkReader
27
+
28
+ constructor(
29
+ readonly file: TabixIndexedFile,
30
+ private adapter: BaseFeatureDataAdapter,
31
+ ) {
32
+ this.reader = new WalkReader(file)
33
+ }
34
+
35
+ /**
36
+ * The haplotypes the header names, `sample#haplotype` in byte order, or
37
+ * undefined when it names none
38
+ */
39
+ async haplotypeNames(opts: BaseOptions = {}) {
40
+ const { haplotypes } = await this.reader.header(opts)
41
+ return haplotypes.length > 0 ? haplotypes : undefined
42
+ }
43
+
44
+ budgets() {
45
+ return {
46
+ stepBudget: this.adapter.getConf('walkStepBudget') as number,
47
+ byteBudget: this.adapter.getConf('walkByteBudget') as number,
48
+ budgetNames: BUDGET_SLOTS,
49
+ }
50
+ }
51
+
52
+ /**
53
+ * The graph display's cut of a window: the reference walk, the walks of
54
+ * `haplotypes` (every walk when unset), and the nodes and links they visit.
55
+ * Walk rows draw runs, so the step budget spares them.
56
+ */
57
+ async subgraph(
58
+ refName: string,
59
+ region: { start: number; end: number },
60
+ { signal, haplotypes, walkRows }: SubgraphAdapterOptions,
61
+ ) {
62
+ const budgets = this.budgets()
63
+ const { graph, kept, fragments } = await this.reader.cut(refName, region, {
64
+ ...budgets,
65
+ stepBudget: walkRows ? Infinity : budgets.stepBudget,
66
+ signal,
67
+ keep: walksKept(
68
+ haplotypes?.map(lane => resolvePanSNPrefix(this.adapter, lane)),
69
+ refName,
70
+ ),
71
+ })
72
+ return walkCutFor(graph.tables(kept, fragments), region, walkRows)
73
+ }
74
+
75
+ /**
76
+ * The reference nodes overlapping a window, from the ref rows of its
77
+ * chunks. A row's tabix interval is its chunk's first base, and gfa-to-tabix
78
+ * files a node crossing in from a chunk before under that chunk, so the read
79
+ * starts at queryStart.
80
+ */
81
+ async referenceNodes(
82
+ refName: string,
83
+ region: { start: number; end: number },
84
+ opts: BaseOptions,
85
+ ) {
86
+ const refRows: string[][] = []
87
+ await getLines(
88
+ this.file,
89
+ refName,
90
+ await this.reader.queryStart(region.start, opts),
91
+ region.end,
92
+ {
93
+ signal: opts.signal,
94
+ lineCallback: line => {
95
+ const cols = line.split('\t')
96
+ if (cols[3] === 'ref') {
97
+ refRows.push(cols)
98
+ }
99
+ },
100
+ },
101
+ )
102
+ const seen = new Set<number>()
103
+ return referenceNodes(refRows).filter(node => {
104
+ if (
105
+ node.refName !== refName ||
106
+ node.end <= region.start ||
107
+ node.start >= region.end ||
108
+ seen.has(node.id)
109
+ ) {
110
+ return false
111
+ }
112
+ seen.add(node.id)
113
+ return true
114
+ })
115
+ }
40
116
  }
@@ -99,3 +99,56 @@ test('walkRows measures and stops the same walks', () => {
99
99
  ).rows.find(r => r.label === 'E#1')!
100
100
  expect(short.stop).toEqual({ contigEnds: true, shortBp: 500 })
101
101
  })
102
+
103
+ test('walks named without a haplotype field are rows of their own, on their contigs', () => {
104
+ const twoPart: GraphTables = {
105
+ ...tables,
106
+ walks: {
107
+ ...tables.walks,
108
+ names: ['GRCh38#0#chr1', 'A#a', 'A#b', 'C#1#c', 'D#1#d', 'E#1#e'],
109
+ },
110
+ }
111
+ const rows = walkRowsOf(walkRowRuns(twoPart, repeat)!).rows
112
+ expect(
113
+ rows
114
+ .filter(r => r.sample === 'A')
115
+ .map(({ name, label, haplotype, axis }) => ({
116
+ name,
117
+ label,
118
+ haplotype,
119
+ contig: axis?.contig,
120
+ }))
121
+ .sort((x, y) => x.name.localeCompare(y.name)),
122
+ ).toEqual([
123
+ { name: 'A#a', label: 'A', haplotype: undefined, contig: 'a' },
124
+ { name: 'A#b', label: 'A', haplotype: undefined, contig: 'b' },
125
+ ])
126
+ })
127
+
128
+ test('a reference cut into pieces is one reference row, as walkRows reads it', () => {
129
+ const split: GraphTables = {
130
+ ...tables,
131
+ walks: {
132
+ names: ['GRCh38#0#chr1', 'GRCh38#0#chr1', 'A#1#a'],
133
+ starts: Float64Array.of(0, 3000, 0),
134
+ ends: Float64Array.of(2000, 5000, 5000),
135
+ offsets: Int32Array.of(0, 2, 4, 9),
136
+ steps: Int32Array.of(...[0, 1], ...[3, 4], ...[0, 1, 2, 3, 4]),
137
+ reversed: new Uint8Array(9),
138
+ },
139
+ }
140
+ const region = { start: 2200, end: 2800 }
141
+ const fields = ({ label, bp, offReferenceBp, complete }: WalkRow) => ({
142
+ label,
143
+ bp,
144
+ offReferenceBp,
145
+ complete,
146
+ })
147
+ const ours = walkRowsOf(walkRowRuns(split, region)!)
148
+ const graph = loadGraph(split, 'cut')
149
+ const theirs = walkRows({ ...graph, referencePath: 'GRCh38#0#chr1' }, region)!
150
+ expect(ours.rows.map(fields)).toEqual(theirs.rows.map(fields))
151
+ expect(fields(ours.reference)).toEqual(fields(theirs.reference))
152
+ expect(ours.rows.map(r => r.label)).toEqual(['A#1'])
153
+ expect(ours.rows[0]).toMatchObject({ bp: 1000, complete: true })
154
+ })
@@ -1,3 +1,9 @@
1
+ import {
2
+ panSNContig,
3
+ panSNHaplotype,
4
+ panSNSample,
5
+ } from '@jbrowse/bandage-core/pansn'
6
+
1
7
  import type { GraphTables } from '@jbrowse/bandage-core/gfa/graphTables'
2
8
  import type {
3
9
  WalkAxis,
@@ -44,16 +50,23 @@ export interface WalkRowRuns {
44
50
  flags: Uint8Array
45
51
  }
46
52
 
47
- function walkFields(panSN: string) {
48
- const [sample = '', haplotype = '', ...contig] = panSN.split('#')
49
- return { sample, haplotype: +haplotype, contig: contig.join('#') }
53
+ /**
54
+ * A walk's PanSN fields: `HG002#1#chr1`, or a pggb P-line's `chm13#chr1`,
55
+ * which has no haplotype
56
+ */
57
+ export function walkPath(name: string) {
58
+ const sample = panSNSample(name)
59
+ const haplotype = panSNHaplotype(name)
60
+ return {
61
+ name,
62
+ sample,
63
+ haplotype:
64
+ haplotype === undefined ? undefined : +haplotype.slice(sample.length + 1),
65
+ contig: panSNContig(name),
66
+ }
50
67
  }
51
68
 
52
- interface Path {
53
- name: string
54
- sample: string
55
- haplotype: number
56
- contig: string
69
+ type Path = ReturnType<typeof walkPath> & {
57
70
  start: number
58
71
  from: number
59
72
  to: number
@@ -84,11 +97,13 @@ class RunWriter {
84
97
 
85
98
  /**
86
99
  * `walkRows(graphFromTables(tables), region)` for a cut whose first walk is
87
- * the reference, which every walk-file and GBZ cut is; a step is a node index. A gap between two pieces of one haplotype is a
88
- * step of `-bp`. One difference: a walk reaching only one of the region's
89
- * flanks measures from that flank, where walkRows takes the whole walk the
90
- * cut holds, so its length is a lower bound that no longer grows with the
91
- * cut's context.
100
+ * the reference, which every walk-file and GBZ cut is, in as many pieces as
101
+ * the cut breaks it into; a step is a node index. A gap between two pieces of
102
+ * one haplotype is a step of `-bp`. Two differences: a walk reaching only one of the region's flanks measures from
103
+ * that flank, where walkRows takes the whole walk the cut holds, so its length
104
+ * is a lower bound that no longer grows with the cut's context; and a row
105
+ * keeps its walk's name whole, where graphFromTables reads a two-part name
106
+ * (`chm13#chr1`) as a haplotype `chr1` with no contig.
92
107
  */
93
108
  export function walkRowRuns(
94
109
  { nodes: n, walks: w }: GraphTables,
@@ -99,19 +114,19 @@ export function walkRowRuns(
99
114
  const paths: Path[] = []
100
115
  for (let p = 0; p < w.names.length; p++) {
101
116
  if (w.offsets[p + 1]! > w.offsets[p]!) {
102
- const { sample, haplotype, contig } = walkFields(w.names[p]!)
103
117
  paths.push({
104
- name: `${sample}#${haplotype}#${contig}`,
105
- sample,
106
- haplotype,
107
- contig,
118
+ ...walkPath(w.names[p]!),
108
119
  start: w.starts[p]!,
109
120
  from: w.offsets[p]!,
110
121
  to: w.offsets[p + 1]!,
111
122
  })
112
123
  }
113
124
  }
114
- const [first, ...others] = paths
125
+ const referencePieces = paths
126
+ .filter(p => p.name === paths[0]?.name)
127
+ .sort((a, b) => a.start - b.start)
128
+ const first = referencePieces[0]
129
+ const others = paths.filter(p => p.name !== first?.name)
115
130
  if (!first || others.length === 0) {
116
131
  return undefined
117
132
  }
@@ -130,9 +145,9 @@ export function walkRowRuns(
130
145
  let flankedBefore = false
131
146
  let flankedAfter = false
132
147
  const cut = region && region.end > region.start ? region : undefined
133
- {
134
- let pos = first.start
135
- for (let s = first.from; s < first.to; s++) {
148
+ for (const piece of referencePieces) {
149
+ let pos = piece.start
150
+ for (let s = piece.from; s < piece.to; s++) {
136
151
  const node = w.steps[s]!
137
152
  const len = lengthOf(node)
138
153
  if (Number.isNaN(spanStart[node]!)) {
@@ -345,7 +360,10 @@ export function walkRowRuns(
345
360
  out.endRow()
346
361
  return {
347
362
  name: path.name,
348
- label: `${path.sample}#${path.haplotype}`,
363
+ label:
364
+ path.haplotype === undefined
365
+ ? path.sample
366
+ : `${path.sample}#${path.haplotype}`,
349
367
  sample: path.sample,
350
368
  haplotype: path.haplotype,
351
369
  bp,
@@ -357,7 +375,7 @@ export function walkRowRuns(
357
375
  }
358
376
  }
359
377
 
360
- const reference = rowOf([first])
378
+ const reference = rowOf(referencePieces)
361
379
  const rowList = [...walks.values()].map(pieces => ({
362
380
  meta: rowOf(pieces),
363
381
  row: out.offsets.length - 2,