@diagrammo/dgmo 0.46.0 → 0.48.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (534) hide show
  1. package/dist/advanced.d.ts +3 -855
  2. package/dist/advanced.js +541 -70107
  3. package/dist/analyzer-UYKBXAKR.js +15 -0
  4. package/dist/auto.js +1 -1
  5. package/dist/auto.mjs +1 -1
  6. package/dist/block.js +83 -69417
  7. package/dist/calculator-LDYQCDDY.js +11 -0
  8. package/dist/charts-d3-QOZHKTCT.js +25 -0
  9. package/dist/chunk-2DXMZRWJ.js +800 -0
  10. package/dist/chunk-2LACINWN.js +786 -0
  11. package/dist/chunk-33LCUH4H.js +893 -0
  12. package/dist/chunk-3CIOJ5YJ.js +35 -0
  13. package/dist/chunk-3HDBAXF5.js +86 -0
  14. package/dist/chunk-3JLKYGBQ.js +290 -0
  15. package/dist/chunk-3MQQNXPD.js +415 -0
  16. package/dist/chunk-3R5POCTZ.js +967 -0
  17. package/dist/chunk-3SMGIJDD.js +112 -0
  18. package/dist/chunk-44CM72SQ.js +409 -0
  19. package/dist/chunk-4GRI7MEB.js +1604 -0
  20. package/dist/chunk-4OHACBGX.js +606 -0
  21. package/dist/chunk-5DH3KYFH.js +21 -0
  22. package/dist/chunk-5MLYVTYM.js +76 -0
  23. package/dist/chunk-5QXL4NYS.js +553 -0
  24. package/dist/chunk-5WRI5ZAA.js +31 -0
  25. package/dist/chunk-6EK55ART.js +117 -0
  26. package/dist/chunk-6FZYCKXB.js +2065 -0
  27. package/dist/chunk-6HO5ZTXK.js +277 -0
  28. package/dist/chunk-6KFWC4CO.js +7 -0
  29. package/dist/chunk-6MTWIQVO.js +121 -0
  30. package/dist/chunk-6NYWYNBI.js +329 -0
  31. package/dist/chunk-6SM5NJQH.js +620 -0
  32. package/dist/chunk-725LDASD.js +422 -0
  33. package/dist/chunk-76OWZ3BA.js +130 -0
  34. package/{src/block/parser.ts → dist/chunk-7JEGTRLD.js} +100 -189
  35. package/dist/chunk-A44USUV7.js +923 -0
  36. package/dist/chunk-A4GOY3PE.js +25 -0
  37. package/dist/chunk-A4VFQH5G.js +251 -0
  38. package/dist/chunk-AELOBFW5.js +375 -0
  39. package/dist/chunk-AN2MOO5T.js +522 -0
  40. package/dist/chunk-BMR4CIEO.js +41 -0
  41. package/dist/chunk-BS7SV7C3.js +51 -0
  42. package/dist/chunk-BUPDIHML.js +613 -0
  43. package/dist/chunk-BUVJFRPY.js +887 -0
  44. package/dist/chunk-C3Z4DX3V.js +112 -0
  45. package/dist/chunk-CI2G3OEB.js +1818 -0
  46. package/dist/chunk-CKNVO24D.js +54 -0
  47. package/dist/chunk-CUVHQHZ7.js +391 -0
  48. package/dist/chunk-CXK4DZH3.js +114 -0
  49. package/dist/{editor.cjs → chunk-DKEPS5LH.js} +14 -225
  50. package/dist/chunk-DU7W62N5.js +487 -0
  51. package/dist/chunk-DZAQBW35.js +751 -0
  52. package/dist/chunk-EB6FPQUP.js +316 -0
  53. package/dist/chunk-EMS2SK26.js +376 -0
  54. package/dist/chunk-ERLBPDGU.js +510 -0
  55. package/dist/chunk-EVCVDTXE.js +732 -0
  56. package/dist/chunk-FCIIHT24.js +384 -0
  57. package/dist/chunk-FFUN5D66.js +97 -0
  58. package/dist/chunk-FITOLXSX.js +714 -0
  59. package/dist/chunk-FMDSF3OV.js +68 -0
  60. package/dist/chunk-FNB3PNGF.js +410 -0
  61. package/dist/chunk-G2H74XJZ.js +2104 -0
  62. package/dist/chunk-GMR5MFHJ.js +339 -0
  63. package/dist/chunk-H6PI3KJT.js +305 -0
  64. package/dist/chunk-I46X5VUA.js +59 -0
  65. package/dist/chunk-I5IGB56C.js +255 -0
  66. package/dist/chunk-IH6ZMVQ5.js +3950 -0
  67. package/dist/chunk-IJZFN3HM.js +463 -0
  68. package/dist/chunk-IPWPWM6R.js +3079 -0
  69. package/dist/chunk-IYULMRBE.js +23 -0
  70. package/dist/chunk-IZMRON6C.js +167 -0
  71. package/dist/chunk-J5FIXQRT.js +813 -0
  72. package/dist/chunk-JED6ZDEE.js +73 -0
  73. package/dist/chunk-JEUJOX5X.js +136 -0
  74. package/dist/chunk-JM4LEPY3.js +436 -0
  75. package/dist/chunk-L3UHP6TD.js +101 -0
  76. package/dist/chunk-L66GEFB6.js +524 -0
  77. package/dist/chunk-LIOHW36Z.js +469 -0
  78. package/dist/chunk-LP7EC4MI.js +16 -0
  79. package/dist/chunk-MITI6NOL.js +548 -0
  80. package/dist/chunk-MO5YHHMQ.js +168 -0
  81. package/dist/chunk-MZSBFAPJ.js +885 -0
  82. package/dist/chunk-NNAAJVBU.js +9 -0
  83. package/dist/chunk-NOTA6TEI.js +47 -0
  84. package/dist/chunk-NURGB5Z4.js +335 -0
  85. package/dist/chunk-NX4EQS3U.js +1062 -0
  86. package/dist/chunk-NXMESY4J.js +1 -0
  87. package/dist/chunk-OJR3MQYV.js +1458 -0
  88. package/dist/chunk-OURHMODR.js +454 -0
  89. package/dist/chunk-P235SVAH.js +1057 -0
  90. package/dist/chunk-P4RH4OSP.js +1038 -0
  91. package/dist/chunk-P6T66CCZ.js +226 -0
  92. package/dist/chunk-PBAHEPLQ.js +223 -0
  93. package/dist/chunk-PBHE5UG6.js +1530 -0
  94. package/dist/chunk-PDKUANDZ.js +35 -0
  95. package/dist/chunk-PWIHZ6NJ.js +296 -0
  96. package/{src/c4/layout.ts → dist/chunk-QEBUY5NY.js} +384 -1140
  97. package/dist/chunk-QLCCJEJI.js +253 -0
  98. package/dist/chunk-QOOUC2J5.js +52 -0
  99. package/dist/chunk-QYTPPDAO.js +402 -0
  100. package/dist/chunk-R3XI2VC7.js +801 -0
  101. package/dist/chunk-RCERWOJR.js +154 -0
  102. package/dist/chunk-RJE5JTVK.js +362 -0
  103. package/dist/chunk-RTLBSQ3G.js +1155 -0
  104. package/dist/chunk-RUNNI6VE.js +448 -0
  105. package/dist/chunk-RVMRZAPB.js +354 -0
  106. package/dist/chunk-RYEGFMUO.js +955 -0
  107. package/dist/chunk-RZCF2VVM.js +528 -0
  108. package/dist/chunk-SML67VPV.js +1025 -0
  109. package/dist/chunk-SQ7IY53Q.js +44 -0
  110. package/dist/chunk-SQAG3UXH.js +161 -0
  111. package/dist/chunk-SRTU6HMQ.js +1851 -0
  112. package/dist/chunk-T2OE3YQB.js +550 -0
  113. package/dist/chunk-T3UBLACL.js +299 -0
  114. package/dist/chunk-TLVIHFRO.js +296 -0
  115. package/dist/chunk-TYS6CSS5.js +52 -0
  116. package/dist/chunk-U6EQI3U6.js +130 -0
  117. package/dist/chunk-UJ4FJLBF.js +86 -0
  118. package/dist/chunk-ULOEATBZ.js +347 -0
  119. package/dist/chunk-UNCCZ3NU.js +127 -0
  120. package/dist/chunk-USZW2HHU.js +197 -0
  121. package/dist/chunk-UWN33VNX.js +1610 -0
  122. package/dist/chunk-UZ7EZW3B.js +40 -0
  123. package/dist/chunk-VOKGH627.js +37 -0
  124. package/dist/chunk-WBRZJOEC.js +204 -0
  125. package/dist/chunk-WCSOJFU3.js +380 -0
  126. package/dist/chunk-WCT3NYCV.js +160 -0
  127. package/dist/chunk-WVAJFAIA.js +529 -0
  128. package/dist/chunk-X5LCYCL5.js +394 -0
  129. package/dist/chunk-XAKEUMEJ.js +1256 -0
  130. package/dist/chunk-Y2ZE3MSG.js +590 -0
  131. package/dist/chunk-Y3J7VR52.js +646 -0
  132. package/dist/chunk-Y5QGVTVC.js +33 -0
  133. package/dist/chunk-YAA6IID3.js +548 -0
  134. package/dist/chunk-YGJTDXVR.js +692 -0
  135. package/{src/utils/time-ticks.ts → dist/chunk-YRYKTYMQ.js} +45 -112
  136. package/dist/chunk-YYVTBZZC.js +61 -0
  137. package/dist/chunk-Z5CGSFJB.js +647 -0
  138. package/{src/gantt/calculator.ts → dist/chunk-Z72XRXD4.js} +236 -428
  139. package/dist/chunk-ZGR7ULFJ.js +978 -0
  140. package/dist/chunk-ZJEL4H2R.js +851 -0
  141. package/dist/chunk-ZO27FGJS.js +554 -0
  142. package/dist/collapse-CLPJD2RL.js +11 -0
  143. package/dist/collapse-SLINJQW6.js +9 -0
  144. package/dist/collapse-ZWUFFCK2.js +9 -0
  145. package/dist/compute-G55RG53N.js +8 -0
  146. package/dist/dgmo-router-DZCIDUO5.js +73 -0
  147. package/dist/dimensions-J3FHIMJ6.js +22 -0
  148. package/dist/element.js +1 -1
  149. package/dist/element.mjs +1 -1
  150. package/dist/flowchart-parser-ZNSKIBVR.js +21 -0
  151. package/dist/flowchart-renderer-ZAEOL4DX.js +35 -0
  152. package/dist/index.d.ts +3 -469
  153. package/dist/index.js +137 -69685
  154. package/dist/layout-46N4JJYK.js +17 -0
  155. package/dist/layout-5RA6QWR6.js +26 -0
  156. package/dist/layout-5T6RH5M6.js +14 -0
  157. package/dist/layout-7QXCNOVS.js +13 -0
  158. package/dist/layout-EE6GE46Y.js +17 -0
  159. package/dist/layout-G23F7LDH.js +9 -0
  160. package/dist/layout-HRBZPFCN.js +23 -0
  161. package/dist/layout-HWDSRX5E.js +20 -0
  162. package/dist/layout-HYZPWAK5.js +18 -0
  163. package/dist/layout-N76NR4ZO.js +19 -0
  164. package/dist/layout-OB3RI63S.js +14 -0
  165. package/dist/layout-S2QBYSK2.js +15 -0
  166. package/dist/layout-UM5F4DJB.js +15 -0
  167. package/dist/layout-search-OYV77HHS.js +1655 -0
  168. package/dist/load-data-EHDDZ5ZS.js +8 -0
  169. package/dist/palettes-PRJ2RI6D.js +56 -0
  170. package/dist/parser-2MFP2H2E.js +11 -0
  171. package/dist/parser-3STE62GB.js +21 -0
  172. package/dist/parser-4FL2CMDG.js +18 -0
  173. package/dist/parser-4OGOBJJ4.js +13 -0
  174. package/dist/parser-6BTRLKRZ.js +15 -0
  175. package/dist/parser-AQETEVRW.js +21 -0
  176. package/dist/parser-CFZD5IBL.js +12 -0
  177. package/dist/parser-FUSKFGZN.js +16 -0
  178. package/dist/parser-I6KXEYFK.js +13 -0
  179. package/dist/parser-IXSZZQHY.js +18 -0
  180. package/dist/parser-JAL2HD4I.js +13 -0
  181. package/dist/parser-KNSQSKQO.js +20 -0
  182. package/dist/parser-N5O4IHUN.js +13 -0
  183. package/dist/parser-NGU3ETO6.js +16 -0
  184. package/dist/parser-NRAI6O25.js +19 -0
  185. package/dist/parser-O5J4VBII.js +11 -0
  186. package/dist/parser-OV7NOBW6.js +21 -0
  187. package/dist/parser-PQ3POX45.js +23 -0
  188. package/dist/parser-TFZFBVWN.js +11 -0
  189. package/dist/parser-TRTHDFPP.js +18 -0
  190. package/dist/parser-VQP2UJ6W.js +11 -0
  191. package/dist/parser-XDVELHRK.js +13 -0
  192. package/dist/parser-XKRP7QKP.js +11 -0
  193. package/dist/parser-Y6WTBU76.js +14 -0
  194. package/dist/parser-ZSDPZYQP.js +12 -0
  195. package/dist/renderer-2XQQQ6QR.js +28 -0
  196. package/dist/renderer-5NDVNDAZ.js +26 -0
  197. package/dist/renderer-6EEZHMH4.js +26 -0
  198. package/dist/renderer-6W5462RB.js +43 -0
  199. package/dist/renderer-7MVLWWQD.js +22 -0
  200. package/dist/renderer-AWWAF6XD.js +33 -0
  201. package/dist/renderer-BYCNMBF7.js +34 -0
  202. package/dist/renderer-CLJHQPBJ.js +29 -0
  203. package/dist/renderer-GHPZMFU2.js +15 -0
  204. package/dist/renderer-JGBKUCZD.js +16 -0
  205. package/dist/renderer-KQB7UGDS.js +19 -0
  206. package/dist/renderer-PGX2TKVS.js +24 -0
  207. package/dist/renderer-QIWVHWZO.js +15 -0
  208. package/dist/renderer-RPOA5YPZ.js +35 -0
  209. package/dist/renderer-SMIBCRVB.js +24 -0
  210. package/dist/renderer-T7YKC5VP.js +28 -0
  211. package/dist/renderer-TOCOVLHS.js +37 -0
  212. package/dist/renderer-UWA5XS74.js +39 -0
  213. package/dist/renderer-VACVQYML.js +25 -0
  214. package/dist/renderer-W4M6QLIJ.js +37 -0
  215. package/dist/renderer-WZRREQ2P.js +23 -0
  216. package/dist/renderer-YY2OSF6E.js +27 -0
  217. package/dist/renderer-Z7I3W2PP.js +24 -0
  218. package/dist/renderer-ZGNTUWC4.js +22 -0
  219. package/dist/renderer-ZTR3DER3.js +19 -0
  220. package/dist/renderer-radial-C32GPJQ7.js +21 -0
  221. package/dist/resolver-NOAA7ID7.js +13 -0
  222. package/dist/state-collapse-HCGD6IVJ.js +8 -0
  223. package/dist/state-parser-Z2V23ATR.js +18 -0
  224. package/dist/state-renderer-RFYHSP4K.js +34 -0
  225. package/dist/themes-1-CuKpeH.d.ts +908 -0
  226. package/package.json +19 -70
  227. package/dist/advanced.cjs +0 -71648
  228. package/dist/advanced.d.cts +0 -6571
  229. package/dist/auto.cjs +0 -70648
  230. package/dist/auto.d.cts +0 -54
  231. package/dist/block.cjs +0 -70208
  232. package/dist/block.d.cts +0 -115
  233. package/dist/editor.d.cts +0 -26
  234. package/dist/element.cjs +0 -70530
  235. package/dist/element.d.cts +0 -62
  236. package/dist/highlight.cjs +0 -852
  237. package/dist/highlight.d.cts +0 -49
  238. package/dist/index.cjs +0 -70491
  239. package/dist/index.d.cts +0 -626
  240. package/dist/pert.cjs +0 -325
  241. package/dist/pert.d.cts +0 -559
  242. package/src/advanced.ts +0 -873
  243. package/src/arc/parser.ts +0 -15
  244. package/src/arc/renderer.ts +0 -633
  245. package/src/auto/index.ts +0 -621
  246. package/src/auto/shared.ts +0 -370
  247. package/src/auto/styles.ts +0 -73
  248. package/src/block/layout.ts +0 -198
  249. package/src/block/renderer.ts +0 -484
  250. package/src/block/types.ts +0 -67
  251. package/src/boxes-and-lines/collapse.ts +0 -96
  252. package/src/boxes-and-lines/focus.ts +0 -257
  253. package/src/boxes-and-lines/label-placement.ts +0 -339
  254. package/src/boxes-and-lines/layout-grouped.ts +0 -912
  255. package/src/boxes-and-lines/layout-layered.ts +0 -722
  256. package/src/boxes-and-lines/layout-search.ts +0 -1678
  257. package/src/boxes-and-lines/layout.ts +0 -398
  258. package/src/boxes-and-lines/parser.ts +0 -1181
  259. package/src/boxes-and-lines/renderer.ts +0 -1465
  260. package/src/boxes-and-lines/types.ts +0 -66
  261. package/src/c4/parser.ts +0 -1158
  262. package/src/c4/renderer.ts +0 -2008
  263. package/src/c4/types.ts +0 -89
  264. package/src/chart-type-registry.ts +0 -529
  265. package/src/chart-types.ts +0 -225
  266. package/src/chart.ts +0 -685
  267. package/src/charts-d3/bar.ts +0 -338
  268. package/src/charts-d3/chord.ts +0 -140
  269. package/src/charts-d3/function.ts +0 -147
  270. package/src/charts-d3/funnel.ts +0 -104
  271. package/src/charts-d3/heatmap.ts +0 -145
  272. package/src/charts-d3/index.ts +0 -337
  273. package/src/charts-d3/interactions.ts +0 -477
  274. package/src/charts-d3/line.ts +0 -344
  275. package/src/charts-d3/mount.ts +0 -151
  276. package/src/charts-d3/pie.ts +0 -117
  277. package/src/charts-d3/polar.ts +0 -89
  278. package/src/charts-d3/radar.ts +0 -0
  279. package/src/charts-d3/sankey.ts +0 -0
  280. package/src/charts-d3/scatter.ts +0 -185
  281. package/src/charts-d3/shared.ts +0 -208
  282. package/src/class/layout.ts +0 -326
  283. package/src/class/parser.ts +0 -635
  284. package/src/class/renderer.ts +0 -793
  285. package/src/class/types.ts +0 -58
  286. package/src/cli-banner.ts +0 -106
  287. package/src/cli.ts +0 -1080
  288. package/src/colors-diagnostics.ts +0 -35
  289. package/src/colors.ts +0 -379
  290. package/src/completion-types.ts +0 -27
  291. package/src/completion.ts +0 -2668
  292. package/src/cycle/layout.ts +0 -1026
  293. package/src/cycle/parser.ts +0 -387
  294. package/src/cycle/renderer.ts +0 -653
  295. package/src/cycle/types.ts +0 -101
  296. package/src/d3.ts +0 -1434
  297. package/src/data-chart-parser.ts +0 -1007
  298. package/src/dgmo-router.ts +0 -296
  299. package/src/diagnostics-registry.ts +0 -108
  300. package/src/diagnostics.ts +0 -0
  301. package/src/dimensions.ts +0 -20
  302. package/src/directives-registry.ts +0 -396
  303. package/src/editor/dgmo.grammar +0 -76
  304. package/src/editor/dgmo.grammar.d.ts +0 -2
  305. package/src/editor/dgmo.grammar.js +0 -18
  306. package/src/editor/dgmo.grammar.terms.d.ts +0 -5
  307. package/src/editor/dgmo.grammar.terms.js +0 -35
  308. package/src/editor/highlight-api.ts +0 -596
  309. package/src/editor/highlight.ts +0 -36
  310. package/src/editor/index.ts +0 -27
  311. package/src/editor/keywords.ts +0 -128
  312. package/src/editor/tokens.ts +0 -30
  313. package/src/element/index.ts +0 -432
  314. package/src/embed/css.ts +0 -312
  315. package/src/embed/escape.ts +0 -15
  316. package/src/embed/index.ts +0 -280
  317. package/src/er/classify.ts +0 -228
  318. package/src/er/layout.ts +0 -530
  319. package/src/er/parser.ts +0 -750
  320. package/src/er/renderer.ts +0 -725
  321. package/src/er/types.ts +0 -52
  322. package/src/error-card.ts +0 -269
  323. package/src/event-line/diagnostics.ts +0 -117
  324. package/src/event-line/parser.ts +0 -643
  325. package/src/event-line/renderer.ts +0 -1742
  326. package/src/event-line/types.ts +0 -74
  327. package/src/fonts.ts +0 -3
  328. package/src/gantt/parser.ts +0 -1641
  329. package/src/gantt/renderer.ts +0 -3787
  330. package/src/gantt/resolver.ts +0 -172
  331. package/src/gantt/types.ts +0 -206
  332. package/src/graph/diagnostics.ts +0 -64
  333. package/src/graph/edge-spline.ts +0 -29
  334. package/src/graph/flowchart-parser.ts +0 -759
  335. package/src/graph/flowchart-renderer.ts +0 -817
  336. package/src/graph/layout.ts +0 -472
  337. package/src/graph/notes.ts +0 -21
  338. package/src/graph/state-collapse.ts +0 -78
  339. package/src/graph/state-parser.ts +0 -574
  340. package/src/graph/state-renderer.ts +0 -653
  341. package/src/graph/types.ts +0 -65
  342. package/src/index.ts +0 -261
  343. package/src/infra/compute.ts +0 -1385
  344. package/src/infra/layout.ts +0 -888
  345. package/src/infra/parser.ts +0 -1234
  346. package/src/infra/renderer.ts +0 -2534
  347. package/src/infra/roles.ts +0 -75
  348. package/src/infra/types.ts +0 -216
  349. package/src/infra/validation.ts +0 -201
  350. package/src/journey-map/layout.ts +0 -490
  351. package/src/journey-map/parser.ts +0 -557
  352. package/src/journey-map/renderer.ts +0 -1580
  353. package/src/journey-map/types.ts +0 -47
  354. package/src/kanban/mutations.ts +0 -175
  355. package/src/kanban/parser.ts +0 -509
  356. package/src/kanban/renderer.ts +0 -1449
  357. package/src/kanban/types.ts +0 -39
  358. package/src/label-layout.ts +0 -368
  359. package/src/map/colorize.ts +0 -54
  360. package/src/map/completion.ts +0 -316
  361. package/src/map/context-labels.ts +0 -609
  362. package/src/map/data/PROVENANCE.json +0 -1
  363. package/src/map/data/README.md +0 -40
  364. package/src/map/data/airport-collisions.json +0 -1
  365. package/src/map/data/airports.json +0 -1
  366. package/src/map/data/gazetteer.json +0 -1
  367. package/src/map/data/lakes.json +0 -1
  368. package/src/map/data/mountain-ranges.json +0 -1
  369. package/src/map/data/na-lakes.json +0 -1
  370. package/src/map/data/na-land.json +0 -1
  371. package/src/map/data/region-names.json +0 -1
  372. package/src/map/data/rivers.json +0 -1
  373. package/src/map/data/types.ts +0 -121
  374. package/src/map/data/us-states.json +0 -1
  375. package/src/map/data/water-bodies.json +0 -1
  376. package/src/map/data/world-coarse.json +0 -1
  377. package/src/map/data/world-detail.json +0 -1
  378. package/src/map/diagnostics.ts +0 -134
  379. package/src/map/dimensions.ts +0 -150
  380. package/src/map/geo-query.ts +0 -295
  381. package/src/map/geo.ts +0 -438
  382. package/src/map/invert.ts +0 -111
  383. package/src/map/layout.ts +0 -5069
  384. package/src/map/legend-band.ts +0 -127
  385. package/src/map/load-data.ts +0 -186
  386. package/src/map/parser.ts +0 -783
  387. package/src/map/renderer.ts +0 -1445
  388. package/src/map/resolved-types.ts +0 -160
  389. package/src/map/resolver.ts +0 -1146
  390. package/src/map/types.ts +0 -194
  391. package/src/mindmap/collapse.ts +0 -67
  392. package/src/mindmap/layout.ts +0 -649
  393. package/src/mindmap/parser.ts +0 -430
  394. package/src/mindmap/renderer.ts +0 -637
  395. package/src/mindmap/text-wrap.ts +0 -218
  396. package/src/mindmap/types.ts +0 -56
  397. package/src/org/collapse.ts +0 -151
  398. package/src/org/layout.ts +0 -1262
  399. package/src/org/parser.ts +0 -519
  400. package/src/org/renderer.ts +0 -866
  401. package/src/org/resolver.ts +0 -636
  402. package/src/palettes/atlas.ts +0 -77
  403. package/src/palettes/blueprint.ts +0 -73
  404. package/src/palettes/catppuccin.ts +0 -84
  405. package/src/palettes/color-utils.ts +0 -443
  406. package/src/palettes/index.ts +0 -67
  407. package/src/palettes/nord.ts +0 -67
  408. package/src/palettes/registry.ts +0 -114
  409. package/src/palettes/slate.ts +0 -73
  410. package/src/palettes/tidewater.ts +0 -73
  411. package/src/palettes/tokyo-night.ts +0 -86
  412. package/src/palettes/types.ts +0 -88
  413. package/src/pert/analyzer.ts +0 -1183
  414. package/src/pert/diagnostics.ts +0 -110
  415. package/src/pert/index.ts +0 -43
  416. package/src/pert/internal.ts +0 -252
  417. package/src/pert/layout.ts +0 -971
  418. package/src/pert/monte-carlo.ts +0 -446
  419. package/src/pert/parser.ts +0 -1606
  420. package/src/pert/renderer.ts +0 -4009
  421. package/src/pert/share-normalize.ts +0 -62
  422. package/src/pert/types.ts +0 -604
  423. package/src/pyramid/parser.ts +0 -185
  424. package/src/pyramid/renderer.ts +0 -768
  425. package/src/pyramid/types.ts +0 -28
  426. package/src/quadrant/parser.ts +0 -18
  427. package/src/quadrant/renderer.ts +0 -638
  428. package/src/raci/diagnostics.ts +0 -213
  429. package/src/raci/index.ts +0 -23
  430. package/src/raci/mutations.ts +0 -321
  431. package/src/raci/parser.ts +0 -699
  432. package/src/raci/renderer.ts +0 -1627
  433. package/src/raci/types.ts +0 -82
  434. package/src/raci/variants.ts +0 -320
  435. package/src/render.ts +0 -268
  436. package/src/ring/parser.ts +0 -212
  437. package/src/ring/renderer.ts +0 -490
  438. package/src/ring/types.ts +0 -27
  439. package/src/sequence/collapse.ts +0 -174
  440. package/src/sequence/parser.ts +0 -1596
  441. package/src/sequence/participant-inference.ts +0 -152
  442. package/src/sequence/renderer.ts +0 -3168
  443. package/src/sequence/tag-resolution.ts +0 -164
  444. package/src/sharing.ts +0 -211
  445. package/src/sitemap/collapse.ts +0 -100
  446. package/src/sitemap/layout.ts +0 -925
  447. package/src/sitemap/parser.ts +0 -755
  448. package/src/sitemap/renderer.ts +0 -828
  449. package/src/sitemap/types.ts +0 -42
  450. package/src/slope/parser.ts +0 -18
  451. package/src/slope/renderer.ts +0 -378
  452. package/src/swimlane/diagnostics.ts +0 -94
  453. package/src/swimlane/layout.ts +0 -597
  454. package/src/swimlane/parser.ts +0 -613
  455. package/src/swimlane/renderer.ts +0 -681
  456. package/src/swimlane/types.ts +0 -151
  457. package/src/tech-radar/interactive.ts +0 -1129
  458. package/src/tech-radar/layout.ts +0 -197
  459. package/src/tech-radar/parser.ts +0 -436
  460. package/src/tech-radar/renderer.ts +0 -1239
  461. package/src/tech-radar/shared.ts +0 -151
  462. package/src/tech-radar/types.ts +0 -86
  463. package/src/themes.ts +0 -22
  464. package/src/timeline/parser.ts +0 -472
  465. package/src/timeline/renderer.ts +0 -3024
  466. package/src/timeline/types.ts +0 -33
  467. package/src/timeline/viz-parser.ts +0 -21
  468. package/src/treemap/diagnostics.ts +0 -62
  469. package/src/treemap/layout-radial.ts +0 -233
  470. package/src/treemap/layout.ts +0 -189
  471. package/src/treemap/parser.ts +0 -463
  472. package/src/treemap/renderer-radial.ts +0 -438
  473. package/src/treemap/renderer.ts +0 -572
  474. package/src/treemap/treemap-shared.ts +0 -309
  475. package/src/treemap/types.ts +0 -60
  476. package/src/utils/arrow-markers.ts +0 -51
  477. package/src/utils/arrows-diagnostics.ts +0 -62
  478. package/src/utils/arrows.ts +0 -227
  479. package/src/utils/brand.ts +0 -61
  480. package/src/utils/card.ts +0 -183
  481. package/src/utils/collapse-engine/tree-edges.ts +0 -115
  482. package/src/utils/collapse-engine/tree.ts +0 -117
  483. package/src/utils/d3-helpers.ts +0 -219
  484. package/src/utils/d3-types.ts +0 -9
  485. package/src/utils/description-helpers.ts +0 -38
  486. package/src/utils/duration.ts +0 -312
  487. package/src/utils/export-container.ts +0 -41
  488. package/src/utils/expr-eval.ts +0 -27
  489. package/src/utils/fit-canvas.ts +0 -64
  490. package/src/utils/hover-styles.ts +0 -673
  491. package/src/utils/inline-markdown.ts +0 -96
  492. package/src/utils/legend-constants.ts +0 -48
  493. package/src/utils/legend-d3.ts +0 -618
  494. package/src/utils/legend-integration.ts +0 -107
  495. package/src/utils/legend-layout.ts +0 -776
  496. package/src/utils/legend-svg.ts +0 -214
  497. package/src/utils/legend-types.ts +0 -272
  498. package/src/utils/name-normalize.ts +0 -163
  499. package/src/utils/note-box/constants.ts +0 -25
  500. package/src/utils/note-box/index.ts +0 -11
  501. package/src/utils/note-box/metrics.ts +0 -90
  502. package/src/utils/note-box/svg.ts +0 -331
  503. package/src/utils/notes/bounds.ts +0 -30
  504. package/src/utils/notes/build.ts +0 -131
  505. package/src/utils/notes/index.ts +0 -18
  506. package/src/utils/notes/model.ts +0 -19
  507. package/src/utils/notes/parse.ts +0 -131
  508. package/src/utils/notes/place.ts +0 -177
  509. package/src/utils/notes/resolve.ts +0 -88
  510. package/src/utils/number-format.ts +0 -36
  511. package/src/utils/parsing.ts +0 -953
  512. package/src/utils/reserved-key-registry.ts +0 -239
  513. package/src/utils/safe-href.ts +0 -38
  514. package/src/utils/scaling.ts +0 -97
  515. package/src/utils/svg-embed.ts +0 -251
  516. package/src/utils/tag-groups.ts +0 -607
  517. package/src/utils/text-measure.ts +0 -122
  518. package/src/utils/title-constants.ts +0 -24
  519. package/src/utils/visual-conventions.ts +0 -61
  520. package/src/utils/wrapped-desc.ts +0 -87
  521. package/src/venn/parser.ts +0 -18
  522. package/src/venn/renderer.ts +0 -1047
  523. package/src/version-control/diagnostics.ts +0 -67
  524. package/src/version-control/parser.ts +0 -544
  525. package/src/version-control/renderer.ts +0 -724
  526. package/src/version-control/types.ts +0 -100
  527. package/src/visualizations/parse.ts +0 -1560
  528. package/src/visualizations/types.ts +0 -262
  529. package/src/wireframe/layout.ts +0 -479
  530. package/src/wireframe/parser.ts +0 -1114
  531. package/src/wireframe/renderer.ts +0 -1318
  532. package/src/wireframe/types.ts +0 -110
  533. package/src/wordcloud/parser.ts +0 -18
  534. package/src/wordcloud/renderer.ts +0 -357
@@ -1,1465 +0,0 @@
1
- // ============================================================
2
- // Boxes and Lines Diagram — D3 SVG Renderer
3
- // ============================================================
4
-
5
- import * as d3Selection from 'd3-selection';
6
- import {
7
- renderNoteBox,
8
- renderNoteConnector,
9
- renderNoteBadge,
10
- noteConnectorPoints,
11
- NOTE_BADGE_RADIUS,
12
- } from '../utils/note-box';
13
- import * as d3Shape from 'd3-shape';
14
- import { FONT_FAMILY } from '../fonts';
15
- import { renderIntegratedLegend } from '../utils/legend-integration';
16
- import { getMaxLegendReservedHeight } from '../utils/legend-layout';
17
- import type {
18
- LegendCallbacks,
19
- LegendGroupData,
20
- ControlsGroupToggle,
21
- } from '../utils/legend-types';
22
- import {
23
- TITLE_FONT_SIZE,
24
- TITLE_FONT_WEIGHT,
25
- TITLE_Y,
26
- } from '../utils/title-constants';
27
- import {
28
- contrastText,
29
- mix,
30
- relativeLuminance,
31
- shapeFill,
32
- valueRampColor,
33
- themeBaseBg,
34
- } from '../palettes/color-utils';
35
- import { resolveColor } from '../colors';
36
- import { resolveTagColor } from '../utils/tag-groups';
37
- import type { TagGroup } from '../utils/tag-groups';
38
- import type { PaletteColors } from '../palettes';
39
- import { renderInlineText } from '../utils/inline-markdown';
40
- import {
41
- wrapDescriptionLines,
42
- type WrappedDescLine,
43
- } from '../utils/wrapped-desc';
44
- import type { ParsedBoxesAndLines, BLNode } from './types';
45
- import type { BLLayoutResult, BLLayoutNode, BLLayoutEdge } from './layout';
46
- import { ScaleContext } from '../utils/scaling';
47
- import {
48
- CHAR_WIDTH_RATIO,
49
- measureText,
50
- truncateText,
51
- } from '../utils/text-measure';
52
-
53
- // ── Constants (aligned with infra pattern) ─────────────────
54
- const DIAGRAM_PADDING = 20;
55
- // Box labels run smaller than the 13px org/infra use — boxes-and-lines nodes are
56
- // narrower (~97px), so a smaller label fits more text per line before wrapping.
57
- const NODE_FONT_SIZE = 11;
58
- const MIN_NODE_FONT_SIZE = 9;
59
- const EDGE_LABEL_FONT_SIZE = 11;
60
- import {
61
- EDGE_STROKE_WIDTH,
62
- NODE_STROKE_WIDTH,
63
- } from '../utils/visual-conventions'; // shared (Story 111.1)
64
- const NODE_RX = 8;
65
- // Intentional deviation (conventions §3): boxes-and-lines uses a 4px collapse
66
- // bar (and 4px separator gap in layout.ts) — denser than the 6px default.
67
- const COLLAPSE_BAR_HEIGHT = 4;
68
- const ARROWHEAD_W = 5;
69
- const ARROWHEAD_H = 4;
70
- const DESC_FONT_SIZE = 10; // matches infra META_FONT_SIZE
71
- const DESC_LINE_HEIGHT = 1.4; // 14px row height at 10px font (matches infra META_LINE_HEIGHT)
72
- const MAX_DESC_LINES = 6;
73
- const NODE_TEXT_PADDING = 12;
74
- const GROUP_RX = 8;
75
- const GROUP_LABEL_FONT_SIZE = 14;
76
- const GROUP_LABEL_ZONE = 32;
77
- // % tint floor so the ramp minimum still reads as "low, present" (mirror map).
78
- const RAMP_FLOOR = 15;
79
- const VALUE_FONT_SIZE = 11;
80
-
81
- type D3Svg = d3Selection.Selection<SVGSVGElement, unknown, null, undefined>;
82
-
83
- // ── Edge path generators ───────────────────────────────────
84
- const lineGeneratorLR = d3Shape
85
- .line<{ x: number; y: number }>()
86
- .x((d) => d.x)
87
- .y((d) => d.y)
88
- .curve(d3Shape.curveBasis);
89
-
90
- const lineGeneratorTB = d3Shape
91
- .line<{ x: number; y: number }>()
92
- .x((d) => d.x)
93
- .y((d) => d.y)
94
- .curve(d3Shape.curveBasis);
95
-
96
- // Straight (linear) generator for pinned-layout connectors (Canvas Editor
97
- // spike). curveBasis collapses a 2-point polyline, so border-clipped straight
98
- // edges must draw linearly.
99
- const lineGeneratorStraight = d3Shape
100
- .line<{ x: number; y: number }>()
101
- .x((d) => d.x)
102
- .y((d) => d.y)
103
- .curve(d3Shape.curveLinear);
104
-
105
- // ── Text fitting ───────────────────────────────────────────
106
-
107
- function splitCamelCase(word: string): string[] {
108
- const parts: string[] = [];
109
- let start = 0;
110
- for (let i = 1; i < word.length; i++) {
111
- // In-bounds by loop guard (i >= 1 and i < word.length).
112
- const prev = word[i - 1]!;
113
- const curr = word[i]!;
114
- const next = i + 1 < word.length ? word[i + 1]! : '';
115
- const lowerToUpper =
116
- prev >= 'a' && prev <= 'z' && curr >= 'A' && curr <= 'Z';
117
- const upperRunEnd =
118
- prev >= 'A' &&
119
- prev <= 'Z' &&
120
- curr >= 'A' &&
121
- curr <= 'Z' &&
122
- next >= 'a' &&
123
- next <= 'z';
124
- if (lowerToUpper || upperRunEnd) {
125
- parts.push(word.slice(start, i));
126
- start = i;
127
- }
128
- }
129
- parts.push(word.slice(start));
130
- return parts.length > 1 ? parts : [word];
131
- }
132
-
133
- /**
134
- * Fit a label into a header zone for described nodes.
135
- * Strategy: split first (spaces, dashes, camelCase), wrap into lines,
136
- * shrink font if needed, truncate individual lines with "…" — never hard-break.
137
- */
138
- function fitLabelToHeader(
139
- label: string,
140
- nodeWidth: number,
141
- maxLines: number
142
- ): { lines: string[]; fontSize: number } {
143
- const maxTextWidth = nodeWidth - NODE_TEXT_PADDING * 2;
144
-
145
- // Split on spaces and dashes, then camelCase split each part
146
- const rawParts = label.split(/(\s+|-)/);
147
- const words: string[] = [];
148
- for (const part of rawParts) {
149
- if (!part || /^\s+$/.test(part) || part === '-') continue;
150
- words.push(...splitCamelCase(part));
151
- }
152
-
153
- for (
154
- let fontSize = NODE_FONT_SIZE;
155
- fontSize >= MIN_NODE_FONT_SIZE;
156
- fontSize--
157
- ) {
158
- if (maxTextWidth < measureText('MM', fontSize)) continue;
159
-
160
- // Wrap words into lines (greedy, by measured pixel width)
161
- const lines: string[] = [];
162
- let current = '';
163
- for (const word of words) {
164
- const test = current ? `${current} ${word}` : word;
165
- if (measureText(test, fontSize) <= maxTextWidth) {
166
- current = test;
167
- } else {
168
- if (current) lines.push(current);
169
- current = word;
170
- }
171
- }
172
- if (current) lines.push(current);
173
-
174
- const fits = (l: string): boolean =>
175
- measureText(l, fontSize) <= maxTextWidth;
176
-
177
- // All lines fit at this font? Done.
178
- if (lines.length <= maxLines && lines.every(fits)) {
179
- return { lines, fontSize };
180
- }
181
-
182
- // Lines fit in count but some are too wide? Truncate those lines.
183
- if (lines.length <= maxLines) {
184
- const result = lines.map((l) =>
185
- fits(l) ? l : truncateText(l, fontSize, maxTextWidth)
186
- );
187
- return { lines: result, fontSize };
188
- }
189
-
190
- // Too many lines — take first maxLines, truncate last + any oversized
191
- const result = lines
192
- .slice(0, maxLines)
193
- .map((l) => (fits(l) ? l : truncateText(l, fontSize, maxTextWidth)));
194
- // In-bounds: result has exactly maxLines entries (from .slice(0, maxLines)).
195
- const last = result[maxLines - 1]!;
196
- if (!last.endsWith('\u2026')) {
197
- result[maxLines - 1] = truncateText(
198
- last + '\u2026',
199
- fontSize,
200
- maxTextWidth
201
- );
202
- }
203
- return { lines: result, fontSize };
204
- }
205
-
206
- // Fallback at min font
207
- const truncated = truncateText(label, MIN_NODE_FONT_SIZE, maxTextWidth);
208
- return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
209
- }
210
-
211
- // ── Color helpers ──────────────────────────────────────────
212
-
213
- function nodeColors(
214
- node: BLNode,
215
- tagGroups: readonly TagGroup[],
216
- activeGroupName: string | null,
217
- palette: PaletteColors,
218
- isDark: boolean,
219
- value: {
220
- active: boolean;
221
- hue: string;
222
- /** Two explicit endpoint colours (`heat Risk green red`). When set, the
223
- * value's position on the ramp is carried by HUE, so the box follows the
224
- * STANDARD box convention (solid colour outline + 25% faded fill) rather than
225
- * the map's saturated choropleth fill. A single-colour ramp encodes value by
226
- * saturation/lightness and keeps the choropleth fill (no hue to spare). */
227
- twoColor: boolean;
228
- fillForValue: (v: number) => string;
229
- },
230
- solid?: boolean
231
- ): { fill: string; stroke: string; text: string } {
232
- // Untagged-neutral fill, reused by the value path for no-value boxes.
233
- const neutralFill = mix(palette.bg, palette.text, isDark ? 90 : 95);
234
- if (value.active) {
235
- if (node.value === undefined) {
236
- // No-value box: neutral fill, ramp-hue stroke (present so the app's
237
- // --bl-node-stroke hover-dim still works).
238
- const text = contrastText(
239
- neutralFill,
240
- palette.textOnFillLight,
241
- palette.textOnFillDark
242
- );
243
- return { fill: neutralFill, stroke: value.hue, text };
244
- }
245
- // Value box: render the ramp colour like any tagged box — a 25% faded
246
- // (muted) fill + a solid colour outline; `solid-fill` opts into the full
247
- // fill. The outline differs by ramp kind: a two-colour ramp carries value by
248
- // HUE, so each box's outline is its own ramp colour (red→green); a
249
- // single-colour ramp has one hue, so the outline is the constant ramp hue and
250
- // value reads from the muted fill depth.
251
- const rampColor = value.fillForValue(node.value);
252
- const fill = shapeFill(palette, rampColor, isDark, {
253
- ...(solid !== undefined && { solid }),
254
- });
255
- const stroke = value.twoColor ? rampColor : value.hue;
256
- const text = contrastText(
257
- fill,
258
- palette.textOnFillLight,
259
- palette.textOnFillDark
260
- );
261
- return { fill, stroke, text };
262
- }
263
- const tagColor = resolveTagColor(
264
- node.metadata,
265
- [...tagGroups],
266
- activeGroupName
267
- );
268
- if (tagColor) {
269
- const fill = shapeFill(palette, tagColor, isDark, {
270
- ...(solid !== undefined && { solid }),
271
- });
272
- const stroke = tagColor;
273
- const text = contrastText(
274
- fill,
275
- palette.textOnFillLight,
276
- palette.textOnFillDark
277
- );
278
- return { fill, stroke, text };
279
- }
280
- // Untagged fallback (subtle-neutral — out of scope per TD-2 / F2;
281
- // intentionally near-invisible so default nodes recede).
282
- const fill = mix(palette.bg, palette.text, isDark ? 90 : 95);
283
- const stroke = mix(palette.text, palette.bg, isDark ? 60 : 40);
284
- const text = palette.text;
285
- return { fill, stroke, text };
286
- }
287
-
288
- function edgeColor(
289
- edge: BLLayoutEdge,
290
- tagGroups: readonly TagGroup[],
291
- activeGroupName: string | null,
292
- palette: PaletteColors
293
- ): string {
294
- // Only color edges that have explicit tag metadata — otherwise neutral
295
- const hasTagMeta =
296
- Object.keys(edge.metadata).length > 0 && activeGroupName != null;
297
- if (hasTagMeta) {
298
- const tagColor = resolveTagColor(
299
- edge.metadata,
300
- [...tagGroups],
301
- activeGroupName
302
- );
303
- if (tagColor) return tagColor;
304
- }
305
- return palette.textMuted;
306
- }
307
-
308
- // ── Arrowhead markers ──────────────────────────────────────
309
-
310
- function ensureArrowMarkers(
311
- defs: d3Selection.Selection<SVGDefsElement, unknown, null, undefined>,
312
- colors: Set<string>
313
- ): void {
314
- for (const color of colors) {
315
- const id = `bl-arrow-${color.replace('#', '')}`;
316
- if (!defs.select(`#${id}`).empty()) continue;
317
- defs
318
- .append('marker')
319
- .attr('id', id)
320
- .attr('viewBox', `0 0 ${ARROWHEAD_W * 2} ${ARROWHEAD_H * 2}`)
321
- .attr('refX', ARROWHEAD_W * 2)
322
- .attr('refY', ARROWHEAD_H)
323
- .attr('markerWidth', ARROWHEAD_W)
324
- .attr('markerHeight', ARROWHEAD_H)
325
- .attr('orient', 'auto')
326
- .append('polygon')
327
- .attr(
328
- 'points',
329
- `0,0 ${ARROWHEAD_W * 2},${ARROWHEAD_H} 0,${ARROWHEAD_H * 2}`
330
- )
331
- .attr('fill', color);
332
-
333
- // Reverse marker for bidirectional
334
- const revId = `bl-arrow-rev-${color.replace('#', '')}`;
335
- if (!defs.select(`#${revId}`).empty()) continue;
336
- defs
337
- .append('marker')
338
- .attr('id', revId)
339
- .attr('viewBox', `0 0 ${ARROWHEAD_W * 2} ${ARROWHEAD_H * 2}`)
340
- .attr('refX', 0)
341
- .attr('refY', ARROWHEAD_H)
342
- .attr('markerWidth', ARROWHEAD_W)
343
- .attr('markerHeight', ARROWHEAD_H)
344
- .attr('orient', 'auto')
345
- .append('polygon')
346
- .attr(
347
- 'points',
348
- `${ARROWHEAD_W * 2},0 0,${ARROWHEAD_H} ${ARROWHEAD_W * 2},${ARROWHEAD_H * 2}`
349
- )
350
- .attr('fill', color);
351
- }
352
- }
353
-
354
- // ── Main render function ───────────────────────────────────
355
-
356
- interface BLRenderOptions {
357
- onClickItem?: (lineNumber: number) => void;
358
- exportDims?: { width?: number; height?: number };
359
- activeTagGroup?: string | null;
360
- hiddenTagValues?: Map<string, Set<string>>;
361
- hideDescriptions?: boolean;
362
- controlsExpanded?: boolean;
363
- onToggleDescriptions?: (active: boolean) => void;
364
- onToggleControlsExpand?: () => void;
365
- exportMode?: boolean;
366
- /** When 'app', the description toggle is hosted by the app overlay strip
367
- * (inline gear suppressed, controls row + anchor reserved). */
368
- controlsHost?: 'app' | 'inline';
369
- /** Explicit value-ramp domain override. When provided, the choropleth ramp
370
- * uses these endpoints instead of computing min/max from `parsed.nodes`.
371
- * Focus mode passes the GLOBAL (pre-filter) domain so neighbor colours stay
372
- * stable when only a subset is rendered (Decision 20 / FM1). */
373
- rampDomain?: { min: number; max: number };
374
- }
375
-
376
- export function renderBoxesAndLines(
377
- container: HTMLDivElement,
378
- parsed: ParsedBoxesAndLines,
379
- layout: BLLayoutResult,
380
- palette: PaletteColors,
381
- isDark: boolean,
382
- options?: BLRenderOptions
383
- ): void {
384
- const {
385
- onClickItem,
386
- exportDims,
387
- activeTagGroup,
388
- hiddenTagValues,
389
- hideDescriptions,
390
- controlsExpanded,
391
- onToggleDescriptions,
392
- onToggleControlsExpand,
393
- exportMode = false,
394
- controlsHost,
395
- rampDomain,
396
- } = options ?? {};
397
- d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
398
-
399
- const width = exportDims?.width ?? container.clientWidth;
400
- const height = exportDims?.height ?? container.clientHeight;
401
- if (width <= 0 || height <= 0) return;
402
-
403
- const sctx = ScaleContext.identity();
404
-
405
- const sDiagramPadding = sctx.aesthetic(DIAGRAM_PADDING);
406
- const sMinNodeFontSize = sctx.text(MIN_NODE_FONT_SIZE);
407
- const sEdgeLabelFontSize = sctx.text(EDGE_LABEL_FONT_SIZE);
408
- const sEdgeStrokeWidth = sctx.structural(EDGE_STROKE_WIDTH);
409
- const sNodeStrokeWidth = sctx.structural(NODE_STROKE_WIDTH);
410
- const sCollapseBarHeight = sctx.structural(COLLAPSE_BAR_HEIGHT);
411
- const sDescFontSize = sctx.text(DESC_FONT_SIZE);
412
- const sGroupLabelFontSize = sctx.text(GROUP_LABEL_FONT_SIZE);
413
- const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
414
- const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
415
- const sTitleY = sctx.structural(TITLE_Y);
416
-
417
- // ── Value ramp + active-dimension resolution (mirror of map's value model) ──
418
- // The ramp is computed in the renderer (architectural divergence from the
419
- // map, which precomputes in layout) — node sizes are value-independent, and
420
- // this file already owns all colouring + the legend build. Hoisted ONCE
421
- // before the node loop so `fillForValue` is not recomputed per node.
422
- const nodeValues = parsed.nodes
423
- .filter((n) => n.value !== undefined)
424
- .map((n) => n.value!);
425
- const hasRamp = nodeValues.length > 0;
426
- // Anchor the low end at the lowest value (not 0) to maximise within-diagram
427
- // dynamic range; mirrors the map's region-heat ramp. Equal-value data
428
- // (rampMin === rampMax) falls back to t = 1 in fillForValue below.
429
- // A caller-supplied domain (focus mode) wins so colours don't shift when a
430
- // subset is rendered; otherwise derive from the nodes on screen.
431
- const rampMin = rampDomain?.min ?? (hasRamp ? Math.min(...nodeValues) : 0);
432
- const rampMax = rampDomain?.max ?? Math.max(...nodeValues);
433
- // Default hue = palette.primary (NOT red like the map — boxes have no water to
434
- // stand out against, and red reads as alarm on a neutral metric). A trailing
435
- // color on `heat` overrides.
436
- const rampHue =
437
- resolveColor(parsed.boxMetricColor ?? '', palette) ?? palette.primary;
438
- // Explicit LOW endpoint (`heat Risk green red`); absent ⇒ single-colour
439
- // (neutral low). Only recognized names peel, so resolveColor always succeeds.
440
- const rampLow = parsed.boxMetricLowColor
441
- ? (resolveColor(parsed.boxMetricLowColor, palette) ?? undefined)
442
- : undefined;
443
- // Lift the ramp anchor off the near-black surface on dark themes so the
444
- // lowest values read as a clear muted tint rather than sinking to the surface.
445
- const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
446
- const rampLowFloor = mix(rampHue, rampBase, RAMP_FLOOR);
447
- const fillForValue = (v: number): string => {
448
- const t = rampMax > rampMin ? (v - rampMin) / (rampMax - rampMin) : 1;
449
- // Two-colour ramp: shared low→high interpolation (direct or via midpoint).
450
- if (rampLow !== undefined)
451
- return valueRampColor(rampLow, rampHue, t, { isDark });
452
- // Single/zero-colour: byte-identical to pre-change (same numeric pct, no
453
- // float round-trip).
454
- const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
455
- return mix(rampHue, rampBase, pct);
456
- };
457
- const VALUE_NAME = hasRamp ? parsed.boxMetric?.trim() || 'Value' : null;
458
-
459
- // Local active-dimension resolver — mirror map's inline matchColorGroup /
460
- // activeIsScore. Do NOT extend the shared resolveActiveTagGroup (it has a
461
- // fixed 3-arg signature consumed by 7 chart types). On a name collision
462
- // between a tag group and the metric label, the tag group wins (AC9).
463
- const matchColorGroup = (v: string): string | null => {
464
- const lv = v.trim().toLowerCase();
465
- if (lv === '' || lv === 'none') return null;
466
- const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
467
- if (tg) return tg.name;
468
- if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
469
- return v; // unknown name passes through → renders neutral
470
- };
471
- const override = activeTagGroup; // string | null | undefined
472
- let activeGroup: string | null;
473
- if (override !== undefined) {
474
- activeGroup = override === null ? null : matchColorGroup(override);
475
- } else if (parsed.options['active-tag'] !== undefined) {
476
- activeGroup = matchColorGroup(parsed.options['active-tag']);
477
- } else {
478
- // Default-active dimension: value ramp when any box has a value, else the
479
- // first declared tag group, else none.
480
- activeGroup =
481
- VALUE_NAME ??
482
- (parsed.tagGroups.length > 0 ? parsed.tagGroups[0]!.name : null);
483
- }
484
- const activeIsValue = VALUE_NAME !== null && activeGroup === VALUE_NAME;
485
-
486
- // Synthetic legend group for the value ramp (empty entries + gradient),
487
- // prepended to the tag groups handed to renderLegendD3 — exactly like the
488
- // map's VALUE_NAME group. The shared legend infra renders the gradient capsule
489
- // ONLY when it is the active group (legendState.activeGroup === its name).
490
- const valueGroup: LegendGroupData | null =
491
- VALUE_NAME !== null
492
- ? {
493
- name: VALUE_NAME,
494
- entries: [],
495
- gradient: {
496
- min: rampMin,
497
- max: rampMax,
498
- low: rampLow ?? rampLowFloor,
499
- high: rampHue,
500
- },
501
- }
502
- : null;
503
- const legendGroups: readonly LegendGroupData[] = [
504
- ...(valueGroup ? [valueGroup] : []),
505
- ...parsed.tagGroups,
506
- ];
507
-
508
- // Reserve legend height only when a legend will actually render. App-hosted
509
- // controls move the Descriptions toggle to the app overlay, so a
510
- // descriptions-only chart (no tag groups) reserves nothing.
511
- const reserveHasDescriptions = parsed.nodes.some(
512
- (n) => n.description && n.description.length > 0
513
- );
514
- const willRenderLegend =
515
- legendGroups.length > 0 ||
516
- (reserveHasDescriptions && controlsHost !== 'app');
517
- const sLegendHeight = willRenderLegend
518
- ? sctx.structural(
519
- getMaxLegendReservedHeight(
520
- {
521
- groups: legendGroups,
522
- position: { placement: 'top-center', titleRelation: 'below-title' },
523
- mode: exportMode ? 'export' : 'preview',
524
- },
525
- width
526
- )
527
- )
528
- : 0;
529
-
530
- // Build hidden set
531
- const hidden = hiddenTagValues ?? parsed.initialHiddenTagValues;
532
-
533
- // Build node lookup
534
- const nodeMap = new Map<string, BLNode>();
535
- for (const node of parsed.nodes) nodeMap.set(node.label, node);
536
-
537
- // Build layout node lookup
538
- const layoutNodeMap = new Map<string, BLLayoutNode>();
539
- for (const ln of layout.nodes) layoutNodeMap.set(ln.label, ln);
540
-
541
- // Compute diagram bounds for scaling
542
- const showTitle = !!parsed.title && parsed.options['no-title'] !== 'on';
543
- const titleOffset = showTitle ? 40 : 0;
544
- const hasAnyDescriptions = parsed.nodes.some(
545
- (n) => n.description && n.description.length > 0
546
- );
547
- const needsLegend =
548
- legendGroups.length > 0 || (hasAnyDescriptions && onToggleDescriptions);
549
- const legendH = needsLegend ? sLegendHeight + 8 : 0;
550
-
551
- const groupLabelsSet = new Set(layout.groups.map((g) => g.label));
552
- let labelZoneExtension = 0;
553
- for (const group of parsed.groups) {
554
- if (group.children.some((c) => groupLabelsSet.has(c))) {
555
- labelZoneExtension += sGroupLabelZone;
556
- }
557
- }
558
-
559
- const contentW = layout.width;
560
- const contentH = layout.height + titleOffset + legendH + labelZoneExtension;
561
-
562
- const scaleX = width / (contentW + sDiagramPadding * 2);
563
- const scaleY = height / (contentH + sDiagramPadding * 2);
564
- const scale = Math.min(scaleX, scaleY, 3);
565
-
566
- // Pinned (`layout`-block) coordinates can leave the content's real extent
567
- // sitting off-centre inside layout.width/height — e.g. nodes pinned far from
568
- // the origin bake a wide gap on one side. Re-centre the content within its
569
- // own box by equalizing opposite margins. Gated to pinned diagrams so the
570
- // auto-layout path (and its snapshots) stays byte-identical: that path
571
- // already produces symmetric margins.
572
- let centerShiftX = 0;
573
- let centerShiftY = 0;
574
- if (parsed.nodePositions && parsed.nodePositions.size > 0) {
575
- let bMinX = Infinity,
576
- bMinY = Infinity,
577
- bMaxX = -Infinity,
578
- bMaxY = -Infinity;
579
- const accB = (x: number, y: number) => {
580
- if (x < bMinX) bMinX = x;
581
- if (x > bMaxX) bMaxX = x;
582
- if (y < bMinY) bMinY = y;
583
- if (y > bMaxY) bMaxY = y;
584
- };
585
- for (const n of layout.nodes) {
586
- accB(n.x - n.width / 2, n.y - n.height / 2);
587
- accB(n.x + n.width / 2, n.y + n.height / 2);
588
- }
589
- for (const g of layout.groups) {
590
- accB(g.x - g.width / 2, g.y - g.height / 2);
591
- accB(g.x + g.width / 2, g.y + g.height / 2);
592
- }
593
- for (const e of layout.edges) for (const p of e.points) accB(p.x, p.y);
594
- if (Number.isFinite(bMinX)) {
595
- centerShiftX = (layout.width - bMaxX - bMinX) / 2;
596
- centerShiftY = (layout.height - bMaxY - bMinY) / 2;
597
- }
598
- }
599
-
600
- const offsetX = (width - contentW * scale) / 2 + centerShiftX * scale;
601
- const offsetY =
602
- sDiagramPadding + titleOffset + legendH + centerShiftY * scale;
603
-
604
- const svg: D3Svg = d3Selection
605
- .select(container)
606
- .append('svg')
607
- .attr('width', width)
608
- .attr('height', height)
609
- .attr('viewBox', `0 0 ${width} ${height}`)
610
- .attr('preserveAspectRatio', 'xMidYMin meet')
611
- .style('font-family', FONT_FAMILY)
612
- .style('background', palette.bg);
613
-
614
- if (sctx.isBelowFloor) {
615
- svg.attr('width', '100%');
616
- }
617
-
618
- const defs = svg.append('defs');
619
-
620
- if (showTitle) {
621
- svg
622
- .append('text')
623
- .attr('x', width / 2)
624
- .attr('y', sTitleY)
625
- .attr('text-anchor', 'middle')
626
- .attr('font-size', sTitleFontSize)
627
- .attr('font-weight', TITLE_FONT_WEIGHT)
628
- .attr('fill', palette.text)
629
- .text(parsed.title);
630
- }
631
-
632
- // Main diagram group with scaling
633
- const diagramG = svg
634
- .append('g')
635
- .attr('transform', `translate(${offsetX},${offsetY}) scale(${scale})`);
636
-
637
- // Collect all edge colors for arrowhead markers
638
- const arrowColors = new Set<string>();
639
- const edgeColorMap = new Map<number, string>();
640
- for (let i = 0; i < layout.edges.length; i++) {
641
- const c = edgeColor(
642
- // In-bounds by loop guard.
643
- layout.edges[i]!,
644
- parsed.tagGroups,
645
- activeGroup,
646
- palette
647
- );
648
- arrowColors.add(c);
649
- edgeColorMap.set(i, c);
650
- }
651
- ensureArrowMarkers(defs, arrowColors);
652
-
653
- // ── Render groups (bottom layer, largest first for nesting) ──
654
- const sortedGroups = [...layout.groups].sort(
655
- (a, b) => b.width * b.height - a.width * a.height
656
- );
657
- // Identify groups that contain sub-groups — only those need extra label space
658
- const groupLabels = new Set(layout.groups.map((g) => g.label));
659
- const hasSubGroups = new Set<string>();
660
- for (const group of parsed.groups) {
661
- for (const child of group.children) {
662
- if (groupLabels.has(child)) hasSubGroups.add(group.label);
663
- }
664
- }
665
-
666
- for (const group of sortedGroups) {
667
- const gx = group.x - group.width / 2;
668
- // Only extend top for groups that contain sub-groups (dagre under-pads these)
669
- const needsExtra = !group.collapsed && hasSubGroups.has(group.label);
670
- const gy = needsExtra
671
- ? group.y - group.height / 2 - sGroupLabelZone
672
- : group.y - group.height / 2;
673
- const groupHeight = needsExtra
674
- ? group.height + sGroupLabelZone
675
- : group.height;
676
-
677
- const groupG = diagramG
678
- .append('g')
679
- .attr(
680
- 'class',
681
- group.collapsed ? 'bl-group bl-group-collapsed' : 'bl-group'
682
- )
683
- .attr('data-line-number', String(group.lineNumber))
684
- .attr('data-node-id', group.label)
685
- .attr('data-group-toggle', group.label)
686
- .style('cursor', 'pointer');
687
-
688
- if (group.collapsed) {
689
- // Collapsed: solid rounded rect matching node style + 6px collapse bar
690
- const fillColor = themeBaseBg(palette, isDark);
691
- const strokeColor = palette.border;
692
-
693
- groupG
694
- .append('rect')
695
- .attr('x', gx)
696
- .attr('y', gy)
697
- .attr('width', group.width)
698
- .attr('height', group.height)
699
- .attr('rx', NODE_RX)
700
- .attr('ry', NODE_RX)
701
- .attr('fill', fillColor)
702
- .attr('stroke', strokeColor)
703
- .attr('stroke-width', sNodeStrokeWidth);
704
-
705
- const clipId = `bl-clip-${group.label.replace(/[[\]\s]/g, '')}`;
706
- groupG
707
- .append('clipPath')
708
- .attr('id', clipId)
709
- .append('rect')
710
- .attr('x', gx)
711
- .attr('y', gy)
712
- .attr('width', group.width)
713
- .attr('height', group.height)
714
- .attr('rx', NODE_RX);
715
- groupG
716
- .append('rect')
717
- .attr('x', gx)
718
- .attr('y', gy + group.height - sCollapseBarHeight)
719
- .attr('width', group.width)
720
- .attr('height', sCollapseBarHeight)
721
- .attr('fill', strokeColor)
722
- .attr('clip-path', `url(#${clipId})`)
723
- .attr('class', 'bl-collapse-bar');
724
-
725
- const maxLabelLines = Math.max(
726
- 2,
727
- Math.floor((group.height - 16) / (sMinNodeFontSize * 1.3))
728
- );
729
- const fitted = fitLabelToHeader(group.label, group.width, maxLabelLines);
730
- const lineH = fitted.fontSize * 1.3;
731
- const totalH = fitted.lines.length * lineH;
732
- for (let li = 0; li < fitted.lines.length; li++) {
733
- groupG
734
- .append('text')
735
- .attr('class', 'bl-group-label')
736
- .attr('x', group.x)
737
- .attr('y', group.y - totalH / 2 + lineH / 2 + li * lineH)
738
- .attr('text-anchor', 'middle')
739
- .attr('dominant-baseline', 'central')
740
- .attr('font-family', FONT_FAMILY)
741
- .attr('font-size', fitted.fontSize)
742
- .attr('font-weight', '600')
743
- .attr('fill', palette.text)
744
- // In-bounds by loop guard.
745
- .text(fitted.lines[li]!);
746
- }
747
- } else {
748
- // Expanded: background container with label
749
- groupG
750
- .append('rect')
751
- .attr('x', gx)
752
- .attr('y', gy)
753
- .attr('width', group.width)
754
- .attr('height', groupHeight)
755
- .attr('rx', GROUP_RX)
756
- .attr('ry', GROUP_RX)
757
- .attr('fill', mix(palette.surface, palette.bg, 40))
758
- .attr('stroke', palette.textMuted)
759
- .attr('stroke-width', 1)
760
- .attr('stroke-opacity', 0.35);
761
-
762
- groupG
763
- .append('text')
764
- .attr('class', 'bl-group-label')
765
- .attr('x', gx + group.width / 2)
766
- .attr('y', gy + 18)
767
- .attr('text-anchor', 'middle')
768
- .attr('font-family', FONT_FAMILY)
769
- .attr('font-size', sGroupLabelFontSize)
770
- .attr('font-weight', '600')
771
- .attr('fill', palette.text)
772
- .text(group.label);
773
- }
774
- }
775
-
776
- // ── Render edges ───────────────────────────────────────
777
- // Edge labels are placed in layout (label-placement.ts) and drawn AFTER the
778
- // node loop (see the bl-edge-labels layer) so boxes never clip them.
779
- for (let i = 0; i < layout.edges.length; i++) {
780
- // In-bounds by loop guard.
781
- const le = layout.edges[i]!;
782
- const color = edgeColorMap.get(i) ?? palette.textMuted;
783
-
784
- // Check if hidden
785
- if (hidden.size > 0) {
786
- let isHidden = false;
787
- for (const [groupKey, hiddenVals] of hidden) {
788
- const val = le.metadata[groupKey];
789
- if (val && hiddenVals.has(val.toLowerCase())) {
790
- isHidden = true;
791
- break;
792
- }
793
- }
794
- if (isHidden) continue;
795
- }
796
-
797
- // Self-loop: render as a smooth circular arc below the node
798
- if (le.source === le.target) {
799
- const nodeLayout = layoutNodeMap.get(le.source);
800
- if (nodeLayout) {
801
- const edgeG = diagramG
802
- .append('g')
803
- .attr('class', 'bl-edge-group')
804
- .attr('data-line-number', String(le.lineNumber))
805
- // Endpoint node labels for baked-CSS connection-highlight.
806
- .attr('data-from', le.source)
807
- .attr('data-to', le.target);
808
-
809
- const markerId = `bl-arrow-${color.replace('#', '')}`;
810
- const cx = nodeLayout.x;
811
- const cy = nodeLayout.y;
812
- const hw = nodeLayout.width / 2;
813
- const hh = nodeLayout.height / 2;
814
- const pad = 20; // clearance from node edge
815
-
816
- // Arc exits from bottom of right side, swings wide, returns to right of bottom side
817
- const startX = cx + hw;
818
- const startY = cy + hh * 0.4;
819
- const endX = cx + hw * 0.4;
820
- const endY = cy + hh;
821
-
822
- // Control points swing far out to create a smooth circular arc
823
- const cp1x = startX + hw + pad;
824
- const cp1y = startY;
825
- const cp2x = endX;
826
- const cp2y = endY + hh + pad;
827
-
828
- edgeG
829
- .append('path')
830
- .attr('class', 'bl-edge')
831
- .attr(
832
- 'd',
833
- `M ${startX} ${startY} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${endX} ${endY}`
834
- )
835
- .attr('fill', 'none')
836
- .attr('stroke', color)
837
- .attr('stroke-width', sEdgeStrokeWidth)
838
- .attr('marker-end', `url(#${markerId})`);
839
- }
840
- continue;
841
- }
842
-
843
- // Parallel edge fan: construct explicit 5-point geometry so lines
844
- // bundle at ports and visibly spread apart in the middle.
845
- let points: { x: number; y: number }[];
846
- if (le.yOffset !== 0 && le.parallelCount > 1) {
847
- const srcLayout = layoutNodeMap.get(le.source);
848
- const tgtLayout = layoutNodeMap.get(le.target);
849
- const srcY = srcLayout?.y ?? le.points[0]?.y ?? 0;
850
- const tgtY = tgtLayout?.y ?? le.points[le.points.length - 1]?.y ?? 0;
851
- const srcX = le.points[0]?.x ?? 0;
852
- const tgtX = le.points[le.points.length - 1]?.x ?? 0;
853
- const midX = (srcX + tgtX) / 2;
854
- const midY = (srcY + tgtY) / 2;
855
-
856
- points = [
857
- { x: srcX, y: srcY }, // port (bundled)
858
- { x: srcX + (midX - srcX) * 0.3, y: srcY + le.yOffset * 0.5 }, // separate
859
- { x: midX, y: midY + le.yOffset }, // full spread
860
- { x: tgtX - (tgtX - midX) * 0.3, y: tgtY + le.yOffset * 0.5 }, // converge
861
- { x: tgtX, y: tgtY }, // port (bundled)
862
- ];
863
- } else {
864
- points = le.points.map((p) => ({ x: p.x, y: p.y }));
865
- }
866
- if (points.length < 2) continue;
867
-
868
- const edgeG = diagramG
869
- .append('g')
870
- .attr('class', 'bl-edge-group')
871
- .attr('data-line-number', String(le.lineNumber))
872
- // Endpoint node labels for baked-CSS connection-highlight.
873
- .attr('data-from', le.source)
874
- .attr('data-to', le.target);
875
-
876
- const markerId = `bl-arrow-${color.replace('#', '')}`;
877
- const gen = le.straight
878
- ? lineGeneratorStraight
879
- : parsed.direction === 'TB'
880
- ? lineGeneratorTB
881
- : lineGeneratorLR;
882
- const path = edgeG
883
- .append('path')
884
- .attr('class', 'bl-edge')
885
- .attr('d', gen(points) ?? '')
886
- .attr('fill', 'none')
887
- .attr('stroke', color)
888
- .attr('stroke-width', sEdgeStrokeWidth)
889
- .attr('marker-end', `url(#${markerId})`);
890
-
891
- if (le.bidirectional) {
892
- const revId = `bl-arrow-rev-${color.replace('#', '')}`;
893
- path.attr('marker-start', `url(#${revId})`);
894
- }
895
- }
896
-
897
- // ── Render nodes ───────────────────────────────────────
898
- for (const ln of layout.nodes) {
899
- const node = nodeMap.get(ln.label);
900
- if (!node) continue;
901
-
902
- // Check if hidden
903
- if (hidden.size > 0) {
904
- let isHidden = false;
905
- for (const [groupKey, hiddenVals] of hidden) {
906
- const val = node.metadata[groupKey];
907
- if (val && hiddenVals.has(val.toLowerCase())) {
908
- isHidden = true;
909
- break;
910
- }
911
- }
912
- if (isHidden) continue;
913
- }
914
-
915
- const solid = parsed.options['solid-fill'] === 'on';
916
- const colors = nodeColors(
917
- node,
918
- parsed.tagGroups,
919
- activeGroup,
920
- palette,
921
- isDark,
922
- {
923
- active: activeIsValue,
924
- hue: rampHue,
925
- twoColor: rampLow !== undefined,
926
- fillForValue,
927
- },
928
- solid
929
- );
930
- // Divider matches the org-card convention: the box stroke normally, but the
931
- // contrast text colour in solid mode (where stroke == fill and would vanish).
932
- const dividerStroke = solid ? colors.text : colors.stroke;
933
-
934
- const nodeG = diagramG
935
- .append('g')
936
- .attr('class', 'bl-node')
937
- .attr('transform', `translate(${ln.x},${ln.y})`)
938
- .attr('data-line-number', node.lineNumber)
939
- .attr('data-node-id', node.label)
940
- .style('cursor', onClickItem ? 'pointer' : 'default')
941
- .style('--bl-node-stroke', colors.stroke);
942
-
943
- // Add tag metadata as data attributes for legend hover dimming
944
- for (const [key, val] of Object.entries(node.metadata)) {
945
- nodeG.attr(`data-tag-${key.toLowerCase()}`, val.toLowerCase());
946
- }
947
-
948
- // Numeric value drives the gradient scrub; guard on !== undefined so a
949
- // legitimate `heat: 0` still emits data-value="0" (0 is falsy).
950
- if (node.value !== undefined) {
951
- nodeG.attr('data-value', node.value);
952
- }
953
-
954
- if (onClickItem) {
955
- nodeG.on('click', (event: Event) => {
956
- // Don't intercept clicks on links in description text
957
- const target = event.target as Element | null;
958
- if (target?.closest('a')) return;
959
- onClickItem(node.lineNumber);
960
- });
961
- }
962
-
963
- // Rectangle card
964
- const x = -ln.width / 2;
965
- const y = -ln.height / 2;
966
-
967
- // Background rect
968
- nodeG
969
- .append('rect')
970
- .attr('x', x)
971
- .attr('y', y)
972
- .attr('width', ln.width)
973
- .attr('height', ln.height)
974
- .attr('rx', NODE_RX)
975
- .attr('ry', NODE_RX)
976
- .attr('fill', colors.fill)
977
- .attr('stroke', colors.stroke)
978
- .attr('stroke-width', sNodeStrokeWidth);
979
-
980
- // All text centered vertically using dominant-baseline: central
981
- const desc = node.description;
982
- if (desc && desc.length > 0 && !hideDescriptions) {
983
- const MAX_LABEL_LINES = 3;
984
- const fitted = fitLabelToHeader(node.label, ln.width, MAX_LABEL_LINES);
985
- const labelLines = fitted.lines;
986
- const labelLineH = fitted.fontSize * 1.3;
987
- const labelTotalH = labelLines.length * labelLineH;
988
- const headerH = labelTotalH + 12;
989
- const headerCenterY = -ln.height / 2 + headerH / 2;
990
- for (let li = 0; li < labelLines.length; li++) {
991
- nodeG
992
- .append('text')
993
- .attr('x', 0)
994
- .attr(
995
- 'y',
996
- headerCenterY - labelTotalH / 2 + labelLineH / 2 + li * labelLineH
997
- )
998
- .attr('text-anchor', 'middle')
999
- .attr('dominant-baseline', 'central')
1000
- .attr('font-size', fitted.fontSize)
1001
- .attr('font-weight', 'bold')
1002
- .attr('fill', colors.text)
1003
- // In-bounds by loop guard.
1004
- .text(labelLines[li]!);
1005
- }
1006
-
1007
- // Single divider under the title (org-card convention) — everything else
1008
- // renders below it as one body section (no second divider / footer band).
1009
- const sepY = -ln.height / 2 + headerH;
1010
- nodeG
1011
- .append('line')
1012
- .attr('x1', -ln.width / 2)
1013
- .attr('y1', sepY)
1014
- .attr('x2', ln.width / 2)
1015
- .attr('y2', sepY)
1016
- .attr('stroke', dividerStroke)
1017
- .attr('stroke-opacity', 0.3)
1018
- .attr('stroke-width', 1);
1019
-
1020
- const descStartY = sepY + 4 + sDescFontSize;
1021
- const maxTextWidth = ln.width - NODE_TEXT_PADDING * 2;
1022
- // Char budget for the shared (char-based) bullet-aware wrapper. Derived
1023
- // from the shared average glyph ratio so it stays in step with the
1024
- // pixel measurer used everywhere else here.
1025
- const charsPerLine = Math.floor(
1026
- maxTextWidth / (sDescFontSize * CHAR_WIDTH_RATIO)
1027
- );
1028
- const descLineH = sDescFontSize * DESC_LINE_HEIGHT;
1029
-
1030
- // Estimate display length — strip markdown syntax for measurement
1031
- const displayLen = (text: string): number =>
1032
- text
1033
- .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // [text](url) → text
1034
- .replace(/\*\*(.+?)\*\*/g, '$1') // **bold** → bold
1035
- .replace(/\*(.+?)\*/g, '$1') // *italic* → italic
1036
- .replace(/`(.+?)`/g, '$1') // `code` → code
1037
- .replace(/https?:\/\/\S+/g, (u) => u.slice(0, 20)).length; // bare URLs shortened
1038
-
1039
- // Build wrapped lines from description. Convert "- " to bullet glyph
1040
- // and let the shared helper split bullet lines into first/cont rows
1041
- // so continuation text aligns under the bullet body, not the glyph.
1042
- const normalizedLines: string[] = [];
1043
- for (const descLine of desc) {
1044
- let normalized = descLine.startsWith('- ')
1045
- ? '\u2022 ' + descLine.slice(2)
1046
- : descLine;
1047
- // Normalize bare URLs: `http example.com` → `http://example.com`
1048
- normalized = normalized.replace(
1049
- /\bhttps?\s+([\w][\w.-]+\.[a-z]{2,}(?:\/\S*)?)/gi,
1050
- (_, domain) => `https://${domain}`
1051
- );
1052
- normalizedLines.push(normalized);
1053
- }
1054
-
1055
- const wrappedLinesShared: WrappedDescLine[] = wrapDescriptionLines(
1056
- normalizedLines,
1057
- charsPerLine,
1058
- displayLen
1059
- );
1060
-
1061
- const truncated = wrappedLinesShared.length > MAX_DESC_LINES;
1062
- const visibleLines = truncated
1063
- ? wrappedLinesShared.slice(0, MAX_DESC_LINES)
1064
- : wrappedLinesShared;
1065
-
1066
- // Bullet glyph at the description's left edge; body text indented so
1067
- // continuation lines align under the first word past the bullet.
1068
- const BULLET_GLYPH_X = -ln.width / 2 + 6;
1069
- const BULLET_BODY_X = BULLET_GLYPH_X + 10;
1070
-
1071
- // Description must stay legible on ANY fill. On the default light/tinted
1072
- // fills keep the subtle muted grey; on a dark/saturated fill (e.g.
1073
- // solid-fill) the fixed grey sinks in — switch to a muted tint of the
1074
- // box's contrast-correct text colour so it reads while staying
1075
- // subordinate to the title.
1076
- const descColor =
1077
- relativeLuminance(colors.fill) > 0.5
1078
- ? palette.textMuted
1079
- : mix(colors.text, colors.fill, 75);
1080
-
1081
- for (let li = 0; li < visibleLines.length; li++) {
1082
- // In-bounds by loop guard.
1083
- const line = visibleLines[li]!;
1084
- let lineText = line.text;
1085
- // Truncate last line if there are more lines beyond the cap
1086
- if (truncated && li === visibleLines.length - 1) {
1087
- lineText =
1088
- measureText(lineText, sDescFontSize) >= maxTextWidth
1089
- ? truncateText(lineText, sDescFontSize, maxTextWidth)
1090
- : lineText + '\u2026';
1091
- }
1092
- const y = descStartY + li * descLineH;
1093
- if (line.kind === 'bullet-first') {
1094
- nodeG
1095
- .append('text')
1096
- .attr('x', BULLET_GLYPH_X)
1097
- .attr('y', y)
1098
- .attr('text-anchor', 'start')
1099
- .attr('dominant-baseline', 'central')
1100
- .attr('font-size', sDescFontSize)
1101
- .attr('fill', descColor)
1102
- .text('\u2022');
1103
- }
1104
- const isBullet =
1105
- line.kind === 'bullet-first' || line.kind === 'bullet-cont';
1106
- const textEl = nodeG
1107
- .append('text')
1108
- .attr('x', isBullet ? BULLET_BODY_X : 0)
1109
- .attr('y', y)
1110
- .attr('text-anchor', isBullet ? 'start' : 'middle')
1111
- .attr('dominant-baseline', 'central')
1112
- .attr('font-size', DESC_FONT_SIZE)
1113
- .attr('fill', descColor);
1114
- renderInlineText(textEl, lineText, palette, sDescFontSize);
1115
- }
1116
-
1117
- // Tooltip when truncated
1118
- if (truncated) {
1119
- const fullText = desc.join(' ');
1120
- const tooltipText =
1121
- fullText.length > 200 ? fullText.slice(0, 199) + '\u2026' : fullText;
1122
- nodeG.append('title').text(tooltipText);
1123
- }
1124
-
1125
- // Value sits in the SAME body section, directly after the description \u2014
1126
- // no second divider / footer band (org-card: title, one line, body).
1127
- if (parsed.showValues && node.value !== undefined) {
1128
- const valueLabel = parsed.boxMetric
1129
- ? `${parsed.boxMetric}: ${node.value}`
1130
- : String(node.value);
1131
- nodeG
1132
- .append('text')
1133
- .attr('class', 'bl-node-value')
1134
- .attr('x', 0)
1135
- .attr('y', descStartY + visibleLines.length * descLineH)
1136
- .attr('text-anchor', 'middle')
1137
- .attr('dominant-baseline', 'central')
1138
- .attr('font-size', VALUE_FONT_SIZE)
1139
- .attr('font-weight', '600')
1140
- .attr('fill', colors.text)
1141
- .text(valueLabel);
1142
- }
1143
- } else if (parsed.showValues && node.value !== undefined) {
1144
- // Plain node with show-values: label header + thin divider + a
1145
- // "Metric: value" line below (org/infra card style), instead of a
1146
- // vertically-centered label with a floating number.
1147
- const valueLabel = parsed.boxMetric
1148
- ? `${parsed.boxMetric}: ${node.value}`
1149
- : String(node.value);
1150
- // Fixed header zone (not label-height-driven) so the divider sits at a
1151
- // UNIFORM Y across every box, regardless of label line count (infra/org
1152
- // both anchor the separator to a constant header height).
1153
- const headerH = ln.height / 2;
1154
- const sepY = -ln.height / 2 + headerH;
1155
- const fitted = fitLabelToHeader(node.label, ln.width, 2);
1156
- const labelLineH = fitted.fontSize * 1.3;
1157
- const labelTotalH = fitted.lines.length * labelLineH;
1158
- const headerCenterY = -ln.height / 2 + headerH / 2;
1159
- for (let li = 0; li < fitted.lines.length; li++) {
1160
- nodeG
1161
- .append('text')
1162
- .attr('x', 0)
1163
- .attr(
1164
- 'y',
1165
- headerCenterY - labelTotalH / 2 + labelLineH / 2 + li * labelLineH
1166
- )
1167
- .attr('text-anchor', 'middle')
1168
- .attr('dominant-baseline', 'central')
1169
- .attr('font-size', fitted.fontSize)
1170
- .attr('font-weight', 'bold')
1171
- .attr('fill', colors.text)
1172
- // In-bounds by loop guard.
1173
- .text(fitted.lines[li]!);
1174
- }
1175
- // Single divider under the title (org-card convention; solid-aware so it
1176
- // stays visible when stroke == fill).
1177
- nodeG
1178
- .append('line')
1179
- .attr('x1', -ln.width / 2)
1180
- .attr('y1', sepY)
1181
- .attr('x2', ln.width / 2)
1182
- .attr('y2', sepY)
1183
- .attr('stroke', dividerStroke)
1184
- .attr('stroke-opacity', 0.3)
1185
- .attr('stroke-width', 1);
1186
- // "Metric: value" centered in the space below the divider.
1187
- nodeG
1188
- .append('text')
1189
- .attr('class', 'bl-node-value')
1190
- .attr('x', 0)
1191
- .attr('y', (sepY + ln.height / 2) / 2)
1192
- .attr('text-anchor', 'middle')
1193
- .attr('dominant-baseline', 'central')
1194
- .attr('font-size', VALUE_FONT_SIZE)
1195
- .attr('fill', colors.text)
1196
- .attr('opacity', 0.85)
1197
- .text(valueLabel);
1198
- } else {
1199
- const maxLabelLines = Math.max(
1200
- 2,
1201
- Math.floor((ln.height - 16) / (sMinNodeFontSize * 1.3))
1202
- );
1203
- const fitted = fitLabelToHeader(node.label, ln.width, maxLabelLines);
1204
- const lineH = fitted.fontSize * 1.3;
1205
- const totalH = fitted.lines.length * lineH;
1206
- for (let li = 0; li < fitted.lines.length; li++) {
1207
- nodeG
1208
- .append('text')
1209
- .attr('x', 0)
1210
- .attr('y', -totalH / 2 + lineH / 2 + li * lineH)
1211
- .attr('text-anchor', 'middle')
1212
- .attr('dominant-baseline', 'central')
1213
- .attr('font-size', fitted.fontSize)
1214
- .attr('font-weight', '600')
1215
- .attr('fill', colors.text)
1216
- // In-bounds by loop guard.
1217
- .text(fitted.lines[li]!);
1218
- }
1219
- }
1220
-
1221
- // ── Note (floated beside the box, or a collapsed corner badge) ──
1222
- // The box keeps its layout position; the note floats in adjacent space.
1223
- // Coords are node-center-local (the node `<g>` is at the box center).
1224
- if (ln.note) {
1225
- if (ln.note.collapsed) {
1226
- renderNoteBadge(
1227
- nodeG,
1228
- {
1229
- x: ln.width / 2 - NOTE_BADGE_RADIUS - 3,
1230
- y: -ln.height / 2 + NOTE_BADGE_RADIUS + 3,
1231
- },
1232
- palette,
1233
- {
1234
- isDark,
1235
- ...(ln.note.color && { color: ln.note.color }),
1236
- lineNumber: ln.note.lineNumber,
1237
- endLineNumber: ln.note.endLineNumber,
1238
- }
1239
- );
1240
- } else {
1241
- const [cx1, cy1, cx2, cy2] = noteConnectorPoints(
1242
- { width: ln.width, height: ln.height },
1243
- ln.note
1244
- );
1245
- renderNoteConnector(nodeG, cx1, cy1, cx2, cy2, palette);
1246
- renderNoteBox(
1247
- nodeG,
1248
- {
1249
- x: ln.note.x,
1250
- y: ln.note.y,
1251
- width: ln.note.width,
1252
- height: ln.note.height,
1253
- },
1254
- ln.note.lines,
1255
- palette,
1256
- {
1257
- isDark,
1258
- ...(ln.note.color && { color: ln.note.color }),
1259
- lineNumber: ln.note.lineNumber,
1260
- endLineNumber: ln.note.endLineNumber,
1261
- interactive: true,
1262
- }
1263
- );
1264
- }
1265
- }
1266
- }
1267
-
1268
- // ── Render edge labels ─────────────────────────────────
1269
- // Drawn AFTER nodes (and groups) so the halo + text always paint on top and
1270
- // are never clipped by a box. Positions/wrapping come from label-placement
1271
- // (layout); this is a pure consumer. Labels stay in their own layer rather
1272
- // than inside each edge group, but carry data-line-number so line-hover/click
1273
- // still highlights the matching label.
1274
- const labelLayer = diagramG.append('g').attr('class', 'bl-edge-labels');
1275
- const labelLineHeight = sEdgeLabelFontSize * 1.3;
1276
- for (const le of layout.edges) {
1277
- if (!le.labelLines || le.labelLines.length === 0) continue;
1278
- if (le.labelX === undefined || le.labelY === undefined) continue;
1279
- const lx = le.labelX;
1280
- const ly = le.labelY;
1281
-
1282
- // Honour the same tag-value hiding as the edges themselves.
1283
- if (hidden.size > 0) {
1284
- let isHidden = false;
1285
- for (const [groupKey, hiddenVals] of hidden) {
1286
- const val = le.metadata[groupKey];
1287
- if (val && hiddenVals.has(val.toLowerCase())) {
1288
- isHidden = true;
1289
- break;
1290
- }
1291
- }
1292
- if (isHidden) continue;
1293
- }
1294
-
1295
- const lw = le.labelWidth ?? 0;
1296
- const lh = le.labelHeight ?? sEdgeLabelFontSize + 6;
1297
- const lines = le.labelLines;
1298
- const labelG = labelLayer
1299
- .append('g')
1300
- .attr('class', 'bl-edge-label')
1301
- .attr('data-line-number', String(le.lineNumber));
1302
-
1303
- labelG
1304
- .append('rect')
1305
- .attr('x', lx - lw / 2)
1306
- .attr('y', ly - lh / 2)
1307
- .attr('width', lw)
1308
- .attr('height', lh)
1309
- .attr('rx', 3)
1310
- .attr('fill', palette.bg)
1311
- // No border; semi-transparent so the line stays faintly visible behind the
1312
- // label while the (full-opacity) text on top stays crisp.
1313
- .attr('opacity', 0.72);
1314
-
1315
- const text = labelG
1316
- .append('text')
1317
- .attr('x', lx)
1318
- .attr('text-anchor', 'middle')
1319
- .attr('font-family', FONT_FAMILY)
1320
- .attr('font-size', sEdgeLabelFontSize)
1321
- .attr('fill', palette.textMuted);
1322
- const firstY =
1323
- ly - ((lines.length - 1) / 2) * labelLineHeight + sEdgeLabelFontSize / 3;
1324
- lines.forEach((line, k) => {
1325
- text
1326
- .append('tspan')
1327
- .attr('x', lx)
1328
- .attr('y', firstY + k * labelLineHeight)
1329
- .text(line);
1330
- });
1331
- }
1332
-
1333
- // ── Render legend ──────────────────────────────────────
1334
- const hasDescriptions = parsed.nodes.some(
1335
- (n) => n.description && n.description.length > 0
1336
- );
1337
- // App-hosted: the Descriptions control moves to the app overlay, so a
1338
- // descriptions-only legend (no tag groups) has nothing left to render. The
1339
- // value ramp (a synthetic group in legendGroups) also forces a legend.
1340
- const hasLegend =
1341
- legendGroups.length > 0 || (hasDescriptions && controlsHost !== 'app');
1342
-
1343
- if (hasLegend) {
1344
- // Build controls group for description toggle. App-hosted controls own the
1345
- // toggling, so the group is built (to gate + size the row) even without the
1346
- // inline-gear callback.
1347
- let controlsGroup: { toggles: ControlsGroupToggle[] } | undefined;
1348
- if (hasDescriptions && (onToggleDescriptions || controlsHost === 'app')) {
1349
- controlsGroup = {
1350
- toggles: [
1351
- {
1352
- id: 'descriptions',
1353
- type: 'toggle',
1354
- label: 'Descriptions',
1355
- active: !hideDescriptions,
1356
- onToggle: () => {},
1357
- },
1358
- ],
1359
- };
1360
- }
1361
-
1362
- const legendCallbacks: LegendCallbacks = {
1363
- ...(onToggleControlsExpand !== undefined && {
1364
- onControlsExpand: onToggleControlsExpand,
1365
- }),
1366
- onControlsToggle: (toggleId, active) => {
1367
- if (toggleId === 'descriptions' && onToggleDescriptions) {
1368
- onToggleDescriptions(active);
1369
- }
1370
- },
1371
- };
1372
- const legendG = svg
1373
- .append('g')
1374
- .attr('transform', `translate(0,${titleOffset + 4})`);
1375
- renderIntegratedLegend(legendG, {
1376
- groups: legendGroups,
1377
- activeGroup,
1378
- mode: exportMode ? 'export' : 'preview',
1379
- // Keep inactive sibling tag groups visible as collapsed pills so the user
1380
- // can click one to flip the active colouring dimension (preview only).
1381
- showInactivePills: true,
1382
- ...(controlsGroup !== undefined && { controlsGroup }),
1383
- ...(controlsHost !== undefined && { controlsHost }),
1384
- ...(controlsExpanded !== undefined && { controlsExpanded }),
1385
- callbacks: legendCallbacks,
1386
- palette,
1387
- isDark,
1388
- width,
1389
- });
1390
- legendG.selectAll('[data-legend-group]').classed('bl-legend-group', true);
1391
- }
1392
-
1393
- // ── Focus mode: one reusable hover-reveal icon (interactive only) ──
1394
- // A single hidden icon the app repositions over the hovered box/group and
1395
- // stamps `data-focus-id`/`data-focus-kind` on (Decision 22 / ADR-4) — NOT one
1396
- // per node (~4k elements on a large graph). Appended to the SVG root so the
1397
- // app positions it in root (screen-mapped) coordinates, counter-scaled to a
1398
- // constant size regardless of fit. Excluded from export like org's icon.
1399
- if (!exportDims && !exportMode) {
1400
- const iconSize = 14;
1401
- const focusG = svg
1402
- .append('g')
1403
- .attr('class', 'bl-focus-icon')
1404
- .attr('data-export-ignore', 'true')
1405
- .style('display', 'none')
1406
- .style('pointer-events', 'auto')
1407
- .style('cursor', 'pointer');
1408
- // Hit area
1409
- focusG
1410
- .append('rect')
1411
- .attr('x', -3)
1412
- .attr('y', -3)
1413
- .attr('width', iconSize + 6)
1414
- .attr('height', iconSize + 6)
1415
- .attr('fill', 'transparent');
1416
- // Scope/target icon: outer circle + inner dot (mirrors org-focus-icon)
1417
- const cx = iconSize / 2;
1418
- const cy = iconSize / 2;
1419
- focusG
1420
- .append('circle')
1421
- .attr('cx', cx)
1422
- .attr('cy', cy)
1423
- .attr('r', iconSize / 2 - 1)
1424
- .attr('fill', palette.bg)
1425
- .attr('stroke', palette.textMuted)
1426
- .attr('stroke-width', 1.5);
1427
- focusG
1428
- .append('circle')
1429
- .attr('cx', cx)
1430
- .attr('cy', cy)
1431
- .attr('r', 2)
1432
- .attr('fill', palette.textMuted);
1433
- }
1434
- }
1435
-
1436
- // ── Export helper ──────────────────────────────────────────
1437
-
1438
- export function renderBoxesAndLinesForExport(
1439
- container: HTMLDivElement,
1440
- parsed: ParsedBoxesAndLines,
1441
- layout: BLLayoutResult,
1442
- palette: PaletteColors,
1443
- isDark: boolean,
1444
- options?: {
1445
- exportDims?: { width: number; height: number };
1446
- activeTagGroup?: string | null;
1447
- hiddenTagValues?: Map<string, Set<string>>;
1448
- exportMode?: boolean;
1449
- }
1450
- ): void {
1451
- renderBoxesAndLines(container, parsed, layout, palette, isDark, {
1452
- ...(options?.exportDims !== undefined && {
1453
- exportDims: options.exportDims,
1454
- }),
1455
- ...(options?.activeTagGroup !== undefined && {
1456
- activeTagGroup: options.activeTagGroup,
1457
- }),
1458
- ...(options?.hiddenTagValues !== undefined && {
1459
- hiddenTagValues: options.hiddenTagValues,
1460
- }),
1461
- ...(options?.exportMode !== undefined && {
1462
- exportMode: options.exportMode,
1463
- }),
1464
- });
1465
- }