@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
package/src/map/renderer.ts
DELETED
|
@@ -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
|
-
}
|