@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,1742 +0,0 @@
1
- // ============================================================
2
- // Event Line — D3 SVG Renderer (spec §28)
3
- // ============================================================
4
- //
5
- // Horizontal spine of point events. Each event: a dot at its date position
6
- // (or even index under no-scale), a vertical leader line, and an org-style card
7
- // (utils/card.ts `renderNodeCard` chrome + a hand-drawn divider and a
8
- // pyramid/ring prose body). The event's date rides INSIDE its card as a muted
9
- // subtitle, so it travels with the event instead of sitting on the axis. The
10
- // spine carries no date tick ruler. Cards auto-alternate above/below and pack
11
- // into stacked lanes on collision. The tag legend uses the shared
12
- // `renderIntegratedLegend`.
13
-
14
- import * as d3Selection from 'd3-selection';
15
- import { FONT_FAMILY } from '../fonts';
16
- import {
17
- TITLE_FONT_SIZE,
18
- TITLE_FONT_WEIGHT,
19
- TITLE_Y,
20
- } from '../utils/title-constants';
21
- import {
22
- CARD_RADIUS,
23
- HEADER_HEIGHT,
24
- LABEL_FONT_SIZE,
25
- NODE_STROKE_WIDTH,
26
- SEPARATOR_GAP,
27
- } from '../utils/visual-conventions';
28
- import {
29
- contrastText,
30
- getSeriesColors,
31
- mix,
32
- shapeFill,
33
- themeBaseBg,
34
- } from '../palettes/color-utils';
35
- import { resolveColor } from '../colors';
36
- import { renderNodeCard } from '../utils/card';
37
- import { renderInlineText } from '../utils/inline-markdown';
38
- import { CHAR_WIDTH_RATIO } from '../utils/text-measure';
39
- import {
40
- wrapDescriptionLines,
41
- type WrappedDescLine,
42
- } from '../utils/wrapped-desc';
43
- import { renderIntegratedLegend } from '../utils/legend-integration';
44
- import { formatDateLabel } from '../timeline/renderer';
45
- import type { LegendGroupData } from '../utils/legend-types';
46
- import {
47
- resolveActiveTagGroup,
48
- resolveTagColor,
49
- type TagGroup,
50
- } from '../utils/tag-groups';
51
- import type { PaletteColors } from '../palettes';
52
- import type { D3ExportDimensions } from '../utils/d3-types';
53
- import type { EventLineEra, EventLineEvent, ParsedEventLine } from './types';
54
-
55
- // ── Geometry constants ───────────────────────────────────────
56
- const CARD_W = 210;
57
- const H_MARGIN = 58;
58
- const TITLE_AREA = 48;
59
- const LEGEND_BAND = 34;
60
- const LEADER_ABOVE = 46;
61
- const LEADER_BELOW = 46;
62
- const DESC_FONT = 11.5;
63
- const DESC_LINE_H = 16;
64
- const CARD_PAD = 9;
65
- const CARD_BODY_TOP = HEADER_HEIGHT + SEPARATOR_GAP;
66
- const DOT_R = 5.5;
67
- const MIN_SPACING = 96;
68
- const LANE_GAP = 16;
69
- const NEUTRAL_TAG = '#999999';
70
- // Horizontal placement: a card may slide sideways as long as its dot stays at
71
- // least CARD_INSET from either edge (so a vertical leader still lands cleanly
72
- // on it). Cards fan side-by-side until they can't, then stack into lanes.
73
- const CARD_INSET = 18;
74
- const FAN_GAP = 6;
75
- // The event's date rides INSIDE the card as a muted subtitle directly adjacent to
76
- // the title (on the side away from the spine), so a date travels with its event
77
- // instead of sitting on the axis. This much header height is reserved for it.
78
- const DATE_SUBTITLE_H = 15;
79
- const DATE_SUBTITLE_FONT = 10;
80
- // `no-box` only: a soft tag-tinted "shelf" sits behind the title + date so the
81
- // header reads as a unit, and a full-strength colored bar on its spine-side edge
82
- // is the leader's landing pad — keeping the dot→block link solid in dense charts.
83
- const SHELF_TINT = 13; // % of the tag color mixed over the theme base
84
- // Thickness of the colored landing lip. The shelf's corner radius is matched to
85
- // it (SHELF_EDGE below) so the lip exactly fills the rounded corners and reads
86
- // as part of the rounded rect — a thinner lip therefore also means slightly
87
- // tighter shelf corners, which keeps the carry-through clean.
88
- const SHELF_EDGE = 4;
89
- // Era `]` bracket band: depth reserved on the side opposite the cards.
90
- const ERA_BLOCK = 30;
91
- const ERA_BRACKET_CAP = 8;
92
- // Padding each era bracket extends past its first/last member dot, and the gap
93
- // kept on each side of a seam when two adjacent brackets are clamped apart.
94
- const ERA_BRACKET_PAD = 14;
95
- const ERA_SEAM_GAP = 3;
96
- // Collapsed-era axis-break glyph (`≈` rotated 90°): two short parallel wavy
97
- // strokes that cross the spine, with the spine blanked between them so the
98
- // timeline visibly breaks where the folded span is.
99
- const BREAK_HALF_H = 6; // squiggle extends this far above AND below the spine
100
- const BREAK_AMP = 2.6; // horizontal wave amplitude
101
- const BREAK_GAP = 5; // spacing between the two waves (= the spine gap width)
102
- // Collapsed era: the `⊓` bar floats this far off the spine (on the card's side);
103
- // its legs drop from the bar to rest their feet ON the timeline.
104
- const ERA_LEG = 13;
105
- const ERA_LABEL_FONT = 11.5;
106
-
107
- // ── Hover interactivity (preview only — inert in the resvg export path) ───────
108
- // All effects are renderer-owned so they work identically in the app, the web
109
- // editor, and Obsidian, and re-bind on every re-render (no orphaned listeners).
110
- // CSS supplies the smooth self-effects + the dim/highlight state classes; a
111
- // single delegated handler on the SVG root correlates the pieces of one event,
112
- // an era's members, and legend-entry → category focus.
113
- const HL = 'dgmo-evt-hl';
114
- const DIM = 'dgmo-evt-dim';
115
- const ERA_HL = 'dgmo-evt-era-hl';
116
- // Clicking a legend entry collapses that tag value's cards (and their leaders)
117
- // to bare dots — the dot stays on the spine in its tag color and still reveals
118
- // its card on hover; the legend entry dims with a hollow swatch so the muted
119
- // state is self-documenting and reversible. Preview-only
120
- // (never in `exportMode`), so static SVG/PNG always carries the full set.
121
- const COLLAPSED = 'dgmo-evt-collapsed';
122
- const OFF = 'dgmo-evt-off';
123
- const HIDDEN_ATTR = 'data-evt-hidden';
124
- const HOVER_CSS =
125
- `.dgmo-event-dot{transition:transform .12s ease;transform-box:fill-box;transform-origin:center}` +
126
- `.dgmo-event-leader,.dgmo-event-card,.dgmo-event-era{transition:opacity .12s ease,filter .12s ease}` +
127
- `.dgmo-event-dot,.dgmo-event-card,.dgmo-event-era{cursor:default}` +
128
- `[data-legend-entry]{cursor:pointer}` +
129
- `.${DIM}{opacity:.2}` +
130
- `.dgmo-event-dot.${HL}{transform:scale(1.55)}` +
131
- `.dgmo-event-leader.${HL}{stroke-opacity:1;stroke-width:2.5}` +
132
- `.dgmo-event-card.${HL}{filter:drop-shadow(0 2px 5px rgba(0,0,0,.22))}` +
133
- `.dgmo-event-era.${ERA_HL}{filter:drop-shadow(0 1px 2px rgba(0,0,0,.28))}` +
134
- // Collapsed-to-dot: hide the card + its leader, keep the dot. A hover that
135
- // glows the event (HL on all its pieces) transiently re-reveals the card —
136
- // the extra class out-specifies both the collapse rule and DIM.
137
- `.dgmo-event-card.${COLLAPSED},.dgmo-event-leader.${COLLAPSED}{opacity:0;pointer-events:none}` +
138
- `.dgmo-event-card.${COLLAPSED}.${HL},.dgmo-event-leader.${COLLAPSED}.${HL}{opacity:1;pointer-events:auto}` +
139
- `[data-legend-entry].${OFF} text{opacity:.5}`;
140
-
141
- type Side = 'above' | 'below';
142
-
143
- interface Placed {
144
- /** `event` = a visible event card; `era` = a collapsed era's summary card. */
145
- kind: 'event' | 'era';
146
- event: EventLineEvent | null;
147
- era: EventLineEra | null;
148
- members: readonly EventLineEvent[];
149
- label: string;
150
- date: string | null;
151
- dateValue: number | null;
152
- /** True for a `TBD` future event — drawn with a hollow dot + dashed leader. */
153
- future: boolean;
154
- /** Bracketed-TBD gap `[lo, hi]` (dateValue units) for the "somewhere in here"
155
- * whisker; null for a trailing TBD (dashed spine tail) or any real event. */
156
- futureSpan: readonly [number, number] | null;
157
- lineNumber: number;
158
- /** Name of the enclosing era (for bracket runs), or null. */
159
- eraName: string | null;
160
- color: string;
161
- cardFill: string;
162
- titleColor: string;
163
- lines: WrappedDescLine[];
164
- /** For a collapsed era: per-member tag color, in member order (one per bullet). */
165
- bulletColors: readonly string[];
166
- /** For a collapsed era: per-member tag metadata, in member/bullet order — lets a
167
- * legend hover highlight matching member bullets inside the folded card. */
168
- bulletMeta: readonly Readonly<Record<string, string>>[];
169
- cardH: number;
170
- x: number;
171
- side: Side;
172
- lane: number;
173
- left: number;
174
- /** Collapsed era only: half the date-span width in px, so its bracket stretches
175
- * across the folded run (`x` is the span MIDPOINT). 0 for events. */
176
- spanHalf: number;
177
- }
178
-
179
- export function renderEventLine(
180
- container: HTMLDivElement,
181
- parsed: ParsedEventLine,
182
- palette: PaletteColors,
183
- isDark: boolean,
184
- onClickItem?: (lineNumber: number) => void,
185
- exportDims?: D3ExportDimensions,
186
- tagOverride?: string
187
- ): void {
188
- if (parsed.events.length === 0) return;
189
-
190
- d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
191
- const width = exportDims?.width ?? container.clientWidth;
192
- const heightHint = exportDims?.height ?? container.clientHeight;
193
- if (width <= 0 || heightHint <= 0) return;
194
-
195
- const exportMode = !!exportDims;
196
- // Preview-only legend-muted tag values (persisted on the container so they
197
- // survive every re-render — era collapse/expand, resize, palette change). The
198
- // export path always builds the full set; muting is interactive chrome only.
199
- const hiddenSet = exportMode ? new Set<string>() : readHidden(container);
200
- const seriesColors = getSeriesColors(palette);
201
- const accent = seriesColors[0]!;
202
- const activeGroup = resolveActiveTagGroup(
203
- parsed.tagGroups,
204
- undefined,
205
- tagOverride
206
- );
207
-
208
- const showTitle = !!parsed.title && !parsed.options.noTitle;
209
- const hasLegend =
210
- parsed.tagGroups.length > 0 &&
211
- activeGroup !== null &&
212
- !parsed.options.noLegend;
213
- const titleH = showTitle ? TITLE_AREA : 0;
214
- const legendH = hasLegend ? LEGEND_BAND : 0;
215
- const topUsed = titleH + legendH;
216
-
217
- // ── Eras (§28.6a) ──────────────────────────────────────────
218
- // The era sits OPPOSITE the cards (one-sided → opposite the chosen side;
219
- // alternating → below by default). A collapsed era folds into one event-like
220
- // summary card on the cards' side, with a `]` bracket left on the spine.
221
- const eraByName = new Map(parsed.eras.map((e) => [e.name, e]));
222
- const collapsedSet = new Set(
223
- parsed.eras.filter((e) => e.collapsed).map((e) => e.name)
224
- );
225
- const hasEras = parsed.events.some((e) => e.era && eraByName.has(e.era));
226
- // Only EXPANDED eras draw the bottom `]` bracket band (and reserve room for
227
- // it). A collapsed era is marked by a span bracket on the spine instead.
228
- const hasExpandedEra = parsed.events.some(
229
- (e) => e.era && eraByName.has(e.era) && !collapsedSet.has(e.era)
230
- );
231
- const sideOpt = parsed.options.side;
232
- const alternate = sideOpt === 'alternate';
233
- const eraSide: Side = alternate
234
- ? 'below'
235
- : sideOpt === 'above'
236
- ? 'below'
237
- : 'above';
238
-
239
- // Slots in source order: a visible event, or one summary per collapsed era.
240
- type Slot =
241
- | { kind: 'event'; event: EventLineEvent }
242
- | { kind: 'era'; era: EventLineEra; members: EventLineEvent[] };
243
- const slots: Slot[] = [];
244
- const emitted = new Set<string>();
245
- for (const event of parsed.events) {
246
- const name = event.era;
247
- if (name && collapsedSet.has(name)) {
248
- if (!emitted.has(name)) {
249
- emitted.add(name);
250
- slots.push({
251
- kind: 'era',
252
- era: eraByName.get(name)!,
253
- members: parsed.events.filter((e) => e.era === name),
254
- });
255
- }
256
- } else {
257
- slots.push({ kind: 'event', event });
258
- }
259
- }
260
-
261
- // ── Per-slot color + wrapped body + card height ──
262
- const charsPerLine = Math.max(
263
- 8,
264
- Math.floor((CARD_W - CARD_PAD * 2) / (DESC_FONT * CHAR_WIDTH_RATIO))
265
- );
266
- const cardHeight = (lines: WrappedDescLine[], hasDate: boolean): number =>
267
- HEADER_HEIGHT +
268
- (hasDate ? DATE_SUBTITLE_H : 0) +
269
- (lines.length > 0 ? SEPARATOR_GAP + lines.length * DESC_LINE_H : 0) +
270
- (lines.length > 0 ? CARD_PAD : 6);
271
- // An event's solid color = its active-tag color (or the neutral accent).
272
- const eventColor = (ev: EventLineEvent): string => {
273
- if (!activeGroup) return accent;
274
- const tc = resolveTagColor(
275
- ev.metadata,
276
- parsed.tagGroups as TagGroup[],
277
- activeGroup
278
- );
279
- return tc && tc !== NEUTRAL_TAG
280
- ? (resolveColor(tc, palette) ?? tc)
281
- : accent;
282
- };
283
- let evIdx = 0;
284
- const placed: Placed[] = slots.map((slot): Placed => {
285
- // Events AND collapsed eras share the alternation in source order, so a
286
- // collapsed era takes its natural above/below slot like any other entry.
287
- const side: Side = alternate
288
- ? evIdx++ % 2 === 0
289
- ? 'above'
290
- : 'below'
291
- : (sideOpt as Side);
292
- if (slot.kind === 'event') {
293
- const event = slot.event;
294
- const solid = eventColor(event);
295
- const cardFill = shapeFill(palette, solid, isDark, { solid: false });
296
- const titleColor = contrastText(
297
- cardFill,
298
- palette.textOnFillLight,
299
- palette.textOnFillDark
300
- );
301
- const lines = wrapDescription(event.description, charsPerLine);
302
- return {
303
- kind: 'event',
304
- event,
305
- era: null,
306
- members: [],
307
- label: event.label,
308
- date: event.date,
309
- dateValue: event.dateValue,
310
- future: event.future,
311
- futureSpan: event.futureSpan,
312
- lineNumber: event.lineNumber,
313
- eraName: event.era,
314
- color: solid,
315
- cardFill,
316
- titleColor,
317
- lines,
318
- bulletColors: [],
319
- bulletMeta: [],
320
- cardH: cardHeight(lines, !!event.date),
321
- x: 0,
322
- side,
323
- lane: 0,
324
- left: 0,
325
- spanHalf: 0,
326
- };
327
- }
328
- // Collapsed era → an event-like summary card: era name + bulleted members.
329
- // Color: an explicit `[Name] color` wins; otherwise the era is BLACK unless
330
- // every member shares one tag, in which case it adopts that tag color.
331
- const era = slot.era;
332
- const memberColors = slot.members.map(eventColor);
333
- const uniformTag =
334
- activeGroup != null &&
335
- memberColors.length > 0 &&
336
- memberColors.every((c) => c === memberColors[0]) &&
337
- memberColors[0] !== accent;
338
- const solid = era.color
339
- ? (resolveColor(era.color, palette) ?? palette.text)
340
- : uniformTag
341
- ? memberColors[0]!
342
- : palette.text;
343
- // A collapsed era is a folded summary — render it much lighter than an event
344
- // card (a faint ~10% tint) so it recedes rather than competing for attention.
345
- const cardFill = mix(solid, themeBaseBg(palette, isDark), 10);
346
- const titleColor = contrastText(
347
- cardFill,
348
- palette.textOnFillLight,
349
- palette.textOnFillDark
350
- );
351
- // A collapsed era folds a date RANGE into one card. Anchor it at the span
352
- // MIDPOINT so its card sits over the middle of the run, and its bracket (drawn
353
- // later) stretches across the whole [first … last] member range — the folded
354
- // span keeps its true timeline footprint instead of collapsing to a point.
355
- let spanLo: number | null = null;
356
- let spanHi: number | null = null;
357
- for (const m of slot.members) {
358
- if (m.dateValue === null) continue;
359
- if (spanLo === null || m.dateValue < spanLo) spanLo = m.dateValue;
360
- if (spanHi === null || m.dateValue > spanHi) spanHi = m.dateValue;
361
- }
362
- const repDateValue =
363
- spanLo !== null && spanHi !== null ? (spanLo + spanHi) / 2 : null;
364
- // Show every folded member — a collapsed era summarizes the whole run, so
365
- // the card grows to fit rather than truncating with a "+N more" line. A
366
- // member whose tag value is legend-muted (§28.5) drops out of the bullet
367
- // list too — so de-selecting a category quiets it whether its events are
368
- // expanded (collapsed-to-dot) or folded inside a collapsed era. The card
369
- // height auto-shrinks to the surviving bullets.
370
- const visibleMembers = slot.members.filter(
371
- (m) => !eventTagHidden(m.metadata, hiddenSet)
372
- );
373
- const memberStrs = visibleMembers.map(
374
- (m) =>
375
- `• ${m.date ? `${m.future ? 'TBD' : formatDateLabel(m.date)} ` : ''}${m.label}`
376
- );
377
- const bulletColors = visibleMembers.map(eventColor);
378
- const bulletMeta = visibleMembers.map((m) => m.metadata);
379
- const lines = wrapDescription(memberStrs, charsPerLine);
380
- return {
381
- kind: 'era',
382
- event: null,
383
- era,
384
- members: slot.members,
385
- label: era.name,
386
- date: null,
387
- dateValue: repDateValue,
388
- future: false,
389
- futureSpan: null,
390
- lineNumber: era.lineNumber,
391
- eraName: era.name,
392
- color: solid,
393
- cardFill,
394
- titleColor,
395
- lines,
396
- bulletColors,
397
- bulletMeta,
398
- cardH: cardHeight(lines, false),
399
- x: 0,
400
- side,
401
- lane: 0,
402
- left: 0,
403
- spanHalf: 0,
404
- };
405
- });
406
-
407
- // ── X positions ──
408
- // Draw to scale whenever every slot has a date — a collapsed era is anchored at
409
- // its earliest member (above), so it scales like any other point. Even spacing
410
- // is reserved for explicit `no-scale` or a slot that genuinely lacks a date.
411
- const scaled =
412
- parsed.options.scale && placed.every((p) => p.dateValue !== null);
413
- const innerW = width - H_MARGIN * 2;
414
- const MIN_DOT_GAP = 16; // legibility floor between coincident-ish dots
415
- const STRETCH_CAP = 8; // widen the date axis at most this many ×
416
-
417
- // BROKEN AXIS: a collapsed era is COMPRESSED to a fixed-width capsule (its
418
- // squiggle says "lots of timeline folded here"), so the expanded events take
419
- // over the freed width and draw to scale among themselves. The timeline is a
420
- // chronological sequence of SEGMENTS — a capsule for each collapsed era, and a
421
- // to-scale "run" for each maximal stretch of expanded events between them. Dead
422
- // time (gaps with no expanded events) costs only a fixed `SEG_GAP`, never width.
423
- const COLLAPSE_W = 64; // fixed px width of a collapsed-era capsule
424
- const SEG_GAP = 16; // gap between adjacent axis segments
425
- type Seg =
426
- | { kind: 'capsule'; p: Placed }
427
- | { kind: 'run'; events: Placed[]; lo: number; hi: number };
428
- const segments: Seg[] = [];
429
- let run: Placed[] = [];
430
- const flushRun = (): void => {
431
- if (!run.length) return;
432
- const ds = run.map((e) => e.dateValue!);
433
- segments.push({
434
- kind: 'run',
435
- events: run,
436
- lo: Math.min(...ds),
437
- hi: Math.max(...ds),
438
- });
439
- run = [];
440
- };
441
- for (const p of placed) {
442
- if (p.kind === 'era') {
443
- flushRun();
444
- segments.push({ kind: 'capsule', p });
445
- } else {
446
- run.push(p);
447
- }
448
- }
449
- flushRun();
450
- // Total to-scale time = the sum of every expanded run's own date span.
451
- const totalRunTime = segments.reduce(
452
- (s, seg) => s + (seg.kind === 'run' ? seg.hi - seg.lo : 0),
453
- 0
454
- );
455
-
456
- // Position every entry by walking the segments left-to-right. Expanded runs
457
- // collectively scale across `innerW × stretch`; capsules + inter-segment gaps add
458
- // a fixed amount on top. `stretch` (≥1) widens the expanded scale to fill a wide
459
- // panel without distorting proportions; a small MIN_DOT_GAP floor only de-overlaps
460
- // coincident-ish dots. Re-runnable for any stretch so the search below probes freely.
461
- const place = (stretch: number): void => {
462
- if (scaled) {
463
- const pxPerUnit =
464
- totalRunTime > 0 ? (innerW * stretch) / totalRunTime : 0;
465
- // Each collapsed-era card centers on its capsule. When two folded eras land
466
- // on the SAME side their wide cards would overlap (capsules are only
467
- // COLLAPSE_W apart), forcing the card off its bracket. Float the capsule
468
- // right instead — enough that its centered card clears the previous
469
- // same-side era card — so the card stays squarely over its `⊓` (a straight
470
- // vertical leader) and the spine spacing absorbs the slack. Opposite-side
471
- // eras stay tight (their cards don't collide).
472
- const eraCardRight: Record<Side, number> = {
473
- above: -Infinity,
474
- below: -Infinity,
475
- };
476
- let cursor = H_MARGIN;
477
- for (const seg of segments) {
478
- if (seg.kind === 'capsule') {
479
- let x = cursor + COLLAPSE_W / 2;
480
- const minX = Math.max(
481
- CARD_W / 2 + 6, // keep the centered card on-canvas (left ≥ 6)
482
- eraCardRight[seg.p.side] + FAN_GAP + CARD_W / 2 // clear prior same-side card
483
- );
484
- if (x < minX) {
485
- cursor += minX - x;
486
- x = minX;
487
- }
488
- seg.p.x = x;
489
- seg.p.spanHalf = COLLAPSE_W / 2;
490
- eraCardRight[seg.p.side] = x + CARD_W / 2;
491
- cursor += COLLAPSE_W + SEG_GAP;
492
- } else {
493
- for (const e of seg.events) {
494
- e.x = cursor + (e.dateValue! - seg.lo) * pxPerUnit;
495
- e.spanHalf = 0;
496
- }
497
- cursor += (seg.hi - seg.lo) * pxPerUnit + SEG_GAP;
498
- }
499
- }
500
- // Within a run, distinct dates that map closer than MIN_DOT_GAP get nudged
501
- // apart for legibility; identical dates keep a shared x.
502
- let prevX = -Infinity;
503
- let prevVal: number | null = null;
504
- for (const p of [...placed].sort((a, b) => a.x - b.x)) {
505
- if (p.kind === 'era') {
506
- prevX = p.x + p.spanHalf;
507
- prevVal = null;
508
- continue;
509
- }
510
- if (p.dateValue === prevVal) {
511
- p.x = prevX;
512
- continue;
513
- }
514
- if (p.x < prevX + MIN_DOT_GAP) p.x = prevX + MIN_DOT_GAP;
515
- prevX = p.x;
516
- prevVal = p.dateValue;
517
- }
518
- } else {
519
- placed.forEach((p) => {
520
- p.spanHalf = 0;
521
- });
522
- const n = placed.length;
523
- const spacing = n > 1 ? Math.max(MIN_SPACING, innerW / (n - 1)) : 0;
524
- placed.forEach((p, i) => {
525
- p.x = H_MARGIN + i * spacing;
526
- });
527
- }
528
- // ── Spread crowded dots into horizontal slack (scaled only) ──
529
- // When several events bunch up next to a tall collapsed-era card, they get
530
- // shoved into deep lanes far from the spine even though the chart has empty
531
- // room to the right. Nudge each crowded EVENT dot rightward — per side, since
532
- // collisions are per side — just far enough to clear the previous same-side
533
- // card, but never within a card-width of the NEXT obstacle. Era capsules are
534
- // fixed anchors (never moved); a well-spaced event acts as a wall that caps
535
- // the spread. Iterated to a fixpoint so a cluster relaxes as its members move.
536
- // Scale is already non-literal here (collapsed eras break it), so trading a
537
- // dot's exact date position for a shallow, readable lane is a good deal.
538
- if (scaled) {
539
- const SPREAD = CARD_W + FAN_GAP; // min center-to-center so cards don't overlap
540
- for (let iter = 0; iter < 8; iter++) {
541
- let moved = false;
542
- for (const side of ['above', 'below'] as Side[]) {
543
- const obs = placed
544
- .filter((p) => p.side === side)
545
- .sort((a, b) => a.x - b.x);
546
- for (let i = 1; i < obs.length; i++) {
547
- const p = obs[i]!;
548
- if (p.kind !== 'event') continue; // only event dots move; eras anchor
549
- const prev = obs[i - 1]!;
550
- // Coincident-date events legitimately share an x and stack with faded
551
- // leaders — never fan those apart.
552
- if (prev.kind === 'event' && p.dateValue === prev.dateValue)
553
- continue;
554
- const need = prev.x + SPREAD; // clear the previous same-side card
555
- if (need <= p.x) continue;
556
- // Cap only against the next FIXED era anchor (capsules never move); a
557
- // movable event downstream is NOT a cap — it cascades right in turn,
558
- // and a far, well-spaced event naturally stays put (its `need` check
559
- // no-ops) and acts as the wall. Capping by movable events would pin
560
- // the cluster left and leave it stacked behind the tall era card.
561
- const nxt = obs[i + 1];
562
- const cap = nxt?.kind === 'era' ? nxt.x - SPREAD : Infinity;
563
- const target = Math.min(need, cap);
564
- if (target > p.x + 0.5) {
565
- p.x = target;
566
- moved = true;
567
- }
568
- }
569
- }
570
- if (!moved) break;
571
- }
572
- }
573
- // ── Fan side-by-side, stack into lanes when too tight ──
574
- // Each card keeps its dot within [left+INSET, left+CARD_W-INSET] so a vertical
575
- // leader lands on it. A card joins the innermost lane where it clears the
576
- // previous card in that lane by FAN_GAP; otherwise it opens a new lane. Cards
577
- // are NEVER pulled back over a neighbour (no right-edge clamp) — boxes must
578
- // never overlap, so the canvas grows instead (contentW below).
579
- for (const side of ['above', 'below'] as Side[]) {
580
- const arr = placed
581
- .filter((p) => p.side === side)
582
- .sort((a, b) => a.x - b.x);
583
- const laneRight: number[] = [];
584
- for (let i = 0; i < arr.length; i++) {
585
- const p = arr[i]!;
586
- const next = arr[i + 1];
587
- // A collapsed-era summary card centers on its own capsule (the `⊓`
588
- // mark), so consecutive eras cascade exactly with their capsule spacing
589
- // and each card sits over the spot it folds — spatial order = time
590
- // order. Only expanded events fan-pull left when crowded by a neighbour.
591
- const crowdedRight =
592
- p.kind !== 'era' && !!next && next.x - p.x < CARD_W + FAN_GAP;
593
- const preferred = crowdedRight
594
- ? p.x - CARD_W + CARD_INSET
595
- : p.x - CARD_W / 2;
596
- const maxLeft = p.x - CARD_INSET;
597
- const minLeft = p.x - CARD_W + CARD_INSET;
598
- let lane = laneRight.length;
599
- let left = Math.max(preferred, minLeft);
600
- for (let l = 0; l < laneRight.length; l++) {
601
- const want = Math.max(preferred, laneRight[l]! + FAN_GAP, minLeft);
602
- if (want <= maxLeft) {
603
- lane = l;
604
- left = want;
605
- break;
606
- }
607
- }
608
- p.lane = lane;
609
- p.left = Math.max(6, left); // clamp the LEFT edge only — never the right
610
- laneRight[lane] = p.left + CARD_W;
611
- }
612
- }
613
- };
614
-
615
- interface Layout {
616
- contentW: number;
617
- contentH: number; // unclamped content height — used to target the aspect
618
- totalH: number;
619
- spineY: number;
620
- eraBaseY: number;
621
- laneNear: (p: Placed) => number;
622
- }
623
-
624
- // Derive all size-dependent geometry from the current placement.
625
- const derive = (): Layout => {
626
- // Grow the canvas to fit the widest card edge so nothing clips or overlaps.
627
- const contentW = Math.max(
628
- width,
629
- Math.max(...placed.map((p) => p.x + p.spanHalf)) + H_MARGIN,
630
- Math.max(...placed.map((p) => p.left + CARD_W)) + 6
631
- );
632
- // Lanes are packed with VARIABLE heights: each lane is only as tall as its own
633
- // tallest card, so a single 3-bullet card no longer pushes every lane on that
634
- // side down by its height. `laneOffset[side][lane]` is the distance from the
635
- // spine-side leader to that lane's near edge = Σ(prior lane heights + gap).
636
- const laneOffset: Record<Side, number[]> = { above: [], below: [] };
637
- for (const side of ['above', 'below'] as Side[]) {
638
- const laneH: number[] = [];
639
- for (const p of placed) {
640
- if (p.side !== side) continue;
641
- laneH[p.lane] = Math.max(laneH[p.lane] ?? 0, p.cardH);
642
- }
643
- const off: number[] = [];
644
- let acc = 0;
645
- for (let l = 0; l < laneH.length; l++) {
646
- off[l] = acc;
647
- acc += (laneH[l] ?? 0) + LANE_GAP;
648
- }
649
- laneOffset[side] = off;
650
- }
651
- const laneNear = (p: Placed): number =>
652
- (p.side === 'above' ? LEADER_ABOVE : LEADER_BELOW) +
653
- (laneOffset[p.side][p.lane] ?? 0);
654
- const ext = (side: Side): number =>
655
- Math.max(
656
- 0,
657
- ...placed
658
- .filter((p) => p.side === side)
659
- .map((p) => laneNear(p) + p.cardH)
660
- );
661
- // Dates ride inside the cards; the spine carries no tick ruler, so no extra
662
- // band is reserved below it.
663
- const contentAbove = ext('above');
664
- const contentBelow = ext('below');
665
- // The era `]` bracket band lives beyond the content opposite the cards.
666
- const aboveExt =
667
- contentAbove + (hasExpandedEra && eraSide === 'above' ? ERA_BLOCK : 0);
668
- const belowExt =
669
- contentBelow + (hasExpandedEra && eraSide === 'below' ? ERA_BLOCK : 0);
670
- const TOP_PAD = 14;
671
- const BOT_PAD = 14;
672
- const spineY = topUsed + TOP_PAD + aboveExt;
673
- const eraBaseY =
674
- eraSide === 'above'
675
- ? spineY - (contentAbove + 14)
676
- : spineY + (contentBelow + 14);
677
- const contentH = spineY + belowExt + BOT_PAD;
678
- // Live preview fills the panel (heightHint floor) so the diagram doesn't
679
- // float in a short panel; export crops tight to content so the rasterized
680
- // PNG/SVG has no dead whitespace below the cards (mirrors raci's renderer).
681
- const totalH = exportMode ? contentH : Math.max(heightHint, contentH);
682
- return { contentW, contentH, totalH, spineY, eraBaseY, laneNear };
683
- };
684
-
685
- // Choose the axis stretch. The preview/export both fit the whole diagram
686
- // (`meet`), so when the natural layout is TALLER than the available panel the
687
- // fit is height-limited and wastes horizontal space. Widen the date axis — which
688
- // also shortens the layout (fewer lanes) — until the content aspect reaches the
689
- // panel aspect, where the fit-scale peaks. Past that, extra width only shrinks
690
- // it, so stop there. `DGMO_EVT_STRETCH` forces a fixed multiplier.
691
- const envStretch = Number(
692
- (globalThis as { process?: { env?: Record<string, string> } }).process
693
- ?.env?.['DGMO_EVT_STRETCH']
694
- );
695
- let layout: Layout;
696
- if (Number.isFinite(envStretch) && envStretch > 0) {
697
- place(envStretch);
698
- layout = derive();
699
- } else {
700
- place(1);
701
- layout = derive();
702
- const panelAspect = width / heightHint;
703
- const aspectOf = (l: Layout): number => l.contentW / l.contentH;
704
- if (
705
- scaled &&
706
- layout.contentH > heightHint &&
707
- aspectOf(layout) < panelAspect
708
- ) {
709
- place(STRETCH_CAP);
710
- const capped = derive();
711
- if (aspectOf(capped) <= panelAspect) {
712
- layout = capped; // even the max stretch can't reach the panel aspect
713
- } else {
714
- let loF = 1;
715
- let hiF = STRETCH_CAP;
716
- for (let it = 0; it < 8; it++) {
717
- const mid = (loF + hiF) / 2;
718
- place(mid);
719
- if (aspectOf(derive()) >= panelAspect) hiF = mid;
720
- else loF = mid;
721
- }
722
- place(hiF);
723
- layout = derive();
724
- }
725
- }
726
- }
727
- const { contentW, totalH, spineY, eraBaseY, laneNear } = layout;
728
-
729
- // ── SVG root ──
730
- const svg = d3Selection
731
- .select(container)
732
- .append('svg')
733
- .attr('width', contentW)
734
- .attr('height', totalH)
735
- .attr('viewBox', `0 0 ${contentW} ${totalH}`)
736
- .attr('preserveAspectRatio', 'xMidYMin meet')
737
- .attr('xmlns', 'http://www.w3.org/2000/svg')
738
- .style('font-family', FONT_FAMILY);
739
-
740
- if (!exportMode) svg.append('style').text(HOVER_CSS);
741
-
742
- // No opaque full-canvas background rect: the SVG's CSS `background` style
743
- // (and resvg's background option on PNG export) supplies the fill, so
744
- // transparent-theme / Obsidian embeds blend with the host instead of
745
- // showing a dark box — matching every other chart type.
746
-
747
- if (showTitle) {
748
- const t = svg
749
- .append('text')
750
- .attr('class', 'chart-title')
751
- .attr('x', contentW / 2)
752
- .attr('y', TITLE_Y)
753
- .attr('text-anchor', 'middle')
754
- .attr('fill', palette.text)
755
- .attr('font-family', FONT_FAMILY)
756
- .attr('font-size', TITLE_FONT_SIZE)
757
- .attr('font-weight', TITLE_FONT_WEIGHT)
758
- .attr('data-line-number', parsed.titleLineNumber ?? 1)
759
- .text(parsed.title!);
760
- if (onClickItem && parsed.titleLineNumber) {
761
- const ln = parsed.titleLineNumber;
762
- t.style('cursor', 'pointer').on('click', () => onClickItem(ln));
763
- }
764
- }
765
-
766
- // ── Shared tag legend ──
767
- if (hasLegend) {
768
- const legendG = svg
769
- .append('g')
770
- .attr('transform', `translate(0, ${titleH})`);
771
- const groups: LegendGroupData[] = parsed.tagGroups.map((g) => ({
772
- name: g.name,
773
- entries: g.entries.map((e) => ({
774
- value: e.value,
775
- color: resolveColor(e.color, palette) ?? e.color,
776
- })),
777
- }));
778
- renderIntegratedLegend(legendG, {
779
- groups,
780
- palette,
781
- isDark,
782
- width: contentW,
783
- mode: exportMode ? 'export' : 'preview',
784
- activeGroup,
785
- });
786
- // Mute-toggling a category (§28.5) means clicking its legend entry, but the
787
- // painted swatch + label are a tiny target. Back each entry with a
788
- // transparent hit rect spanning its whole bounding box (+ padding) so the
789
- // entire swatch-and-label region is clickable. Preview only — exports carry
790
- // no interaction, so their snapshots are untouched.
791
- if (!exportMode) addLegendEntryHitAreas(legendG);
792
- }
793
-
794
- // ── Spine ──
795
- // Extend to cover collapsed-era brackets, which reach p.x ± spanHalf.
796
- const x0 = Math.min(...placed.map((p) => p.x - p.spanHalf));
797
- const x1 = Math.max(...placed.map((p) => p.x + p.spanHalf));
798
- const spineLeft = x0 - 20;
799
- const spineRight = x1 + 20;
800
- // A trailing TBD (open horizon) turns the spine DASHED past the last real
801
- // event — the timeline literally trails off into the unscheduled future.
802
- const hasTrailingFuture =
803
- scaled && placed.some((p) => p.future && !p.futureSpan);
804
- const realRightX = placed.length
805
- ? Math.max(
806
- spineLeft,
807
- ...placed.filter((p) => !p.future).map((p) => p.x + p.spanHalf)
808
- )
809
- : spineRight;
810
- const solidRight = hasTrailingFuture ? realRightX : spineRight;
811
- svg
812
- .append('line')
813
- .attr('x1', spineLeft)
814
- .attr('y1', spineY)
815
- .attr('x2', solidRight)
816
- .attr('y2', spineY)
817
- .attr('stroke', palette.text)
818
- .attr('stroke-width', 2.5)
819
- .attr('stroke-linecap', 'round');
820
- if (hasTrailingFuture) {
821
- svg
822
- .append('line')
823
- .attr('x1', solidRight)
824
- .attr('y1', spineY)
825
- .attr('x2', spineRight)
826
- .attr('y2', spineY)
827
- .attr('stroke', palette.text)
828
- .attr('stroke-width', 2.5)
829
- .attr('stroke-linecap', 'round')
830
- .attr('stroke-opacity', 0.45)
831
- .attr('stroke-dasharray', '2 6');
832
- }
833
-
834
- // A bracketed TBD plots at an INFERRED point inside a known gap. The hollow
835
- // dot + "TBD" caption carry the uncertain read on their own — no dashed
836
- // "somewhere in here" whisker is drawn across the spine.
837
-
838
- // ── Leaders + cards ──
839
- // Box geometry up front so leaders can test box crossings and so leaders draw
840
- // BEHIND every card (a card always covers the leaders beneath it).
841
- const geo = placed.map((p) => {
842
- const near =
843
- p.side === 'above' ? spineY - laneNear(p) : spineY + laneNear(p);
844
- const top = p.side === 'above' ? near - p.cardH : near;
845
- return { p, near, top };
846
- });
847
- // A leader runs vertically at its dot's x from the spine to its card. If that
848
- // segment passes through ANOTHER card's box, fade it so the text stays clear.
849
- const leaderCrossesBox = (owner: Placed, near: number): boolean => {
850
- const lo = Math.min(spineY, near);
851
- const hi = Math.max(spineY, near);
852
- return geo.some(
853
- (g) =>
854
- g.p !== owner &&
855
- owner.x > g.p.left &&
856
- owner.x < g.p.left + CARD_W &&
857
- hi > g.top &&
858
- lo < g.top + g.p.cardH
859
- );
860
- };
861
-
862
- // A collapsed era's `⊓` bar floats on the card's side; the era card's leader
863
- // stops AT the bar rather than running to the spine.
864
- const eraBarY = (p: Placed): number =>
865
- spineY + ERA_LEG * (p.side === 'above' ? -1 : 1);
866
-
867
- // Leaders first (behind all cards).
868
- for (const { p, near } of geo) {
869
- const spineEnd = p.kind === 'era' ? eraBarY(p) : spineY;
870
- const leader = svg
871
- .append('line')
872
- .attr('class', 'dgmo-event-leader')
873
- .attr('x1', p.x)
874
- .attr('y1', spineEnd)
875
- .attr('x2', p.x)
876
- .attr('y2', near)
877
- .attr('stroke', p.color)
878
- .attr('stroke-width', 1.5)
879
- .attr(
880
- 'stroke-opacity',
881
- leaderCrossesBox(p, near) ? 0.18 : p.future ? 0.4 : 0.65
882
- );
883
- // A future (TBD) event's leader stays SOLID but faded to 40% — the tentative
884
- // read comes from the fade, matching the faded card bar and dot (no dashes).
885
- applyHoverHooks(leader, p);
886
- }
887
-
888
- // Cards on top.
889
- let shelfClipSeq = 0;
890
- for (const { p, top } of geo) {
891
- const left = p.left;
892
-
893
- const cardG = svg
894
- .append('g')
895
- .attr('class', 'dgmo-event-card')
896
- .attr('transform', `translate(${left}, ${top})`)
897
- .attr('data-line-number', p.lineNumber);
898
- applyHoverHooks(cardG, p);
899
- if (p.kind === 'era') {
900
- // App hook: a collapsed-era summary card toggles back open on click.
901
- cardG.attr('data-era', p.era!.name).attr('data-era-collapsed', 'true');
902
- }
903
- if (onClickItem) {
904
- const ln = p.lineNumber;
905
- cardG.style('cursor', 'pointer').on('click', () => onClickItem(ln));
906
- // A transparent hit-rect over the whole card bounds so the ENTIRE card is
907
- // clickable — in no-box mode only the shelf + text glyphs are painted, so
908
- // without this the empty space around a collapsed era's bullet list would
909
- // not register clicks. Added first → sits beneath the visible content.
910
- cardG
911
- .append('rect')
912
- .attr('x', 0)
913
- .attr('y', 0)
914
- .attr('width', CARD_W)
915
- .attr('height', p.cardH)
916
- .attr('fill', 'transparent')
917
- .attr('pointer-events', 'all');
918
- }
919
-
920
- // The date rides as a muted subtitle adjacent to the title, on the side AWAY
921
- // from the spine (so it reads title → date going outward). Collapsed-era cards
922
- // carry no date (their members list their own dates).
923
- const dateStr =
924
- p.kind === 'event' && p.date
925
- ? p.future
926
- ? 'TBD'
927
- : formatDateLabel(p.date)
928
- : null;
929
- const dateH = dateStr ? DATE_SUBTITLE_H : 0;
930
-
931
- if (parsed.options.noBox) {
932
- // Card-less style: a tag-colored label + muted date sitting on a soft
933
- // tag-tinted "shelf", with the leader docking into the shelf's colored
934
- // spine-side edge so the dot→block link stays solid in dense charts. The
935
- // title (the anchor) sits nearest the spine; for above-side blocks the
936
- // header order flips to description → date → title.
937
- const titleNearTop = p.side === 'below';
938
- const headBandTop = titleNearTop ? 0 : p.cardH - HEADER_HEIGHT;
939
- // The shelf wraps the whole header band (title + date). Its spine-side edge
940
- // (top for below-side blocks, bottom otherwise) is where the leader lands.
941
- const shelfTop = titleNearTop ? 0 : p.cardH - HEADER_HEIGHT - dateH;
942
- const shelfH = HEADER_HEIGHT + dateH;
943
- // Clip both the tint and the colored edge to ONE rounded-rect mask so the
944
- // edge carries through the shelf's curved corners (the collapse-bar idiom,
945
- // utils/card.ts) instead of reading as a straight bar bolted on top.
946
- const clipId = `dgmo-evt-shelf-${shelfClipSeq++}`;
947
- cardG
948
- .append('clipPath')
949
- .attr('id', clipId)
950
- .append('rect')
951
- .attr('x', 0)
952
- .attr('y', shelfTop)
953
- .attr('width', CARD_W)
954
- .attr('height', shelfH)
955
- .attr('rx', SHELF_EDGE);
956
- cardG
957
- .append('rect')
958
- .attr('x', 0)
959
- .attr('y', shelfTop)
960
- .attr('width', CARD_W)
961
- .attr('height', shelfH)
962
- .attr('clip-path', `url(#${clipId})`)
963
- .attr('fill', mix(p.color, themeBaseBg(palette, isDark), SHELF_TINT));
964
- const edgeY = titleNearTop ? shelfTop : shelfTop + shelfH - SHELF_EDGE;
965
- // The colored leader-landing edge. A future (TBD) card draws the SAME solid
966
- // edge faded to 40% — the tentative read comes from the fade, matching the
967
- // faded leader + dot (no dashes).
968
- cardG
969
- .append('rect')
970
- .attr('x', 0)
971
- .attr('y', edgeY)
972
- .attr('width', CARD_W)
973
- .attr('height', SHELF_EDGE)
974
- .attr('clip-path', `url(#${clipId})`)
975
- .attr('fill', p.color)
976
- .attr('fill-opacity', p.future ? 0.4 : 1);
977
- cardG
978
- .append('text')
979
- .attr('x', CARD_PAD)
980
- .attr('y', headBandTop + HEADER_HEIGHT / 2 + LABEL_FONT_SIZE / 2 - 2)
981
- .attr('fill', p.color)
982
- .attr('font-family', FONT_FAMILY)
983
- .attr('font-size', LABEL_FONT_SIZE)
984
- .attr('font-weight', 700)
985
- .text(p.label);
986
- if (dateStr) {
987
- cardG
988
- .append('text')
989
- .attr('x', CARD_PAD)
990
- .attr(
991
- 'y',
992
- (titleNearTop ? HEADER_HEIGHT : headBandTop - dateH) +
993
- DATE_SUBTITLE_FONT +
994
- 2
995
- )
996
- .attr('fill', mix(palette.text, palette.bg, 55))
997
- .attr('font-family', FONT_FAMILY)
998
- .attr('font-size', DATE_SUBTITLE_FONT)
999
- .attr('font-weight', 600)
1000
- .text(dateStr);
1001
- }
1002
- if (p.lines.length > 0) {
1003
- const startBaseline = titleNearTop
1004
- ? CARD_BODY_TOP + dateH + DESC_FONT
1005
- : CARD_PAD + DESC_FONT;
1006
- renderBody(
1007
- cardG,
1008
- p.lines,
1009
- palette.text,
1010
- palette,
1011
- startBaseline,
1012
- p.bulletColors,
1013
- p.bulletMeta
1014
- );
1015
- }
1016
- } else {
1017
- // Org-card chrome: rect + bold centered title (reuses utils/card.ts).
1018
- renderNodeCard(cardG, {
1019
- width: CARD_W,
1020
- height: p.cardH,
1021
- rx: CARD_RADIUS,
1022
- fill: p.cardFill,
1023
- stroke: p.color,
1024
- strokeWidth: NODE_STROKE_WIDTH,
1025
- label: p.label,
1026
- labelColor: p.titleColor,
1027
- labelFontSize: LABEL_FONT_SIZE,
1028
- headerHeight: HEADER_HEIGHT,
1029
- });
1030
-
1031
- if (dateStr) {
1032
- cardG
1033
- .append('text')
1034
- .attr('x', CARD_W / 2)
1035
- .attr('y', HEADER_HEIGHT + DATE_SUBTITLE_FONT + 1)
1036
- .attr('text-anchor', 'middle')
1037
- .attr('fill', mix(p.titleColor, p.cardFill, 60))
1038
- .attr('font-family', FONT_FAMILY)
1039
- .attr('font-size', DATE_SUBTITLE_FONT)
1040
- .attr('font-weight', 600)
1041
- .text(dateStr);
1042
- }
1043
-
1044
- if (p.lines.length > 0) {
1045
- // Divider (org convention: 1px, 30% opacity) below the title + date band.
1046
- cardG
1047
- .append('line')
1048
- .attr('x1', 0)
1049
- .attr('y1', HEADER_HEIGHT + dateH)
1050
- .attr('x2', CARD_W)
1051
- .attr('y2', HEADER_HEIGHT + dateH)
1052
- .attr('stroke', p.titleColor)
1053
- .attr('stroke-opacity', 0.3)
1054
- .attr('stroke-width', 1);
1055
- renderBody(
1056
- cardG,
1057
- p.lines,
1058
- p.titleColor,
1059
- palette,
1060
- CARD_BODY_TOP + dateH + DESC_FONT,
1061
- p.bulletColors,
1062
- p.bulletMeta
1063
- );
1064
- }
1065
- }
1066
- }
1067
-
1068
- // No date ruler: dates ride inside the cards, and the spine carries no
1069
- // year/period tick markers (removed by request — the timeline shows order,
1070
- // not a measured date axis).
1071
-
1072
- // ── Dots (events) + span brackets (collapsed eras) on the spine ──
1073
- for (const p of placed) {
1074
- if (p.kind === 'era') {
1075
- // A collapsed era terminates on the spine as a `⊓` stretched across its
1076
- // folded date range (`p.spanHalf`), so it keeps its true timeline footprint;
1077
- // a minimum width keeps a single-date era visible. No dot, no separate bottom
1078
- // bracket — the card title names it. Matches the card's color so the leader
1079
- // reads straight through.
1080
- const col = p.color;
1081
- const half = Math.max(16, p.spanHalf);
1082
- // Clamp the legs to the drawn spine extent so the feet always land ON the
1083
- // line (never poking off the left/right end of the timeline).
1084
- const spineLeft = x0 - 20;
1085
- const spineRight = x1 + 20;
1086
- const bx0 = Math.max(spineLeft, p.x - half);
1087
- const bx1 = Math.min(spineRight, p.x + half);
1088
- // A `⊓`: the bar floats off the spine on the card's side (where the leader
1089
- // lands), and a leg drops from each end down to rest its foot ON the spine.
1090
- const barY = eraBarY(p);
1091
- const eg = svg
1092
- .append('g')
1093
- .attr('class', 'dgmo-event-era')
1094
- .attr('data-era', p.era!.name)
1095
- .attr('data-era-collapsed', 'true')
1096
- .attr('data-line-number', p.lineNumber);
1097
- if (onClickItem) {
1098
- // Transparent hit-rect spanning the whole ⊓ + squiggle so the entire
1099
- // bracket area is clickable, not just the thin strokes. Added first →
1100
- // beneath the visible glyph.
1101
- const hy0 = Math.min(barY, spineY) - 4;
1102
- const hy1 = Math.max(barY, spineY) + 8;
1103
- eg.append('rect')
1104
- .attr('x', bx0)
1105
- .attr('y', hy0)
1106
- .attr('width', bx1 - bx0)
1107
- .attr('height', hy1 - hy0)
1108
- .attr('fill', 'transparent')
1109
- .attr('pointer-events', 'all');
1110
- }
1111
- eg.append('path')
1112
- .attr(
1113
- 'd',
1114
- `M${bx0},${spineY} L${bx0},${barY} L${bx1},${barY} L${bx1},${spineY}`
1115
- )
1116
- .attr('fill', 'none')
1117
- .attr('stroke', col)
1118
- .attr('stroke-width', 1.5)
1119
- .attr('stroke-linecap', 'round')
1120
- .attr('stroke-linejoin', 'round');
1121
- // Axis-break glyph bisecting the bracket: the folded span is not to scale.
1122
- // Blank the spine between the two waves first so the timeline reads as
1123
- // genuinely broken, then draw the squiggles over the gap.
1124
- const breakCx = (bx0 + bx1) / 2;
1125
- eg.append('line')
1126
- .attr('x1', breakCx - BREAK_GAP / 2)
1127
- .attr('y1', spineY)
1128
- .attr('x2', breakCx + BREAK_GAP / 2)
1129
- .attr('y2', spineY)
1130
- .attr('stroke', palette.bg)
1131
- .attr('stroke-width', 5)
1132
- .attr('stroke-linecap', 'butt');
1133
- for (const d of axisBreakPaths(breakCx, spineY)) {
1134
- eg.append('path')
1135
- .attr('d', d)
1136
- .attr('fill', 'none')
1137
- .attr('stroke', palette.text)
1138
- .attr('stroke-width', 2)
1139
- .attr('stroke-linecap', 'round')
1140
- .attr('stroke-linejoin', 'round');
1141
- }
1142
- if (onClickItem) {
1143
- const ln = p.lineNumber;
1144
- eg.style('cursor', 'pointer').on('click', () => onClickItem(ln));
1145
- }
1146
- continue;
1147
- }
1148
- // A real event is a SOLID colored dot; a TBD is a colored RING with a
1149
- // background fill (hollow) — opaque, never transparent. The tentative read
1150
- // also carries through its faded leader + card bar and "TBD" caption.
1151
- const dot = svg
1152
- .append('circle')
1153
- .attr('class', 'dgmo-event-dot')
1154
- .attr('cx', p.x)
1155
- .attr('cy', spineY)
1156
- .attr('r', DOT_R)
1157
- .attr('fill', p.future ? palette.bg : p.color)
1158
- .attr('stroke', p.future ? p.color : palette.bg)
1159
- .attr('stroke-width', 2)
1160
- .attr('data-line-number', p.lineNumber);
1161
- applyHoverHooks(dot, p);
1162
- }
1163
-
1164
- // ── Era `]` brackets ─────────────────────────────────────────
1165
- // A horizontal bracket on the side OPPOSITE the cards marks each era's run.
1166
- // Expanded: spans the run. Collapsed: a short bracket centered on the summary,
1167
- // so the era stays on the timeline even when folded. No chevrons — the bracket
1168
- // (and the summary card) are the affordance; the app wires click-to-toggle.
1169
- if (hasEras) {
1170
- interface EraRun {
1171
- era: EventLineEra;
1172
- collapsed: boolean;
1173
- firstX: number;
1174
- lastX: number;
1175
- }
1176
- const runs: EraRun[] = [];
1177
- let prevName: string | null = null;
1178
- for (const p of placed) {
1179
- const name = p.eraName;
1180
- if (name && eraByName.has(name)) {
1181
- if (prevName === name && runs.length > 0) {
1182
- const r = runs[runs.length - 1]!;
1183
- r.firstX = Math.min(r.firstX, p.x);
1184
- r.lastX = Math.max(r.lastX, p.x);
1185
- } else {
1186
- runs.push({
1187
- era: eraByName.get(name)!,
1188
- collapsed: p.kind === 'era',
1189
- firstX: p.x,
1190
- lastX: p.x,
1191
- });
1192
- }
1193
- prevName = name;
1194
- } else {
1195
- prevName = null;
1196
- }
1197
- }
1198
- const clampX = (v: number): number =>
1199
- Math.max(4, Math.min(contentW - 4, v));
1200
- const cap = eraSide === 'above' ? 1 : -1; // bracket caps point toward the spine
1201
- // Each expanded era's bracket pads its run by ±ERA_BRACKET_PAD. When two
1202
- // adjacent eras' boundary events sit close on the date scale those pads
1203
- // collide, so split the seam at the midpoint between the runs (kept outside
1204
- // each run's own dots) — neighbouring brackets then never overlap.
1205
- const drawn = runs
1206
- .filter((r) => !r.collapsed) // collapsed eras are marked on the spine instead
1207
- .map((r) => ({
1208
- run: r,
1209
- x0: clampX(r.firstX - ERA_BRACKET_PAD),
1210
- x1: clampX(r.lastX + ERA_BRACKET_PAD),
1211
- }))
1212
- .sort((a, b) => a.run.firstX - b.run.firstX);
1213
- for (let i = 1; i < drawn.length; i++) {
1214
- const prev = drawn[i - 1]!;
1215
- const cur = drawn[i]!;
1216
- if (prev.x1 <= cur.x0) continue;
1217
- const mid = (prev.run.lastX + cur.run.firstX) / 2;
1218
- prev.x1 = Math.max(prev.run.lastX, Math.min(prev.x1, mid - ERA_SEAM_GAP));
1219
- cur.x0 = Math.min(cur.run.firstX, Math.max(cur.x0, mid + ERA_SEAM_GAP));
1220
- }
1221
- for (const { run: r, x0, x1 } of drawn) {
1222
- const neutral = !r.era.color;
1223
- const col = neutral
1224
- ? palette.text
1225
- : (resolveColor(r.era.color!, palette) ?? palette.text);
1226
- const op = neutral ? 0.5 : 0.85;
1227
- const y = eraBaseY;
1228
- const eg = svg
1229
- .append('g')
1230
- .attr('class', 'dgmo-event-era')
1231
- .attr('data-era', r.era.name)
1232
- .attr('data-era-collapsed', String(r.collapsed))
1233
- .attr('data-line-number', r.era.lineNumber);
1234
- eg.append('path')
1235
- .attr(
1236
- 'd',
1237
- `M${x0},${y + ERA_BRACKET_CAP * cap} L${x0},${y} L${x1},${y} L${x1},${y + ERA_BRACKET_CAP * cap}`
1238
- )
1239
- .attr('fill', 'none')
1240
- .attr('stroke', col)
1241
- .attr('stroke-width', 1.5)
1242
- .attr('stroke-linecap', 'round')
1243
- .attr('stroke-linejoin', 'round')
1244
- .attr('stroke-opacity', op);
1245
- eg.append('text')
1246
- .attr('x', (r.firstX + r.lastX) / 2)
1247
- .attr('y', eraSide === 'above' ? y - 6 : y + ERA_LABEL_FONT + 4)
1248
- .attr('text-anchor', 'middle')
1249
- .attr('font-family', FONT_FAMILY)
1250
- .attr('font-size', ERA_LABEL_FONT)
1251
- .attr('font-weight', 700)
1252
- .attr('fill', col)
1253
- .attr('fill-opacity', neutral ? 0.85 : 1)
1254
- .text(r.era.name);
1255
- if (onClickItem) {
1256
- const ln = r.era.lineNumber;
1257
- eg.style('cursor', 'pointer').on('click', () => onClickItem(ln));
1258
- }
1259
- }
1260
- }
1261
-
1262
- // ── Hover + legend-toggle wiring (preview only) ──
1263
- // Delegated handlers on the SVG root, so they re-bind with every render and
1264
- // never leak per-node listeners. Hover priority: legend entry → category
1265
- // focus, else era bracket → era + members, else any event piece → whole-event
1266
- // glow. A legend CLICK mutes that category and re-renders.
1267
- if (!exportMode) {
1268
- const root = svg.node();
1269
- wireEventLineHover(root);
1270
- if (root) {
1271
- wireLegendToggle(container, root, () =>
1272
- renderEventLine(
1273
- container,
1274
- parsed,
1275
- palette,
1276
- isDark,
1277
- onClickItem,
1278
- exportDims,
1279
- tagOverride
1280
- )
1281
- );
1282
- // Re-apply the persisted muted state to the freshly built SVG (live event
1283
- // cards collapse-to-dot + legend entries struck/hollow). Era bullets are
1284
- // already excluded at build time above.
1285
- applyEvtHidden(container, root);
1286
- }
1287
- }
1288
- }
1289
-
1290
- /** Share the same hover hooks across every piece of one event: a common
1291
- * `data-evt` id (hover any → light all), the enclosing era for era focus, and
1292
- * `data-tag-<group>` values mirroring the legend's `data-legend-entry`. */
1293
- function applyHoverHooks<E extends d3Selection.BaseType>(
1294
- sel: d3Selection.Selection<E, unknown, null, undefined>,
1295
- p: Placed
1296
- ): void {
1297
- sel.attr('data-evt', p.lineNumber);
1298
- if (p.eraName) sel.attr('data-evt-era', p.eraName);
1299
- if (p.event) {
1300
- for (const [k, v] of Object.entries(p.event.metadata)) {
1301
- sel.attr(`data-tag-${k.toLowerCase()}`, String(v).toLowerCase());
1302
- }
1303
- }
1304
- }
1305
-
1306
- // Everything that can dim. The spine, the year ruler, the title, and the legend
1307
- // carry none of these classes, so they always stay lit — focusing anything
1308
- // fades the rest, never the timeline itself.
1309
- const DIMMABLE_SEL =
1310
- '.dgmo-event-dot,.dgmo-event-leader,.dgmo-event-card,.dgmo-event-era';
1311
- const PIN_ATTR = 'data-evt-pin';
1312
-
1313
- /** A focus target: a single event (by its `data-evt` id, = source line), an era
1314
- * (by name), or a tag value (a legend category). `null` clears the focus. */
1315
- export type EventLineFocus =
1316
- | { readonly kind: 'event'; readonly id: string }
1317
- | { readonly kind: 'era'; readonly name: string }
1318
- | { readonly kind: 'tag'; readonly group: string; readonly value: string };
1319
-
1320
- function clearEvtFocus(root: Element): void {
1321
- root
1322
- .querySelectorAll(`.${HL},.${DIM},.${ERA_HL}`)
1323
- .forEach((el) => el.classList.remove(HL, DIM, ERA_HL));
1324
- }
1325
-
1326
- // Dim every dimmable element that isn't part of the subject; glow/emphasize the
1327
- // subject. A spec that matches nothing is a no-op (never an all-dim void).
1328
- function applyEvtFocus(root: Element, spec: EventLineFocus | null): void {
1329
- clearEvtFocus(root);
1330
- if (!spec) return;
1331
- const els = [...root.querySelectorAll(DIMMABLE_SEL)];
1332
- if (spec.kind === 'event') {
1333
- const keep = (el: Element): boolean =>
1334
- el.getAttribute('data-evt') === spec.id;
1335
- if (!els.some(keep)) return;
1336
- els.forEach((el) => el.classList.add(keep(el) ? HL : DIM));
1337
- return;
1338
- }
1339
- if (spec.kind === 'era') {
1340
- const keep = (el: Element): boolean =>
1341
- el.getAttribute('data-evt-era') === spec.name ||
1342
- (el.classList.contains('dgmo-event-era') &&
1343
- el.getAttribute('data-era') === spec.name);
1344
- if (!els.some(keep)) return;
1345
- els.forEach((el) => {
1346
- if (!keep(el)) el.classList.add(DIM);
1347
- else if (el.classList.contains('dgmo-event-era'))
1348
- el.classList.add(ERA_HL);
1349
- });
1350
- return;
1351
- }
1352
- const attr = `data-tag-${spec.group}`;
1353
- const matches = (el: Element): boolean =>
1354
- el.getAttribute(attr) === spec.value;
1355
- // Member bullets folded inside a collapsed era carry the same `data-tag-*` as
1356
- // their source events, so a legend hover can surface them too.
1357
- const bullets = [...root.querySelectorAll('.dgmo-evt-bullet')];
1358
- // An era that holds ≥1 matching member bullet stays lit (its card, spine glyph,
1359
- // and leader), with the non-matching member bullets dimmed inside it.
1360
- const litEras = new Set<string>();
1361
- for (const b of bullets) {
1362
- if (!matches(b)) continue;
1363
- const card = b.closest('.dgmo-event-card');
1364
- const era =
1365
- card?.getAttribute('data-era') ?? card?.getAttribute('data-evt-era');
1366
- if (era) litEras.add(era);
1367
- }
1368
- if (!els.some(matches) && litEras.size === 0) return;
1369
- const inLitEra = (el: Element): boolean => {
1370
- const era = el.getAttribute('data-era') ?? el.getAttribute('data-evt-era');
1371
- return !!era && litEras.has(era);
1372
- };
1373
- els.forEach((el) => {
1374
- if (!matches(el) && !inLitEra(el)) el.classList.add(DIM);
1375
- });
1376
- for (const b of bullets) if (!matches(b)) b.classList.add(DIM);
1377
- }
1378
-
1379
- function readPin(root: Element): EventLineFocus | null {
1380
- const raw = root.getAttribute(PIN_ATTR);
1381
- if (!raw) return null;
1382
- try {
1383
- return JSON.parse(raw) as EventLineFocus;
1384
- } catch {
1385
- return null;
1386
- }
1387
- }
1388
-
1389
- /**
1390
- * Clear the preview-only legend-muted tag set persisted on a container (the
1391
- * collapsed-to-dot categories from §28.5). View state is per-document, so a host
1392
- * resets it when the source changes (switching files / edits) — the same
1393
- * contract the app uses for live era collapse toggles. No-op if nothing is set.
1394
- */
1395
- export function clearEventLineMuted(container: HTMLElement): void {
1396
- container.removeAttribute(HIDDEN_ATTR);
1397
- }
1398
-
1399
- /**
1400
- * Pin a persistent focus on a rendered event-line — e.g. driven by the editor
1401
- * cursor — dimming everything except the target, exactly like hover. Hovering
1402
- * temporarily overrides the pin; leaving the diagram reverts to it. Pass `null`
1403
- * to clear. No-op when the container holds no event-line SVG.
1404
- */
1405
- export function focusEventLine(
1406
- container: HTMLElement,
1407
- spec: EventLineFocus | null
1408
- ): void {
1409
- const root = container.querySelector('svg');
1410
- if (!root) return;
1411
- if (spec) root.setAttribute(PIN_ATTR, JSON.stringify(spec));
1412
- else root.removeAttribute(PIN_ATTR);
1413
- applyEvtFocus(root, spec);
1414
- }
1415
-
1416
- // ── Legend-toggled card hiding (preview only) ───────────────
1417
- // A muted category is recorded on the CONTAINER (not the throwaway SVG) as a
1418
- // `"group:value"` list, so it survives every re-render — era collapse/expand,
1419
- // resize, palette change. The hidden set is independent of focus state
1420
- // (HL/DIM): collapse and focus compose because they live in different classes.
1421
- function readHidden(el: Element): Set<string> {
1422
- const raw = el.getAttribute(HIDDEN_ATTR);
1423
- if (!raw) return new Set();
1424
- try {
1425
- return new Set(JSON.parse(raw) as string[]);
1426
- } catch {
1427
- return new Set();
1428
- }
1429
- }
1430
-
1431
- // Prepend a transparent hit rect to each legend entry so the full swatch+label
1432
- // region toggles its category, not just the tiny dot/glyphs. Sized from the
1433
- // entry's own bbox (+ padding) and inserted first so it sits behind the visible
1434
- // marks; a delegated click on it still resolves to the entry via `closest()`.
1435
- function addLegendEntryHitAreas(
1436
- legendG: d3Selection.Selection<SVGGElement, unknown, null, undefined>
1437
- ): void {
1438
- const PAD_X = 6;
1439
- const PAD_Y = 7;
1440
- legendG
1441
- .selectAll<SVGGElement, unknown>('[data-legend-entry]')
1442
- .each(function () {
1443
- let bb: { x: number; y: number; width: number; height: number };
1444
- try {
1445
- bb = this.getBBox();
1446
- } catch {
1447
- return; // no layout (e.g. jsdom) — the entry itself stays clickable
1448
- }
1449
- if (!bb.width || !bb.height) return;
1450
- d3Selection
1451
- .select(this)
1452
- .insert('rect', ':first-child')
1453
- .attr('x', bb.x - PAD_X)
1454
- .attr('y', bb.y - PAD_Y)
1455
- .attr('width', bb.width + PAD_X * 2)
1456
- .attr('height', bb.height + PAD_Y * 2)
1457
- .attr('fill', 'transparent')
1458
- .attr('data-legend-hit', '');
1459
- });
1460
- }
1461
-
1462
- // True when an event carries any legend-muted `group:value`. The keying matches
1463
- // `applyHoverHooks` (metadata key + value, both lowercased) so it lines up with
1464
- // the `data-tag-<group>` attributes and the legend's `data-legend-entry`.
1465
- function eventTagHidden(
1466
- metadata: Record<string, string>,
1467
- hidden: ReadonlySet<string>
1468
- ): boolean {
1469
- if (hidden.size === 0) return false;
1470
- for (const [k, v] of Object.entries(metadata)) {
1471
- if (hidden.has(`${k.toLowerCase()}:${String(v).toLowerCase()}`))
1472
- return true;
1473
- }
1474
- return false;
1475
- }
1476
-
1477
- // Reconcile the rendered SVG to the container's hidden set: collapse matching
1478
- // cards/leaders (never the dot) and mark the matching legend entries muted
1479
- // (struck label + hollow swatch). The collapsed-era BULLETS are handled at build
1480
- // time (excluded from the summary card); this covers the live event cards and
1481
- // the legend chrome. Idempotent — safe to call on every render.
1482
- function applyEvtHidden(container: Element, root: Element): void {
1483
- const hidden = readHidden(container);
1484
- const isHidden = (el: Element): boolean => {
1485
- for (const key of hidden) {
1486
- const sep = key.indexOf(':');
1487
- if (sep < 0) continue;
1488
- const group = key.slice(0, sep);
1489
- const value = key.slice(sep + 1);
1490
- if (el.getAttribute(`data-tag-${group}`) === value) return true;
1491
- }
1492
- return false;
1493
- };
1494
- root
1495
- .querySelectorAll('.dgmo-event-card,.dgmo-event-leader')
1496
- .forEach((el) => el.classList.toggle(COLLAPSED, isHidden(el)));
1497
- root.querySelectorAll('[data-legend-entry]').forEach((entry) => {
1498
- const group = entry
1499
- .closest('[data-legend-group]')
1500
- ?.getAttribute('data-legend-group');
1501
- const value = entry.getAttribute('data-legend-entry');
1502
- setLegendEntryOff(entry, !!group && hidden.has(`${group}:${value}`));
1503
- });
1504
- }
1505
-
1506
- // Hollow the legend swatch (fill → none, stroke = its own color) for the muted
1507
- // state, restoring the stashed fill when un-muted.
1508
- function setLegendEntryOff(entry: Element, off: boolean): void {
1509
- entry.classList.toggle(OFF, off);
1510
- const c = entry.querySelector('circle');
1511
- if (!c) return;
1512
- if (off) {
1513
- if (!c.hasAttribute('data-fill0'))
1514
- c.setAttribute('data-fill0', c.getAttribute('fill') ?? '');
1515
- c.setAttribute('fill', 'none');
1516
- c.setAttribute('stroke', c.getAttribute('data-fill0') || 'currentColor');
1517
- c.setAttribute('stroke-width', '1.5');
1518
- } else {
1519
- const fill0 = c.getAttribute('data-fill0');
1520
- if (fill0 !== null) c.setAttribute('fill', fill0);
1521
- c.removeAttribute('stroke');
1522
- c.removeAttribute('stroke-width');
1523
- }
1524
- }
1525
-
1526
- function toggleHidden(container: Element, group: string, value: string): void {
1527
- const hidden = readHidden(container);
1528
- const key = `${group}:${value}`;
1529
- if (hidden.has(key)) hidden.delete(key);
1530
- else hidden.add(key);
1531
- container.setAttribute(HIDDEN_ATTR, JSON.stringify([...hidden]));
1532
- }
1533
-
1534
- // A click on a legend entry mutes/un-mutes that tag value, then re-renders so
1535
- // both effects flow from one build: live event cards collapse-to-dot AND any
1536
- // member folded inside a collapsed era drops out of its summary bullets. The
1537
- // hidden set lives on the container, so the rebuild (and any later era toggle /
1538
- // resize) reads it back and stays in sync. Renderer-owned, so it behaves the
1539
- // same in the app, web editor, and Obsidian.
1540
- function wireLegendToggle(
1541
- container: HTMLElement,
1542
- root: SVGSVGElement,
1543
- rerender: () => void
1544
- ): void {
1545
- root.addEventListener('click', (e) => {
1546
- const t = e.target as Element | null;
1547
- if (!t || typeof t.closest !== 'function') return;
1548
- const entry = t.closest('[data-legend-entry]');
1549
- if (!entry) return;
1550
- const value = entry.getAttribute('data-legend-entry');
1551
- const group = entry
1552
- .closest('[data-legend-group]')
1553
- ?.getAttribute('data-legend-group');
1554
- if (!group || !value) return;
1555
- toggleHidden(container, group, value);
1556
- // Preserve the editor-cursor focus pin across the rebuild — the host
1557
- // re-applies it only on its own renders, not on this renderer-owned one.
1558
- const pin = readPin(root);
1559
- rerender();
1560
- if (pin) focusEventLine(container, pin);
1561
- // This rebuild replaced the SVG, so any post-render normalization the host
1562
- // applied to the previous root (e.g. fit-to-canvas sizing) is gone. Signal
1563
- // the host to re-apply it. Hosts that render the renderer's intrinsic size
1564
- // verbatim can ignore the event.
1565
- if (typeof CustomEvent !== 'undefined')
1566
- container.dispatchEvent(new CustomEvent('dgmo-event-line-rerender'));
1567
- });
1568
- }
1569
-
1570
- // One delegated handler: hover focuses transiently; on mouseout (or over bare
1571
- // canvas) it reverts to the pinned cursor focus rather than clearing outright.
1572
- function wireEventLineHover(root: SVGSVGElement | null): void {
1573
- if (!root) return;
1574
- root.addEventListener('mouseover', (e) => {
1575
- const t = e.target as Element | null;
1576
- if (!t || typeof t.closest !== 'function') return;
1577
-
1578
- const entry = t.closest('[data-legend-entry]');
1579
- if (entry) {
1580
- const value = entry.getAttribute('data-legend-entry');
1581
- const group = entry
1582
- .closest('[data-legend-group]')
1583
- ?.getAttribute('data-legend-group');
1584
- applyEvtFocus(
1585
- root,
1586
- group && value ? { kind: 'tag', group, value } : readPin(root)
1587
- );
1588
- return;
1589
- }
1590
- const era = t.closest('.dgmo-event-era');
1591
- if (era) {
1592
- applyEvtFocus(root, {
1593
- kind: 'era',
1594
- name: era.getAttribute('data-era') ?? '',
1595
- });
1596
- return;
1597
- }
1598
- const evt = t.closest('[data-evt]');
1599
- if (evt) {
1600
- // A collapsed-era summary card carries `data-era` as well as `data-evt`;
1601
- // focus it as an ERA so its `⊓` bracket + axis-break squiggle (a
1602
- // `.dgmo-event-era` group, no `data-evt`) stay lit with the card instead
1603
- // of fading. Plain events focus by their `data-evt` id.
1604
- const eraName = evt.getAttribute('data-era');
1605
- applyEvtFocus(
1606
- root,
1607
- eraName
1608
- ? { kind: 'era', name: eraName }
1609
- : { kind: 'event', id: evt.getAttribute('data-evt') ?? '' }
1610
- );
1611
- return;
1612
- }
1613
- // Bare canvas / title / legend chrome → revert to the pinned focus.
1614
- applyEvtFocus(root, readPin(root));
1615
- });
1616
- root.addEventListener('mouseout', (e) => {
1617
- const to = (e as MouseEvent).relatedTarget as Node | null;
1618
- if (!to || !root.contains(to)) applyEvtFocus(root, readPin(root));
1619
- });
1620
- }
1621
-
1622
- export function renderEventLineForExport(
1623
- container: HTMLDivElement,
1624
- parsed: ParsedEventLine,
1625
- palette: PaletteColors,
1626
- isDark: boolean,
1627
- exportDims?: D3ExportDimensions,
1628
- tagOverride?: string
1629
- ): void {
1630
- renderEventLine(
1631
- container,
1632
- parsed,
1633
- palette,
1634
- isDark,
1635
- undefined,
1636
- exportDims,
1637
- tagOverride
1638
- );
1639
- }
1640
-
1641
- // ── helpers ──
1642
-
1643
- // A vertical "≈ rotated 90°" axis-break glyph: two parallel wavy strokes that
1644
- // cross the timeline at a collapsed era to signal the span there is folded and
1645
- // NOT drawn to scale. Returns the two path `d` strings, centered on (cx, cy).
1646
- function axisBreakPaths(cx: number, cy: number): [string, string] {
1647
- const HUMPS = 2;
1648
- const STEPS = 48;
1649
- const wave = (x0: number): string => {
1650
- let d = '';
1651
- for (let i = 0; i <= STEPS; i++) {
1652
- const t = i / STEPS;
1653
- const y = cy - BREAK_HALF_H + t * 2 * BREAK_HALF_H;
1654
- const x = x0 + BREAK_AMP * Math.sin(t * Math.PI * HUMPS);
1655
- d += `${i === 0 ? 'M' : 'L'}${x.toFixed(2)},${y.toFixed(2)}`;
1656
- if (i < STEPS) d += ' ';
1657
- }
1658
- return d;
1659
- };
1660
- return [wave(cx - BREAK_GAP / 2), wave(cx + BREAK_GAP / 2)];
1661
- }
1662
-
1663
- function wrapDescription(
1664
- lines: readonly string[],
1665
- charsPerLine: number
1666
- ): WrappedDescLine[] {
1667
- const out: WrappedDescLine[] = [];
1668
- for (const line of lines) {
1669
- if (line === '') {
1670
- out.push({ text: '', kind: 'plain' });
1671
- continue;
1672
- }
1673
- out.push(...wrapDescriptionLines([line], charsPerLine));
1674
- }
1675
- return out;
1676
- }
1677
-
1678
- function renderBody(
1679
- cardG: d3Selection.Selection<SVGGElement, unknown, null, undefined>,
1680
- lines: WrappedDescLine[],
1681
- bodyColor: string,
1682
- palette: PaletteColors,
1683
- startBaseline = CARD_BODY_TOP + DESC_FONT,
1684
- // Per-bullet color (collapsed-era member list — each bullet takes its event's
1685
- // tag color). Consumed in bullet-first order; falls back to bodyColor.
1686
- bulletColors: readonly string[] = [],
1687
- // Per-bullet tag metadata, same order as bulletColors. Stamped on each bullet
1688
- // line as `data-tag-<group>` (+ the `dgmo-evt-bullet` class) so a legend hover
1689
- // can highlight matching members inside a folded era.
1690
- bulletMeta: readonly Readonly<Record<string, string>>[] = []
1691
- ): void {
1692
- let y = startBaseline;
1693
- let bulletIdx = 0;
1694
- // The whole bulleted entry (marker + text) takes its member's tag color so a
1695
- // collapsed era reads like a mini tag-colored list; continuation lines keep
1696
- // it, plain lines (e.g. "+N more") fall back to the body color.
1697
- let lineColor = bodyColor;
1698
- let lineMeta: Readonly<Record<string, string>> | null = null;
1699
- const tagBullet = (
1700
- el: d3Selection.Selection<SVGTextElement, unknown, null, undefined>
1701
- ): void => {
1702
- if (!lineMeta) return;
1703
- el.attr('class', 'dgmo-evt-bullet');
1704
- for (const [k, v] of Object.entries(lineMeta))
1705
- el.attr(`data-tag-${k.toLowerCase()}`, String(v).toLowerCase());
1706
- };
1707
- for (const line of lines) {
1708
- const isBullet =
1709
- line.kind === 'bullet-first' || line.kind === 'bullet-cont';
1710
- const bodyX = CARD_PAD + (isBullet ? 12 : 0);
1711
- if (line.kind === 'bullet-first') {
1712
- lineColor = bulletColors[bulletIdx] ?? bodyColor;
1713
- lineMeta = bulletMeta[bulletIdx] ?? null;
1714
- bulletIdx++;
1715
- const marker = cardG
1716
- .append('text')
1717
- .attr('x', CARD_PAD)
1718
- .attr('y', y)
1719
- .attr('text-anchor', 'start')
1720
- .attr('fill', lineColor)
1721
- .attr('font-family', FONT_FAMILY)
1722
- .attr('font-size', DESC_FONT)
1723
- .attr('font-weight', 700)
1724
- .text('•');
1725
- tagBullet(marker);
1726
- } else if (!isBullet) {
1727
- lineColor = bodyColor;
1728
- lineMeta = null;
1729
- }
1730
- const t = cardG
1731
- .append('text')
1732
- .attr('x', bodyX)
1733
- .attr('y', y)
1734
- .attr('text-anchor', 'start')
1735
- .attr('fill', isBullet ? lineColor : bodyColor)
1736
- .attr('font-family', FONT_FAMILY)
1737
- .attr('font-size', DESC_FONT);
1738
- if (isBullet) tagBullet(t);
1739
- renderInlineText(t, line.text, palette);
1740
- y += DESC_LINE_H;
1741
- }
1742
- }