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
@@ -143,3 +143,21 @@ test('a phased allele pairs with its own haplotype, not by length', () => {
143
143
  })
144
144
  expect(lone[0]!.call).toBeUndefined()
145
145
  })
146
+
147
+ // A haplotype's walk broken across two contigs makes two rows of one
148
+ // haplotype, and its phased allele goes to the one walk across the array.
149
+ test("a phased allele goes to one of its haplotype's walks", () => {
150
+ const piece = (bp: number, complete: boolean) => ({
151
+ ...walk('HG1', bp, complete),
152
+ haplotype: 1,
153
+ })
154
+ const call = { bp: 3100, spanningReads: 4, haplotype: 1 }
155
+ const rows = withCalls([piece(900, false), piece(3161, true)], {
156
+ HG1: calls(call),
157
+ })
158
+ expect(rows.map(r => r.call?.bp)).toEqual([undefined, 3100])
159
+ const both = withCalls([piece(400, true), piece(3161, true)], {
160
+ HG1: calls(call),
161
+ })
162
+ expect(both.map(r => r.call?.agrees)).toEqual([undefined, true])
163
+ })
@@ -14,7 +14,7 @@ import type { RepeatCall } from './repeatFeatures'
14
14
  // the array take the allele that fit one spanning it, and the spanning walk
15
15
  // then read as a disagreement.
16
16
 
17
- export interface WalkCall extends RepeatCall {
17
+ interface WalkCall extends RepeatCall {
18
18
  // undefined where the walk and the allele are not both measurements
19
19
  agrees?: boolean
20
20
  }
@@ -74,50 +74,53 @@ function scored(row: { bp: number; complete: boolean }, call: RepeatCall) {
74
74
  }
75
75
  }
76
76
 
77
- // A phased genotype names its walk's haplotype outright. The rest go by
78
- // length: the walks that span the array against the alleles reads spanned,
79
- // then everything else against what those two left. That decides the
80
- // ambiguous cases on merit: which walk carries the one allele a lone spanning
81
- // read supports, and which allele a walk that stops inside the array is drawn
82
- // against.
77
+ // A phased genotype names its walk's haplotype outright, though a contig
78
+ // break can give that haplotype two walks. Within a haplotype, and among the
79
+ // unphased alleles, pairing goes by length: the walks that span the array
80
+ // against the alleles reads spanned, then everything else against what those
81
+ // two left. That decides the ambiguous cases on merit: which walk takes the
82
+ // one allele a lone spanning read supports, and which allele a walk that stops
83
+ // inside the array is drawn against.
83
84
  function pairSample<
84
85
  R extends { bp: number; complete: boolean; haplotype?: number },
85
86
  >(rows: R[], calls: RepeatCall[]) {
86
87
  const out = new Map<number, WalkCall>()
87
88
  const taken = new Set<RepeatCall>()
88
- rows.forEach((row, i) => {
89
- const call = calls.find(
90
- c => c.haplotype !== undefined && c.haplotype === row.haplotype,
91
- )
92
- if (call) {
93
- taken.add(call)
94
- out.set(i, scored(row, call))
95
- }
96
- })
97
- const unphased = calls.filter(call => call.haplotype === undefined)
98
89
  const ranked = byLength(rows, r => r.bp)
99
- const stages = [
100
- [ranked.filter(r => r.item.complete), unphased.filter(backed)],
101
- [ranked, unphased],
102
- ] as const
103
- for (const [pool, group] of stages) {
104
- const free = pool.filter(r => !out.has(r.i))
105
- const open = byLength(
106
- group.filter(call => !taken.has(call)),
107
- call => call.bp,
90
+ const pair = (pool: typeof ranked, group: RepeatCall[]) => {
91
+ const stages = [
92
+ [pool.filter(r => r.item.complete), group.filter(backed)],
93
+ [pool, group],
94
+ ] as const
95
+ for (const [walks, alleles] of stages) {
96
+ const free = walks.filter(r => !out.has(r.i))
97
+ const open = byLength(
98
+ alleles.filter(call => !taken.has(call)),
99
+ call => call.bp,
100
+ )
101
+ nearest(
102
+ free.map(r => r.item.bp),
103
+ open.map(c => c.item.bp),
104
+ ).forEach((pick, k) => {
105
+ if (pick !== undefined) {
106
+ const row = free[k]!
107
+ const call = open[pick]!.item
108
+ taken.add(call)
109
+ out.set(row.i, scored(row.item, call))
110
+ }
111
+ })
112
+ }
113
+ }
114
+ for (const haplotype of new Set(calls.flatMap(c => c.haplotype ?? []))) {
115
+ pair(
116
+ ranked.filter(r => r.item.haplotype === haplotype),
117
+ calls.filter(c => c.haplotype === haplotype),
108
118
  )
109
- nearest(
110
- free.map(r => r.item.bp),
111
- open.map(c => c.item.bp),
112
- ).forEach((pick, k) => {
113
- if (pick !== undefined) {
114
- const row = free[k]!
115
- const call = open[pick]!.item
116
- taken.add(call)
117
- out.set(row.i, scored(row.item, call))
118
- }
119
- })
120
119
  }
120
+ pair(
121
+ ranked,
122
+ calls.filter(c => c.haplotype === undefined),
123
+ )
121
124
  return out
122
125
  }
123
126
 
@@ -135,7 +138,12 @@ export function withCalls<
135
138
  const paired = new Map<R, WalkCall>()
136
139
  const bySample = new Map<string, R[]>()
137
140
  for (const row of rows) {
138
- bySample.set(row.sample, [...(bySample.get(row.sample) ?? []), row])
141
+ const group = bySample.get(row.sample)
142
+ if (group) {
143
+ group.push(row)
144
+ } else {
145
+ bySample.set(row.sample, [row])
146
+ }
139
147
  }
140
148
  for (const [sample, group] of bySample) {
141
149
  const called = calls[sample]
@@ -7,7 +7,8 @@ const mockSession = {
7
7
  tracks: [] as Record<string, unknown>[],
8
8
  rpcManager: { call: mockRpcCall },
9
9
  assemblyManager: {
10
- has: () => false,
10
+ has: (_name: string) => false,
11
+ get: (_name: string) => undefined as { name: string } | undefined,
11
12
  assemblyList: [] as { name: string; aliases: string[] }[],
12
13
  },
13
14
  }
@@ -41,6 +42,11 @@ vi.mock(import('@jbrowse/core/configuration'), async importOriginal => ({
41
42
  ),
42
43
  }))
43
44
 
45
+ const mockReadFile = vi.hoisted(() => vi.fn())
46
+ vi.mock('@jbrowse/core/util/io', () => ({
47
+ openLocation: () => ({ readFile: mockReadFile }),
48
+ }))
49
+
44
50
  // pangenome_hprc's segments track once NA20809 haplotype 2 is loaded
45
51
  const TRACK = {
46
52
  trackId: 'segments',
@@ -98,6 +104,8 @@ async function cut(region: typeof ON_HG38, gfa = GFA) {
98
104
  beforeEach(() => {
99
105
  mockRpcCall.mockReset()
100
106
  mockSession.tracks = [TRACK]
107
+ mockSession.assemblyManager.has = () => false
108
+ mockSession.assemblyManager.get = () => undefined
101
109
  mockSession.assemblyManager.assemblyList = []
102
110
  })
103
111
 
@@ -116,6 +124,17 @@ test('a cut on another assembly the track names is refused, naming the reference
116
124
  expect(mockRpcCall).not.toHaveBeenCalled()
117
125
  })
118
126
 
127
+ test("a track naming its reference by an alias of the region's assembly cuts it", async () => {
128
+ mockSession.tracks = [{ ...TRACK, assemblyNames: ['GRCh38', 'NA20809.2'] }]
129
+ const hg38 = { name: 'hg38', aliases: ['GRCh38'] }
130
+ const known = (name: string) => name === 'hg38' || name === 'GRCh38'
131
+ mockSession.assemblyManager.has = known
132
+ mockSession.assemblyManager.get = name => (known(name) ? hg38 : undefined)
133
+ const model = await cut(ON_HG38)
134
+ expect(model.error).toBeUndefined()
135
+ expect(model.nodeCount).toBe(3)
136
+ })
137
+
119
138
  test('a cut that finds no segments is an error, not an empty graph', async () => {
120
139
  const model = await cut(ON_HG38, 'H\tVN:Z:1.0')
121
140
  expect(String(model.error)).toMatch(/No graph segments/)
@@ -300,11 +319,11 @@ describe('reads', () => {
300
319
  secondary: false,
301
320
  }
302
321
 
303
- async function cutWithReads(reads: () => Promise<unknown>) {
322
+ async function cutWithReads(reads: () => Promise<unknown>, gfa = PATHS) {
304
323
  mockSession.tracks = [READS_TRACK]
305
324
  mockRpcCall.mockImplementation((_sid: unknown, method: string) =>
306
325
  method === 'GetSubgraph'
307
- ? Promise.resolve(PATHS)
326
+ ? Promise.resolve(gfa)
308
327
  : method === 'GetGraphReads'
309
328
  ? reads()
310
329
  : Promise.reject(new Error(`Unexpected RPC: ${method}`)),
@@ -343,6 +362,51 @@ describe('reads', () => {
343
362
  expect(model.colorSchemeLock?.value).toBe('By strand')
344
363
  })
345
364
 
365
+ // the panels draw no reads, so the boxes take the scheme picked
366
+ test('a tube map split into panels colours its boxes beside reads', async () => {
367
+ const model = await cutWithReads(
368
+ () => Promise.resolve({ records: [READ], total: 1 }),
369
+ PATHS.replace('P\talt', 'P\tA#1#c').concat('\nP\tB#1#c\t1+,2+,4+\t*'),
370
+ )
371
+ expect(model.tubeMapReads).toBe(true)
372
+ model.setFacet('sample')
373
+ await model.recomputeLayout()
374
+ model.setColorScheme('reference-position')
375
+ expect(model.layoutResult?.tubeMapPanels).toHaveLength(2)
376
+ expect(model.tubeMapReads).toBe(false)
377
+ expect(model.colorSchemeLock).toBeUndefined()
378
+ expect(model.effectiveColorScheme).toBe('reference-position')
379
+ })
380
+
381
+ // the reads take the reds and blues, so the tubes keep their greys, which
382
+ // the key names walk by walk
383
+ test('beside reads a tube colour column leaves the key naming each walk', async () => {
384
+ const model = await cutWithReads(() =>
385
+ Promise.resolve({ records: [READ], total: 1 }),
386
+ )
387
+ mockSession.tracks = [
388
+ {
389
+ ...READS_TRACK,
390
+ adapter: {
391
+ ...READS_TRACK.adapter,
392
+ samplesTsvLocation: {
393
+ uri: 'samples.tsv',
394
+ locationType: 'UriLocation',
395
+ },
396
+ },
397
+ },
398
+ ]
399
+ mockReadFile.mockResolvedValue('name\tpopulation\nalt\tYRI\n')
400
+ await model.loadWalkRowSampleTable()
401
+ model.setTubeMapColorBy('population')
402
+ expect(model.tubeMapColoring?.field).toBe('population')
403
+ expect(model.tubeMapKeys.values).toBeUndefined()
404
+ expect(model.pathLegend.map(e => [e.name, e.color])).toEqual([
405
+ ['hg38', model.tubeMapTubeColors![0]],
406
+ ['alt', model.tubeMapTubeColors![1]],
407
+ ])
408
+ })
409
+
346
410
  // A tube map draws the window alone, so its ramp key speaks for its boxes:
347
411
  // h0 is an allele before the window that the tube map trims away
348
412
  test("a tube map's ramp key names no node the window trimmed away", async () => {
@@ -0,0 +1,24 @@
1
+ import type { Feature } from '@jbrowse/core/util'
2
+
3
+ export type FeatureLike = Feature | Record<string, unknown>
4
+
5
+ export function ownField(f: FeatureLike, name: string): unknown {
6
+ return typeof (f as Feature).get === 'function'
7
+ ? (f as Feature).get(name)
8
+ : (f as Record<string, unknown>)[name]
9
+ }
10
+
11
+ export interface TrackChoice {
12
+ trackId: string
13
+ name?: string
14
+ adapterType: string
15
+ }
16
+
17
+ export function namedTrack<T extends TrackChoice>(tracks: T[], named: string) {
18
+ return tracks.find(t => t.trackId === named)
19
+ }
20
+
21
+ // what a track-name hint such as /repeat|tandem/ is matched against
22
+ export function trackLabel(t: TrackChoice) {
23
+ return `${t.trackId} ${t.name ?? ''}`
24
+ }
@@ -4,6 +4,8 @@ import { pathOrigin } from '@jbrowse/bandage-core/pathAnchoring'
4
4
  import { drawTubeMapHighlight } from '@jbrowse/bandage-core/tubeMap/draw'
5
5
  import { groupKeyComparator } from '@jbrowse/core/util/groupKeys'
6
6
 
7
+ import { columnValueOf, groupsByColumn, plural } from './walkRowGroups'
8
+
7
9
  import type { SampleRow } from './walkRowGroups'
8
10
  import type { FacetBy } from '@jbrowse/bandage-core/facetGrid'
9
11
  import type { TubeMapDrawing } from '@jbrowse/bandage-core/layout/tubeMapLayout'
@@ -115,57 +117,47 @@ export function valueTubeColors(
115
117
  })
116
118
  }
117
119
 
118
- // A walk's value in a column: its haplotype's row (`HG00097#1`), else its
119
- // sample's, as groupWalkRows reads it
120
+ // A PanSN walk's value in a column
120
121
  export function columnKeyOf(field: string, table: SampleRow[]) {
121
- const valueOf = new Map(table.map(row => [row.name, row[field] ?? '']))
122
+ const valueOf = columnValueOf(field, table)
122
123
  return (walk: string) => {
123
124
  const { sample, haplotype } = parsePanSN(walk)
124
- return valueOf.get(`${sample}#${haplotype}`) || valueOf.get(sample) || ''
125
+ return valueOf(`${sample}#${haplotype}`, sample)
125
126
  }
126
127
  }
127
128
 
128
- function plural(n: number, noun: string) {
129
- return `${n} ${noun}${n === 1 ? '' : 's'}`
130
- }
131
-
132
129
  // Each non-reference walk's group
133
130
  export function tubeMapPanelGroups(
134
131
  graph: Graph,
135
132
  split: TubeMapPanelSplit,
136
133
  ): TubeMapPanelGroup[] {
134
+ const names = (graph.paths ?? [])
135
+ .map(p => p.name)
136
+ .filter(name => walkOf(name) !== graph.referencePath)
137
+ if (split.by === 'column') {
138
+ const keyOf = columnKeyOf(split.field, split.table)
139
+ return groupsByColumn(
140
+ names,
141
+ name => keyOf(walkOf(name)),
142
+ split,
143
+ 'sample',
144
+ name => parsePanSN(walkOf(name)).sample,
145
+ ).map(({ key, members, title }) => ({ key, paths: members, label: title }))
146
+ }
137
147
  const keyOf =
138
- split.by === 'column'
139
- ? columnKeyOf(split.field, split.table)
140
- : split.by === 'sample'
141
- ? (walk: string) => parsePanSN(walk).sample
142
- : (walk: string) => walk
148
+ split.by === 'sample'
149
+ ? (walk: string) => parsePanSN(walk).sample
150
+ : (walk: string) => walk
143
151
  const groups = new Map<string, string[]>()
144
- for (const { name } of graph.paths ?? []) {
145
- const walk = walkOf(name)
146
- if (walk !== graph.referencePath) {
147
- const key = keyOf(walk)
148
- const list = groups.get(key)
149
- if (list) {
150
- list.push(name)
151
- } else {
152
- groups.set(key, [name])
153
- }
152
+ for (const name of names) {
153
+ const key = keyOf(walkOf(name))
154
+ const list = groups.get(key)
155
+ if (list) {
156
+ list.push(name)
157
+ } else {
158
+ groups.set(key, [name])
154
159
  }
155
160
  }
156
- if (split.by === 'column') {
157
- return [...groups.keys()]
158
- .sort(groupKeyComparator(split.domain))
159
- .map(key => {
160
- const paths = groups.get(key)!
161
- const samples = new Set(paths.map(p => parsePanSN(walkOf(p)).sample))
162
- return {
163
- key,
164
- paths,
165
- label: `${key || `${split.field}: none`} · ${plural(samples.size, 'sample')}`,
166
- }
167
- })
168
- }
169
161
  const domain = split.domain ?? []
170
162
  const rank = new Map(domain.map((key, i) => [key, i]))
171
163
  const listed = (key: string) => rank.get(key) ?? domain.length
@@ -1,4 +1,3 @@
1
- import { cutsWholeWalks as wholeWalksRule } from '@jbrowse/bandage-core/layout/walkStrip'
2
1
  import {
3
2
  LAYOUT_MODE_VALUES,
4
3
  layoutModeByValue,
@@ -8,20 +7,9 @@ import { getSession } from '@jbrowse/core/util'
8
7
  import { addDisposer, types } from '@jbrowse/mobx-state-tree'
9
8
  import { reaction } from 'mobx'
10
9
 
11
- import {
12
- GraphPaneMixin,
13
- MAX_GRAPH_REGION_BP,
14
- fileName,
15
- formatSpanBp,
16
- } from './model'
17
- import {
18
- configHaplotypes,
19
- graphReferenceAssembly,
20
- offReferenceProblem,
21
- } from '../graphTrackConfig'
22
10
  import { HOVER_HIGHLIGHT_VALUES } from './hoverHighlight'
23
11
  import { lenientMaybeEnum } from './lenientEnum'
24
- import { trackAdapterConfig } from '../panSNAliases/trackAdapterConfig'
12
+ import { GraphPane, fileName, formatSpanBp } from './model'
25
13
 
26
14
  import type { GraphLayers } from './graphLayers'
27
15
  import type { NodeColor } from './nodeColor'
@@ -39,7 +27,7 @@ export default function stateModelFactory() {
39
27
  .compose(
40
28
  'GraphGenomeView',
41
29
  BaseViewModel,
42
- GraphPaneMixin(),
30
+ GraphPane,
43
31
  types.model({
44
32
  type: types.literal('GraphGenomeView'),
45
33
  // a whole GFA the view loads on attach, kept so the source round-trips
@@ -51,9 +39,6 @@ export default function stateModelFactory() {
51
39
  // sessions and the docs' figure specs state it. Following the linear
52
40
  // view is the track's job (LinearGraphDisplay).
53
41
  loadedTrackId: types.optional(types.string, ''),
54
- subgraphContext: types.optional(types.number, 1),
55
- subgraphHaplotypes: types.maybe(types.frozen<string[]>()),
56
- maxRegionBp: types.optional(types.number, MAX_GRAPH_REGION_BP),
57
42
  // unset is force
58
43
  layoutMode: lenientMaybeEnum(LAYOUT_MODE_VALUES),
59
44
  color: types.maybe(types.frozen<NodeColor>()),
@@ -107,16 +92,6 @@ export default function stateModelFactory() {
107
92
  get paneMeasured() {
108
93
  return self.widthMeasured
109
94
  },
110
- // the set stated, else the source track's defaultHaplotypes
111
- get cutHaplotypes() {
112
- const track = this.sourceTrack
113
- return (
114
- self.subgraphHaplotypes ??
115
- (track
116
- ? configHaplotypes(trackAdapterConfig(self, track))
117
- : undefined)
118
- )
119
- },
120
95
  get sourceGfaLocation() {
121
96
  return self.gfaLocation
122
97
  },
@@ -163,15 +138,10 @@ export default function stateModelFactory() {
163
138
  if (!self.loadedTrackId || !region) {
164
139
  return
165
140
  }
166
- const track = self.sourceTrack
141
+ const adapter = self.sourceAdapter
167
142
  const span = region.end - region.start
168
- const problem = track
169
- ? offReferenceProblem(
170
- graphReferenceAssembly(track),
171
- region.assemblyName,
172
- )
173
- : undefined
174
- if (!track) {
143
+ const problem = self.offReference(region)
144
+ if (!adapter) {
175
145
  self.setError(
176
146
  new Error(
177
147
  `The track this graph was cut from, "${self.loadedTrackId}", is not in this session`,
@@ -186,16 +156,7 @@ export default function stateModelFactory() {
186
156
  ),
187
157
  )
188
158
  } else {
189
- return self.cutSubgraph(trackAdapterConfig(self, track), region, {
190
- hops: self.subgraphContext,
191
- haplotypes: self.cutHaplotypes,
192
- ...(self.chosenLayoutMode === 'walkrows'
193
- ? { walkRows: true }
194
- : {}),
195
- ...(self.cutsWholeWalks
196
- ? { snarls: 'overlapping' as const }
197
- : {}),
198
- })
159
+ return self.cutSubgraph(adapter, region, self.cutOptions)
199
160
  }
200
161
  return undefined
201
162
  },
@@ -205,18 +166,9 @@ export default function stateModelFactory() {
205
166
  retryLoad() {
206
167
  void self.load()
207
168
  },
208
- // walk rows, and the strip of them under a node layout, measure whole
209
- // walks, which a GBZ cut only follows when asked, so a change in whether
210
- // the drawing needs them cuts the track again; the strip's toggle does
211
- // the same (afterAttach)
169
+ // the strip's toggle cuts the track again too (afterAttach)
212
170
  relayoutFrom(from: LayoutModeValue) {
213
- const before = wholeWalksRule(layoutModeByValue(from), {
214
- walkStrip: self.walkStrip,
215
- host: !!self.host,
216
- })
217
- return self.loadedTrackId &&
218
- (self.cutsWholeWalks !== before ||
219
- (from === 'walkrows') !== (self.chosenLayoutMode === 'walkrows'))
171
+ return self.loadedTrackId && self.cutChangedFrom(from)
220
172
  ? self.load()
221
173
  : self.recomputeLayout()
222
174
  },
@@ -1,5 +1,12 @@
1
+ import { walkRowsTree } from '@jbrowse/bandage-core/layout/walkRowDraw'
1
2
  import { groupKeyComparator } from '@jbrowse/core/util/groupKeys'
2
3
 
4
+ import type { El } from '@jbrowse/bandage-core/el'
5
+ import type {
6
+ WalkRowsFrame,
7
+ WalkRowsWithCalls,
8
+ } from '@jbrowse/bandage-core/layout/walkRowDraw'
9
+
3
10
  // Walk rows stacked into one section per value of a sample table's column,
4
11
  // as jbrowse-plugin-tandem-repeat's "Group by…" stacks its alleles. Placement
5
12
  // here, not in bandage-core's walkRowsTree, which puts row i at i * rowPx.
@@ -71,46 +78,71 @@ export function groupByOf(value: unknown): WalkRowGroupBy | undefined {
71
78
  : { field }
72
79
  }
73
80
 
74
- function haplotypes(rows: { label: string }[]) {
75
- const n = new Set(rows.map(r => r.label)).size
76
- return `${n} haplotype${n === 1 ? '' : 's'}`
81
+ export function plural(n: number, noun: string) {
82
+ return `${n} ${noun}${n === 1 ? '' : 's'}`
77
83
  }
78
84
 
79
- // The rows reordered section by section, each keeping the order it came in.
80
- // A row's value is its table row's, looked up by haplotype (`HG00097#1`)
81
- // and then by sample (`HG00097`); rows with none stack last.
82
- export function groupWalkRows<R extends { sample: string; label: string }>(
83
- rows: R[],
84
- table: SampleRow[],
85
- { field, domain }: WalkRowGroupBy,
86
- ) {
85
+ // A walk's value in a column: its haplotype's row (`HG00097#1`), else its
86
+ // sample's (`HG00097`), else ''
87
+ export function columnValueOf(field: string, table: SampleRow[]) {
87
88
  const valueOf = new Map(table.map(row => [row.name, row[field] ?? '']))
88
- const groups = new Map<string, R[]>()
89
- for (const row of rows) {
90
- const key = valueOf.get(row.label) || valueOf.get(row.sample) || ''
89
+ return (haplotype: string, sample: string) =>
90
+ valueOf.get(haplotype) || valueOf.get(sample) || ''
91
+ }
92
+
93
+ // Items grouped by their value in a column, each group in the order its items
94
+ // came in, the groups in the order a JBrowse Group by sorts them, and each
95
+ // titled with its value and how many distinct `noun`s its items are
96
+ export function groupsByColumn<T>(
97
+ items: readonly T[],
98
+ valueOf: (item: T) => string,
99
+ { field, domain }: { field: string; domain?: readonly string[] },
100
+ noun: string,
101
+ nounOf: (item: T) => string,
102
+ ) {
103
+ const groups = new Map<string, T[]>()
104
+ for (const item of items) {
105
+ const key = valueOf(item)
91
106
  const group = groups.get(key)
92
107
  if (group) {
93
- group.push(row)
108
+ group.push(item)
94
109
  } else {
95
- groups.set(key, [row])
110
+ groups.set(key, [item])
96
111
  }
97
112
  }
113
+ return [...groups.keys()].sort(groupKeyComparator(domain)).map(key => {
114
+ const members = groups.get(key)!
115
+ const n = new Set(members.map(nounOf)).size
116
+ return {
117
+ key,
118
+ members,
119
+ title: `${key || `${field}: none`} · ${plural(n, noun)}`,
120
+ }
121
+ })
122
+ }
123
+
124
+ // The rows reordered section by section; rows with no value stack last
125
+ export function groupWalkRows<R extends { sample: string; label: string }>(
126
+ rows: R[],
127
+ table: SampleRow[],
128
+ groupBy: WalkRowGroupBy,
129
+ ) {
130
+ const valueOf = columnValueOf(groupBy.field, table)
131
+ const groups = groupsByColumn(
132
+ rows,
133
+ row => valueOf(row.label, row.sample),
134
+ groupBy,
135
+ 'haplotype',
136
+ row => row.label,
137
+ )
98
138
  let first = 1
99
- const sections: WalkRowSection[] = [...groups.keys()]
100
- .sort(groupKeyComparator(domain))
101
- .map(key => {
102
- const members = groups.get(key)!
103
- const section = {
104
- key,
105
- title: `${key || `${field}: none`} · ${haplotypes(members)}`,
106
- first,
107
- count: members.length,
108
- }
109
- first += members.length
110
- return section
111
- })
139
+ const sections: WalkRowSection[] = groups.map(({ key, members, title }) => {
140
+ const section = { key, title, first, count: members.length }
141
+ first += members.length
142
+ return section
143
+ })
112
144
  return {
113
- rows: sections.flatMap(s => groups.get(s.key)!),
145
+ rows: groups.flatMap(g => g.members),
114
146
  sections,
115
147
  }
116
148
  }
@@ -156,3 +188,45 @@ export function sectionPlacement(sections: WalkRowSection[], rowPx: number) {
156
188
  },
157
189
  }
158
190
  }
191
+
192
+ // The rows' trees, a section at a time while grouped. walkRowsTree puts its
193
+ // i-th row at i * rowPx below its reference row and draws that row too, so
194
+ // each section is drawn shifted down to its place with its reference row
195
+ // dropped.
196
+ export function walkRowTrees(
197
+ bars: WalkRowsWithCalls,
198
+ sections: WalkRowSection[],
199
+ rowY: (i: number) => number,
200
+ frame: WalkRowsFrame & { rowPx: number },
201
+ o: Parameters<typeof walkRowsTree>[2] & { idPrefix: string },
202
+ ): El[] {
203
+ if (sections.length === 0) {
204
+ return [walkRowsTree(bars, frame, o)]
205
+ }
206
+ return [
207
+ walkRowsTree({ ...bars, rows: [] }, frame, o),
208
+ ...sections.flatMap((section, k) => {
209
+ const tree = walkRowsTree(
210
+ {
211
+ ...bars,
212
+ rows: bars.rows.slice(
213
+ section.first - 1,
214
+ section.first - 1 + section.count,
215
+ ),
216
+ },
217
+ {
218
+ ...frame,
219
+ translateY:
220
+ frame.translateY +
221
+ (rowY(section.first) - frame.rowPx) * frame.scaleY,
222
+ },
223
+ { ...o, idPrefix: `${o.idPrefix}-s${k}` },
224
+ )
225
+ return tree.children.filter(
226
+ (c): c is El =>
227
+ typeof c !== 'string' &&
228
+ c.attrs['data-testid'] !== 'graph-walk-reference',
229
+ )
230
+ }),
231
+ ]
232
+ }
@@ -121,3 +121,9 @@ test('a solid run under the ramp is one piece in its own hue', () => {
121
121
  const off = bars.rows[0]!.runs[1]!
122
122
  expect(runPieces(off, ramp, frame.scaleX)).toHaveLength(1)
123
123
  })
124
+
125
+ test('the spans state how far the longest row reaches', () => {
126
+ const all = [bars.reference, ...bars.rows]
127
+ expect(walkRowSpans(all, frame.scaleX).longest).toBe(3500)
128
+ expect(walkRowSpans([], frame.scaleX).longest).toBe(1)
129
+ })