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,84 @@
1
+ import { convertGFAToGraph } from './gfa/gfaConverter'
2
+ import { buildNeighbors, nodeReferenceSpan } from './referenceSpan'
3
+ import { parseGFA } from '../gfa-core/index'
4
+
5
+ // The worked example from gfatools/doc/rGFA.md, the same fixture the anchored
6
+ // layout tests use: chr1 runs v1->v2->v3->v4, and foo carries an alternate
7
+ // allele (v5,v6) leaving after v2 and rejoining at v4.
8
+ const RGFA = `S\tv1\tAAAAA\tLN:i:5\tSN:Z:chr1\tSO:i:0\tSR:i:0
9
+ S\tv2\tCCC\tLN:i:3\tSN:Z:chr1\tSO:i:5\tSR:i:0
10
+ S\tv3\tGG\tLN:i:2\tSN:Z:chr1\tSO:i:8\tSR:i:0
11
+ S\tv4\tTTTTTTT\tLN:i:7\tSN:Z:chr1\tSO:i:10\tSR:i:0
12
+ S\tv5\tAC\tLN:i:2\tSN:Z:foo\tSO:i:8\tSR:i:1
13
+ S\tv6\tGTA\tLN:i:3\tSN:Z:foo\tSO:i:10\tSR:i:1
14
+ L\tv1\t+\tv2\t+\t0M
15
+ L\tv2\t+\tv3\t+\t0M
16
+ L\tv3\t+\tv4\t+\t0M
17
+ L\tv2\t+\tv5\t+\t0M
18
+ L\tv5\t+\tv6\t+\t0M
19
+ L\tv6\t+\tv4\t+\t0M`
20
+
21
+ function setup(gfa: string) {
22
+ const graph = convertGFAToGraph(parseGFA(gfa))
23
+ const nodeById = new Map(graph.nodes.map(n => [n.id, n]))
24
+ const neighbors = buildNeighbors(graph)
25
+ return (nodeId: string) => nodeReferenceSpan({ nodeId, nodeById, neighbors })
26
+ }
27
+
28
+ test('a backbone node states its own reference span', () => {
29
+ const span = setup(RGFA)
30
+ expect(span('v1+')).toEqual({ start: 0, end: 5 })
31
+ expect(span('v2+')).toEqual({ start: 5, end: 8 })
32
+ expect(span('v4+')).toEqual({ start: 10, end: 17 })
33
+ })
34
+
35
+ // v5/v6's SO is an offset on `foo`, not on chr1, so it has no reference span of
36
+ // its own. The allele leaves after v2 (ends at 8) and rejoins at v4 (starts at
37
+ // 10), so what it replaces on chr1 is 8-10 — which is v3's span, the reference
38
+ // allele of the same bubble.
39
+ test('an off-reference allele resolves to the interval it branches into', () => {
40
+ const span = setup(RGFA)
41
+ expect(span('v5+')).toEqual({ start: 8, end: 10 })
42
+ expect(span('v6+')).toEqual({ start: 8, end: 10 })
43
+ })
44
+
45
+ // A pure insertion's flanks abut, so there is exactly one reference point to
46
+ // mark. The span collapses to it rather than covering either flank; the linear
47
+ // view floors the drawn width so it stays visible.
48
+ test('an insertion collapses to the reference point it is inserted at', () => {
49
+ const insertion = `S\tv1\tAAAAA\tLN:i:5\tSN:Z:chr1\tSO:i:0\tSR:i:0
50
+ S\tv2\tCCCCC\tLN:i:5\tSN:Z:chr1\tSO:i:5\tSR:i:0
51
+ S\tins\tGGG\tLN:i:3\tSN:Z:alt\tSO:i:0\tSR:i:1
52
+ L\tv1\t+\tv2\t+\t0M
53
+ L\tv1\t+\tins\t+\t0M
54
+ L\tins\t+\tv2\t+\t0M`
55
+ expect(setup(insertion)('ins+')).toEqual({ start: 5, end: 5 })
56
+ })
57
+
58
+ // At the edge of a fetched window a bubble can have only one flank present.
59
+ // Falling back to that flank's own span keeps the highlight in the right place
60
+ // instead of dropping it.
61
+ test('an allele with one flank in the window takes that flank span', () => {
62
+ const clipped = `S\tv1\tAAAAA\tLN:i:5\tSN:Z:chr1\tSO:i:0\tSR:i:0
63
+ S\talt\tGGG\tLN:i:3\tSN:Z:alt\tSO:i:0\tSR:i:1
64
+ L\tv1\t+\talt\t+\t0M`
65
+ expect(setup(clipped)('alt+')).toEqual({ start: 0, end: 5 })
66
+ })
67
+
68
+ test('a node with no backbone anywhere in reach has no reference span', () => {
69
+ const plain = `S\t1\tACGT
70
+ S\t2\tGGCC
71
+ L\t1\t+\t2\t+\t0M`
72
+ expect(setup(plain)('1+')).toBeUndefined()
73
+ })
74
+
75
+ test('an unknown node id has no reference span', () => {
76
+ expect(setup(RGFA)('nope+')).toBeUndefined()
77
+ })
78
+
79
+ test('neighbors are undirected, so a bubble is reachable from either flank', () => {
80
+ const neighbors = buildNeighbors(convertGFAToGraph(parseGFA(RGFA)))
81
+ expect(neighbors.get('v2+')).toContain('v1+')
82
+ expect(neighbors.get('v2+')).toContain('v5+')
83
+ expect(neighbors.get('v4+')).toContain('v6+')
84
+ })
@@ -0,0 +1,133 @@
1
+ import { isBackbone } from './anchoredNodes'
2
+
3
+ import type { Graph, GraphNode } from './types'
4
+
5
+ // How long an allele chain can be and still be located from both ends. An
6
+ // allele is a run of off-reference segments between two backbone flanks, so the
7
+ // two flanks sit at *different* hop depths from any segment in the middle and
8
+ // the walk has to keep going after it finds the first one. Minigraph alleles are
9
+ // short runs, so this reaches both flanks throughout the fixtures; a longer run
10
+ // degrades to the flanks it did reach rather than to nothing.
11
+ const MAX_BACKBONE_HOPS = 4
12
+
13
+ export interface ReferenceSpan {
14
+ start: number
15
+ end: number
16
+ }
17
+
18
+ // Undirected adjacency over node ids. Edges carry the topology in both
19
+ // directions for this purpose: finding the backbone a node hangs off doesn't
20
+ // care which way the L line was written.
21
+ export function buildNeighbors(graph: Graph) {
22
+ const neighbors = new Map<string, string[]>()
23
+ function add(from: string, to: string) {
24
+ const list = neighbors.get(from)
25
+ if (list) {
26
+ list.push(to)
27
+ } else {
28
+ neighbors.set(from, [to])
29
+ }
30
+ }
31
+ for (const edge of graph.edges) {
32
+ add(edge.from, edge.to)
33
+ add(edge.to, edge.from)
34
+ }
35
+ return neighbors
36
+ }
37
+
38
+ // The reference interval an allele branches into: between the end of the
39
+ // backbone segment upstream of it and the start of the one downstream.
40
+ // `[min(ends), max(starts)]` picks those two out of the reached flanks without
41
+ // needing to know which is which, and it degrades usefully in both directions —
42
+ // a pure insertion, whose flanks abut, collapses to the single reference point
43
+ // it is inserted at, and a bubble with only one flank inside the window yields
44
+ // that flank's own span.
45
+ function flankingSpan({
46
+ nodeId,
47
+ nodeById,
48
+ neighbors,
49
+ }: {
50
+ nodeId: string
51
+ nodeById: Map<string, GraphNode>
52
+ neighbors: Map<string, string[]>
53
+ }) {
54
+ const seen = new Set([nodeId])
55
+ let frontier = [nodeId]
56
+ let minEnd = Infinity
57
+ let maxStart = -Infinity
58
+
59
+ // Backbone nodes are terminal — they are collected, never queued — so the walk
60
+ // cannot cross the reference into a neighbouring bubble. It stays inside the
61
+ // allele it started in and stops on its own at the flanks.
62
+ for (let hop = 0; hop < MAX_BACKBONE_HOPS && frontier.length > 0; hop++) {
63
+ const next: string[] = []
64
+ for (const id of frontier) {
65
+ const adjacent = neighbors.get(id)
66
+ if (adjacent) {
67
+ for (const neighborId of adjacent) {
68
+ if (!seen.has(neighborId)) {
69
+ seen.add(neighborId)
70
+ const neighbor = nodeById.get(neighborId)
71
+ if (neighbor && isBackbone(neighbor)) {
72
+ minEnd = Math.min(minEnd, neighbor.stable.start + neighbor.length)
73
+ maxStart = Math.max(maxStart, neighbor.stable.start)
74
+ } else {
75
+ next.push(neighborId)
76
+ }
77
+ }
78
+ }
79
+ }
80
+ }
81
+ frontier = next
82
+ }
83
+
84
+ return minEnd === Infinity
85
+ ? undefined
86
+ : {
87
+ start: Math.min(minEnd, maxStart),
88
+ end: Math.max(minEnd, maxStart),
89
+ }
90
+ }
91
+
92
+ // The reference interval to highlight for a node, in the coordinates of the
93
+ // stable sequence the subgraph was cut from.
94
+ //
95
+ // A rank-0 node states its own, so the interval is exact. A rank>0 allele has
96
+ // none: its SO is an offset on a *different* stable sequence and is not
97
+ // comparable to the reference axis (see the axis note in
98
+ // layout/anchoredLayout.ts), so it is located by the backbone it branches from
99
+ // instead.
100
+ export function nodeReferenceSpan({
101
+ nodeId,
102
+ nodeById,
103
+ neighbors,
104
+ }: {
105
+ nodeId: string
106
+ nodeById: Map<string, GraphNode>
107
+ neighbors: Map<string, string[]>
108
+ }): ReferenceSpan | undefined {
109
+ const node = nodeById.get(nodeId)
110
+ return node === undefined
111
+ ? undefined
112
+ : isBackbone(node)
113
+ ? { start: node.stable.start, end: node.stable.start + node.length }
114
+ : flankingSpan({ nodeId, nodeById, neighbors })
115
+ }
116
+
117
+ // Every node's midpoint on the reference, keyed by id: what a position-based
118
+ // colour scheme ramps over. Alleles come through the same flanking walk hover
119
+ // sync uses, so a bubble takes the colour of the reference interval it branches
120
+ // from instead of dropping out of the ramp — which is the whole point, since an
121
+ // allele is exactly what a linear view cannot draw at a position of its own.
122
+ export function referenceMidpoints(graph: Graph) {
123
+ const nodeById = new Map(graph.nodes.map(node => [node.id, node]))
124
+ const neighbors = buildNeighbors(graph)
125
+ const midpoints = new Map<string, number>()
126
+ for (const node of graph.nodes) {
127
+ const span = nodeReferenceSpan({ nodeId: node.id, nodeById, neighbors })
128
+ if (span) {
129
+ midpoints.set(node.id, (span.start + span.end) / 2)
130
+ }
131
+ }
132
+ return midpoints
133
+ }
@@ -0,0 +1,172 @@
1
+ import { describe, test } from 'vitest'
2
+
3
+ import { deletionEdges } from './deletionEdges'
4
+ import { graphLabels } from './graphLabels'
5
+ import { buildGeometry, computeReferenceRamp } from './renderer/GeometryBuilder'
6
+ import { EdgeSpatialIndex, SpatialIndex } from './util/SpatialIndex'
7
+
8
+ import type { Graph, GraphNode, NodeSegment } from './types'
9
+
10
+ // `pnpm bench`. Not part of `pnpm test`: these assert nothing, they report the
11
+ // cost of the four passes that run while a user is holding the mouse down.
12
+ //
13
+ // Which pass runs when, because the difference is the whole point of the
14
+ // numbers:
15
+ //
16
+ // graphLabels every mousemove of a pan, undebounced — it places labels in
17
+ // screen space, so a translate genuinely moves them
18
+ // buildGeometry once per debounced pan/zoom, and once per frame of a drag
19
+ // the two indexes lazily, on the first mousemove after the positions move
20
+ //
21
+ // So a millisecond in `graphLabels` costs about ten times what a millisecond in
22
+ // `buildGeometry` does, and both are budgeted against the ~10 ms that
23
+ // agent-docs/GRAPH_SCALE_AND_LOD.md measures a redraw of a 1-2k node cut at.
24
+ //
25
+ // Vitest 5 moved `bench` from a top-level export to a test-context fixture:
26
+ // each measurement is a `test()` that awaits `bench(name, fn).run()`.
27
+
28
+ // A bubble-chain rGFA: a rank-0 backbone with an alt allele and a bare deletion
29
+ // edge hanging off every other segment. Both anchored layouts draw this shape,
30
+ // and it is what the scale notes above were measured on.
31
+ function benchGraph(backboneCount: number) {
32
+ const nodes: GraphNode[] = []
33
+ const edges: Graph['edges'] = []
34
+ let pos = 0
35
+ for (let i = 0; i < backboneCount; i++) {
36
+ const length = 300 + (i % 17) * 40
37
+ nodes.push({
38
+ id: `b${i}+`,
39
+ name: `b${i}`,
40
+ length,
41
+ depth: 5,
42
+ stable: { refName: 'chr1', start: pos, rank: 0 },
43
+ })
44
+ pos += length
45
+ if (i > 0) {
46
+ edges.push({ from: `b${i - 1}+`, to: `b${i}+` })
47
+ }
48
+ if (i % 2 === 1 && i > 1) {
49
+ nodes.push({
50
+ id: `a${i}+`,
51
+ name: `a${i}`,
52
+ length: 40 + (i % 7) * 11,
53
+ depth: 2,
54
+ stable: { refName: `HG${i % 40}#1#chr1`, start: i * 100, rank: 1 },
55
+ })
56
+ edges.push({ from: `b${i - 2}+`, to: `a${i}+` })
57
+ edges.push({ from: `a${i}+`, to: `b${i}+` })
58
+ edges.push({ from: `b${i - 2}+`, to: `b${i}+` })
59
+ }
60
+ }
61
+ return { name: 'bench', nodes, edges, anchoredBy: 'tags' } satisfies Graph
62
+ }
63
+
64
+ // The anchored layout's shape: one row per rank, y already in screen px, so the
65
+ // two axes are in different units — the case every `yToX` conversion is for.
66
+ function benchLayout(graph: Graph) {
67
+ const nodePositions: Record<string, NodeSegment[]> = {}
68
+ for (const node of graph.nodes) {
69
+ const { start, rank } = node.stable!
70
+ nodePositions[node.id] = [
71
+ { x: start, y: rank * 20 },
72
+ { x: start + node.length, y: rank * 20 },
73
+ ]
74
+ }
75
+ return nodePositions
76
+ }
77
+
78
+ const AXIS = { scaleX: 0.01, scaleY: 1 }
79
+ const VIEWPORT = { translateX: 40, translateY: 40, width: 900, height: 600 }
80
+
81
+ for (const backbone of [1000, 5000]) {
82
+ const graph = benchGraph(backbone)
83
+ const nodePositions = benchLayout(graph)
84
+ const nodeById = new Map(graph.nodes.map(n => [n.id, n]))
85
+ const nodeLengths = new Map(graph.nodes.map(n => [n.id, n.length]))
86
+ const deletions = deletionEdges(graph)
87
+ const bypassed = new Map(deletions.map(d => [d.edgeIndex, d.bypassed]))
88
+ const referenceRamp = computeReferenceRamp(graph, {
89
+ start: 0,
90
+ end: pos(graph),
91
+ })
92
+
93
+ describe(`${graph.nodes.length} nodes / ${graph.edges.length} edges`, () => {
94
+ // The pan case: same layout, same zoom, a new translate. Everything a frame
95
+ // can reuse is reused, so this is the number that has to stay small.
96
+ let tx = VIEWPORT.translateX
97
+ test('graphLabels (pan)', async ({ bench }) => {
98
+ await bench('graphLabels (pan)', () => {
99
+ graphLabels({
100
+ nodePositions,
101
+ nodeLengths,
102
+ deletions,
103
+ axis: AXIS,
104
+ ...VIEWPORT,
105
+ translateX: tx++ % 80,
106
+ })
107
+ }).run()
108
+ })
109
+
110
+ // The drag case: the positions moved in place, so nothing carries over.
111
+ let version = 0
112
+ test('graphLabels (drag)', async ({ bench }) => {
113
+ await bench('graphLabels (drag)', () => {
114
+ graphLabels({
115
+ nodePositions,
116
+ nodeLengths,
117
+ deletions,
118
+ axis: AXIS,
119
+ ...VIEWPORT,
120
+ version: ++version,
121
+ })
122
+ }).run()
123
+ })
124
+
125
+ test('buildGeometry', async ({ bench }) => {
126
+ await bench('buildGeometry', () => {
127
+ buildGeometry({
128
+ nodePositions,
129
+ graph,
130
+ nodeById,
131
+ colorScheme: 'reference-position',
132
+ contigThickness: 10,
133
+ connectorThickness: 4,
134
+ drawPaths: false,
135
+ axis: AXIS,
136
+ referenceRamp,
137
+ deletions: bypassed,
138
+ })
139
+ }).run()
140
+ })
141
+
142
+ // Once per graph, feeding the pass above.
143
+ test('computeReferenceRamp', async ({ bench }) => {
144
+ await bench('computeReferenceRamp', () => {
145
+ computeReferenceRamp(graph, { start: 0, end: pos(graph) })
146
+ }).run()
147
+ })
148
+
149
+ test('SpatialIndex (nodes)', async ({ bench }) => {
150
+ await bench('SpatialIndex (nodes)', () => {
151
+ new SpatialIndex(nodePositions)
152
+ }).run()
153
+ })
154
+
155
+ test('EdgeSpatialIndex', async ({ bench }) => {
156
+ await bench('EdgeSpatialIndex', () => {
157
+ new EdgeSpatialIndex(
158
+ nodePositions,
159
+ graph,
160
+ false,
161
+ AXIS,
162
+ undefined,
163
+ bypassed,
164
+ )
165
+ }).run()
166
+ })
167
+ })
168
+ }
169
+
170
+ function pos(graph: Graph) {
171
+ return graph.nodes.reduce((sum, n) => sum + n.length, 0)
172
+ }
@@ -0,0 +1,226 @@
1
+ import stateModelFactory from './model'
2
+ import { MAX_PATH_COLORS } from './pathColors'
3
+
4
+ import type { RenderBatch, Renderer } from './renderer/types'
5
+
6
+ const mockRpcCall = vi.fn()
7
+ const mockSession = {
8
+ tracks: [] as { trackId: string; [key: string]: unknown }[],
9
+ rpcManager: { call: mockRpcCall },
10
+ assemblyNames: [] as string[],
11
+ views: [] as unknown[],
12
+ addView() {
13
+ return { id: 'view-1' }
14
+ },
15
+ }
16
+
17
+ vi.mock('@jbrowse/core/util', () => ({
18
+ getSession: () => mockSession,
19
+ isSessionModelWithWidgets: () => false,
20
+ parseLocString: () => ({}),
21
+ getEnv: () => ({}),
22
+ useWidthSetter: () => {},
23
+ measureText: () => 0,
24
+ IntervalTree: class {},
25
+ getSnapshot: () => ({}),
26
+ applySnapshot: () => {},
27
+ objectHash: () => '',
28
+ }))
29
+
30
+ // Partial, not wholesale: core modules pulled in transitively (BaseFeatureWidget's
31
+ // config schema) call ConfigurationSchema at module-eval time.
32
+ vi.mock(import('@jbrowse/core/configuration'), async importOriginal => ({
33
+ ...(await importOriginal()),
34
+ readConfObject: vi.fn(),
35
+ }))
36
+
37
+ const GFA = 'H\tVN:Z:1.0\nS\t1\tACGT\nS\t2\tGGCC\nL\t1\t+\t2\t+\t0M\n'
38
+
39
+ // A layout whose extent dwarfs the pane, which is what every real force layout
40
+ // produces: bandageAutoScale sizes a drawing in tens of thousands of FMMM units
41
+ // and zoom-to-fit is what brings it on screen.
42
+ const WIDE_LAYOUT = {
43
+ nodePositions: {
44
+ '1+': [
45
+ { x: 0, y: 0 },
46
+ { x: 40000, y: 0 },
47
+ ],
48
+ '1-': [
49
+ { x: 0, y: 30000 },
50
+ { x: 40000, y: 30000 },
51
+ ],
52
+ '2+': [
53
+ { x: 50000, y: 0 },
54
+ { x: 90000, y: 0 },
55
+ ],
56
+ '2-': [
57
+ { x: 50000, y: 30000 },
58
+ { x: 90000, y: 30000 },
59
+ ],
60
+ },
61
+ }
62
+
63
+ function fakeRenderer(uploads: RenderBatch[]) {
64
+ const renderer: Renderer = {
65
+ resize: () => {},
66
+ uploadGeometry: (batch: RenderBatch) => {
67
+ uploads.push(batch)
68
+ },
69
+ updateTransform: () => {},
70
+ render: () => {},
71
+ setNodeHighlights: () => {},
72
+ setEdgeHighlight: () => {},
73
+ destroy: () => {},
74
+ } as unknown as Renderer
75
+ return renderer
76
+ }
77
+
78
+ // Regression: a graph whose layout is much larger than the pane drew a blank
79
+ // canvas on the whole-file import path. buildGeometry culls to a viewport
80
+ // derived from the *current* transform, read untracked, so the build that runs
81
+ // before zoom-to-fit lands sees a pane-sized window of a 90,000-unit drawing
82
+ // and discards every node. Whether anything rebuilt afterwards decided whether
83
+ // the user saw the graph or an empty white pane, and on the 4,749-node HPRC
84
+ // chrM pggb graph they saw the pane.
85
+ test('a layout larger than the pane still uploads geometry', async () => {
86
+ mockRpcCall.mockImplementation((_id: string, method: string) =>
87
+ method === 'GraphComputeLayout'
88
+ ? Promise.resolve({ result: WIDE_LAYOUT, duration: 5 })
89
+ : Promise.reject(new Error(`Unexpected RPC: ${method}`)),
90
+ )
91
+ const model = stateModelFactory().create({ type: 'GraphGenomeView' })
92
+ const uploads: RenderBatch[] = []
93
+ // The mount order the app actually has: the canvas is rendered only once
94
+ // `hasGraph` is true, so the graph and its layout exist *before* any autorun
95
+ // is installed, and the width lands after the canvas is measured.
96
+ await model.loadGFA(GFA, 'wide')
97
+ model.startRenderingBackend(fakeRenderer(uploads))
98
+ model.setWidth(1000)
99
+ // past the pan/zoom debounce, so any rebuild it would have scheduled has run
100
+ await new Promise(resolve => setTimeout(resolve, 400))
101
+
102
+ expect(uploads.length).toBeGreaterThan(0)
103
+ expect(uploads.at(-1)!.nodeStrokes.length).toBeGreaterThan(0)
104
+ })
105
+
106
+ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
107
+
108
+ async function drawnModel() {
109
+ mockRpcCall.mockImplementation((_id: string, method: string) =>
110
+ method === 'GraphComputeLayout'
111
+ ? Promise.resolve({ result: WIDE_LAYOUT, duration: 5 })
112
+ : Promise.reject(new Error(`Unexpected RPC: ${method}`)),
113
+ )
114
+ const model = stateModelFactory().create({ type: 'GraphGenomeView' })
115
+ await model.loadGFA(GFA, 'wide')
116
+ model.startRenderingBackend(fakeRenderer([]))
117
+ model.setWidth(1000)
118
+ // past the zoom-to-fit autorun and the pan/zoom debounce, so the transform
119
+ // has stopped moving on its own before a test moves it
120
+ await sleep(400)
121
+ return model
122
+ }
123
+
124
+ // jbrowse-components' `installClearHoverOnViewportChange` rule, on this view's
125
+ // own three axes. A hover is set by a mousemove; a wheel zoom, a toolbar zoom
126
+ // and a pan all move the drawing under a stationary cursor and fire no pointer
127
+ // event, so without this the tooltip goes on naming a node that has moved away
128
+ // and `hoverHighlight` goes on publishing its span to the paired linear view.
129
+ test('a zoom drops a hover the pointer can no longer be over', async () => {
130
+ const model = await drawnModel()
131
+ model.setHoveredNode('1+')
132
+ model.setHoveredEdge(0)
133
+ model.setSelectedNode('1+')
134
+
135
+ model.zoom(1.5, 100, 100)
136
+
137
+ expect(model.hoveredNode).toBeNull()
138
+ expect(model.hoveredEdge).toBeNull()
139
+ // A click is a choice, and the content moving does not unmake it.
140
+ expect(model.selectedNode).toBe('1+')
141
+ })
142
+
143
+ test('a pan drops it too', async () => {
144
+ const model = await drawnModel()
145
+ model.setHoveredNode('1+')
146
+ model.setTransform(model.scale, model.translateX + 40, model.translateY)
147
+ expect(model.hoveredNode).toBeNull()
148
+ })
149
+
150
+ // The two signals name different events and are read by different consumers:
151
+ // `viewportDirty` says the window onto the drawing moved past what was built,
152
+ // `positionsVersion` says the drawing itself did. Conflating them rebuilt a
153
+ // 12k-edge hit index on the first mousemove after every pan, for an index that
154
+ // is in layout units and had not changed.
155
+ test('a pan past the built window moves the viewport signal and a node drag moves the positions one', async () => {
156
+ const model = await drawnModel()
157
+ const dirty = model.viewportDirty
158
+ const positions = model.positionsVersion
159
+
160
+ model.setTransform(model.scale, model.translateX + 5 * model.width, 0)
161
+ await sleep(300)
162
+ expect(model.viewportDirty).toBeGreaterThan(dirty)
163
+ expect(model.positionsVersion).toBe(positions)
164
+
165
+ model.moveNode('1+', 10, 10)
166
+ await sleep(100)
167
+ expect(model.positionsVersion).toBe(positions + 1)
168
+ })
169
+
170
+ // A build covers a pane on every side of the one shown, so an ordinary pan is
171
+ // a repaint of strokes already uploaded: no debounce, no blank margin, no
172
+ // rebuild. A zoom changes what the strokes are and rebuilds however small.
173
+ test('a pan inside the built window rebuilds nothing, a zoom always does', async () => {
174
+ const model = await drawnModel()
175
+ const dirty = model.viewportDirty
176
+
177
+ model.setTransform(model.scale, model.translateX + 40, model.translateY + 40)
178
+ await sleep(300)
179
+ expect(model.viewportDirty).toBe(dirty)
180
+
181
+ model.zoom(1.01, 100, 100)
182
+ await sleep(300)
183
+ expect(model.viewportDirty).toBe(dirty + 1)
184
+ })
185
+
186
+ // The three consumers of a path colour disagreed about how many paths is too
187
+ // many: only the node stripes stopped. A Minigraph-Cactus cut writes one W
188
+ // record per haplotype and walks every backbone link with all of them, so the
189
+ // two that did not stop drew a stroke per path per edge — measured at 99,800
190
+ // strokes and 64 ms of geometry for 200 paths over 499 links — under a key
191
+ // hundreds of rows tall, naming stripes that were not drawn.
192
+ function pathGFA(count: number) {
193
+ const paths = Array.from(
194
+ { length: count },
195
+ (_, i) => `P\tHG${i}#1#chr\t1+,2+\t*`,
196
+ ).join('\n')
197
+ return `H\tVN:Z:1.0\nS\t1\tACGT\nS\t2\tGGCC\nL\t1\t+\t2\t+\t0M\n${paths}\n`
198
+ }
199
+
200
+ async function pathModel(count: number) {
201
+ mockRpcCall.mockImplementation((_id: string, method: string) =>
202
+ method === 'GraphComputeLayout'
203
+ ? Promise.resolve({ result: WIDE_LAYOUT, duration: 5 })
204
+ : Promise.reject(new Error(`Unexpected RPC: ${method}`)),
205
+ )
206
+ const model = stateModelFactory().create({ type: 'GraphGenomeView' })
207
+ await model.loadGFA(pathGFA(count), 'paths')
208
+ model.setDrawPaths(true)
209
+ return model
210
+ }
211
+
212
+ test('at the colour cap the paths are drawn and named', async () => {
213
+ const model = await pathModel(MAX_PATH_COLORS)
214
+ expect(model.pathCount).toBe(MAX_PATH_COLORS)
215
+ expect(model.effectiveDrawPaths).toBe(true)
216
+ expect(model.pathLegend).toHaveLength(MAX_PATH_COLORS)
217
+ })
218
+
219
+ test('past it the whole of drawPaths resolves off, key included', async () => {
220
+ const model = await pathModel(MAX_PATH_COLORS + 1)
221
+ expect(model.pathCount).toBe(MAX_PATH_COLORS + 1)
222
+ // the switch keeps the user's choice; what the drawing does with it is here
223
+ expect(model.drawPaths).toBe(true)
224
+ expect(model.effectiveDrawPaths).toBe(false)
225
+ expect(model.pathLegend).toEqual([])
226
+ })