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,212 @@
1
+ import { orderedLayout, referenceOrder } from './orderedLayout'
2
+ import { ROW_HEIGHT_PX } from './rowSpacing'
3
+ import { parseGFA } from '../../gfa-core/index'
4
+ import { convertGFAToGraph } from '../gfa/gfaConverter'
5
+
6
+ // chr1 runs v1->v2->v3->v4; two single-segment alleles, a1 (foo) and a2 (bar),
7
+ // each replace v3 between the same anchors, so they share a layer and have to
8
+ // take different lanes. The file lists the L-lines in reference direction.
9
+ const BUBBLE = `S\tv1\tAAAAA\tLN:i:5\tSN:Z:chr1\tSO:i:0\tSR:i:0
10
+ S\tv2\tCCC\tLN:i:3\tSN:Z:chr1\tSO:i:5\tSR:i:0
11
+ S\tv3\tGG\tLN:i:2\tSN:Z:chr1\tSO:i:8\tSR:i:0
12
+ S\tv4\tTTTTTTT\tLN:i:7\tSN:Z:chr1\tSO:i:10\tSR:i:0
13
+ S\ta1\tAC\tLN:i:2\tSN:Z:foo\tSO:i:8\tSR:i:1
14
+ S\ta2\tT\tLN:i:1\tSN:Z:bar\tSO:i:8\tSR:i:2
15
+ L\tv1\t+\tv2\t+\t0M
16
+ L\tv2\t+\tv3\t+\t0M
17
+ L\tv3\t+\tv4\t+\t0M
18
+ L\tv2\t+\ta1\t+\t0M
19
+ L\ta1\t+\tv4\t+\t0M
20
+ L\tv2\t+\ta2\t+\t0M
21
+ L\ta2\t+\tv4\t+\t0M`
22
+
23
+ const BACKBONE = ['v1+', 'v2+', 'v3+', 'v4+']
24
+
25
+ function graphOf(gfa: string) {
26
+ return convertGFAToGraph(parseGFA(gfa))
27
+ }
28
+
29
+ function layout(gfa: string) {
30
+ return orderedLayout(graphOf(gfa))!.nodePositions
31
+ }
32
+
33
+ const centre = (seg: { x: number }[]) => (seg[0]!.x + seg.at(-1)!.x) / 2
34
+
35
+ test('backbone x strictly increases in reference order', () => {
36
+ const pos = layout(BUBBLE)
37
+ const xs = BACKBONE.map(id => centre(pos[id]!))
38
+ for (let i = 1; i < xs.length; i++) {
39
+ expect(xs[i]!).toBeGreaterThan(xs[i - 1]!)
40
+ }
41
+ })
42
+
43
+ test('no two nodes share a layer and a lane', () => {
44
+ const pos = layout(BUBBLE)
45
+ const cells = Object.values(pos).map(seg => `${centre(seg)},${seg[0]!.y}`)
46
+ expect(new Set(cells).size).toBe(cells.length)
47
+ })
48
+
49
+ test('the backbone holds lane 0 and nothing else does', () => {
50
+ const pos = layout(BUBBLE)
51
+ for (const id of BACKBONE) {
52
+ expect(pos[id]!.every(p => p.y === 0)).toBe(true)
53
+ }
54
+ expect(pos['a1+']![0]!.y).not.toBe(0)
55
+ expect(pos['a2+']![0]!.y).not.toBe(0)
56
+ })
57
+
58
+ test('every edge runs left to right', () => {
59
+ const graph = graphOf(BUBBLE)
60
+ const pos = orderedLayout(graph)!.nodePositions
61
+ for (const { from, to } of graph.edges) {
62
+ expect(centre(pos[from]!)).toBeLessThan(centre(pos[to]!))
63
+ }
64
+ })
65
+
66
+ test("a bubble's two alleles share a layer on different lanes", () => {
67
+ const pos = layout(BUBBLE)
68
+ const a1 = pos['a1+']!
69
+ const a2 = pos['a2+']!
70
+ expect(centre(a1)).toBe(centre(a2))
71
+ expect(a1[0]!.y).not.toBe(a2[0]!.y)
72
+ // both one or more lane pitches off the reference line
73
+ expect(Math.abs(a1[0]!.y) % ROW_HEIGHT_PX).toBe(0)
74
+ expect(Math.abs(a2[0]!.y)).toBeGreaterThanOrEqual(ROW_HEIGHT_PX)
75
+ })
76
+
77
+ test('an allele sits between its anchors, not under one of them', () => {
78
+ const pos = layout(BUBBLE)
79
+ expect(centre(pos['a1+']!)).toBeGreaterThan(centre(pos['v2+']!))
80
+ expect(centre(pos['a1+']!)).toBeLessThan(centre(pos['v4+']!))
81
+ })
82
+
83
+ test('a node is a two-point polyline as wide as the log of its bp', () => {
84
+ const pos = layout(BUBBLE)
85
+ const seg = pos['v4+']!
86
+ expect(seg).toHaveLength(2)
87
+ expect(seg[1]!.x - seg[0]!.x).toBeCloseTo(10 + 8 * Math.log2(8), 5)
88
+ })
89
+
90
+ test('x is order, and y a lane pitch in screen px', () => {
91
+ const result = orderedLayout(graphOf(BUBBLE))!
92
+ expect(result.referenceAxis).toBe(false)
93
+ expect(result.pixelRows).toBe(true)
94
+ expect(result.rowLabels).toBeUndefined()
95
+ expect(result.alleleDeletions).toBeUndefined()
96
+ })
97
+
98
+ test('a graph without a backbone gets no ordered layout', () => {
99
+ const plain = `S\t1\tACGT
100
+ S\t2\tGGCC
101
+ L\t1\t+\t2\t+\t0M`
102
+ expect(orderedLayout(graphOf(plain))).toBeUndefined()
103
+ })
104
+
105
+ test('the same graph always gets the same drawing', () => {
106
+ const graph = graphOf(BUBBLE)
107
+ expect(orderedLayout(graph)).toEqual(orderedLayout(graph))
108
+ expect(referenceOrder(graph)).toEqual(referenceOrder(graphOf(BUBBLE)))
109
+ })
110
+
111
+ test('reference order is backbone by offset, alleles just past their anchor', () => {
112
+ expect(referenceOrder(graphOf(BUBBLE))).toEqual([
113
+ 'v1+',
114
+ 'v2+',
115
+ 'a1+',
116
+ 'a2+',
117
+ 'v3+',
118
+ 'v4+',
119
+ ])
120
+ })
121
+
122
+ // r1 r2 r3 r4 along chr1 at 0, 1, 2, 3, a base apart as a base-level graph's
123
+ // backbone is, with whatever `extra` hangs off it.
124
+ function baseLevel(extra: string[]) {
125
+ return graphOf(
126
+ [
127
+ ...[0, 1, 2, 3].map(
128
+ i => `S\tr${i + 1}\tA\tLN:i:1\tSN:Z:chr1\tSO:i:${i}\tSR:i:0`,
129
+ ),
130
+ ...[1, 2, 3].map(i => `L\tr${i}\t+\tr${i + 1}\t+\t0M`),
131
+ ...extra,
132
+ ].join('\n'),
133
+ )
134
+ }
135
+
136
+ const off = (name: string) => `S\t${name}\tA\tLN:i:1\tSN:Z:alt\tSO:i:0\tSR:i:1`
137
+ const link = (a: string, b: string) => `L\t${a}\t+\t${b}\t+\t0M`
138
+
139
+ // The search from the backbone meets itself in the middle of x1..x4, so x3 and
140
+ // x4 are claimed from r4. Sorted after it, their links into r4 turned round.
141
+ test('both halves of a multi-node allele sort between its anchors', () => {
142
+ const chain = ['r1', 'x1', 'x2', 'x3', 'x4', 'r4']
143
+ const graph = baseLevel([
144
+ ...['x1', 'x2', 'x3', 'x4'].map(off),
145
+ ...chain.slice(1).map((n, i) => link(chain[i]!, n)),
146
+ ])
147
+ const order = referenceOrder(graph)
148
+ const places = chain.map(n => order.indexOf(`${n}+`))
149
+ expect(places).toEqual([...places].sort((a, b) => a - b))
150
+ })
151
+
152
+ // Six nodes off r1, whose next backbone node is a base away. Nudged half a bp
153
+ // per step, the chain's keys walked past r2, r3 and r4.
154
+ test('a long allele stays beside its anchor however close the next node is', () => {
155
+ const names = ['y1', 'y2', 'y3', 'y4', 'y5', 'y6']
156
+ const graph = baseLevel([
157
+ ...names.map(off),
158
+ link('r1', 'y1'),
159
+ ...names.slice(1).map((n, i) => link(names[i]!, n)),
160
+ ])
161
+ expect(referenceOrder(graph)).toEqual([
162
+ 'r1+',
163
+ ...names.map(n => `${n}+`),
164
+ 'r2+',
165
+ 'r3+',
166
+ 'r4+',
167
+ ])
168
+ })
169
+
170
+ // t1 and t2 are claimed from r4, and the left reaches them only at t1. t2 is a
171
+ // dead end of the search that the left never reached, so steps from r4 say
172
+ // nothing about where it lies: sorted first, as the far half of a chain is, it
173
+ // would have nothing before it. The run stays after its anchor.
174
+ test('a run with a dead end the left never reached stays after its anchor', () => {
175
+ const graph = baseLevel([
176
+ ...['s1', 't1', 't2'].map(off),
177
+ link('r1', 's1'),
178
+ link('s1', 't1'),
179
+ link('t1', 'r4'),
180
+ link('t1', 't2'),
181
+ ])
182
+ const order = referenceOrder(graph)
183
+ const at = (name: string) => order.indexOf(`${name}+`)
184
+ expect(at('t1')).toBeGreaterThan(at('r4'))
185
+ expect(at('t2')).toBeGreaterThan(at('t1'))
186
+ })
187
+
188
+ // A walk says where its nodes lie. w2 links only to r4, so the links alone put
189
+ // it after r4; the walk enters it from w1, off r1.
190
+ const WALKED = [
191
+ ...['w1', 'w2'].map(off),
192
+ link('r1', 'w1'),
193
+ link('w2', 'r4'),
194
+ 'W\tref\t0\tchr1\t0\t4\t>r1>r2>r3>r4',
195
+ ]
196
+
197
+ test('a walk places the nodes it visits after the node it came from', () => {
198
+ const graph = baseLevel([...WALKED, 'W\thap\t1\tctg\t0\t4\t>r1>w1>w2>r4'])
199
+ expect(referenceOrder(graph)).toEqual([
200
+ 'r1+',
201
+ 'w1+',
202
+ 'w2+',
203
+ 'r2+',
204
+ 'r3+',
205
+ 'r4+',
206
+ ])
207
+ })
208
+
209
+ test('a walk on the reverse strand is read end-first', () => {
210
+ const graph = baseLevel([...WALKED, 'W\thap\t1\tctg\t0\t4\t<r4<w2<w1<r1'])
211
+ expect(referenceOrder(graph).slice(0, 3)).toEqual(['r1+', 'w1+', 'w2+'])
212
+ })
@@ -0,0 +1,396 @@
1
+ import { ROW_HEIGHT_PX } from './rowSpacing'
2
+ import { isBackbone } from '../anchoredNodes'
3
+
4
+ import type { AnchoredNode } from '../anchoredNodes'
5
+ import type { Graph, GraphNode, LayoutResult, NodeSegment } from '../types'
6
+
7
+ // Reference-ordered layered layout: x is reference ORDER, not bp. Backbone
8
+ // segments take increasing layers by stable offset, every other node the layer
9
+ // between the anchors it hangs off, and a node is as wide as the log of its bp,
10
+ // so a SNP allele gets the same room as a 10 kb segment and a bubble reads as a
11
+ // lens rather than a bar under a line. y is a lane in screen px with the
12
+ // reference pinned at lane 0 (docs/layout-experiments.md, experiment 3).
13
+
14
+ const GUTTER = 12
15
+ const LANE_PX = ROW_HEIGHT_PX
16
+
17
+ function nodeWidth(node: GraphNode) {
18
+ return 10 + 8 * Math.log2(1 + node.length)
19
+ }
20
+
21
+ function compare<T extends string | number>(a: T, b: T) {
22
+ return a < b ? -1 : a > b ? 1 : 0
23
+ }
24
+
25
+ // Where a node sorts: `at` is the rank of a placed node, the backbone and what
26
+ // the walks visit, and a node the search claims sorts before or after the one
27
+ // that claimed it, by its steps from it. A tuple rather than an offset nudged
28
+ // per step: on a base-level graph the backbone nodes are a bp apart, and a
29
+ // chain a few nodes long nudged its keys past the ones that follow.
30
+ interface OrderKey {
31
+ at: number
32
+ side: -1 | 0 | 1
33
+ hops: number
34
+ }
35
+
36
+ function compareKeys(a: OrderKey, b: OrderKey) {
37
+ return (
38
+ compare(a.at, b.at) || compare(a.side, b.side) || compare(a.hops, b.hops)
39
+ )
40
+ }
41
+
42
+ const UNREACHED: OrderKey = { at: Infinity, side: 0, hops: 0 }
43
+
44
+ // Every node id in reference order: backbone by stable offset, every other node
45
+ // beside a backbone node (orderKeys), nodes nothing reaches last. Name breaks
46
+ // ties, so the order is a strict total order and the same graph always gets
47
+ // the same one.
48
+ export function referenceOrder(graph: Graph) {
49
+ const byId = new Map(graph.nodes.map(n => [n.id, n]))
50
+ const key = orderKeys(graph)
51
+ return graph.nodes
52
+ .map(n => n.id)
53
+ .sort(
54
+ (a, b) =>
55
+ compareKeys(key.get(a) ?? UNREACHED, key.get(b) ?? UNREACHED) ||
56
+ compare(byId.get(a)!.name, byId.get(b)!.name) ||
57
+ compare(a, b),
58
+ )
59
+ }
60
+
61
+ // A walk states where its nodes lie, which the links alone do not. Starting
62
+ // from the backbone in offset order, each node a walk visits for the first time
63
+ // goes into the order immediately after the node that walk came from, and the
64
+ // nodes a walk visits before it meets a placed one go immediately before it.
65
+ //
66
+ // That keeps every true boundary. A walk's step joins two nodes on the same
67
+ // side of a node every haplotype crosses once, unless one of them is that
68
+ // node, so by induction the nodes between two such boundaries stay one
69
+ // unbroken stretch of the order and no link among them reaches across. On the
70
+ // eight-haplotype KIV-2 cut (15,808 nodes) the walks imply 28 bubbles and this
71
+ // derives those 28, with 5 of 65,776 walk steps against the order. Placing
72
+ // every node by the links alone derived 21, with 23,308 steps against it.
73
+ //
74
+ // A walk on the reverse strand is read end-first.
75
+ function placeByWalks(graph: Graph, backbone: AnchoredNode[]) {
76
+ const isNode = new Set(graph.nodes.map(n => n.id))
77
+ const startOf = new Map(backbone.map(n => [n.id, n.stable.start]))
78
+ const next = new Map<string, string | undefined>()
79
+ const prev = new Map<string, string | undefined>()
80
+ backbone.forEach((n, i) => {
81
+ prev.set(n.id, backbone[i - 1]?.id)
82
+ next.set(n.id, backbone[i + 1]?.id)
83
+ })
84
+ let head = backbone[0]?.id
85
+ const insertAfter = (at: string, id: string) => {
86
+ const after = next.get(at)
87
+ next.set(at, id)
88
+ prev.set(id, at)
89
+ next.set(id, after)
90
+ if (after !== undefined) {
91
+ prev.set(after, id)
92
+ }
93
+ }
94
+ const insertBefore = (at: string, id: string) => {
95
+ const before = prev.get(at)
96
+ if (before === undefined) {
97
+ head = id
98
+ prev.set(id, undefined)
99
+ next.set(id, at)
100
+ prev.set(at, id)
101
+ } else {
102
+ insertAfter(before, id)
103
+ }
104
+ }
105
+
106
+ for (const path of graph.paths ?? []) {
107
+ let rising = 0
108
+ let falling = 0
109
+ let last: number | undefined
110
+ for (const id of path.nodeIds) {
111
+ const start = startOf.get(id)
112
+ if (start !== undefined) {
113
+ if (last !== undefined && start !== last) {
114
+ if (start > last) {
115
+ rising++
116
+ } else {
117
+ falling++
118
+ }
119
+ }
120
+ last = start
121
+ }
122
+ }
123
+ const ids = falling > rising ? [...path.nodeIds].reverse() : path.nodeIds
124
+ let from: string | undefined
125
+ const leading = new Set<string>()
126
+ for (const id of ids) {
127
+ if (next.has(id)) {
128
+ if (from === undefined) {
129
+ for (const lead of leading) {
130
+ insertBefore(id, lead)
131
+ }
132
+ }
133
+ from = id
134
+ } else if (isNode.has(id)) {
135
+ if (from === undefined) {
136
+ leading.add(id)
137
+ } else {
138
+ insertAfter(from, id)
139
+ from = id
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ const placed: string[] = []
146
+ for (let id = head; id !== undefined; id = next.get(id)) {
147
+ placed.push(id)
148
+ }
149
+ return placed
150
+ }
151
+
152
+ // The nodes no walk visits, which is every off-reference node of a graph with
153
+ // no walks: each goes beside the nearest placed node that reaches it (BFS over
154
+ // the links as undirected, seeded in order so a tie goes left).
155
+ //
156
+ // That search meets itself in the middle of an allele, so the far half of a
157
+ // chain A>X1>X2>B is claimed from B, its RIGHT anchor. Placed after B, as every
158
+ // claimed node once was, X2 sorted past the node it leads into, its link was
159
+ // turned round, and the insertion stopped being a bubble. So a run of nodes
160
+ // claimed from one placed node lies BEFORE it, farthest node first, when it is
161
+ // such a far half: it touches nothing right of that node, and every dead end of
162
+ // the search inside it is a place the search from the left arrived. Steps are
163
+ // a position only along a chain. In a tangle the farthest node is a dead end
164
+ // with nothing before it, and sorted first its links to its real neighbours
165
+ // sweep across every boundary between, so any other run stays after.
166
+ function placeBySearch(
167
+ adjacent: Map<string, string[]>,
168
+ key: Map<string, OrderKey>,
169
+ ) {
170
+ const queue = [...key.entries()]
171
+ .sort(([a, ka], [b, kb]) => compareKeys(ka, kb) || compare(a, b))
172
+ .map(([id]) => id)
173
+ const placed = queue.length
174
+ // the queue grows while the loop walks it, which for-of follows
175
+ for (const id of queue) {
176
+ const from = key.get(id)!
177
+ for (const nb of adjacent.get(id) ?? []) {
178
+ if (!key.has(nb)) {
179
+ key.set(nb, { at: from.at, side: 1, hops: from.hops + 1 })
180
+ queue.push(nb)
181
+ }
182
+ }
183
+ }
184
+
185
+ const searched = new Set(queue.slice(placed))
186
+ const sided = new Set<string>()
187
+ for (const seed of searched) {
188
+ if (sided.has(seed)) {
189
+ continue
190
+ }
191
+ const { at } = key.get(seed)!
192
+ const run = [seed]
193
+ sided.add(seed)
194
+ let farHalf = true
195
+ for (const id of run) {
196
+ const { hops } = key.get(id)!
197
+ let deadEnd = true
198
+ let touchesLeft = false
199
+ for (const nb of adjacent.get(id) ?? []) {
200
+ const other = key.get(nb)!
201
+ if (other.at === at && searched.has(nb)) {
202
+ deadEnd &&= other.hops <= hops
203
+ if (!sided.has(nb)) {
204
+ sided.add(nb)
205
+ run.push(nb)
206
+ }
207
+ } else if (other.at !== at) {
208
+ touchesLeft ||= other.at < at
209
+ farHalf &&= other.at < at
210
+ }
211
+ }
212
+ farHalf &&= !deadEnd || touchesLeft
213
+ }
214
+ if (farHalf) {
215
+ for (const id of run) {
216
+ const k = key.get(id)!
217
+ key.set(id, { ...k, side: -1, hops: -k.hops })
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ function orderKeys(graph: Graph) {
224
+ const ids = new Set(graph.nodes.map(n => n.id))
225
+ const adjacent = new Map<string, string[]>()
226
+ for (const e of graph.edges) {
227
+ if (!ids.has(e.from) || !ids.has(e.to)) {
228
+ continue
229
+ }
230
+ ;(adjacent.get(e.from) ?? adjacent.set(e.from, []).get(e.from)!).push(e.to)
231
+ ;(adjacent.get(e.to) ?? adjacent.set(e.to, []).get(e.to)!).push(e.from)
232
+ }
233
+ const backbone = graph.nodes
234
+ .filter(isBackbone)
235
+ .sort(
236
+ (a, b) =>
237
+ compare(a.stable.start, b.stable.start) || compare(a.name, b.name),
238
+ )
239
+ const key = new Map<string, OrderKey>()
240
+ placeByWalks(graph, backbone).forEach((id, rank) => {
241
+ key.set(id, { at: rank, side: 0, hops: 0 })
242
+ })
243
+ placeBySearch(adjacent, key)
244
+ return key
245
+ }
246
+
247
+ export interface LayeredGraph {
248
+ layers: string[][]
249
+ layerOf: Map<string, number>
250
+ // predecessors in layer order, for the y sweep
251
+ preds: Map<string, string[]>
252
+ }
253
+
254
+ // The layered DAG the ordered layout draws and the bubble decomposition reads
255
+ // (bubbles/bubblesFromGraph.ts), from one function so the two cannot disagree
256
+ // about which nodes share a layer.
257
+ export function layerGraph(graph: Graph): LayeredGraph {
258
+ const byId = new Map(graph.nodes.map(n => [n.id, n]))
259
+ const order = referenceOrder(graph)
260
+ const key = new Map(order.map((id, i) => [id, i]))
261
+ const isRef = (id: string) => isBackbone(byId.get(id)!)
262
+
263
+ // Every edge directed from lower reference key to higher, so the graph is
264
+ // acyclic by construction and no cycle-removal heuristic can misplace the
265
+ // reference.
266
+ const preds = new Map<string, string[]>(order.map(id => [id, []]))
267
+ const succs = new Map<string, string[]>(order.map(id => [id, []]))
268
+ const link = (a: string, b: string) => {
269
+ preds.get(b)!.push(a)
270
+ succs.get(a)!.push(b)
271
+ }
272
+ for (const e of graph.edges) {
273
+ if (e.from === e.to || !key.has(e.from) || !key.has(e.to)) {
274
+ continue
275
+ }
276
+ if (key.get(e.from)! < key.get(e.to)!) {
277
+ link(e.from, e.to)
278
+ } else {
279
+ link(e.to, e.from)
280
+ }
281
+ }
282
+ // The backbone chained by virtual edges, so the reference stays monotone even
283
+ // where adjacent segments are not linked inside the cut.
284
+ const backbone = order.filter(isRef)
285
+ for (let i = 1; i < backbone.length; i++) {
286
+ link(backbone[i - 1]!, backbone[i]!)
287
+ }
288
+
289
+ // Longest-path layering; the key order is a topological order of this DAG.
290
+ const layerOf = new Map<string, number>()
291
+ for (const id of order) {
292
+ let l = 0
293
+ for (const p of preds.get(id)!) {
294
+ l = Math.max(l, layerOf.get(p)! + 1)
295
+ }
296
+ layerOf.set(id, l)
297
+ }
298
+ // Then each allele slides right to centre between the layer its predecessors
299
+ // force and the one its successors allow, so a short allele sits in the
300
+ // middle of its bubble rather than hugging the left anchor.
301
+ for (let i = order.length - 1; i >= 0; i--) {
302
+ const id = order[i]!
303
+ const s = succs.get(id)!
304
+ if (isRef(id) || s.length === 0) {
305
+ continue
306
+ }
307
+ let minSucc = Infinity
308
+ for (const t of s) {
309
+ minSucc = Math.min(minSucc, layerOf.get(t)!)
310
+ }
311
+ const lo = layerOf.get(id)!
312
+ const hi = Math.max(lo, minSucc - 1)
313
+ layerOf.set(id, Math.floor((lo + hi) / 2))
314
+ }
315
+
316
+ const layers: string[][] = []
317
+ for (const id of order) {
318
+ ;(layers[layerOf.get(id)!] ??= []).push(id)
319
+ }
320
+ return { layers, layerOf, preds }
321
+ }
322
+
323
+ export function orderedLayout(graph: Graph): LayoutResult | undefined {
324
+ if (!graph.nodes.some(isBackbone)) {
325
+ return undefined
326
+ }
327
+ const byId = new Map(graph.nodes.map(n => [n.id, n]))
328
+ const isRef = (id: string) => isBackbone(byId.get(id)!)
329
+ const { layers, layerOf: layer, preds } = layerGraph(graph)
330
+
331
+ const layerWidth = layers.map(ids => {
332
+ let w = 0
333
+ for (const id of ids) {
334
+ w = Math.max(w, nodeWidth(byId.get(id)!))
335
+ }
336
+ return w
337
+ })
338
+ const layerX: number[] = []
339
+ let x = 0
340
+ for (let l = 0; l < layers.length; l++) {
341
+ layerX[l] = x
342
+ x += layerWidth[l]! + GUTTER
343
+ }
344
+
345
+ // y: sweep the layers left to right, each allele at the barycenter of its
346
+ // placed predecessors, then at the nearest FREE lane to it. Lane 0 is the
347
+ // reference line and stays reserved even in a layer with no backbone node,
348
+ // or an allele between two backbone segments reads as reference.
349
+ const y = new Map<string, number>()
350
+ for (const ids of layers) {
351
+ const free: { id: string; ideal: number }[] = []
352
+ for (const id of ids) {
353
+ if (isRef(id)) {
354
+ y.set(id, 0)
355
+ continue
356
+ }
357
+ const placed = preds.get(id)!.filter(p => y.has(p))
358
+ let ideal = 0
359
+ for (const p of placed) {
360
+ ideal += y.get(p)! / placed.length
361
+ }
362
+ // a node straight off the backbone wants to leave it, not sit on it
363
+ free.push({ id, ideal: ideal === 0 ? LANE_PX : ideal })
364
+ }
365
+ free.sort((a, b) => a.ideal - b.ideal)
366
+ const taken = new Set([0])
367
+ for (const { id, ideal } of free) {
368
+ let lane = Math.round(ideal / LANE_PX)
369
+ if (lane === 0) {
370
+ lane = ideal >= 0 ? 1 : -1
371
+ }
372
+ for (let d = 1; taken.has(lane); d++) {
373
+ if (!taken.has(lane + d)) {
374
+ lane += d
375
+ } else if (!taken.has(lane - d)) {
376
+ lane -= d
377
+ }
378
+ }
379
+ taken.add(lane)
380
+ y.set(id, lane * LANE_PX)
381
+ }
382
+ }
383
+
384
+ const nodePositions: Record<string, NodeSegment[]> = {}
385
+ for (const n of graph.nodes) {
386
+ const l = layer.get(n.id)!
387
+ const w = nodeWidth(n)
388
+ const cx = layerX[l]! + layerWidth[l]! / 2
389
+ const ny = y.get(n.id)!
390
+ nodePositions[n.id] = [
391
+ { x: cx - w / 2, y: ny },
392
+ { x: cx + w / 2, y: ny },
393
+ ]
394
+ }
395
+ return { nodePositions, referenceAxis: false, pixelRows: true }
396
+ }