kirograph 0.12.1

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 (434) hide show
  1. package/README.md +1171 -0
  2. package/dist/architecture/index.d.ts +11 -0
  3. package/dist/architecture/index.d.ts.map +1 -0
  4. package/dist/architecture/index.js +207 -0
  5. package/dist/architecture/index.js.map +7 -0
  6. package/dist/architecture/layers/csharp.d.ts +6 -0
  7. package/dist/architecture/layers/csharp.d.ts.map +1 -0
  8. package/dist/architecture/layers/csharp.js +100 -0
  9. package/dist/architecture/layers/csharp.js.map +7 -0
  10. package/dist/architecture/layers/elixir.js +116 -0
  11. package/dist/architecture/layers/elixir.js.map +7 -0
  12. package/dist/architecture/layers/go.d.ts +7 -0
  13. package/dist/architecture/layers/go.d.ts.map +1 -0
  14. package/dist/architecture/layers/go.js +117 -0
  15. package/dist/architecture/layers/go.js.map +7 -0
  16. package/dist/architecture/layers/index.d.ts +30 -0
  17. package/dist/architecture/layers/index.d.ts.map +1 -0
  18. package/dist/architecture/layers/index.js +100 -0
  19. package/dist/architecture/layers/index.js.map +7 -0
  20. package/dist/architecture/layers/java.d.ts +7 -0
  21. package/dist/architecture/layers/java.d.ts.map +1 -0
  22. package/dist/architecture/layers/java.js +119 -0
  23. package/dist/architecture/layers/java.js.map +7 -0
  24. package/dist/architecture/layers/python.d.ts +7 -0
  25. package/dist/architecture/layers/python.d.ts.map +1 -0
  26. package/dist/architecture/layers/python.js +111 -0
  27. package/dist/architecture/layers/python.js.map +7 -0
  28. package/dist/architecture/layers/ruby.d.ts +6 -0
  29. package/dist/architecture/layers/ruby.d.ts.map +1 -0
  30. package/dist/architecture/layers/ruby.js +95 -0
  31. package/dist/architecture/layers/ruby.js.map +7 -0
  32. package/dist/architecture/layers/rust.d.ts +6 -0
  33. package/dist/architecture/layers/rust.d.ts.map +1 -0
  34. package/dist/architecture/layers/rust.js +98 -0
  35. package/dist/architecture/layers/rust.js.map +7 -0
  36. package/dist/architecture/layers/types.d.ts +2 -0
  37. package/dist/architecture/layers/types.d.ts.map +1 -0
  38. package/dist/architecture/layers/types.js +17 -0
  39. package/dist/architecture/layers/types.js.map +7 -0
  40. package/dist/architecture/layers/typescript.d.ts +9 -0
  41. package/dist/architecture/layers/typescript.d.ts.map +1 -0
  42. package/dist/architecture/layers/typescript.js +143 -0
  43. package/dist/architecture/layers/typescript.js.map +7 -0
  44. package/dist/architecture/manifest/cargo.d.ts +3 -0
  45. package/dist/architecture/manifest/cargo.d.ts.map +1 -0
  46. package/dist/architecture/manifest/cargo.js +94 -0
  47. package/dist/architecture/manifest/cargo.js.map +7 -0
  48. package/dist/architecture/manifest/csproj.d.ts +3 -0
  49. package/dist/architecture/manifest/csproj.d.ts.map +1 -0
  50. package/dist/architecture/manifest/csproj.js +75 -0
  51. package/dist/architecture/manifest/csproj.js.map +7 -0
  52. package/dist/architecture/manifest/go.d.ts +3 -0
  53. package/dist/architecture/manifest/go.d.ts.map +1 -0
  54. package/dist/architecture/manifest/go.js +85 -0
  55. package/dist/architecture/manifest/go.js.map +7 -0
  56. package/dist/architecture/manifest/gradle.d.ts +3 -0
  57. package/dist/architecture/manifest/gradle.d.ts.map +1 -0
  58. package/dist/architecture/manifest/gradle.js +80 -0
  59. package/dist/architecture/manifest/gradle.js.map +7 -0
  60. package/dist/architecture/manifest/index.d.ts +12 -0
  61. package/dist/architecture/manifest/index.d.ts.map +1 -0
  62. package/dist/architecture/manifest/index.js +130 -0
  63. package/dist/architecture/manifest/index.js.map +7 -0
  64. package/dist/architecture/manifest/maven.d.ts +3 -0
  65. package/dist/architecture/manifest/maven.d.ts.map +1 -0
  66. package/dist/architecture/manifest/maven.js +76 -0
  67. package/dist/architecture/manifest/maven.js.map +7 -0
  68. package/dist/architecture/manifest/npm.d.ts +3 -0
  69. package/dist/architecture/manifest/npm.d.ts.map +1 -0
  70. package/dist/architecture/manifest/npm.js +103 -0
  71. package/dist/architecture/manifest/npm.js.map +7 -0
  72. package/dist/architecture/manifest/python.d.ts +3 -0
  73. package/dist/architecture/manifest/python.d.ts.map +1 -0
  74. package/dist/architecture/manifest/python.js +105 -0
  75. package/dist/architecture/manifest/python.js.map +7 -0
  76. package/dist/architecture/manifest/types.d.ts +2 -0
  77. package/dist/architecture/manifest/types.d.ts.map +1 -0
  78. package/dist/architecture/manifest/types.js +17 -0
  79. package/dist/architecture/manifest/types.js.map +7 -0
  80. package/dist/architecture/types.d.ts +91 -0
  81. package/dist/architecture/types.d.ts.map +1 -0
  82. package/dist/architecture/types.js +17 -0
  83. package/dist/architecture/types.js.map +7 -0
  84. package/dist/assets/logo.png +0 -0
  85. package/dist/banner.d.ts +6 -0
  86. package/dist/banner.d.ts.map +1 -0
  87. package/dist/banner.js +67 -0
  88. package/dist/banner.js.map +1 -0
  89. package/dist/bin/banner.d.ts +6 -0
  90. package/dist/bin/banner.d.ts.map +1 -0
  91. package/dist/bin/banner.js +88 -0
  92. package/dist/bin/banner.js.map +7 -0
  93. package/dist/bin/commands/affected.d.ts +3 -0
  94. package/dist/bin/commands/affected.d.ts.map +1 -0
  95. package/dist/bin/commands/affected.js +78 -0
  96. package/dist/bin/commands/affected.js.map +7 -0
  97. package/dist/bin/commands/architecture.d.ts +3 -0
  98. package/dist/bin/commands/architecture.d.ts.map +1 -0
  99. package/dist/bin/commands/architecture.js +125 -0
  100. package/dist/bin/commands/architecture.js.map +7 -0
  101. package/dist/bin/commands/caveman.js +136 -0
  102. package/dist/bin/commands/caveman.js.map +7 -0
  103. package/dist/bin/commands/context.d.ts +3 -0
  104. package/dist/bin/commands/context.d.ts.map +1 -0
  105. package/dist/bin/commands/context.js +81 -0
  106. package/dist/bin/commands/context.js.map +7 -0
  107. package/dist/bin/commands/coupling.d.ts +3 -0
  108. package/dist/bin/commands/coupling.d.ts.map +1 -0
  109. package/dist/bin/commands/coupling.js +164 -0
  110. package/dist/bin/commands/coupling.js.map +7 -0
  111. package/dist/bin/commands/dashboard.d.ts +3 -0
  112. package/dist/bin/commands/dashboard.d.ts.map +1 -0
  113. package/dist/bin/commands/dashboard.js +209 -0
  114. package/dist/bin/commands/dashboard.js.map +7 -0
  115. package/dist/bin/commands/dead-code.js +77 -0
  116. package/dist/bin/commands/dead-code.js.map +7 -0
  117. package/dist/bin/commands/export.js +2620 -0
  118. package/dist/bin/commands/export.js.map +7 -0
  119. package/dist/bin/commands/files.d.ts +3 -0
  120. package/dist/bin/commands/files.d.ts.map +1 -0
  121. package/dist/bin/commands/files.js +104 -0
  122. package/dist/bin/commands/files.js.map +7 -0
  123. package/dist/bin/commands/help.d.ts +4 -0
  124. package/dist/bin/commands/help.d.ts.map +1 -0
  125. package/dist/bin/commands/help.js +212 -0
  126. package/dist/bin/commands/help.js.map +7 -0
  127. package/dist/bin/commands/hotspots.js +77 -0
  128. package/dist/bin/commands/hotspots.js.map +7 -0
  129. package/dist/bin/commands/index.d.ts +3 -0
  130. package/dist/bin/commands/index.d.ts.map +1 -0
  131. package/dist/bin/commands/index.js +58 -0
  132. package/dist/bin/commands/index.js.map +7 -0
  133. package/dist/bin/commands/init.d.ts +3 -0
  134. package/dist/bin/commands/init.d.ts.map +1 -0
  135. package/dist/bin/commands/init.js +68 -0
  136. package/dist/bin/commands/init.js.map +7 -0
  137. package/dist/bin/commands/install.d.ts +3 -0
  138. package/dist/bin/commands/install.d.ts.map +1 -0
  139. package/dist/bin/commands/install.js +34 -0
  140. package/dist/bin/commands/install.js.map +7 -0
  141. package/dist/bin/commands/mark-dirty.d.ts +3 -0
  142. package/dist/bin/commands/mark-dirty.d.ts.map +1 -0
  143. package/dist/bin/commands/mark-dirty.js +51 -0
  144. package/dist/bin/commands/mark-dirty.js.map +7 -0
  145. package/dist/bin/commands/package.d.ts +3 -0
  146. package/dist/bin/commands/package.d.ts.map +1 -0
  147. package/dist/bin/commands/package.js +139 -0
  148. package/dist/bin/commands/package.js.map +7 -0
  149. package/dist/bin/commands/path.js +93 -0
  150. package/dist/bin/commands/path.js.map +7 -0
  151. package/dist/bin/commands/qdrant.d.ts +3 -0
  152. package/dist/bin/commands/qdrant.d.ts.map +1 -0
  153. package/dist/bin/commands/qdrant.js +159 -0
  154. package/dist/bin/commands/qdrant.js.map +1 -0
  155. package/dist/bin/commands/query.d.ts +3 -0
  156. package/dist/bin/commands/query.d.ts.map +1 -0
  157. package/dist/bin/commands/query.js +47 -0
  158. package/dist/bin/commands/query.js.map +7 -0
  159. package/dist/bin/commands/serve.d.ts +3 -0
  160. package/dist/bin/commands/serve.d.ts.map +1 -0
  161. package/dist/bin/commands/serve.js +59 -0
  162. package/dist/bin/commands/serve.js.map +7 -0
  163. package/dist/bin/commands/snapshot.js +122 -0
  164. package/dist/bin/commands/snapshot.js.map +7 -0
  165. package/dist/bin/commands/status.d.ts +3 -0
  166. package/dist/bin/commands/status.d.ts.map +1 -0
  167. package/dist/bin/commands/status.js +107 -0
  168. package/dist/bin/commands/status.js.map +7 -0
  169. package/dist/bin/commands/stop.d.ts +3 -0
  170. package/dist/bin/commands/stop.d.ts.map +1 -0
  171. package/dist/bin/commands/stop.js +81 -0
  172. package/dist/bin/commands/stop.js.map +1 -0
  173. package/dist/bin/commands/surprising.js +79 -0
  174. package/dist/bin/commands/surprising.js.map +7 -0
  175. package/dist/bin/commands/sync-if-dirty.d.ts +3 -0
  176. package/dist/bin/commands/sync-if-dirty.d.ts.map +1 -0
  177. package/dist/bin/commands/sync-if-dirty.js +67 -0
  178. package/dist/bin/commands/sync-if-dirty.js.map +7 -0
  179. package/dist/bin/commands/sync.d.ts +3 -0
  180. package/dist/bin/commands/sync.d.ts.map +1 -0
  181. package/dist/bin/commands/sync.js +81 -0
  182. package/dist/bin/commands/sync.js.map +7 -0
  183. package/dist/bin/commands/typesense.d.ts +3 -0
  184. package/dist/bin/commands/typesense.d.ts.map +1 -0
  185. package/dist/bin/commands/typesense.js +126 -0
  186. package/dist/bin/commands/typesense.js.map +1 -0
  187. package/dist/bin/commands/uninit.d.ts +4 -0
  188. package/dist/bin/commands/uninit.d.ts.map +1 -0
  189. package/dist/bin/commands/uninit.js +123 -0
  190. package/dist/bin/commands/uninit.js.map +7 -0
  191. package/dist/bin/commands/unlock.d.ts +3 -0
  192. package/dist/bin/commands/unlock.d.ts.map +1 -0
  193. package/dist/bin/commands/unlock.js +53 -0
  194. package/dist/bin/commands/unlock.js.map +7 -0
  195. package/dist/bin/commands/utils.d.ts +12 -0
  196. package/dist/bin/commands/utils.d.ts.map +1 -0
  197. package/dist/bin/commands/utils.js +56 -0
  198. package/dist/bin/commands/utils.js.map +7 -0
  199. package/dist/bin/installer/archive.js +230 -0
  200. package/dist/bin/installer/archive.js.map +7 -0
  201. package/dist/bin/installer/caveman.js +57 -0
  202. package/dist/bin/installer/caveman.js.map +7 -0
  203. package/dist/bin/installer/cli-agent.d.ts +15 -0
  204. package/dist/bin/installer/cli-agent.d.ts.map +1 -0
  205. package/dist/bin/installer/cli-agent.js +89 -0
  206. package/dist/bin/installer/cli-agent.js.map +7 -0
  207. package/dist/bin/installer/config-prompt.d.ts +13 -0
  208. package/dist/bin/installer/config-prompt.d.ts.map +1 -0
  209. package/dist/bin/installer/config-prompt.js +158 -0
  210. package/dist/bin/installer/config-prompt.js.map +7 -0
  211. package/dist/bin/installer/dashboard.d.ts +3 -0
  212. package/dist/bin/installer/dashboard.d.ts.map +1 -0
  213. package/dist/bin/installer/dashboard.js +149 -0
  214. package/dist/bin/installer/dashboard.js.map +7 -0
  215. package/dist/bin/installer/hooks.d.ts +5 -0
  216. package/dist/bin/installer/hooks.d.ts.map +1 -0
  217. package/dist/bin/installer/hooks.js +155 -0
  218. package/dist/bin/installer/hooks.js.map +7 -0
  219. package/dist/bin/installer/index.d.ts +11 -0
  220. package/dist/bin/installer/index.d.ts.map +1 -0
  221. package/dist/bin/installer/index.js +228 -0
  222. package/dist/bin/installer/index.js.map +7 -0
  223. package/dist/bin/installer/mcp.d.ts +5 -0
  224. package/dist/bin/installer/mcp.d.ts.map +1 -0
  225. package/dist/bin/installer/mcp.js +80 -0
  226. package/dist/bin/installer/mcp.js.map +7 -0
  227. package/dist/bin/installer/prompts.d.ts +28 -0
  228. package/dist/bin/installer/prompts.d.ts.map +1 -0
  229. package/dist/bin/installer/prompts.js +134 -0
  230. package/dist/bin/installer/prompts.js.map +7 -0
  231. package/dist/bin/installer/qdrant-dashboard.d.ts +4 -0
  232. package/dist/bin/installer/qdrant-dashboard.d.ts.map +1 -0
  233. package/dist/bin/installer/qdrant-dashboard.js +115 -0
  234. package/dist/bin/installer/qdrant-dashboard.js.map +7 -0
  235. package/dist/bin/installer/steering.d.ts +5 -0
  236. package/dist/bin/installer/steering.d.ts.map +1 -0
  237. package/dist/bin/installer/steering.js +283 -0
  238. package/dist/bin/installer/steering.js.map +7 -0
  239. package/dist/bin/kirograph.d.ts +6 -0
  240. package/dist/bin/kirograph.d.ts.map +1 -0
  241. package/dist/bin/kirograph.js +95 -0
  242. package/dist/bin/kirograph.js.map +7 -0
  243. package/dist/bin/progress.d.ts +14 -0
  244. package/dist/bin/progress.d.ts.map +1 -0
  245. package/dist/bin/progress.js +201 -0
  246. package/dist/bin/progress.js.map +7 -0
  247. package/dist/bin/ui.d.ts +11 -0
  248. package/dist/bin/ui.d.ts.map +1 -0
  249. package/dist/bin/ui.js +71 -0
  250. package/dist/bin/ui.js.map +7 -0
  251. package/dist/config.d.ts +48 -0
  252. package/dist/config.d.ts.map +1 -0
  253. package/dist/config.js +273 -0
  254. package/dist/config.js.map +7 -0
  255. package/dist/context/index.d.ts +61 -0
  256. package/dist/context/index.d.ts.map +1 -0
  257. package/dist/context/index.js +224 -0
  258. package/dist/context/index.js.map +7 -0
  259. package/dist/core/file-tree.d.ts +15 -0
  260. package/dist/core/file-tree.d.ts.map +1 -0
  261. package/dist/core/file-tree.js +69 -0
  262. package/dist/core/file-tree.js.map +7 -0
  263. package/dist/core/lock-manager.d.ts +20 -0
  264. package/dist/core/lock-manager.d.ts.map +1 -0
  265. package/dist/core/lock-manager.js +120 -0
  266. package/dist/core/lock-manager.js.map +7 -0
  267. package/dist/core/pipeline.d.ts +37 -0
  268. package/dist/core/pipeline.d.ts.map +1 -0
  269. package/dist/core/pipeline.js +375 -0
  270. package/dist/core/pipeline.js.map +7 -0
  271. package/dist/core/snapshot.js +141 -0
  272. package/dist/core/snapshot.js.map +7 -0
  273. package/dist/db/database.d.ts +133 -0
  274. package/dist/db/database.d.ts.map +1 -0
  275. package/dist/db/database.js +929 -0
  276. package/dist/db/database.js.map +7 -0
  277. package/dist/db/schema.sql +174 -0
  278. package/dist/errors.d.ts +49 -0
  279. package/dist/errors.d.ts.map +1 -0
  280. package/dist/errors.js +160 -0
  281. package/dist/errors.js.map +7 -0
  282. package/dist/extraction/extractor.d.ts +29 -0
  283. package/dist/extraction/extractor.d.ts.map +1 -0
  284. package/dist/extraction/extractor.js +764 -0
  285. package/dist/extraction/extractor.js.map +7 -0
  286. package/dist/extraction/grammars.d.ts +48 -0
  287. package/dist/extraction/grammars.d.ts.map +1 -0
  288. package/dist/extraction/grammars.js +166 -0
  289. package/dist/extraction/grammars.js.map +7 -0
  290. package/dist/extraction/languages.d.ts +9 -0
  291. package/dist/extraction/languages.d.ts.map +1 -0
  292. package/dist/extraction/languages.js +103 -0
  293. package/dist/extraction/languages.js.map +7 -0
  294. package/dist/extraction/wasm/tree-sitter-pascal.wasm +0 -0
  295. package/dist/frameworks/csharp.d.ts +8 -0
  296. package/dist/frameworks/csharp.d.ts.map +1 -0
  297. package/dist/frameworks/csharp.js +93 -0
  298. package/dist/frameworks/csharp.js.map +7 -0
  299. package/dist/frameworks/elixir.js +142 -0
  300. package/dist/frameworks/elixir.js.map +7 -0
  301. package/dist/frameworks/express.d.ts +8 -0
  302. package/dist/frameworks/express.d.ts.map +1 -0
  303. package/dist/frameworks/express.js +143 -0
  304. package/dist/frameworks/express.js.map +7 -0
  305. package/dist/frameworks/go.d.ts +8 -0
  306. package/dist/frameworks/go.d.ts.map +1 -0
  307. package/dist/frameworks/go.js +85 -0
  308. package/dist/frameworks/go.js.map +7 -0
  309. package/dist/frameworks/index.d.ts +30 -0
  310. package/dist/frameworks/index.d.ts.map +1 -0
  311. package/dist/frameworks/index.js +243 -0
  312. package/dist/frameworks/index.js.map +7 -0
  313. package/dist/frameworks/java.d.ts +8 -0
  314. package/dist/frameworks/java.d.ts.map +1 -0
  315. package/dist/frameworks/java.js +87 -0
  316. package/dist/frameworks/java.js.map +7 -0
  317. package/dist/frameworks/laravel.d.ts +9 -0
  318. package/dist/frameworks/laravel.d.ts.map +1 -0
  319. package/dist/frameworks/laravel.js +115 -0
  320. package/dist/frameworks/laravel.js.map +7 -0
  321. package/dist/frameworks/python.d.ts +10 -0
  322. package/dist/frameworks/python.d.ts.map +1 -0
  323. package/dist/frameworks/python.js +158 -0
  324. package/dist/frameworks/python.js.map +7 -0
  325. package/dist/frameworks/react.d.ts +9 -0
  326. package/dist/frameworks/react.d.ts.map +1 -0
  327. package/dist/frameworks/react.js +230 -0
  328. package/dist/frameworks/react.js.map +7 -0
  329. package/dist/frameworks/ruby.d.ts +8 -0
  330. package/dist/frameworks/ruby.d.ts.map +1 -0
  331. package/dist/frameworks/ruby.js +136 -0
  332. package/dist/frameworks/ruby.js.map +7 -0
  333. package/dist/frameworks/rust.d.ts +8 -0
  334. package/dist/frameworks/rust.d.ts.map +1 -0
  335. package/dist/frameworks/rust.js +82 -0
  336. package/dist/frameworks/rust.js.map +7 -0
  337. package/dist/frameworks/svelte.d.ts +8 -0
  338. package/dist/frameworks/svelte.d.ts.map +1 -0
  339. package/dist/frameworks/svelte.js +174 -0
  340. package/dist/frameworks/svelte.js.map +7 -0
  341. package/dist/frameworks/swift.d.ts +10 -0
  342. package/dist/frameworks/swift.d.ts.map +1 -0
  343. package/dist/frameworks/swift.js +151 -0
  344. package/dist/frameworks/swift.js.map +7 -0
  345. package/dist/frameworks/types.d.ts +37 -0
  346. package/dist/frameworks/types.d.ts.map +1 -0
  347. package/dist/frameworks/types.js +17 -0
  348. package/dist/frameworks/types.js.map +7 -0
  349. package/dist/graph/queries.d.ts +53 -0
  350. package/dist/graph/queries.d.ts.map +1 -0
  351. package/dist/graph/queries.js +224 -0
  352. package/dist/graph/queries.js.map +7 -0
  353. package/dist/graph/traversal.d.ts +35 -0
  354. package/dist/graph/traversal.d.ts.map +1 -0
  355. package/dist/graph/traversal.js +148 -0
  356. package/dist/graph/traversal.js.map +7 -0
  357. package/dist/index.d.ts +102 -0
  358. package/dist/index.d.ts.map +1 -0
  359. package/dist/index.js +303 -0
  360. package/dist/index.js.map +7 -0
  361. package/dist/installer/index.d.ts +10 -0
  362. package/dist/installer/index.d.ts.map +1 -0
  363. package/dist/installer/index.js +526 -0
  364. package/dist/installer/index.js.map +1 -0
  365. package/dist/mcp/server.d.ts +16 -0
  366. package/dist/mcp/server.d.ts.map +1 -0
  367. package/dist/mcp/server.js +116 -0
  368. package/dist/mcp/server.js.map +7 -0
  369. package/dist/mcp/tools.d.ts +37 -0
  370. package/dist/mcp/tools.d.ts.map +1 -0
  371. package/dist/mcp/tools.js +779 -0
  372. package/dist/mcp/tools.js.map +7 -0
  373. package/dist/mcp/transport.d.ts +29 -0
  374. package/dist/mcp/transport.d.ts.map +1 -0
  375. package/dist/mcp/transport.js +70 -0
  376. package/dist/mcp/transport.js.map +7 -0
  377. package/dist/resolution/index.d.ts +56 -0
  378. package/dist/resolution/index.d.ts.map +1 -0
  379. package/dist/resolution/index.js +384 -0
  380. package/dist/resolution/index.js.map +7 -0
  381. package/dist/resolution/name-matcher.d.ts +25 -0
  382. package/dist/resolution/name-matcher.d.ts.map +1 -0
  383. package/dist/resolution/name-matcher.js +60 -0
  384. package/dist/resolution/name-matcher.js.map +7 -0
  385. package/dist/scripts/postinstall.js +64 -0
  386. package/dist/search/query-utils.d.ts +21 -0
  387. package/dist/search/query-utils.d.ts.map +1 -0
  388. package/dist/search/query-utils.js +219 -0
  389. package/dist/search/query-utils.js.map +7 -0
  390. package/dist/search/searcher.d.ts +15 -0
  391. package/dist/search/searcher.d.ts.map +1 -0
  392. package/dist/search/searcher.js +49 -0
  393. package/dist/search/searcher.js.map +7 -0
  394. package/dist/sync/index.d.ts +33 -0
  395. package/dist/sync/index.d.ts.map +1 -0
  396. package/dist/sync/index.js +200 -0
  397. package/dist/sync/index.js.map +7 -0
  398. package/dist/types.d.ts +131 -0
  399. package/dist/types.d.ts.map +1 -0
  400. package/dist/types.js +37 -0
  401. package/dist/types.js.map +7 -0
  402. package/dist/utils.d.ts +52 -0
  403. package/dist/utils.d.ts.map +1 -0
  404. package/dist/utils.js +254 -0
  405. package/dist/utils.js.map +7 -0
  406. package/dist/vectors/index.d.ts +71 -0
  407. package/dist/vectors/index.d.ts.map +1 -0
  408. package/dist/vectors/index.js +480 -0
  409. package/dist/vectors/index.js.map +7 -0
  410. package/dist/vectors/lancedb-index.d.ts +50 -0
  411. package/dist/vectors/lancedb-index.d.ts.map +1 -0
  412. package/dist/vectors/lancedb-index.js +153 -0
  413. package/dist/vectors/lancedb-index.js.map +7 -0
  414. package/dist/vectors/orama-index.d.ts +54 -0
  415. package/dist/vectors/orama-index.d.ts.map +1 -0
  416. package/dist/vectors/orama-index.js +213 -0
  417. package/dist/vectors/orama-index.js.map +7 -0
  418. package/dist/vectors/pglite-index.d.ts +53 -0
  419. package/dist/vectors/pglite-index.d.ts.map +1 -0
  420. package/dist/vectors/pglite-index.js +194 -0
  421. package/dist/vectors/pglite-index.js.map +7 -0
  422. package/dist/vectors/qdrant-index.d.ts +70 -0
  423. package/dist/vectors/qdrant-index.d.ts.map +1 -0
  424. package/dist/vectors/qdrant-index.js +364 -0
  425. package/dist/vectors/qdrant-index.js.map +7 -0
  426. package/dist/vectors/typesense-index.d.ts +75 -0
  427. package/dist/vectors/typesense-index.d.ts.map +1 -0
  428. package/dist/vectors/typesense-index.js +453 -0
  429. package/dist/vectors/typesense-index.js.map +7 -0
  430. package/dist/vectors/vec-index.d.ts +52 -0
  431. package/dist/vectors/vec-index.d.ts.map +1 -0
  432. package/dist/vectors/vec-index.js +198 -0
  433. package/dist/vectors/vec-index.js.map +7 -0
  434. package/package.json +67 -0
@@ -0,0 +1,2620 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+ var export_exports = {};
30
+ __export(export_exports, {
31
+ register: () => register
32
+ });
33
+ module.exports = __toCommonJS(export_exports);
34
+ var fs = __toESM(require("fs"));
35
+ var path = __toESM(require("path"));
36
+ var import_child_process = require("child_process");
37
+ var import_ui = require("../ui");
38
+ function openBrowser(filePath) {
39
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
40
+ (0, import_child_process.spawn)(cmd, [filePath], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
41
+ }
42
+ async function generateExport(projectPath, opts) {
43
+ const KiroGraph = (await Promise.resolve().then(() => require("../../index.js"))).default;
44
+ const target = path.resolve(projectPath ?? process.cwd());
45
+ const cg = await KiroGraph.open(target);
46
+ const nodes = cg.getAllNodes();
47
+ const edges = cg.getAllEdges();
48
+ cg.close();
49
+ const projectName = path.basename(target);
50
+ let logoBase64;
51
+ const logoCandidates = [
52
+ path.join(__dirname, "../../assets/logo.png"),
53
+ path.join(__dirname, "../../../assets/logo.png")
54
+ ];
55
+ for (const p of logoCandidates) {
56
+ if (fs.existsSync(p)) {
57
+ logoBase64 = fs.readFileSync(p).toString("base64");
58
+ break;
59
+ }
60
+ }
61
+ const fileModified = {};
62
+ const uniquePaths = [...new Set(nodes.map((n) => n.filePath))];
63
+ for (const fp of uniquePaths) {
64
+ try {
65
+ fileModified[fp] = fs.statSync(path.join(target, fp)).mtimeMs;
66
+ } catch {
67
+ fileModified[fp] = 0;
68
+ }
69
+ }
70
+ const outDir = opts.output ? path.resolve(opts.output) : path.join(target, ".kirograph", "export");
71
+ fs.mkdirSync(outDir, { recursive: true });
72
+ const { html, css, js } = buildFiles(nodes, edges, projectName, opts.includeContains, logoBase64, fileModified);
73
+ fs.writeFileSync(path.join(outDir, "index.html"), html, "utf8");
74
+ fs.writeFileSync(path.join(outDir, "app.css"), css, "utf8");
75
+ fs.writeFileSync(path.join(outDir, "app.js"), js, "utf8");
76
+ const indexPath = path.join(outDir, "index.html");
77
+ const edgeCount = opts.includeContains ? edges.length : edges.filter((e) => e.kind !== "contains").length;
78
+ console.log();
79
+ console.log(` ${import_ui.violet}${import_ui.bold}Graph exported${import_ui.reset}`);
80
+ console.log(` ${import_ui.dim}nodes ${import_ui.reset}${import_ui.bold}${nodes.length}${import_ui.reset}`);
81
+ console.log(` ${import_ui.dim}edges ${import_ui.reset}${import_ui.bold}${edgeCount}${import_ui.reset}${opts.includeContains ? "" : import_ui.dim + " (contains edges excluded)" + import_ui.reset}`);
82
+ console.log(` ${import_ui.dim}output ${import_ui.reset}${import_ui.green}${outDir}${import_ui.reset}`);
83
+ console.log();
84
+ return indexPath;
85
+ }
86
+ const KIND_COLOR = {
87
+ class: "#9b59b6",
88
+ struct: "#8e44ad",
89
+ interface: "#6c3483",
90
+ trait: "#7d3c98",
91
+ protocol: "#6c3483",
92
+ function: "#5b6abf",
93
+ method: "#7986cb",
94
+ component: "#26a69a",
95
+ route: "#2e7d32",
96
+ variable: "#546e7a",
97
+ constant: "#455a64",
98
+ property: "#607d8b",
99
+ field: "#78909c",
100
+ enum: "#e67e22",
101
+ enum_member: "#f39c12",
102
+ type_alias: "#00838f",
103
+ namespace: "#00695c",
104
+ parameter: "#616161",
105
+ import: "#37474f",
106
+ export: "#37474f",
107
+ file: "#263238",
108
+ module: "#1c313a"
109
+ };
110
+ const EDGE_COLOR = {
111
+ calls: "#7986cb",
112
+ imports: "#546e7a",
113
+ exports: "#546e7a",
114
+ extends: "#ab47bc",
115
+ implements: "#8e24aa",
116
+ references: "#455a64",
117
+ type_of: "#00838f",
118
+ returns: "#0277bd",
119
+ instantiates: "#6a1b9a",
120
+ overrides: "#ad1457",
121
+ decorates: "#e67e22",
122
+ contains: "#263238"
123
+ };
124
+ const EDGE_DASHED = {
125
+ imports: true,
126
+ references: true,
127
+ type_of: true,
128
+ returns: true
129
+ };
130
+ function lighten(hex) {
131
+ const n = parseInt(hex.slice(1), 16);
132
+ const r = Math.min(255, (n >> 16 & 255) + 60);
133
+ const g = Math.min(255, (n >> 8 & 255) + 60);
134
+ const b = Math.min(255, (n & 255) + 60);
135
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
136
+ }
137
+ function escHtml(s) {
138
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
139
+ }
140
+ function buildFiles(nodes, edges, projectName, includeContains, logoBase64, fileModified) {
141
+ const filteredEdges = includeContains ? edges : edges.filter((e) => e.kind !== "contains");
142
+ const degree = /* @__PURE__ */ new Map();
143
+ for (const e of filteredEdges) {
144
+ degree.set(e.source, (degree.get(e.source) ?? 0) + 1);
145
+ degree.set(e.target, (degree.get(e.target) ?? 0) + 1);
146
+ }
147
+ const maxDegree = Math.max(0, ...degree.values());
148
+ const visNodes = nodes.map((n) => {
149
+ const fp = n.filePath ?? "";
150
+ const parts = fp.split("/").filter(Boolean);
151
+ const dir = parts.length >= 2 ? parts.slice(0, 2).join("/") : parts[0] ?? "";
152
+ return {
153
+ id: n.id,
154
+ label: n.name,
155
+ color: {
156
+ background: KIND_COLOR[n.kind] ?? "#424242",
157
+ border: lighten(KIND_COLOR[n.kind] ?? "#424242"),
158
+ highlight: { background: "#e040fb", border: "#ea80fc" },
159
+ hover: { background: lighten(KIND_COLOR[n.kind] ?? "#424242"), border: "#ea80fc" }
160
+ },
161
+ size: Math.max(8, Math.min(40, 8 + (degree.get(n.id) ?? 0) * 1.5)),
162
+ font: { size: 11, color: "#e0e0e0", face: "monospace" },
163
+ kind: n.kind,
164
+ filePath: n.filePath,
165
+ dir,
166
+ startLine: n.startLine,
167
+ qualifiedName: n.qualifiedName,
168
+ signature: n.signature ?? null,
169
+ isExported: n.isExported ?? false,
170
+ degree: degree.get(n.id) ?? 0,
171
+ borderWidth: n.isExported ? 2 : 1,
172
+ borderWidthSelected: 3,
173
+ lastModified: fileModified ? fileModified[fp] ?? 0 : 0
174
+ };
175
+ });
176
+ const visEdges = filteredEdges.map((e, i) => ({
177
+ id: i,
178
+ ekind: e.kind,
179
+ from: e.source,
180
+ to: e.target,
181
+ label: e.kind,
182
+ dashes: EDGE_DASHED[e.kind] ?? false,
183
+ color: { color: EDGE_COLOR[e.kind] ?? "#546e7a", opacity: e.kind === "contains" ? 0.2 : 0.6 },
184
+ width: ["extends", "implements", "calls"].includes(e.kind) ? 2 : 1,
185
+ font: { size: 9, color: "#546e7a", align: "middle" },
186
+ arrows: e.kind !== "contains" ? { to: { enabled: true, scaleFactor: 0.5 } } : {},
187
+ smooth: { type: "curvedCW", roundness: 0.1 }
188
+ }));
189
+ const allNodeKinds = [...new Set(nodes.map((n) => n.kind))].sort();
190
+ const allEdgeKinds = [...new Set(filteredEdges.map((e) => e.kind))].sort();
191
+ const logoTag = logoBase64 ? `<img src="data:image/png;base64,${logoBase64}" alt="KiroGraph">` : `<h1>\u2B21 KiroGraph</h1>`;
192
+ const loaderLogoTag = logoBase64 ? `<img src="data:image/png;base64,${logoBase64}" alt="KiroGraph">` : `<div style="color:#c792ea;font-size:18px;font-weight:600;letter-spacing:.05em">\u2B21 KiroGraph</div>`;
193
+ const nodeLegend = allNodeKinds.map((k) => `<div class="legend-item" data-nkind="${k}">
194
+ <div class="legend-dot" style="background:${KIND_COLOR[k] ?? "#424242"}"></div>
195
+ <span>${k}</span>
196
+ <span class="legend-count">${nodes.filter((n) => n.kind === k).length}</span>
197
+ </div>`).join("");
198
+ const edgeLegend = allEdgeKinds.map((k) => `<div class="legend-item" data-ekind="${k}">
199
+ <div class="legend-line" style="${EDGE_DASHED[k] ? `border-top:2px dashed ${EDGE_COLOR[k] ?? "#546e7a"};height:0` : `background:${EDGE_COLOR[k] ?? "#546e7a"}`}"></div>
200
+ <span>${k}</span>
201
+ <span class="legend-count">${filteredEdges.filter((e) => e.kind === k).length}</span>
202
+ </div>`).join("");
203
+ const html = `<!DOCTYPE html>
204
+ <html lang="en">
205
+ <head>
206
+ <meta charset="UTF-8">
207
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
208
+ <title>KiroGraph \u2014 ${escHtml(projectName)}</title>
209
+ <link rel="stylesheet" href="./app.css">
210
+ <link rel="preload" href="https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js" as="script">
211
+ <script src="https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script>
212
+ </head>
213
+ <body>
214
+
215
+ <div id="init-loader">
216
+ ${loaderLogoTag}
217
+ <div class="init-title">${visNodes.length} nodes \xB7 ${visEdges.length} edges \xB7 ${escHtml(projectName)}</div>
218
+ <div id="init-progress-wrap"><div id="init-progress-bar"></div></div>
219
+ <div id="init-status">Loading\u2026</div>
220
+ </div>
221
+
222
+ <div id="main">
223
+ <div id="loader"><div class="spinner"></div></div>
224
+ <div id="path-bar">Click a node to set start\u2026</div>
225
+ <div id="graph"></div>
226
+
227
+ <!-- Floating left sidebar: brand + search + tools, all stacked -->
228
+ <div id="float-sidebar">
229
+ <div id="float-brand">
230
+ ${logoTag}
231
+ <div id="float-brand-info">
232
+ <span id="proj">${escHtml(projectName)}</span>
233
+ <span id="stats">${visNodes.length} nodes \xB7 ${visEdges.length} edges</span>
234
+ </div>
235
+ </div>
236
+
237
+ <div id="float-left">
238
+ <input id="search" type="text" placeholder="\u{1F50D} Search symbols\u2026">
239
+
240
+ <div class="float-group vertical">
241
+ <button class="fbtn" id="btn-fit" title="Fit graph to view">\u229E <span class="fbtn-label">Fit</span></button>
242
+ <button class="fbtn active" id="btn-physics" title="Toggle physics">\u26A1 <span class="fbtn-label">Physics</span></button>
243
+
244
+ <div class="fslider-row" id="physics-speed-row">
245
+ <span class="fslider-label">Slow</span>
246
+ <input type="range" id="physics-speed" min="1" max="10" value="5" step="1" title="Physics speed">
247
+ <span class="fslider-label">Fast</span>
248
+ </div>
249
+
250
+ <button class="fbtn" id="btn-png" title="Export PNG">\u{1F4F7} <span class="fbtn-label">PNG</span></button>
251
+ </div>
252
+
253
+ <div class="float-group vertical">
254
+ <button class="fbtn" id="btn-focus" title="Focus on node and neighbors">\u25CE <span class="fbtn-label">Focus</span></button>
255
+ <button class="fbtn" id="btn-path" title="Find path between two nodes">\u27F6 <span class="fbtn-label">Path</span></button>
256
+ <button class="fbtn" id="btn-cluster" title="Cluster by directory">\u2B21 <span class="fbtn-label">Cluster</span></button>
257
+ <button class="fbtn" id="btn-heat" title="Heat map by file recency">\u{1F321} <span class="fbtn-label">Heat</span></button>
258
+ <button class="fbtn" id="btn-charts" title="Analytics charts">\u{1F4CA} <span class="fbtn-label">Charts</span></button>
259
+ </div>
260
+
261
+ <div id="heat-legend">
262
+ <div class="heat-legend-title">\u{1F321} File recency</div>
263
+ <div id="heat-gradient"></div>
264
+ <div class="heat-stops">
265
+ <div class="heat-stop" data-tip="Modified in the last 24 hours"><span class="heat-stop-dot" style="background:#e74c3c"></span>Today</div>
266
+ <div class="heat-stop" data-tip="Modified in the last week"><span class="heat-stop-dot" style="background:#e67e22"></span>This week</div>
267
+ <div class="heat-stop" data-tip="Modified in the last month"><span class="heat-stop-dot" style="background:#f1c40f"></span>This month</div>
268
+ <div class="heat-stop" data-tip="Modified in the last 6 months"><span class="heat-stop-dot" style="background:#27ae60"></span>6 months</div>
269
+ <div class="heat-stop" data-tip="Modified more than 6 months ago"><span class="heat-stop-dot" style="background:#2980b9"></span>Older</div>
270
+ </div>
271
+ <div id="heat-tip"></div>
272
+ </div>
273
+ </div>
274
+ </div><!-- /float-sidebar -->
275
+
276
+ <div id="minimap"><canvas id="minimap-canvas" width="180" height="120"></canvas></div>
277
+ <div id="ctx-menu"></div>
278
+
279
+ <!-- Charts modal -->
280
+ <div id="charts-modal" style="display:none">
281
+ <div id="charts-panel">
282
+ <div id="charts-header">
283
+ <span>\u{1F4CA} Graph Analytics</span>
284
+ <button id="charts-close">\u2715</button>
285
+ </div>
286
+ <div id="charts-body">
287
+ <div class="chart-block span-3">
288
+ <div class="chart-title">Top 15 Most Connected Symbols</div>
289
+
290
+ <div class="chart-insight">Symbols with the highest combined in + out degree are the load-bearing pillars of your codebase.<br>A very long bar on an internal helper is a red flag \u2014 it means many things depend on one place that wasn't designed as an API.</div>
291
+ <canvas id="chart-bar" width="1080" height="280"></canvas>
292
+ </div>
293
+ <div class="chart-block span-1">
294
+ <div class="chart-title">Node Distribution by Kind</div>
295
+ <div class="chart-insight">Shows what your codebase is made of at a glance \u2014 heavy on functions vs. classes vs. types.<br>A dominant slice (e.g. 80% variables) can indicate over-abstraction or, conversely, too little structure.</div>
296
+ <canvas id="chart-pie" width="540" height="280"></canvas>
297
+ </div>
298
+ <div class="chart-block span-2">
299
+ <div class="chart-title">Degree Distribution \u2014 Connectivity Curve</div>
300
+ <div class="chart-insight">Healthy codebases show a steep left peak (most symbols have few connections) and a long tail.<br>A flat or multi-modal curve signals coupling problems \u2014 too many symbols are over-connected.</div>
301
+ <canvas id="chart-line" width="720" height="280"></canvas>
302
+ </div>
303
+
304
+ <div class="chart-block span-3">
305
+ <div class="chart-title">Top 15 Callers \u2014 Highest Out-Degree</div>
306
+ <div class="chart-insight">High out-degree means this symbol knows too much about the rest of the system \u2014 a classic god-object signal.<br>If the top callers are entry points or orchestrators that's fine; if they're utility helpers, consider splitting them.</div>
307
+ <canvas id="chart-callers" width="1080" height="280"></canvas>
308
+ </div>
309
+
310
+ <div class="chart-block span-3">
311
+ <div class="chart-title">Top 15 Callees \u2014 Highest In-Degree</div>
312
+ <div class="chart-insight">These are your real public API \u2014 change them and many things break. High in-degree is expected for core utilities.<br>Surprise entries here (a private helper, a constant) indicate hidden coupling you should make explicit.</div>
313
+ <canvas id="chart-callees" width="1080" height="280"></canvas>
314
+ </div>
315
+
316
+ <div class="chart-block span-3">
317
+ <div class="chart-title">Files by Symbol Count</div>
318
+ <div class="chart-insight">Files with many symbols are candidates for splitting \u2014 they likely handle multiple responsibilities.<br>Cross-reference with the coupling matrix: a large file that also couples to many directories is a high-priority refactor target.</div>
319
+ <canvas id="chart-files" width="1080" height="280"></canvas>
320
+ </div>
321
+
322
+ <div class="chart-block span-1">
323
+ <div class="chart-title">Edge Kind Distribution</div>
324
+ <div class="chart-insight">Tells you which relationship patterns dominate: calls, imports, inheritance, containment.<br>An unusually high import ratio vs. calls can mean modules are tightly wired at the file level without using each other's symbols.</div>
325
+ <canvas id="chart-edgekinds" width="400" height="280"></canvas>
326
+ </div>
327
+
328
+ <div class="chart-block span-1">
329
+ <div class="chart-title">Dead Code by Kind</div>
330
+ <div class="chart-insight">These symbols are never referenced from within the indexed codebase \u2014 strong deletion candidates.<br>Focus on functions and classes; variables and constants are more likely to be used via dynamic patterns not captured by static analysis.</div>
331
+ <canvas id="chart-deadcode" width="400" height="280"></canvas>
332
+ </div>
333
+
334
+ <div class="chart-block span-1">
335
+ <div class="chart-title">Symbol Count per Directory</div>
336
+ <div class="chart-insight">A single directory holding most symbols is a monolith smell \u2014 the rest of the structure may be cosmetic.<br>Balanced distribution suggests good separation of concerns; a very tall single bar suggests your folder structure doesn't reflect your actual architecture.</div>
337
+ <canvas id="chart-dirs" width="400" height="280"></canvas>
338
+ </div>
339
+
340
+ <div class="chart-block span-3">
341
+ <div class="chart-title">Exported vs Unexported by Kind</div>
342
+ <div class="chart-insight">Shows how much of each symbol type is intentionally public. A large unexported slice is healthy \u2014 it means most logic is encapsulated.<br>If nearly everything is exported, your module boundaries are weak and consumers have too much surface area to depend on.</div>
343
+ <canvas id="chart-exported" width="1080" height="260"></canvas>
344
+ </div>
345
+
346
+ <div class="chart-block span-3">
347
+ <div class="chart-title">Directory Coupling Matrix</div>
348
+ <div class="chart-insight">Each row is a source directory, each column is a target \u2014 a bright cell means many edges cross from that row's folder into that column's folder.<br>Symmetrically bright off-diagonal pairs (A\u2194B and B\u2194A) reveal circular coupling between modules that should be cleanly layered.</div>
349
+ <canvas id="chart-coupling" width="1080" height="420"></canvas>
350
+ </div>
351
+
352
+ <div class="chart-block span-2">
353
+ <div class="chart-title">In-Degree vs Out-Degree Scatter</div>
354
+ <div class="chart-insight">Top-left = Sinks (referenced a lot, calls nothing) \u2014 typical for pure data types or leaf utilities.<br>Bottom-right = Sources (calls a lot, rarely referenced) \u2014 often entry points or wiring code. Top-right = Hubs \u2014 investigate these first.</div>
355
+ <canvas id="chart-scatter" width="720" height="320"></canvas>
356
+ </div>
357
+
358
+ <div class="chart-block span-1">
359
+ <div class="chart-title">Average Degree per File</div>
360
+ <div class="chart-insight">Files with high average degree are deeply entangled regardless of their size \u2014 even a 2-symbol file can score high.<br>This metric catches hidden complexity better than raw symbol count: a small file with a huge average is often the real coupling hotspot.</div>
361
+ <canvas id="chart-avgdeg" width="400" height="320"></canvas>
362
+ </div>
363
+
364
+ </div>
365
+ </div>
366
+ </div>
367
+
368
+ <div id="panel">
369
+ <div id="panel-tabs">
370
+ <div class="tab active" data-tab="detail">Detail</div>
371
+ <div class="tab" data-tab="legend">Legend</div>
372
+ <div class="tab" data-tab="filters">Filters</div>
373
+ </div>
374
+ <div id="panel-content">
375
+
376
+ <!-- Detail tab -->
377
+ <div id="tab-detail">
378
+ <div id="history-nav">
379
+ <button class="hist-btn" id="hist-back" disabled>\u2039</button>
380
+ <span id="hist-label">no selection</span>
381
+ <button class="hist-btn" id="hist-forward" disabled>\u203A</button>
382
+ </div>
383
+ <p class="detail-empty">Click a node to inspect it.</p>
384
+ </div>
385
+
386
+ <!-- Legend tab -->
387
+ <div id="tab-legend" style="display:none">
388
+ <div class="legend-section">
389
+ <div class="legend-title">Node kinds <span style="color:#37474f;font-size:9px">(click to toggle)</span></div>
390
+ ${nodeLegend}
391
+ </div>
392
+ <div class="legend-section">
393
+ <div class="legend-title">Edge kinds <span style="color:#37474f;font-size:9px">(click to toggle)</span></div>
394
+ ${edgeLegend}
395
+ </div>
396
+ </div>
397
+
398
+ <!-- Filters tab -->
399
+ <div id="tab-filters" style="display:none">
400
+ <div class="filter-section">
401
+ <div class="filter-label">
402
+ Min degree
403
+ <span class="filter-value" id="degree-val">0</span>
404
+ </div>
405
+ <input type="range" id="degree-slider" min="0" max="${maxDegree}" value="0" step="1">
406
+ <div class="filter-hint">Hide nodes with fewer than N connections. Use to surface the most-connected symbols.</div>
407
+ </div>
408
+ </div>
409
+
410
+ </div>
411
+ </div>
412
+ </div>
413
+
414
+ <script src="./app.js"></script>
415
+ </body>
416
+ </html>`;
417
+ const css = buildCss();
418
+ const js = buildJs(visNodes, visEdges, KIND_COLOR, EDGE_COLOR);
419
+ return { html, css, js };
420
+ }
421
+ function buildCss() {
422
+ return `*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
423
+
424
+ body {
425
+ background: #0a0a0f;
426
+ color: #e0e0e0;
427
+ font-family: 'SF Mono','Fira Code','Consolas',monospace;
428
+ font-size: 13px;
429
+ height: 100vh;
430
+ overflow: hidden;
431
+ }
432
+
433
+ /* \u2500\u2500 Main fills full viewport \u2500\u2500 */
434
+ #main { display: flex; width: 100vw; height: 100vh; overflow: hidden; position: relative; }
435
+ #graph { flex: 1; background: #0a0a0f; }
436
+
437
+ /* \u2500\u2500 Left sidebar: single stacking container \u2500\u2500 */
438
+ #float-sidebar {
439
+ position: absolute;
440
+ top: 12px;
441
+ left: 12px;
442
+ z-index: 50;
443
+ display: flex;
444
+ flex-direction: column;
445
+ gap: 8px;
446
+ max-height: calc(100vh - 24px);
447
+ overflow-y: auto;
448
+ overflow-x: visible;
449
+ }
450
+ #float-sidebar::-webkit-scrollbar { display: none; }
451
+
452
+ /* \u2500\u2500 Brand card \u2500\u2500 */
453
+ #float-brand {
454
+ display: flex;
455
+ align-items: center;
456
+ gap: 10px;
457
+ background: rgba(8, 8, 18, 0.97);
458
+ border: 1px solid #3a3a5e;
459
+ border-radius: 12px;
460
+ padding: 8px 14px 8px 8px;
461
+ box-shadow: 0 4px 24px rgba(0,0,0,.75), 0 0 0 1px rgba(121,134,203,.08);
462
+ flex-shrink: 0;
463
+ }
464
+ #float-brand img {
465
+ height: 46px;
466
+ width: auto;
467
+ object-fit: contain;
468
+ border-radius: 6px;
469
+ }
470
+ #float-brand h1 { font-size: 13px; font-weight: 600; color: #c792ea; letter-spacing: .05em; }
471
+ #float-brand-info { display: flex; flex-direction: column; gap: 2px; }
472
+ #proj { color: #c792ea; font-size: 12px; font-weight: 600; white-space: nowrap; }
473
+ #stats { color: #6272a4; font-size: 10px; white-space: nowrap; }
474
+
475
+ /* \u2500\u2500 Search + buttons column \u2500\u2500 */
476
+ #float-left {
477
+ display: flex;
478
+ flex-direction: column;
479
+ gap: 8px;
480
+ }
481
+
482
+ /* \u2500\u2500 Search inside left column \u2500\u2500 */
483
+ #search {
484
+ background: rgba(8, 8, 18, 0.97);
485
+ border: 1px solid #3a3a5e;
486
+ border-radius: 10px;
487
+ color: #e8e8f0;
488
+ padding: 8px 14px;
489
+ font-family: inherit;
490
+ font-size: 12px;
491
+ width: 200px;
492
+ outline: none;
493
+ transition: border-color .2s, box-shadow .2s;
494
+ box-shadow: 0 4px 24px rgba(0,0,0,.75);
495
+ }
496
+ #search:focus { border-color: #7986cb; box-shadow: 0 0 0 2px rgba(121,134,203,.3); }
497
+ #search::placeholder { color: #4a4a6a; }
498
+
499
+ /* \u2500\u2500 Shared float group \u2500\u2500 */
500
+ .float-group {
501
+ display: flex;
502
+ flex-direction: column;
503
+ background: rgba(8, 8, 18, 0.97);
504
+ border: 1px solid #3a3a5e;
505
+ border-radius: 12px;
506
+ overflow: hidden;
507
+ box-shadow: 0 4px 24px rgba(0,0,0,.75), 0 0 0 1px rgba(121,134,203,.06);
508
+ width: 200px;
509
+ }
510
+
511
+ /* \u2500\u2500 Float button \u2500\u2500 */
512
+ .fbtn {
513
+ display: flex;
514
+ align-items: center;
515
+ gap: 10px;
516
+ background: transparent;
517
+ border: none;
518
+ border-bottom: 1px solid #252538;
519
+ color: #9da8cc;
520
+ padding: 9px 14px;
521
+ font-family: inherit;
522
+ font-size: 14px;
523
+ cursor: pointer;
524
+ transition: background .12s, color .12s;
525
+ white-space: nowrap;
526
+ text-align: left;
527
+ }
528
+ .fbtn:last-child { border-bottom: none; }
529
+ .fbtn:hover { background: #16162a; color: #ffffff; }
530
+ .fbtn.active { background: #1e1e40; color: #c792ea; border-left: 3px solid #c792ea; padding-left: 11px; }
531
+ .fbtn.warn { color: #e67e22; }
532
+ .fslider-row {
533
+ display: flex;
534
+ align-items: center;
535
+ gap: 6px;
536
+ padding: 7px 14px;
537
+ border-bottom: 1px solid #252538;
538
+ }
539
+ .fslider-label { font-size: 9px; color: #7a86aa; flex-shrink: 0; letter-spacing: .03em; }
540
+ #physics-speed {
541
+ flex: 1;
542
+ -webkit-appearance: none;
543
+ appearance: none;
544
+ height: 3px;
545
+ background: #2e2e52;
546
+ border-radius: 2px;
547
+ outline: none;
548
+ cursor: pointer;
549
+ }
550
+ #physics-speed::-webkit-slider-thumb {
551
+ -webkit-appearance: none;
552
+ width: 12px;
553
+ height: 12px;
554
+ border-radius: 50%;
555
+ background: #7986cb;
556
+ cursor: pointer;
557
+ border: 2px solid #c792ea;
558
+ }
559
+ #physics-speed::-moz-range-thumb {
560
+ width: 12px;
561
+ height: 12px;
562
+ border-radius: 50%;
563
+ background: #7986cb;
564
+ cursor: pointer;
565
+ border: 2px solid #c792ea;
566
+ }
567
+ .fbtn-label {
568
+ font-size: 11px;
569
+ font-weight: 500;
570
+ letter-spacing: .02em;
571
+ }
572
+
573
+ /* compat: keep .btn for any remaining uses */
574
+ .btn { display: none; }
575
+
576
+ /* \u2500\u2500 Init loader (full screen, shown on page load) \u2500\u2500 */
577
+ #init-loader {
578
+ position: fixed;
579
+ inset: 0;
580
+ background: #0a0a0f;
581
+ z-index: 1000;
582
+ display: flex;
583
+ flex-direction: column;
584
+ align-items: center;
585
+ justify-content: center;
586
+ gap: 20px;
587
+ transition: opacity .4s ease;
588
+ }
589
+ #init-loader.fade-out { opacity: 0; pointer-events: none; }
590
+ #init-loader img { max-height: 180px; max-width: 320px; width: auto; height: auto; opacity: .95; }
591
+ #init-loader .init-title { color: #546e7a; font-size: 12px; letter-spacing: .05em; }
592
+ #init-progress-wrap {
593
+ width: 220px;
594
+ background: #1a1a2e;
595
+ border-radius: 4px;
596
+ height: 3px;
597
+ overflow: hidden;
598
+ }
599
+ #init-progress-bar {
600
+ height: 100%;
601
+ width: 0%;
602
+ background: linear-gradient(90deg, #7986cb, #c792ea);
603
+ border-radius: 4px;
604
+ transition: width .1s linear;
605
+ }
606
+ #init-status { color: #37474f; font-size: 11px; }
607
+
608
+ /* \u2500\u2500 Operation loader (translucent overlay for filter ops) \u2500\u2500 */
609
+ #loader {
610
+ display: none;
611
+ position: absolute;
612
+ inset: 0;
613
+ background: rgba(10,10,15,.55);
614
+ backdrop-filter: blur(2px);
615
+ z-index: 200;
616
+ align-items: center;
617
+ justify-content: center;
618
+ pointer-events: none;
619
+ }
620
+ .spinner {
621
+ width: 26px; height: 26px;
622
+ border: 3px solid #1e1e2e;
623
+ border-top-color: #c792ea;
624
+ border-radius: 50%;
625
+ animation: spin .7s linear infinite;
626
+ }
627
+ @keyframes spin { to { transform: rotate(360deg); } }
628
+
629
+ /* \u2500\u2500 Path mode bar \u2500\u2500 */
630
+ #path-bar {
631
+ display: none;
632
+ position: absolute;
633
+ bottom: 14px;
634
+ left: 50%;
635
+ transform: translateX(-50%);
636
+ background: #12121a;
637
+ border: 1px solid #7986cb;
638
+ border-radius: 8px;
639
+ padding: 8px 16px;
640
+ font-size: 12px;
641
+ color: #c792ea;
642
+ z-index: 50;
643
+ pointer-events: none;
644
+ box-shadow: 0 4px 20px rgba(0,0,0,.6);
645
+ }
646
+
647
+ /* \u2500\u2500 Side panel \u2500\u2500 */
648
+ #panel {
649
+ width: 280px;
650
+ background: #12121a;
651
+ border-left: 1px solid #1e1e2e;
652
+ display: flex;
653
+ flex-direction: column;
654
+ overflow: hidden;
655
+ flex-shrink: 0;
656
+ transition: width .2s;
657
+ }
658
+
659
+ #panel-tabs { display: flex; border-bottom: 1px solid #1e1e2e; flex-shrink: 0; }
660
+ .tab {
661
+ flex: 1; padding: 8px; text-align: center;
662
+ font-size: 11px; color: #546e7a; cursor: pointer;
663
+ border-bottom: 2px solid transparent; transition: all .15s;
664
+ }
665
+ .tab.active { color: #c792ea; border-bottom-color: #c792ea; }
666
+ #panel-content { flex: 1; overflow-y: auto; padding: 12px; }
667
+
668
+ /* \u2500\u2500 Detail tab \u2500\u2500 */
669
+ .detail-empty { color: #37474f; font-size: 12px; padding: 8px 0; }
670
+ .detail-name { font-size: 15px; font-weight: 600; color: #c792ea; margin-bottom: 4px; word-break: break-all; }
671
+ .detail-kind {
672
+ display: inline-block; font-size: 10px; padding: 2px 7px;
673
+ border-radius: 10px; background: #1e1e2e; color: #7986cb; margin-bottom: 10px;
674
+ }
675
+ .detail-row { display: flex; gap: 8px; margin-bottom: 5px; font-size: 11px; }
676
+ .detail-label { color: #37474f; min-width: 60px; flex-shrink: 0; }
677
+ .detail-val { color: #90a4ae; word-break: break-all; }
678
+ .detail-sig {
679
+ margin-top: 8px; background: #0d0d1a; border: 1px solid #1e1e2e;
680
+ border-radius: 5px; padding: 8px; font-size: 11px; color: #7986cb;
681
+ word-break: break-all; white-space: pre-wrap;
682
+ }
683
+ .detail-actions { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; }
684
+ .action-btn {
685
+ background: #1a1a2e; border: 1px solid #2a2a3e; border-radius: 4px;
686
+ color: #7986cb; padding: 4px 8px; font-size: 11px; cursor: pointer;
687
+ font-family: inherit; transition: all .15s;
688
+ }
689
+ .action-btn:hover { border-color: #7986cb; color: #e0e0e0; }
690
+
691
+ /* \u2500\u2500 History breadcrumb \u2500\u2500 */
692
+ #history-nav {
693
+ display: flex; align-items: center; gap: 6px;
694
+ margin-bottom: 10px; padding-bottom: 10px;
695
+ border-bottom: 1px solid #1e1e2e;
696
+ }
697
+ .hist-btn {
698
+ background: none; border: 1px solid #2a2a3e; border-radius: 4px;
699
+ color: #546e7a; padding: 2px 7px; font-size: 12px; cursor: pointer;
700
+ transition: all .15s;
701
+ }
702
+ .hist-btn:not(:disabled):hover { border-color: #7986cb; color: #e0e0e0; }
703
+ .hist-btn:disabled { opacity: 0.3; cursor: default; }
704
+ #hist-label { font-size: 10px; color: #37474f; flex: 1; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
705
+
706
+ /* \u2500\u2500 Path result \u2500\u2500 */
707
+ .path-result { margin-top: 10px; }
708
+ .path-step {
709
+ display: flex; align-items: flex-start; gap: 6px;
710
+ margin-bottom: 2px; font-size: 11px;
711
+ }
712
+ .path-step-num { color: #37474f; min-width: 18px; text-align: right; flex-shrink: 0; }
713
+ .path-step-name { color: #c792ea; font-weight: 600; cursor: pointer; }
714
+ .path-step-name:hover { text-decoration: underline; }
715
+ .path-step-kind { color: #546e7a; }
716
+ .path-connector { color: #2a2a3e; margin-left: 22px; font-size: 10px; margin-bottom: 2px; }
717
+
718
+ /* \u2500\u2500 Path endpoint cards \u2500\u2500 */
719
+ .path-endpoints { margin-top: 14px; border-top: 1px solid #1e1e2e; padding-top: 12px; }
720
+ .path-endpoint-label { font-size: 9px; color: #37474f; text-transform: uppercase; letter-spacing: .1em; margin-bottom: 4px; }
721
+ .node-card {
722
+ background: #0d0d1a; border: 1px solid #1e1e2e; border-radius: 5px;
723
+ padding: 8px 10px; margin-bottom: 4px;
724
+ }
725
+ .node-card-name { font-size: 13px; font-weight: 600; color: #c792ea; margin-bottom: 4px; word-break: break-all; }
726
+
727
+ /* \u2500\u2500 Legend tab \u2500\u2500 */
728
+ .legend-section { margin-bottom: 14px; }
729
+ .legend-title { font-size: 10px; color: #546e7a; text-transform: uppercase; letter-spacing: .08em; margin-bottom: 7px; }
730
+ .legend-item {
731
+ display: flex; align-items: center; gap: 7px; margin-bottom: 3px;
732
+ font-size: 11px; color: #90a4ae; cursor: pointer; padding: 2px 4px;
733
+ border-radius: 4px; transition: background .1s;
734
+ }
735
+ .legend-item:hover { background: #1a1a2e; }
736
+ .legend-item.dimmed { opacity: .3; }
737
+ .legend-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
738
+ .legend-line { width: 18px; height: 2px; flex-shrink: 0; }
739
+ .legend-count{ margin-left: auto; color: #37474f; font-size: 10px; }
740
+
741
+ /* \u2500\u2500 Filters tab \u2500\u2500 */
742
+ .filter-section { margin-bottom: 18px; }
743
+ .filter-label { font-size: 10px; color: #546e7a; text-transform: uppercase; letter-spacing: .08em; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; }
744
+ .filter-value { color: #c792ea; font-size: 12px; }
745
+ input[type=range] {
746
+ width: 100%; accent-color: #7986cb;
747
+ background: transparent; cursor: pointer;
748
+ }
749
+ .filter-hint { font-size: 10px; color: #37474f; margin-top: 5px; }
750
+
751
+ /* \u2500\u2500 Scrollbar \u2500\u2500 */
752
+ ::-webkit-scrollbar { width: 4px; }
753
+ ::-webkit-scrollbar-track { background: transparent; }
754
+ ::-webkit-scrollbar-thumb { background: #2a2a3e; border-radius: 3px; }
755
+
756
+ /* \u2500\u2500 Tooltip \u2500\u2500 */
757
+ .vis-tooltip {
758
+ background: #12121a !important; border: 1px solid #2a2a3e !important;
759
+ color: #e0e0e0 !important; font-family: 'SF Mono','Fira Code',monospace !important;
760
+ font-size: 12px !important; border-radius: 6px !important; padding: 8px 10px !important;
761
+ }
762
+
763
+ /* \u2500\u2500 Minimap \u2500\u2500 */
764
+ #minimap {
765
+ position: absolute;
766
+ bottom: 14px;
767
+ left: 14px;
768
+ width: 180px;
769
+ height: 120px;
770
+ background: rgba(10,10,15,.85);
771
+ border: 1px solid #2a2a3e;
772
+ border-radius: 6px;
773
+ overflow: hidden;
774
+ z-index: 40;
775
+ cursor: crosshair;
776
+ }
777
+ #minimap-canvas { display: block; width: 180px; height: 120px; }
778
+
779
+ /* \u2500\u2500 Context menu \u2500\u2500 */
780
+ #ctx-menu {
781
+ display: none;
782
+ position: fixed;
783
+ z-index: 500;
784
+ background: #12121a;
785
+ border: 1px solid #2a2a3e;
786
+ border-radius: 6px;
787
+ padding: 4px 0;
788
+ min-width: 180px;
789
+ box-shadow: 0 6px 24px rgba(0,0,0,.7);
790
+ font-size: 12px;
791
+ }
792
+ .ctx-item {
793
+ padding: 6px 14px;
794
+ color: #90a4ae;
795
+ cursor: pointer;
796
+ white-space: nowrap;
797
+ transition: background .1s;
798
+ }
799
+ .ctx-item:hover { background: #1a1a2e; color: #e0e0e0; }
800
+ .ctx-sep { height: 1px; background: #1e1e2e; margin: 3px 0; }
801
+
802
+ /* \u2500\u2500 Charts modal \u2500\u2500 */
803
+ #charts-modal {
804
+ position: fixed;
805
+ inset: 0;
806
+ background: rgba(0, 0, 8, 0.88);
807
+ backdrop-filter: blur(8px);
808
+ z-index: 600;
809
+ display: flex;
810
+ align-items: center;
811
+ justify-content: center;
812
+ }
813
+ #charts-panel {
814
+ background: #0e0e1c;
815
+ border: 1px solid #3a3a5e;
816
+ border-radius: 14px;
817
+ width: 80vw;
818
+ max-height: 90vh;
819
+ display: flex;
820
+ flex-direction: column;
821
+ box-shadow: 0 24px 80px rgba(0,0,0,.95), 0 0 0 1px rgba(121,134,203,.12);
822
+ overflow: hidden;
823
+ }
824
+ #charts-header {
825
+ display: flex;
826
+ align-items: center;
827
+ justify-content: space-between;
828
+ padding: 14px 18px;
829
+ border-bottom: 1px solid #252540;
830
+ font-size: 14px;
831
+ font-weight: 700;
832
+ color: #e0d0ff;
833
+ letter-spacing: .03em;
834
+ flex-shrink: 0;
835
+ background: #111120;
836
+ }
837
+ #charts-close {
838
+ background: #1a1a30;
839
+ border: 1px solid #3a3a5e;
840
+ border-radius: 6px;
841
+ color: #9da8cc;
842
+ padding: 4px 10px;
843
+ font-size: 13px;
844
+ cursor: pointer;
845
+ font-family: inherit;
846
+ transition: all .15s;
847
+ }
848
+ #charts-close:hover { background: #252545; border-color: #c792ea; color: #ffffff; }
849
+ #charts-body {
850
+ overflow-y: auto;
851
+ padding: 18px;
852
+ display: grid;
853
+ grid-template-columns: repeat(3, 1fr);
854
+ gap: 18px;
855
+ }
856
+ .chart-block.span-1 { grid-column: span 1; }
857
+ .chart-block.span-2 { grid-column: span 2; }
858
+ .chart-block.span-3 { grid-column: 1 / -1; }
859
+ .chart-block {
860
+ background: #0a0a1c;
861
+ border: 1px solid #2e2e52;
862
+ border-radius: 10px;
863
+ padding: 16px;
864
+ }
865
+ .chart-title {
866
+ font-size: 13px;
867
+ font-weight: 700;
868
+ color: #e8eeff;
869
+ margin-bottom: 2px;
870
+ }
871
+ .chart-sub {
872
+ font-size: 10px;
873
+ color: #7a82aa;
874
+ margin-bottom: 6px;
875
+ }
876
+ .chart-insight {
877
+ font-size: 10px;
878
+ color: #9ba3c8;
879
+ background: rgba(121,134,203,0.08);
880
+ border-left: 2px solid #5b6abf;
881
+ padding: 5px 8px;
882
+ margin-bottom: 12px;
883
+ border-radius: 0 4px 4px 0;
884
+ line-height: 1.5;
885
+ }
886
+ .chart-block canvas {
887
+ display: block;
888
+ width: 100%;
889
+ height: auto;
890
+ border-radius: 6px;
891
+ }
892
+
893
+ /* \u2500\u2500 Heat legend (inside sidebar) \u2500\u2500 */
894
+ #heat-legend {
895
+ display: none;
896
+ background: rgba(8, 8, 18, 0.97);
897
+ border: 1px solid #3a3a5e;
898
+ border-radius: 12px;
899
+ padding: 12px 14px;
900
+ box-shadow: 0 4px 24px rgba(0,0,0,.75);
901
+ width: 200px;
902
+ }
903
+ .heat-legend-title {
904
+ font-size: 11px;
905
+ font-weight: 600;
906
+ color: #9da8cc;
907
+ margin-bottom: 10px;
908
+ }
909
+ #heat-gradient {
910
+ height: 6px;
911
+ border-radius: 3px;
912
+ background: linear-gradient(90deg, #e74c3c, #e67e22, #f1c40f, #27ae60, #2980b9);
913
+ margin-bottom: 10px;
914
+ }
915
+ .heat-stops {
916
+ display: flex;
917
+ flex-direction: column;
918
+ gap: 5px;
919
+ }
920
+ .heat-stop {
921
+ display: flex;
922
+ align-items: center;
923
+ gap: 8px;
924
+ font-size: 11px;
925
+ color: #7a86aa;
926
+ cursor: default;
927
+ padding: 2px 4px;
928
+ border-radius: 4px;
929
+ transition: background .1s, color .1s;
930
+ }
931
+ .heat-stop:hover { background: #16162a; color: #e0e0e0; }
932
+ .heat-stop-dot {
933
+ width: 9px; height: 9px;
934
+ border-radius: 50%;
935
+ flex-shrink: 0;
936
+ }
937
+ #heat-tip {
938
+ margin-top: 8px;
939
+ font-size: 10px;
940
+ color: #4a4a6a;
941
+ min-height: 14px;
942
+ font-style: italic;
943
+ }
944
+ `;
945
+ }
946
+ function buildJs(visNodes, visEdges, kindColors, edgeColors) {
947
+ return `// \u2500\u2500 Data \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
948
+ const NODES_DATA = ${JSON.stringify(visNodes)};
949
+ const EDGES_DATA = ${JSON.stringify(visEdges)};
950
+ const KIND_COLORS = ${JSON.stringify(kindColors)};
951
+ const EDGE_COLORS = ${JSON.stringify(edgeColors)};
952
+
953
+ const nodesDS = new vis.DataSet(NODES_DATA);
954
+ const edgesDS = new vis.DataSet(EDGES_DATA);
955
+
956
+ // \u2500\u2500 Precomputed lookups \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
957
+ const nodeById = {};
958
+ NODES_DATA.forEach(n => { nodeById[n.id] = n; });
959
+
960
+ // Adjacency map (undirected) for focus + path BFS
961
+ const adj = {};
962
+ EDGES_DATA.forEach(e => {
963
+ if (!adj[e.from]) adj[e.from] = new Set();
964
+ if (!adj[e.to]) adj[e.to] = new Set();
965
+ adj[e.from].add(e.to);
966
+ adj[e.to].add(e.from);
967
+ });
968
+
969
+ // Edge lookup for path edge highlighting: "fromId|toId" -> edge id
970
+ const edgeMap = {};
971
+ EDGES_DATA.forEach(e => {
972
+ edgeMap[e.from + '|' + e.to] = e.id;
973
+ edgeMap[e.to + '|' + e.from] = e.id;
974
+ });
975
+
976
+ // Original colors for path restore
977
+ const originalColors = {};
978
+ NODES_DATA.forEach(n => { originalColors[n.id] = n.color; });
979
+ const originalEdgeColors = {};
980
+ EDGES_DATA.forEach(e => { originalEdgeColors[e.id] = e.color; });
981
+
982
+ // \u2500\u2500 Filter state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
983
+ const hiddenNodeKinds = new Set();
984
+ const hiddenEdgeKinds = new Set();
985
+ let minDegree = 0;
986
+ let focusActive = false;
987
+ let focusSet = new Set();
988
+ let searchActive = false;
989
+ let searchIds = new Set();
990
+ let pathHighlightActive = false;
991
+
992
+ // \u2500\u2500 Path mode state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
993
+ let pathMode = false;
994
+ let pathStep = 0;
995
+ let pathFromId = null;
996
+ let pathToId = null;
997
+
998
+ // \u2500\u2500 History state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
999
+ const navHistory = [];
1000
+ let histIdx = -1;
1001
+
1002
+ // \u2500\u2500 vis.js network \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1003
+ const network = new vis.Network(
1004
+ document.getElementById('graph'),
1005
+ { nodes: nodesDS, edges: edgesDS },
1006
+ {
1007
+ physics: {
1008
+ enabled: true,
1009
+ solver: 'forceAtlas2Based',
1010
+ forceAtlas2Based: { gravitationalConstant: -80, centralGravity: 0.005, springLength: 120, springConstant: 0.08, damping: 0.6, avoidOverlap: 0.6 },
1011
+ stabilization: { iterations: 150, fit: true },
1012
+ },
1013
+ interaction: { hover: true, tooltipDelay: 150, hideEdgesOnDrag: true, multiselect: false },
1014
+ nodes: { shape: 'dot', scaling: { min: 6, max: 40 }, shadow: { enabled: true, color: 'rgba(0,0,0,.6)', size: 8, x: 2, y: 2 } },
1015
+ edges: { smooth: { type: 'continuous' }, selectionWidth: 2, hoverWidth: 1.5 },
1016
+ layout: { improvedLayout: false },
1017
+ }
1018
+ );
1019
+
1020
+ // \u2500\u2500 Init loader \u2014 driven by stabilization progress \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1021
+ const initLoader = document.getElementById('init-loader');
1022
+ const progressBar = document.getElementById('init-progress-bar');
1023
+ const initStatus = document.getElementById('init-status');
1024
+
1025
+ network.on('stabilizationProgress', params => {
1026
+ const pct = Math.round((params.iterations / params.total) * 100);
1027
+ progressBar.style.width = pct + '%';
1028
+ initStatus.textContent = 'Laying out graph\u2026 ' + pct + '%';
1029
+ });
1030
+
1031
+ network.on('stabilizationIterationsDone', () => {
1032
+ progressBar.style.width = '100%';
1033
+ initStatus.textContent = 'Done';
1034
+ // Disable physics AND future stabilization so DataSet updates never re-trigger
1035
+ // layout passes \u2014 which would cause nodes to jump/drag on the next click.
1036
+ network.setOptions({ physics: { enabled: false, stabilization: { enabled: false } } });
1037
+ physicsOn = false;
1038
+ document.getElementById('btn-physics').classList.remove('active');
1039
+ initLoader.classList.add('fade-out');
1040
+ setTimeout(() => initLoader.remove(), 420);
1041
+ });
1042
+
1043
+ // \u2500\u2500 Central filter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1044
+ function applyFilters(skipLoader) {
1045
+ withLoader(skipLoader, () => {
1046
+ const nodeUpdates = [];
1047
+ const edgeUpdates = [];
1048
+
1049
+ NODES_DATA.forEach(n => {
1050
+ let hidden = false;
1051
+ if (hiddenNodeKinds.has(n.kind)) hidden = true;
1052
+ else if (n.degree < minDegree) hidden = true;
1053
+ else if (focusActive && !focusSet.has(n.id)) hidden = true;
1054
+ else if (searchActive && !searchIds.has(n.id)) hidden = true;
1055
+ nodeUpdates.push({ id: n.id, hidden });
1056
+ });
1057
+
1058
+ EDGES_DATA.forEach(e => {
1059
+ const hidden = hiddenEdgeKinds.has(e.ekind);
1060
+ edgeUpdates.push({ id: e.id, hidden });
1061
+ });
1062
+
1063
+ nodesDS.update(nodeUpdates);
1064
+ edgesDS.update(edgeUpdates);
1065
+ });
1066
+ }
1067
+
1068
+ // \u2500\u2500 Loader helper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1069
+ function withLoader(skip, fn) {
1070
+ const loader = document.getElementById('loader');
1071
+ if (skip) { fn(); return; }
1072
+ loader.style.display = 'flex';
1073
+ requestAnimationFrame(() => requestAnimationFrame(() => { fn(); loader.style.display = 'none'; }));
1074
+ }
1075
+
1076
+ // \u2500\u2500 Panel tabs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1077
+ document.querySelectorAll('.tab').forEach(tab => {
1078
+ tab.addEventListener('click', () => {
1079
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
1080
+ tab.classList.add('active');
1081
+ const t = tab.dataset.tab;
1082
+ document.getElementById('tab-detail').style.display = t === 'detail' ? '' : 'none';
1083
+ document.getElementById('tab-legend').style.display = t === 'legend' ? '' : 'none';
1084
+ document.getElementById('tab-filters').style.display = t === 'filters' ? '' : 'none';
1085
+ });
1086
+ });
1087
+
1088
+ // \u2500\u2500 History \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1089
+ function pushHistory(nodeId) {
1090
+ navHistory.splice(histIdx + 1);
1091
+ navHistory.push(nodeId);
1092
+ if (navHistory.length > 50) navHistory.shift();
1093
+ histIdx = navHistory.length - 1;
1094
+ updateHistoryNav();
1095
+ }
1096
+
1097
+ function updateHistoryNav() {
1098
+ const n = nodeById[navHistory[histIdx]];
1099
+ const label = document.getElementById('hist-label');
1100
+ const back = document.getElementById('hist-back');
1101
+ const fwd = document.getElementById('hist-forward');
1102
+ if (!label) return; // elements may not exist (e.g. panel is in empty/path state)
1103
+ label.textContent = n ? n.label : 'no selection';
1104
+ back.disabled = histIdx <= 0;
1105
+ fwd.disabled = histIdx >= navHistory.length - 1;
1106
+ }
1107
+
1108
+ document.getElementById('hist-back').addEventListener('click', () => {
1109
+ if (histIdx > 0) { histIdx--; updateHistoryNav(); showDetail(navHistory[histIdx], false); network.selectNodes([navHistory[histIdx]]); }
1110
+ });
1111
+ document.getElementById('hist-forward').addEventListener('click', () => {
1112
+ if (histIdx < navHistory.length - 1) { histIdx++; updateHistoryNav(); showDetail(navHistory[histIdx], false); network.selectNodes([navHistory[histIdx]]); }
1113
+ });
1114
+
1115
+ // \u2500\u2500 Detail panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1116
+ function showDetail(nodeId, addToHistory) {
1117
+ if (addToHistory !== false) pushHistory(nodeId);
1118
+ const n = nodeById[nodeId];
1119
+ if (!n) return;
1120
+
1121
+ document.getElementById('tab-detail').innerHTML =
1122
+ '<div id="history-nav">' +
1123
+ '<button class="hist-btn" id="hist-back"' + (histIdx <= 0 ? ' disabled' : '') + '>\u2039</button>' +
1124
+ '<span id="hist-label" title="' + esc(n.label) + '">' + esc(n.label) + '</span>' +
1125
+ '<button class="hist-btn" id="hist-forward"' + (histIdx >= navHistory.length - 1 ? ' disabled' : '') + '>\u203A</button>' +
1126
+ '</div>' +
1127
+ '<div class="detail-name">' + esc(n.label) + '</div>' +
1128
+ '<span class="detail-kind">' + esc(n.kind) + '</span>' +
1129
+ '<div class="detail-row"><span class="detail-label">file</span><span class="detail-val">' + esc(n.filePath) + ':' + n.startLine + '</span></div>' +
1130
+ (n.qualifiedName !== n.label ? '<div class="detail-row"><span class="detail-label">qualified</span><span class="detail-val">' + esc(n.qualifiedName) + '</span></div>' : '') +
1131
+ '<div class="detail-row"><span class="detail-label">degree</span><span class="detail-val">' + n.degree + ' connections</span></div>' +
1132
+ '<div class="detail-row"><span class="detail-label">exported</span><span class="detail-val">' + (n.isExported ? '\u2713' : '\u2014') + '</span></div>' +
1133
+ (n.signature ? '<div class="detail-sig">' + esc(n.signature) + '</div>' : '') +
1134
+ '<div class="detail-actions">' +
1135
+ '<button class="action-btn" data-action="copy" data-ref="' + esc(n.filePath + ':' + n.startLine) + '">\u2398 Copy ref</button>' +
1136
+ '<button class="action-btn" data-action="path-from" data-nodeid="' + esc(nodeId) + '">\u27F6 Path from here</button>' +
1137
+ '</div>';
1138
+
1139
+ // Re-attach history buttons
1140
+ document.getElementById('hist-back').addEventListener('click', () => {
1141
+ if (histIdx > 0) { histIdx--; updateHistoryNav(); showDetail(navHistory[histIdx], false); network.selectNodes([navHistory[histIdx]]); }
1142
+ });
1143
+ document.getElementById('hist-forward').addEventListener('click', () => {
1144
+ if (histIdx < navHistory.length - 1) { histIdx++; updateHistoryNav(); showDetail(navHistory[histIdx], false); network.selectNodes([navHistory[histIdx]]); }
1145
+ });
1146
+ }
1147
+
1148
+ function copyToClipboard(text) {
1149
+ navigator.clipboard.writeText(text).catch(() => {});
1150
+ }
1151
+
1152
+ // \u2500\u2500 Panel event delegation (replaces all inline onclick) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1153
+ document.getElementById('panel-content').addEventListener('click', function(e) {
1154
+ var el = e.target.closest('[data-action]');
1155
+ if (!el) return;
1156
+ var action = el.dataset.action;
1157
+ if (action === 'copy') { copyToClipboard(el.dataset.ref); }
1158
+ if (action === 'path-from') { setPathFrom(el.dataset.nodeid); }
1159
+ if (action === 'show-detail') { showDetail(el.dataset.nodeid, true); network.selectNodes([el.dataset.nodeid]); }
1160
+ if (action === 'reset') { resetToEmpty(); }
1161
+ });
1162
+
1163
+ // \u2500\u2500 Network click handler \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1164
+ let lastSelectedId = null;
1165
+
1166
+ network.on('click', params => {
1167
+ if (params.nodes.length === 0) {
1168
+ if (pathMode) exitPath(true);
1169
+ lastSelectedId = null;
1170
+ return;
1171
+ }
1172
+
1173
+ const nodeId = params.nodes[0];
1174
+
1175
+ if (pathMode) {
1176
+ handlePathClick(nodeId);
1177
+ return;
1178
+ }
1179
+
1180
+ if (lastSelectedId && lastSelectedId !== nodeId) {
1181
+ runPathBFS(lastSelectedId, nodeId);
1182
+ lastSelectedId = null;
1183
+ return;
1184
+ }
1185
+
1186
+ lastSelectedId = nodeId;
1187
+ showDetail(nodeId, true);
1188
+ network.focus(nodeId, { scale: 1.5, animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
1189
+
1190
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
1191
+ document.querySelector('[data-tab="detail"]').classList.add('active');
1192
+ document.getElementById('tab-detail').style.display = '';
1193
+ document.getElementById('tab-legend').style.display = 'none';
1194
+ document.getElementById('tab-filters').style.display = 'none';
1195
+ });
1196
+
1197
+ network.on('doubleClick', params => {
1198
+ if (focusActive && params.nodes.length === 0) exitFocus();
1199
+ });
1200
+
1201
+ // \u2500\u2500 Focus mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1202
+ document.getElementById('btn-focus').addEventListener('click', () => {
1203
+ if (focusActive) { exitFocus(); return; }
1204
+ const selected = network.getSelectedNodes();
1205
+ if (selected.length === 0) {
1206
+ document.getElementById('btn-focus').classList.add('warn');
1207
+ setTimeout(() => document.getElementById('btn-focus').classList.remove('warn'), 800);
1208
+ return;
1209
+ }
1210
+ enterFocus(selected[0]);
1211
+ });
1212
+
1213
+ function enterFocus(nodeId) {
1214
+ exitPath(false);
1215
+ focusActive = true;
1216
+ focusSet = new Set([nodeId, ...(adj[nodeId] ?? [])]);
1217
+ document.getElementById('btn-focus').classList.add('active');
1218
+ applyFilters();
1219
+ }
1220
+
1221
+ function exitFocus() {
1222
+ focusActive = false;
1223
+ focusSet = new Set();
1224
+ document.getElementById('btn-focus').classList.remove('active');
1225
+ applyFilters();
1226
+ }
1227
+
1228
+ // \u2500\u2500 Path mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1229
+ document.getElementById('btn-path').addEventListener('click', () => {
1230
+ if (pathMode) { exitPath(true); } else { enterPath(); }
1231
+ });
1232
+
1233
+ function enterPath() {
1234
+ exitFocus();
1235
+ pathMode = true;
1236
+ pathStep = 1;
1237
+ pathFromId = null;
1238
+ pathToId = null;
1239
+ clearPathHighlight();
1240
+ document.getElementById('btn-path').classList.add('active');
1241
+ document.getElementById('path-bar').style.display = 'block';
1242
+ document.getElementById('path-bar').textContent = 'Click a node to set start\u2026';
1243
+ }
1244
+
1245
+ function exitPath(resetHighlight) {
1246
+ pathMode = false;
1247
+ pathStep = 0;
1248
+ document.getElementById('btn-path').classList.remove('active');
1249
+ document.getElementById('path-bar').style.display = 'none';
1250
+ if (resetHighlight) clearPathHighlight();
1251
+ }
1252
+
1253
+ function setPathFrom(nodeId) {
1254
+ enterPath();
1255
+ handlePathClick(nodeId);
1256
+ }
1257
+
1258
+ function handlePathClick(nodeId) {
1259
+ if (pathStep === 1) {
1260
+ pathFromId = nodeId;
1261
+ pathStep = 2;
1262
+ document.getElementById('path-bar').textContent = 'From: ' + (nodeById[nodeId] && nodeById[nodeId].label || nodeId) + ' \u2014 now click destination\u2026';
1263
+ network.selectNodes([nodeId]);
1264
+ } else if (pathStep === 2) {
1265
+ pathToId = nodeId;
1266
+ exitPath(false);
1267
+ runPathBFS(pathFromId, pathToId);
1268
+ }
1269
+ }
1270
+
1271
+ function runPathBFS(fromId, toId) {
1272
+ if (fromId === toId) return;
1273
+ const prev = new Map();
1274
+ const queue = [fromId];
1275
+ const visited = new Set([fromId]);
1276
+
1277
+ outer: while (queue.length > 0) {
1278
+ const cur = queue.shift();
1279
+ const neighbors = adj[cur] || new Set();
1280
+ for (const nb of neighbors) {
1281
+ if (!visited.has(nb)) {
1282
+ visited.add(nb);
1283
+ prev.set(nb, cur);
1284
+ if (nb === toId) break outer;
1285
+ queue.push(nb);
1286
+ }
1287
+ }
1288
+ }
1289
+
1290
+ if (!prev.has(toId)) {
1291
+ showPathResult([], fromId, toId);
1292
+ return;
1293
+ }
1294
+
1295
+ const pathIds = [];
1296
+ let cur = toId;
1297
+ while (cur !== undefined) { pathIds.unshift(cur); cur = prev.get(cur); }
1298
+
1299
+ highlightPath(pathIds);
1300
+ showPathResult(pathIds, fromId, toId);
1301
+ }
1302
+
1303
+ function highlightPath(pathIds) {
1304
+ clearPathHighlight();
1305
+ pathHighlightActive = true;
1306
+
1307
+ const pathSet = new Set(pathIds);
1308
+ const DIM = { background: '#141420', border: '#1a1a2e', highlight: { background: '#1a1a2e', border: '#2a2a3e' } };
1309
+ const GOLD = { background: '#f39c12', border: '#e67e22', highlight: { background: '#ffd700', border: '#f39c12' } };
1310
+
1311
+ const nodeUpdates = NODES_DATA.map(n => ({ id: n.id, color: pathSet.has(n.id) ? GOLD : DIM }));
1312
+ nodesDS.update(nodeUpdates);
1313
+
1314
+ const pathEdgeIds = new Set();
1315
+ for (let i = 0; i < pathIds.length - 1; i++) {
1316
+ const eid = edgeMap[pathIds[i] + '|' + pathIds[i + 1]];
1317
+ if (eid !== undefined) pathEdgeIds.add(eid);
1318
+ }
1319
+
1320
+ const edgeUpdates = EDGES_DATA.map(e => ({
1321
+ id: e.id,
1322
+ color: pathEdgeIds.has(e.id) ? { color: '#f39c12', opacity: 1 } : { color: '#1a1a2e', opacity: 0.15 },
1323
+ width: pathEdgeIds.has(e.id) ? 3 : 1,
1324
+ }));
1325
+ edgesDS.update(edgeUpdates);
1326
+
1327
+ network.selectNodes(pathIds);
1328
+ network.fit({ nodes: pathIds, animation: { duration: 500, easingFunction: 'easeInOutQuad' } });
1329
+ }
1330
+
1331
+ function clearPathHighlight() {
1332
+ if (!pathHighlightActive) return;
1333
+ pathHighlightActive = false;
1334
+ nodesDS.update(NODES_DATA.map(n => ({ id: n.id, color: originalColors[n.id], width: n.borderWidth })));
1335
+ edgesDS.update(EDGES_DATA.map(e => ({ id: e.id, color: originalEdgeColors[e.id], width: e.width })));
1336
+ }
1337
+
1338
+ function nodeCard(n) {
1339
+ if (!n) return '';
1340
+ return '<div class="node-card">' +
1341
+ '<div class="node-card-name">' + esc(n.label) + '</div>' +
1342
+ '<span class="detail-kind">' + esc(n.kind) + '</span>' +
1343
+ '<div class="detail-row" style="margin-top:6px"><span class="detail-label">file</span><span class="detail-val">' + esc(n.filePath) + ':' + n.startLine + '</span></div>' +
1344
+ '<div class="detail-row"><span class="detail-label">degree</span><span class="detail-val">' + n.degree + ' connections</span></div>' +
1345
+ (n.signature ? '<div class="detail-sig" style="margin-top:6px">' + esc(n.signature) + '</div>' : '') +
1346
+ '</div>';
1347
+ }
1348
+
1349
+ function showPathResult(pathIds, fromId, toId) {
1350
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
1351
+ document.querySelector('[data-tab="detail"]').classList.add('active');
1352
+ document.getElementById('tab-detail').style.display = '';
1353
+ document.getElementById('tab-legend').style.display = 'none';
1354
+ document.getElementById('tab-filters').style.display = 'none';
1355
+
1356
+ const from = nodeById[fromId];
1357
+ const to = nodeById[toId];
1358
+
1359
+ if (pathIds.length === 0) {
1360
+ document.getElementById('tab-detail').innerHTML =
1361
+ '<div id="history-nav">' +
1362
+ '<button class="hist-btn" id="hist-back" disabled>\u2039</button>' +
1363
+ '<span id="hist-label">path result</span>' +
1364
+ '<button class="hist-btn" id="hist-forward" disabled>\u203A</button>' +
1365
+ '</div>' +
1366
+ '<p style="color:#e67e22;font-size:12px;margin-top:8px">No path found between <b>' + esc(from && from.label) + '</b> and <b>' + esc(to && to.label) + '</b>.</p>' +
1367
+ nodeCard(from) +
1368
+ nodeCard(to) +
1369
+ '<button class="action-btn" style="margin-top:10px" data-action="reset">\u2715 Clear</button>';
1370
+ return;
1371
+ }
1372
+
1373
+ const steps = pathIds.map((id, i) => {
1374
+ const n = nodeById[id];
1375
+ return (i > 0 ? '<div class="path-connector">\u2502</div>' : '') +
1376
+ '<div class="path-step">' +
1377
+ '<span class="path-step-num">' + (i + 1) + '.</span>' +
1378
+ '<span>' +
1379
+ '<span class="path-step-name" data-action="show-detail" data-nodeid="' + esc(id) + '">' + esc(n && n.label || id) + '</span>' +
1380
+ '<span class="path-step-kind"> ' + esc(n && n.kind || '') + '</span>' +
1381
+ '</span>' +
1382
+ '</div>';
1383
+ }).join('');
1384
+
1385
+ document.getElementById('tab-detail').innerHTML =
1386
+ '<div id="history-nav">' +
1387
+ '<button class="hist-btn" id="hist-back" disabled>\u2039</button>' +
1388
+ '<span id="hist-label">path: ' + pathIds.length + ' hops</span>' +
1389
+ '<button class="hist-btn" id="hist-forward" disabled>\u203A</button>' +
1390
+ '</div>' +
1391
+ '<div style="font-size:11px;color:#546e7a;margin-bottom:12px">' + esc(from && from.label) + ' \u2192 ' + esc(to && to.label) + '</div>' +
1392
+ '<div class="path-result">' + steps + '</div>' +
1393
+ '<div class="path-endpoints">' +
1394
+ '<div class="path-endpoint-label">From</div>' + nodeCard(from) +
1395
+ '<div class="path-endpoint-label" style="margin-top:10px">To</div>' + nodeCard(to) +
1396
+ '</div>' +
1397
+ '<button class="action-btn" style="margin-top:12px" data-action="reset">\u2715 Clear</button>';
1398
+ }
1399
+
1400
+ // \u2500\u2500 Node kind filter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1401
+ document.querySelectorAll('.legend-item[data-nkind]').forEach(item => {
1402
+ item.addEventListener('click', () => {
1403
+ const kind = item.dataset.nkind;
1404
+ hiddenNodeKinds.has(kind) ? hiddenNodeKinds.delete(kind) : hiddenNodeKinds.add(kind);
1405
+ item.classList.toggle('dimmed', hiddenNodeKinds.has(kind));
1406
+ applyFilters();
1407
+ });
1408
+ });
1409
+
1410
+ // \u2500\u2500 Edge kind filter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1411
+ document.querySelectorAll('.legend-item[data-ekind]').forEach(item => {
1412
+ item.addEventListener('click', () => {
1413
+ const kind = item.dataset.ekind;
1414
+ hiddenEdgeKinds.has(kind) ? hiddenEdgeKinds.delete(kind) : hiddenEdgeKinds.add(kind);
1415
+ item.classList.toggle('dimmed', hiddenEdgeKinds.has(kind));
1416
+ applyFilters();
1417
+ });
1418
+ });
1419
+
1420
+ // \u2500\u2500 Degree slider \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1421
+ document.getElementById('degree-slider').addEventListener('input', e => {
1422
+ minDegree = parseInt(e.target.value, 10);
1423
+ document.getElementById('degree-val').textContent = String(minDegree);
1424
+ applyFilters();
1425
+ });
1426
+
1427
+ // \u2500\u2500 Search \u2014 Feature 1: glow instead of hide \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1428
+ let searchTimer = null;
1429
+ document.getElementById('search').addEventListener('input', e => {
1430
+ clearTimeout(searchTimer);
1431
+ const q = e.target.value.trim().toLowerCase();
1432
+ searchTimer = setTimeout(() => {
1433
+ withLoader(false, () => {
1434
+ if (!q) {
1435
+ searchActive = false;
1436
+ searchIds = new Set();
1437
+ // Restore original colors
1438
+ nodesDS.update(NODES_DATA.map(n => ({
1439
+ id: n.id, color: originalColors[n.id], size: n.size, borderWidth: n.borderWidth,
1440
+ })));
1441
+ } else {
1442
+ const matches = NODES_DATA.filter(n =>
1443
+ n.label.toLowerCase().includes(q) ||
1444
+ n.qualifiedName.toLowerCase().includes(q) ||
1445
+ n.filePath.toLowerCase().includes(q)
1446
+ );
1447
+ searchActive = true;
1448
+ searchIds = new Set(matches.map(n => n.id));
1449
+ // Apply glow to matching, dim non-matching \u2014 no hiding
1450
+ const DIM_COLOR = { background: '#111118', border: '#1a1a2a', highlight: { background: '#111118', border: '#1a1a2a' }, hover: { background: '#111118', border: '#1a1a2a' } };
1451
+ const GLOW_BORDER = '#ffffff';
1452
+ nodesDS.update(NODES_DATA.map(n => {
1453
+ if (searchIds.has(n.id)) {
1454
+ return { id: n.id, color: { ...originalColors[n.id], border: GLOW_BORDER }, size: n.size * 1.4, borderWidth: 3 };
1455
+ } else {
1456
+ return { id: n.id, color: DIM_COLOR, size: n.size, borderWidth: n.borderWidth };
1457
+ }
1458
+ }));
1459
+ }
1460
+ // Still apply other filters (kind/degree) for hidden state
1461
+ applyFilters(true);
1462
+ if (searchActive && searchIds.size > 0) {
1463
+ network.fit({ nodes: [...searchIds], animation: { duration: 400, easingFunction: 'easeInOutQuad' } });
1464
+ } else if (!searchActive) {
1465
+ network.fit({ animation: { duration: 400, easingFunction: 'easeInOutQuad' } });
1466
+ }
1467
+ });
1468
+ }, 180);
1469
+ });
1470
+
1471
+ // \u2500\u2500 Toolbar buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1472
+ document.getElementById('btn-fit').addEventListener('click', () =>
1473
+ network.fit({ animation: { duration: 600, easingFunction: 'easeInOutQuad' } })
1474
+ );
1475
+
1476
+ let physicsOn = true;
1477
+ document.getElementById('btn-physics').addEventListener('click', () => {
1478
+ physicsOn = !physicsOn;
1479
+ network.setOptions({ physics: { enabled: physicsOn } });
1480
+ document.getElementById('btn-physics').classList.toggle('active', physicsOn);
1481
+ document.getElementById('physics-speed-row').style.display = physicsOn ? 'flex' : 'none';
1482
+ });
1483
+
1484
+ document.getElementById('physics-speed').addEventListener('input', function() {
1485
+ const v = parseInt(this.value); // 1..10
1486
+ // Damping: high value = slow (lots of friction), low value = fast (little friction)
1487
+ // Speed 1 \u2192 damping 0.9, Speed 10 \u2192 damping 0.05
1488
+ const damping = 0.9 - (v - 1) * (0.85 / 9);
1489
+ network.setOptions({ physics: { forceAtlas2Based: { damping: Math.round(damping * 100) / 100 } } });
1490
+ });
1491
+
1492
+
1493
+ document.getElementById('btn-png').addEventListener('click', () => {
1494
+ const canvas = document.querySelector('#graph canvas');
1495
+ if (!canvas) return;
1496
+ const tmp = document.createElement('canvas');
1497
+ tmp.width = canvas.width;
1498
+ tmp.height = canvas.height;
1499
+ const ctx = tmp.getContext('2d');
1500
+ ctx.fillStyle = '#0a0a0f';
1501
+ ctx.fillRect(0, 0, tmp.width, tmp.height);
1502
+ ctx.drawImage(canvas, 0, 0);
1503
+ const a = document.createElement('a');
1504
+ a.href = tmp.toDataURL('image/png');
1505
+ a.download = 'kirograph-' + Date.now() + '.png';
1506
+ a.click();
1507
+ });
1508
+
1509
+ // \u2500\u2500 Feature 2: Cluster by directory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1510
+ let clusterActive = false;
1511
+ const clusterIds = [];
1512
+ let savedPositions = {};
1513
+
1514
+ function setClusterButtonsVisible(visible) {
1515
+ ['btn-focus', 'btn-path', 'btn-heat'].forEach(id => {
1516
+ document.getElementById(id).style.display = visible ? '' : 'none';
1517
+ });
1518
+ }
1519
+
1520
+ document.getElementById('btn-cluster').addEventListener('click', () => {
1521
+ if (clusterActive) {
1522
+ // \u2500\u2500 Uncluster \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1523
+ clusterIds.length = 0;
1524
+ clusterActive = false;
1525
+ document.getElementById('btn-cluster').classList.remove('active');
1526
+
1527
+ // Restore nodes at saved positions with physics off so they snap back exactly
1528
+ network.setOptions({ physics: { enabled: false } });
1529
+ nodesDS.clear();
1530
+ edgesDS.clear();
1531
+ nodesDS.add(NODES_DATA.map(n => ({
1532
+ ...n,
1533
+ hidden: false,
1534
+ color: originalColors[n.id],
1535
+ x: savedPositions[n.id] ? savedPositions[n.id].x : undefined,
1536
+ y: savedPositions[n.id] ? savedPositions[n.id].y : undefined,
1537
+ })));
1538
+ edgesDS.add(EDGES_DATA.map(e => ({ ...e, hidden: false, color: originalEdgeColors[e.id] })));
1539
+ network.fit({ animation: { duration: 400, easingFunction: 'easeInOutQuad' } });
1540
+
1541
+ // Restore hidden buttons
1542
+ setClusterButtonsVisible(true);
1543
+
1544
+ } else {
1545
+ // \u2500\u2500 Cluster \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1546
+
1547
+ // Deactivate incompatible modes before clustering
1548
+ if (heatActive) {
1549
+ heatActive = false;
1550
+ document.getElementById('btn-heat').classList.remove('active');
1551
+ document.getElementById('heat-legend').style.display = 'none';
1552
+ nodesDS.update(NODES_DATA.map(n => ({ id: n.id, color: originalColors[n.id] })));
1553
+ }
1554
+ if (focusActive) exitFocus();
1555
+ if (pathMode) exitPath(true);
1556
+ network.unselectAll();
1557
+
1558
+ // Snapshot positions before clustering
1559
+ savedPositions = network.getPositions(NODES_DATA.map(n => n.id));
1560
+
1561
+ // Hide incompatible buttons
1562
+ setClusterButtonsVisible(false);
1563
+
1564
+ const dirs = [...new Set(NODES_DATA.map(n => n.dir).filter(Boolean))];
1565
+ dirs.forEach(dir => {
1566
+ const dirNodes = NODES_DATA.filter(n => n.dir === dir);
1567
+ if (dirNodes.length < 2) return;
1568
+ const nodeIds = new Set(dirNodes.map(n => n.id));
1569
+ const cid = 'cluster:' + dir;
1570
+ network.cluster({
1571
+ clusterNodeProperties: {
1572
+ id: cid,
1573
+ label: '\\uD83D\\uDCC1 ' + dir + ' (' + dirNodes.length + ')',
1574
+ shape: 'box',
1575
+ color: { background: '#1e1e3a', border: '#7986cb', highlight: { background: '#2a2a4e', border: '#c792ea' } },
1576
+ font: { size: 13, color: '#c792ea', face: 'monospace' },
1577
+ borderWidth: 2,
1578
+ size: 20,
1579
+ },
1580
+ clusterEdgeProperties: {
1581
+ color: { color: '#ffffff', opacity: 0.7 },
1582
+ dashes: true,
1583
+ width: 2,
1584
+ smooth: { type: 'continuous' },
1585
+ },
1586
+ joinCondition: function(nodeOptions) { return nodeIds.has(nodeOptions.id); },
1587
+ });
1588
+ clusterIds.push(cid);
1589
+ });
1590
+ clusterActive = true;
1591
+ document.getElementById('btn-cluster').classList.add('active');
1592
+ }
1593
+ });
1594
+
1595
+ // \u2500\u2500 Feature 3: Minimap \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1596
+ function drawMinimap() {
1597
+ const canvas = document.getElementById('minimap-canvas');
1598
+ const ctx = canvas.getContext('2d');
1599
+ const W = 180, H = 120;
1600
+ ctx.clearRect(0, 0, W, H);
1601
+ ctx.fillStyle = 'rgba(10,10,15,0.9)';
1602
+ ctx.fillRect(0, 0, W, H);
1603
+
1604
+ const positions = network.getPositions();
1605
+ const ids = Object.keys(positions);
1606
+ if (ids.length === 0) return;
1607
+
1608
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
1609
+ ids.forEach(id => {
1610
+ minX = Math.min(minX, positions[id].x); maxX = Math.max(maxX, positions[id].x);
1611
+ minY = Math.min(minY, positions[id].y); maxY = Math.max(maxY, positions[id].y);
1612
+ });
1613
+ const rangeX = maxX - minX || 1, rangeY = maxY - minY || 1;
1614
+ const pad = 10;
1615
+ const scaleX = (W - pad * 2) / rangeX, scaleY = (H - pad * 2) / rangeY;
1616
+ const scale = Math.min(scaleX, scaleY);
1617
+
1618
+ function toCanvas(x, y) {
1619
+ return {
1620
+ cx: pad + (x - minX) * scale,
1621
+ cy: pad + (y - minY) * scale,
1622
+ };
1623
+ }
1624
+
1625
+ // Draw edges as thin lines
1626
+ ctx.strokeStyle = 'rgba(121,134,203,0.2)';
1627
+ ctx.lineWidth = 0.5;
1628
+ EDGES_DATA.forEach(e => {
1629
+ const fp = positions[e.from], tp = positions[e.to];
1630
+ if (!fp || !tp) return;
1631
+ const f = toCanvas(fp.x, fp.y), t = toCanvas(tp.x, tp.y);
1632
+ ctx.beginPath(); ctx.moveTo(f.cx, f.cy); ctx.lineTo(t.cx, t.cy); ctx.stroke();
1633
+ });
1634
+
1635
+ // Draw nodes as dots
1636
+ ids.forEach(id => {
1637
+ const p = positions[id];
1638
+ const c = toCanvas(p.x, p.y);
1639
+ const n = nodeById[id];
1640
+ ctx.beginPath();
1641
+ ctx.arc(c.cx, c.cy, 2, 0, Math.PI * 2);
1642
+ ctx.fillStyle = (n && n.color && n.color.background) ? n.color.background : '#546e7a';
1643
+ ctx.fill();
1644
+ });
1645
+
1646
+ // Draw viewport rectangle
1647
+ const vp = network.getViewPosition();
1648
+ const vs = network.getScale();
1649
+ const graphEl = document.getElementById('graph');
1650
+ const vpW = graphEl.clientWidth / vs, vpH = graphEl.clientHeight / vs;
1651
+ const vpMinX = vp.x - vpW / 2, vpMinY = vp.y - vpH / 2;
1652
+ const tl = toCanvas(vpMinX, vpMinY);
1653
+ const br = toCanvas(vpMinX + vpW, vpMinY + vpH);
1654
+ ctx.strokeStyle = 'rgba(199,146,234,0.7)';
1655
+ ctx.lineWidth = 1;
1656
+ ctx.strokeRect(tl.cx, tl.cy, br.cx - tl.cx, br.cy - tl.cy);
1657
+ }
1658
+
1659
+ network.on('afterDrawing', drawMinimap);
1660
+
1661
+ document.getElementById('minimap').addEventListener('click', function(e) {
1662
+ const rect = this.getBoundingClientRect();
1663
+ const W = 180, H = 120;
1664
+ const mx = e.clientX - rect.left, my = e.clientY - rect.top;
1665
+ const positions = network.getPositions();
1666
+ const ids = Object.keys(positions);
1667
+ if (ids.length === 0) return;
1668
+ let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
1669
+ ids.forEach(id => {
1670
+ minX = Math.min(minX, positions[id].x); maxX = Math.max(maxX, positions[id].x);
1671
+ minY = Math.min(minY, positions[id].y); maxY = Math.max(maxY, positions[id].y);
1672
+ });
1673
+ const pad = 10;
1674
+ const rangeX = maxX - minX || 1, rangeY = maxY - minY || 1;
1675
+ const scaleX = (W - pad * 2) / rangeX, scaleY = (H - pad * 2) / rangeY;
1676
+ const scale = Math.min(scaleX, scaleY);
1677
+ const gx = (mx - pad) / scale + minX;
1678
+ const gy = (my - pad) / scale + minY;
1679
+ network.moveTo({ position: { x: gx, y: gy }, animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
1680
+ });
1681
+
1682
+ // \u2500\u2500 Feature 4: Right-click context menu \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1683
+ const ctxMenu = document.getElementById('ctx-menu');
1684
+ let ctxNodeId = null;
1685
+
1686
+ network.on('oncontext', function(params) {
1687
+ params.event.preventDefault();
1688
+ const nodeId = network.getNodeAt(params.pointer.DOM);
1689
+ ctxNodeId = nodeId || null;
1690
+ if (!nodeId) { ctxMenu.style.display = 'none'; return; }
1691
+ ctxMenu.innerHTML =
1692
+ '<div class="ctx-item" data-ctx="focus-neighbors" data-nodeid="' + nodeId + '">\u25CE Focus neighbors</div>' +
1693
+ '<div class="ctx-item" data-ctx="path-from" data-nodeid="' + nodeId + '">\u27F6 Path from here</div>' +
1694
+ '<div class="ctx-sep"></div>' +
1695
+ '<div class="ctx-item" data-ctx="copy-id" data-nodeid="' + nodeId + '">\u2398 Copy ID</div>' +
1696
+ '<div class="ctx-item" data-ctx="copy-path" data-nodeid="' + nodeId + '">\u2398 Copy file path</div>' +
1697
+ '<div class="ctx-sep"></div>' +
1698
+ '<div class="ctx-item" data-ctx="highlight-kind" data-nodeid="' + nodeId + '">\u25C8 Highlight same kind</div>';
1699
+ ctxMenu.style.display = 'block';
1700
+ ctxMenu.style.left = params.event.clientX + 'px';
1701
+ ctxMenu.style.top = params.event.clientY + 'px';
1702
+ });
1703
+
1704
+ document.addEventListener('click', () => { ctxMenu.style.display = 'none'; });
1705
+
1706
+ ctxMenu.addEventListener('click', function(e) {
1707
+ const item = e.target.closest('[data-ctx]');
1708
+ if (!item) return;
1709
+ const action = item.dataset.ctx;
1710
+ const nid = item.dataset.nodeid;
1711
+ ctxMenu.style.display = 'none';
1712
+ if (action === 'focus-neighbors') {
1713
+ enterFocus(nid);
1714
+ } else if (action === 'path-from') {
1715
+ setPathFrom(nid);
1716
+ } else if (action === 'copy-id') {
1717
+ copyToClipboard(nid);
1718
+ } else if (action === 'copy-path') {
1719
+ const n = nodeById[nid];
1720
+ copyToClipboard(n ? n.filePath : nid);
1721
+ } else if (action === 'highlight-kind') {
1722
+ const n = nodeById[nid];
1723
+ if (!n) return;
1724
+ const kind = n.kind;
1725
+ const DIM = { background: '#111118', border: '#1a1a2a', highlight: { background: '#111118', border: '#1a1a2a' }, hover: { background: '#111118', border: '#1a1a2a' } };
1726
+ pathHighlightActive = true;
1727
+ nodesDS.update(NODES_DATA.map(nd => ({
1728
+ id: nd.id,
1729
+ color: nd.kind === kind ? originalColors[nd.id] : DIM,
1730
+ })));
1731
+ }
1732
+ });
1733
+
1734
+ // \u2500\u2500 Feature 5: Heat map overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1735
+ let heatActive = false;
1736
+
1737
+ function hslToHex(h, s, l) {
1738
+ s /= 100; l /= 100;
1739
+ const a = s * Math.min(l, 1 - l);
1740
+ const f = n => { const k = (n + h / 30) % 12; const c = l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); return Math.round(255 * c).toString(16).padStart(2, '0'); };
1741
+ return '#' + f(0) + f(8) + f(4);
1742
+ }
1743
+
1744
+ document.getElementById('btn-heat').addEventListener('click', () => {
1745
+ heatActive = !heatActive;
1746
+ document.getElementById('btn-heat').classList.toggle('active', heatActive);
1747
+ document.getElementById('heat-legend').style.display = heatActive ? 'block' : 'none';
1748
+ if (heatActive) {
1749
+ const times = NODES_DATA.map(n => n.lastModified).filter(t => t > 0);
1750
+ const minT = Math.min(...times), maxT = Math.max(...times);
1751
+ const range = maxT - minT || 1;
1752
+ nodesDS.update(NODES_DATA.map(n => {
1753
+ const t = n.lastModified || minT;
1754
+ const ratio = (t - minT) / range; // 1 = newest, 0 = oldest
1755
+ // newest = warm red (H=0), oldest = cool blue (H=210)
1756
+ const h = Math.round((1 - ratio) * 210);
1757
+ const bg = hslToHex(h, 70, 45);
1758
+ return { id: n.id, color: { background: bg, border: hslToHex(h, 70, 60), highlight: { background: bg, border: '#fff' }, hover: { background: hslToHex(h, 70, 55), border: '#fff' } } };
1759
+ }));
1760
+ } else {
1761
+ nodesDS.update(NODES_DATA.map(n => ({ id: n.id, color: originalColors[n.id] })));
1762
+ }
1763
+ });
1764
+
1765
+ // Heat stop hover tooltips
1766
+ document.querySelectorAll('.heat-stop').forEach(function(el) {
1767
+ var tip = document.getElementById('heat-tip');
1768
+ el.addEventListener('mouseenter', function() { if (tip) tip.textContent = el.dataset.tip || ''; });
1769
+ el.addEventListener('mouseleave', function() { if (tip) tip.textContent = ''; });
1770
+ });
1771
+
1772
+ // \u2500\u2500 Keyboard shortcuts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1773
+ document.addEventListener('keydown', e => {
1774
+ if (e.key === 'Escape') {
1775
+ if (pathMode) { exitPath(true); }
1776
+ if (focusActive) { exitFocus(); }
1777
+ }
1778
+ if (e.key === 'f' && !e.metaKey && !e.ctrlKey && e.target.tagName !== 'INPUT') {
1779
+ network.fit({ animation: { duration: 400, easingFunction: 'easeInOutQuad' } });
1780
+ }
1781
+ });
1782
+
1783
+ // \u2500\u2500 Reset to initial state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1784
+ function resetToEmpty() {
1785
+ // 1. Restore every node and edge to its original color and make all visible
1786
+ pathHighlightActive = false;
1787
+ nodesDS.update(NODES_DATA.map(n => ({ id: n.id, hidden: false, color: originalColors[n.id] })));
1788
+ edgesDS.update(EDGES_DATA.map(e => ({ id: e.id, hidden: false, color: originalEdgeColors[e.id] })));
1789
+
1790
+ // 2. Clear network selection and zoom back to full graph
1791
+ network.unselectAll();
1792
+ network.fit({ animation: { duration: 300, easingFunction: 'easeInOutQuad' } });
1793
+
1794
+ // 3. Reset all state variables
1795
+ lastSelectedId = null;
1796
+ pathMode = false;
1797
+ pathStep = 0;
1798
+ pathFromId = null;
1799
+ pathToId = null;
1800
+ focusActive = false;
1801
+ focusSet = new Set();
1802
+ searchActive = false;
1803
+ searchIds = new Set();
1804
+ minDegree = 0;
1805
+ hiddenNodeKinds.clear();
1806
+ hiddenEdgeKinds.clear();
1807
+
1808
+ // 4. Reset UI controls
1809
+ document.getElementById('btn-path').classList.remove('active');
1810
+ document.getElementById('btn-focus').classList.remove('active');
1811
+ document.getElementById('path-bar').style.display = 'none';
1812
+ document.getElementById('search').value = '';
1813
+ document.getElementById('degree-slider').value = '0';
1814
+ document.getElementById('degree-val').textContent = '0';
1815
+ document.querySelectorAll('.legend-item').forEach(el => el.classList.remove('dimmed'));
1816
+
1817
+ // Reset heat map
1818
+ if (heatActive) {
1819
+ heatActive = false;
1820
+ document.getElementById('btn-heat').classList.remove('active');
1821
+ document.getElementById('heat-legend').style.display = 'none';
1822
+ }
1823
+
1824
+ // Reset context menu
1825
+ document.getElementById('ctx-menu').style.display = 'none';
1826
+ ctxNodeId = null;
1827
+
1828
+ // 5. Restore detail panel (must keep hist-nav in DOM so next showDetail doesn't crash)
1829
+ document.getElementById('tab-detail').innerHTML =
1830
+ '<div id="history-nav">' +
1831
+ '<button class="hist-btn" id="hist-back" disabled>\u2039</button>' +
1832
+ '<span id="hist-label">no selection</span>' +
1833
+ '<button class="hist-btn" id="hist-forward" disabled>\u203A</button>' +
1834
+ '</div>' +
1835
+ '<p class="detail-empty">Click a node to inspect it.</p>';
1836
+ document.getElementById('hist-back').addEventListener('click', () => {
1837
+ if (histIdx > 0) { histIdx--; updateHistoryNav(); showDetail(navHistory[histIdx], false); network.selectNodes([navHistory[histIdx]]); }
1838
+ });
1839
+ document.getElementById('hist-forward').addEventListener('click', () => {
1840
+ if (histIdx < navHistory.length - 1) { histIdx++; updateHistoryNav(); showDetail(navHistory[histIdx], false); network.selectNodes([navHistory[histIdx]]); }
1841
+ });
1842
+ }
1843
+
1844
+ // \u2500\u2500 In/out degree precomputation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1845
+ var _inDeg = null, _outDeg = null;
1846
+ function ensureInOutDeg() {
1847
+ if (_inDeg) return;
1848
+ _inDeg = {}; _outDeg = {};
1849
+ NODES_DATA.forEach(function(n) { _inDeg[n.id] = 0; _outDeg[n.id] = 0; });
1850
+ EDGES_DATA.forEach(function(e) {
1851
+ _outDeg[e.from] = (_outDeg[e.from] || 0) + 1;
1852
+ _inDeg[e.to] = (_inDeg[e.to] || 0) + 1;
1853
+ });
1854
+ }
1855
+
1856
+ // \u2500\u2500 Shared horizontal bar renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1857
+ // rows: [{label, sublabel, value, color}], sorted descending, already sliced
1858
+ function drawHBar(canvasId, rows) {
1859
+ var canvas = document.getElementById(canvasId);
1860
+ if (!canvas) return;
1861
+ var ctx = canvas.getContext('2d');
1862
+ var W = canvas.width, H = canvas.height;
1863
+ ctx.clearRect(0, 0, W, H);
1864
+ if (!rows.length) {
1865
+ ctx.fillStyle = '#5a6280'; ctx.font = '12px monospace';
1866
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
1867
+ ctx.fillText('No data', W / 2, H / 2); return;
1868
+ }
1869
+ var maxVal = Math.max.apply(null, rows.map(function(r) { return r.value; })) || 1;
1870
+ var PAD_L = 210, PAD_R = 60, PAD_T = 14, PAD_B = 14;
1871
+ var barH = Math.floor((H - PAD_T - PAD_B) / rows.length);
1872
+ var barGap = Math.max(2, Math.floor(barH * 0.2));
1873
+ var barThick = barH - barGap;
1874
+ var barMaxW = W - PAD_L - PAD_R;
1875
+
1876
+ ctx.strokeStyle = 'rgba(80,80,120,0.6)'; ctx.lineWidth = 1;
1877
+ for (var g = 0; g <= 4; g++) {
1878
+ var gx = PAD_L + Math.round((g / 4) * barMaxW);
1879
+ ctx.beginPath(); ctx.moveTo(gx, PAD_T); ctx.lineTo(gx, H - PAD_B); ctx.stroke();
1880
+ }
1881
+
1882
+ rows.forEach(function(row, i) {
1883
+ var y = PAD_T + i * barH + Math.floor(barGap / 2);
1884
+ var w = Math.max(4, Math.round((row.value / maxVal) * barMaxW));
1885
+ var grad = ctx.createLinearGradient(PAD_L, 0, PAD_L + w, 0);
1886
+ grad.addColorStop(0, row.color); grad.addColorStop(1, row.color + '88');
1887
+ ctx.fillStyle = grad;
1888
+ ctx.beginPath(); ctx.roundRect(PAD_L, y, w, barThick, 3); ctx.fill();
1889
+
1890
+ var lbl = row.label.length > 28 ? row.label.slice(0, 26) + '\u2026' : row.label;
1891
+ ctx.fillStyle = '#c8d4f0'; ctx.font = '11px monospace';
1892
+ ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
1893
+ ctx.fillText(lbl, PAD_L - 8, y + barThick / 2);
1894
+ ctx.fillStyle = '#c792ea'; ctx.font = 'bold 11px monospace';
1895
+ ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
1896
+ ctx.fillText(String(row.value), PAD_L + w + 6, y + barThick / 2);
1897
+ });
1898
+ }
1899
+
1900
+ // \u2500\u2500 Shared vertical bar renderer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1901
+ // entries: [[label, value]], sorted descending, already sliced; colorFn(label,i)->color
1902
+ function drawVBar(canvasId, entries, colorFn) {
1903
+ var canvas = document.getElementById(canvasId);
1904
+ if (!canvas) return;
1905
+ var ctx = canvas.getContext('2d');
1906
+ var W = canvas.width, H = canvas.height;
1907
+ ctx.clearRect(0, 0, W, H);
1908
+ if (!entries.length) {
1909
+ ctx.fillStyle = '#5a6280'; ctx.font = '12px monospace';
1910
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
1911
+ ctx.fillText('No data', W / 2, H / 2); return;
1912
+ }
1913
+ var maxVal = Math.max.apply(null, entries.map(function(e) { return e[1]; })) || 1;
1914
+ var N = entries.length;
1915
+ var PAD_L = 32, PAD_R = 14, PAD_T = 14, PAD_B = 56;
1916
+ var plotW = W - PAD_L - PAD_R, plotH = H - PAD_T - PAD_B;
1917
+ var barW = Math.floor(plotW / N), barGap = Math.max(3, Math.floor(barW * 0.2));
1918
+ var barThick = barW - barGap;
1919
+
1920
+ ctx.strokeStyle = 'rgba(80,80,120,0.6)'; ctx.lineWidth = 1;
1921
+ for (var g = 1; g <= 4; g++) {
1922
+ var gy = PAD_T + Math.round((1 - g / 4) * plotH);
1923
+ ctx.beginPath(); ctx.moveTo(PAD_L, gy); ctx.lineTo(W - PAD_R, gy); ctx.stroke();
1924
+ ctx.fillStyle = '#7a86aa'; ctx.font = '9px monospace';
1925
+ ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
1926
+ ctx.fillText(String(Math.round(maxVal * g / 4)), PAD_L - 3, gy);
1927
+ }
1928
+
1929
+ entries.forEach(function(entry, i) {
1930
+ var lbl = entry[0], val = entry[1];
1931
+ var x = PAD_L + i * barW + Math.floor(barGap / 2);
1932
+ var bh = Math.max(2, Math.round((val / maxVal) * plotH));
1933
+ var color = colorFn(lbl, i);
1934
+ var grad = ctx.createLinearGradient(0, PAD_T + plotH - bh, 0, PAD_T + plotH);
1935
+ grad.addColorStop(0, color); grad.addColorStop(1, color + '88');
1936
+ ctx.fillStyle = grad;
1937
+ ctx.beginPath(); ctx.roundRect(x, PAD_T + plotH - bh, barThick, bh, 3); ctx.fill();
1938
+ ctx.fillStyle = '#c792ea'; ctx.font = 'bold 10px monospace';
1939
+ ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
1940
+ ctx.fillText(String(val), x + barThick / 2, PAD_T + plotH - bh - 2);
1941
+ var short = lbl.length > 10 ? lbl.slice(0, 9) + '\u2026' : lbl;
1942
+ ctx.save(); ctx.translate(x + barThick / 2, PAD_T + plotH + 6); ctx.rotate(Math.PI / 4);
1943
+ ctx.fillStyle = '#9ba3c8'; ctx.font = '9px monospace';
1944
+ ctx.textAlign = 'left'; ctx.textBaseline = 'top';
1945
+ ctx.fillText(short, 0, 0); ctx.restore();
1946
+ });
1947
+ }
1948
+
1949
+ // \u2500\u2500 4. Top callers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1950
+ function drawTopCallers() {
1951
+ ensureInOutDeg();
1952
+ var rows = NODES_DATA.slice()
1953
+ .sort(function(a, b) { return (_outDeg[b.id] || 0) - (_outDeg[a.id] || 0); })
1954
+ .slice(0, 15).filter(function(n) { return (_outDeg[n.id] || 0) > 0; })
1955
+ .map(function(n) { return { label: n.label, sublabel: n.kind, value: _outDeg[n.id] || 0, color: KIND_COLORS[n.kind] || '#546e7a' }; });
1956
+ drawHBar('chart-callers', rows);
1957
+ }
1958
+
1959
+ // \u2500\u2500 5. Top callees \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1960
+ function drawTopCallees() {
1961
+ ensureInOutDeg();
1962
+ var rows = NODES_DATA.slice()
1963
+ .sort(function(a, b) { return (_inDeg[b.id] || 0) - (_inDeg[a.id] || 0); })
1964
+ .slice(0, 15).filter(function(n) { return (_inDeg[n.id] || 0) > 0; })
1965
+ .map(function(n) { return { label: n.label, sublabel: n.kind, value: _inDeg[n.id] || 0, color: KIND_COLORS[n.kind] || '#546e7a' }; });
1966
+ drawHBar('chart-callees', rows);
1967
+ }
1968
+
1969
+ // \u2500\u2500 6. Files by symbol count \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1970
+ function drawFilesBySymbolCount() {
1971
+ var counts = {};
1972
+ NODES_DATA.forEach(function(n) { counts[n.filePath] = (counts[n.filePath] || 0) + 1; });
1973
+ var rows = Object.entries(counts)
1974
+ .sort(function(a, b) { return b[1] - a[1]; }).slice(0, 15)
1975
+ .map(function(e) {
1976
+ var parts = e[0].split('/');
1977
+ var short = parts.length > 2 ? '\u2026/' + parts.slice(-2).join('/') : e[0];
1978
+ return { label: short, sublabel: e[0], value: e[1], color: '#7986cb' };
1979
+ });
1980
+ drawHBar('chart-files', rows);
1981
+ }
1982
+
1983
+ // \u2500\u2500 7. Edge kind distribution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1984
+ function drawEdgeKindDistribution() {
1985
+ var canvas = document.getElementById('chart-edgekinds');
1986
+ if (!canvas) return;
1987
+ var ctx = canvas.getContext('2d');
1988
+ var W = canvas.width, H = canvas.height;
1989
+ ctx.clearRect(0, 0, W, H);
1990
+
1991
+ var counts = {};
1992
+ EDGES_DATA.forEach(function(e) { counts[e.ekind] = (counts[e.ekind] || 0) + 1; });
1993
+ var entries = Object.entries(counts).sort(function(a, b) { return b[1] - a[1]; });
1994
+ var total = EDGES_DATA.length || 1;
1995
+ var cx = Math.round(W * 0.35), cy = H / 2;
1996
+ var outerR = Math.min(cx, cy) - 16, innerR = outerR * 0.5;
1997
+ var startAngle = -Math.PI / 2;
1998
+
1999
+ entries.forEach(function(entry) {
2000
+ var kind = entry[0], count = entry[1];
2001
+ var angle = (count / total) * 2 * Math.PI;
2002
+ var color = EDGE_COLORS[kind] || '#546e7a';
2003
+ ctx.beginPath(); ctx.moveTo(cx, cy);
2004
+ ctx.arc(cx, cy, outerR, startAngle, startAngle + angle);
2005
+ ctx.closePath(); ctx.fillStyle = color; ctx.fill();
2006
+ ctx.strokeStyle = '#0d0d1a'; ctx.lineWidth = 2; ctx.stroke();
2007
+ startAngle += angle;
2008
+ });
2009
+
2010
+ ctx.beginPath(); ctx.arc(cx, cy, innerR, 0, 2 * Math.PI);
2011
+ ctx.fillStyle = '#0d0d1a'; ctx.fill();
2012
+ ctx.fillStyle = '#c792ea'; ctx.font = 'bold 18px monospace';
2013
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
2014
+ ctx.fillText(String(EDGES_DATA.length), cx, cy - 7);
2015
+ ctx.fillStyle = '#7a86aa'; ctx.font = '10px monospace';
2016
+ ctx.fillText('edges', cx, cy + 9);
2017
+
2018
+ var LEG_X = Math.round(W * 0.64), rowH = Math.floor((H - 28) / Math.min(entries.length, 12));
2019
+ entries.slice(0, 12).forEach(function(entry, i) {
2020
+ var kind = entry[0], count = entry[1];
2021
+ var y = 14 + i * rowH;
2022
+ var color = EDGE_COLORS[kind] || '#546e7a';
2023
+ ctx.fillStyle = color; ctx.fillRect(LEG_X, y + Math.floor(rowH / 2) - 2, 14, 3);
2024
+ ctx.fillStyle = '#b0c4d8'; ctx.font = '11px monospace';
2025
+ ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
2026
+ ctx.fillText(kind, LEG_X + 18, y + rowH / 2);
2027
+ ctx.fillStyle = '#7a86aa'; ctx.font = '10px monospace'; ctx.textAlign = 'right';
2028
+ ctx.fillText(count + ' ' + ((count / total) * 100).toFixed(1) + '%', W - 6, y + rowH / 2);
2029
+ });
2030
+ }
2031
+
2032
+ // \u2500\u2500 8. Dead code by kind \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2033
+ function drawDeadCodeByKind() {
2034
+ ensureInOutDeg();
2035
+ var canvas = document.getElementById('chart-deadcode');
2036
+ if (!canvas) return;
2037
+ var ctx = canvas.getContext('2d');
2038
+ var W = canvas.width, H = canvas.height;
2039
+ ctx.clearRect(0, 0, W, H);
2040
+
2041
+ var counts = {};
2042
+ NODES_DATA.forEach(function(n) {
2043
+ if (!n.isExported && (_inDeg[n.id] || 0) === 0)
2044
+ counts[n.kind] = (counts[n.kind] || 0) + 1;
2045
+ });
2046
+ var entries = Object.entries(counts).sort(function(a, b) { return b[1] - a[1]; });
2047
+ if (!entries.length) {
2048
+ ctx.fillStyle = '#27ae60'; ctx.font = '13px monospace';
2049
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
2050
+ ctx.fillText('\u2713 No dead code detected', W / 2, H / 2); return;
2051
+ }
2052
+ drawVBar('chart-deadcode', entries, function(kind) { return KIND_COLORS[kind] || '#546e7a'; });
2053
+ }
2054
+
2055
+ // \u2500\u2500 9. Exported vs unexported by kind \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2056
+ function drawExportedRatio() {
2057
+ var canvas = document.getElementById('chart-exported');
2058
+ if (!canvas) return;
2059
+ var ctx = canvas.getContext('2d');
2060
+ var W = canvas.width, H = canvas.height;
2061
+ ctx.clearRect(0, 0, W, H);
2062
+
2063
+ var kindData = {};
2064
+ NODES_DATA.forEach(function(n) {
2065
+ if (!kindData[n.kind]) kindData[n.kind] = { exp: 0, unexp: 0 };
2066
+ if (n.isExported) kindData[n.kind].exp++; else kindData[n.kind].unexp++;
2067
+ });
2068
+ var entries = Object.entries(kindData)
2069
+ .sort(function(a, b) { return (b[1].exp + b[1].unexp) - (a[1].exp + a[1].unexp); })
2070
+ .slice(0, 16);
2071
+ var N = entries.length;
2072
+ if (!N) return;
2073
+
2074
+ var PAD_L = 96, PAD_R = 100, PAD_T = 32, PAD_B = 14;
2075
+ var rowH = Math.floor((H - PAD_T - PAD_B) / N);
2076
+ var rowGap = Math.max(2, Math.floor(rowH * 0.25));
2077
+ var barThick = rowH - rowGap;
2078
+ var barMaxW = W - PAD_L - PAD_R;
2079
+
2080
+ // Legend
2081
+ ctx.fillStyle = '#c792ea'; ctx.font = 'bold 10px monospace';
2082
+ ctx.textAlign = 'left'; ctx.textBaseline = 'top';
2083
+ ctx.fillText('\u25A0 Exported', PAD_L, 10);
2084
+ ctx.fillStyle = '#7a86aa'; ctx.fillText('\u25A0 Unexported', PAD_L + 88, 10);
2085
+
2086
+ entries.forEach(function(entry, i) {
2087
+ var kind = entry[0], d = entry[1];
2088
+ var total = d.exp + d.unexp;
2089
+ var y = PAD_T + i * rowH + Math.floor(rowGap / 2);
2090
+ var expW = Math.round((d.exp / total) * barMaxW);
2091
+ var color = KIND_COLORS[kind] || '#546e7a';
2092
+
2093
+ if (expW > 0) {
2094
+ ctx.fillStyle = color;
2095
+ ctx.beginPath(); ctx.roundRect(PAD_L, y, expW, barThick, [3, 0, 0, 3]); ctx.fill();
2096
+ }
2097
+ if (expW < barMaxW) {
2098
+ ctx.fillStyle = color + '44';
2099
+ ctx.beginPath(); ctx.roundRect(PAD_L + expW, y, barMaxW - expW, barThick, [0, 3, 3, 0]); ctx.fill();
2100
+ }
2101
+
2102
+ ctx.fillStyle = '#b0c4d8'; ctx.font = '11px monospace';
2103
+ ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
2104
+ ctx.fillText(kind, PAD_L - 8, y + barThick / 2);
2105
+ ctx.fillStyle = '#7a86aa'; ctx.font = '10px monospace'; ctx.textAlign = 'left';
2106
+ ctx.fillText(d.exp + ' / ' + total, W - PAD_R + 8, y + barThick / 2);
2107
+ });
2108
+ }
2109
+
2110
+ // \u2500\u2500 10. Directory coupling matrix \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2111
+ function drawDirectoryCoupling() {
2112
+ var canvas = document.getElementById('chart-coupling');
2113
+ if (!canvas) return;
2114
+ var ctx = canvas.getContext('2d');
2115
+ var W = canvas.width, H = canvas.height;
2116
+ ctx.clearRect(0, 0, W, H);
2117
+
2118
+ var dirCounts = {};
2119
+ NODES_DATA.forEach(function(n) { if (n.dir) dirCounts[n.dir] = (dirCounts[n.dir] || 0) + 1; });
2120
+ var dirs = Object.keys(dirCounts).sort(function(a, b) { return dirCounts[b] - dirCounts[a]; }).slice(0, 12);
2121
+ var N = dirs.length;
2122
+ if (N < 2) {
2123
+ ctx.fillStyle = '#5a6280'; ctx.font = '12px monospace';
2124
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
2125
+ ctx.fillText('Not enough directories', W / 2, H / 2); return;
2126
+ }
2127
+
2128
+ var dirIdx = {};
2129
+ dirs.forEach(function(d, i) { dirIdx[d] = i; });
2130
+ var nodeDir = {};
2131
+ NODES_DATA.forEach(function(n) { if (n.dir) nodeDir[n.id] = n.dir; });
2132
+
2133
+ var matrix = [];
2134
+ for (var i = 0; i < N; i++) { matrix.push([]); for (var j = 0; j < N; j++) matrix[i].push(0); }
2135
+ EDGES_DATA.forEach(function(e) {
2136
+ var fd = nodeDir[e.from], td = nodeDir[e.to];
2137
+ if (fd && td && fd !== td && dirIdx[fd] !== undefined && dirIdx[td] !== undefined)
2138
+ matrix[dirIdx[fd]][dirIdx[td]]++;
2139
+ });
2140
+ var maxVal = 0;
2141
+ for (var i = 0; i < N; i++) for (var j = 0; j < N; j++) if (matrix[i][j] > maxVal) maxVal = matrix[i][j];
2142
+
2143
+ // Layout: row labels on the left, column labels at the bottom
2144
+ var PAD_TOP = 10, PAD_RIGHT = 14;
2145
+ var LABEL_W = 150; // left margin for row labels
2146
+ var LABEL_BOT = 100; // bottom margin for column labels (rotated 45\xB0)
2147
+
2148
+ var matrixW = W - LABEL_W - PAD_RIGHT;
2149
+ var matrixH = H - PAD_TOP - LABEL_BOT;
2150
+ var cellW = Math.floor(matrixW / N);
2151
+ var cellH = Math.floor(matrixH / N);
2152
+ var matrixTop = PAD_TOP;
2153
+ var matrixLeft = LABEL_W;
2154
+
2155
+ // \u2500\u2500 Matrix cells \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2156
+ dirs.forEach(function(d, i) {
2157
+ var y = matrixTop + i * cellH;
2158
+ dirs.forEach(function(d2, j) {
2159
+ var x = matrixLeft + j * cellW;
2160
+ var val = matrix[i][j];
2161
+ var intensity = maxVal > 0 ? val / maxVal : 0;
2162
+ ctx.fillStyle = i === j
2163
+ ? 'rgba(20,20,48,0.8)'
2164
+ : 'rgba(121,134,203,' + (0.07 + intensity * 0.88).toFixed(2) + ')';
2165
+ ctx.fillRect(x + 1, y + 1, cellW - 2, cellH - 2);
2166
+ if (val > 0 && i !== j) {
2167
+ ctx.fillStyle = intensity > 0.45 ? '#ffffff' : '#b0badd';
2168
+ ctx.font = 'bold ' + (cellW > 42 ? '11' : '9') + 'px monospace';
2169
+ ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
2170
+ ctx.fillText(String(val), x + cellW / 2, y + cellH / 2);
2171
+ }
2172
+ });
2173
+ });
2174
+
2175
+ // \u2500\u2500 Row labels (left, right-aligned, white) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2176
+ dirs.forEach(function(d, i) {
2177
+ var y = matrixTop + i * cellH + cellH / 2;
2178
+ var short = d.length > 18 ? d.slice(0, 16) + '\u2026' : d;
2179
+ ctx.fillStyle = '#ffffff'; ctx.font = '11px monospace';
2180
+ ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
2181
+ ctx.fillText(short, matrixLeft - 8, y);
2182
+ });
2183
+
2184
+ // \u2500\u2500 Column labels (bottom, rotated -45\xB0, white) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2185
+ dirs.forEach(function(d, j) {
2186
+ var x = matrixLeft + j * cellW + cellW / 2;
2187
+ var y = matrixTop + matrixH + 8;
2188
+ var short = d.length > 18 ? d.slice(0, 16) + '\u2026' : d;
2189
+ ctx.save();
2190
+ ctx.translate(x, y);
2191
+ ctx.rotate(Math.PI / 4); // 45\xB0 downward-right \u2192 reads bottom-left to top-right
2192
+ ctx.fillStyle = '#ffffff'; ctx.font = '9px monospace';
2193
+ ctx.textAlign = 'left'; ctx.textBaseline = 'middle';
2194
+ ctx.fillText(short, 0, 0);
2195
+ ctx.restore();
2196
+ });
2197
+ }
2198
+
2199
+ // \u2500\u2500 11. Symbol count per directory \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2200
+ function drawSymbolCountPerDir() {
2201
+ var counts = {};
2202
+ NODES_DATA.forEach(function(n) { if (n.dir) counts[n.dir] = (counts[n.dir] || 0) + 1; });
2203
+ var entries = Object.entries(counts).sort(function(a, b) { return b[1] - a[1]; }).slice(0, 15);
2204
+ drawVBar('chart-dirs', entries, function(lbl, i) {
2205
+ var hue = (i * 37) % 360;
2206
+ return 'hsl(' + hue + ',55%,45%)';
2207
+ });
2208
+ }
2209
+
2210
+ // \u2500\u2500 12. In/out scatter \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2211
+ function drawInOutScatter() {
2212
+ ensureInOutDeg();
2213
+ var canvas = document.getElementById('chart-scatter');
2214
+ if (!canvas) return;
2215
+ var ctx = canvas.getContext('2d');
2216
+ var W = canvas.width, H = canvas.height;
2217
+ ctx.clearRect(0, 0, W, H);
2218
+
2219
+ var PAD_L = 46, PAD_R = 14, PAD_T = 14, PAD_B = 46;
2220
+ var plotW = W - PAD_L - PAD_R, plotH = H - PAD_T - PAD_B;
2221
+ var maxOut = 1, maxIn = 1;
2222
+ NODES_DATA.forEach(function(n) {
2223
+ if ((_outDeg[n.id] || 0) > maxOut) maxOut = _outDeg[n.id];
2224
+ if ((_inDeg[n.id] || 0) > maxIn) maxIn = _inDeg[n.id];
2225
+ });
2226
+
2227
+ // Grid + axis labels
2228
+ ctx.strokeStyle = 'rgba(80,80,120,0.6)'; ctx.lineWidth = 1;
2229
+ for (var g = 0; g <= 4; g++) {
2230
+ var gx = PAD_L + Math.round((g / 4) * plotW);
2231
+ var gy = PAD_T + Math.round((g / 4) * plotH);
2232
+ ctx.beginPath(); ctx.moveTo(gx, PAD_T); ctx.lineTo(gx, PAD_T + plotH); ctx.stroke();
2233
+ ctx.beginPath(); ctx.moveTo(PAD_L, gy); ctx.lineTo(PAD_L + plotW, gy); ctx.stroke();
2234
+ if (g > 0) {
2235
+ ctx.fillStyle = '#7a86aa'; ctx.font = '9px monospace';
2236
+ ctx.textAlign = 'center'; ctx.textBaseline = 'top';
2237
+ ctx.fillText(Math.round(maxOut * g / 4), gx, PAD_T + plotH + 4);
2238
+ ctx.textAlign = 'right'; ctx.textBaseline = 'middle';
2239
+ ctx.fillText(Math.round(maxIn * (1 - g / 4)), PAD_L - 3, PAD_T + Math.round((g / 4) * plotH));
2240
+ }
2241
+ }
2242
+
2243
+ // Quadrant labels
2244
+ ctx.fillStyle = 'rgba(121,134,203,0.35)'; ctx.font = '10px monospace'; ctx.textAlign = 'center';
2245
+ ctx.textBaseline = 'middle';
2246
+ ctx.fillText('Hubs', PAD_L + plotW * 0.75, PAD_T + plotH * 0.12);
2247
+ ctx.fillText('Sources', PAD_L + plotW * 0.75, PAD_T + plotH * 0.88);
2248
+ ctx.fillText('Sinks', PAD_L + plotW * 0.25, PAD_T + plotH * 0.12);
2249
+ ctx.fillText('Isolated',PAD_L + plotW * 0.25, PAD_T + plotH * 0.88);
2250
+
2251
+ // Dots
2252
+ ctx.globalAlpha = 0.72;
2253
+ NODES_DATA.forEach(function(n) {
2254
+ var ox = _outDeg[n.id] || 0, iy = _inDeg[n.id] || 0;
2255
+ var x = PAD_L + Math.round((ox / maxOut) * plotW);
2256
+ var y = PAD_T + Math.round((1 - iy / maxIn) * plotH);
2257
+ ctx.beginPath(); ctx.arc(x, y, 3, 0, 2 * Math.PI);
2258
+ ctx.fillStyle = KIND_COLORS[n.kind] || '#546e7a'; ctx.fill();
2259
+ });
2260
+ ctx.globalAlpha = 1;
2261
+
2262
+ // Axis labels
2263
+ ctx.fillStyle = '#7a86aa'; ctx.font = '10px monospace';
2264
+ ctx.textAlign = 'center'; ctx.textBaseline = 'bottom';
2265
+ ctx.fillText('out-degree (calls made)', PAD_L + plotW / 2, H);
2266
+ ctx.save(); ctx.translate(11, PAD_T + plotH / 2); ctx.rotate(-Math.PI / 2);
2267
+ ctx.textAlign = 'center'; ctx.textBaseline = 'top';
2268
+ ctx.fillText('in-degree (times referenced)', 0, 0); ctx.restore();
2269
+ }
2270
+
2271
+ // \u2500\u2500 13. Average degree per file \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2272
+ function drawAvgDegreePerFile() {
2273
+ var fileDeg = {}, fileCount = {};
2274
+ NODES_DATA.forEach(function(n) {
2275
+ fileDeg[n.filePath] = (fileDeg[n.filePath] || 0) + n.degree;
2276
+ fileCount[n.filePath] = (fileCount[n.filePath] || 0) + 1;
2277
+ });
2278
+ var rows = Object.keys(fileDeg).map(function(fp) {
2279
+ var avg = fileDeg[fp] / fileCount[fp];
2280
+ var parts = fp.split('/');
2281
+ var short = parts.length > 2 ? '\u2026/' + parts.slice(-2).join('/') : fp;
2282
+ return { label: short, sublabel: fileCount[fp] + ' symbols', value: Math.round(avg * 10) / 10, color: '#00838f' };
2283
+ }).sort(function(a, b) { return b.value - a.value; }).slice(0, 15);
2284
+ drawHBar('chart-avgdeg', rows);
2285
+ }
2286
+
2287
+ // \u2500\u2500 Charts modal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2288
+ document.getElementById('btn-charts').addEventListener('click', function() {
2289
+ document.getElementById('charts-modal').style.display = 'flex';
2290
+ drawCharts();
2291
+ });
2292
+ document.getElementById('charts-close').addEventListener('click', function() {
2293
+ document.getElementById('charts-modal').style.display = 'none';
2294
+ });
2295
+ document.getElementById('charts-modal').addEventListener('click', function(e) {
2296
+ if (e.target === this) this.style.display = 'none';
2297
+ });
2298
+
2299
+ function drawCharts() {
2300
+ [
2301
+ drawBarChart, drawPieChart, drawLineChart,
2302
+ drawTopCallers, drawTopCallees, drawFilesBySymbolCount,
2303
+ drawEdgeKindDistribution, drawDeadCodeByKind, drawExportedRatio,
2304
+ drawDirectoryCoupling, drawSymbolCountPerDir, drawInOutScatter,
2305
+ drawAvgDegreePerFile,
2306
+ ].forEach(function(fn) {
2307
+ try { fn(); } catch(e) { console.warn('[kirograph chart] ' + fn.name + ':', e); }
2308
+ });
2309
+ }
2310
+
2311
+ // \u2500\u2500 Bar chart: Top 15 most connected symbols \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2312
+ function drawBarChart() {
2313
+ var canvas = document.getElementById('chart-bar');
2314
+ var ctx = canvas.getContext('2d');
2315
+ var W = canvas.width, H = canvas.height;
2316
+ ctx.clearRect(0, 0, W, H);
2317
+
2318
+ var top = NODES_DATA.slice().sort(function(a, b) { return b.degree - a.degree; }).slice(0, 15);
2319
+ if (top.length === 0) return;
2320
+
2321
+ var maxDeg = top[0].degree || 1;
2322
+ var PAD_L = 170, PAD_R = 54, PAD_T = 14, PAD_B = 14;
2323
+ var barH = Math.floor((H - PAD_T - PAD_B) / top.length);
2324
+ var barGap = Math.max(2, Math.floor(barH * 0.22));
2325
+ var barThick = barH - barGap;
2326
+ var barMaxW = W - PAD_L - PAD_R;
2327
+
2328
+ // Grid lines
2329
+ ctx.strokeStyle = 'rgba(80,80,120,0.6)';
2330
+ ctx.lineWidth = 1;
2331
+ for (var g = 0; g <= 4; g++) {
2332
+ var gx = PAD_L + Math.round((g / 4) * barMaxW);
2333
+ ctx.beginPath(); ctx.moveTo(gx, PAD_T); ctx.lineTo(gx, H - PAD_B); ctx.stroke();
2334
+ }
2335
+
2336
+ top.forEach(function(n, i) {
2337
+ var y = PAD_T + i * barH + Math.floor(barGap / 2);
2338
+ var w = Math.max(4, Math.round((n.degree / maxDeg) * barMaxW));
2339
+ var color = KIND_COLORS[n.kind] || '#546e7a';
2340
+
2341
+ // Gradient bar
2342
+ var grad = ctx.createLinearGradient(PAD_L, 0, PAD_L + w, 0);
2343
+ grad.addColorStop(0, color);
2344
+ grad.addColorStop(1, color + '88');
2345
+ ctx.fillStyle = grad;
2346
+ ctx.beginPath();
2347
+ ctx.roundRect(PAD_L, y, w, barThick, 3);
2348
+ ctx.fill();
2349
+
2350
+ // Node label
2351
+ var label = n.label.length > 22 ? n.label.slice(0, 20) + '\\u2026' : n.label;
2352
+ ctx.fillStyle = '#c8d4f0';
2353
+ ctx.font = '11px monospace';
2354
+ ctx.textAlign = 'right';
2355
+ ctx.textBaseline = 'middle';
2356
+ ctx.fillText(label, PAD_L - 8, y + barThick / 2);
2357
+
2358
+ // Degree value
2359
+ ctx.fillStyle = '#c792ea';
2360
+ ctx.font = 'bold 11px monospace';
2361
+ ctx.textAlign = 'left';
2362
+ ctx.textBaseline = 'middle';
2363
+ ctx.fillText(String(n.degree), PAD_L + w + 6, y + barThick / 2);
2364
+ });
2365
+ }
2366
+
2367
+ // \u2500\u2500 Pie chart: Node count by kind \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2368
+ function drawPieChart() {
2369
+ var canvas = document.getElementById('chart-pie');
2370
+ var ctx = canvas.getContext('2d');
2371
+ var W = canvas.width, H = canvas.height;
2372
+ ctx.clearRect(0, 0, W, H);
2373
+
2374
+ var counts = {};
2375
+ NODES_DATA.forEach(function(n) { counts[n.kind] = (counts[n.kind] || 0) + 1; });
2376
+ var entries = Object.entries(counts).sort(function(a, b) { return b[1] - a[1]; });
2377
+ var total = NODES_DATA.length || 1;
2378
+
2379
+ var cx = Math.round(W * 0.36), cy = H / 2;
2380
+ var outerR = Math.min(cx, cy) - 16;
2381
+ var innerR = outerR * 0.52;
2382
+
2383
+ var startAngle = -Math.PI / 2;
2384
+
2385
+ entries.forEach(function(entry) {
2386
+ var kind = entry[0], count = entry[1];
2387
+ var angle = (count / total) * 2 * Math.PI;
2388
+ var color = KIND_COLORS[kind] || '#546e7a';
2389
+
2390
+ ctx.beginPath();
2391
+ ctx.moveTo(cx, cy);
2392
+ ctx.arc(cx, cy, outerR, startAngle, startAngle + angle);
2393
+ ctx.closePath();
2394
+ ctx.fillStyle = color;
2395
+ ctx.fill();
2396
+
2397
+ // Gap stroke
2398
+ ctx.strokeStyle = '#0d0d1a';
2399
+ ctx.lineWidth = 2;
2400
+ ctx.stroke();
2401
+
2402
+ startAngle += angle;
2403
+ });
2404
+
2405
+ // Donut hole
2406
+ ctx.beginPath();
2407
+ ctx.arc(cx, cy, innerR, 0, 2 * Math.PI);
2408
+ ctx.fillStyle = '#0d0d1a';
2409
+ ctx.fill();
2410
+
2411
+ // Center label
2412
+ ctx.fillStyle = '#c792ea';
2413
+ ctx.font = 'bold 20px monospace';
2414
+ ctx.textAlign = 'center';
2415
+ ctx.textBaseline = 'middle';
2416
+ ctx.fillText(String(total), cx, cy - 7);
2417
+ ctx.fillStyle = '#7a86aa';
2418
+ ctx.font = '10px monospace';
2419
+ ctx.fillText('nodes', cx, cy + 10);
2420
+
2421
+ // Legend
2422
+ var LEG_X = Math.round(W * 0.68);
2423
+ var LEG_Y_START = 16;
2424
+ var rowH = Math.floor((H - LEG_Y_START * 2) / Math.min(entries.length, 14));
2425
+
2426
+ entries.slice(0, 14).forEach(function(entry, i) {
2427
+ var kind = entry[0], count = entry[1];
2428
+ var y = LEG_Y_START + i * rowH;
2429
+ var color = KIND_COLORS[kind] || '#546e7a';
2430
+ ctx.fillStyle = color;
2431
+ ctx.beginPath();
2432
+ ctx.roundRect(LEG_X, y + Math.floor(rowH / 2) - 5, 10, 10, 2);
2433
+ ctx.fill();
2434
+ ctx.fillStyle = '#c8d4f0';
2435
+ ctx.font = '11px monospace';
2436
+ ctx.textAlign = 'left';
2437
+ ctx.textBaseline = 'middle';
2438
+ ctx.fillText(kind, LEG_X + 14, y + rowH / 2);
2439
+ var pct = ((count / total) * 100).toFixed(1);
2440
+ ctx.fillStyle = '#7a86aa';
2441
+ ctx.font = '10px monospace';
2442
+ ctx.textAlign = 'right';
2443
+ ctx.fillText(count + ' ' + pct + '%', W - 8, y + rowH / 2);
2444
+ });
2445
+ }
2446
+
2447
+ // \u2500\u2500 Line chart: Degree distribution \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2448
+ function drawLineChart() {
2449
+ var canvas = document.getElementById('chart-line');
2450
+ var ctx = canvas.getContext('2d');
2451
+ var W = canvas.width, H = canvas.height;
2452
+ ctx.clearRect(0, 0, W, H);
2453
+
2454
+ var degMap = {};
2455
+ NODES_DATA.forEach(function(n) { degMap[n.degree] = (degMap[n.degree] || 0) + 1; });
2456
+ var maxDegVal = 0;
2457
+ Object.keys(degMap).forEach(function(k) { var v = Number(k); if (v > maxDegVal) maxDegVal = v; });
2458
+ if (maxDegVal === 0) return;
2459
+
2460
+ var BINS = Math.min(40, maxDegVal + 1);
2461
+ var binSize = Math.ceil((maxDegVal + 1) / BINS);
2462
+ var bins = [];
2463
+ for (var b = 0; b < BINS; b++) {
2464
+ var cnt = 0;
2465
+ for (var d = b * binSize; d < (b + 1) * binSize; d++) cnt += degMap[d] || 0;
2466
+ bins.push({ deg: b * binSize, count: cnt });
2467
+ }
2468
+ var maxCount = 1;
2469
+ bins.forEach(function(b) { if (b.count > maxCount) maxCount = b.count; });
2470
+
2471
+ var PAD_L = 44, PAD_R = 14, PAD_T = 14, PAD_B = 36;
2472
+ var plotW = W - PAD_L - PAD_R;
2473
+ var plotH = H - PAD_T - PAD_B;
2474
+
2475
+ // Grid
2476
+ ctx.strokeStyle = 'rgba(80,80,120,0.6)';
2477
+ ctx.lineWidth = 1;
2478
+ for (var g = 0; g <= 4; g++) {
2479
+ var gy = PAD_T + Math.round((g / 4) * plotH);
2480
+ ctx.beginPath(); ctx.moveTo(PAD_L, gy); ctx.lineTo(W - PAD_R, gy); ctx.stroke();
2481
+ var gval = Math.round(maxCount * (1 - g / 4));
2482
+ ctx.fillStyle = '#7a86aa';
2483
+ ctx.font = '9px monospace';
2484
+ ctx.textAlign = 'right';
2485
+ ctx.textBaseline = 'middle';
2486
+ ctx.fillText(String(gval), PAD_L - 5, gy);
2487
+ }
2488
+
2489
+ var pts = bins.map(function(bin, i) {
2490
+ return {
2491
+ x: PAD_L + Math.round((i / (BINS - 1 || 1)) * plotW),
2492
+ y: PAD_T + Math.round((1 - bin.count / maxCount) * plotH),
2493
+ };
2494
+ });
2495
+
2496
+ // Area fill
2497
+ var areaGrad = ctx.createLinearGradient(0, PAD_T, 0, PAD_T + plotH);
2498
+ areaGrad.addColorStop(0, 'rgba(121,134,203,0.55)');
2499
+ areaGrad.addColorStop(1, 'rgba(121,134,203,0.04)');
2500
+
2501
+ ctx.beginPath();
2502
+ ctx.moveTo(pts[0].x, PAD_T + plotH);
2503
+ pts.forEach(function(p) { ctx.lineTo(p.x, p.y); });
2504
+ ctx.lineTo(pts[pts.length - 1].x, PAD_T + plotH);
2505
+ ctx.closePath();
2506
+ ctx.fillStyle = areaGrad;
2507
+ ctx.fill();
2508
+
2509
+ // Line
2510
+ ctx.beginPath();
2511
+ pts.forEach(function(p, i) { if (i === 0) ctx.moveTo(p.x, p.y); else ctx.lineTo(p.x, p.y); });
2512
+ ctx.strokeStyle = '#9fa8da';
2513
+ ctx.lineWidth = 2.5;
2514
+ ctx.lineJoin = 'round';
2515
+ ctx.stroke();
2516
+
2517
+ // Peak dot
2518
+ var peakIdx = 0;
2519
+ bins.forEach(function(bin, i) { if (bin.count > bins[peakIdx].count) peakIdx = i; });
2520
+ ctx.beginPath();
2521
+ ctx.arc(pts[peakIdx].x, pts[peakIdx].y, 4, 0, 2 * Math.PI);
2522
+ ctx.fillStyle = '#c792ea';
2523
+ ctx.fill();
2524
+
2525
+ // X axis labels
2526
+ var labelStep = Math.ceil(BINS / 8);
2527
+ ctx.fillStyle = '#7a86aa';
2528
+ ctx.font = '9px monospace';
2529
+ ctx.textAlign = 'center';
2530
+ ctx.textBaseline = 'top';
2531
+ bins.forEach(function(bin, i) {
2532
+ if (i % labelStep !== 0 && i !== bins.length - 1) return;
2533
+ ctx.fillText(String(bin.deg), pts[i].x, PAD_T + plotH + 6);
2534
+ });
2535
+
2536
+ // X axis label
2537
+ ctx.fillStyle = '#9ba3c8';
2538
+ ctx.font = '10px monospace';
2539
+ ctx.textAlign = 'center';
2540
+ ctx.textBaseline = 'bottom';
2541
+ ctx.fillText('connections', PAD_L + plotW / 2, H);
2542
+ }
2543
+
2544
+ // \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2545
+ function esc(s) {
2546
+ return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
2547
+ }
2548
+
2549
+ `;
2550
+ }
2551
+ function printExportHelp() {
2552
+ const c = {
2553
+ reset: "\x1B[0m",
2554
+ bold: "\x1B[1m",
2555
+ dim: "\x1B[2m",
2556
+ violet: "\x1B[38;5;99m",
2557
+ lavender: "\x1B[38;5;141m",
2558
+ paleLavender: "\x1B[38;5;183m",
2559
+ purple: "\x1B[38;5;135m",
2560
+ gray: "\x1B[90m"
2561
+ };
2562
+ console.log(`
2563
+ ${c.bold}${c.paleLavender}USAGE${c.reset}`);
2564
+ console.log(` ${c.lavender}kirograph export${c.reset} ${c.gray}<command>${c.reset} ${c.dim}[options] [path]${c.reset}
2565
+ `);
2566
+ console.log(`${c.bold}${c.paleLavender}COMMANDS${c.reset}
2567
+ `);
2568
+ const cmds = [
2569
+ { name: "build", args: "[path]", desc: "Generate the dashboard in .kirograph/export/" },
2570
+ { name: "start", args: "[path]", desc: "Generate the dashboard and open it in the browser" }
2571
+ ];
2572
+ const nameWidth = Math.max(...cmds.map((c2) => (c2.name + " " + c2.args).length)) + 2;
2573
+ for (const cmd of cmds) {
2574
+ const sig = cmd.name + " " + cmd.args;
2575
+ const pad = " ".repeat(Math.max(0, nameWidth - sig.length));
2576
+ console.log(` ${c.lavender}${cmd.name}${c.reset} ${c.dim}${cmd.args}${c.reset}${pad}${c.gray}${cmd.desc}${c.reset}`);
2577
+ }
2578
+ console.log(`
2579
+ ${c.bold}${c.paleLavender}OPTIONS${c.reset}
2580
+ `);
2581
+ console.log(` ${c.purple}-o, --output <dir>${c.reset} ${c.gray}Custom output directory${c.reset}`);
2582
+ console.log(` ${c.purple}--include-contains${c.reset} ${c.gray}Include structural contains edges (adds noise, off by default)${c.reset}`);
2583
+ console.log(` ${c.purple}-h, --help${c.reset} ${c.gray}Show this help${c.reset}
2584
+ `);
2585
+ console.log(`${c.bold}${c.paleLavender}EXAMPLES${c.reset}
2586
+ `);
2587
+ const examples = [
2588
+ ["kirograph export start", "Generate and open the dashboard in the browser"],
2589
+ ["kirograph export build", "Generate only (no browser)"],
2590
+ ["kirograph export build -o /tmp/graph", "Write dashboard files to a custom directory"],
2591
+ ["kirograph export start --include-contains", "Include structural contains edges"]
2592
+ ];
2593
+ for (const [ex, desc] of examples) {
2594
+ console.log(` ${c.violet}$${c.reset} ${c.lavender}${ex}${c.reset}`);
2595
+ console.log(` ${c.dim}${desc}${c.reset}`);
2596
+ }
2597
+ console.log();
2598
+ }
2599
+ function register(program) {
2600
+ const exportCmd = program.command("export").description("Export the graph as an interactive dashboard");
2601
+ exportCmd.configureHelp({ formatHelp: () => "" });
2602
+ exportCmd.helpInformation = () => {
2603
+ printExportHelp();
2604
+ return "";
2605
+ };
2606
+ exportCmd.command("build [projectPath]").description("Generate the dashboard files in .kirograph/export/").option("-o, --output <dir>", "Output directory path").option("--include-contains", "Include structural contains edges (adds noise, off by default)", false).action(async (projectPath, opts) => {
2607
+ await generateExport(projectPath, opts);
2608
+ });
2609
+ exportCmd.command("start [projectPath]").description("Generate the dashboard files and open in the browser").option("-o, --output <dir>", "Output directory path").option("--include-contains", "Include structural contains edges (adds noise, off by default)", false).action(async (projectPath, opts) => {
2610
+ const indexPath = await generateExport(projectPath, opts);
2611
+ console.log(` ${import_ui.dim}Opening in browser\u2026${import_ui.reset}
2612
+ `);
2613
+ openBrowser(indexPath);
2614
+ });
2615
+ }
2616
+ // Annotate the CommonJS export names for ESM import in node:
2617
+ 0 && (module.exports = {
2618
+ register
2619
+ });
2620
+ //# sourceMappingURL=export.js.map