@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,1445 +0,0 @@
1
- // Renderer (step 4, part 2): MapLayout → SVG, via d3-selection. THIN — all
2
- // geometry/color/collision decisions are layout.ts; this only emits attributes.
3
- // Layering regions → legs(edges/routes) → POIs → labels → legend (§24B.11).
4
- // Mirrors every structured renderer's contract (container in, SVG appended,
5
- // returns void). Legend uses renderLegendD3 — the d3-selection legend path —
6
- // NOT the string renderLegendSvg (AR1).
7
- import * as d3Selection from 'd3-selection';
8
- import { FONT_FAMILY } from '../fonts';
9
- import {
10
- TITLE_FONT_SIZE,
11
- TITLE_FONT_WEIGHT,
12
- TITLE_Y,
13
- } from '../utils/title-constants';
14
- import { mix } from '../palettes/color-utils';
15
- import { measureText } from '../utils/text-measure';
16
- import { renderIntegratedLegend } from '../utils/legend-integration';
17
- import type { LegendConfig } from '../utils/legend-types';
18
- import { mapLegendConfig, mapLegendGroups } from './legend-band';
19
- import type { PaletteColors } from '../palettes/types';
20
- import type { D3ExportDimensions } from '../utils/d3-types';
21
- import type { MapData, ResolvedMap } from './resolved-types';
22
- import {
23
- layoutMap,
24
- parsePathRings,
25
- type MapLayoutRegion,
26
- type MapLayoutCoastlineStyle,
27
- type PlacedLabel,
28
- } from './layout';
29
-
30
- const LABEL_FONT = 11;
31
-
32
- // ── Coastline water-lines helpers (opt-in `coastline`, §24B.2) ──
33
- // Geometry is derived from the already-drawn region paths: each outer ring is
34
- // buffered as a symmetric SVG stroke band then eroded (flat-water overdraw) to a
35
- // thin offshore ring; a luminance <mask> reveals only the water side. See the
36
- // render block + ADR-1/6 in the tech-spec.
37
-
38
- /** Even-odd point-in-ring test (screen space). */
39
- function pointInRing(
40
- px: number,
41
- py: number,
42
- ring: ReadonlyArray<[number, number]>
43
- ): boolean {
44
- let inside = false;
45
- for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46
- const [xi, yi] = ring[i]!;
47
- const [xj, yj] = ring[j]!;
48
- if (yi > py !== yj > py && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi)
49
- inside = !inside;
50
- }
51
- return inside;
52
- }
53
-
54
- /** Build an SVG subpath `d` (`M…L…Z`) from a ring's points. */
55
- function ringToPath(ring: ReadonlyArray<[number, number]>): string {
56
- let d = '';
57
- for (let i = 0; i < ring.length; i++)
58
- d += (i ? 'L' : 'M') + ring[i]![0] + ',' + ring[i]![1];
59
- return d + 'Z';
60
- }
61
-
62
- /** Open SVG polyline (`M…L…`, no `Z`) from a run of points. */
63
- function polylineToPath(pts: ReadonlyArray<[number, number]>): string {
64
- let d = '';
65
- for (let i = 0; i < pts.length; i++)
66
- d += (i ? 'L' : 'M') + pts[i]![0] + ',' + pts[i]![1];
67
- return d;
68
- }
69
-
70
- /** Coast subpaths for one ring, dropping any edge that runs ALONG a canvas edge.
71
- * A region clipped to the viewport (the antimeridian on a world map, or a
72
- * regional clipExtent cut) gains a synthetic straight edge collinear with the
73
- * frame — that edge is NOT a real coast and must not be buffered into a coast
74
- * band (which would ring the cut with water-lines short of the edge). Without a
75
- * `frame` the ring is returned closed (`M…Z`) as before. With one, the ring is
76
- * split at every frame-collinear edge into open coast arcs (`M…L…`), so the land
77
- * runs cleanly to the edge and only true coastline gets a water-line. */
78
- function ringToCoastPaths(
79
- ring: ReadonlyArray<[number, number]>,
80
- frame?: { w: number; h: number }
81
- ): string[] {
82
- if (!frame) return [ringToPath(ring)];
83
- const n = ring.length;
84
- const eps = 0.75;
85
- const onL = (x: number): boolean => Math.abs(x) <= eps;
86
- const onR = (x: number): boolean => Math.abs(x - frame.w) <= eps;
87
- const onT = (y: number): boolean => Math.abs(y) <= eps;
88
- const onB = (y: number): boolean => Math.abs(y - frame.h) <= eps;
89
- const isFrameEdge = (
90
- a: readonly [number, number],
91
- b: readonly [number, number]
92
- ): boolean =>
93
- (onL(a[0]) && onL(b[0])) ||
94
- (onR(a[0]) && onR(b[0])) ||
95
- (onT(a[1]) && onT(b[1])) ||
96
- (onB(a[1]) && onB(b[1]));
97
- // No frame-collinear edge anywhere → ordinary interior coastline (closed).
98
- let firstBreak = -1;
99
- for (let i = 0; i < n; i++)
100
- if (isFrameEdge(ring[i]!, ring[(i + 1) % n]!)) {
101
- firstBreak = i;
102
- break;
103
- }
104
- if (firstBreak === -1) return [ringToPath(ring)];
105
- // Walk the loop from just after the first cut, accumulating runs of real-coast
106
- // edges into open polylines and breaking at each frame-collinear edge.
107
- const paths: string[] = [];
108
- let cur: Array<[number, number]> = [];
109
- const start = (firstBreak + 1) % n;
110
- for (let k = 0; k < n; k++) {
111
- const i = (start + k) % n;
112
- const a = ring[i]!;
113
- const b = ring[(i + 1) % n]!;
114
- if (isFrameEdge(a, b)) {
115
- if (cur.length >= 2) paths.push(polylineToPath(cur));
116
- cur = [];
117
- continue;
118
- }
119
- if (cur.length === 0) cur.push(a);
120
- cur.push(b);
121
- }
122
- if (cur.length >= 2) paths.push(polylineToPath(cur));
123
- return paths;
124
- }
125
-
126
- /** Coast outlines to buffer: every region's OUTER rings whose bbox extent clears
127
- * `minExtent`. Holes/enclaves are skipped via containment depth (even depth =
128
- * outer landmass boundary, odd = a hole) so an enclave (Lesotho) or a lake-hole
129
- * is never ringed as a fake coast on land (R11). `minExtent` is a bare
130
- * degenerate-ring floor now — every island, however small, grows coast rings. */
131
- function coastlineOuterRings(
132
- regions: readonly MapLayoutRegion[],
133
- minExtent: number,
134
- frame?: { w: number; h: number }
135
- ): string[] {
136
- const paths: string[] = [];
137
- for (const r of regions) {
138
- // Reuse the rings parsed once in layoutMap; fall back for older layouts.
139
- const rings =
140
- (r.rings as Array<Array<[number, number]>>) ?? parsePathRings(r.d);
141
- for (let i = 0; i < rings.length; i++) {
142
- const ring = rings[i]!;
143
- if (ring.length < 3) continue;
144
- let minX = Infinity;
145
- let minY = Infinity;
146
- let maxX = -Infinity;
147
- let maxY = -Infinity;
148
- for (const [x, y] of ring) {
149
- if (x < minX) minX = x;
150
- if (x > maxX) maxX = x;
151
- if (y < minY) minY = y;
152
- if (y > maxY) maxY = y;
153
- }
154
- if (Math.max(maxX - minX, maxY - minY) < minExtent) continue;
155
- const [fx, fy] = ring[0]!;
156
- let depth = 0;
157
- for (let j = 0; j < rings.length; j++)
158
- if (j !== i && pointInRing(fx, fy, rings[j]!)) depth++;
159
- if (depth % 2 === 1) continue; // hole/enclave — skip
160
- paths.push(...ringToCoastPaths(ring, frame));
161
- }
162
- }
163
- return paths;
164
- }
165
-
166
- /** Stroke the coast-parallel water-lines into a masked group. Per line, outer→
167
- * inner so the inner ring draws on top: a colour pass (the symmetric buffer
168
- * band) then a flat-water overdraw that erodes it to a thin offshore ring. The
169
- * group's `<mask>` keeps only the water-side half of each band.
170
- *
171
- * The outer→inner ordering protects a single ring (the inner band never reaches
172
- * the outer ring because `d1+thickness < d2`, the layout invariant). It does NOT
173
- * protect across regions: where two coasts sit closer than ~2·d1 (a tripoint, a
174
- * narrow strait, an inset box edge), one region's flat-water overdraw can paint
175
- * over a neighbour's inner ring — the same accepted "tripoint stub / narrow
176
- * inlet fills solid" artifact the tech-spec calls out, bounded by small d.
177
- *
178
- * Perf: every outer ring in a given (level, pass) shares identical stroke
179
- * attributes, so they collapse into ONE multi-subpath `<path>` (each ring's
180
- * `d` already starts with `M`, so joining is a valid compound path). A world
181
- * map drops from ~6k coastline paths to 2 per ring-level (~10 total), which is
182
- * ~87% of the whole map's path count — the dominant cost in any repaint. The
183
- * only visible consequence: the colour-band pass strokes at `stroke-opacity`,
184
- * and a single path's stroke rasterises as ONE coverage region, so adjacent
185
- * bands that overlap (straits/tripoints) no longer double-darken — they read
186
- * uniform, which is if anything cleaner. Draw order (all colour bands, then all
187
- * flat-water, outer level first) is unchanged, so the single-ring invariant and
188
- * the accepted cross-region overdraw artifact behave exactly as before. */
189
- function appendWaterLines(
190
- g: Sel,
191
- defs: Sel,
192
- coastId: string,
193
- outerRings: readonly string[],
194
- style: MapLayoutCoastlineStyle,
195
- flatWater: string
196
- ): void {
197
- const d = outerRings.join(' ');
198
- if (!d) return;
199
- // The coast geometry is byte-for-byte identical across every line-level and
200
- // both passes (only stroke colour/width differ). On a world map that compound
201
- // path is ~200KB, so emitting it ~12× inline ballooned the SVG to multi-MB and
202
- // overflowed the SSG HTML reparse. Define it once and `<use>` it: same pixels,
203
- // a fraction of the bytes.
204
- defs.append('path').attr('id', coastId).attr('d', d).attr('fill', 'none');
205
- const ref = `#${coastId}`;
206
- const linesOuterFirst = [...style.lines].sort((a, b) => b.d - a.d);
207
- for (const line of linesOuterFirst) {
208
- g.append('use')
209
- .attr('href', ref)
210
- .attr('stroke', style.color)
211
- .attr('stroke-width', 2 * (line.d + line.thickness))
212
- .attr('stroke-opacity', line.opacity)
213
- .attr('stroke-linejoin', 'round')
214
- .attr('stroke-linecap', 'round');
215
- g.append('use')
216
- .attr('href', ref)
217
- .attr('stroke', flatWater)
218
- .attr('stroke-width', 2 * line.d)
219
- .attr('stroke-linejoin', 'round')
220
- .attr('stroke-linecap', 'round');
221
- }
222
- }
223
-
224
- // Per-render namespace for SVG def ids (clipPaths, masks, filters, markers, the
225
- // `<use>`-shared coast path). SVG `url(#id)` / `href="#id"` resolve document-
226
- // globally to the FIRST matching id, so when several maps are inlined on one
227
- // page (the docs gallery, an MDX page) shared constant ids made every later map
228
- // reference the first map's defs — its coast `<use>` ghosted through. A
229
- // monotonic per-render suffix makes every render's ids unique. NOT reset between
230
- // renders, so re-renders (legend flips) and same-page siblings never collide.
231
- let mapInstanceCounter = 0;
232
-
233
- /** Render a resolved map into `container` (d3-selection appends an `<svg>`). */
234
- export function renderMap(
235
- container: HTMLDivElement,
236
- resolved: ResolvedMap,
237
- data: MapData,
238
- palette: PaletteColors,
239
- isDark: boolean,
240
- onClickItem?: (lineNumber: number) => void,
241
- exportDims?: D3ExportDimensions,
242
- /** Live override of the active colouring group (interactive legend flip). */
243
- activeGroupOverride?: string | null
244
- ): void {
245
- d3Selection.select(container).selectAll(':not([data-d3-tooltip])').remove();
246
- const width = exportDims?.width ?? container.clientWidth;
247
- const height = exportDims?.height ?? container.clientHeight;
248
- if (width <= 0 || height <= 0) return;
249
-
250
- const layout = layoutMap(
251
- resolved,
252
- data,
253
- { width, height },
254
- {
255
- palette,
256
- isDark,
257
- // Export-only: forward the contain-fit request from mapExportDimensions so a
258
- // clamped/floored (off-aspect) export canvas letterboxes instead of
259
- // stretch-distorting. The in-app preview pane passes no exportDims → unset →
260
- // keeps the global stretch-fill.
261
- preferContain: exportDims?.preferContain ?? false,
262
- // Reserve the legend band for the mode actually drawn below (export shows
263
- // only the active group; preview keeps the inactive pills).
264
- legendMode: exportDims ? 'export' : 'preview',
265
- ...(activeGroupOverride !== undefined && {
266
- activeGroup: activeGroupOverride,
267
- }),
268
- }
269
- );
270
-
271
- const svg = d3Selection
272
- .select(container)
273
- .append('svg')
274
- .attr('width', width)
275
- .attr('height', height)
276
- .attr('viewBox', `0 0 ${width} ${height}`)
277
- .attr('preserveAspectRatio', 'xMidYMin meet')
278
- .attr('xmlns', 'http://www.w3.org/2000/svg')
279
- .style('font-family', FONT_FAMILY)
280
- // Match the SVG element background to the water rect so any letterboxing
281
- // (when the host container's aspect differs from the viewBox) shows water,
282
- // not the gray palette bg that finalizeSvgExport would otherwise apply —
283
- // i.e. no stray band above/below the map.
284
- .style('background', layout.background);
285
-
286
- svg
287
- .append('rect')
288
- .attr('width', width)
289
- .attr('height', height)
290
- .attr('fill', layout.background);
291
-
292
- // Arrowhead markers for directed legs. Sized in user-space (NOT the SVG
293
- // default of stroke-width units) so a heavy weighted line doesn't blow the
294
- // arrowhead up to a giant wedge. The size grows gently with the line width —
295
- // enough to stay distinct from the stroke — but is firmly capped.
296
- const defs = svg.append('defs');
297
- // Namespace every def id minted below so multiple maps on one page don't share
298
- // `url(#…)` targets (see mapInstanceCounter).
299
- const uid = mapInstanceCounter++;
300
- const nid = (base: string): string => `${base}__m${uid}`;
301
- // Dampened: ~8px at the thinnest leg, easing toward a 15px cap as legs widen.
302
- const arrowSize = (w: number): number => Math.min(15, 7 + w * 0.95);
303
-
304
- // Neutral bg (not the water-tinted backdrop) so label halos read over both
305
- // land and ocean.
306
- const haloColor = palette.bg;
307
-
308
- // Title / subtitle / caption are rendered LAST (see end of function) so they
309
- // sit in the foreground above the basemap, POIs, and labels.
310
-
311
- // ── Regions ──
312
- const gRegions = svg.append('g').attr('class', 'dgmo-map-regions');
313
- const drawRegion = (
314
- g: Sel,
315
- r: MapLayoutRegion,
316
- strokeWidth: number
317
- ): void => {
318
- const p = g
319
- .append('path')
320
- .attr('d', r.d)
321
- .attr('fill', r.fill)
322
- .attr('stroke', r.stroke)
323
- .attr('stroke-width', strokeWidth);
324
- // Display name on EVERY region (authored + base/context) so the app can
325
- // surface it on hover — decorative metadata, no visible text drawn here.
326
- if (r.label) p.attr('data-region-name', r.label);
327
- // ISO id on EVERY political region (authored + base/context + inset states),
328
- // so the Inspect tool can resolve a reverse-geocoded `{iso}` to its drawn path
329
- // and outline it. Distinct from `data-region` (data-layer-only, legend hover):
330
- // this lands on base/context land too. Lakes carry no iso (not a place).
331
- if (r.id && r.id !== 'lake') p.attr('data-iso', r.id);
332
- // Area-weighted centroid (px) the app anchors the hover label to — robust to
333
- // antimeridian crossers where a bounding-box centre lands in open ocean.
334
- if (r.labelX !== undefined && r.labelY !== undefined) {
335
- p.attr('data-label-x', r.labelX).attr('data-label-y', r.labelY);
336
- }
337
- // Data layer? Tag it so the app can highlight on legend hover / gradient
338
- // scrub. `data-value` for ramp-proximity, `data-tag-<group>` per tag value
339
- // (both lowercased to match the lowercased legend-entry attributes).
340
- if (r.layer !== 'base') {
341
- p.classed('dgmo-map-region', true).attr('data-region', r.id);
342
- if (r.value !== undefined) p.attr('data-value', r.value);
343
- if (r.tags) {
344
- for (const [group, value] of Object.entries(r.tags)) {
345
- p.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
346
- }
347
- }
348
- }
349
- if (r.lineNumber >= 0) {
350
- p.attr('data-line-number', r.lineNumber);
351
- if (onClickItem) {
352
- p.style('cursor', 'pointer').on('click', () =>
353
- onClickItem(r.lineNumber)
354
- );
355
- }
356
- }
357
- };
358
- for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
359
-
360
- // ── Relief (mountain-range hachure over ALL land, under rivers/POIs/labels) ──
361
- // Rule horizontal lines across the whole canvas, clipped to the INTERSECTION
362
- // of (a) the union of range polygons and (b) the land — nested clipPaths, so
363
- // the hachure never bleeds onto water (coarse range polygons overrun the
364
- // coast, and horizontal lines on the sea read as the water convention). The
365
- // land clip is every drawn region except lakes — INCLUDING value-/tag-coloured
366
- // regions, so the relief texture sits ATOP the choropleth/tag fills (a range
367
- // crossing a valued state still reads as mountains there). It stays below
368
- // rivers, POIs, and labels. Explicit <line>s in a <clipPath> (not a tiled
369
- // <pattern>) dodge WKWebView/resvg pattern quirks. A non-scaling stroke keeps
370
- // the width constant in device px at any zoom/DPR (uniform, no moire); kept
371
- // sub-pixel + low-contrast so the texture stays faint. Decorative — no data attrs.
372
- if (layout.relief.length && layout.reliefHatch) {
373
- const h = layout.reliefHatch;
374
- const rangeClipId = nid('dgmo-relief-clip');
375
- const landClipId = nid('dgmo-relief-land');
376
- const rangeClip = defs.append('clipPath').attr('id', rangeClipId);
377
- for (const s of layout.relief) rangeClip.append('path').attr('d', s.d);
378
- const landClip = defs.append('clipPath').attr('id', landClipId);
379
- for (const r of layout.regions)
380
- if (r.id !== 'lake') landClip.append('path').attr('d', r.d);
381
- const gRelief = svg
382
- .append('g')
383
- .attr('clip-path', `url(#${landClipId})`) // outer: land only
384
- // Decorative texture — never a pointer target, so region hover (the app's
385
- // name-on-hover) always reaches the region path beneath. WebKit hit-tests
386
- // masked/clipped overlays unlike Chromium, so this must be explicit.
387
- .style('pointer-events', 'none')
388
- .append('g')
389
- .attr('class', 'dgmo-map-relief')
390
- .attr('clip-path', `url(#${rangeClipId})`) // inner: ∩ ranges
391
- .attr('stroke', h.color)
392
- .attr('stroke-width', h.width)
393
- // Non-scaling stroke = constant device width at any zoom/DPR (uniform,
394
- // no moire). NOT crispEdges — that snaps to a solid ~1px in WebKit and
395
- // reads far too heavy; plain AA keeps the sub-pixel lines whisper-thin.
396
- .attr('vector-effect', 'non-scaling-stroke');
397
- for (let y = h.spacing; y < height; y += h.spacing) {
398
- gRelief
399
- .append('line')
400
- .attr('x1', 0)
401
- .attr('y1', y)
402
- .attr('x2', width)
403
- .attr('y2', y);
404
- }
405
- }
406
-
407
- // ── Coastline water-lines (faint nautical-chart lines on the WATER side) ──
408
- // 2 discrete coast-parallel lines hugging the ocean shore + lake shores, fading
409
- // seaward. Each region's outer ring is buffered as a symmetric SVG stroke band
410
- // then eroded with a flat-water overdraw to a thin offshore ring; a luminance
411
- // <mask> (white canvas − black land + white lakes) reveals only the water side,
412
- // so land/land borders self-remove (their band falls on
413
- // the neighbour's land, which the mask hides — no topojson.mesh needed). NOT a
414
- // clipPath: sibling clip paths UNION (can't subtract land). Decorative — no data
415
- // attrs, plain strokes. Below rivers/POIs/legs/labels, above region/relief fills
416
- // (so with `relief` on, water-lines sit on water and relief on land — disjoint).
417
- // §24B.2, ADR-1/3/6.
418
- if (layout.coastlineStyle) {
419
- const cs = layout.coastlineStyle;
420
- const maskId = nid('dgmo-map-water-mask');
421
- const mask = defs
422
- .append('mask')
423
- .attr('id', maskId)
424
- // userSpaceOnUse: the default objectBoundingBox clamps the mask region to
425
- // the group's own bbox and drops the canvas-edge reveal (round-2 #2).
426
- .attr('maskUnits', 'userSpaceOnUse')
427
- .attr('x', 0)
428
- .attr('y', 0)
429
- .attr('width', width)
430
- .attr('height', height);
431
- mask
432
- .append('rect')
433
- .attr('x', 0)
434
- .attr('y', 0)
435
- .attr('width', width)
436
- .attr('height', height)
437
- .attr('fill', 'white');
438
- // All land fills are opaque black and all lake fills opaque white, so each
439
- // group collapses into ONE compound path (land first, lakes over it to
440
- // re-reveal their interiors). Identical pixels, ~hundreds of paths → 2.
441
- const landD = layout.regions
442
- .filter((r) => r.id !== 'lake')
443
- .map((r) => r.d)
444
- .join(' ');
445
- const lakeD = layout.regions
446
- .filter((r) => r.id === 'lake')
447
- .map((r) => r.d)
448
- .join(' ');
449
- if (landD) mask.append('path').attr('d', landD).attr('fill', 'black');
450
- if (lakeD) mask.append('path').attr('d', lakeD).attr('fill', 'white');
451
- // Moat around each AK/HI inset box: the opaque box is drawn in the FOREGROUND
452
- // over open ocean, so the main-map offshore rings get chopped by its border
453
- // and butt into it — sloppy. Paint each box quad black (interior, under the
454
- // box) with a black stroke whose half-width = the band's outer reach, so the
455
- // main rings also clear a margin OUTSIDE the border. The ring-ends then fall
456
- // in open water away from the frame instead of crashing into it.
457
- if (layout.insets.length) {
458
- const reach = Math.max(0, ...cs.lines.map((l) => l.d + l.thickness));
459
- for (const box of layout.insets) {
460
- const d =
461
- box.points.map((p, i) => `${i ? 'L' : 'M'}${p[0]},${p[1]}`).join('') +
462
- 'Z';
463
- mask
464
- .append('path')
465
- .attr('d', d)
466
- .attr('fill', 'black')
467
- .attr('stroke', 'black')
468
- .attr('stroke-width', 2 * reach)
469
- .attr('stroke-linejoin', 'round');
470
- }
471
- }
472
- // NO frame band: a synthetic frame-cut edge (clipExtent/cullFeatureToView
473
- // trims region `d` to the view rect) has the region INTERIOR — land — on the
474
- // canvas-interior side, which the mask already paints black, so its band is
475
- // hidden anyway. A border band here only suppressed REAL coastal rings near
476
- // the canvas edge, leaving an empty top/bottom strip — so the water-lines now
477
- // carry through to every edge of the visible map.
478
- const gWater = svg
479
- .append('g')
480
- .attr('class', 'dgmo-map-water-lines')
481
- .attr('fill', 'none')
482
- // Decorative nautical lines — never a pointer target. Without this the wide
483
- // pre-mask coastal ring bands swallow region hover over coastal countries in
484
- // WebKit (which hit-tests masked content unlike Chromium); e.g. Portugal.
485
- .style('pointer-events', 'none')
486
- .attr('mask', `url(#${maskId})`);
487
- appendWaterLines(
488
- gWater,
489
- defs,
490
- nid('dgmo-map-coast'),
491
- // Pass the canvas frame so edges collinear with it (the antimeridian on a
492
- // world map, regional clipExtent cuts) don't get ringed as fake coast —
493
- // land runs cleanly to the render-area edge.
494
- coastlineOuterRings(layout.regions, cs.minExtent, {
495
- w: width,
496
- h: height,
497
- }),
498
- cs,
499
- layout.background
500
- );
501
- // Restore the seaward half of the coast stroke: the rings' flat-water erosion
502
- // overdraws repaint the water out to d_max, which paints over the water-side
503
- // half of each region's coast outline and makes coastlines read faded. Re-
504
- // stroke every region inside the SAME masked group (so it only repaints the
505
- // water side — the land side was never touched, and interior land/land borders
506
- // stay hidden), on top of the rings. The strokes sit at the coast (offset 0),
507
- // well inside d0, so they never cover the offshore rings.
508
- // Group the restroke by stroke colour (base borders share one colour; the
509
- // occasional data region may differ) so same-coloured coasts collapse into a
510
- // single compound path instead of one path per region.
511
- const byStroke = new Map<string, string[]>();
512
- for (const r of layout.regions) {
513
- const arr = byStroke.get(r.stroke);
514
- if (arr) arr.push(r.d);
515
- else byStroke.set(r.stroke, [r.d]);
516
- }
517
- for (const [stroke, ds] of byStroke)
518
- gWater
519
- .append('path')
520
- .attr('d', ds.join(' '))
521
- .attr('stroke', stroke)
522
- .attr('stroke-width', 0.5)
523
- .attr('stroke-linejoin', 'round');
524
- }
525
-
526
- // ── Rivers (thin water centerlines over the land, under POIs/edges) ──
527
- if (layout.rivers.length) {
528
- const gRivers = svg
529
- .append('g')
530
- .attr('class', 'dgmo-map-rivers')
531
- .attr('fill', 'none')
532
- .style('pointer-events', 'none'); // decorative — pass hover to regions below
533
- for (const r of layout.rivers) {
534
- gRivers
535
- .append('path')
536
- .attr('d', r.d)
537
- .attr('stroke', r.color)
538
- .attr('stroke-width', r.width)
539
- .attr('stroke-linecap', 'round')
540
- .attr('stroke-linejoin', 'round');
541
- }
542
- }
543
-
544
- // ── City dots — a faint gazetteer scatter for geographic orientation, over
545
- // the basemap but under connectors/POIs/labels. Decorative (no pointer events,
546
- // no data attributes); muted ink at low opacity so it reads as texture, not
547
- // data. Suppressed entirely by the `no-cities` directive (empty array). ──
548
- if (layout.cityDots.length) {
549
- const gCities = svg
550
- .append('g')
551
- .attr('class', 'dgmo-map-cities')
552
- .attr('fill', palette.textMuted)
553
- .attr('fill-opacity', 0.28)
554
- .style('pointer-events', 'none');
555
- for (const c of layout.cityDots) {
556
- gCities.append('circle').attr('cx', c.cx).attr('cy', c.cy).attr('r', c.r);
557
- }
558
- }
559
-
560
- // ── Label clarity patch — repaint the clean region FILL under each POI label
561
- // so the basemap line work beneath it (region borders, relief hachure, coast +
562
- // river lines) dissolves where it would clutter the text. Crucially this is NOT
563
- // a halo: the patch IS the same fill that already surrounds the label, so over
564
- // plain land it is invisible and ONLY the crossing lines disappear — no white
565
- // pad over flat colour. A blurred-blob mask feathers each patch into the map
566
- // (soft edges, no hard rectangle). Sits above all basemap line work, below the
567
- // dots / legs / labels. POI labels only; context/water labels keep their lines
568
- // (they read as cartographic texture, not clutter).
569
- //
570
- // Collapsed cluster members are HIDDEN at rest, so their labels aren't on screen
571
- // — patching under them would fade the basemap beneath an invisible label. So
572
- // each cluster's member patches go in a per-cluster group tagged
573
- // `data-cluster-deco`, which the app's spiderfy controller toggles in lockstep
574
- // with the legs / member dots: the fade only appears once the fan is expanded.
575
- // Non-clustered POI labels (and ALL labels in export / `no-cluster-pois`, where
576
- // the fan is permanently open) keep the always-on patch so the static image
577
- // matches what's on screen.
578
- const clusterUi = !exportDims && !resolved.directives.noClusterPois;
579
- const poiLabels = layout.labels.filter(
580
- (l) => l.poiId !== undefined && !l.hidden
581
- );
582
- if (poiLabels.length) {
583
- const patchBlurId = nid('dgmo-map-label-patch-blur');
584
- // Soft falloff so the patch dissolves into the surrounding basemap instead of
585
- // ending on a hard edge. Tuned at the 11px label font. One shared filter for
586
- // every patch group.
587
- defs
588
- .append('filter')
589
- .attr('id', patchBlurId)
590
- .attr('x', '-50%')
591
- .attr('y', '-50%')
592
- .attr('width', '200%')
593
- .attr('height', '200%')
594
- .append('feGaussianBlur')
595
- .attr('in', 'SourceGraphic')
596
- .attr('stdDeviation', 2.5);
597
- const PAD = 8; // ≳ blur reach so culled regions can't lose a fringe
598
-
599
- // Build one patch (mask of blurred blobs + the region fills they cross) for a
600
- // set of labels. `decoCluster` tags the group so the app toggles it with the
601
- // rest of the cluster's spiderfy decoration.
602
- const buildPatch = (
603
- labels: typeof poiLabels,
604
- maskId: string,
605
- decoCluster?: string
606
- ): void => {
607
- if (!labels.length) return;
608
- const patchMask = defs
609
- .append('mask')
610
- .attr('id', maskId)
611
- // userSpaceOnUse: the patch group's bbox is the whole basemap, but keep
612
- // the mask region pinned to the canvas (parity with the water masks).
613
- .attr('maskUnits', 'userSpaceOnUse')
614
- .attr('x', 0)
615
- .attr('y', 0)
616
- .attr('width', width)
617
- .attr('height', height);
618
- // White blobs = where the patch shows; the surrounding black hides it. Each
619
- // blob is a SOLID rounded rect sized to the text bbox so the core fully
620
- // hides the line work behind the glyphs; the blur only feathers the outer
621
- // edge. Solid core → if an engine drops the in-mask blur (some WebKit
622
- // builds), it degrades to a clean stadium patch, never a halo.
623
- const blobs = patchMask
624
- .append('g')
625
- .attr('filter', `url(#${patchBlurId})`);
626
- // Padded rects (incl. the blur reach) drive both the mask AND the region
627
- // cull below, so the two never disagree.
628
- const blobRects: Array<{
629
- x0: number;
630
- y0: number;
631
- x1: number;
632
- y1: number;
633
- }> = [];
634
- for (const l of labels) {
635
- const multi = l.lines && l.lines.length > 1;
636
- const tw = multi
637
- ? Math.max(...l.lines!.map((s) => measureText(s, LABEL_FONT)))
638
- : measureText(l.text, LABEL_FONT);
639
- const th = multi ? l.lines!.length * (LABEL_FONT + 2) : LABEL_FONT;
640
- // Anchor → blob centre x; cy nudged up from the text baseline to the
641
- // glyph visual centre.
642
- const cx =
643
- l.anchor === 'start'
644
- ? l.x + tw / 2
645
- : l.anchor === 'end'
646
- ? l.x - tw / 2
647
- : l.x;
648
- const cy = l.y - LABEL_FONT * 0.3;
649
- const w = tw + 8;
650
- const h = th + 6;
651
- blobs
652
- .append('rect')
653
- .attr('x', cx - w / 2)
654
- .attr('y', cy - h / 2)
655
- .attr('width', w)
656
- .attr('height', h)
657
- .attr('rx', h / 2)
658
- .attr('fill', 'white');
659
- blobRects.push({
660
- x0: cx - w / 2 - PAD,
661
- y0: cy - h / 2 - PAD,
662
- x1: cx + w / 2 + PAD,
663
- y1: cy + h / 2 + PAD,
664
- });
665
- }
666
- const gPatch = svg
667
- .append('g')
668
- .attr('class', 'dgmo-map-label-patch')
669
- .attr('mask', `url(#${maskId})`)
670
- // Decorative cover — never a pointer target so region hover / name-on-
671
- // hover reaches the real region paths beneath (WebKit hit-tests masked
672
- // content).
673
- .style('pointer-events', 'none');
674
- // Per-cluster patch: tag as cluster decoration so the spiderfy controller
675
- // shows/hides it in step with the fan. (The app collapses all `[data-
676
- // cluster-deco]` to opacity 0 at rest, pre-paint, so there's no flash.)
677
- if (decoCluster !== undefined)
678
- gPatch.attr('data-cluster-deco', decoCluster);
679
- // Redraw fills only (no stroke), in document order, but ONLY for regions
680
- // whose bbox overlaps a label blob — the mask already hides everything
681
- // else, so emitting the rest is pure SVG weight (matters on dense world
682
- // maps). The kept regions reproduce the exact composite colour under each
683
- // label, so the patch is seamless and the only visible change is the
684
- // vanished line work.
685
- for (const r of layout.regions) {
686
- // bbox is precomputed once in layoutMap (roadmap #2); fall back to
687
- // parsing only for layouts predating that field.
688
- let minX: number, minY: number, maxX: number, maxY: number;
689
- if (r.bbox) {
690
- [minX, minY, maxX, maxY] = r.bbox;
691
- } else {
692
- minX = Infinity;
693
- minY = Infinity;
694
- maxX = -Infinity;
695
- maxY = -Infinity;
696
- for (const ring of parsePathRings(r.d))
697
- for (const [px, py] of ring) {
698
- if (px < minX) minX = px;
699
- if (px > maxX) maxX = px;
700
- if (py < minY) minY = py;
701
- if (py > maxY) maxY = py;
702
- }
703
- }
704
- const hit = blobRects.some(
705
- (b) => minX <= b.x1 && maxX >= b.x0 && minY <= b.y1 && maxY >= b.y0
706
- );
707
- if (hit) gPatch.append('path').attr('d', r.d).attr('fill', r.fill);
708
- }
709
- };
710
-
711
- if (clusterUi) {
712
- // Always-on patch for labels that are visible at rest (non-cluster members).
713
- buildPatch(
714
- poiLabels.filter((l) => l.clusterMember === undefined),
715
- nid('dgmo-map-label-patch')
716
- );
717
- // Per-cluster patches, hidden until the fan expands.
718
- const byCluster = new Map<string, typeof poiLabels>();
719
- for (const l of poiLabels) {
720
- if (l.clusterMember === undefined) continue;
721
- const arr = byCluster.get(l.clusterMember);
722
- if (arr) arr.push(l);
723
- else byCluster.set(l.clusterMember, [l]);
724
- }
725
- // Safe mask id from the iteration index — cluster ids can be coord-based
726
- // (`@lat,lon`) and aren't `url(#…)`-safe; the raw id rides on the
727
- // data-cluster-deco attribute instead.
728
- let ci = 0;
729
- for (const [cid, labs] of byCluster)
730
- buildPatch(labs, nid(`dgmo-map-label-patch-c${ci++}`), cid);
731
- } else {
732
- // Export / `no-cluster-pois`: fan is permanently open → one always-on patch
733
- // over every POI label.
734
- buildPatch(poiLabels, nid('dgmo-map-label-patch'));
735
- }
736
- }
737
-
738
- // ── AK / HI insets (albers-usa) — drawn in the FOREGROUND so the opaque ocean
739
- // box hides the main-map neighbour land (Mexico's Baja) behind it; the state
740
- // then draws on top, framed by the box border. ──
741
- if (layout.insets.length) {
742
- const insetG = svg.append('g').attr('class', 'dgmo-map-insets');
743
- layout.insets.forEach((box, bi) => {
744
- // Angled-top quad frame — rides under the conus coast so it never covers
745
- // neighbouring states. Closed path from the four corners.
746
- const d =
747
- box.points.map((p, i) => `${i ? 'L' : 'M'}${p[0]},${p[1]}`).join('') +
748
- 'Z';
749
- insetG
750
- .append('path')
751
- .attr('d', d)
752
- .attr('fill', layout.background)
753
- .attr('stroke', mix(palette.text, palette.bg, 55))
754
- .attr('stroke-width', 1)
755
- .attr('stroke-linejoin', 'round');
756
- // Neighbour land (Canada beside Alaska) clipped to this box, behind the
757
- // state — so a land border reads as land rather than sprouting coast rings.
758
- if (box.contextLand) {
759
- const clipId = nid(`dgmo-map-inset-clip-${bi}`);
760
- defs.append('clipPath').attr('id', clipId).append('path').attr('d', d);
761
- insetG
762
- .append('path')
763
- .attr('d', box.contextLand.d)
764
- .attr('fill', box.contextLand.fill)
765
- .attr('clip-path', `url(#${clipId})`);
766
- }
767
- });
768
- for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
769
-
770
- // Inset coastline water-lines (AK/HI box interiors) for visual parity with
771
- // the main map. Mask = the inset box quads (white reveal) − inset regions
772
- // (black land / white lake); buffer+erode the inset region outer rings the
773
- // same way. Inside the inset group so it composites over the box fills.
774
- if (layout.coastlineStyle) {
775
- const cs = layout.coastlineStyle;
776
- const maskId = nid('dgmo-map-inset-water-mask');
777
- const mask = defs
778
- .append('mask')
779
- .attr('id', maskId)
780
- .attr('maskUnits', 'userSpaceOnUse')
781
- .attr('x', 0)
782
- .attr('y', 0)
783
- .attr('width', width)
784
- .attr('height', height);
785
- // White reveal = the box interior, but eroded inward by `reach` (a black
786
- // border stroke) so the seaward rings fade out before the frame instead of
787
- // being hard-clipped at it — clipped ring fragments otherwise read as
788
- // artifacts bumping into the crisp 1px border. Mirrors the main-map moat,
789
- // applied inward.
790
- const reach = Math.max(0, ...cs.lines.map((l) => l.d + l.thickness));
791
- for (const box of layout.insets) {
792
- const d =
793
- box.points.map((p, i) => `${i ? 'L' : 'M'}${p[0]},${p[1]}`).join('') +
794
- 'Z';
795
- mask
796
- .append('path')
797
- .attr('d', d)
798
- .attr('fill', 'white')
799
- .attr('stroke', 'black')
800
- .attr('stroke-width', 2 * reach)
801
- .attr('stroke-linejoin', 'round');
802
- }
803
- // Neighbour land masks as land too — clipped to its box so it can't darken
804
- // an adjacent inset — keeping the AK/Canada land border free of rings.
805
- layout.insets.forEach((box, bi) => {
806
- if (box.contextLand)
807
- mask
808
- .append('path')
809
- .attr('d', box.contextLand.d)
810
- .attr('fill', 'black')
811
- .attr('clip-path', `url(#${nid(`dgmo-map-inset-clip-${bi}`)})`);
812
- });
813
- for (const r of layout.insetRegions)
814
- if (r.id !== 'lake')
815
- mask.append('path').attr('d', r.d).attr('fill', 'black');
816
- for (const r of layout.insetRegions)
817
- if (r.id === 'lake')
818
- mask.append('path').attr('d', r.d).attr('fill', 'white');
819
- // Clip the water-line strokes to the inset box quads — the mask controls
820
- // which side reads as water, but SVG strokes still extend stroke-width/2
821
- // past their path, so without this the seaward rings bleed over the box
822
- // border. Union of all inset quads = one clipPath shared by the group.
823
- const clipId = nid('dgmo-map-inset-water-clip');
824
- const clip = defs.append('clipPath').attr('id', clipId);
825
- for (const box of layout.insets) {
826
- const d =
827
- box.points.map((p, i) => `${i ? 'L' : 'M'}${p[0]},${p[1]}`).join('') +
828
- 'Z';
829
- clip.append('path').attr('d', d);
830
- }
831
- // Nest clip (outer) + mask (inner) rather than both on one element —
832
- // WebKit (Tauri) renders the combined attributes inconsistently; the
833
- // nested form is honored by every engine.
834
- const gInsetWater = insetG
835
- .append('g')
836
- .attr('clip-path', `url(#${clipId})`)
837
- .append('g')
838
- .attr('class', 'dgmo-map-inset-water-lines')
839
- .attr('fill', 'none')
840
- .style('pointer-events', 'none') // decorative — pass hover to inset regions
841
- .attr('mask', `url(#${maskId})`);
842
- appendWaterLines(
843
- gInsetWater,
844
- defs,
845
- nid('dgmo-map-inset-coast'),
846
- coastlineOuterRings(layout.insetRegions, cs.minExtent),
847
- cs,
848
- layout.background
849
- );
850
- // Restore the seaward half of the inset coast strokes (see main pass).
851
- for (const r of layout.insetRegions)
852
- gInsetWater
853
- .append('path')
854
- .attr('d', r.d)
855
- .attr('stroke', r.stroke)
856
- .attr('stroke-width', 0.5)
857
- .attr('stroke-linejoin', 'round');
858
- }
859
-
860
- // Re-stroke each inset frame ON TOP (stroke-only, no fill) as the final
861
- // inset layer — the early border path sits under the contextLand fill,
862
- // region fills, and water-lines, so its inner half gets covered near the
863
- // top/right edges and the frame reads as thin/fuzzy. This overlay keeps a
864
- // crisp full-width border on every side.
865
- for (const box of layout.insets) {
866
- const d =
867
- box.points.map((p, i) => `${i ? 'L' : 'M'}${p[0]},${p[1]}`).join('') +
868
- 'Z';
869
- insetG
870
- .append('path')
871
- .attr('d', d)
872
- .attr('fill', 'none')
873
- .attr('stroke', mix(palette.text, palette.bg, 55))
874
- .attr('stroke-width', 1)
875
- .attr('stroke-linejoin', 'round')
876
- .style('pointer-events', 'none');
877
- }
878
- }
879
-
880
- // Code↔diagram sync: tag a synced element with its 1-based source line and,
881
- // in preview, make it jump the editor cursor on click. Source-derived elements
882
- // (regions, POIs, legs, labels) all carry `lineNumber`; generated context
883
- // labels use 0 as a "no source line" sentinel, so guard on `>= 1`.
884
- const wireSync = <E extends Element>(
885
- sel: d3Selection.Selection<E, unknown, null, undefined>,
886
- lineNumber: number
887
- ): void => {
888
- if (lineNumber < 1) return;
889
- sel.attr('data-line-number', lineNumber);
890
- if (onClickItem)
891
- sel.style('cursor', 'pointer').on('click', () => onClickItem(lineNumber));
892
- };
893
-
894
- // ── Legs (edges + route legs) ──
895
- const gLegs = svg
896
- .append('g')
897
- .attr('class', 'dgmo-map-legs')
898
- .attr('fill', 'none');
899
- // Parse a single-segment leg path (`M…L…` or `M…Q…`) into its end point and the
900
- // unit tangent arriving there. Returns null for anything unexpected (then the
901
- // leg renders without the trimmed-arrow treatment).
902
- const legEnd = (
903
- d: string
904
- ): { ex: number; ey: number; tx: number; ty: number } | null => {
905
- const m =
906
- /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
907
- d
908
- );
909
- if (!m) return null;
910
- const x0 = +m[1]!;
911
- const y0 = +m[2]!;
912
- let ex: number;
913
- let ey: number;
914
- let fromX: number;
915
- let fromY: number;
916
- if (m[3] !== undefined) {
917
- ex = +m[3]!;
918
- ey = +m[4]!;
919
- fromX = x0;
920
- fromY = y0;
921
- } else {
922
- ex = +m[7]!;
923
- ey = +m[8]!;
924
- fromX = +m[5]!; // quadratic control → tangent at the end
925
- fromY = +m[6]!;
926
- }
927
- const dx = ex - fromX;
928
- const dy = ey - fromY;
929
- const len = Math.hypot(dx, dy);
930
- if (!(len > 0)) return null;
931
- return { ex, ey, tx: dx / len, ty: dy / len };
932
- };
933
- layout.legs.forEach((leg) => {
934
- // A directed leg's stroke is TRIMMED back to the arrowhead base so a heavy
935
- // weighted line doesn't run through (and bulge past) the narrowing arrowhead.
936
- // The arrowhead itself is drawn as a filled triangle at the true endpoint,
937
- // sized to always be a touch wider than the stroke so the stroke end is fully
938
- // covered. Undirected (or unparseable) legs keep the full path.
939
- const end = leg.arrow ? legEnd(leg.d) : null;
940
- let drawD = leg.d;
941
- let arrowTri: string | null = null;
942
- if (end) {
943
- const aLen = arrowSize(leg.width); // arrow depth along the line
944
- const aHalf = Math.max(aLen * 0.5, leg.width * 0.6 + 1); // half base ≥ stroke
945
- const bx = end.ex - end.tx * aLen; // base centre
946
- const by = end.ey - end.ty * aLen;
947
- const px = -end.ty; // perpendicular
948
- const py = end.tx;
949
- arrowTri = `M${bx + px * aHalf},${by + py * aHalf}L${end.ex},${end.ey}L${bx - px * aHalf},${by - py * aHalf}z`;
950
- // Re-point the stroke's end at the arrow base (small along-tangent shift; for
951
- // an arc leg the negligible end-curvature change is hidden by the arrowhead).
952
- drawD = leg.d.replace(
953
- /(-?[\d.]+),(-?[\d.]+)$/,
954
- `${bx.toFixed(2)},${by.toFixed(2)}`
955
- );
956
- }
957
- const p = gLegs
958
- .append('path')
959
- .attr('d', drawD)
960
- .attr('stroke', leg.color)
961
- .attr('stroke-width', leg.width)
962
- .attr('stroke-linecap', 'round')
963
- // Endpoint POI ids so a focused leg can co-highlight its two POIs (§17 sync).
964
- .attr('data-from-id', leg.fromId)
965
- .attr('data-to-id', leg.toId);
966
- // Tag the line per tag value (lowercased, matching the legend-entry attrs) so
967
- // a legend hover spotlights only the matching lines (§24B.6) — mirrors POIs.
968
- if (leg.tags) {
969
- for (const [group, value] of Object.entries(leg.tags)) {
970
- p.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
971
- }
972
- }
973
- // A 0-width invisible leg path is hard to hit; pointer-events on the visible
974
- // stroke is enough for the line widths legs use.
975
- wireSync(p, leg.lineNumber);
976
- // Surface the leg's weight on hover. A native <title> gives a tooltip on the
977
- // visible stroke in every surface (app, exported SVG, Obsidian, web) with no
978
- // viewer code; `data-value` is also emitted so an app overlay can style it
979
- // later. The label (when any) prefixes the number for context ("fiber: 28").
980
- if (leg.value !== undefined) {
981
- p.attr('data-value', leg.value);
982
- p.append('title').text(
983
- leg.label !== undefined ? `${leg.label}: ${leg.value}` : `${leg.value}`
984
- );
985
- }
986
- if (arrowTri) {
987
- // Filled arrowhead at the true endpoint — a flat triangle whose base sits
988
- // where the trimmed stroke ends, so the line meets it cleanly with no
989
- // show-through. Round join keeps the tip from looking spiky on thin legs.
990
- const a = gLegs
991
- .append('path')
992
- .attr('d', arrowTri)
993
- .attr('fill', leg.color)
994
- .attr('stroke', leg.color)
995
- .attr('stroke-width', 1)
996
- .attr('stroke-linejoin', 'round')
997
- .attr('data-from-id', leg.fromId)
998
- .attr('data-to-id', leg.toId);
999
- // Mirror the leg's tag attrs onto the arrowhead so a legend hover dims/keeps
1000
- // the arrow in lockstep with its line (§24B.6) — else the arrow stays dimmed
1001
- // while its matching line stays bright.
1002
- if (leg.tags) {
1003
- for (const [group, value] of Object.entries(leg.tags)) {
1004
- a.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
1005
- }
1006
- }
1007
- }
1008
- if (leg.label !== undefined && leg.labelX !== undefined) {
1009
- // Text shade is contrast-picked in layout against the fill under the label
1010
- // (dark scored country ⇒ light text, pale land ⇒ dark), with the ghost halo
1011
- // only when that contrast is marginal. Fall back to the muted default for
1012
- // legs that predate the computed style.
1013
- const lt = emitText(
1014
- gLegs,
1015
- leg.labelX,
1016
- leg.labelY ?? 0,
1017
- leg.label,
1018
- 'middle',
1019
- leg.labelColor ?? palette.textMuted,
1020
- leg.labelHaloColor ?? haloColor,
1021
- leg.labelHalo ?? true,
1022
- LABEL_FONT - 1
1023
- );
1024
- wireSync(lt, leg.lineNumber);
1025
- }
1026
- });
1027
-
1028
- // ── Coincident-stack spider legs + hub (under the dots) ──
1029
- // Legs + hub are DECORATIVE (`data-cluster-deco`, pointer-events none) — the app
1030
- // toggles only their opacity. Drawn VISIBLE so export + the no-JS default show
1031
- // the expanded fan. An invisible hit-area circle (interactive only) owns all
1032
- // pointer interaction so hover/click drives the spiderfy controller robustly.
1033
- // `no-cluster-pois` (or any export) keeps the fan permanently expanded: draw
1034
- // the legs/hub/members visible but emit NO hit-area + NO badge, so there is
1035
- // nothing for the app's spiderfy controller to collapse — the map reads the
1036
- // same on screen as on paper.
1037
- const gSpider = svg.append('g').attr('class', 'dgmo-map-spider');
1038
- for (const cl of layout.clusters) {
1039
- // Pointer hit-area — bottom of the stack so member dots still take their own
1040
- // clicks (line-jump); clicks on the empty centre fall through to here.
1041
- if (clusterUi) {
1042
- gSpider
1043
- .append('circle')
1044
- .attr('cx', cl.cx)
1045
- .attr('cy', cl.cy)
1046
- .attr('r', cl.hitR)
1047
- .attr('fill', 'transparent')
1048
- .attr('data-cluster-hit', cl.id)
1049
- .style('cursor', 'pointer');
1050
- }
1051
- for (const leg of cl.legs) {
1052
- gSpider
1053
- .append('line')
1054
- .attr('x1', cl.cx)
1055
- .attr('y1', cl.cy)
1056
- .attr('x2', leg.x2)
1057
- .attr('y2', leg.y2)
1058
- .attr('stroke', leg.color)
1059
- .attr('stroke-width', 1)
1060
- .attr('data-cluster-deco', cl.id)
1061
- .style('pointer-events', 'none');
1062
- }
1063
- // Faint neutral hub anchoring the legs (RQ3).
1064
- gSpider
1065
- .append('circle')
1066
- .attr('cx', cl.cx)
1067
- .attr('cy', cl.cy)
1068
- .attr('r', 2)
1069
- .attr('fill', mix(palette.textMuted, palette.bg, 40))
1070
- .attr('data-cluster-deco', cl.id)
1071
- .style('pointer-events', 'none');
1072
- }
1073
-
1074
- // ── POIs ──
1075
- const gPois = svg.append('g').attr('class', 'dgmo-map-pois');
1076
- for (const poi of layout.pois) {
1077
- if (poi.isOrigin) {
1078
- gPois
1079
- .append('circle')
1080
- .attr('cx', poi.cx)
1081
- .attr('cy', poi.cy)
1082
- .attr('r', poi.r + 3)
1083
- .attr('fill', 'none')
1084
- .attr('stroke', poi.stroke)
1085
- .attr('stroke-width', 1.5);
1086
- }
1087
- const c = gPois
1088
- .append('circle')
1089
- .attr('cx', poi.cx)
1090
- .attr('cy', poi.cy)
1091
- .attr('r', poi.r)
1092
- .attr('fill', poi.fill)
1093
- .attr('fill-opacity', poi.fillOpacity)
1094
- .attr('stroke', poi.stroke)
1095
- .attr('stroke-width', 1)
1096
- .attr('data-line-number', poi.lineNumber)
1097
- .attr('data-poi', poi.id);
1098
- // Coincident-stack member: tag so the app hides it when collapsing the stack.
1099
- if (poi.clusterId !== undefined)
1100
- c.attr('data-cluster-member', poi.clusterId);
1101
- // Tag the marker per tag value (lowercased, matching the lowercased
1102
- // legend-entry attributes) so the app can spotlight it on legend hover.
1103
- if (poi.tags) {
1104
- for (const [group, value] of Object.entries(poi.tags)) {
1105
- c.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
1106
- }
1107
- }
1108
- if (onClickItem) {
1109
- c.style('cursor', 'pointer').on('click', () =>
1110
- onClickItem(poi.lineNumber)
1111
- );
1112
- }
1113
- if (poi.routeNumber !== undefined) {
1114
- emitText(
1115
- gPois,
1116
- poi.cx,
1117
- poi.cy + 3,
1118
- String(poi.routeNumber),
1119
- 'middle',
1120
- palette.bg,
1121
- poi.fill,
1122
- false,
1123
- LABEL_FONT - 2
1124
- );
1125
- }
1126
- }
1127
-
1128
- // ── Labels (leaders + halo text) ──
1129
- const gLabels = svg.append('g').attr('class', 'dgmo-map-labels');
1130
- for (const lab of layout.labels) {
1131
- // Hover-only labels: OMIT entirely from static export (export = the
1132
- // hover-less default view); in preview emit invisible + flagged so the app
1133
- // can reveal them on hover. They carry no leader, so the leader block below
1134
- // is skipped naturally.
1135
- if (lab.hidden) {
1136
- if (exportDims) continue;
1137
- emitText(
1138
- gLabels,
1139
- lab.x,
1140
- lab.y,
1141
- lab.text,
1142
- lab.anchor,
1143
- lab.color,
1144
- lab.haloColor,
1145
- lab.halo,
1146
- lab.fontSize ?? LABEL_FONT,
1147
- lab.italic,
1148
- lab.letterSpacing
1149
- )
1150
- .attr('data-poi', lab.poiId ?? null)
1151
- .attr('data-poi-hidden', '')
1152
- .style('opacity', 0)
1153
- .style('pointer-events', 'none');
1154
- continue;
1155
- }
1156
- if (lab.leader) {
1157
- const line = gLabels
1158
- .append('line')
1159
- .attr('x1', lab.leader.x1)
1160
- .attr('y1', lab.leader.y1)
1161
- .attr('x2', lab.leader.x2)
1162
- .attr('y2', lab.leader.y2)
1163
- // Tie the leader to its dot by colour; neutral grey when it has none.
1164
- .attr(
1165
- 'stroke',
1166
- lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
1167
- )
1168
- .attr('stroke-width', lab.leaderColor ? 1 : 0.75);
1169
- if (lab.poiId !== undefined) line.attr('data-poi', lab.poiId);
1170
- // Spiderfy member leader: toggle it with the badge (same as its text).
1171
- if (lab.clusterMember !== undefined)
1172
- line.attr('data-cluster-member', lab.clusterMember);
1173
- wireSync(line, lab.lineNumber);
1174
- }
1175
- // Region callout anchor dot: a small disc at the (too-small) region's true
1176
- // centroid, in the region's fill, so the leader visibly originates on the map.
1177
- if (lab.calloutDot) {
1178
- gLabels
1179
- .append('circle')
1180
- .attr('cx', lab.calloutDot.x)
1181
- .attr('cy', lab.calloutDot.y)
1182
- .attr('r', 2.5)
1183
- .attr('fill', lab.calloutDot.color)
1184
- .attr('stroke', mix(palette.text, palette.bg, 55))
1185
- .attr('stroke-width', 0.75);
1186
- }
1187
- const t = emitText(
1188
- gLabels,
1189
- lab.x,
1190
- lab.y,
1191
- lab.text,
1192
- lab.anchor,
1193
- lab.color,
1194
- lab.haloColor,
1195
- lab.halo,
1196
- lab.fontSize ?? LABEL_FONT,
1197
- lab.italic,
1198
- lab.letterSpacing,
1199
- lab.lines,
1200
- lab.valueLine
1201
- );
1202
- // POI labels are spotlightable: tag with the POI id and make the text the
1203
- // hover target (the app dims the other dots/labels on enter).
1204
- if (lab.poiId !== undefined) {
1205
- t.attr('data-poi', lab.poiId).style('cursor', 'default');
1206
- }
1207
- // Coincident-stack member label: hidden by the app when its stack collapses.
1208
- if (lab.clusterMember !== undefined) {
1209
- t.attr('data-cluster-member', lab.clusterMember);
1210
- }
1211
- // Click a region/POI label to jump to its source line (sets cursor:pointer,
1212
- // overriding the default above for POI labels).
1213
- wireSync(t, lab.lineNumber);
1214
- }
1215
-
1216
- // ── Coincident-stack badges (collapsed view). Interactive ONLY — a static
1217
- // export keeps the expanded fan (every label visible), so no badge there. A
1218
- // neutral dot ringed with the bare member count, emitted hidden; the app shows
1219
- // it at rest and hides it (revealing the spider) on click. ──
1220
- if (clusterUi && layout.clusters.length) {
1221
- const gBadge = svg.append('g').attr('class', 'dgmo-map-cluster-badges');
1222
- for (const cl of layout.clusters) {
1223
- // Decorative: the hit-area (drawn under the dots) owns hover + click; the
1224
- // badge just shows the count, so it never intercepts pointer events.
1225
- const g = gBadge
1226
- .append('g')
1227
- .attr('data-cluster', cl.id)
1228
- .style('opacity', 0)
1229
- .style('pointer-events', 'none');
1230
- const R = 9;
1231
- // Inner neutral disc + outer ring → reads as "more than one here" (RQ2).
1232
- g.append('circle')
1233
- .attr('cx', cl.cx)
1234
- .attr('cy', cl.cy)
1235
- .attr('r', R)
1236
- .attr('fill', mix(palette.textMuted, palette.bg, 35))
1237
- .attr('stroke', palette.textMuted)
1238
- .attr('stroke-width', 1);
1239
- g.append('circle')
1240
- .attr('cx', cl.cx)
1241
- .attr('cy', cl.cy)
1242
- .attr('r', R + 2.5)
1243
- .attr('fill', 'none')
1244
- .attr('stroke', palette.textMuted)
1245
- .attr('stroke-width', 1);
1246
- // Directional colour beads: one small dot threaded on the outer ring per
1247
- // member, placed at the ANGLE of that member's spider leg (centroid → its
1248
- // expanded position) and filled with the member's own marker colour. So the
1249
- // collapsed badge previews WHAT is stacked here and roughly WHERE each item
1250
- // will fan out, before the spider opens. A bg-coloured halo keeps adjacent
1251
- // beads + the ring line legible.
1252
- const beadR = R + 2.5;
1253
- for (const leg of cl.legs) {
1254
- const a = Math.atan2(leg.y2 - cl.cy, leg.x2 - cl.cx);
1255
- g.append('circle')
1256
- .attr('class', 'dgmo-map-cluster-bead')
1257
- .attr('cx', cl.cx + beadR * Math.cos(a))
1258
- .attr('cy', cl.cy + beadR * Math.sin(a))
1259
- .attr('r', 1.8)
1260
- .attr('fill', leg.color)
1261
- .attr('stroke', palette.bg)
1262
- .attr('stroke-width', 0.5);
1263
- }
1264
- // Bare count (RQ1).
1265
- emitText(
1266
- g,
1267
- cl.cx,
1268
- cl.cy + 3,
1269
- String(cl.count),
1270
- 'middle',
1271
- palette.text,
1272
- palette.bg,
1273
- false,
1274
- LABEL_FONT
1275
- );
1276
- }
1277
- }
1278
-
1279
- // ── Legend (categorical via renderLegendD3 + ramp/size/weight blocks; AR1) ──
1280
- if (layout.legend) {
1281
- const legendY =
1282
- (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) +
1283
- (layout.subtitle ? TITLE_FONT_SIZE : 0) +
1284
- 8;
1285
- const legendG = svg
1286
- .append('g')
1287
- .attr('class', 'dgmo-map-legend')
1288
- .attr('transform', `translate(0, ${legendY})`);
1289
- // The value ramp is a selectable colouring group alongside the tag groups
1290
- // (the user flips between them); its capsule renders the gradient inline.
1291
- // Built from the shared helper so the drawn legend matches the band the
1292
- // layout reserved for it (see legend-band.ts).
1293
- const groups = mapLegendGroups(layout.legend);
1294
- if (groups.length > 0) {
1295
- const config: LegendConfig = mapLegendConfig(
1296
- groups,
1297
- exportDims ? 'export' : 'preview'
1298
- );
1299
- renderIntegratedLegend(legendG, {
1300
- ...config,
1301
- palette,
1302
- isDark,
1303
- width,
1304
- activeGroup: layout.legend.activeGroup,
1305
- });
1306
- }
1307
- }
1308
-
1309
- // ── Title / subtitle / caption (foreground — drawn last so they sit above the
1310
- // basemap, POIs, and labels; layout reserves top padding so POIs clear them) ──
1311
- // Soft bg halo so the banner stays legible over busy land/water (the muted
1312
- // subtitle/caption otherwise wash out on mid-toned palettes like nord).
1313
- if (layout.title) {
1314
- svg
1315
- .append('text')
1316
- .attr('class', 'dgmo-map-title')
1317
- .attr('x', width / 2)
1318
- .attr('y', TITLE_Y)
1319
- .attr('text-anchor', 'middle')
1320
- .attr('font-size', TITLE_FONT_SIZE)
1321
- .attr('font-weight', TITLE_FONT_WEIGHT)
1322
- .attr('fill', palette.text)
1323
- .attr('paint-order', 'stroke fill')
1324
- .attr('stroke', palette.bg)
1325
- .attr('stroke-width', 4)
1326
- .attr('stroke-linejoin', 'round')
1327
- .attr('stroke-opacity', 0.7)
1328
- .text(layout.title);
1329
- }
1330
- if (layout.subtitle) {
1331
- svg
1332
- .append('text')
1333
- .attr('class', 'dgmo-map-subtitle')
1334
- .attr('x', width / 2)
1335
- .attr('y', TITLE_Y + TITLE_FONT_SIZE)
1336
- .attr('text-anchor', 'middle')
1337
- .attr('font-size', LABEL_FONT + 1)
1338
- .attr('fill', palette.textMuted)
1339
- .attr('paint-order', 'stroke fill')
1340
- .attr('stroke', palette.bg)
1341
- .attr('stroke-width', 3)
1342
- .attr('stroke-linejoin', 'round')
1343
- .attr('stroke-opacity', 0.7)
1344
- .text(layout.subtitle);
1345
- }
1346
- if (layout.caption) {
1347
- svg
1348
- .append('text')
1349
- .attr('x', width / 2)
1350
- .attr('y', height - 8)
1351
- .attr('text-anchor', 'middle')
1352
- .attr('font-size', LABEL_FONT)
1353
- .attr('fill', palette.textMuted)
1354
- .attr('paint-order', 'stroke fill')
1355
- .attr('stroke', palette.bg)
1356
- .attr('stroke-width', 3)
1357
- .attr('stroke-linejoin', 'round')
1358
- .attr('stroke-opacity', 0.7)
1359
- .text(layout.caption);
1360
- }
1361
- }
1362
-
1363
- /** Export wrapper (no click handler) — matches the structured-renderer contract. */
1364
- export function renderMapForExport(
1365
- container: HTMLDivElement,
1366
- resolved: ResolvedMap,
1367
- data: MapData,
1368
- palette: PaletteColors,
1369
- isDark: boolean,
1370
- exportDims?: D3ExportDimensions
1371
- ): void {
1372
- renderMap(container, resolved, data, palette, isDark, undefined, exportDims);
1373
- }
1374
-
1375
- type Sel = d3Selection.Selection<SVGGElement, unknown, null, undefined>;
1376
-
1377
- function emitText(
1378
- g: Sel,
1379
- x: number,
1380
- y: number,
1381
- text: string,
1382
- anchor: PlacedLabel['anchor'],
1383
- color: string,
1384
- halo: string,
1385
- withHalo: boolean,
1386
- fontSize: number,
1387
- italic?: boolean,
1388
- letterSpacing?: number,
1389
- lines?: readonly string[],
1390
- valueLine?: string
1391
- ): d3Selection.Selection<SVGTextElement, unknown, null, undefined> {
1392
- const t = g
1393
- .append('text')
1394
- .attr('x', x)
1395
- .attr('y', y)
1396
- .attr('text-anchor', anchor)
1397
- .attr('font-size', fontSize)
1398
- .attr('fill', color);
1399
- // Multi-line context labels (water names): stack centred tspans around `y` so
1400
- // the block's vertical centre matches the placement rect. Single-line keeps the
1401
- // bare `.text()` form so every other call site stays byte-identical.
1402
- if (lines && lines.length > 1) {
1403
- const lineHeight = fontSize + 2; // MUST match context-labels LINE_HEIGHT
1404
- const startDy = -((lines.length - 1) / 2) * lineHeight;
1405
- lines.forEach((ln, i) => {
1406
- t.append('tspan')
1407
- .attr('x', x)
1408
- .attr('dy', i === 0 ? startDy : lineHeight)
1409
- .text(ln);
1410
- });
1411
- } else if (valueLine !== undefined) {
1412
- // A choropleth region's name over its metric value: name on top, value below
1413
- // in a smaller, dimmer line (same fill, reduced opacity). Raise the block so
1414
- // its centre sits on the placement `y`. The halo (set below) wraps both lines.
1415
- const valueSize = Math.round(fontSize * 0.82);
1416
- t.append('tspan')
1417
- .attr('x', x)
1418
- .attr('dy', -fontSize * 0.28)
1419
- .text(text);
1420
- t.append('tspan')
1421
- .attr('x', x)
1422
- .attr('dy', fontSize * 0.92)
1423
- .attr('font-size', valueSize)
1424
- .attr('fill-opacity', 0.72)
1425
- .text(valueLine);
1426
- } else {
1427
- t.text(text);
1428
- }
1429
- // Cartographic styling for context labels; absent on every other call site so
1430
- // existing output stays byte-identical (only emitted when explicitly set).
1431
- if (italic) t.attr('font-style', 'italic');
1432
- if (letterSpacing) t.attr('letter-spacing', letterSpacing);
1433
- if (withHalo) {
1434
- // Thin, even outline (2px / 1px-per-side at the 11px label font — 3px read
1435
- // top-heavy as adjacent glyph tops merged their strokes). Round join + cap
1436
- // keep the edge uniform around every glyph.
1437
- t.attr('paint-order', 'stroke fill')
1438
- .attr('stroke', halo)
1439
- .attr('stroke-width', 2)
1440
- .attr('stroke-linejoin', 'round')
1441
- .attr('stroke-linecap', 'round')
1442
- .attr('stroke-opacity', 0.55);
1443
- }
1444
- return t;
1445
- }