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