codebase-onboarder 0.5.0 → 0.7.0

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 (448) hide show
  1. package/README.md +196 -47
  2. package/bin/build-if-source.js +23 -0
  3. package/cli/commands.d.ts +26 -0
  4. package/cli/commands.js +836 -811
  5. package/cli/commands.js.map +1 -0
  6. package/cli/explorer/advanced.d.ts +14 -0
  7. package/cli/explorer/advanced.js +449 -0
  8. package/cli/explorer/advanced.js.map +1 -0
  9. package/cli/explorer/app.d.ts +1 -0
  10. package/cli/explorer/app.js +396 -229
  11. package/cli/explorer/app.js.map +1 -0
  12. package/cli/explorer/assistant.d.ts +9 -0
  13. package/cli/explorer/assistant.js +58 -0
  14. package/cli/explorer/assistant.js.map +1 -0
  15. package/cli/explorer/commands.d.ts +17 -0
  16. package/cli/explorer/commands.js +400 -162
  17. package/cli/explorer/commands.js.map +1 -0
  18. package/cli/explorer/featureViews.d.ts +124 -0
  19. package/cli/explorer/featureViews.js +179 -0
  20. package/cli/explorer/featureViews.js.map +1 -0
  21. package/cli/explorer/github.d.ts +21 -0
  22. package/cli/explorer/github.js +73 -0
  23. package/cli/explorer/github.js.map +1 -0
  24. package/cli/explorer/graphs.d.ts +7 -0
  25. package/cli/explorer/graphs.js +173 -0
  26. package/cli/explorer/graphs.js.map +1 -0
  27. package/cli/explorer/picker.d.ts +22 -0
  28. package/cli/explorer/picker.js +150 -0
  29. package/cli/explorer/picker.js.map +1 -0
  30. package/cli/explorer/report.d.ts +85 -0
  31. package/cli/explorer/report.js +60 -0
  32. package/cli/explorer/report.js.map +1 -0
  33. package/cli/explorer/session.d.ts +449 -0
  34. package/cli/explorer/session.js +184 -95
  35. package/cli/explorer/session.js.map +1 -0
  36. package/cli/explorer/views.d.ts +19 -0
  37. package/cli/explorer/views.js +496 -412
  38. package/cli/explorer/views.js.map +1 -0
  39. package/cli/explorer/wrap.d.ts +1 -0
  40. package/cli/explorer/wrap.js +33 -0
  41. package/cli/explorer/wrap.js.map +1 -0
  42. package/cli/main.d.ts +1 -0
  43. package/cli/main.js +235 -138
  44. package/cli/main.js.map +1 -0
  45. package/cli/prompt.d.ts +5 -0
  46. package/cli/prompt.js +84 -80
  47. package/cli/prompt.js.map +1 -0
  48. package/cli/scanOptions.d.ts +2 -0
  49. package/cli/scanOptions.js +11 -0
  50. package/cli/scanOptions.js.map +1 -0
  51. package/cli/ui.d.ts +33 -0
  52. package/cli/ui.js +55 -35
  53. package/cli/ui.js.map +1 -0
  54. package/cli/wizard.d.ts +187 -0
  55. package/cli/wizard.js +304 -284
  56. package/cli/wizard.js.map +1 -0
  57. package/package.json +15 -4
  58. package/public/app.d.ts +1 -0
  59. package/public/app.js +1057 -1009
  60. package/public/app.js.map +1 -0
  61. package/public/browser.d.ts +68 -0
  62. package/public/browser.js +2 -0
  63. package/public/browser.js.map +1 -0
  64. package/public/index.html +30 -13
  65. package/public/js/about.d.ts +6 -0
  66. package/public/js/about.js +165 -172
  67. package/public/js/about.js.map +1 -0
  68. package/public/js/aiDraft.d.ts +3 -0
  69. package/public/js/aiDraft.js +116 -124
  70. package/public/js/aiDraft.js.map +1 -0
  71. package/public/js/analysisPanel.d.ts +7 -0
  72. package/public/js/analysisPanel.js +166 -166
  73. package/public/js/analysisPanel.js.map +1 -0
  74. package/public/js/analysisReport.d.ts +38 -0
  75. package/public/js/analysisReport.js +105 -110
  76. package/public/js/analysisReport.js.map +1 -0
  77. package/public/js/api.d.ts +26 -0
  78. package/public/js/api.js +176 -167
  79. package/public/js/api.js.map +1 -0
  80. package/public/js/atlas.d.ts +6 -0
  81. package/public/js/atlas.js +50 -58
  82. package/public/js/atlas.js.map +1 -0
  83. package/public/js/blameView.d.ts +2 -0
  84. package/public/js/blameView.js +36 -37
  85. package/public/js/blameView.js.map +1 -0
  86. package/public/js/codeTab.d.ts +3 -0
  87. package/public/js/codeTab.js +175 -180
  88. package/public/js/codeTab.js.map +1 -0
  89. package/public/js/codeViewer.d.ts +5 -0
  90. package/public/js/codeViewer.js +162 -150
  91. package/public/js/codeViewer.js.map +1 -0
  92. package/public/js/components/FileChip.d.ts +9 -0
  93. package/public/js/components/FileChip.js +45 -49
  94. package/public/js/components/FileChip.js.map +1 -0
  95. package/public/js/components/MetricSparkline.d.ts +7 -0
  96. package/public/js/components/MetricSparkline.js +35 -44
  97. package/public/js/components/MetricSparkline.js.map +1 -0
  98. package/public/js/components/RiskBadge.d.ts +9 -0
  99. package/public/js/components/RiskBadge.js +55 -63
  100. package/public/js/components/RiskBadge.js.map +1 -0
  101. package/public/js/deepAnalysisView.d.ts +99 -0
  102. package/public/js/deepAnalysisView.js +282 -302
  103. package/public/js/deepAnalysisView.js.map +1 -0
  104. package/public/js/diagramPane.d.ts +8 -0
  105. package/public/js/diagramPane.js +133 -135
  106. package/public/js/diagramPane.js.map +1 -0
  107. package/public/js/diffView.d.ts +6 -0
  108. package/public/js/diffView.js +222 -209
  109. package/public/js/diffView.js.map +1 -0
  110. package/public/js/docsView.d.ts +2 -0
  111. package/public/js/docsView.js +441 -442
  112. package/public/js/docsView.js.map +1 -0
  113. package/public/js/dom.d.ts +189 -0
  114. package/public/js/dom.js +5 -0
  115. package/public/js/dom.js.map +1 -0
  116. package/public/js/fileSourceBrowser.d.ts +5 -0
  117. package/public/js/fileSourceBrowser.js +33 -41
  118. package/public/js/fileSourceBrowser.js.map +1 -0
  119. package/public/js/flameGraph.d.ts +5 -0
  120. package/public/js/flameGraph.js +62 -72
  121. package/public/js/flameGraph.js.map +1 -0
  122. package/public/js/forceGraph.d.ts +10 -0
  123. package/public/js/forceGraph.js +581 -556
  124. package/public/js/forceGraph.js.map +1 -0
  125. package/public/js/heatmap.d.ts +4 -0
  126. package/public/js/heatmap.js +142 -164
  127. package/public/js/heatmap.js.map +1 -0
  128. package/public/js/highlight.d.ts +2 -0
  129. package/public/js/highlight.js +116 -107
  130. package/public/js/highlight.js.map +1 -0
  131. package/public/js/html.d.ts +2 -0
  132. package/public/js/html.js +14 -44
  133. package/public/js/html.js.map +1 -0
  134. package/public/js/insightsView.d.ts +5 -0
  135. package/public/js/insightsView.js +93 -111
  136. package/public/js/insightsView.js.map +1 -0
  137. package/public/js/inspector.d.ts +37 -0
  138. package/public/js/inspector.js +416 -445
  139. package/public/js/inspector.js.map +1 -0
  140. package/public/js/llm.d.ts +113 -0
  141. package/public/js/llm.js +233 -272
  142. package/public/js/llm.js.map +1 -0
  143. package/public/js/mapView.d.ts +3 -0
  144. package/public/js/mapView.js +121 -120
  145. package/public/js/mapView.js.map +1 -0
  146. package/public/js/markdown.d.ts +3 -0
  147. package/public/js/markdown.js +84 -91
  148. package/public/js/markdown.js.map +1 -0
  149. package/public/js/mindmap.d.ts +32 -0
  150. package/public/js/mindmap.js +244 -259
  151. package/public/js/mindmap.js.map +1 -0
  152. package/public/js/mobilePanels.d.ts +5 -0
  153. package/public/js/mobilePanels.js +50 -0
  154. package/public/js/mobilePanels.js.map +1 -0
  155. package/public/js/repoFiles.d.ts +12 -0
  156. package/public/js/repoFiles.js +28 -27
  157. package/public/js/repoFiles.js.map +1 -0
  158. package/public/js/reviewView.d.ts +28 -0
  159. package/public/js/reviewView.js +256 -0
  160. package/public/js/reviewView.js.map +1 -0
  161. package/public/js/sbomView.d.ts +3 -0
  162. package/public/js/sbomView.js +59 -67
  163. package/public/js/sbomView.js.map +1 -0
  164. package/public/js/scanCache.d.ts +16 -0
  165. package/public/js/scanCache.js +67 -65
  166. package/public/js/scanCache.js.map +1 -0
  167. package/public/js/search.d.ts +7 -0
  168. package/public/js/search.js +868 -800
  169. package/public/js/search.js.map +1 -0
  170. package/public/js/searchLocal.d.ts +42 -0
  171. package/public/js/searchLocal.js +87 -0
  172. package/public/js/searchLocal.js.map +1 -0
  173. package/public/js/serverSettings.d.ts +5 -0
  174. package/public/js/serverSettings.js +153 -164
  175. package/public/js/serverSettings.js.map +1 -0
  176. package/public/js/state.d.ts +798 -0
  177. package/public/js/state.js +114 -122
  178. package/public/js/state.js.map +1 -0
  179. package/public/js/tour.d.ts +6 -0
  180. package/public/js/tour.js +5 -7
  181. package/public/js/tour.js.map +1 -0
  182. package/public/js/transitions.d.ts +2 -0
  183. package/public/js/transitions.js +9 -10
  184. package/public/js/transitions.js.map +1 -0
  185. package/public/js/tree.d.ts +12 -0
  186. package/public/js/tree.js +157 -154
  187. package/public/js/tree.js.map +1 -0
  188. package/public/js/workflowsView.d.ts +3 -0
  189. package/public/js/workflowsView.js +15 -22
  190. package/public/js/workflowsView.js.map +1 -0
  191. package/public/search.css +495 -0
  192. package/public/styles.css +163 -346
  193. package/public/workbench.css +166 -0
  194. package/server/apiAuth.d.ts +3 -0
  195. package/server/apiAuth.js +18 -20
  196. package/server/apiAuth.js.map +1 -0
  197. package/server/apiDiff.d.ts +2 -0
  198. package/server/apiDiff.js +20 -18
  199. package/server/apiDiff.js.map +1 -0
  200. package/server/apiDocs.d.ts +2 -0
  201. package/server/apiDocs.js +38 -40
  202. package/server/apiDocs.js.map +1 -0
  203. package/server/apiFile.d.ts +1 -0
  204. package/server/apiFile.js +14 -17
  205. package/server/apiFile.js.map +1 -0
  206. package/server/apiGitBlame.d.ts +2 -0
  207. package/server/apiGitBlame.js +50 -51
  208. package/server/apiGitBlame.js.map +1 -0
  209. package/server/apiMcp.d.ts +4 -0
  210. package/server/apiMcp.js +64 -66
  211. package/server/apiMcp.js.map +1 -0
  212. package/server/apiReview.d.ts +2 -0
  213. package/server/apiReview.js +45 -0
  214. package/server/apiReview.js.map +1 -0
  215. package/server/apiScan.d.ts +5 -0
  216. package/server/apiScan.js +90 -71
  217. package/server/apiScan.js.map +1 -0
  218. package/server/apiSearch.d.ts +33 -0
  219. package/server/apiSearch.js +184 -200
  220. package/server/apiSearch.js.map +1 -0
  221. package/server/apiSettings.d.ts +4 -0
  222. package/server/apiSettings.js +84 -85
  223. package/server/apiSettings.js.map +1 -0
  224. package/server/apiTools.d.ts +3 -0
  225. package/server/apiTools.js +55 -56
  226. package/server/apiTools.js.map +1 -0
  227. package/server/auth.d.ts +5 -0
  228. package/server/auth.js +54 -51
  229. package/server/auth.js.map +1 -0
  230. package/server/config.d.ts +42 -0
  231. package/server/config.js +189 -194
  232. package/server/config.js.map +1 -0
  233. package/server/daemon.d.ts +12 -0
  234. package/server/daemon.js +124 -114
  235. package/server/daemon.js.map +1 -0
  236. package/server/fileSourceNode.d.ts +2 -0
  237. package/server/fileSourceNode.js +34 -36
  238. package/server/fileSourceNode.js.map +1 -0
  239. package/server/gitClone.d.ts +9 -0
  240. package/server/gitClone.js +59 -61
  241. package/server/gitClone.js.map +1 -0
  242. package/server/gitDiff.d.ts +36 -0
  243. package/server/gitDiff.js +271 -169
  244. package/server/gitDiff.js.map +1 -0
  245. package/server/gitHistory.d.ts +12 -0
  246. package/server/gitHistory.js +80 -80
  247. package/server/gitHistory.js.map +1 -0
  248. package/server/gitRemote.d.ts +12 -0
  249. package/server/gitRemote.js +60 -0
  250. package/server/gitRemote.js.map +1 -0
  251. package/server/htmlText.d.ts +4 -0
  252. package/server/htmlText.js +16 -23
  253. package/server/htmlText.js.map +1 -0
  254. package/server/http.d.ts +5 -0
  255. package/server/http.js +27 -30
  256. package/server/http.js.map +1 -0
  257. package/server/httpGuards.d.ts +2 -0
  258. package/server/httpGuards.js +47 -43
  259. package/server/httpGuards.js.map +1 -0
  260. package/server/https.d.ts +34 -0
  261. package/server/https.js +121 -111
  262. package/server/https.js.map +1 -0
  263. package/server/index.d.ts +40 -0
  264. package/server/index.js +139 -147
  265. package/server/index.js.map +1 -0
  266. package/server/layout.d.ts +26 -0
  267. package/server/layout.js +59 -37
  268. package/server/layout.js.map +1 -0
  269. package/server/llmProxy.d.ts +3 -0
  270. package/server/llmProxy.js +137 -135
  271. package/server/llmProxy.js.map +1 -0
  272. package/server/logger.d.ts +38 -0
  273. package/server/logger.js +198 -211
  274. package/server/logger.js.map +1 -0
  275. package/server/mcp/analysis.d.ts +29 -0
  276. package/server/mcp/analysis.js +99 -120
  277. package/server/mcp/analysis.js.map +1 -0
  278. package/server/mcp/http.d.ts +9 -0
  279. package/server/mcp/http.js +128 -137
  280. package/server/mcp/http.js.map +1 -0
  281. package/server/mcp/runner.d.ts +68 -0
  282. package/server/mcp/runner.js +208 -218
  283. package/server/mcp/runner.js.map +1 -0
  284. package/server/mcp/server.d.ts +29 -0
  285. package/server/mcp/server.js +166 -179
  286. package/server/mcp/server.js.map +1 -0
  287. package/server/mcp/standalone.d.ts +1 -0
  288. package/server/mcp/standalone.js +8 -10
  289. package/server/mcp/standalone.js.map +1 -0
  290. package/server/mcp/tools.d.ts +2020 -0
  291. package/server/mcp/tools.js +523 -574
  292. package/server/mcp/tools.js.map +1 -0
  293. package/server/paths.d.ts +3 -0
  294. package/server/paths.js +17 -15
  295. package/server/paths.js.map +1 -0
  296. package/server/pidfile.d.ts +9 -0
  297. package/server/pidfile.js +43 -45
  298. package/server/pidfile.js.map +1 -0
  299. package/server/review.d.ts +5 -0
  300. package/server/review.js +35 -0
  301. package/server/review.js.map +1 -0
  302. package/server/router.d.ts +33 -0
  303. package/server/router.js +191 -197
  304. package/server/router.js.map +1 -0
  305. package/server/scanRequest.d.ts +3 -0
  306. package/server/scanRequest.js +36 -0
  307. package/server/scanRequest.js.map +1 -0
  308. package/server/searchIndex.d.ts +17 -0
  309. package/server/searchIndex.js +83 -80
  310. package/server/searchIndex.js.map +1 -0
  311. package/server/sessions.d.ts +14 -0
  312. package/server/sessions.js +62 -70
  313. package/server/sessions.js.map +1 -0
  314. package/server/startup.d.ts +118 -0
  315. package/server/startup.js +143 -148
  316. package/server/startup.js.map +1 -0
  317. package/server/static.d.ts +6 -0
  318. package/server/static.js +28 -31
  319. package/server/static.js.map +1 -0
  320. package/server/tools/install.d.ts +4 -0
  321. package/server/tools/install.js +176 -185
  322. package/server/tools/install.js.map +1 -0
  323. package/server/tools/parse.d.ts +8 -0
  324. package/server/tools/parse.js +112 -114
  325. package/server/tools/parse.js.map +1 -0
  326. package/server/tools/platform.d.ts +9 -0
  327. package/server/tools/platform.js +49 -47
  328. package/server/tools/platform.js.map +1 -0
  329. package/server/tools/registry.d.ts +137 -0
  330. package/server/tools/registry.js +173 -172
  331. package/server/tools/registry.js.map +1 -0
  332. package/server/tools/scan.d.ts +34 -0
  333. package/server/tools/scan.js +89 -98
  334. package/server/tools/scan.js.map +1 -0
  335. package/server/tools.d.ts +34 -0
  336. package/server/tools.js +94 -107
  337. package/server/tools.js.map +1 -0
  338. package/server/tunnel.d.ts +29 -0
  339. package/server/tunnel.js +61 -66
  340. package/server/tunnel.js.map +1 -0
  341. package/shared/analyzer/docs.d.ts +16 -0
  342. package/shared/analyzer/docs.js +99 -93
  343. package/shared/analyzer/docs.js.map +1 -0
  344. package/shared/analyzer/explainLocal.d.ts +4 -0
  345. package/shared/analyzer/explainLocal.js +122 -142
  346. package/shared/analyzer/explainLocal.js.map +1 -0
  347. package/shared/analyzer/github.d.ts +37 -0
  348. package/shared/analyzer/github.js +79 -0
  349. package/shared/analyzer/github.js.map +1 -0
  350. package/shared/analyzer/graph.d.ts +41 -0
  351. package/shared/analyzer/graph.js +393 -400
  352. package/shared/analyzer/graph.js.map +1 -0
  353. package/shared/analyzer/health.d.ts +33 -0
  354. package/shared/analyzer/health.js +197 -193
  355. package/shared/analyzer/health.js.map +1 -0
  356. package/shared/analyzer/history.d.ts +29 -0
  357. package/shared/analyzer/history.js +110 -111
  358. package/shared/analyzer/history.js.map +1 -0
  359. package/shared/analyzer/jsonConfig.d.ts +2 -0
  360. package/shared/analyzer/jsonConfig.js +61 -0
  361. package/shared/analyzer/jsonConfig.js.map +1 -0
  362. package/shared/analyzer/languages/csharp.d.ts +26 -0
  363. package/shared/analyzer/languages/csharp.js +83 -88
  364. package/shared/analyzer/languages/csharp.js.map +1 -0
  365. package/shared/analyzer/languages/generic.d.ts +23 -0
  366. package/shared/analyzer/languages/generic.js +94 -105
  367. package/shared/analyzer/languages/generic.js.map +1 -0
  368. package/shared/analyzer/languages/go.d.ts +24 -0
  369. package/shared/analyzer/languages/go.js +45 -54
  370. package/shared/analyzer/languages/go.js.map +1 -0
  371. package/shared/analyzer/languages/index.d.ts +6 -0
  372. package/shared/analyzer/languages/index.js +19 -20
  373. package/shared/analyzer/languages/index.js.map +1 -0
  374. package/shared/analyzer/languages/java.d.ts +20 -0
  375. package/shared/analyzer/languages/java.js +73 -78
  376. package/shared/analyzer/languages/java.js.map +1 -0
  377. package/shared/analyzer/languages/javascript.d.ts +12 -0
  378. package/shared/analyzer/languages/javascript.js +258 -209
  379. package/shared/analyzer/languages/javascript.js.map +1 -0
  380. package/shared/analyzer/languages/python.d.ts +23 -0
  381. package/shared/analyzer/languages/python.js +92 -100
  382. package/shared/analyzer/languages/python.js.map +1 -0
  383. package/shared/analyzer/languages/rust.d.ts +17 -0
  384. package/shared/analyzer/languages/rust.js +96 -98
  385. package/shared/analyzer/languages/rust.js.map +1 -0
  386. package/shared/analyzer/languages/typescript.d.ts +11 -0
  387. package/shared/analyzer/languages/typescript.js +117 -116
  388. package/shared/analyzer/languages/typescript.js.map +1 -0
  389. package/shared/analyzer/licenses.d.ts +18 -0
  390. package/shared/analyzer/licenses.js +130 -133
  391. package/shared/analyzer/licenses.js.map +1 -0
  392. package/shared/analyzer/metrics.d.ts +20 -0
  393. package/shared/analyzer/metrics.js +112 -104
  394. package/shared/analyzer/metrics.js.map +1 -0
  395. package/shared/analyzer/pathUtil.d.ts +10 -0
  396. package/shared/analyzer/pathUtil.js +29 -35
  397. package/shared/analyzer/pathUtil.js.map +1 -0
  398. package/shared/analyzer/patterns.d.ts +11 -0
  399. package/shared/analyzer/patterns.js +231 -253
  400. package/shared/analyzer/patterns.js.map +1 -0
  401. package/shared/analyzer/scan.d.ts +62 -0
  402. package/shared/analyzer/scan.js +451 -442
  403. package/shared/analyzer/scan.js.map +1 -0
  404. package/shared/analyzer/scanControl.d.ts +11 -0
  405. package/shared/analyzer/scanControl.js +62 -0
  406. package/shared/analyzer/scanControl.js.map +1 -0
  407. package/shared/analyzer/security.d.ts +18 -0
  408. package/shared/analyzer/security.js +159 -159
  409. package/shared/analyzer/security.js.map +1 -0
  410. package/shared/analyzer/services.d.ts +5 -0
  411. package/shared/analyzer/services.js +141 -138
  412. package/shared/analyzer/services.js.map +1 -0
  413. package/shared/analyzer/stack.d.ts +6 -0
  414. package/shared/analyzer/stack.js +134 -141
  415. package/shared/analyzer/stack.js.map +1 -0
  416. package/shared/analyzer/tour.d.ts +2 -0
  417. package/shared/analyzer/tour.js +45 -53
  418. package/shared/analyzer/tour.js.map +1 -0
  419. package/shared/analyzer/util.d.ts +8 -0
  420. package/shared/analyzer/util.js +130 -76
  421. package/shared/analyzer/util.js.map +1 -0
  422. package/shared/analyzer/workflows.d.ts +8 -0
  423. package/shared/analyzer/workflows.js +139 -134
  424. package/shared/analyzer/workflows.js.map +1 -0
  425. package/shared/contracts.d.ts +250 -0
  426. package/shared/contracts.js +2 -0
  427. package/shared/contracts.js.map +1 -0
  428. package/shared/diagram/aiFacts.d.ts +35 -0
  429. package/shared/diagram/aiFacts.js +102 -105
  430. package/shared/diagram/aiFacts.js.map +1 -0
  431. package/shared/diagram/aiMermaid.d.ts +5 -0
  432. package/shared/diagram/aiMermaid.js +46 -45
  433. package/shared/diagram/aiMermaid.js.map +1 -0
  434. package/shared/diagram/atlas.d.ts +16 -0
  435. package/shared/diagram/atlas.js +51 -60
  436. package/shared/diagram/atlas.js.map +1 -0
  437. package/shared/diagram/mermaid.d.ts +43 -0
  438. package/shared/diagram/mermaid.js +348 -370
  439. package/shared/diagram/mermaid.js.map +1 -0
  440. package/shared/review/contracts.d.ts +95 -0
  441. package/shared/review/contracts.js +2 -0
  442. package/shared/review/contracts.js.map +1 -0
  443. package/shared/review/review.d.ts +10 -0
  444. package/shared/review/review.js +184 -0
  445. package/shared/review/review.js.map +1 -0
  446. package/shared/search/query.d.ts +32 -0
  447. package/shared/search/query.js +395 -388
  448. package/shared/search/query.js.map +1 -0
@@ -1,430 +1,423 @@
1
1
  // High-performance, interactive force-directed graph canvas engine.
2
2
  // Implements Barnes-Hut physics, neighbor subgraph highlighting,
3
3
  // theme-aware crisp text rendering, and rich interactive controls.
4
-
5
4
  export function initForceGraph(canvas, options) {
6
- const opts = options || {};
7
- const ctx = canvas.getContext('2d');
8
-
9
- let animationFrameId = null;
10
- let nodes = [];
11
- let edges = [];
12
- let communities = {};
13
-
14
- let transform = { x: 0, y: 0, k: 1 };
15
- let width = canvas.width;
16
- let height = canvas.height;
17
-
18
- let alpha = 1.0;
19
- const alphaDecay = 0.985;
20
- const alphaMin = 0.001;
21
-
22
- let draggedNode = null;
23
- let hoveredNode = null;
24
- let selectedNode = null;
25
- let highlightPath = null;
26
- let searchQuery = '';
27
-
28
- // Adjacency maps for fast interactive neighbor highlighting
29
- let inNeighbors = new Map(); // node -> Set of source nodes
30
- let outNeighbors = new Map(); // node -> Set of target nodes
31
- let nodeEdges = new Map(); // node -> Set of edges
32
-
33
- function resize() {
34
- const parent = canvas.parentElement;
35
- if (!parent) return;
36
- const rect = parent.getBoundingClientRect();
37
- if (rect.width === 0 || rect.height === 0) return;
38
-
39
- const dpr = window.devicePixelRatio || 1;
40
- canvas.width = rect.width * dpr;
41
- canvas.height = rect.height * dpr;
42
- canvas.style.width = rect.width + 'px';
43
- canvas.style.height = rect.height + 'px';
44
- ctx.scale(dpr, dpr);
45
-
46
- width = rect.width;
47
- height = rect.height;
48
-
49
- if (nodes.length > 0 && transform.k === 1 && transform.x === 0 && transform.y === 0) {
50
- fitToScreen();
51
- } else {
52
- render();
5
+ const opts = options || {};
6
+ const ctx = canvas.getContext('2d');
7
+ const interactions = new AbortController();
8
+ const listen = (target, type, handler, options = {}) => {
9
+ target.addEventListener(type, handler, { ...options, signal: interactions.signal });
10
+ };
11
+ let animationFrameId = null;
12
+ let viewActive = true;
13
+ let active = !document.hidden;
14
+ let destroyed = false;
15
+ let nodes = [];
16
+ let edges = [];
17
+ let communities = {};
18
+ let transform = { x: 0, y: 0, k: 1 };
19
+ let width = canvas.width;
20
+ let height = canvas.height;
21
+ let alpha = 1.0;
22
+ const alphaDecay = 0.985;
23
+ const alphaMin = 0.001;
24
+ let draggedNode = null;
25
+ let hoveredNode = null;
26
+ let selectedNode = null;
27
+ let highlightPath = null;
28
+ let searchQuery = '';
29
+ let compactLabels = new Set();
30
+ // Adjacency maps for fast interactive neighbor highlighting
31
+ let inNeighbors = new Map(); // node -> Set of source nodes
32
+ let outNeighbors = new Map(); // node -> Set of target nodes
33
+ let nodeEdges = new Map(); // node -> Set of edges
34
+ function resize() {
35
+ if (!active || destroyed)
36
+ return;
37
+ const parent = canvas.parentElement;
38
+ if (!parent)
39
+ return;
40
+ const rect = parent.getBoundingClientRect();
41
+ if (rect.width === 0 || rect.height === 0)
42
+ return;
43
+ const dpr = window.devicePixelRatio || 1;
44
+ canvas.width = rect.width * dpr;
45
+ canvas.height = rect.height * dpr;
46
+ canvas.style.width = rect.width + 'px';
47
+ canvas.style.height = rect.height + 'px';
48
+ ctx.scale(dpr, dpr);
49
+ width = rect.width;
50
+ height = rect.height;
51
+ if (nodes.length > 0 && transform.k === 1 && transform.x === 0 && transform.y === 0) {
52
+ fitToScreen();
53
+ }
54
+ else {
55
+ render();
56
+ }
53
57
  }
54
- }
55
-
56
- // Quadtree spatial index for Barnes-Hut O(N log N) repulsion
57
- class QuadTree {
58
- constructor(x, y, w, h) {
59
- this.x = x;
60
- this.y = y;
61
- this.w = w;
62
- this.h = h;
63
- this.nodes = [];
64
- this.mass = 0;
65
- this.cx = 0;
66
- this.cy = 0;
67
- this.children = null;
58
+ // Quadtree spatial index for Barnes-Hut O(N log N) repulsion
59
+ class QuadTree {
60
+ x;
61
+ y;
62
+ w;
63
+ h;
64
+ nodes;
65
+ mass;
66
+ cx;
67
+ cy;
68
+ children;
69
+ constructor(x, y, w, h) {
70
+ this.x = x;
71
+ this.y = y;
72
+ this.w = w;
73
+ this.h = h;
74
+ this.nodes = [];
75
+ this.mass = 0;
76
+ this.cx = 0;
77
+ this.cy = 0;
78
+ this.children = null;
79
+ }
80
+ insert(node) {
81
+ if (this.children) {
82
+ const i = (node.x > this.x + this.w / 2 ? 1 : 0) + (node.y > this.y + this.h / 2 ? 2 : 0);
83
+ this.children[i].insert(node);
84
+ }
85
+ else {
86
+ this.nodes.push(node);
87
+ if (this.nodes.length > 1 && this.w > 4) {
88
+ this.subdivide();
89
+ }
90
+ }
91
+ this.mass += 1;
92
+ this.cx = (this.cx * (this.mass - 1) + node.x) / this.mass;
93
+ this.cy = (this.cy * (this.mass - 1) + node.y) / this.mass;
94
+ }
95
+ subdivide() {
96
+ const hw = this.w / 2;
97
+ const hh = this.h / 2;
98
+ this.children = [
99
+ new QuadTree(this.x, this.y, hw, hh),
100
+ new QuadTree(this.x + hw, this.y, hw, hh),
101
+ new QuadTree(this.x, this.y + hh, hw, hh),
102
+ new QuadTree(this.x + hw, this.y + hh, hw, hh)
103
+ ];
104
+ const oldNodes = this.nodes;
105
+ this.nodes = [];
106
+ for (const n of oldNodes) {
107
+ const i = (n.x > this.x + hw ? 1 : 0) + (n.y > this.y + hh ? 2 : 0);
108
+ this.children[i].insert(n);
109
+ }
110
+ }
111
+ applyForce(node, theta = 0.85) {
112
+ if (this.mass === 0)
113
+ return;
114
+ const dx = this.cx - node.x;
115
+ const dy = this.cy - node.y;
116
+ const distSq = dx * dx + dy * dy;
117
+ if (this.w / Math.sqrt(distSq || 1) < theta || !this.children) {
118
+ if (distSq > 0.01) {
119
+ const dist = Math.sqrt(distSq);
120
+ // Scale repulsion force inversely proportional to distance
121
+ const f = -120 * this.mass / (distSq + 100);
122
+ node.vx += (dx / dist) * f;
123
+ node.vy += (dy / dist) * f;
124
+ }
125
+ }
126
+ else {
127
+ for (const child of this.children) {
128
+ child.applyForce(node, theta);
129
+ }
130
+ }
131
+ }
68
132
  }
69
-
70
- insert(node) {
71
- if (this.children) {
72
- const i = (node.x > this.x + this.w / 2 ? 1 : 0) + (node.y > this.y + this.h / 2 ? 2 : 0);
73
- this.children[i].insert(node);
74
- } else {
75
- this.nodes.push(node);
76
- if (this.nodes.length > 1 && this.w > 4) {
77
- this.subdivide();
133
+ function buildAdjacency() {
134
+ inNeighbors = new Map();
135
+ outNeighbors = new Map();
136
+ nodeEdges = new Map();
137
+ for (const n of nodes) {
138
+ inNeighbors.set(n, new Set());
139
+ outNeighbors.set(n, new Set());
140
+ nodeEdges.set(n, new Set());
141
+ }
142
+ for (const e of edges) {
143
+ if (e.source && e.target) {
144
+ outNeighbors.get(e.source)?.add(e.target);
145
+ inNeighbors.get(e.target)?.add(e.source);
146
+ nodeEdges.get(e.source)?.add(e);
147
+ nodeEdges.get(e.target)?.add(e);
148
+ }
78
149
  }
79
- }
80
- this.mass += 1;
81
- this.cx = (this.cx * (this.mass - 1) + node.x) / this.mass;
82
- this.cy = (this.cy * (this.mass - 1) + node.y) / this.mass;
83
150
  }
84
-
85
- subdivide() {
86
- const hw = this.w / 2;
87
- const hh = this.h / 2;
88
- this.children = [
89
- new QuadTree(this.x, this.y, hw, hh),
90
- new QuadTree(this.x + hw, this.y, hw, hh),
91
- new QuadTree(this.x, this.y + hh, hw, hh),
92
- new QuadTree(this.x + hw, this.y + hh, hw, hh)
93
- ];
94
- const oldNodes = this.nodes;
95
- this.nodes = [];
96
- for (const n of oldNodes) {
97
- const i = (n.x > this.x + hw ? 1 : 0) + (n.y > this.y + hh ? 2 : 0);
98
- this.children[i].insert(n);
99
- }
151
+ function update(newNodes, newEdges, newComms) {
152
+ communities = newComms || {};
153
+ const posMap = new Map(nodes.map(n => [n.path, { x: n.x, y: n.y, vx: n.vx, vy: n.vy }]));
154
+ nodes = newNodes.map((n, i) => {
155
+ const p = posMap.get(n.path);
156
+ const angle = (i / Math.max(1, newNodes.length)) * Math.PI * 2;
157
+ const dist = Math.min(width, height) * 0.35;
158
+ return {
159
+ ...n,
160
+ x: p ? p.x : Math.cos(angle) * dist + (Math.random() - 0.5) * 40,
161
+ y: p ? p.y : Math.sin(angle) * dist + (Math.random() - 0.5) * 40,
162
+ vx: p ? p.vx : 0,
163
+ vy: p ? p.vy : 0,
164
+ r: Math.max(5, Math.min(24, 4 + Math.sqrt(n.fanIn || 0) * 2.5)),
165
+ basename: n.path.split('/').pop() || n.path
166
+ };
167
+ });
168
+ compactLabels = new Set([...nodes]
169
+ .sort((a, b) => b.fanIn - a.fanIn)
170
+ .slice(0, 8)
171
+ .map((node) => node.path));
172
+ const nodeByPath = new Map(nodes.map(n => [n.path, n]));
173
+ edges = (newEdges || []).map(e => ({
174
+ source: nodeByPath.get(e.from),
175
+ target: nodeByPath.get(e.to),
176
+ from: e.from,
177
+ to: e.to
178
+ })).filter(e => e.source && e.target);
179
+ buildAdjacency();
180
+ alpha = 1.0;
181
+ if (active && !destroyed && !animationFrameId)
182
+ animationFrameId = requestAnimationFrame(tick);
100
183
  }
101
-
102
- applyForce(node, theta = 0.85) {
103
- if (this.mass === 0) return;
104
- const dx = this.cx - node.x;
105
- const dy = this.cy - node.y;
106
- const distSq = dx * dx + dy * dy;
107
-
108
- if (this.w / Math.sqrt(distSq || 1) < theta || !this.children) {
109
- if (distSq > 0.01) {
110
- const dist = Math.sqrt(distSq);
111
- // Scale repulsion force inversely proportional to distance
112
- const f = -120 * this.mass / (distSq + 100);
113
- node.vx += (dx / dist) * f;
114
- node.vy += (dy / dist) * f;
184
+ function tick() {
185
+ if (!active || destroyed) {
186
+ animationFrameId = null;
187
+ return;
188
+ }
189
+ if (alpha > alphaMin) {
190
+ alpha *= alphaDecay;
191
+ // Repulsion via Quadtree
192
+ if (nodes.length > 0) {
193
+ let minX = nodes[0].x, maxX = nodes[0].x;
194
+ let minY = nodes[0].y, maxY = nodes[0].y;
195
+ for (const n of nodes) {
196
+ if (n.x < minX)
197
+ minX = n.x;
198
+ if (n.x > maxX)
199
+ maxX = n.x;
200
+ if (n.y < minY)
201
+ minY = n.y;
202
+ if (n.y > maxY)
203
+ maxY = n.y;
204
+ }
205
+ const span = Math.max(maxX - minX, maxY - minY, 100);
206
+ const qt = new QuadTree(minX - 20, minY - 20, span + 40, span + 40);
207
+ for (const n of nodes)
208
+ qt.insert(n);
209
+ for (const n of nodes)
210
+ qt.applyForce(n);
211
+ }
212
+ // Hooke's Law Edge Springs
213
+ for (const e of edges) {
214
+ const dx = e.target.x - e.source.x;
215
+ const dy = e.target.y - e.source.y;
216
+ const dist = Math.sqrt(dx * dx + dy * dy) || 1;
217
+ const targetDist = 45 + (e.source.r + e.target.r) * 1.5;
218
+ const f = (dist - targetDist) * 0.04 * alpha;
219
+ const fx = (dx / dist) * f;
220
+ const fy = (dy / dist) * f;
221
+ e.source.vx += fx;
222
+ e.source.vy += fy;
223
+ e.target.vx -= fx;
224
+ e.target.vy -= fy;
225
+ }
226
+ // Center Gravity
227
+ for (const n of nodes) {
228
+ n.vx -= n.x * 0.015 * alpha;
229
+ n.vy -= n.y * 0.015 * alpha;
230
+ }
231
+ // Velocity Verlet update
232
+ for (const n of nodes) {
233
+ if (n !== draggedNode) {
234
+ n.vx *= 0.65;
235
+ n.vy *= 0.65;
236
+ n.x += n.vx;
237
+ n.y += n.vy;
238
+ }
239
+ }
115
240
  }
116
- } else {
117
- for (const child of this.children) {
118
- child.applyForce(node, theta);
241
+ render();
242
+ if (alpha > alphaMin || draggedNode) {
243
+ animationFrameId = requestAnimationFrame(tick);
244
+ }
245
+ else {
246
+ animationFrameId = null;
119
247
  }
120
- }
121
248
  }
122
- }
123
-
124
- function buildAdjacency() {
125
- inNeighbors = new Map();
126
- outNeighbors = new Map();
127
- nodeEdges = new Map();
128
-
129
- for (const n of nodes) {
130
- inNeighbors.set(n, new Set());
131
- outNeighbors.set(n, new Set());
132
- nodeEdges.set(n, new Set());
249
+ function reheat() {
250
+ alpha = 0.5;
251
+ if (active && !destroyed && !animationFrameId)
252
+ animationFrameId = requestAnimationFrame(tick);
133
253
  }
134
-
135
- for (const e of edges) {
136
- if (e.source && e.target) {
137
- outNeighbors.get(e.source)?.add(e.target);
138
- inNeighbors.get(e.target)?.add(e.source);
139
- nodeEdges.get(e.source)?.add(e);
140
- nodeEdges.get(e.target)?.add(e);
141
- }
254
+ function isDarkTheme() {
255
+ return document.documentElement.dataset.theme === 'dark';
142
256
  }
143
- }
144
-
145
- function update(newNodes, newEdges, newComms) {
146
- communities = newComms || {};
147
-
148
- const posMap = new Map(nodes.map(n => [n.path, { x: n.x, y: n.y, vx: n.vx, vy: n.vy }]));
149
-
150
- nodes = newNodes.map((n, i) => {
151
- const p = posMap.get(n.path);
152
- const angle = (i / Math.max(1, newNodes.length)) * Math.PI * 2;
153
- const dist = Math.min(width, height) * 0.35;
154
- return {
155
- ...n,
156
- x: p ? p.x : Math.cos(angle) * dist + (Math.random() - 0.5) * 40,
157
- y: p ? p.y : Math.sin(angle) * dist + (Math.random() - 0.5) * 40,
158
- vx: p ? p.vx : 0,
159
- vy: p ? p.vy : 0,
160
- r: Math.max(5, Math.min(24, 4 + Math.sqrt(n.fanIn || 0) * 2.5)),
161
- basename: n.path.split('/').pop() || n.path
162
- };
163
- });
164
-
165
- const nodeByPath = new Map(nodes.map(n => [n.path, n]));
166
- edges = (newEdges || []).map(e => ({
167
- source: nodeByPath.get(e.from),
168
- target: nodeByPath.get(e.to),
169
- from: e.from,
170
- to: e.to
171
- })).filter(e => e.source && e.target);
172
-
173
- buildAdjacency();
174
-
175
- alpha = 1.0;
176
- if (!animationFrameId) tick();
177
- }
178
-
179
- function tick() {
180
- if (alpha > alphaMin) {
181
- alpha *= alphaDecay;
182
-
183
- // Repulsion via Quadtree
184
- if (nodes.length > 0) {
185
- let minX = nodes[0].x, maxX = nodes[0].x;
186
- let minY = nodes[0].y, maxY = nodes[0].y;
257
+ function render() {
258
+ if (!active || destroyed)
259
+ return;
260
+ ctx.save();
261
+ ctx.clearRect(0, 0, width, height);
262
+ ctx.translate(transform.x, transform.y);
263
+ ctx.scale(transform.k, transform.k);
264
+ const dark = isDarkTheme();
265
+ const activeNode = hoveredNode || selectedNode;
266
+ const hasFocus = Boolean(activeNode || highlightPath || searchQuery);
267
+ const inSet = activeNode ? inNeighbors.get(activeNode) : null;
268
+ const outSet = activeNode ? outNeighbors.get(activeNode) : null;
269
+ // Draw Edges
270
+ for (const e of edges) {
271
+ const isOut = activeNode && e.source === activeNode;
272
+ const isIn = activeNode && e.target === activeNode;
273
+ const isConnected = isOut || isIn;
274
+ ctx.beginPath();
275
+ ctx.moveTo(e.source.x, e.source.y);
276
+ ctx.lineTo(e.target.x, e.target.y);
277
+ if (isConnected) {
278
+ ctx.strokeStyle = isOut ? '#f43f5e' : '#38bdf8'; // Outbound (rose) vs Inbound (cyan)
279
+ ctx.lineWidth = 2.4 / transform.k;
280
+ ctx.globalAlpha = 0.95;
281
+ }
282
+ else {
283
+ ctx.strokeStyle = dark ? 'rgba(255, 255, 255, 0.25)' : 'rgba(30, 41, 59, 0.22)';
284
+ ctx.lineWidth = 1 / transform.k;
285
+ ctx.globalAlpha = hasFocus ? 0.04 : 0.45;
286
+ }
287
+ ctx.stroke();
288
+ }
289
+ // Draw Nodes
187
290
  for (const n of nodes) {
188
- if (n.x < minX) minX = n.x; if (n.x > maxX) maxX = n.x;
189
- if (n.y < minY) minY = n.y; if (n.y > maxY) maxY = n.y;
291
+ const isTarget = n === activeNode;
292
+ const isNeighbor = activeNode && (inSet?.has(n) || outSet?.has(n));
293
+ const isPathMatch = highlightPath && n.path === highlightPath;
294
+ const isSearchMatch = searchQuery && n.path.toLowerCase().includes(searchQuery.toLowerCase());
295
+ const isHighlighted = isTarget || isNeighbor || isPathMatch || isSearchMatch;
296
+ const nodeAlpha = hasFocus ? (isHighlighted ? 1.0 : 0.18) : 0.92;
297
+ ctx.globalAlpha = nodeAlpha;
298
+ // Node circle fill
299
+ ctx.beginPath();
300
+ ctx.arc(n.x, n.y, n.r, 0, 2 * Math.PI);
301
+ const hue = n.community ? (n.community * 137.5) % 360 : ((n.fanIn * 37) % 360);
302
+ ctx.fillStyle = `hsl(${hue}, 75%, ${dark ? '55%' : '45%'})`;
303
+ ctx.fill();
304
+ // Outer border / glow ring
305
+ if (isHighlighted) {
306
+ ctx.lineWidth = (isTarget ? 3.5 : 2) / transform.k;
307
+ ctx.strokeStyle = isTarget ? (dark ? '#ffffff' : '#0f172a') : (inSet?.has(n) ? '#38bdf8' : '#f43f5e');
308
+ ctx.stroke();
309
+ }
310
+ else {
311
+ ctx.lineWidth = 1 / transform.k;
312
+ ctx.strokeStyle = dark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)';
313
+ ctx.stroke();
314
+ }
190
315
  }
191
- const span = Math.max(maxX - minX, maxY - minY, 100);
192
- const qt = new QuadTree(minX - 20, minY - 20, span + 40, span + 40);
193
- for (const n of nodes) qt.insert(n);
194
- for (const n of nodes) qt.applyForce(n);
195
- }
196
-
197
- // Hooke's Law Edge Springs
198
- for (const e of edges) {
199
- const dx = e.target.x - e.source.x;
200
- const dy = e.target.y - e.source.y;
201
- const dist = Math.sqrt(dx * dx + dy * dy) || 1;
202
- const targetDist = 45 + (e.source.r + e.target.r) * 1.5;
203
- const f = (dist - targetDist) * 0.04 * alpha;
204
- const fx = (dx / dist) * f;
205
- const fy = (dy / dist) * f;
206
- e.source.vx += fx; e.source.vy += fy;
207
- e.target.vx -= fx; e.target.vy -= fy;
208
- }
209
-
210
- // Center Gravity
211
- for (const n of nodes) {
212
- n.vx -= n.x * 0.015 * alpha;
213
- n.vy -= n.y * 0.015 * alpha;
214
- }
215
-
216
- // Velocity Verlet update
217
- for (const n of nodes) {
218
- if (n !== draggedNode) {
219
- n.vx *= 0.65;
220
- n.vy *= 0.65;
221
- n.x += n.vx;
222
- n.y += n.vy;
316
+ // Draw Crisp Theme-Aware Text Labels (with dark/light halo outline)
317
+ ctx.textAlign = 'left';
318
+ ctx.textBaseline = 'middle';
319
+ for (const n of nodes) {
320
+ const isTarget = n === activeNode;
321
+ const isNeighbor = activeNode && (inSet?.has(n) || outSet?.has(n));
322
+ const isPathMatch = highlightPath && n.path === highlightPath;
323
+ const isSearchMatch = searchQuery && n.path.toLowerCase().includes(searchQuery.toLowerCase());
324
+ const shouldShowLabel = width < 600
325
+ ? isTarget || isNeighbor || isPathMatch || isSearchMatch ||
326
+ (compactLabels.has(n.path) && transform.k >= 0.4) || transform.k >= 1.8
327
+ : isTarget || isNeighbor || isPathMatch || isSearchMatch ||
328
+ (n.fanIn >= 2 && transform.k >= 0.4) || transform.k >= 1.1;
329
+ if (!shouldShowLabel)
330
+ continue;
331
+ const isProminent = isTarget || isNeighbor || isPathMatch || isSearchMatch;
332
+ ctx.globalAlpha = hasFocus ? (isProminent ? 1.0 : 0.15) : 0.95;
333
+ const fontSize = Math.max(10, Math.min(14, 11 / Math.sqrt(transform.k)));
334
+ ctx.font = `${isProminent ? '600' : '500'} ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace`;
335
+ const textX = n.x + n.r + 4 / transform.k;
336
+ const textY = n.y;
337
+ const label = n.basename;
338
+ // Stroke halo background for contrast against any background
339
+ ctx.lineWidth = 3.5 / transform.k;
340
+ ctx.lineJoin = 'round';
341
+ ctx.strokeStyle = dark ? 'rgba(10, 14, 23, 0.92)' : 'rgba(255, 255, 255, 0.95)';
342
+ ctx.strokeText(label, textX, textY);
343
+ // Bright / High-contrast foreground text
344
+ if (isTarget) {
345
+ ctx.fillStyle = dark ? '#38bdf8' : '#0284c7';
346
+ }
347
+ else if (isProminent) {
348
+ ctx.fillStyle = dark ? '#f8fafc' : '#0f172a';
349
+ }
350
+ else {
351
+ ctx.fillStyle = dark ? '#cbd5e1' : '#334155';
352
+ }
353
+ ctx.fillText(label, textX, textY);
223
354
  }
224
- }
225
- }
226
-
227
- render();
228
-
229
- if (alpha > alphaMin || draggedNode) {
230
- animationFrameId = requestAnimationFrame(tick);
231
- } else {
232
- animationFrameId = null;
233
- }
234
- }
235
-
236
- function reheat() {
237
- alpha = 0.5;
238
- if (!animationFrameId) tick();
239
- }
240
-
241
- function isDarkTheme() {
242
- return document.documentElement.dataset.theme === 'dark';
243
- }
244
-
245
- function render() {
246
- ctx.save();
247
- ctx.clearRect(0, 0, width, height);
248
-
249
- ctx.translate(transform.x, transform.y);
250
- ctx.scale(transform.k, transform.k);
251
-
252
- const dark = isDarkTheme();
253
- const activeNode = hoveredNode || selectedNode;
254
- const hasFocus = Boolean(activeNode || highlightPath || searchQuery);
255
-
256
- const inSet = activeNode ? inNeighbors.get(activeNode) : null;
257
- const outSet = activeNode ? outNeighbors.get(activeNode) : null;
258
-
259
- // Draw Edges
260
- for (const e of edges) {
261
- const isOut = activeNode && e.source === activeNode;
262
- const isIn = activeNode && e.target === activeNode;
263
- const isConnected = isOut || isIn;
264
-
265
- ctx.beginPath();
266
- ctx.moveTo(e.source.x, e.source.y);
267
- ctx.lineTo(e.target.x, e.target.y);
268
-
269
- if (isConnected) {
270
- ctx.strokeStyle = isOut ? '#f43f5e' : '#38bdf8'; // Outbound (rose) vs Inbound (cyan)
271
- ctx.lineWidth = 2.4 / transform.k;
272
- ctx.globalAlpha = 0.95;
273
- } else {
274
- ctx.strokeStyle = dark ? 'rgba(255, 255, 255, 0.25)' : 'rgba(30, 41, 59, 0.22)';
275
- ctx.lineWidth = 1 / transform.k;
276
- ctx.globalAlpha = hasFocus ? 0.04 : 0.45;
277
- }
278
- ctx.stroke();
355
+ ctx.restore();
279
356
  }
280
-
281
- // Draw Nodes
282
- for (const n of nodes) {
283
- const isTarget = n === activeNode;
284
- const isNeighbor = activeNode && (inSet?.has(n) || outSet?.has(n));
285
- const isPathMatch = highlightPath && n.path === highlightPath;
286
- const isSearchMatch = searchQuery && n.path.toLowerCase().includes(searchQuery.toLowerCase());
287
- const isHighlighted = isTarget || isNeighbor || isPathMatch || isSearchMatch;
288
-
289
- const nodeAlpha = hasFocus ? (isHighlighted ? 1.0 : 0.18) : 0.92;
290
- ctx.globalAlpha = nodeAlpha;
291
-
292
- // Node circle fill
293
- ctx.beginPath();
294
- ctx.arc(n.x, n.y, n.r, 0, 2 * Math.PI);
295
-
296
- const hue = n.community ? (n.community * 137.5) % 360 : ((n.fanIn * 37) % 360);
297
- ctx.fillStyle = `hsl(${hue}, 75%, ${dark ? '55%' : '45%'})`;
298
- ctx.fill();
299
-
300
- // Outer border / glow ring
301
- if (isHighlighted) {
302
- ctx.lineWidth = (isTarget ? 3.5 : 2) / transform.k;
303
- ctx.strokeStyle = isTarget ? (dark ? '#ffffff' : '#0f172a') : (inSet?.has(n) ? '#38bdf8' : '#f43f5e');
304
- ctx.stroke();
305
- } else {
306
- ctx.lineWidth = 1 / transform.k;
307
- ctx.strokeStyle = dark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.2)';
308
- ctx.stroke();
309
- }
310
- }
311
-
312
- // Draw Crisp Theme-Aware Text Labels (with dark/light halo outline)
313
- ctx.textAlign = 'left';
314
- ctx.textBaseline = 'middle';
315
-
316
- for (const n of nodes) {
317
- const isTarget = n === activeNode;
318
- const isNeighbor = activeNode && (inSet?.has(n) || outSet?.has(n));
319
- const isPathMatch = highlightPath && n.path === highlightPath;
320
- const isSearchMatch = searchQuery && n.path.toLowerCase().includes(searchQuery.toLowerCase());
321
- const shouldShowLabel = isTarget || isNeighbor || isPathMatch || isSearchMatch || (n.fanIn >= 2 && transform.k >= 0.4) || transform.k >= 1.1;
322
-
323
- if (!shouldShowLabel) continue;
324
-
325
- const isProminent = isTarget || isNeighbor || isPathMatch || isSearchMatch;
326
- ctx.globalAlpha = hasFocus ? (isProminent ? 1.0 : 0.15) : 0.95;
327
-
328
- const fontSize = Math.max(10, Math.min(14, 11 / Math.sqrt(transform.k)));
329
- ctx.font = `${isProminent ? '600' : '500'} ${fontSize}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace`;
330
-
331
- const textX = n.x + n.r + 4 / transform.k;
332
- const textY = n.y;
333
- const label = n.basename;
334
-
335
- // Stroke halo background for contrast against any background
336
- ctx.lineWidth = 3.5 / transform.k;
337
- ctx.lineJoin = 'round';
338
- ctx.strokeStyle = dark ? 'rgba(10, 14, 23, 0.92)' : 'rgba(255, 255, 255, 0.95)';
339
- ctx.strokeText(label, textX, textY);
340
-
341
- // Bright / High-contrast foreground text
342
- if (isTarget) {
343
- ctx.fillStyle = dark ? '#38bdf8' : '#0284c7';
344
- } else if (isProminent) {
345
- ctx.fillStyle = dark ? '#f8fafc' : '#0f172a';
346
- } else {
347
- ctx.fillStyle = dark ? '#cbd5e1' : '#334155';
348
- }
349
- ctx.fillText(label, textX, textY);
350
- }
351
-
352
- ctx.restore();
353
- }
354
-
355
- function fitToScreen() {
356
- if (nodes.length === 0) {
357
- transform = { x: width / 2, y: height / 2, k: 1 };
358
- render();
359
- return;
357
+ function fitToScreen() {
358
+ if (nodes.length === 0) {
359
+ transform = { x: width / 2, y: height / 2, k: 1 };
360
+ render();
361
+ return;
362
+ }
363
+ let minX = nodes[0].x, maxX = nodes[0].x;
364
+ let minY = nodes[0].y, maxY = nodes[0].y;
365
+ for (const n of nodes) {
366
+ if (n.x - n.r < minX)
367
+ minX = n.x - n.r;
368
+ if (n.x + n.r > maxX)
369
+ maxX = n.x + n.r;
370
+ if (n.y - n.r < minY)
371
+ minY = n.y - n.r;
372
+ if (n.y + n.r > maxY)
373
+ maxY = n.y + n.r;
374
+ }
375
+ const spanW = Math.max(40, maxX - minX);
376
+ const spanH = Math.max(40, maxY - minY);
377
+ const padding = 60;
378
+ const k = Math.min((width - padding * 2) / spanW, (height - padding * 2) / spanH, 2.0);
379
+ const cx = (minX + maxX) / 2;
380
+ const cy = (minY + maxY) / 2;
381
+ transform.k = Math.max(0.2, k);
382
+ transform.x = width / 2 - cx * transform.k;
383
+ transform.y = height / 2 - cy * transform.k;
384
+ render();
360
385
  }
361
-
362
- let minX = nodes[0].x, maxX = nodes[0].x;
363
- let minY = nodes[0].y, maxY = nodes[0].y;
364
- for (const n of nodes) {
365
- if (n.x - n.r < minX) minX = n.x - n.r;
366
- if (n.x + n.r > maxX) maxX = n.x + n.r;
367
- if (n.y - n.r < minY) minY = n.y - n.r;
368
- if (n.y + n.r > maxY) maxY = n.y + n.r;
386
+ function resetLayout() {
387
+ const dist = Math.min(width, height) * 0.35;
388
+ nodes.forEach((n, i) => {
389
+ const angle = (i / Math.max(1, nodes.length)) * Math.PI * 2;
390
+ n.x = Math.cos(angle) * dist + (Math.random() - 0.5) * 30;
391
+ n.y = Math.sin(angle) * dist + (Math.random() - 0.5) * 30;
392
+ n.vx = 0;
393
+ n.vy = 0;
394
+ });
395
+ fitToScreen();
396
+ alpha = 1.0;
397
+ if (active && !destroyed && !animationFrameId)
398
+ animationFrameId = requestAnimationFrame(tick);
369
399
  }
370
-
371
- const spanW = Math.max(40, maxX - minX);
372
- const spanH = Math.max(40, maxY - minY);
373
- const padding = 60;
374
-
375
- const k = Math.min(
376
- (width - padding * 2) / spanW,
377
- (height - padding * 2) / spanH,
378
- 2.0
379
- );
380
-
381
- const cx = (minX + maxX) / 2;
382
- const cy = (minY + maxY) / 2;
383
-
384
- transform.k = Math.max(0.2, k);
385
- transform.x = width / 2 - cx * transform.k;
386
- transform.y = height / 2 - cy * transform.k;
387
-
388
- render();
389
- }
390
-
391
- function resetLayout() {
392
- const dist = Math.min(width, height) * 0.35;
393
- nodes.forEach((n, i) => {
394
- const angle = (i / Math.max(1, nodes.length)) * Math.PI * 2;
395
- n.x = Math.cos(angle) * dist + (Math.random() - 0.5) * 30;
396
- n.y = Math.sin(angle) * dist + (Math.random() - 0.5) * 30;
397
- n.vx = 0;
398
- n.vy = 0;
399
- });
400
- fitToScreen();
401
- alpha = 1.0;
402
- if (!animationFrameId) tick();
403
- }
404
-
405
- function flyToNode(node) {
406
- if (!node) return;
407
- const targetK = Math.max(1.2, transform.k);
408
- transform.k = targetK;
409
- transform.x = width / 2 - node.x * targetK;
410
- transform.y = height / 2 - node.y * targetK;
411
- selectedNode = node;
412
- render();
413
- }
414
-
415
- function updateTooltip(node, clientX, clientY) {
416
- const tooltip = document.getElementById('graphTooltip');
417
- if (!tooltip) return;
418
-
419
- if (!node) {
420
- tooltip.hidden = true;
421
- return;
400
+ function flyToNode(node) {
401
+ if (!node)
402
+ return;
403
+ const targetK = Math.max(1.2, transform.k);
404
+ transform.k = targetK;
405
+ transform.x = width / 2 - node.x * targetK;
406
+ transform.y = height / 2 - node.y * targetK;
407
+ selectedNode = node;
408
+ render();
422
409
  }
423
-
424
- const inCount = inNeighbors.get(node)?.size || node.fanIn || 0;
425
- const outCount = outNeighbors.get(node)?.size || node.fanOut || 0;
426
-
427
- tooltip.innerHTML = `
410
+ function updateTooltip(node, clientX, clientY) {
411
+ const tooltip = document.getElementById('graphTooltip');
412
+ if (!tooltip)
413
+ return;
414
+ if (!node) {
415
+ tooltip.hidden = true;
416
+ return;
417
+ }
418
+ const inCount = inNeighbors.get(node)?.size || node.fanIn || 0;
419
+ const outCount = outNeighbors.get(node)?.size || node.fanOut || 0;
420
+ tooltip.innerHTML = `
428
421
  <div style="font-weight:600; font-size:13px; margin-bottom:4px; display:flex; align-items:center; gap:6px;">
429
422
  <span style="display:inline-block; width:8px; height:8px; border-radius:50%; background:hsl(${(node.community * 137.5) % 360}, 75%, 50%);"></span>
430
423
  ${node.basename}
@@ -437,156 +430,188 @@ export function initForceGraph(canvas, options) {
437
430
  </div>
438
431
  <div style="font-size:10px; color:var(--faint); margin-top:6px; font-style:italic;">Click to open · Double-click to center</div>
439
432
  `;
440
-
441
- tooltip.style.left = Math.min(clientX + 14, window.innerWidth - 260) + 'px';
442
- tooltip.style.top = Math.min(clientY + 14, window.innerHeight - 160) + 'px';
443
- tooltip.hidden = false;
444
- }
445
-
446
- // --- Interaction Event Handlers ---
447
- let isPanning = false;
448
- let startX = 0, startY = 0;
449
- let lastClickTime = 0;
450
-
451
- canvas.addEventListener('mousedown', e => {
452
- const rect = canvas.getBoundingClientRect();
453
- const mx = (e.clientX - rect.left - transform.x) / transform.k;
454
- const my = (e.clientY - rect.top - transform.y) / transform.k;
455
-
456
- draggedNode = nodes.find(n => {
457
- const dx = n.x - mx;
458
- const dy = n.y - my;
459
- return Math.sqrt(dx * dx + dy * dy) <= (n.r + 4);
460
- });
461
-
462
- if (draggedNode) {
463
- draggedNode.vx = 0;
464
- draggedNode.vy = 0;
465
- reheat();
466
- } else {
467
- isPanning = true;
468
- startX = e.clientX - transform.x;
469
- startY = e.clientY - transform.y;
470
- }
471
- });
472
-
473
- canvas.addEventListener('mousemove', e => {
474
- const rect = canvas.getBoundingClientRect();
475
-
476
- if (draggedNode) {
477
- draggedNode.x = (e.clientX - rect.left - transform.x) / transform.k;
478
- draggedNode.y = (e.clientY - rect.top - transform.y) / transform.k;
479
- draggedNode.vx = 0;
480
- draggedNode.vy = 0;
481
- updateTooltip(draggedNode, e.clientX, e.clientY);
482
- if (!animationFrameId) render();
483
- } else if (isPanning) {
484
- transform.x = e.clientX - startX;
485
- transform.y = e.clientY - startY;
486
- if (!animationFrameId) render();
487
- } else {
488
- const mx = (e.clientX - rect.left - transform.x) / transform.k;
489
- const my = (e.clientY - rect.top - transform.y) / transform.k;
490
-
491
- const newHover = nodes.find(n => {
492
- const dx = n.x - mx;
493
- const dy = n.y - my;
494
- return Math.sqrt(dx * dx + dy * dy) <= (n.r + 4);
495
- });
496
-
497
- if (newHover !== hoveredNode) {
498
- hoveredNode = newHover || null;
499
- canvas.style.cursor = hoveredNode ? 'pointer' : 'grab';
500
- updateTooltip(hoveredNode, e.clientX, e.clientY);
501
- if (!animationFrameId) render();
502
- } else if (hoveredNode) {
503
- updateTooltip(hoveredNode, e.clientX, e.clientY);
504
- }
433
+ tooltip.style.left = Math.min(clientX + 14, window.innerWidth - 260) + 'px';
434
+ tooltip.style.top = Math.min(clientY + 14, window.innerHeight - 160) + 'px';
435
+ tooltip.hidden = false;
505
436
  }
506
- });
507
-
508
- window.addEventListener('mouseup', () => {
509
- draggedNode = null;
510
- isPanning = false;
511
- });
512
-
513
- canvas.addEventListener('wheel', e => {
514
- e.preventDefault();
515
- const rect = canvas.getBoundingClientRect();
516
- const mx = e.clientX - rect.left;
517
- const my = e.clientY - rect.top;
518
-
519
- const zoomFactor = e.deltaY < 0 ? 1.15 : 0.87;
520
- const newK = Math.max(0.1, Math.min(5.0, transform.k * zoomFactor));
521
-
522
- transform.x = mx - (mx - transform.x) * (newK / transform.k);
523
- transform.y = my - (my - transform.y) * (newK / transform.k);
524
- transform.k = newK;
525
-
526
- if (!animationFrameId) render();
527
- }, { passive: false });
528
-
529
- canvas.addEventListener('click', e => {
530
- const now = Date.now();
531
- const isDouble = now - lastClickTime < 300;
532
- lastClickTime = now;
533
-
534
- if (hoveredNode) {
535
- if (isDouble) {
536
- flyToNode(hoveredNode);
537
- } else {
538
- selectedNode = hoveredNode;
539
- canvas.dispatchEvent(new CustomEvent('graph-node-click', {
540
- detail: { path: hoveredNode.path },
541
- bubbles: true
542
- }));
543
- }
544
- } else {
545
- selectedNode = null;
546
- if (!animationFrameId) render();
547
- }
548
- });
549
-
550
- canvas.addEventListener('mouseleave', () => {
551
- hoveredNode = null;
552
- updateTooltip(null);
553
- if (!animationFrameId) render();
554
- });
555
-
556
- // Wire Controls HUD
557
- const resetBtn = document.getElementById('graphReset');
558
- const zoomInBtn = document.getElementById('graphZoomIn');
559
- const zoomOutBtn = document.getElementById('graphZoomOut');
560
-
561
- if (resetBtn) resetBtn.onclick = () => resetLayout();
562
- if (zoomInBtn) zoomInBtn.onclick = () => {
563
- const newK = Math.min(5.0, transform.k * 1.25);
564
- transform.x = width / 2 - (width / 2 - transform.x) * (newK / transform.k);
565
- transform.y = height / 2 - (height / 2 - transform.y) * (newK / transform.k);
566
- transform.k = newK;
567
- if (!animationFrameId) render();
568
- };
569
- if (zoomOutBtn) zoomOutBtn.onclick = () => {
570
- const newK = Math.max(0.1, transform.k * 0.8);
571
- transform.x = width / 2 - (width / 2 - transform.x) * (newK / transform.k);
572
- transform.y = height / 2 - (height / 2 - transform.y) * (newK / transform.k);
573
- transform.k = newK;
574
- if (!animationFrameId) render();
575
- };
576
-
577
- window.addEventListener('resize', resize);
578
- setTimeout(resize, 50);
579
-
580
- return {
581
- update,
582
- resize,
583
- fit: fitToScreen,
584
- reset: resetLayout,
585
- search: q => { searchQuery = q; if (!animationFrameId) render(); },
586
- highlight: path => { highlightPath = path; if (!animationFrameId) render(); },
587
- destroy: () => {
588
- window.removeEventListener('resize', resize);
589
- if (animationFrameId) cancelAnimationFrame(animationFrameId);
437
+ // --- Interaction Event Handlers ---
438
+ let isPanning = false;
439
+ let startX = 0, startY = 0;
440
+ let lastClickTime = 0;
441
+ const nodeAt = e => {
442
+ const rect = canvas.getBoundingClientRect();
443
+ const mx = (e.clientX - rect.left - transform.x) / transform.k;
444
+ const my = (e.clientY - rect.top - transform.y) / transform.k;
445
+ return nodes.find(n => {
446
+ const dx = n.x - mx;
447
+ const dy = n.y - my;
448
+ return Math.sqrt(dx * dx + dy * dy) <= (n.r + 4);
449
+ });
450
+ };
451
+ listen(canvas, 'pointerdown', e => {
452
+ canvas.setPointerCapture(e.pointerId);
453
+ draggedNode = nodeAt(e);
454
+ if (draggedNode) {
455
+ draggedNode.vx = 0;
456
+ draggedNode.vy = 0;
457
+ reheat();
458
+ }
459
+ else {
460
+ isPanning = true;
461
+ startX = e.clientX - transform.x;
462
+ startY = e.clientY - transform.y;
463
+ }
464
+ });
465
+ listen(canvas, 'pointermove', e => {
466
+ const rect = canvas.getBoundingClientRect();
467
+ if (draggedNode) {
468
+ draggedNode.x = (e.clientX - rect.left - transform.x) / transform.k;
469
+ draggedNode.y = (e.clientY - rect.top - transform.y) / transform.k;
470
+ draggedNode.vx = 0;
471
+ draggedNode.vy = 0;
472
+ updateTooltip(draggedNode, e.clientX, e.clientY);
473
+ if (!animationFrameId)
474
+ render();
475
+ }
476
+ else if (isPanning) {
477
+ transform.x = e.clientX - startX;
478
+ transform.y = e.clientY - startY;
479
+ if (!animationFrameId)
480
+ render();
481
+ }
482
+ else {
483
+ const newHover = nodeAt(e);
484
+ if (newHover !== hoveredNode) {
485
+ hoveredNode = newHover || null;
486
+ canvas.style.cursor = hoveredNode ? 'pointer' : 'grab';
487
+ updateTooltip(hoveredNode, e.clientX, e.clientY);
488
+ if (!animationFrameId)
489
+ render();
490
+ }
491
+ else if (hoveredNode) {
492
+ updateTooltip(hoveredNode, e.clientX, e.clientY);
493
+ }
494
+ }
495
+ });
496
+ const stopPointer = e => {
497
+ draggedNode = null;
498
+ isPanning = false;
499
+ if (canvas.hasPointerCapture(e.pointerId))
500
+ canvas.releasePointerCapture(e.pointerId);
501
+ };
502
+ listen(canvas, 'pointerup', stopPointer);
503
+ listen(canvas, 'pointercancel', stopPointer);
504
+ listen(canvas, 'wheel', e => {
505
+ e.preventDefault();
506
+ const rect = canvas.getBoundingClientRect();
507
+ const mx = e.clientX - rect.left;
508
+ const my = e.clientY - rect.top;
509
+ const zoomFactor = e.deltaY < 0 ? 1.15 : 0.87;
510
+ const newK = Math.max(0.1, Math.min(5.0, transform.k * zoomFactor));
511
+ transform.x = mx - (mx - transform.x) * (newK / transform.k);
512
+ transform.y = my - (my - transform.y) * (newK / transform.k);
513
+ transform.k = newK;
514
+ if (!animationFrameId)
515
+ render();
516
+ }, { passive: false });
517
+ listen(canvas, 'click', e => {
518
+ const now = Date.now();
519
+ const isDouble = now - lastClickTime < 300;
520
+ lastClickTime = now;
521
+ const clickedNode = nodeAt(e);
522
+ if (clickedNode) {
523
+ if (isDouble) {
524
+ flyToNode(clickedNode);
525
+ }
526
+ else {
527
+ selectedNode = clickedNode;
528
+ canvas.dispatchEvent(new CustomEvent('graph-node-click', {
529
+ detail: { path: clickedNode.path },
530
+ bubbles: true
531
+ }));
532
+ }
533
+ }
534
+ else {
535
+ selectedNode = null;
536
+ if (!animationFrameId)
537
+ render();
538
+ }
539
+ });
540
+ listen(canvas, 'mouseleave', () => {
541
+ hoveredNode = null;
542
+ updateTooltip(null);
543
+ if (!animationFrameId)
544
+ render();
545
+ });
546
+ // Wire Controls HUD
547
+ const resetBtn = document.getElementById('graphReset');
548
+ const zoomInBtn = document.getElementById('graphZoomIn');
549
+ const zoomOutBtn = document.getElementById('graphZoomOut');
550
+ if (resetBtn)
551
+ listen(resetBtn, 'click', () => resetLayout());
552
+ if (zoomInBtn)
553
+ listen(zoomInBtn, 'click', () => {
554
+ const newK = Math.min(5.0, transform.k * 1.25);
555
+ transform.x = width / 2 - (width / 2 - transform.x) * (newK / transform.k);
556
+ transform.y = height / 2 - (height / 2 - transform.y) * (newK / transform.k);
557
+ transform.k = newK;
558
+ if (!animationFrameId)
559
+ render();
560
+ });
561
+ if (zoomOutBtn)
562
+ listen(zoomOutBtn, 'click', () => {
563
+ const newK = Math.max(0.1, transform.k * 0.8);
564
+ transform.x = width / 2 - (width / 2 - transform.x) * (newK / transform.k);
565
+ transform.y = height / 2 - (height / 2 - transform.y) * (newK / transform.k);
566
+ transform.k = newK;
567
+ if (!animationFrameId)
568
+ render();
569
+ });
570
+ function updateActivity() {
571
+ const next = viewActive && !document.hidden && !destroyed;
572
+ if (next === active)
573
+ return;
574
+ active = next;
575
+ if (!active) {
576
+ if (animationFrameId)
577
+ cancelAnimationFrame(animationFrameId);
578
+ animationFrameId = null;
579
+ draggedNode = null;
580
+ isPanning = false;
581
+ updateTooltip(null);
582
+ }
583
+ else {
584
+ resize();
585
+ if (alpha > alphaMin && !animationFrameId)
586
+ animationFrameId = requestAnimationFrame(tick);
587
+ }
590
588
  }
591
- };
589
+ function setActive(value) { viewActive = value; updateActivity(); }
590
+ window.addEventListener('resize', resize);
591
+ document.addEventListener('visibilitychange', updateActivity);
592
+ const initialResize = setTimeout(resize, 50);
593
+ return {
594
+ update,
595
+ resize,
596
+ setActive,
597
+ fit: fitToScreen,
598
+ reset: resetLayout,
599
+ search: q => { searchQuery = q; if (!animationFrameId)
600
+ render(); },
601
+ highlight: path => { highlightPath = path; if (!animationFrameId)
602
+ render(); },
603
+ destroy: () => {
604
+ destroyed = true;
605
+ active = false;
606
+ interactions.abort();
607
+ updateTooltip(null);
608
+ clearTimeout(initialResize);
609
+ document.removeEventListener('visibilitychange', updateActivity);
610
+ window.removeEventListener('resize', resize);
611
+ if (animationFrameId)
612
+ cancelAnimationFrame(animationFrameId);
613
+ animationFrameId = null;
614
+ }
615
+ };
592
616
  }
617
+ //# sourceMappingURL=forceGraph.js.map