jbrowse-plugin-graphgenomeviewer 1.0.0 → 3.0.3

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 (283) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +202 -24
  3. package/dist/chunks/AddTrackWorkflow-Y3OBHM33.js +2 -0
  4. package/dist/chunks/AddTrackWorkflow-Y3OBHM33.js.map +7 -0
  5. package/dist/chunks/GbzBaseSyntenyAdapter-EFIOI2SN.js +8 -0
  6. package/dist/chunks/GbzBaseSyntenyAdapter-EFIOI2SN.js.map +7 -0
  7. package/dist/chunks/GraphGenomeView-UD5WCV3Q.js +14 -0
  8. package/dist/chunks/GraphGenomeView-UD5WCV3Q.js.map +7 -0
  9. package/dist/chunks/MinigraphBubbleAdapter-B75VQK7T.js +2 -0
  10. package/dist/chunks/MinigraphBubbleAdapter-B75VQK7T.js.map +7 -0
  11. package/dist/chunks/RgfaTabixAdapter-IT7I5RR3.js +3 -0
  12. package/dist/chunks/RgfaTabixAdapter-IT7I5RR3.js.map +7 -0
  13. package/dist/chunks/bandage-layout-FIOZ7B3E.js +1660 -0
  14. package/dist/chunks/bandage-layout-FIOZ7B3E.js.map +7 -0
  15. package/dist/chunks/chunk-D7XFR2IV.js +2 -0
  16. package/dist/chunks/chunk-D7XFR2IV.js.map +7 -0
  17. package/dist/chunks/chunk-F4C3BEPV.js +2 -0
  18. package/dist/chunks/chunk-F4C3BEPV.js.map +7 -0
  19. package/dist/chunks/chunk-FCWXYIFC.js +2 -0
  20. package/dist/chunks/chunk-FCWXYIFC.js.map +7 -0
  21. package/dist/chunks/chunk-FY3BTKY4.js +2 -0
  22. package/dist/chunks/chunk-FY3BTKY4.js.map +7 -0
  23. package/dist/chunks/chunk-J4OLMLS5.js +2 -0
  24. package/dist/chunks/chunk-J4OLMLS5.js.map +7 -0
  25. package/dist/chunks/chunk-JCG7RLMB.js +2 -0
  26. package/dist/chunks/chunk-JCG7RLMB.js.map +7 -0
  27. package/dist/chunks/chunk-KWAXLUPQ.js +2 -0
  28. package/dist/chunks/chunk-KWAXLUPQ.js.map +7 -0
  29. package/dist/chunks/chunk-LGKM2PZH.js +2 -0
  30. package/dist/chunks/chunk-LGKM2PZH.js.map +7 -0
  31. package/dist/chunks/chunk-TN6FK5UD.js +2 -0
  32. package/dist/chunks/chunk-TN6FK5UD.js.map +7 -0
  33. package/dist/chunks/chunk-X3X74OGB.js +2 -0
  34. package/dist/chunks/chunk-X3X74OGB.js.map +7 -0
  35. package/dist/chunks/chunk-XEJ47DLD.js +2 -0
  36. package/dist/chunks/chunk-XEJ47DLD.js.map +7 -0
  37. package/dist/chunks/chunk-ZNGDWZ6Z.js +2 -0
  38. package/dist/chunks/chunk-ZNGDWZ6Z.js.map +7 -0
  39. package/dist/chunks/executeDiagonalize-XMPEDHVZ.js +2 -0
  40. package/dist/chunks/executeDiagonalize-XMPEDHVZ.js.map +7 -0
  41. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js +3 -0
  42. package/dist/jbrowse-plugin-graphgenomeviewer.esm.js.map +7 -0
  43. package/package.json +94 -49
  44. package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.test.ts +427 -0
  45. package/src/GbzBaseSyntenyAdapter/GbzBaseSyntenyAdapter.ts +461 -0
  46. package/src/GbzBaseSyntenyAdapter/configSchema.ts +181 -0
  47. package/src/GbzBaseSyntenyAdapter/index.ts +27 -0
  48. package/src/GbzBaseSyntenyAdapter/test_data/micb-kir3dl1.gbz.db +0 -0
  49. package/src/GbzBaseSyntenyAdapter/test_data/micb-kir3dl1.haplotype-index.db +0 -0
  50. package/src/GetSubgraph.test.ts +193 -0
  51. package/src/GetSubgraph.ts +93 -0
  52. package/src/GraphAddTrackWorkflow/AddTrackWorkflow.tsx +167 -0
  53. package/src/GraphAddTrackWorkflow/buildTrackConfig.test.ts +150 -0
  54. package/src/GraphAddTrackWorkflow/buildTrackConfig.ts +114 -0
  55. package/src/GraphAddTrackWorkflow/index.ts +20 -0
  56. package/src/GraphComputeLayout.ts +46 -0
  57. package/src/GraphGenomeView/anchoredNodes.ts +101 -0
  58. package/src/GraphGenomeView/bubbleSpreads.ts +102 -0
  59. package/src/GraphGenomeView/bubbles/bubbleHalos.test.ts +58 -0
  60. package/src/GraphGenomeView/bubbles/bubbleHalos.ts +149 -0
  61. package/src/GraphGenomeView/bubbles/bubbleRoutes.test.ts +141 -0
  62. package/src/GraphGenomeView/bubbles/bubblesFromGraph.test.ts +239 -0
  63. package/src/GraphGenomeView/bubbles/bubblesFromGraph.ts +210 -0
  64. package/src/GraphGenomeView/bubbles/classifyBubble.test.ts +172 -0
  65. package/src/GraphGenomeView/bubbles/classifyBubble.ts +138 -0
  66. package/src/GraphGenomeView/bubbles/popBubble.test.ts +73 -0
  67. package/src/GraphGenomeView/bubbles/popBubble.ts +26 -0
  68. package/src/GraphGenomeView/colorSchemes.ts +36 -0
  69. package/src/GraphGenomeView/components/BubbleHalos.tsx +162 -0
  70. package/src/GraphGenomeView/components/BubbleOverlay.tsx +181 -0
  71. package/src/GraphGenomeView/components/ColorSchemeSelect.tsx +41 -0
  72. package/src/GraphGenomeView/components/GenePins.tsx +107 -0
  73. package/src/GraphGenomeView/components/GraphCanvas.tsx +759 -0
  74. package/src/GraphGenomeView/components/GraphGenomeView.test.tsx +77 -0
  75. package/src/GraphGenomeView/components/GraphGenomeView.tsx +18 -17
  76. package/src/GraphGenomeView/components/GraphLoadStatus.tsx +84 -0
  77. package/src/GraphGenomeView/components/GraphSettingsDialog.tsx +327 -0
  78. package/src/GraphGenomeView/components/GraphStats.tsx +82 -0
  79. package/src/GraphGenomeView/components/GraphToolbar.tsx +84 -0
  80. package/src/GraphGenomeView/components/ImportForm.tsx +124 -0
  81. package/src/GraphGenomeView/components/LabelChip.tsx +65 -0
  82. package/src/GraphGenomeView/components/LayoutSelect.tsx +56 -0
  83. package/src/GraphGenomeView/components/RepeatSelect.tsx +51 -0
  84. package/src/GraphGenomeView/components/SettingsMenu.tsx +82 -0
  85. package/src/GraphGenomeView/components/SubgraphContextSelect.tsx +75 -0
  86. package/src/GraphGenomeView/components/SubgraphHaplotypesField.tsx +86 -0
  87. package/src/GraphGenomeView/components/WalkRowsOverlay.tsx +252 -0
  88. package/src/GraphGenomeView/components/WalkSelect.tsx +51 -0
  89. package/src/GraphGenomeView/components/overlayLabels.test.ts +56 -0
  90. package/src/GraphGenomeView/components/overlayLabels.ts +75 -0
  91. package/src/GraphGenomeView/deletionEdges.test.ts +198 -0
  92. package/src/GraphGenomeView/deletionEdges.ts +217 -0
  93. package/src/GraphGenomeView/genes/geneFeatures.ts +107 -0
  94. package/src/GraphGenomeView/genes/genePins.ts +130 -0
  95. package/src/GraphGenomeView/genes/genes.test.ts +153 -0
  96. package/src/GraphGenomeView/gfa/gfaConverter.test.ts +394 -0
  97. package/src/GraphGenomeView/gfa/gfaConverter.ts +235 -0
  98. package/src/GraphGenomeView/graphLabels.budget.test.ts +36 -0
  99. package/src/GraphGenomeView/graphLabels.test.ts +548 -0
  100. package/src/GraphGenomeView/graphLabels.ts +640 -0
  101. package/src/GraphGenomeView/index.ts +29 -7
  102. package/src/GraphGenomeView/laneRamp.test.ts +97 -0
  103. package/src/GraphGenomeView/laneRamp.ts +87 -0
  104. package/src/GraphGenomeView/launchKeys.test.ts +63 -0
  105. package/src/GraphGenomeView/layout/anchoredLayout.test.ts +213 -0
  106. package/src/GraphGenomeView/layout/anchoredLayout.ts +80 -0
  107. package/src/GraphGenomeView/layout/bubbleCrossing.test.ts +190 -0
  108. package/src/GraphGenomeView/layout/drawnScale.ts +199 -0
  109. package/src/GraphGenomeView/layout/mergeRuns.test.ts +100 -0
  110. package/src/GraphGenomeView/layout/mergeRuns.ts +203 -0
  111. package/src/GraphGenomeView/layout/orderedLayout.test.ts +212 -0
  112. package/src/GraphGenomeView/layout/orderedLayout.ts +396 -0
  113. package/src/GraphGenomeView/layout/orientToReference.test.ts +110 -0
  114. package/src/GraphGenomeView/layout/orientToReference.ts +79 -0
  115. package/src/GraphGenomeView/layout/placeOffReference.ts +147 -0
  116. package/src/GraphGenomeView/layout/referenceSeeds.test.ts +79 -0
  117. package/src/GraphGenomeView/layout/referenceSeeds.ts +89 -0
  118. package/src/GraphGenomeView/layout/rowSpacing.ts +5 -0
  119. package/src/GraphGenomeView/layout/sampleRowLayout.test.ts +318 -0
  120. package/src/GraphGenomeView/layout/sampleRowLayout.ts +134 -0
  121. package/src/GraphGenomeView/layout/variantMapLayout.test.ts +59 -0
  122. package/src/GraphGenomeView/layout/variantMapLayout.ts +20 -0
  123. package/src/GraphGenomeView/layout/walkRowLayout.ts +54 -0
  124. package/src/GraphGenomeView/layout/walkRows.test.ts +178 -0
  125. package/src/GraphGenomeView/layout/walkRows.ts +177 -0
  126. package/src/GraphGenomeView/layoutModes.ts +131 -0
  127. package/src/GraphGenomeView/model.test.ts +2696 -0
  128. package/src/GraphGenomeView/model.ts +2672 -94
  129. package/src/GraphGenomeView/nodeWidths.ts +67 -0
  130. package/src/GraphGenomeView/pathAnchoring.test.ts +252 -0
  131. package/src/GraphGenomeView/pathAnchoring.ts +164 -0
  132. package/src/GraphGenomeView/pathColors.test.ts +83 -0
  133. package/src/GraphGenomeView/pathColors.ts +122 -0
  134. package/src/GraphGenomeView/referenceSpan.test.ts +84 -0
  135. package/src/GraphGenomeView/referenceSpan.ts +133 -0
  136. package/src/GraphGenomeView/renderPasses.bench.ts +172 -0
  137. package/src/GraphGenomeView/renderPipeline.test.ts +226 -0
  138. package/src/GraphGenomeView/renderer/Canvas2DRenderer.test.ts +205 -0
  139. package/src/GraphGenomeView/renderer/Canvas2DRenderer.ts +246 -0
  140. package/src/GraphGenomeView/renderer/GeometryBuilder.test.ts +723 -0
  141. package/src/GraphGenomeView/renderer/GeometryBuilder.ts +882 -0
  142. package/src/GraphGenomeView/renderer/GraphRenderer.ts +13 -0
  143. package/src/GraphGenomeView/renderer/colorBits.ts +43 -0
  144. package/src/GraphGenomeView/renderer/recordingCanvas.ts +64 -0
  145. package/src/GraphGenomeView/renderer/types.ts +84 -0
  146. package/src/GraphGenomeView/renderer/walkFade.test.ts +100 -0
  147. package/src/GraphGenomeView/repeats/repeatFeatures.test.ts +114 -0
  148. package/src/GraphGenomeView/repeats/repeatFeatures.ts +202 -0
  149. package/src/GraphGenomeView/repeats/test_data/abca7_trgt_walks.json +575 -0
  150. package/src/GraphGenomeView/repeats/walkCalls.test.ts +127 -0
  151. package/src/GraphGenomeView/repeats/walkCalls.ts +132 -0
  152. package/src/GraphGenomeView/stress.test.ts +214 -0
  153. package/src/GraphGenomeView/subgraphLoad.test.ts +245 -0
  154. package/src/GraphGenomeView/types.ts +141 -0
  155. package/src/GraphGenomeView/util/SpatialIndex.test.ts +73 -0
  156. package/src/GraphGenomeView/util/SpatialIndex.ts +312 -0
  157. package/src/GraphGenomeView/util/edgeCurves.test.ts +93 -0
  158. package/src/GraphGenomeView/util/edgeCurves.ts +94 -0
  159. package/src/GraphGenomeView/util/geometry.test.ts +447 -0
  160. package/src/GraphGenomeView/util/geometry.ts +656 -0
  161. package/src/GraphGenomeView/util/hitDetection.test.ts +307 -0
  162. package/src/GraphGenomeView/util/hitDetection.ts +330 -0
  163. package/src/GraphGenomeView/util/wheelZoom.test.ts +40 -0
  164. package/src/GraphGenomeView/util/wheelZoom.ts +26 -0
  165. package/src/GraphGenomeView/walkHighlight.test.ts +54 -0
  166. package/src/GraphGenomeView/walkHighlight.ts +56 -0
  167. package/src/LaunchGraphGenomeView.ts +46 -0
  168. package/src/MinigraphBubbleAdapter/MinigraphBubbleAdapter.test.ts +139 -0
  169. package/src/MinigraphBubbleAdapter/MinigraphBubbleAdapter.ts +86 -0
  170. package/src/MinigraphBubbleAdapter/bubbleLine.ts +92 -0
  171. package/src/MinigraphBubbleAdapter/configSchema.ts +109 -0
  172. package/src/MinigraphBubbleAdapter/index.ts +22 -0
  173. package/src/MinigraphBubbleAdapter/test_data/hprc_bubbles_mhc.bed.gz +0 -0
  174. package/src/MinigraphBubbleAdapter/test_data/hprc_bubbles_mhc.bed.gz.tbi +0 -0
  175. package/src/RgfaTabixAdapter/RgfaTabixAdapter.test.ts +285 -0
  176. package/src/RgfaTabixAdapter/RgfaTabixAdapter.ts +194 -0
  177. package/src/RgfaTabixAdapter/bubbleTier.test.ts +84 -0
  178. package/src/RgfaTabixAdapter/configSchema.ts +142 -0
  179. package/src/RgfaTabixAdapter/index.ts +24 -0
  180. package/src/RgfaTabixAdapter/rgfaBed.test.ts +158 -0
  181. package/src/RgfaTabixAdapter/rgfaBed.ts +156 -0
  182. package/src/RgfaTabixAdapter/test_data/bubble_tier_chrY.links.bed.gz +0 -0
  183. package/src/RgfaTabixAdapter/test_data/bubble_tier_chrY.links.bed.gz.tbi +0 -0
  184. package/src/RgfaTabixAdapter/test_data/bubble_tier_chrY.segs.bed.gz +0 -0
  185. package/src/RgfaTabixAdapter/test_data/bubble_tier_chrY.segs.bed.gz.tbi +0 -0
  186. package/src/RgfaTabixAdapter/test_data/pggb_ecoli.links.bed.gz +0 -0
  187. package/src/RgfaTabixAdapter/test_data/pggb_ecoli.links.bed.gz.tbi +0 -0
  188. package/src/RgfaTabixAdapter/test_data/pggb_ecoli.segs.bed.gz +0 -0
  189. package/src/RgfaTabixAdapter/test_data/pggb_ecoli.segs.bed.gz.tbi +0 -0
  190. package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.links.bed.gz +0 -0
  191. package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.links.bed.gz.tbi +0 -0
  192. package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.segs.bed.gz +0 -0
  193. package/src/RgfaTabixAdapter/test_data/rgfa_ecoli.segs.bed.gz.tbi +0 -0
  194. package/src/alleleProjection/projectAlleles.test.ts +75 -0
  195. package/src/alleleProjection/projectAlleles.ts +190 -0
  196. package/src/bandage/README.md +121 -0
  197. package/src/bandage/bandage-layout.d.ts +14 -0
  198. package/src/bandage/bandage-layout.js +0 -0
  199. package/src/bandage/native/CMakeLists.txt +45 -0
  200. package/src/bandage/native/include/graph.h +188 -0
  201. package/src/bandage/native/include/graphlayout.h +74 -0
  202. package/src/bandage/native/include/settings.h +86 -0
  203. package/src/bandage/native/include/types.h +56 -0
  204. package/src/bandage/native/src/bindings.cpp +164 -0
  205. package/src/bandage/native/src/graphlayout.cpp +621 -0
  206. package/src/bandage/resizableDecode.test.ts +58 -0
  207. package/src/gfa-core/gfaParser.bench.ts +55 -0
  208. package/src/gfa-core/gfaParser.test.ts +168 -0
  209. package/src/gfa-core/gfaParser.ts +237 -0
  210. package/src/gfa-core/index.ts +10 -0
  211. package/src/hoverSync/GraphNodeHighlight.tsx +62 -0
  212. package/src/hoverSync/graphViewHighlights.test.ts +158 -0
  213. package/src/hoverSync/graphViewHighlights.ts +119 -0
  214. package/src/hoverSync/index.test.tsx +36 -0
  215. package/src/hoverSync/index.tsx +19 -0
  216. package/src/hoverSync/lgvHover.test.ts +202 -0
  217. package/src/hoverSync/lgvHover.ts +136 -0
  218. package/src/index.ts +46 -15
  219. package/src/launchFromGraph/contributors.test.ts +197 -0
  220. package/src/launchFromGraph/contributors.ts +0 -0
  221. package/src/launchFromGraph/graphMenuItems.ts +211 -0
  222. package/src/launchFromGraph/launchFromGraph.test.ts +489 -0
  223. package/src/launchFromGraph/launchFromGraph.ts +265 -0
  224. package/src/launchFromGraph/launchTracks.test.ts +72 -0
  225. package/src/launchFromGraph/launchTracks.ts +50 -0
  226. package/src/launchFromGraph/linearViewTarget.test.ts +82 -0
  227. package/src/launchFromGraph/linearViewTarget.ts +75 -0
  228. package/src/launchFromGraph/syntenyTracks.ts +47 -0
  229. package/src/launchSubgraph/index.ts +152 -0
  230. package/src/launchSubgraph/launchSubgraph.test.ts +264 -0
  231. package/src/launchSubgraph/launchSubgraphView.ts +128 -0
  232. package/src/launchSubgraph/linearViewMenuItems.test.ts +176 -0
  233. package/src/launchSubgraph/linearViewMenuItems.ts +107 -0
  234. package/src/launchSubgraph/multiWayLaunch.test.ts +289 -0
  235. package/src/launchSubgraph/subgraphMenuItems.ts +73 -0
  236. package/src/launchSubgraph/subgraphTracks.test.ts +165 -0
  237. package/src/launchSubgraph/subgraphTracks.ts +151 -0
  238. package/src/launchSubgraph/testEnv.ts +258 -0
  239. package/src/loadBandage.ts +22 -0
  240. package/src/panSNTabix.test.ts +72 -0
  241. package/src/panSNTabix.ts +149 -0
  242. package/src/pansn.ts +106 -0
  243. package/src/pluginName.ts +3 -0
  244. package/src/synteny/ComparativeAdapterBase.ts +77 -0
  245. package/src/synteny/SyntenyFeature.ts +116 -0
  246. package/src/synteny/clipFeatureToRegion.test.ts +329 -0
  247. package/src/synteny/clipFeatureToRegion.ts +167 -0
  248. package/src/version.ts +1 -0
  249. package/dist/GraphGenomeView/components/FeatureDialog.d.ts +0 -5
  250. package/dist/GraphGenomeView/components/FeatureDialog.js +0 -18
  251. package/dist/GraphGenomeView/components/FeatureDialog.js.map +0 -1
  252. package/dist/GraphGenomeView/components/GraphGenomeView.d.ts +0 -6
  253. package/dist/GraphGenomeView/components/GraphGenomeView.js +0 -17
  254. package/dist/GraphGenomeView/components/GraphGenomeView.js.map +0 -1
  255. package/dist/GraphGenomeView/components/GraphPanel.d.ts +0 -6
  256. package/dist/GraphGenomeView/components/GraphPanel.js +0 -17
  257. package/dist/GraphGenomeView/components/GraphPanel.js.map +0 -1
  258. package/dist/GraphGenomeView/components/Header.d.ts +0 -6
  259. package/dist/GraphGenomeView/components/Header.js +0 -59
  260. package/dist/GraphGenomeView/components/Header.js.map +0 -1
  261. package/dist/GraphGenomeView/components/SettingsDialog.d.ts +0 -7
  262. package/dist/GraphGenomeView/components/SettingsDialog.js +0 -24
  263. package/dist/GraphGenomeView/components/SettingsDialog.js.map +0 -1
  264. package/dist/GraphGenomeView/index.d.ts +0 -2
  265. package/dist/GraphGenomeView/index.js +0 -15
  266. package/dist/GraphGenomeView/index.js.map +0 -1
  267. package/dist/GraphGenomeView/model.d.ts +0 -80
  268. package/dist/GraphGenomeView/model.js +0 -96
  269. package/dist/GraphGenomeView/model.js.map +0 -1
  270. package/dist/GraphGenomeView/util.d.ts +0 -1
  271. package/dist/GraphGenomeView/util.js +0 -8
  272. package/dist/GraphGenomeView/util.js.map +0 -1
  273. package/dist/index.d.ts +0 -8
  274. package/dist/index.js +0 -24
  275. package/dist/index.js.map +0 -1
  276. package/dist/jbrowse-plugin-graphgenomeviewer.umd.production.min.js +0 -67
  277. package/dist/jbrowse-plugin-graphgenomeviewer.umd.production.min.js.map +0 -7
  278. package/src/GraphGenomeView/components/FeatureDialog.tsx +0 -51
  279. package/src/GraphGenomeView/components/GraphPanel.tsx +0 -37
  280. package/src/GraphGenomeView/components/Header.tsx +0 -90
  281. package/src/GraphGenomeView/components/SettingsDialog.tsx +0 -71
  282. package/src/GraphGenomeView/util.ts +0 -7
  283. package/src/declare.d.ts +0 -1
@@ -0,0 +1,127 @@
1
+ import cohort from './test_data/abca7_trgt_walks.json'
2
+ import { CALL_TOLERANCE, withCalls } from './walkCalls'
3
+
4
+ import type { RepeatCall } from './repeatFeatures'
5
+
6
+ function walk(sample: string, bp: number, complete = true) {
7
+ return { sample, bp, complete }
8
+ }
9
+
10
+ const calls = (...bps: (number | RepeatCall)[]) =>
11
+ bps.map(bp => (typeof bp === 'number' ? { bp } : bp))
12
+
13
+ test('each haplotype takes its own allele, and one of a pair can disagree', () => {
14
+ const rows = withCalls([walk('HG1', 3161), walk('HG1', 3161)], {
15
+ HG1: calls(387, 3161),
16
+ })
17
+ expect(rows.map(r => r.call?.bp)).toEqual([387, 3161])
18
+ expect(rows.map(r => r.call?.agrees)).toEqual([false, true])
19
+ })
20
+
21
+ // AL comes in the genotype's order, which is the genotyper's: TRGTdb's ABCA7
22
+ // record lists the shortest allele first, and a graph's haplotype 1 is not it.
23
+ test('the order a record lists its alleles in does not reach the pairing', () => {
24
+ const short = walk('HG1', 388)
25
+ const long = walk('HG1', 3183)
26
+ for (const listed of [calls(387, 3161), calls(3161, 387)]) {
27
+ expect(
28
+ withCalls([short, long], { HG1: listed }).map(r => r.call?.bp),
29
+ ).toEqual([387, 3161])
30
+ expect(
31
+ withCalls([long, short], { HG1: listed }).map(r => r.call?.bp),
32
+ ).toEqual([3161, 387])
33
+ }
34
+ })
35
+
36
+ test('an allele no read spanned is shown against its walk and never scored', () => {
37
+ const rows = withCalls([walk('HG02559', 5525), walk('HG02559', 493)], {
38
+ HG02559: calls(
39
+ { bp: 491, spanningReads: 1 },
40
+ { bp: 491, spanningReads: 0 },
41
+ ),
42
+ })
43
+ expect(rows.map(r => r.call)).toEqual([
44
+ { bp: 491, spanningReads: 0, agrees: undefined },
45
+ { bp: 491, spanningReads: 1, agrees: true },
46
+ ])
47
+ })
48
+
49
+ // HG00323's two walks against a genotype whose second allele has no spanning
50
+ // read: the walk the read-backed call fits keeps the verdict wherever the two
51
+ // equal lengths sit, because the backed allele is paired first.
52
+ test('the read-backed allele is paired on merit, not on tie order', () => {
53
+ for (const listed of [
54
+ calls({ bp: 2640, spanningReads: 1 }, { bp: 2640, spanningReads: 0 }),
55
+ calls({ bp: 2640, spanningReads: 0 }, { bp: 2640, spanningReads: 1 }),
56
+ ]) {
57
+ const rows = withCalls([walk('HG1', 1642), walk('HG1', 2639)], {
58
+ HG1: listed,
59
+ })
60
+ expect(rows.map(r => r.call?.spanningReads)).toEqual([0, 1])
61
+ expect(rows.map(r => r.call?.agrees)).toEqual([undefined, true])
62
+ }
63
+ })
64
+
65
+ test('a walk that does not span the array keeps its tick and gets no verdict', () => {
66
+ const rows = withCalls(
67
+ [walk('HG04199', 1476), walk('HG04199', 3655, false)],
68
+ { HG04199: calls(1485, 8614) },
69
+ )
70
+ expect(rows.map(r => r.call?.bp)).toEqual([1485, 8614])
71
+ expect(rows.map(r => r.call?.agrees)).toEqual([true, undefined])
72
+ })
73
+
74
+ // HG02976 at ABCA7: one walk spans the array and lands on the longer allele
75
+ // almost exactly, the other stops inside it. Paired on length alone the one
76
+ // that stops takes the allele that fits, and the walk that spans reads as a
77
+ // disagreement against what is left.
78
+ test("a walk that stops inside the array cannot take a spanning walk's allele", () => {
79
+ const rows = withCalls(
80
+ [walk('HG02976', 2884), walk('HG02976', 3653, false)],
81
+ {
82
+ HG02976: calls(2274, 2886),
83
+ },
84
+ )
85
+ expect(rows.map(r => r.call?.bp)).toEqual([2886, 2274])
86
+ expect(rows.map(r => r.call?.agrees)).toEqual([true, undefined])
87
+ })
88
+
89
+ test('a surplus walk or an uncalled sample carries no call', () => {
90
+ const rows = withCalls(
91
+ [walk('HG1', 400), walk('HG1', 2000), walk('HG1', 4000), walk('HG2', 900)],
92
+ { HG1: calls(390, 4010) },
93
+ )
94
+ expect(rows.map(r => r.call?.bp)).toEqual([390, undefined, 4010, undefined])
95
+ expect(withCalls([walk('HG1', 400)], undefined)[0]!.call).toBeUndefined()
96
+ })
97
+
98
+ // Where CALL_TOLERANCE comes from. test_data holds every walk the HPRC graph
99
+ // draws through the ABCA7 VNTR beside TRGT's genotype of the same sample, and
100
+ // the two verdicts land in bands with a gap between them, so any threshold
101
+ // across the gap sorts the cohort the same way. The counts are there to say
102
+ // what a change moved.
103
+ test('the threshold falls in a gap the ABCA7 cohort leaves empty', () => {
104
+ const rows = withCalls(cohort.rows, cohort.calls)
105
+ const apart = (r: (typeof rows)[number]) =>
106
+ Math.abs(r.call!.bp - r.bp) / Math.max(r.call!.bp, r.bp)
107
+ const scored = rows.filter(r => r.call?.agrees !== undefined)
108
+ const near = scored.filter(r => r.call!.agrees)
109
+ const far = scored.filter(r => !r.call!.agrees)
110
+ expect(Math.max(...near.map(apart))).toBeLessThan(0.08)
111
+ expect(Math.min(...far.map(apart))).toBeGreaterThan(0.14)
112
+ expect(CALL_TOLERANCE).toBeGreaterThan(0.08)
113
+ expect(CALL_TOLERANCE).toBeLessThan(0.14)
114
+ expect({
115
+ agrees: near.length,
116
+ apart: far.length,
117
+ unbacked: rows.filter(r => r.call?.spanningReads === 0).length,
118
+ partial: rows.filter(r => r.call && !r.complete).length,
119
+ uncalled: rows.filter(r => !r.call).length,
120
+ }).toEqual({
121
+ agrees: 142,
122
+ apart: 24,
123
+ unbacked: 18,
124
+ partial: 2,
125
+ uncalled: 0,
126
+ })
127
+ })
@@ -0,0 +1,132 @@
1
+ import type { RepeatCall } from './repeatFeatures'
2
+
3
+ // Which genotyped allele belongs to which haplotype walk. A VCF states a
4
+ // sample's alleles in genotype order, which is the genotyper's own order and
5
+ // carries no correspondence to the assembly's haplotype numbering — TRGTdb's
6
+ // ABCA7 record, for one, lists them shortest first. So the walks and the
7
+ // alleles are paired by length instead: the pairing whose total difference is
8
+ // smallest, which for lengths on a line is the non-crossing one.
9
+ //
10
+ // A walk that does not reach both flanking reference nodes is measured whole
11
+ // rather than across the array, and an allele no read spanned is a copy of the
12
+ // one that was called. Neither states a length for this array, so they pair
13
+ // last and take no verdict — pairing them first let a walk that stops inside
14
+ // the array take the allele that fit one spanning it, and the spanning walk
15
+ // then read as a disagreement.
16
+
17
+ export interface WalkCall extends RepeatCall {
18
+ // undefined where the walk and the allele are not both measurements
19
+ agrees?: boolean
20
+ }
21
+
22
+ // Every walk/call pair over the 94 HPRC samples TRGT genotyped at the ABCA7
23
+ // VNTR is either within 8% of its allele or more than 14% from it, so any
24
+ // threshold across that gap separates them the same way.
25
+ export const CALL_TOLERANCE = 0.1
26
+
27
+ function agrees(walkBp: number, callBp: number) {
28
+ return Math.abs(callBp - walkBp) <= CALL_TOLERANCE * Math.max(walkBp, callBp)
29
+ }
30
+
31
+ // Pair two ascending lists of lengths, leaving out the surplus of whichever is
32
+ // longer. `out[i]` is the index in `calls` that `walks[i]` took.
33
+ function nearest(walks: number[], calls: number[]) {
34
+ const W = walks.length
35
+ const C = calls.length
36
+ const cost = Array.from({ length: W + 1 }, () => new Array(C + 1).fill(0))
37
+ const paired = Array.from({ length: W + 1 }, () =>
38
+ new Array<boolean>(C + 1).fill(false),
39
+ )
40
+ for (let i = W - 1; i >= 0; i--) {
41
+ for (let j = C - 1; j >= 0; j--) {
42
+ const together = Math.abs(walks[i]! - calls[j]!) + cost[i + 1]![j + 1]!
43
+ const dropWalk = W - i > C - j ? cost[i + 1]![j]! : Infinity
44
+ const dropCall = C - j > W - i ? cost[i]![j + 1]! : Infinity
45
+ cost[i]![j] = Math.min(together, dropWalk, dropCall)
46
+ paired[i]![j] = cost[i]![j] === together
47
+ }
48
+ }
49
+ const out = new Array<number | undefined>(W).fill(undefined)
50
+ for (let i = 0, j = 0; i < W && j < C;) {
51
+ if (paired[i]![j]) {
52
+ out[i++] = j++
53
+ } else if (W - i > C - j) {
54
+ i++
55
+ } else {
56
+ j++
57
+ }
58
+ }
59
+ return out
60
+ }
61
+
62
+ function byLength<T>(items: T[], bp: (item: T) => number) {
63
+ return items
64
+ .map((item, i) => ({ item, i }))
65
+ .sort((a, b) => bp(a.item) - bp(b.item))
66
+ }
67
+
68
+ const backed = (call: RepeatCall) => call.spanningReads !== 0
69
+
70
+ // The walks that span the array against the alleles reads spanned, then
71
+ // everything else against what those two left. It decides the ambiguous cases
72
+ // on merit: which walk carries the one allele a lone spanning read supports,
73
+ // and which allele a walk that stops inside the array is drawn against.
74
+ function pairSample<R extends { bp: number; complete: boolean }>(
75
+ rows: R[],
76
+ calls: RepeatCall[],
77
+ ) {
78
+ const out = new Map<number, WalkCall>()
79
+ const taken = new Set<RepeatCall>()
80
+ const ranked = byLength(rows, r => r.bp)
81
+ const stages = [
82
+ [ranked.filter(r => r.item.complete), calls.filter(backed)],
83
+ [ranked, calls],
84
+ ] as const
85
+ for (const [pool, group] of stages) {
86
+ const free = pool.filter(r => !out.has(r.i))
87
+ const open = byLength(
88
+ group.filter(call => !taken.has(call)),
89
+ call => call.bp,
90
+ )
91
+ nearest(
92
+ free.map(r => r.item.bp),
93
+ open.map(c => c.item.bp),
94
+ ).forEach((pick, k) => {
95
+ if (pick !== undefined) {
96
+ const row = free[k]!
97
+ const call = open[pick]!.item
98
+ taken.add(call)
99
+ out.set(row.i, {
100
+ ...call,
101
+ agrees:
102
+ row.item.complete && backed(call)
103
+ ? agrees(row.item.bp, call.bp)
104
+ : undefined,
105
+ })
106
+ }
107
+ })
108
+ }
109
+ return out
110
+ }
111
+
112
+ export function withCalls<
113
+ R extends { sample: string; bp: number; complete: boolean },
114
+ >(rows: R[], calls: Record<string, RepeatCall[]> | undefined) {
115
+ if (!calls) {
116
+ return rows as (R & { call?: WalkCall })[]
117
+ }
118
+ const paired = new Map<R, WalkCall>()
119
+ const bySample = new Map<string, R[]>()
120
+ for (const row of rows) {
121
+ bySample.set(row.sample, [...(bySample.get(row.sample) ?? []), row])
122
+ }
123
+ for (const [sample, group] of bySample) {
124
+ const called = calls[sample]
125
+ if (called) {
126
+ for (const [i, call] of pairSample(group, called)) {
127
+ paired.set(group[i]!, call)
128
+ }
129
+ }
130
+ }
131
+ return rows.map(row => ({ ...row, call: paired.get(row) }))
132
+ }
@@ -0,0 +1,214 @@
1
+ import { parseGFA } from '../gfa-core/index'
2
+ import { convertGFAToGraph } from './gfa/gfaConverter'
3
+ import { Canvas2DRenderer } from './renderer/Canvas2DRenderer'
4
+ import { buildGeometry } from './renderer/GeometryBuilder'
5
+ import { recordingCanvas } from './renderer/recordingCanvas'
6
+
7
+ import type { GraphNode, NodeSegment } from './types'
8
+
9
+ // isotropic: one scale for both axes, which is every layout but the row ones
10
+ const iso = (scale = 1) => ({ scaleX: scale, scaleY: scale })
11
+
12
+ // Builds a linear chain of `bubbleCount` diamond bubbles. Each bubble is
13
+ // source -> {altA, altB} -> sink, and the sink is shared with the next
14
+ // bubble's source, so the whole thing is one connected component shaped like
15
+ // a real pangenome backbone with biallelic variant sites.
16
+ //
17
+ // node count = 3 * bubbleCount + 1
18
+ // link count = 4 * bubbleCount
19
+ function generateBubbleGFA(bubbleCount: number) {
20
+ const lines: string[] = ['H\tVN:Z:1.0']
21
+ const segId = (i: number) => i + 1
22
+ const totalNodes = 3 * bubbleCount + 1
23
+
24
+ for (let i = 0; i < totalNodes; i++) {
25
+ lines.push(`S\t${segId(i)}\tACGTACGT`)
26
+ }
27
+
28
+ // Anchor (backbone) node ids: 0, 3, 6, ... ; alts for bubble b are 3b+1, 3b+2
29
+ const refPath: number[] = [segId(0)]
30
+ const altPath: number[] = [segId(0)]
31
+ for (let b = 0; b < bubbleCount; b++) {
32
+ const source = segId(3 * b)
33
+ const altA = segId(3 * b + 1)
34
+ const altB = segId(3 * b + 2)
35
+ const sink = segId(3 * b + 3)
36
+ lines.push(
37
+ `L\t${source}\t+\t${altA}\t+\t0M`,
38
+ `L\t${source}\t+\t${altB}\t+\t0M`,
39
+ `L\t${altA}\t+\t${sink}\t+\t0M`,
40
+ `L\t${altB}\t+\t${sink}\t+\t0M`,
41
+ )
42
+ refPath.push(altA, sink)
43
+ altPath.push(altB, sink)
44
+ }
45
+
46
+ lines.push(
47
+ `P\tref#0#chr1\t${refPath.map(id => `${id}+`).join(',')}\t*`,
48
+ `P\talt#0#chr1\t${altPath.map(id => `${id}+`).join(',')}\t*`,
49
+ )
50
+ return { gfa: lines.join('\n'), totalNodes, totalLinks: 4 * bubbleCount }
51
+ }
52
+
53
+ // Synthetic 2-segment positions for every node so buildGeometry can run
54
+ // without the Bandage WASM layout step (covered by browser tests instead).
55
+ function syntheticPositions(nodes: GraphNode[]) {
56
+ const positions: Record<string, NodeSegment[]> = {}
57
+ for (let i = 0; i < nodes.length; i++) {
58
+ const n = nodes[i]!
59
+ positions[n.id] = [
60
+ { x: i * 10, y: 0 },
61
+ { x: i * 10 + 8, y: 0 },
62
+ ]
63
+ }
64
+ return positions
65
+ }
66
+
67
+ test('parse + convert scales to a few thousand bubbles', () => {
68
+ const { gfa, totalNodes, totalLinks } = generateBubbleGFA(2000)
69
+
70
+ const t0 = performance.now()
71
+ const gfaGraph = parseGFA(gfa)
72
+ const graph = convertGFAToGraph(gfaGraph, 'stress')
73
+ const elapsed = performance.now() - t0
74
+
75
+ expect(gfaGraph.nodes).toHaveLength(totalNodes)
76
+ expect(gfaGraph.links).toHaveLength(totalLinks)
77
+ // forward-strand only graph -> one GraphNode per segment
78
+ expect(graph.nodes).toHaveLength(totalNodes)
79
+ expect(graph.edges).toHaveLength(totalLinks)
80
+ expect(graph.paths).toHaveLength(2)
81
+
82
+ // every edge that lies on a path should have pathIds populated
83
+ const edgesWithPaths = graph.edges.filter(e => e.pathIds?.length)
84
+ expect(edgesWithPaths.length).toBeGreaterThan(0)
85
+
86
+ // Generous ceiling — a linear pass over ~6k nodes / 8k links is milliseconds;
87
+ // this only trips on an accidental O(n^2) regression, not on a slow runner.
88
+ expect(elapsed).toBeLessThan(5000)
89
+ })
90
+
91
+ test('buildGeometry scales to a few thousand nodes', () => {
92
+ const { gfa, totalNodes } = generateBubbleGFA(2000)
93
+ const graph = convertGFAToGraph(parseGFA(gfa), 'stress')
94
+ const nodeById = new Map(graph.nodes.map(n => [n.id, n]))
95
+ const nodePositions = syntheticPositions(graph.nodes)
96
+
97
+ const t0 = performance.now()
98
+ const batch = buildGeometry({
99
+ nodePositions,
100
+ graph,
101
+ nodeById,
102
+ colorScheme: 'depth',
103
+ contigThickness: 10,
104
+ connectorThickness: 4,
105
+ drawPaths: true,
106
+ axis: iso(),
107
+ })
108
+ const elapsed = performance.now() - t0
109
+
110
+ expect(batch.nodeStrokeRuns.size).toBe(totalNodes)
111
+ // striped: every node is on at least one of the two paths
112
+ expect(batch.nodeStrokes.length).toBeGreaterThan(totalNodes)
113
+ expect(batch.edgeCurves.length).toBeGreaterThan(0)
114
+ expect(elapsed).toBeLessThan(5000)
115
+ })
116
+
117
+ test('buildGeometry scaling is roughly linear, not quadratic', () => {
118
+ function timeBuild(bubbleCount: number) {
119
+ const { gfa } = generateBubbleGFA(bubbleCount)
120
+ const graph = convertGFAToGraph(parseGFA(gfa), 'stress')
121
+ const nodeById = new Map(graph.nodes.map(n => [n.id, n]))
122
+ const nodePositions = syntheticPositions(graph.nodes)
123
+ // warm + measure
124
+ const opts = {
125
+ nodePositions,
126
+ graph,
127
+ nodeById,
128
+ colorScheme: 'uniform' as const,
129
+ contigThickness: 10,
130
+ connectorThickness: 4,
131
+ drawPaths: false,
132
+ axis: iso(),
133
+ }
134
+ buildGeometry(opts)
135
+ const t0 = performance.now()
136
+ buildGeometry(opts)
137
+ return performance.now() - t0
138
+ }
139
+
140
+ const small = timeBuild(500)
141
+ const large = timeBuild(2000) // 4x the work
142
+
143
+ // 4x input should be well under 10x time if scaling is linear. The loose
144
+ // bound tolerates timer noise on small absolute durations while still
145
+ // catching a quadratic blow-up (which would be ~16x).
146
+ expect(large).toBeLessThan(small * 10 + 50)
147
+ })
148
+
149
+ test('viewport culling drops the vast majority of off-screen nodes', () => {
150
+ const { gfa } = generateBubbleGFA(2000)
151
+ const graph = convertGFAToGraph(parseGFA(gfa), 'stress')
152
+ const nodeById = new Map(graph.nodes.map(n => [n.id, n]))
153
+ const nodePositions = syntheticPositions(graph.nodes)
154
+
155
+ // nodes are laid out along x at 10px spacing; this window covers ~50 of them
156
+ const batch = buildGeometry({
157
+ nodePositions,
158
+ graph,
159
+ nodeById,
160
+ colorScheme: 'uniform',
161
+ contigThickness: 10,
162
+ connectorThickness: 4,
163
+ drawPaths: false,
164
+ axis: iso(),
165
+ viewportBounds: { minX: 0, minY: -50, maxX: 500, maxY: 50 },
166
+ })
167
+
168
+ expect(batch.nodeStrokeRuns.size).toBeGreaterThan(0)
169
+ expect(batch.nodeStrokeRuns.size).toBeLessThan(graph.nodes.length / 2)
170
+ })
171
+
172
+ // Draw calls per frame, which is what the Canvas2D renderer's cost tracks --
173
+ // and unlike wall-clock they are deterministic, so they can be asserted exactly.
174
+ // The renderer batches everything drawn with one paint into one path, so the
175
+ // count is a function of how many distinct colours and weights the drawing
176
+ // uses and not of how many nodes it has. It used to be 12.6 per node, with a
177
+ // fill per mesh triangle; see agent-docs/GRAPH_SCALE_AND_LOD.md.
178
+ function countDrawCalls(batch: ReturnType<typeof buildGeometry>) {
179
+ const { canvas, strokes, fills } = recordingCanvas()
180
+ const renderer = new Canvas2DRenderer(canvas)
181
+ renderer.uploadGeometry(batch)
182
+ renderer.updateTransform({
183
+ scaleX: 1,
184
+ scaleY: 1,
185
+ translateX: 0,
186
+ translateY: 0,
187
+ dpr: 1,
188
+ })
189
+ renderer.render([1, 1, 1, 1])
190
+ return { strokes: strokes.length, fills: fills.length }
191
+ }
192
+
193
+ function uniformBatch(bubbleCount: number) {
194
+ const { gfa } = generateBubbleGFA(bubbleCount)
195
+ const graph = convertGFAToGraph(parseGFA(gfa), 'stress')
196
+ return buildGeometry({
197
+ nodePositions: syntheticPositions(graph.nodes),
198
+ graph,
199
+ nodeById: new Map(graph.nodes.map(n => [n.id, n])),
200
+ colorScheme: 'uniform',
201
+ contigThickness: 10,
202
+ connectorThickness: 4,
203
+ drawPaths: false,
204
+ axis: iso(),
205
+ })
206
+ }
207
+
208
+ test('draw calls per frame do not grow with the graph', () => {
209
+ const small = countDrawCalls(uniformBatch(100))
210
+ const large = countDrawCalls(uniformBatch(500))
211
+ // one paint each for the edges, the nodes and the arrowheads
212
+ expect(small).toEqual({ strokes: 2, fills: 1 })
213
+ expect(large).toEqual(small)
214
+ })