@diagrammo/dgmo 0.21.1 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/README.md +16 -6
  2. package/dist/advanced.cjs +2230 -503
  3. package/dist/advanced.d.cts +5731 -0
  4. package/dist/advanced.d.ts +5731 -0
  5. package/dist/advanced.js +2226 -503
  6. package/dist/auto.cjs +2272 -479
  7. package/dist/auto.d.cts +39 -0
  8. package/dist/auto.d.ts +39 -0
  9. package/dist/auto.js +124 -124
  10. package/dist/auto.mjs +2274 -480
  11. package/dist/cli.cjs +170 -170
  12. package/dist/editor.cjs +16 -16
  13. package/dist/editor.js +16 -16
  14. package/dist/highlight.cjs +18 -13
  15. package/dist/highlight.js +18 -13
  16. package/dist/index.cjs +2253 -465
  17. package/dist/index.d.cts +339 -0
  18. package/dist/index.d.ts +339 -0
  19. package/dist/index.js +2255 -466
  20. package/dist/internal.cjs +2230 -503
  21. package/dist/internal.d.cts +5731 -0
  22. package/dist/internal.d.ts +5731 -0
  23. package/dist/internal.js +2226 -503
  24. package/dist/map-data/PROVENANCE.json +1 -1
  25. package/dist/map-data/gazetteer.json +1 -1
  26. package/dist/map-data/mountain-ranges.json +1 -1
  27. package/dist/map-data/water-bodies.json +1 -0
  28. package/dist/map-data/world-coarse.json +1 -1
  29. package/dist/map-data/world-detail.json +1 -1
  30. package/docs/language-reference.md +55 -9
  31. package/gallery/fixtures/boxes-and-lines.dgmo +6 -4
  32. package/gallery/fixtures/map-categorical-world.dgmo +16 -0
  33. package/gallery/fixtures/map-categorical.dgmo +0 -1
  34. package/gallery/fixtures/map-choropleth.dgmo +0 -1
  35. package/gallery/fixtures/map-coastline.dgmo +7 -0
  36. package/gallery/fixtures/map-colorize.dgmo +11 -0
  37. package/gallery/fixtures/map-direct-color.dgmo +0 -1
  38. package/gallery/fixtures/map-reference-world.dgmo +11 -0
  39. package/gallery/fixtures/map-region-scope.dgmo +0 -3
  40. package/gallery/fixtures/map-route.dgmo +0 -1
  41. package/package.json +1 -1
  42. package/src/advanced.ts +12 -1
  43. package/src/boxes-and-lines/parser.ts +39 -0
  44. package/src/boxes-and-lines/renderer.ts +205 -20
  45. package/src/boxes-and-lines/types.ts +9 -0
  46. package/src/cli.ts +1 -1
  47. package/src/completion.ts +36 -30
  48. package/src/cycle/renderer.ts +14 -1
  49. package/src/d3.ts +20 -6
  50. package/src/editor/highlight-api.ts +4 -0
  51. package/src/editor/keywords.ts +16 -16
  52. package/src/infra/renderer.ts +35 -7
  53. package/src/map/colorize.ts +54 -0
  54. package/src/map/context-labels.ts +429 -0
  55. package/src/map/data/PROVENANCE.json +1 -1
  56. package/src/map/data/README.md +6 -0
  57. package/src/map/data/gazetteer.json +1 -1
  58. package/src/map/data/mountain-ranges.json +1 -1
  59. package/src/map/data/types.ts +34 -0
  60. package/src/map/data/water-bodies.json +1 -0
  61. package/src/map/data/world-coarse.json +1 -1
  62. package/src/map/data/world-detail.json +1 -1
  63. package/src/map/dimensions.ts +117 -0
  64. package/src/map/geo-query.ts +21 -3
  65. package/src/map/geo.ts +47 -1
  66. package/src/map/layout.ts +1408 -266
  67. package/src/map/load-data.ts +10 -2
  68. package/src/map/parser.ts +42 -116
  69. package/src/map/renderer.ts +604 -14
  70. package/src/map/resolved-types.ts +16 -2
  71. package/src/map/resolver.ts +208 -59
  72. package/src/map/types.ts +30 -32
  73. package/src/mindmap/renderer.ts +10 -1
  74. package/src/palettes/atlas.ts +77 -0
  75. package/src/palettes/blueprint.ts +73 -0
  76. package/src/palettes/color-utils.ts +58 -1
  77. package/src/palettes/index.ts +12 -3
  78. package/src/palettes/slate.ts +73 -0
  79. package/src/palettes/tidewater.ts +73 -0
  80. package/src/render.ts +8 -1
  81. package/src/tech-radar/renderer.ts +3 -0
  82. package/src/tech-radar/types.ts +3 -0
  83. package/src/utils/d3-types.ts +5 -0
  84. package/src/utils/legend-layout.ts +21 -4
  85. package/src/utils/legend-types.ts +7 -0
  86. package/src/utils/reserved-key-registry.ts +8 -3
  87. package/src/palettes/bold.ts +0 -67
package/dist/auto.mjs CHANGED
@@ -91,18 +91,18 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
91
91
  const results = [];
92
92
  for (let i = 0; i < points.length; i++) {
93
93
  const pt = points[i];
94
- const labelWidth = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
94
+ const labelWidth2 = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
95
95
  let best = null;
96
96
  const directions = [
97
97
  {
98
98
  // Above
99
99
  gen: (offset) => {
100
- const lx = pt.cx - labelWidth / 2;
100
+ const lx = pt.cx - labelWidth2 / 2;
101
101
  const ly = pt.cy - offset - labelHeight;
102
- if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
102
+ if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
103
103
  return null;
104
104
  return {
105
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
105
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
106
106
  textX: pt.cx,
107
107
  textY: ly + labelHeight / 2,
108
108
  anchor: "middle"
@@ -112,12 +112,12 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
112
112
  {
113
113
  // Below
114
114
  gen: (offset) => {
115
- const lx = pt.cx - labelWidth / 2;
115
+ const lx = pt.cx - labelWidth2 / 2;
116
116
  const ly = pt.cy + offset;
117
- if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
117
+ if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
118
118
  return null;
119
119
  return {
120
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
120
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
121
121
  textX: pt.cx,
122
122
  textY: ly + labelHeight / 2,
123
123
  anchor: "middle"
@@ -129,10 +129,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
129
129
  gen: (offset) => {
130
130
  const lx = pt.cx + offset;
131
131
  const ly = pt.cy - labelHeight / 2;
132
- if (lx + labelWidth > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
132
+ if (lx + labelWidth2 > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
133
133
  return null;
134
134
  return {
135
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
135
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
136
136
  textX: lx,
137
137
  textY: pt.cy,
138
138
  anchor: "start"
@@ -142,13 +142,13 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
142
142
  {
143
143
  // Left
144
144
  gen: (offset) => {
145
- const lx = pt.cx - offset - labelWidth;
145
+ const lx = pt.cx - offset - labelWidth2;
146
146
  const ly = pt.cy - labelHeight / 2;
147
147
  if (lx < chartBounds.left || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
148
148
  return null;
149
149
  return {
150
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
151
- textX: lx + labelWidth,
150
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
151
+ textX: lx + labelWidth2,
152
152
  textY: pt.cy,
153
153
  anchor: "end"
154
154
  };
@@ -198,10 +198,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
198
198
  }
199
199
  }
200
200
  if (!best) {
201
- const lx = pt.cx - labelWidth / 2;
201
+ const lx = pt.cx - labelWidth2 / 2;
202
202
  const ly = pt.cy - minGap - labelHeight;
203
203
  best = {
204
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
204
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
205
205
  textX: pt.cx,
206
206
  textY: ly + labelHeight / 2,
207
207
  anchor: "middle",
@@ -761,6 +761,9 @@ var init_reserved_key_registry = __esm({
761
761
  "value",
762
762
  "label",
763
763
  "style"
764
+ // `surface:` was removed in the 2026-06-02 defaults-on review — it is no longer
765
+ // a recognized metadata key (the route/edge surface feature was cut; §24B.7).
766
+ // A stray `surface: water` is no longer captured as a reserved key.
764
767
  ]);
765
768
  ORG_REGISTRY = staticRegistry([
766
769
  "color",
@@ -815,9 +818,7 @@ var init_reserved_key_registry = __esm({
815
818
  BOXES_AND_LINES_REGISTRY = staticRegistry([
816
819
  "color",
817
820
  "description",
818
- "width",
819
- "split",
820
- "fanout"
821
+ "value"
821
822
  ]);
822
823
  TIMELINE_REGISTRY = staticRegistry([
823
824
  "color",
@@ -1823,77 +1824,266 @@ function getSegmentColors(palette, count) {
1823
1824
  (_, i) => hslToHex(Math.round((startHue + i * step) % 360), avgS, avgL)
1824
1825
  );
1825
1826
  }
1827
+ function politicalTints(palette, count, isDark) {
1828
+ if (count <= 0) return [];
1829
+ const base = isDark ? palette.surface : palette.bg;
1830
+ const c = palette.colors;
1831
+ const swatches = [
1832
+ .../* @__PURE__ */ new Set([
1833
+ c.green,
1834
+ c.yellow,
1835
+ c.orange,
1836
+ c.purple,
1837
+ c.red,
1838
+ c.teal,
1839
+ c.cyan,
1840
+ c.blue
1841
+ ])
1842
+ ];
1843
+ const bands = isDark ? POLITICAL_TINT_BANDS.dark : POLITICAL_TINT_BANDS.light;
1844
+ const out = [];
1845
+ for (const pct of bands) {
1846
+ if (out.length >= count) break;
1847
+ for (const s of swatches) out.push(mix(s, base, pct));
1848
+ }
1849
+ return out.slice(0, count);
1850
+ }
1851
+ var POLITICAL_TINT_BANDS;
1826
1852
  var init_color_utils = __esm({
1827
1853
  "src/palettes/color-utils.ts"() {
1828
1854
  "use strict";
1855
+ POLITICAL_TINT_BANDS = {
1856
+ light: [32, 48, 64, 80],
1857
+ dark: [44, 58, 72, 86]
1858
+ };
1829
1859
  }
1830
1860
  });
1831
1861
 
1832
- // src/palettes/bold.ts
1833
- var boldPalette;
1834
- var init_bold = __esm({
1835
- "src/palettes/bold.ts"() {
1862
+ // src/palettes/atlas.ts
1863
+ var atlasPalette;
1864
+ var init_atlas = __esm({
1865
+ "src/palettes/atlas.ts"() {
1836
1866
  "use strict";
1837
1867
  init_registry();
1838
- boldPalette = {
1839
- id: "bold",
1840
- name: "Bold",
1868
+ atlasPalette = {
1869
+ id: "atlas",
1870
+ name: "Atlas",
1841
1871
  light: {
1842
- bg: "#ffffff",
1843
- surface: "#f0f0f0",
1844
- overlay: "#f0f0f0",
1845
- border: "#cccccc",
1846
- text: "#000000",
1847
- textMuted: "#666666",
1848
- textOnFillLight: "#ffffff",
1849
- textOnFillDark: "#000000",
1850
- primary: "#0000ff",
1851
- secondary: "#ff00ff",
1852
- accent: "#00cccc",
1853
- destructive: "#ff0000",
1872
+ bg: "#f3ead3",
1873
+ // warm manila / parchment
1874
+ surface: "#ece0c0",
1875
+ // deeper paper (cards, panels)
1876
+ overlay: "#e8dab8",
1877
+ // popovers, dropdowns
1878
+ border: "#bcaa86",
1879
+ // muted sepia rule line
1880
+ text: "#463a26",
1881
+ // aged sepia-brown ink
1882
+ textMuted: "#7a6a4f",
1883
+ // faded annotation ink
1884
+ textOnFillLight: "#f7f1de",
1885
+ // parchment (light text on dark fills)
1886
+ textOnFillDark: "#3a2e1c",
1887
+ // deep ink (dark text on light fills)
1888
+ primary: "#5b7a99",
1889
+ // pull-down map ocean (steel-blue)
1890
+ secondary: "#7e9a6f",
1891
+ // lowland sage / celadon
1892
+ accent: "#b07f7c",
1893
+ // dusty rose
1894
+ destructive: "#b25a45",
1895
+ // brick / terracotta
1854
1896
  colors: {
1855
- red: "#ff0000",
1856
- orange: "#ff8000",
1857
- yellow: "#ffcc00",
1858
- green: "#00cc00",
1859
- blue: "#0000ff",
1860
- purple: "#cc00cc",
1861
- teal: "#008080",
1862
- cyan: "#00cccc",
1863
- gray: "#808080",
1864
- black: "#000000",
1865
- white: "#f0f0f0"
1897
+ red: "#bf6a52",
1898
+ // terracotta brick
1899
+ orange: "#cf9a5c",
1900
+ // map tan / ochre
1901
+ yellow: "#cdb35e",
1902
+ // straw / muted lemon
1903
+ green: "#7e9a6f",
1904
+ // sage / celadon lowland
1905
+ blue: "#5b7a99",
1906
+ // steel-blue ocean
1907
+ purple: "#9a7fa6",
1908
+ // dusty lilac / mauve
1909
+ teal: "#6fa094",
1910
+ // muted seafoam
1911
+ cyan: "#79a7b5",
1912
+ // shallow-water blue
1913
+ gray: "#8a7d68",
1914
+ // warm taupe
1915
+ black: "#463a26",
1916
+ // ink
1917
+ white: "#ece0c0"
1918
+ // paper
1866
1919
  }
1867
1920
  },
1868
1921
  dark: {
1869
- bg: "#000000",
1870
- surface: "#111111",
1871
- overlay: "#1a1a1a",
1872
- border: "#333333",
1873
- text: "#ffffff",
1874
- textMuted: "#aaaaaa",
1875
- textOnFillLight: "#ffffff",
1876
- textOnFillDark: "#000000",
1877
- primary: "#00ccff",
1878
- secondary: "#ff00ff",
1879
- accent: "#ffff00",
1880
- destructive: "#ff0000",
1922
+ bg: "#1e2a33",
1923
+ // deep map ocean (night globe)
1924
+ surface: "#27353f",
1925
+ // raised ocean
1926
+ overlay: "#2e3d48",
1927
+ // popovers, dropdowns
1928
+ border: "#3d4f5c",
1929
+ // depth-contour line
1930
+ text: "#e8dcc0",
1931
+ // parchment ink, inverted
1932
+ textMuted: "#a89a7d",
1933
+ // faded label
1934
+ textOnFillLight: "#f7f1de",
1935
+ // parchment
1936
+ textOnFillDark: "#1a242c",
1937
+ // deep ocean ink
1938
+ primary: "#7ba0bf",
1939
+ // brighter ocean
1940
+ secondary: "#9bb588",
1941
+ // sage, lifted
1942
+ accent: "#cf9a96",
1943
+ // dusty rose, lifted
1944
+ destructive: "#c9745c",
1945
+ // brick, lifted
1881
1946
  colors: {
1882
- red: "#ff0000",
1883
- orange: "#ff8000",
1884
- yellow: "#ffff00",
1885
- green: "#00ff00",
1886
- blue: "#0066ff",
1887
- purple: "#ff00ff",
1888
- teal: "#00cccc",
1889
- cyan: "#00ffff",
1890
- gray: "#808080",
1891
- black: "#111111",
1892
- white: "#ffffff"
1947
+ red: "#cf7a60",
1948
+ // terracotta
1949
+ orange: "#d9a96a",
1950
+ // tan / ochre
1951
+ yellow: "#d8c074",
1952
+ // straw
1953
+ green: "#9bb588",
1954
+ // sage lowland
1955
+ blue: "#7ba0bf",
1956
+ // ocean
1957
+ purple: "#b59ac0",
1958
+ // lilac / mauve
1959
+ teal: "#85b3a6",
1960
+ // seafoam
1961
+ cyan: "#92bccb",
1962
+ // shallow-water blue
1963
+ gray: "#9a8d76",
1964
+ // warm taupe
1965
+ black: "#27353f",
1966
+ // raised ocean
1967
+ white: "#e8dcc0"
1968
+ // parchment
1893
1969
  }
1894
1970
  }
1895
1971
  };
1896
- registerPalette(boldPalette);
1972
+ registerPalette(atlasPalette);
1973
+ }
1974
+ });
1975
+
1976
+ // src/palettes/blueprint.ts
1977
+ var blueprintPalette;
1978
+ var init_blueprint = __esm({
1979
+ "src/palettes/blueprint.ts"() {
1980
+ "use strict";
1981
+ init_registry();
1982
+ blueprintPalette = {
1983
+ id: "blueprint",
1984
+ name: "Blueprint",
1985
+ light: {
1986
+ bg: "#f4f8fb",
1987
+ // pale drafting white (faint cyan)
1988
+ surface: "#e6eef4",
1989
+ // drafting panel
1990
+ overlay: "#dde9f1",
1991
+ // popovers, dropdowns
1992
+ border: "#aac3d6",
1993
+ // pale blue grid line
1994
+ text: "#123a5e",
1995
+ // blueprint navy ink
1996
+ textMuted: "#4f7390",
1997
+ // faint draft note
1998
+ textOnFillLight: "#f4f8fb",
1999
+ // drafting white
2000
+ textOnFillDark: "#0c2f4d",
2001
+ // deep blueprint navy
2002
+ primary: "#1f5e8c",
2003
+ // blueprint blue
2004
+ secondary: "#5b7d96",
2005
+ // steel
2006
+ accent: "#b08a3e",
2007
+ // draftsman's ochre highlight
2008
+ destructive: "#c0504d",
2009
+ // correction red
2010
+ colors: {
2011
+ red: "#c25a4e",
2012
+ // correction red
2013
+ orange: "#c2823e",
2014
+ // ochre
2015
+ yellow: "#c2a843",
2016
+ // pencil gold
2017
+ green: "#4f8a6b",
2018
+ // drafting green
2019
+ blue: "#1f5e8c",
2020
+ // blueprint blue
2021
+ purple: "#6f5e96",
2022
+ // indigo pencil
2023
+ teal: "#3a8a8a",
2024
+ // teal
2025
+ cyan: "#3f8fb5",
2026
+ // cyan
2027
+ gray: "#7e8e98",
2028
+ // graphite
2029
+ black: "#123a5e",
2030
+ // navy ink
2031
+ white: "#e6eef4"
2032
+ // panel
2033
+ }
2034
+ },
2035
+ dark: {
2036
+ bg: "#103a5e",
2037
+ // deep blueprint blue (cyanotype ground)
2038
+ surface: "#16466e",
2039
+ // raised sheet
2040
+ overlay: "#1c5180",
2041
+ // popovers, dropdowns
2042
+ border: "#3a6f96",
2043
+ // grid line
2044
+ text: "#eaf2f8",
2045
+ // chalk white
2046
+ textMuted: "#9fc0d6",
2047
+ // faint chalk note
2048
+ textOnFillLight: "#eaf2f8",
2049
+ // chalk white
2050
+ textOnFillDark: "#0c2f4d",
2051
+ // deep blueprint navy
2052
+ primary: "#7fb8d8",
2053
+ // chalk cyan
2054
+ secondary: "#9fb8c8",
2055
+ // pale steel
2056
+ accent: "#d8c27a",
2057
+ // chalk amber
2058
+ destructive: "#e08a7a",
2059
+ // chalk correction red
2060
+ colors: {
2061
+ red: "#e0907e",
2062
+ // chalk red
2063
+ orange: "#e0ab78",
2064
+ // chalk amber
2065
+ yellow: "#e3d089",
2066
+ // chalk gold
2067
+ green: "#93c79e",
2068
+ // chalk green
2069
+ blue: "#8ec3e0",
2070
+ // chalk cyan-blue
2071
+ purple: "#b6a6d8",
2072
+ // chalk indigo
2073
+ teal: "#84c7c2",
2074
+ // chalk teal
2075
+ cyan: "#9fd6e0",
2076
+ // chalk cyan
2077
+ gray: "#aebecb",
2078
+ // chalk graphite
2079
+ black: "#16466e",
2080
+ // raised sheet
2081
+ white: "#eaf2f8"
2082
+ // chalk white
2083
+ }
2084
+ }
2085
+ };
2086
+ registerPalette(blueprintPalette);
1897
2087
  }
1898
2088
  });
1899
2089
 
@@ -2390,6 +2580,120 @@ var init_rose_pine = __esm({
2390
2580
  }
2391
2581
  });
2392
2582
 
2583
+ // src/palettes/slate.ts
2584
+ var slatePalette;
2585
+ var init_slate = __esm({
2586
+ "src/palettes/slate.ts"() {
2587
+ "use strict";
2588
+ init_registry();
2589
+ slatePalette = {
2590
+ id: "slate",
2591
+ name: "Slate",
2592
+ light: {
2593
+ bg: "#ffffff",
2594
+ // clean slide white
2595
+ surface: "#f3f5f8",
2596
+ // light cool-gray panel
2597
+ overlay: "#eaeef3",
2598
+ // popovers, dropdowns
2599
+ border: "#d4dae1",
2600
+ // hairline rule
2601
+ text: "#1f2933",
2602
+ // near-black slate (softer than pure black)
2603
+ textMuted: "#5b6672",
2604
+ // secondary label
2605
+ textOnFillLight: "#ffffff",
2606
+ // light text on dark fills
2607
+ textOnFillDark: "#1f2933",
2608
+ // dark text on light fills
2609
+ primary: "#3b6ea5",
2610
+ // confident corporate blue
2611
+ secondary: "#5b6672",
2612
+ // slate gray
2613
+ accent: "#3a9188",
2614
+ // muted teal accent
2615
+ destructive: "#c0504d",
2616
+ // brick red
2617
+ colors: {
2618
+ red: "#c0504d",
2619
+ // brick
2620
+ orange: "#cc7a33",
2621
+ // muted amber
2622
+ yellow: "#c9a227",
2623
+ // gold (not neon)
2624
+ green: "#5b9357",
2625
+ // forest / sage
2626
+ blue: "#3b6ea5",
2627
+ // corporate blue
2628
+ purple: "#7d5ba6",
2629
+ // muted violet
2630
+ teal: "#3a9188",
2631
+ // teal
2632
+ cyan: "#4f96c4",
2633
+ // steel cyan
2634
+ gray: "#7e8a97",
2635
+ // cool gray
2636
+ black: "#1f2933",
2637
+ // slate ink
2638
+ white: "#f3f5f8"
2639
+ // panel
2640
+ }
2641
+ },
2642
+ dark: {
2643
+ bg: "#161b22",
2644
+ // deep slate (keynote dark)
2645
+ surface: "#202833",
2646
+ // raised panel
2647
+ overlay: "#29323e",
2648
+ // popovers, dropdowns
2649
+ border: "#38424f",
2650
+ // divider
2651
+ text: "#e6eaef",
2652
+ // off-white
2653
+ textMuted: "#9aa5b1",
2654
+ // secondary label
2655
+ textOnFillLight: "#ffffff",
2656
+ // light text on dark fills
2657
+ textOnFillDark: "#161b22",
2658
+ // dark text on light fills
2659
+ primary: "#5b9bd5",
2660
+ // lifted corporate blue
2661
+ secondary: "#8593a3",
2662
+ // slate gray, lifted
2663
+ accent: "#45b3a3",
2664
+ // teal, lifted
2665
+ destructive: "#e07b6e",
2666
+ // brick, lifted
2667
+ colors: {
2668
+ red: "#e07b6e",
2669
+ // brick
2670
+ orange: "#e0975a",
2671
+ // amber
2672
+ yellow: "#d9bd5a",
2673
+ // gold
2674
+ green: "#74b56e",
2675
+ // forest / sage
2676
+ blue: "#5b9bd5",
2677
+ // corporate blue
2678
+ purple: "#a585c9",
2679
+ // violet
2680
+ teal: "#45b3a3",
2681
+ // teal
2682
+ cyan: "#62b0d9",
2683
+ // steel cyan
2684
+ gray: "#95a1ae",
2685
+ // cool gray
2686
+ black: "#202833",
2687
+ // raised panel
2688
+ white: "#e6eaef"
2689
+ // off-white
2690
+ }
2691
+ }
2692
+ };
2693
+ registerPalette(slatePalette);
2694
+ }
2695
+ });
2696
+
2393
2697
  // src/palettes/solarized.ts
2394
2698
  var solarizedPalette;
2395
2699
  var init_solarized = __esm({
@@ -2485,6 +2789,120 @@ var init_solarized = __esm({
2485
2789
  }
2486
2790
  });
2487
2791
 
2792
+ // src/palettes/tidewater.ts
2793
+ var tidewaterPalette;
2794
+ var init_tidewater = __esm({
2795
+ "src/palettes/tidewater.ts"() {
2796
+ "use strict";
2797
+ init_registry();
2798
+ tidewaterPalette = {
2799
+ id: "tidewater",
2800
+ name: "Tidewater",
2801
+ light: {
2802
+ bg: "#eceff0",
2803
+ // weathered sea-mist paper
2804
+ surface: "#e0e4e3",
2805
+ // worn deck panel
2806
+ overlay: "#dadfdf",
2807
+ // popovers, dropdowns
2808
+ border: "#a9b2b3",
2809
+ // muted slate rule
2810
+ text: "#18313f",
2811
+ // ship's-log navy ink
2812
+ textMuted: "#51636b",
2813
+ // faded log entry
2814
+ textOnFillLight: "#f3f5f3",
2815
+ // weathered white
2816
+ textOnFillDark: "#162c38",
2817
+ // deep navy
2818
+ primary: "#1f4e6b",
2819
+ // deep-sea navy
2820
+ secondary: "#b08a4f",
2821
+ // rope / manila tan
2822
+ accent: "#c69a3e",
2823
+ // brass
2824
+ destructive: "#c1433a",
2825
+ // signal-flag red
2826
+ colors: {
2827
+ red: "#c1433a",
2828
+ // signal-flag red
2829
+ orange: "#cc7a38",
2830
+ // weathered amber
2831
+ yellow: "#d6bf5a",
2832
+ // brass gold
2833
+ green: "#4f8a6b",
2834
+ // sea-glass green
2835
+ blue: "#1f4e6b",
2836
+ // deep-sea navy
2837
+ purple: "#6a5a8c",
2838
+ // twilight harbor
2839
+ teal: "#3d8c8c",
2840
+ // sea-glass teal
2841
+ cyan: "#4f9bb5",
2842
+ // shallow water
2843
+ gray: "#8a8d86",
2844
+ // driftwood gray
2845
+ black: "#18313f",
2846
+ // navy ink
2847
+ white: "#e0e4e3"
2848
+ // deck panel
2849
+ }
2850
+ },
2851
+ dark: {
2852
+ bg: "#0f2230",
2853
+ // night-harbor deep sea
2854
+ surface: "#16303f",
2855
+ // raised hull
2856
+ overlay: "#1d3a4a",
2857
+ // popovers, dropdowns
2858
+ border: "#2c4856",
2859
+ // rigging line
2860
+ text: "#e6ebe8",
2861
+ // weathered white
2862
+ textMuted: "#9aaab0",
2863
+ // faded label
2864
+ textOnFillLight: "#f3f5f3",
2865
+ // weathered white
2866
+ textOnFillDark: "#0f2230",
2867
+ // deep sea
2868
+ primary: "#4f9bc4",
2869
+ // lifted sea blue
2870
+ secondary: "#c9a46a",
2871
+ // rope tan, lifted
2872
+ accent: "#d9b25a",
2873
+ // brass, lifted
2874
+ destructive: "#e06a5e",
2875
+ // signal red, lifted
2876
+ colors: {
2877
+ red: "#e06a5e",
2878
+ // signal-flag red
2879
+ orange: "#df9a52",
2880
+ // amber
2881
+ yellow: "#e0c662",
2882
+ // brass gold
2883
+ green: "#6fb58c",
2884
+ // sea-glass green
2885
+ blue: "#4f9bc4",
2886
+ // sea blue
2887
+ purple: "#9486bf",
2888
+ // twilight harbor
2889
+ teal: "#5cb0ac",
2890
+ // sea-glass teal
2891
+ cyan: "#62b4cf",
2892
+ // shallow water
2893
+ gray: "#9aa39c",
2894
+ // driftwood gray
2895
+ black: "#16303f",
2896
+ // raised hull
2897
+ white: "#e6ebe8"
2898
+ // weathered white
2899
+ }
2900
+ }
2901
+ };
2902
+ registerPalette(tidewaterPalette);
2903
+ }
2904
+ });
2905
+
2488
2906
  // src/palettes/tokyo-night.ts
2489
2907
  var tokyoNightPalette;
2490
2908
  var init_tokyo_night = __esm({
@@ -2760,7 +3178,8 @@ var init_monokai = __esm({
2760
3178
  // src/palettes/index.ts
2761
3179
  var palettes_exports = {};
2762
3180
  __export(palettes_exports, {
2763
- boldPalette: () => boldPalette,
3181
+ atlasPalette: () => atlasPalette,
3182
+ blueprintPalette: () => blueprintPalette,
2764
3183
  catppuccinPalette: () => catppuccinPalette,
2765
3184
  contrastText: () => contrastText,
2766
3185
  draculaPalette: () => draculaPalette,
@@ -2781,7 +3200,9 @@ __export(palettes_exports, {
2781
3200
  rosePinePalette: () => rosePinePalette,
2782
3201
  shade: () => shade,
2783
3202
  shapeFill: () => shapeFill,
3203
+ slatePalette: () => slatePalette,
2784
3204
  solarizedPalette: () => solarizedPalette,
3205
+ tidewaterPalette: () => tidewaterPalette,
2785
3206
  tint: () => tint,
2786
3207
  tokyoNightPalette: () => tokyoNightPalette
2787
3208
  });
@@ -2791,17 +3212,21 @@ var init_palettes = __esm({
2791
3212
  "use strict";
2792
3213
  init_registry();
2793
3214
  init_color_utils();
2794
- init_bold();
3215
+ init_atlas();
3216
+ init_blueprint();
2795
3217
  init_catppuccin();
2796
3218
  init_gruvbox();
2797
3219
  init_nord();
2798
3220
  init_one_dark();
2799
3221
  init_rose_pine();
3222
+ init_slate();
2800
3223
  init_solarized();
3224
+ init_tidewater();
2801
3225
  init_tokyo_night();
2802
3226
  init_dracula();
2803
3227
  init_monokai();
2804
- init_bold();
3228
+ init_atlas();
3229
+ init_blueprint();
2805
3230
  init_catppuccin();
2806
3231
  init_dracula();
2807
3232
  init_gruvbox();
@@ -2809,9 +3234,15 @@ var init_palettes = __esm({
2809
3234
  init_nord();
2810
3235
  init_one_dark();
2811
3236
  init_rose_pine();
3237
+ init_slate();
2812
3238
  init_solarized();
3239
+ init_tidewater();
2813
3240
  init_tokyo_night();
2814
3241
  palettes = {
3242
+ atlas: atlasPalette,
3243
+ blueprint: blueprintPalette,
3244
+ slate: slatePalette,
3245
+ tidewater: tidewaterPalette,
2815
3246
  nord: nordPalette,
2816
3247
  catppuccin: catppuccinPalette,
2817
3248
  solarized: solarizedPalette,
@@ -2820,8 +3251,7 @@ var init_palettes = __esm({
2820
3251
  oneDark: oneDarkPalette,
2821
3252
  rosePine: rosePinePalette,
2822
3253
  dracula: draculaPalette,
2823
- monokai: monokaiPalette,
2824
- bold: boldPalette
3254
+ monokai: monokaiPalette
2825
3255
  };
2826
3256
  }
2827
3257
  });
@@ -3331,6 +3761,9 @@ function controlsGroupCapsuleWidth(toggles) {
3331
3761
  }
3332
3762
  return w;
3333
3763
  }
3764
+ function isAppHostedControls(config, isExport) {
3765
+ return !isExport && config.controlsHost === "app" && !!config.controlsGroup && config.controlsGroup.toggles.length > 0;
3766
+ }
3334
3767
  function buildControlsGroupLayout(config, state) {
3335
3768
  const cg = config.controlsGroup;
3336
3769
  if (!cg || cg.toggles.length === 0) return void 0;
@@ -3384,6 +3817,7 @@ function buildControlsGroupLayout(config, state) {
3384
3817
  function computeLegendLayout(config, state, containerWidth) {
3385
3818
  const { groups, controls: configControls, mode } = config;
3386
3819
  const isExport = mode === "export";
3820
+ const gated = isAppHostedControls(config, isExport);
3387
3821
  const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
3388
3822
  if (isExport && !activeGroupName) {
3389
3823
  return {
@@ -3394,7 +3828,7 @@ function computeLegendLayout(config, state, containerWidth) {
3394
3828
  pills: []
3395
3829
  };
3396
3830
  }
3397
- const controlsGroupLayout = isExport ? void 0 : buildControlsGroupLayout(config, state);
3831
+ const controlsGroupLayout = isExport || gated ? void 0 : buildControlsGroupLayout(config, state);
3398
3832
  const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0 || !!g.gradient);
3399
3833
  if (visibleGroups.length === 0 && (!configControls || configControls.length === 0) && !controlsGroupLayout) {
3400
3834
  return {
@@ -8296,8 +8730,8 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8296
8730
  const pt = points[i];
8297
8731
  const ptSize = pt.size ?? symbolSize;
8298
8732
  const minGap = ptSize / 2 + 4;
8299
- const labelWidth = pt.name.length * fontSize * 0.6 + 8;
8300
- const labelX = pt.px - labelWidth / 2;
8733
+ const labelWidth2 = pt.name.length * fontSize * 0.6 + 8;
8734
+ const labelX = pt.px - labelWidth2 / 2;
8301
8735
  let bestLabelY = 0;
8302
8736
  let bestOffset = Infinity;
8303
8737
  let placed = false;
@@ -8309,7 +8743,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8309
8743
  const candidate = {
8310
8744
  x: labelX,
8311
8745
  y: labelY,
8312
- w: labelWidth,
8746
+ w: labelWidth2,
8313
8747
  h: labelHeight
8314
8748
  };
8315
8749
  let collision = false;
@@ -8351,7 +8785,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8351
8785
  const labelRect = {
8352
8786
  x: labelX,
8353
8787
  y: bestLabelY,
8354
- w: labelWidth,
8788
+ w: labelWidth2,
8355
8789
  h: labelHeight
8356
8790
  };
8357
8791
  placedLabels.push(labelRect);
@@ -8387,7 +8821,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8387
8821
  shape: {
8388
8822
  x: labelX - bgPad,
8389
8823
  y: bestLabelY - bgPad,
8390
- width: labelWidth + bgPad * 2,
8824
+ width: labelWidth2 + bgPad * 2,
8391
8825
  height: labelHeight + bgPad * 2
8392
8826
  },
8393
8827
  style: { fill: bg },
@@ -15823,10 +16257,6 @@ function parseMap(content) {
15823
16257
  handleTag(trimmed, lineNumber);
15824
16258
  continue;
15825
16259
  }
15826
- if ((firstWord === "muted" || firstWord === "natural") && trimmed === firstWord) {
15827
- handleDirective(firstWord, "", lineNumber);
15828
- continue;
15829
- }
15830
16260
  if (DIRECTIVE_SET.has(firstWord) && !trimmed.slice(firstWord.length).trimStart().startsWith(":")) {
15831
16261
  handleDirective(
15832
16262
  firstWord,
@@ -15873,24 +16303,6 @@ function parseMap(content) {
15873
16303
  pushWarning(line12, `Duplicate directive "${key}" \u2014 last value wins.`);
15874
16304
  };
15875
16305
  switch (key) {
15876
- case "region":
15877
- dup(d.region);
15878
- d.region = value;
15879
- break;
15880
- case "projection":
15881
- dup(d.projection);
15882
- if (value && ![
15883
- "equirectangular",
15884
- "natural-earth",
15885
- "albers-usa",
15886
- "mercator"
15887
- ].includes(value))
15888
- pushWarning(
15889
- line12,
15890
- `Unknown projection "${value}" (expected equirectangular | natural-earth | albers-usa | mercator).`
15891
- );
15892
- d.projection = value;
15893
- break;
15894
16306
  case "region-metric": {
15895
16307
  dup(d.regionMetric);
15896
16308
  const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
@@ -15906,91 +16318,43 @@ function parseMap(content) {
15906
16318
  dup(d.flowMetric);
15907
16319
  d.flowMetric = value;
15908
16320
  break;
15909
- case "scale":
15910
- dup(d.scale);
15911
- {
15912
- const s = parseScale(value, line12);
15913
- if (s) d.scale = s;
15914
- }
15915
- break;
15916
- case "region-labels":
15917
- dup(d.regionLabels);
15918
- if (value && !["full", "abbrev", "off"].includes(value))
15919
- pushWarning(
15920
- line12,
15921
- `Unknown region-labels "${value}" (expected full | abbrev | off).`
15922
- );
15923
- d.regionLabels = value;
15924
- break;
15925
- case "poi-labels":
15926
- dup(d.poiLabels);
15927
- if (value && !["off", "auto", "all"].includes(value))
15928
- pushWarning(
15929
- line12,
15930
- `Unknown poi-labels "${value}" (expected off | auto | all).`
15931
- );
15932
- d.poiLabels = value;
15933
- break;
15934
- case "default-country":
15935
- dup(d.defaultCountry);
15936
- d.defaultCountry = value;
15937
- break;
15938
- case "default-state":
15939
- dup(d.defaultState);
15940
- d.defaultState = value;
16321
+ case "locale":
16322
+ dup(d.locale);
16323
+ d.locale = value;
15941
16324
  break;
15942
16325
  case "active-tag":
15943
16326
  dup(d.activeTag);
15944
16327
  d.activeTag = value;
15945
16328
  break;
16329
+ case "caption":
16330
+ dup(d.caption);
16331
+ d.caption = value;
16332
+ break;
16333
+ // ── Cosmetic `no-*` opt-outs: bare flags, idempotent (mirror `no-legend`,
16334
+ // no dup warning); each defaults the feature ON when absent. ──
15946
16335
  case "no-legend":
15947
16336
  d.noLegend = true;
15948
16337
  break;
15949
- case "no-insets":
15950
- d.noInsets = true;
16338
+ case "no-coastline":
16339
+ d.noCoastline = true;
15951
16340
  break;
15952
- case "relief":
15953
- d.relief = true;
16341
+ case "no-relief":
16342
+ d.noRelief = true;
15954
16343
  break;
15955
- case "muted":
15956
- case "natural":
15957
- if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
15958
- pushWarning(
15959
- line12,
15960
- `Conflicting basemap dress \u2014 "${d.basemapStyle}" then "${key}"; last wins.`
15961
- );
15962
- d.basemapStyle = key;
16344
+ case "no-context-labels":
16345
+ d.noContextLabels = true;
15963
16346
  break;
15964
- case "subtitle":
15965
- dup(d.subtitle);
15966
- d.subtitle = value;
16347
+ case "no-region-labels":
16348
+ d.noRegionLabels = true;
15967
16349
  break;
15968
- case "caption":
15969
- dup(d.caption);
15970
- d.caption = value;
16350
+ case "no-poi-labels":
16351
+ d.noPoiLabels = true;
16352
+ break;
16353
+ case "no-colorize":
16354
+ d.noColorize = true;
15971
16355
  break;
15972
16356
  }
15973
16357
  }
15974
- function parseScale(value, line12) {
15975
- const toks = value.split(/\s+/).filter(Boolean);
15976
- const min = Number(toks[0]);
15977
- const max = Number(toks[1]);
15978
- if (!Number.isFinite(min) || !Number.isFinite(max)) {
15979
- pushError(line12, `scale requires numeric <min> <max> (got "${value}").`);
15980
- return null;
15981
- }
15982
- const scale = { min, max };
15983
- if (toks[2] === "center") {
15984
- const c = Number(toks[3]);
15985
- if (Number.isFinite(c)) scale.center = c;
15986
- else
15987
- pushError(
15988
- line12,
15989
- `scale center requires a number (got "${toks[3] ?? ""}").`
15990
- );
15991
- }
15992
- return scale;
15993
- }
15994
16358
  function handleTag(trimmed, line12) {
15995
16359
  const m = matchTagBlockHeading(trimmed);
15996
16360
  if (!m) {
@@ -16190,13 +16554,15 @@ function parseMap(content) {
16190
16554
  pushError(line12, `Edge has an empty endpoint: "${trimmed}".`);
16191
16555
  continue;
16192
16556
  }
16193
- const meta = k === links.length - 1 ? lastSplit.meta : {};
16557
+ const isLast = k === links.length - 1;
16558
+ const meta = isLast ? lastSplit.meta : {};
16559
+ const style = links[k].style === "arc" ? "arc" : "straight";
16194
16560
  edges.push({
16195
16561
  from,
16196
16562
  to,
16197
16563
  ...links[k].label !== void 0 && { label: links[k].label },
16198
16564
  directed: links[k].directed,
16199
- style: links[k].style,
16565
+ style,
16200
16566
  meta,
16201
16567
  lineNumber: line12
16202
16568
  });
@@ -16282,22 +16648,19 @@ var init_parser12 = __esm({
16282
16648
  LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
16283
16649
  AT_RE = /(^|[\s,])at\s*:/i;
16284
16650
  DIRECTIVE_SET = /* @__PURE__ */ new Set([
16285
- "region",
16286
- "projection",
16287
16651
  "region-metric",
16288
16652
  "poi-metric",
16289
16653
  "flow-metric",
16290
- "scale",
16291
- "region-labels",
16292
- "poi-labels",
16293
- "default-country",
16294
- "default-state",
16654
+ "locale",
16295
16655
  "active-tag",
16656
+ "caption",
16296
16657
  "no-legend",
16297
- "no-insets",
16298
- "relief",
16299
- "subtitle",
16300
- "caption"
16658
+ "no-coastline",
16659
+ "no-relief",
16660
+ "no-context-labels",
16661
+ "no-region-labels",
16662
+ "no-poi-labels",
16663
+ "no-colorize"
16301
16664
  ]);
16302
16665
  }
16303
16666
  });
@@ -16475,6 +16838,21 @@ function parseBoxesAndLines(content) {
16475
16838
  }
16476
16839
  continue;
16477
16840
  }
16841
+ if (!contentStarted) {
16842
+ const metricMatch = trimmed.match(/^box-metric\s+(.+)$/i);
16843
+ if (metricMatch) {
16844
+ const { label, colorName } = peelTrailingColorName(
16845
+ metricMatch[1].trim()
16846
+ );
16847
+ result.boxMetric = label;
16848
+ if (colorName !== void 0) result.boxMetricColor = colorName;
16849
+ continue;
16850
+ }
16851
+ if (/^show-values$/i.test(trimmed)) {
16852
+ result.showValues = true;
16853
+ continue;
16854
+ }
16855
+ }
16478
16856
  if (!contentStarted) {
16479
16857
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
16480
16858
  if (optMatch) {
@@ -16853,6 +17231,19 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
16853
17231
  description = [metadata["description"]];
16854
17232
  delete metadata["description"];
16855
17233
  }
17234
+ let value;
17235
+ if (metadata["value"] !== void 0) {
17236
+ const raw = metadata["value"];
17237
+ const num = Number(raw);
17238
+ if (Number.isFinite(num)) {
17239
+ value = num;
17240
+ } else {
17241
+ diagnostics.push(
17242
+ makeDgmoError(lineNum, `value must be a number (got "${raw}")`, "error")
17243
+ );
17244
+ }
17245
+ delete metadata["value"];
17246
+ }
16856
17247
  if (split.alias) {
16857
17248
  nameAliasMap?.set(normalizeName(split.alias), label);
16858
17249
  }
@@ -16861,7 +17252,8 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
16861
17252
  label,
16862
17253
  lineNumber: lineNum,
16863
17254
  metadata,
16864
- ...description !== void 0 && { description }
17255
+ ...description !== void 0 && { description },
17256
+ ...value !== void 0 && { value }
16865
17257
  };
16866
17258
  }
16867
17259
  function splitTargetAndMeta(rest, metaAliasMap) {
@@ -24219,8 +24611,8 @@ function renderKanban(container, parsed, palette, isDark, options) {
24219
24611
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24220
24612
  for (const meta of tagMeta) {
24221
24613
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(`${meta.label}: `);
24222
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24223
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24614
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24615
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24224
24616
  metaY += sCardMetaLineHeight;
24225
24617
  }
24226
24618
  for (const detail of card.details) {
@@ -24564,8 +24956,8 @@ function renderSwimlaneCard(parent, cardLayout, tagGroups, activeTagGroup, palet
24564
24956
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24565
24957
  for (const meta of tagMeta) {
24566
24958
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", palette.textMuted).text(`${meta.label}: `);
24567
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24568
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24959
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24960
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24569
24961
  metaY += sCardMetaLineHeight;
24570
24962
  }
24571
24963
  for (const detail of card.details) {
@@ -25399,8 +25791,8 @@ function classifyEREntities(tables, relationships) {
25399
25791
  }
25400
25792
  }
25401
25793
  const mmParticipants = /* @__PURE__ */ new Set();
25402
- for (const [id, neighbors] of tableStarNeighbors) {
25403
- if (neighbors.size >= 2) mmParticipants.add(id);
25794
+ for (const [id, neighbors2] of tableStarNeighbors) {
25795
+ if (neighbors2.size >= 2) mmParticipants.add(id);
25404
25796
  }
25405
25797
  const indegreeValues = Object.values(indegreeMap);
25406
25798
  const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
@@ -25983,7 +26375,18 @@ function fitLabelToHeader(label, nodeWidth, maxLines) {
25983
26375
  const truncated = label.length > maxChars ? label.slice(0, maxChars - 1) + "\u2026" : label;
25984
26376
  return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
25985
26377
  }
25986
- function nodeColors(node, tagGroups, activeGroupName, palette, isDark, solid) {
26378
+ function nodeColors(node, tagGroups, activeGroupName, palette, isDark, value, solid) {
26379
+ const neutralFill = mix(palette.bg, palette.text, isDark ? 90 : 95);
26380
+ if (value.active) {
26381
+ const fill3 = node.value !== void 0 ? value.fillForValue(node.value) : neutralFill;
26382
+ const stroke3 = value.hue;
26383
+ const text2 = contrastText(
26384
+ fill3,
26385
+ palette.textOnFillLight,
26386
+ palette.textOnFillDark
26387
+ );
26388
+ return { fill: fill3, stroke: stroke3, text: text2 };
26389
+ }
25987
26390
  const tagColor = resolveTagColor(
25988
26391
  node.metadata,
25989
26392
  [...tagGroups],
@@ -26073,7 +26476,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26073
26476
  controlsExpanded,
26074
26477
  onToggleDescriptions,
26075
26478
  onToggleControlsExpand,
26076
- exportMode = false
26479
+ exportMode = false,
26480
+ controlsHost
26077
26481
  } = options ?? {};
26078
26482
  d3Selection6.select(container).selectAll(":not([data-d3-tooltip])").remove();
26079
26483
  const width = exportDims?.width ?? container.clientWidth;
@@ -26091,21 +26495,65 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26091
26495
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26092
26496
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26093
26497
  const sTitleY = sctx.structural(TITLE_Y);
26094
- const sLegendHeight = sctx.structural(
26498
+ const nodeValues = parsed.nodes.filter((n) => n.value !== void 0).map((n) => n.value);
26499
+ const hasRamp = nodeValues.length > 0;
26500
+ const allNonNegative = hasRamp && nodeValues.every((v) => v >= 0);
26501
+ const rampMin = allNonNegative ? 0 : Math.min(...nodeValues);
26502
+ const rampMax = Math.max(...nodeValues);
26503
+ const rampHue = resolveColor(parsed.boxMetricColor ?? "", palette) ?? palette.primary;
26504
+ const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
26505
+ const fillForValue = (v) => {
26506
+ const t = rampMax > rampMin ? (v - rampMin) / (rampMax - rampMin) : 1;
26507
+ const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
26508
+ return mix(rampHue, rampBase, pct);
26509
+ };
26510
+ const VALUE_NAME = hasRamp ? parsed.boxMetric?.trim() || "Value" : null;
26511
+ const matchColorGroup = (v) => {
26512
+ const lv = v.trim().toLowerCase();
26513
+ if (lv === "none") return null;
26514
+ const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
26515
+ if (tg) return tg.name;
26516
+ if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
26517
+ return v;
26518
+ };
26519
+ const override = activeTagGroup;
26520
+ let activeGroup;
26521
+ if (override !== void 0) {
26522
+ activeGroup = override === null ? null : matchColorGroup(override);
26523
+ } else if (parsed.options["active-tag"] !== void 0) {
26524
+ activeGroup = matchColorGroup(parsed.options["active-tag"]);
26525
+ } else {
26526
+ activeGroup = VALUE_NAME ?? (parsed.tagGroups.length > 0 ? parsed.tagGroups[0].name : null);
26527
+ }
26528
+ const activeIsValue = VALUE_NAME !== null && activeGroup === VALUE_NAME;
26529
+ const valueGroup = VALUE_NAME !== null ? {
26530
+ name: VALUE_NAME,
26531
+ entries: [],
26532
+ gradient: {
26533
+ min: rampMin,
26534
+ max: rampMax,
26535
+ hue: rampHue,
26536
+ base: rampBase
26537
+ }
26538
+ } : null;
26539
+ const legendGroups = [
26540
+ ...valueGroup ? [valueGroup] : [],
26541
+ ...parsed.tagGroups
26542
+ ];
26543
+ const reserveHasDescriptions = parsed.nodes.some(
26544
+ (n) => n.description && n.description.length > 0
26545
+ );
26546
+ const willRenderLegend = legendGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26547
+ const sLegendHeight = willRenderLegend ? sctx.structural(
26095
26548
  getMaxLegendReservedHeight(
26096
26549
  {
26097
- groups: parsed.tagGroups,
26550
+ groups: legendGroups,
26098
26551
  position: { placement: "top-center", titleRelation: "below-title" },
26099
26552
  mode: exportMode ? "export" : "preview"
26100
26553
  },
26101
26554
  width
26102
26555
  )
26103
- );
26104
- const activeGroup = resolveActiveTagGroup(
26105
- parsed.tagGroups,
26106
- parsed.options["active-tag"],
26107
- activeTagGroup
26108
- );
26556
+ ) : 0;
26109
26557
  const hidden = hiddenTagValues ?? parsed.initialHiddenTagValues;
26110
26558
  const nodeMap = /* @__PURE__ */ new Map();
26111
26559
  for (const node of parsed.nodes) nodeMap.set(node.label, node);
@@ -26116,7 +26564,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26116
26564
  const hasAnyDescriptions = parsed.nodes.some(
26117
26565
  (n) => n.description && n.description.length > 0
26118
26566
  );
26119
- const needsLegend = parsed.tagGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26567
+ const needsLegend = legendGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26120
26568
  const legendH = needsLegend ? sLegendHeight + 8 : 0;
26121
26569
  const groupLabelsSet = new Set(layout.groups.map((g) => g.label));
26122
26570
  let labelZoneExtension = 0;
@@ -26322,12 +26770,16 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26322
26770
  activeGroup,
26323
26771
  palette,
26324
26772
  isDark,
26773
+ { active: activeIsValue, hue: rampHue, fillForValue },
26325
26774
  parsed.options["solid-fill"] === "on"
26326
26775
  );
26327
26776
  const nodeG = diagramG.append("g").attr("class", "bl-node").attr("transform", `translate(${ln.x},${ln.y})`).attr("data-line-number", node.lineNumber).attr("data-node-id", node.label).style("cursor", onClickItem ? "pointer" : "default").style("--bl-node-stroke", colors.stroke);
26328
26777
  for (const [key, val] of Object.entries(node.metadata)) {
26329
26778
  nodeG.attr(`data-tag-${key.toLowerCase()}`, val.toLowerCase());
26330
26779
  }
26780
+ if (node.value !== void 0) {
26781
+ nodeG.attr("data-value", node.value);
26782
+ }
26331
26783
  if (onClickItem) {
26332
26784
  nodeG.on("click", (event) => {
26333
26785
  const target = event.target;
@@ -26411,14 +26863,30 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26411
26863
  nodeG.append("text").attr("x", 0).attr("y", -totalH / 2 + lineH / 2 + li * lineH).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", fitted.fontSize).attr("font-weight", "600").attr("fill", colors.text).text(fitted.lines[li]);
26412
26864
  }
26413
26865
  }
26866
+ if (parsed.showValues && node.value !== void 0) {
26867
+ const valueText = String(node.value);
26868
+ const descShown = !!(desc && desc.length > 0 && !hideDescriptions);
26869
+ if (descShown) {
26870
+ const padX = 6;
26871
+ const padY = 5;
26872
+ const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
26873
+ const bh = VALUE_FONT_SIZE + 4;
26874
+ const bx = ln.width / 2 - bw - 4;
26875
+ const by = -ln.height / 2 + 4;
26876
+ nodeG.append("rect").attr("x", bx).attr("y", by).attr("width", bw).attr("height", bh).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.85);
26877
+ nodeG.append("text").attr("class", "bl-node-value").attr("x", bx + bw - padX).attr("y", by + padY).attr("text-anchor", "end").attr("dominant-baseline", "central").attr("font-size", VALUE_FONT_SIZE).attr("font-weight", "600").attr("fill", palette.textMuted).text(valueText);
26878
+ } else {
26879
+ nodeG.append("text").attr("class", "bl-node-value").attr("x", 0).attr("y", ln.height / 2 - VALUE_FONT_SIZE).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", VALUE_FONT_SIZE).attr("font-weight", "600").attr("fill", colors.text).attr("opacity", 0.8).text(valueText);
26880
+ }
26881
+ }
26414
26882
  }
26415
26883
  const hasDescriptions = parsed.nodes.some(
26416
26884
  (n) => n.description && n.description.length > 0
26417
26885
  );
26418
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions;
26886
+ const hasLegend = legendGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26419
26887
  if (hasLegend) {
26420
26888
  let controlsGroup;
26421
- if (hasDescriptions && onToggleDescriptions) {
26889
+ if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
26422
26890
  controlsGroup = {
26423
26891
  toggles: [
26424
26892
  {
@@ -26433,10 +26901,17 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26433
26901
  };
26434
26902
  }
26435
26903
  const legendConfig = {
26436
- groups: parsed.tagGroups,
26904
+ groups: legendGroups,
26437
26905
  position: { placement: "top-center", titleRelation: "below-title" },
26438
26906
  mode: exportMode ? "export" : "preview",
26439
- ...controlsGroup !== void 0 && { controlsGroup }
26907
+ // Keep inactive sibling tag groups visible as collapsed pills so the user
26908
+ // can click one to flip the active colouring dimension (preview only —
26909
+ // export shows just the active group). Without this, declaring a second
26910
+ // tag group (e.g. Team) leaves it invisible whenever another group is
26911
+ // active. The app's BoxesAndLinesPreview already wires pill clicks.
26912
+ showInactivePills: true,
26913
+ ...controlsGroup !== void 0 && { controlsGroup },
26914
+ ...controlsHost !== void 0 && { controlsHost }
26440
26915
  };
26441
26916
  const legendState = {
26442
26917
  activeGroup,
@@ -26481,7 +26956,7 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
26481
26956
  }
26482
26957
  });
26483
26958
  }
26484
- var DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, DESC_FONT_SIZE, DESC_LINE_HEIGHT, MAX_DESC_LINES, CHAR_WIDTH_RATIO2, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, GROUP_LABEL_ZONE, lineGeneratorLR, lineGeneratorTB;
26959
+ var DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, DESC_FONT_SIZE, DESC_LINE_HEIGHT, MAX_DESC_LINES, CHAR_WIDTH_RATIO2, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, GROUP_LABEL_ZONE, RAMP_FLOOR, VALUE_FONT_SIZE, lineGeneratorLR, lineGeneratorTB;
26485
26960
  var init_renderer6 = __esm({
26486
26961
  "src/boxes-and-lines/renderer.ts"() {
26487
26962
  "use strict";
@@ -26490,6 +26965,7 @@ var init_renderer6 = __esm({
26490
26965
  init_legend_layout();
26491
26966
  init_title_constants();
26492
26967
  init_color_utils();
26968
+ init_colors();
26493
26969
  init_tag_groups();
26494
26970
  init_inline_markdown();
26495
26971
  init_wrapped_desc();
@@ -26512,6 +26988,8 @@ var init_renderer6 = __esm({
26512
26988
  GROUP_RX = 8;
26513
26989
  GROUP_LABEL_FONT_SIZE = 14;
26514
26990
  GROUP_LABEL_ZONE = 32;
26991
+ RAMP_FLOOR = 15;
26992
+ VALUE_FONT_SIZE = 11;
26515
26993
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
26516
26994
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
26517
26995
  }
@@ -27683,8 +28161,9 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27683
28161
  const containerHeight = exportDims?.height ?? (container.getBoundingClientRect().height || 600);
27684
28162
  d3Selection7.select(container).selectAll("*").remove();
27685
28163
  const svg = d3Selection7.select(container).append("svg").attr("width", containerWidth).attr("height", containerHeight).attr("viewBox", `0 0 ${containerWidth} ${containerHeight}`).attr("preserveAspectRatio", "xMidYMin meet").style("font-family", FONT_FAMILY);
28164
+ const appHosted = options?.controlsHost === "app";
27686
28165
  const hasControls = !!options?.onToggleColorByDepth || !!options?.onToggleDescriptions;
27687
- const hasLegend = parsed.tagGroups.length > 0 || hasControls;
28166
+ const hasLegend = parsed.tagGroups.length > 0 || hasControls && !appHosted;
27688
28167
  const fixedLegend = !isExport && hasLegend;
27689
28168
  const legendReserve = fixedLegend ? getMaxLegendReservedHeight(
27690
28169
  {
@@ -27778,7 +28257,10 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27778
28257
  }),
27779
28258
  position: { placement: "top-center", titleRelation: "below-title" },
27780
28259
  mode: options?.exportMode ? "export" : "preview",
27781
- ...controlsToggles !== void 0 && { controlsGroup: controlsToggles }
28260
+ ...controlsToggles !== void 0 && { controlsGroup: controlsToggles },
28261
+ ...options?.controlsHost !== void 0 && {
28262
+ controlsHost: options.controlsHost
28263
+ }
27782
28264
  };
27783
28265
  const legendState = {
27784
28266
  activeGroup: options?.colorByDepth ? null : activeTagGroup !== void 0 ? activeTagGroup : parsed.options["active-tag"] ?? null,
@@ -28221,8 +28703,8 @@ function computeFieldAlignX(children) {
28221
28703
  for (const child of children) {
28222
28704
  if (child.metadata["_labelField"] === "true" && child.children.length >= 2) {
28223
28705
  const labelEl = child.children[0];
28224
- const labelWidth = labelEl.label.length * CHAR_WIDTH5;
28225
- maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
28706
+ const labelWidth2 = labelEl.label.length * CHAR_WIDTH5;
28707
+ maxLabelWidth = Math.max(maxLabelWidth, labelWidth2);
28226
28708
  labelFieldCount++;
28227
28709
  }
28228
28710
  }
@@ -33187,7 +33669,7 @@ function hasRoles(node) {
33187
33669
  function computeNodeWidth2(node, expanded, options) {
33188
33670
  const badgeVal = node.computedConcurrentInvocations === 0 && node.computedInstances > 1 ? node.computedInstances : 0;
33189
33671
  const badgeLen = badgeVal > 0 ? `${badgeVal}x`.length + 2 : 0;
33190
- const labelWidth = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33672
+ const labelWidth2 = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33191
33673
  const allKeys = [];
33192
33674
  if (node.computedRps > 0) allKeys.push("RPS");
33193
33675
  if (expanded) {
@@ -33231,7 +33713,7 @@ function computeNodeWidth2(node, expanded, options) {
33231
33713
  allKeys.push("overflow");
33232
33714
  }
33233
33715
  }
33234
- if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth);
33716
+ if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth2);
33235
33717
  const maxKeyLen = Math.max(...allKeys.map((k) => k.length));
33236
33718
  let maxRowWidth = 0;
33237
33719
  if (node.computedRps > 0) {
@@ -33319,7 +33801,7 @@ function computeNodeWidth2(node, expanded, options) {
33319
33801
  truncated.length * META_CHAR_WIDTH3 + PADDING_X3
33320
33802
  );
33321
33803
  }
33322
- return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20, descWidth);
33804
+ return Math.max(MIN_NODE_WIDTH2, labelWidth2, maxRowWidth + 20, descWidth);
33323
33805
  }
33324
33806
  function computeNodeHeight2(node, expanded, options) {
33325
33807
  const propCount = countDisplayProps(node, expanded, options);
@@ -34869,8 +35351,9 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
34869
35351
  }
34870
35352
  return groups;
34871
35353
  }
34872
- function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false) {
35354
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false, controlsHost) {
34873
35355
  if (legendGroups.length === 0 && !playback) return;
35356
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34874
35357
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
34875
35358
  if (activeGroup) {
34876
35359
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
@@ -34879,14 +35362,29 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34879
35362
  name: g.name,
34880
35363
  entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
34881
35364
  }));
34882
- if (playback) {
35365
+ if (playback && !appHostedPlayback) {
34883
35366
  allGroups.push({ name: "Playback", entries: [] });
34884
35367
  }
34885
35368
  const legendConfig = {
34886
35369
  groups: allGroups,
34887
35370
  position: { placement: "top-center", titleRelation: "below-title" },
34888
35371
  mode: exportMode ? "export" : "preview",
34889
- showEmptyGroups: true
35372
+ showEmptyGroups: true,
35373
+ ...appHostedPlayback && {
35374
+ controlsHost: "app",
35375
+ controlsGroup: {
35376
+ toggles: [
35377
+ {
35378
+ id: "playback",
35379
+ type: "toggle",
35380
+ label: "Playback",
35381
+ active: true,
35382
+ onToggle: () => {
35383
+ }
35384
+ }
35385
+ ]
35386
+ }
35387
+ }
34890
35388
  };
34891
35389
  const legendState = { activeGroup };
34892
35390
  renderLegendD3(
@@ -34937,8 +35435,9 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34937
35435
  }
34938
35436
  }
34939
35437
  }
34940
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
35438
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes, controlsHost) {
34941
35439
  d3Selection11.select(container).selectAll(":not([data-d3-tooltip])").remove();
35440
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34942
35441
  const ctx = ScaleContext.identity();
34943
35442
  const sc = buildScaledConstants(ctx);
34944
35443
  const legendGroups = computeInfraLegendGroups(
@@ -34947,7 +35446,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
34947
35446
  palette,
34948
35447
  layout.edges
34949
35448
  );
34950
- const hasLegend = legendGroups.length > 0 || !!playback;
35449
+ const hasLegend = legendGroups.length > 0 || !!playback && !appHostedPlayback;
34951
35450
  const fixedLegend = !exportMode && hasLegend;
34952
35451
  const legendDynamicH = hasLegend ? getMaxLegendReservedHeight(
34953
35452
  {
@@ -35091,7 +35590,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35091
35590
  isDark,
35092
35591
  activeGroup ?? null,
35093
35592
  playback ?? void 0,
35094
- exportMode
35593
+ exportMode,
35594
+ controlsHost
35095
35595
  );
35096
35596
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
35097
35597
  } else {
@@ -35104,7 +35604,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35104
35604
  isDark,
35105
35605
  activeGroup ?? null,
35106
35606
  playback ?? void 0,
35107
- exportMode
35607
+ exportMode,
35608
+ controlsHost
35108
35609
  );
35109
35610
  }
35110
35611
  }
@@ -42738,6 +43239,9 @@ function renderTechRadar(container, parsed, palette, isDark, onClickItem, export
42738
43239
  onToggle: (active) => options.onToggleListing(active)
42739
43240
  }
42740
43241
  ]
43242
+ },
43243
+ ...options.controlsHost !== void 0 && {
43244
+ controlsHost: options.controlsHost
42741
43245
  }
42742
43246
  };
42743
43247
  const legendState = {
@@ -44560,7 +45064,7 @@ function computeCycleLayout(parsed, options) {
44560
45064
  const circleNodes = parsed.options["circle-nodes"] === "true";
44561
45065
  const nodeDims = parsed.nodes.map((node) => {
44562
45066
  const hasDesc = !hideDescriptions && node.description.length > 0;
44563
- const labelWidth = Math.max(
45067
+ const labelWidth2 = Math.max(
44564
45068
  MIN_NODE_WIDTH4,
44565
45069
  node.label.length * LABEL_CHAR_W + NODE_PAD_X * 2
44566
45070
  );
@@ -44569,12 +45073,12 @@ function computeCycleLayout(parsed, options) {
44569
45073
  }
44570
45074
  if (!hasDesc) {
44571
45075
  return {
44572
- width: Math.min(MAX_NODE_WIDTH3, labelWidth),
45076
+ width: Math.min(MAX_NODE_WIDTH3, labelWidth2),
44573
45077
  height: PLAIN_NODE_HEIGHT,
44574
45078
  wrappedDesc: []
44575
45079
  };
44576
45080
  }
44577
- return chooseDescribedRectDims(node.description, labelWidth);
45081
+ return chooseDescribedRectDims(node.description, labelWidth2);
44578
45082
  });
44579
45083
  if (circleNodes) {
44580
45084
  const maxDiam = Math.max(...nodeDims.map((d) => d.width));
@@ -44770,10 +45274,10 @@ function computeCycleLayout(parsed, options) {
44770
45274
  scale
44771
45275
  };
44772
45276
  }
44773
- function chooseDescribedRectDims(description, labelWidth) {
45277
+ function chooseDescribedRectDims(description, labelWidth2) {
44774
45278
  const minW = Math.min(
44775
45279
  MAX_NODE_WIDTH3,
44776
- Math.max(MIN_NODE_WIDTH4, labelWidth, DESC_MIN_WIDTH)
45280
+ Math.max(MIN_NODE_WIDTH4, labelWidth2, DESC_MIN_WIDTH)
44777
45281
  );
44778
45282
  let best = null;
44779
45283
  let bestScore = Infinity;
@@ -45202,7 +45706,8 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45202
45706
  const hideDescriptions = (renderOptions?.hideDescriptions ?? false) || parsed.options["no-descriptions"] === "true" || viewState?.hd === true;
45203
45707
  const showDescriptions = !hideDescriptions;
45204
45708
  const hasDescriptions = parsed.nodes.some((n) => n.description.length > 0) || parsed.edges.some((e) => e.description.length > 0);
45205
- const hasLegend = hasDescriptions && !!renderOptions?.onToggleDescriptions;
45709
+ const appHostedControls = renderOptions?.controlsHost === "app";
45710
+ const hasLegend = !appHostedControls && hasDescriptions && !!renderOptions?.onToggleDescriptions;
45206
45711
  const showTitle = !!parsed.title && parsed.options["no-title"] !== "on";
45207
45712
  const legendOffset = hasLegend ? sLegendHeight : 0;
45208
45713
  const layoutHeight = height - (showTitle ? sTitleAreaHeight : 0) - legendOffset;
@@ -45239,7 +45744,10 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45239
45744
  groups: [],
45240
45745
  position: { placement: "top-center", titleRelation: "below-title" },
45241
45746
  mode: renderOptions?.exportMode ? "export" : "preview",
45242
- controlsGroup
45747
+ controlsGroup,
45748
+ ...renderOptions?.controlsHost !== void 0 && {
45749
+ controlsHost: renderOptions.controlsHost
45750
+ }
45243
45751
  };
45244
45752
  const legendState = {
45245
45753
  activeGroup: null,
@@ -45493,7 +46001,7 @@ var init_renderer15 = __esm({
45493
46001
  });
45494
46002
 
45495
46003
  // src/map/geo.ts
45496
- import { feature } from "topojson-client";
46004
+ import { feature, neighbors } from "topojson-client";
45497
46005
  import { geoBounds, geoArea } from "d3-geo";
45498
46006
  function geomObject(topo) {
45499
46007
  const key = Object.keys(topo.objects)[0];
@@ -45511,6 +46019,107 @@ function featureIndex(topo) {
45511
46019
  }
45512
46020
  return idx;
45513
46021
  }
46022
+ function buildAdjacency(topo) {
46023
+ const cached = adjacencyCache.get(topo);
46024
+ if (cached) return cached;
46025
+ const geometries = geomObject(topo).geometries;
46026
+ const nb = neighbors(geometries);
46027
+ const sets = /* @__PURE__ */ new Map();
46028
+ geometries.forEach((g, i) => {
46029
+ if (!g.type || g.type === "null") return;
46030
+ let set = sets.get(g.id);
46031
+ if (!set) {
46032
+ set = /* @__PURE__ */ new Set();
46033
+ sets.set(g.id, set);
46034
+ }
46035
+ for (const j of nb[i] ?? []) {
46036
+ const nid = geometries[j]?.id;
46037
+ if (nid && nid !== g.id) set.add(nid);
46038
+ }
46039
+ });
46040
+ const out = /* @__PURE__ */ new Map();
46041
+ for (const [iso, set] of sets) out.set(iso, [...set].sort());
46042
+ adjacencyCache.set(topo, out);
46043
+ return out;
46044
+ }
46045
+ function decodeFeatures(topo) {
46046
+ return geomObject(topo).geometries.map((g) => {
46047
+ const f = feature(topo, g);
46048
+ return {
46049
+ type: "Feature",
46050
+ id: g.id,
46051
+ properties: g.properties,
46052
+ geometry: f.geometry
46053
+ };
46054
+ });
46055
+ }
46056
+ function pointInRing(lon, lat, ring) {
46057
+ let inside = false;
46058
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46059
+ const xi = ring[i][0];
46060
+ const yi = ring[i][1];
46061
+ const xj = ring[j][0];
46062
+ const yj = ring[j][1];
46063
+ const intersect = yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi;
46064
+ if (intersect) inside = !inside;
46065
+ }
46066
+ return inside;
46067
+ }
46068
+ function pointOnRingEdge(lon, lat, ring) {
46069
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46070
+ const xi = ring[i][0];
46071
+ const yi = ring[i][1];
46072
+ const xj = ring[j][0];
46073
+ const yj = ring[j][1];
46074
+ if (lon < Math.min(xi, xj) - EDGE_EPS || lon > Math.max(xi, xj) + EDGE_EPS)
46075
+ continue;
46076
+ if (lat < Math.min(yi, yj) - EDGE_EPS || lat > Math.max(yi, yj) + EDGE_EPS)
46077
+ continue;
46078
+ const cross = (xj - xi) * (lat - yi) - (yj - yi) * (lon - xi);
46079
+ if (Math.abs(cross) <= EDGE_EPS) return true;
46080
+ }
46081
+ return false;
46082
+ }
46083
+ function pointInGeometry(geometry, lon, lat) {
46084
+ const g = geometry;
46085
+ if (!g) return false;
46086
+ const polys = g.type === "Polygon" ? [g.coordinates] : g.type === "MultiPolygon" ? g.coordinates : [];
46087
+ for (const rings of polys) {
46088
+ if (!rings.length) continue;
46089
+ if (pointOnRingEdge(lon, lat, rings[0])) return true;
46090
+ if (!pointInRing(lon, lat, rings[0])) continue;
46091
+ let inHole = false;
46092
+ for (let h = 1; h < rings.length; h++) {
46093
+ if (pointInRing(lon, lat, rings[h]) && !pointOnRingEdge(lon, lat, rings[h])) {
46094
+ inHole = true;
46095
+ break;
46096
+ }
46097
+ }
46098
+ if (!inHole) return true;
46099
+ }
46100
+ return false;
46101
+ }
46102
+ function regionAt(lonLat, countries, states) {
46103
+ const lon = lonLat[0];
46104
+ const lat = lonLat[1];
46105
+ let country = null;
46106
+ for (const f of countries) {
46107
+ if (pointInGeometry(f.geometry, lon, lat)) {
46108
+ country = { iso: f.id, name: f.properties.name };
46109
+ break;
46110
+ }
46111
+ }
46112
+ let state = null;
46113
+ if (country?.iso === "US" && states) {
46114
+ for (const f of states) {
46115
+ if (pointInGeometry(f.geometry, lon, lat)) {
46116
+ state = { iso: f.id, name: f.properties.name };
46117
+ break;
46118
+ }
46119
+ }
46120
+ }
46121
+ return { country, state };
46122
+ }
45514
46123
  function featureBbox(topo, geomId) {
45515
46124
  const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45516
46125
  if (!geom) return null;
@@ -45628,11 +46237,13 @@ function unionLongitudes(lons) {
45628
46237
  }
45629
46238
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45630
46239
  }
45631
- var fold, DETACH_GAP_DEG, DETACH_AREA_FRAC;
46240
+ var fold, adjacencyCache, EDGE_EPS, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45632
46241
  var init_geo = __esm({
45633
46242
  "src/map/geo.ts"() {
45634
46243
  "use strict";
45635
46244
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
46245
+ adjacencyCache = /* @__PURE__ */ new WeakMap();
46246
+ EDGE_EPS = 1e-9;
45636
46247
  DETACH_GAP_DEG = 10;
45637
46248
  DETACH_AREA_FRAC = 0.25;
45638
46249
  }
@@ -45652,6 +46263,12 @@ function looksUS(lat, lon) {
45652
46263
  if (lat < 15 || lat > 72) return false;
45653
46264
  return lon >= -180 && lon <= -64 || lon >= 172;
45654
46265
  }
46266
+ function looksNorthAmericaNeighbor(lat, lon) {
46267
+ return lat >= 14 && lat <= 72 && lon >= -141 && lon <= -52;
46268
+ }
46269
+ function isWholeSphere(bb) {
46270
+ return bb[0][0] <= -179 && bb[1][0] >= 179 && bb[0][1] <= -89 && bb[1][1] >= 89;
46271
+ }
45655
46272
  function resolveMap(parsed, data) {
45656
46273
  const diagnostics = [...parsed.diagnostics];
45657
46274
  const err = (line12, message, code) => {
@@ -45662,9 +46279,6 @@ function resolveMap(parsed, data) {
45662
46279
  };
45663
46280
  const result = {
45664
46281
  title: parsed.title,
45665
- ...parsed.directives.subtitle !== void 0 && {
45666
- subtitle: parsed.directives.subtitle
45667
- },
45668
46282
  ...parsed.directives.caption !== void 0 && {
45669
46283
  caption: parsed.directives.caption
45670
46284
  },
@@ -45674,7 +46288,7 @@ function resolveMap(parsed, data) {
45674
46288
  // renderer's job (step 4) — the resolver only carries `tags` + `tagGroups`
45675
46289
  // through; it never resolves a tag value to a palette color (#10).
45676
46290
  directives: { ...parsed.directives },
45677
- basemaps: { world: "coarse", subdivisions: [] },
46291
+ basemaps: { world: "detail", subdivisions: [] },
45678
46292
  regions: [],
45679
46293
  pois: [],
45680
46294
  edges: [],
@@ -45683,7 +46297,8 @@ function resolveMap(parsed, data) {
45683
46297
  [-180, -85],
45684
46298
  [180, 85]
45685
46299
  ],
45686
- projection: "natural-earth",
46300
+ projection: "equirectangular",
46301
+ poiFrameContainers: [],
45687
46302
  diagnostics,
45688
46303
  error: parsed.error
45689
46304
  };
@@ -45693,7 +46308,10 @@ function resolveMap(parsed, data) {
45693
46308
  ...[...countryIndex.values()].map((v) => v.name),
45694
46309
  ...[...usStateIndex.values()].map((v) => v.name)
45695
46310
  ];
45696
- const usScoped = parsed.directives.region === "us-states" || parsed.directives.defaultCountry?.toUpperCase() === "US" || parsed.regions.some((r) => {
46311
+ const localeRaw = parsed.directives.locale?.toUpperCase();
46312
+ const localeCountry = localeRaw ? localeRaw.split("-")[0] : void 0;
46313
+ const localeSubdivision = localeRaw && /^[A-Z]{2}-/.test(localeRaw) ? localeRaw : void 0;
46314
+ const usScoped = localeCountry === "US" || parsed.regions.some((r) => {
45697
46315
  const f = fold(r.name);
45698
46316
  return usStateIndex.has(f) && !countryIndex.has(f);
45699
46317
  }) || parsed.regions.some(
@@ -45844,7 +46462,7 @@ function resolveMap(parsed, data) {
45844
46462
  if (!scope)
45845
46463
  warn2(
45846
46464
  line12,
45847
- `"${name}" is ambiguous \u2014 resolved to the most-populous match.`,
46465
+ `"${name}" is ambiguous \u2014 resolved to the most-populous match. Set a default with \`locale <ISO>\` (e.g. \`locale US\` / \`locale US-GA\`) to steer it.`,
45848
46466
  "W_MAP_AMBIGUOUS_NAME"
45849
46467
  );
45850
46468
  }
@@ -45857,17 +46475,21 @@ function resolveMap(parsed, data) {
45857
46475
  return fold(pos.name);
45858
46476
  };
45859
46477
  const poiCountries = [];
45860
- let anyNonUsPoi = false;
46478
+ let anyUsPoi = false;
46479
+ let anyNonNaPoi = false;
45861
46480
  const noteCountry = (iso) => {
45862
46481
  if (iso) {
45863
46482
  poiCountries.push(iso);
45864
- if (iso !== "US") anyNonUsPoi = true;
46483
+ if (iso === "US") anyUsPoi = true;
46484
+ if (iso !== "US" && iso !== "CA" && iso !== "MX") anyNonNaPoi = true;
45865
46485
  }
45866
46486
  };
45867
46487
  const deferred = [];
45868
46488
  for (const p of parsed.pois) {
45869
46489
  if (p.pos.kind === "coords") {
45870
- if (!looksUS(p.pos.lat, p.pos.lon)) anyNonUsPoi = true;
46490
+ if (looksUS(p.pos.lat, p.pos.lon)) anyUsPoi = true;
46491
+ else if (!looksNorthAmericaNeighbor(p.pos.lat, p.pos.lon))
46492
+ anyNonNaPoi = true;
45871
46493
  addResolvedPoi(p.pos.lat, p.pos.lon, p);
45872
46494
  continue;
45873
46495
  }
@@ -45885,14 +46507,15 @@ function resolveMap(parsed, data) {
45885
46507
  deferred.push(p);
45886
46508
  }
45887
46509
  }
45888
- const inferredCountry = parsed.directives.defaultCountry?.toUpperCase() ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46510
+ const inferredCountry = localeCountry ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46511
+ const inferredScope = localeSubdivision ?? inferredCountry;
45889
46512
  for (const p of deferred) {
45890
46513
  if (p.pos.kind !== "name") continue;
45891
46514
  const got = lookupName(
45892
46515
  p.pos.name,
45893
46516
  p.pos.scope,
45894
46517
  p.lineNumber,
45895
- inferredCountry,
46518
+ inferredScope,
45896
46519
  true
45897
46520
  );
45898
46521
  if (got.kind === "ok") {
@@ -45962,7 +46585,8 @@ function resolveMap(parsed, data) {
45962
46585
  const meta = sizeValue !== void 0 ? { value: sizeValue } : {};
45963
46586
  if (pos.kind === "coords") {
45964
46587
  const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
45965
- if (!looksUS(pos.lat, pos.lon)) anyNonUsPoi = true;
46588
+ if (looksUS(pos.lat, pos.lon)) anyUsPoi = true;
46589
+ else if (!looksNorthAmericaNeighbor(pos.lat, pos.lon)) anyNonNaPoi = true;
45966
46590
  if (!registry.has(id)) {
45967
46591
  registerPoi(
45968
46592
  id,
@@ -45985,7 +46609,7 @@ function resolveMap(parsed, data) {
45985
46609
  if (registry.has(f)) return f;
45986
46610
  const aliased = declaredByName.get(f);
45987
46611
  if (aliased) return aliased;
45988
- const got = lookupName(pos.name, pos.scope, line12, inferredCountry, true);
46612
+ const got = lookupName(pos.name, pos.scope, line12, inferredScope, true);
45989
46613
  if (got.kind !== "ok") return null;
45990
46614
  noteCountry(got.iso);
45991
46615
  registerPoi(
@@ -46042,9 +46666,12 @@ function resolveMap(parsed, data) {
46042
46666
  }
46043
46667
  routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
46044
46668
  }
46669
+ const hasUsContent = usSubdivisionReferenced || anyUsPoi || localeCountry === "US";
46670
+ const usOriented = !anyNonNaPoi && !regions.some(
46671
+ (r) => r.layer === "country" && !["US", "CA", "MX"].includes(r.iso)
46672
+ ) && hasUsContent;
46045
46673
  const subdivisions = [];
46046
- if (usSubdivisionReferenced || parsed.directives.region === "us-states")
46047
- subdivisions.push("us-states");
46674
+ if (usSubdivisionReferenced || usOriented) subdivisions.push("us-states");
46048
46675
  const regionBoxes = [];
46049
46676
  for (const ref of referencedRegionIds) {
46050
46677
  const bb = featureBbox(data.usStates, ref.id);
@@ -46062,17 +46689,51 @@ function resolveMap(parsed, data) {
46062
46689
  [-180, -85],
46063
46690
  [180, 85]
46064
46691
  ];
46065
- let extent2 = unioned ? pad(unioned, PAD_FRACTION) : DEFAULT_EXTENT;
46692
+ const basePad = regions.length > 0 ? REGION_PAD_FRACTION : PAD_FRACTION;
46693
+ let extent2 = unioned ? pad(unioned, basePad) : DEFAULT_EXTENT;
46694
+ const isPoiOnly = pois.length > 0 && regions.length === 0;
46695
+ const containerRegionIds = [];
46696
+ if (isPoiOnly) {
46697
+ const countries = decodeFeatures(data.worldDetail);
46698
+ const states = decodeFeatures(data.usStates);
46699
+ const seen = /* @__PURE__ */ new Set();
46700
+ const containerBoxes = [];
46701
+ for (const p of pois) {
46702
+ const { country, state } = regionAt([p.lon, p.lat], countries, states);
46703
+ const id = state?.iso ?? country?.iso;
46704
+ if (!id || seen.has(id)) continue;
46705
+ seen.add(id);
46706
+ containerRegionIds.push(id);
46707
+ const bb = state ? featureBbox(data.usStates, id) : featureBboxPrimary(data.worldCoarse, id);
46708
+ if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46709
+ }
46710
+ const containerUnion = unionExtent(containerBoxes, points);
46711
+ if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46712
+ }
46713
+ if (isPoiOnly) {
46714
+ const cx = (extent2[0][0] + extent2[1][0]) / 2;
46715
+ const cy = (extent2[0][1] + extent2[1][1]) / 2;
46716
+ const lon = extent2[1][0] - extent2[0][0];
46717
+ const lat = extent2[1][1] - extent2[0][1];
46718
+ const longer = Math.max(lon, lat);
46719
+ if (longer > 0 && longer < POI_ZOOM_FLOOR_DEG) {
46720
+ const k = POI_ZOOM_FLOOR_DEG / longer;
46721
+ const halfLon = lon * k / 2;
46722
+ const halfLat = lat * k / 2;
46723
+ extent2 = [
46724
+ [cx - halfLon, cy - halfLat],
46725
+ [cx + halfLon, cy + halfLat]
46726
+ ];
46727
+ }
46728
+ }
46066
46729
  const lonSpan = extent2[1][0] - extent2[0][0];
46067
46730
  const latSpan = extent2[1][1] - extent2[0][1];
46068
46731
  const span = Math.max(lonSpan, latSpan);
46069
46732
  const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
46070
- const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46071
46733
  let projection;
46072
- const override = parsed.directives.projection;
46073
- if (override === "equirectangular" || override === "natural-earth" || override === "albers-usa" || override === "mercator") {
46074
- projection = override;
46075
- } else if (usDominant) {
46734
+ if (isPoiOnly && usOriented && lonSpan < US_NATIONAL_LON_SPAN) {
46735
+ projection = "mercator";
46736
+ } else if (usOriented) {
46076
46737
  projection = "albers-usa";
46077
46738
  } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
46078
46739
  projection = "equirectangular";
@@ -46090,11 +46751,20 @@ function resolveMap(parsed, data) {
46090
46751
  result.edges = edges;
46091
46752
  result.routes = routes;
46092
46753
  result.basemaps = {
46093
- world: span > WORLD_SPAN ? "coarse" : "detail",
46754
+ // Tier is intentionally pinned to detail (50m) at ALL scales. Diagrammo maps
46755
+ // are presentational (palette tints, relief hachures, POI hubs), not
46756
+ // survey-grade — recognizability > generalization: 110m coarse drops the
46757
+ // Italian boot to a stump at world scale. `WORLD_SPAN` lives on only for the
46758
+ // projection decision (the `usOriented`/`span > WORLD_SPAN` chain above); it
46759
+ // no longer gates basemap resolution.
46760
+ // `worldCoarse` is still loaded — it's the authoritative name/bbox index
46761
+ // (featureIndex, featureBboxPrimary), not dead code.
46762
+ world: "detail",
46094
46763
  subdivisions
46095
46764
  };
46096
46765
  result.extent = extent2;
46097
46766
  result.projection = projection;
46767
+ result.poiFrameContainers = containerRegionIds;
46098
46768
  result.error = parsed.error ?? firstError(diagnostics);
46099
46769
  return result;
46100
46770
  }
@@ -46131,7 +46801,7 @@ function firstError(diags) {
46131
46801
  const e = diags.find((d) => d.severity === "error");
46132
46802
  return e ? formatDgmoError(e) : null;
46133
46803
  }
46134
- var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46804
+ var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, REGION_PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, POI_ZOOM_FLOOR_DEG, US_NATIONAL_LON_SPAN, REGION_ALIASES, US_STATE_POSTAL;
46135
46805
  var init_resolver2 = __esm({
46136
46806
  "src/map/resolver.ts"() {
46137
46807
  "use strict";
@@ -46140,8 +46810,11 @@ var init_resolver2 = __esm({
46140
46810
  WORLD_SPAN = 90;
46141
46811
  MERCATOR_MAX_LAT = 80;
46142
46812
  PAD_FRACTION = 0.05;
46813
+ REGION_PAD_FRACTION = 0.12;
46143
46814
  WORLD_LAT_SOUTH = -58;
46144
46815
  WORLD_LAT_NORTH = 78;
46816
+ POI_ZOOM_FLOOR_DEG = 7;
46817
+ US_NATIONAL_LON_SPAN = 48;
46145
46818
  REGION_ALIASES = {
46146
46819
  // Common everyday names → the Natural-Earth display name actually shipped.
46147
46820
  "united states": "united states of america",
@@ -46219,10 +46892,277 @@ var init_resolver2 = __esm({
46219
46892
  }
46220
46893
  });
46221
46894
 
46895
+ // src/map/colorize.ts
46896
+ function assignColors(isos, adjacency) {
46897
+ const sorted = [...isos].sort();
46898
+ const byIso = /* @__PURE__ */ new Map();
46899
+ let maxIndex = -1;
46900
+ for (const iso of sorted) {
46901
+ const taken = /* @__PURE__ */ new Set();
46902
+ for (const n of adjacency.get(iso) ?? []) {
46903
+ const c = byIso.get(n);
46904
+ if (c !== void 0) taken.add(c);
46905
+ }
46906
+ let h = 0;
46907
+ while (taken.has(h)) h++;
46908
+ byIso.set(iso, h);
46909
+ if (h > maxIndex) maxIndex = h;
46910
+ }
46911
+ return { byIso, huesNeeded: maxIndex + 1 };
46912
+ }
46913
+ var init_colorize = __esm({
46914
+ "src/map/colorize.ts"() {
46915
+ "use strict";
46916
+ }
46917
+ });
46918
+
46919
+ // src/map/context-labels.ts
46920
+ function tierBand(maxSpanDeg) {
46921
+ if (maxSpanDeg >= 90) return "world";
46922
+ if (maxSpanDeg >= 20) return "continental";
46923
+ if (maxSpanDeg >= 5) return "regional";
46924
+ return "local";
46925
+ }
46926
+ function labelBudget(width, height, band) {
46927
+ const bandCap = {
46928
+ world: 6,
46929
+ continental: 5,
46930
+ regional: 4,
46931
+ local: 3
46932
+ };
46933
+ const area2 = Math.floor(Math.sqrt(Math.max(0, width * height)) / 150);
46934
+ return Math.max(0, Math.min(area2, bandCap[band]));
46935
+ }
46936
+ function waterEligible(tier, kind, band) {
46937
+ switch (band) {
46938
+ case "world":
46939
+ return tier <= 1 && (kind === "ocean" || kind === "sea");
46940
+ case "continental":
46941
+ return tier <= 2;
46942
+ case "regional":
46943
+ return tier <= 3;
46944
+ case "local":
46945
+ return tier <= 4;
46946
+ }
46947
+ }
46948
+ function insideViewport(p, width, height) {
46949
+ return !!p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && p[0] >= 0 && p[0] <= width && p[1] >= 0 && p[1] <= height;
46950
+ }
46951
+ function labelWidth(text, letterSpacing) {
46952
+ const spacing = letterSpacing > 0 ? Math.max(0, text.length - 1) * letterSpacing : 0;
46953
+ return measureLegendText(text, FONT) + spacing + 2 * PADX;
46954
+ }
46955
+ function wrapLabel2(text, letterSpacing) {
46956
+ const words = text.split(/\s+/).filter(Boolean);
46957
+ if (words.length <= 1) return [text];
46958
+ const maxLines = words.length >= 4 ? 3 : 2;
46959
+ const n = words.length;
46960
+ let best = null;
46961
+ for (let mask = 0; mask < 1 << n - 1; mask++) {
46962
+ const lines = [];
46963
+ let cur = [words[0]];
46964
+ for (let i = 1; i < n; i++) {
46965
+ if (mask & 1 << i - 1) {
46966
+ lines.push(cur.join(" "));
46967
+ cur = [words[i]];
46968
+ } else cur.push(words[i]);
46969
+ }
46970
+ lines.push(cur.join(" "));
46971
+ if (lines.length > maxLines) continue;
46972
+ const cost = Math.round(
46973
+ Math.max(...lines.map((l) => labelWidth(l, letterSpacing)))
46974
+ );
46975
+ const head = labelWidth(lines[0], letterSpacing);
46976
+ if (!best || cost < best.cost || cost === best.cost && lines.length < best.lines.length || cost === best.cost && lines.length === best.lines.length && head > best.head)
46977
+ best = { lines, cost, head };
46978
+ }
46979
+ return best?.lines ?? [text];
46980
+ }
46981
+ function rectAround(cx, cy, lines, letterSpacing) {
46982
+ const w = Math.max(...lines.map((l) => labelWidth(l, letterSpacing)));
46983
+ const h = (lines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY;
46984
+ return { x: cx - w / 2, y: cy - h / 2, w, h };
46985
+ }
46986
+ function rectFits(r, width, height) {
46987
+ return r.x >= 0 && r.y >= 0 && r.x + r.w <= width && r.y + r.h <= height;
46988
+ }
46989
+ function overlapsPadded(a, b, pad2) {
46990
+ return a.x - pad2 < b.x + b.w && a.x + a.w + pad2 > b.x && a.y - pad2 < b.y + b.h && a.y + a.h + pad2 > b.y;
46991
+ }
46992
+ function placeContextLabels(args) {
46993
+ const {
46994
+ projection,
46995
+ dLonSpan,
46996
+ dLatSpan,
46997
+ width,
46998
+ height,
46999
+ waterBodies,
47000
+ countries,
47001
+ palette,
47002
+ project,
47003
+ collides,
47004
+ overLand
47005
+ } = args;
47006
+ void projection;
47007
+ const band = tierBand(Math.max(dLonSpan, dLatSpan));
47008
+ const budget = labelBudget(width, height, band);
47009
+ if (budget <= 0) return [];
47010
+ const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
47011
+ const countryColor = palette.textMuted;
47012
+ const haloColor = palette.bg;
47013
+ const candidates = [];
47014
+ const center = [width / 2, height / 2];
47015
+ for (const e of waterBodies?.entries ?? []) {
47016
+ const [lat, lon, name, tier, kind, alt] = e;
47017
+ if (!waterEligible(tier, kind, band)) continue;
47018
+ const wlines = wrapLabel2(name, WATER_LETTER_SPACING);
47019
+ const anchorsLngLat = [[lon, lat]];
47020
+ for (const a of alt ?? []) anchorsLngLat.push([a[1], a[0]]);
47021
+ let best = null;
47022
+ let bestD = Infinity;
47023
+ let nearestProj = null;
47024
+ let nearestProjD = Infinity;
47025
+ for (const [aLon, aLat] of anchorsLngLat) {
47026
+ const p = project(aLon, aLat);
47027
+ if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
47028
+ const d = (p[0] - center[0]) ** 2 + (p[1] - center[1]) ** 2;
47029
+ if (d < nearestProjD) {
47030
+ nearestProjD = d;
47031
+ nearestProj = p;
47032
+ }
47033
+ if (!insideViewport(p, width, height)) continue;
47034
+ if (d < bestD) {
47035
+ bestD = d;
47036
+ best = p;
47037
+ }
47038
+ }
47039
+ if (!best && tier === 0 && nearestProj) {
47040
+ const overX = Math.max(0, -nearestProj[0], nearestProj[0] - width);
47041
+ const overY = Math.max(0, -nearestProj[1], nearestProj[1] - height);
47042
+ if (overX <= width * EDGE_CLAMP_OVERSHOOT && overY <= height * EDGE_CLAMP_OVERSHOOT) {
47043
+ const halfW = Math.max(...wlines.map((l) => labelWidth(l, WATER_LETTER_SPACING))) / 2;
47044
+ const halfH = ((wlines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY) / 2;
47045
+ const m = EDGE_CLAMP_MARGIN;
47046
+ best = [
47047
+ Math.min(Math.max(nearestProj[0], halfW + m), width - halfW - m),
47048
+ Math.min(Math.max(nearestProj[1], halfH + m), height - halfH - m)
47049
+ ];
47050
+ }
47051
+ }
47052
+ if (!best) continue;
47053
+ candidates.push({
47054
+ text: name,
47055
+ lines: wlines,
47056
+ cx: best[0],
47057
+ cy: best[1],
47058
+ italic: true,
47059
+ letterSpacing: WATER_LETTER_SPACING,
47060
+ color: waterColor,
47061
+ // Water before any country (×1000), then by tier, then kind, then name.
47062
+ sort: tier * 10 + KIND_ORDER[kind]
47063
+ });
47064
+ }
47065
+ const ranked = countries.map((c) => {
47066
+ const [x0, y0, x1, y1] = c.bbox;
47067
+ const w = x1 - x0;
47068
+ const h = y1 - y0;
47069
+ return { c, w, h, area: w * h };
47070
+ }).filter((r) => Number.isFinite(r.area) && r.area > 0).sort((a, b) => b.area - a.area);
47071
+ let ci = 0;
47072
+ for (const r of ranked) {
47073
+ const { c, w, h } = r;
47074
+ if (w > width * 0.66 || h > height * 0.66) continue;
47075
+ if (!insideViewport(c.anchor, width, height)) continue;
47076
+ const text = c.name;
47077
+ const tw = labelWidth(text, 0);
47078
+ if (tw > w || FONT + 2 * PADY > h) continue;
47079
+ candidates.push({
47080
+ text,
47081
+ lines: [text],
47082
+ cx: c.anchor[0],
47083
+ cy: c.anchor[1],
47084
+ italic: false,
47085
+ letterSpacing: 0,
47086
+ color: countryColor,
47087
+ // Always after every water body (+1e6); larger area = earlier.
47088
+ sort: 1e6 + ci++
47089
+ });
47090
+ }
47091
+ candidates.sort((a, b) => a.sort - b.sort);
47092
+ const placed = [];
47093
+ const placedRects = [];
47094
+ for (const cand of candidates) {
47095
+ if (placed.length >= budget) break;
47096
+ const rect = rectAround(cand.cx, cand.cy, cand.lines, cand.letterSpacing);
47097
+ if (!rectFits(rect, width, height)) continue;
47098
+ if (cand.italic && overLand) {
47099
+ const inset = 2;
47100
+ const top = cand.cy - (cand.lines.length - 1) / 2 * LINE_HEIGHT;
47101
+ const touchesLand = cand.lines.some((line12, li) => {
47102
+ const lw = labelWidth(line12, cand.letterSpacing);
47103
+ const x0 = cand.cx - lw / 2 + inset;
47104
+ const x1 = cand.cx + lw / 2 - inset;
47105
+ const xs = [x0, (x0 + cand.cx) / 2, cand.cx, (cand.cx + x1) / 2, x1];
47106
+ const base = top + li * LINE_HEIGHT;
47107
+ return [base, base - FONT * 0.4, base - FONT * 0.8].some(
47108
+ (y) => xs.some((x) => overLand(x, y))
47109
+ );
47110
+ });
47111
+ if (touchesLand) continue;
47112
+ }
47113
+ if (collides(rect)) continue;
47114
+ if (placedRects.some((r) => overlapsPadded(rect, r, CONTEXT_PAD))) continue;
47115
+ placedRects.push(rect);
47116
+ placed.push({
47117
+ x: cand.cx,
47118
+ y: cand.cy,
47119
+ text: cand.text,
47120
+ anchor: "middle",
47121
+ color: cand.color,
47122
+ // No halo: the bg-coloured outline reads as a ghost box behind the text
47123
+ // over the tinted water/land. Context labels are muted enough to sit
47124
+ // cleanly on the basemap without one.
47125
+ halo: false,
47126
+ haloColor,
47127
+ italic: cand.italic,
47128
+ letterSpacing: cand.letterSpacing,
47129
+ ...cand.lines.length > 1 ? { lines: cand.lines } : {},
47130
+ lineNumber: 0
47131
+ });
47132
+ }
47133
+ return placed;
47134
+ }
47135
+ var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, KIND_ORDER;
47136
+ var init_context_labels = __esm({
47137
+ "src/map/context-labels.ts"() {
47138
+ "use strict";
47139
+ init_color_utils();
47140
+ init_legend_constants();
47141
+ FONT = 11;
47142
+ LINE_HEIGHT = FONT + 2;
47143
+ PADX = 4;
47144
+ PADY = 3;
47145
+ WATER_LETTER_SPACING = 1.5;
47146
+ CONTEXT_PAD = 4;
47147
+ EDGE_CLAMP_MARGIN = 8;
47148
+ EDGE_CLAMP_OVERSHOOT = 0.35;
47149
+ KIND_ORDER = {
47150
+ ocean: 0,
47151
+ sea: 1,
47152
+ gulf: 2,
47153
+ bay: 3,
47154
+ strait: 4,
47155
+ channel: 5,
47156
+ sound: 6
47157
+ };
47158
+ }
47159
+ });
47160
+
46222
47161
  // src/map/layout.ts
46223
47162
  import {
46224
47163
  geoPath,
46225
47164
  geoNaturalEarth1,
47165
+ geoEqualEarth,
46226
47166
  geoEquirectangular,
46227
47167
  geoConicEqualArea,
46228
47168
  geoMercator,
@@ -46234,12 +47174,34 @@ function geomObject2(topo) {
46234
47174
  const key = Object.keys(topo.objects)[0];
46235
47175
  return topo.objects[key];
46236
47176
  }
47177
+ function mergeFeatures(a, b) {
47178
+ const polysOf = (f) => {
47179
+ const g = f.geometry;
47180
+ if (!g) return null;
47181
+ if (g.type === "Polygon") return [g.coordinates];
47182
+ if (g.type === "MultiPolygon") return g.coordinates;
47183
+ return null;
47184
+ };
47185
+ const pa = polysOf(a);
47186
+ const pb = polysOf(b);
47187
+ if (!pa || !pb) return a;
47188
+ return {
47189
+ ...a,
47190
+ geometry: { type: "MultiPolygon", coordinates: [...pa, ...pb] }
47191
+ };
47192
+ }
46237
47193
  function decodeLayer(topo) {
47194
+ const cached = decodeCache.get(topo);
47195
+ if (cached) return cached;
46238
47196
  const out = /* @__PURE__ */ new Map();
46239
47197
  for (const g of geomObject2(topo).geometries) {
46240
47198
  const f = feature2(topo, g);
46241
- out.set(g.id, { ...f, id: g.id });
47199
+ if (!f.geometry) continue;
47200
+ const tagged = { ...f, id: g.id };
47201
+ const existing = out.get(g.id);
47202
+ out.set(g.id, existing ? mergeFeatures(existing, tagged) : tagged);
46242
47203
  }
47204
+ decodeCache.set(topo, out);
46243
47205
  return out;
46244
47206
  }
46245
47207
  function projectionFor(family) {
@@ -46248,9 +47210,12 @@ function projectionFor(family) {
46248
47210
  return usConusProjection();
46249
47211
  case "mercator":
46250
47212
  return geoMercator();
47213
+ case "equal-earth":
47214
+ return geoEqualEarth();
47215
+ case "equirectangular":
47216
+ return geoEquirectangular();
46251
47217
  case "natural-earth":
46252
47218
  return geoNaturalEarth1();
46253
- case "equirectangular":
46254
47219
  default:
46255
47220
  return geoEquirectangular();
46256
47221
  }
@@ -46269,13 +47234,11 @@ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46269
47234
  isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT
46270
47235
  );
46271
47236
  }
46272
- function layoutMap(resolved, data, size, opts) {
46273
- const { palette, isDark } = opts;
46274
- const { width, height } = size;
47237
+ function buildMapProjection(resolved, data) {
46275
47238
  const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
46276
- const usCrisp = resolved.projection === "albers-usa" && wantsUsStates && !!data.naLand;
47239
+ const usCrisp = (resolved.projection === "albers-usa" || resolved.projection === "mercator") && wantsUsStates && !!data.naLand;
46277
47240
  const worldTopo = usCrisp ? data.worldDetail : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46278
- const worldLayer = decodeLayer(worldTopo);
47241
+ const worldLayer = new Map(decodeLayer(worldTopo));
46279
47242
  if (usCrisp && data.naLand) {
46280
47243
  const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
46281
47244
  const crisp = decodeLayer(data.naLand);
@@ -46284,16 +47247,141 @@ function layoutMap(resolved, data, size, opts) {
46284
47247
  if (!base) continue;
46285
47248
  const [[bw, bs], [be, bn]] = geoBounds2(base);
46286
47249
  if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
46287
- worldLayer.set(iso, cf);
47250
+ worldLayer.set(iso, { ...cf, properties: base.properties });
46288
47251
  }
46289
47252
  }
46290
47253
  const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
47254
+ const extentOutline = () => {
47255
+ const [[w, s], [e, n]] = resolved.extent;
47256
+ const N = 16;
47257
+ const coords = [];
47258
+ for (let i = 0; i <= N; i++) {
47259
+ const t = i / N;
47260
+ const lon = w + (e - w) * t;
47261
+ const lat = s + (n - s) * t;
47262
+ coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
47263
+ }
47264
+ return {
47265
+ type: "Feature",
47266
+ properties: {},
47267
+ geometry: { type: "MultiPoint", coordinates: coords }
47268
+ };
47269
+ };
47270
+ let fitFeatures;
47271
+ if (resolved.projection === "albers-usa" && usLayer) {
47272
+ fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
47273
+ const neighborPoints = resolved.pois.filter((p) => !inAlaska(p.lon, p.lat) && !inHawaii(p.lon, p.lat)).map((p) => [p.lon, p.lat]);
47274
+ if (neighborPoints.length > 0) {
47275
+ fitFeatures.push({
47276
+ type: "Feature",
47277
+ properties: {},
47278
+ geometry: { type: "MultiPoint", coordinates: neighborPoints }
47279
+ });
47280
+ }
47281
+ for (const r of resolved.regions) {
47282
+ if (r.layer === "country" && (r.iso === "CA" || r.iso === "MX")) {
47283
+ const cf = worldLayer.get(r.iso);
47284
+ if (cf) fitFeatures.push(cf);
47285
+ }
47286
+ }
47287
+ } else {
47288
+ fitFeatures = [extentOutline()];
47289
+ }
47290
+ const fitTarget = { type: "FeatureCollection", features: fitFeatures };
47291
+ const projection = projectionFor(resolved.projection);
47292
+ if (resolved.projection !== "albers-usa") {
47293
+ let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
47294
+ if (centerLon > 180) centerLon -= 360;
47295
+ projection.rotate([-centerLon, 0]);
47296
+ }
47297
+ const fitGB = geoBounds2(fitTarget);
47298
+ const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
47299
+ return {
47300
+ projection,
47301
+ fitTarget,
47302
+ fitIsGlobal,
47303
+ worldLayer,
47304
+ usLayer,
47305
+ usCrisp,
47306
+ wantsUsStates,
47307
+ worldTopo
47308
+ };
47309
+ }
47310
+ function parsePathRings(d) {
47311
+ const rings = [];
47312
+ let cur = [];
47313
+ const re = /([MLZ])([^MLZ]*)/g;
47314
+ let m;
47315
+ while (m = re.exec(d)) {
47316
+ if (m[1] === "Z") {
47317
+ if (cur.length) rings.push(cur);
47318
+ cur = [];
47319
+ continue;
47320
+ }
47321
+ if (m[1] === "M" && cur.length) {
47322
+ rings.push(cur);
47323
+ cur = [];
47324
+ }
47325
+ const nums = m[2].split(/[ ,]+/).map(Number);
47326
+ for (let i = 0; i + 1 < nums.length; i += 2) {
47327
+ const x = nums[i];
47328
+ const y = nums[i + 1];
47329
+ if (Number.isFinite(x) && Number.isFinite(y)) cur.push([x, y]);
47330
+ }
47331
+ }
47332
+ if (cur.length) rings.push(cur);
47333
+ return rings;
47334
+ }
47335
+ function dropAntimeridianWrapSlivers(d, width, height) {
47336
+ const rings = parsePathRings(d);
47337
+ if (rings.length <= 1) return d;
47338
+ const eps = 0.75;
47339
+ const minArea = 3e-3 * width * height;
47340
+ const ringArea = (r) => {
47341
+ let s = 0;
47342
+ for (let i = 0; i < r.length; i++) {
47343
+ const a = r[i];
47344
+ const b = r[(i + 1) % r.length];
47345
+ s += a[0] * b[1] - b[0] * a[1];
47346
+ }
47347
+ return Math.abs(s) / 2;
47348
+ };
47349
+ const areas = rings.map(ringArea);
47350
+ const maxArea = Math.max(...areas);
47351
+ const onVEdge = (a, b) => Math.abs(a[0]) <= eps && Math.abs(b[0]) <= eps || Math.abs(a[0] - width) <= eps && Math.abs(b[0] - width) <= eps;
47352
+ let dropped = false;
47353
+ const kept = rings.filter((r, idx) => {
47354
+ if (areas[idx] >= maxArea || areas[idx] >= minArea) return true;
47355
+ const touches = r.some((p, i) => onVEdge(p, r[(i + 1) % r.length]));
47356
+ if (touches) {
47357
+ dropped = true;
47358
+ return false;
47359
+ }
47360
+ return true;
47361
+ });
47362
+ if (!dropped) return d;
47363
+ return kept.map(
47364
+ (r) => r.map((p, i) => (i ? "L" : "M") + p[0] + "," + p[1]).join("") + "Z"
47365
+ ).join("");
47366
+ }
47367
+ function layoutMap(resolved, data, size, opts) {
47368
+ const { palette, isDark } = opts;
47369
+ const { width, height } = size;
47370
+ const {
47371
+ projection,
47372
+ fitTarget,
47373
+ fitIsGlobal,
47374
+ worldLayer,
47375
+ usLayer,
47376
+ usCrisp,
47377
+ worldTopo
47378
+ } = buildMapProjection(resolved, data);
46291
47379
  const usContext = usLayer !== null;
46292
47380
  const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46293
47381
  const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
46294
- const scaleOverride = resolved.directives.scale;
46295
- const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46296
- const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
47382
+ const allNonNegative = values.length > 0 && values.every((v) => v >= 0);
47383
+ const rampMin = allNonNegative ? 0 : Math.min(...values);
47384
+ const rampMax = Math.max(...values);
46297
47385
  const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46298
47386
  const hasRamp = values.length > 0;
46299
47387
  const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
@@ -46314,7 +47402,7 @@ function layoutMap(resolved, data, size, opts) {
46314
47402
  activeGroup = VALUE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46315
47403
  }
46316
47404
  const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
46317
- const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
47405
+ const mutedBasemap = activeGroup !== null;
46318
47406
  const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46319
47407
  const water = mapBackgroundColor(palette, isDark, mutedBasemap);
46320
47408
  const lakeStroke = mix(regionStroke, water, 45);
@@ -46323,10 +47411,43 @@ function layoutMap(resolved, data, size, opts) {
46323
47411
  palette.bg,
46324
47412
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46325
47413
  );
47414
+ const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && resolved.tagGroups.length === 0;
47415
+ const colorByIso = /* @__PURE__ */ new Map();
47416
+ if (colorizeActive) {
47417
+ const adjacency = /* @__PURE__ */ new Map();
47418
+ const addEdges = (src) => {
47419
+ for (const [iso, ns] of src) {
47420
+ const cur = adjacency.get(iso);
47421
+ if (cur) cur.push(...ns);
47422
+ else adjacency.set(iso, [...ns]);
47423
+ }
47424
+ };
47425
+ addEdges(buildAdjacency(worldTopo));
47426
+ if (usLayer) {
47427
+ addEdges(buildAdjacency(data.usStates));
47428
+ for (const [country, states] of Object.entries(FOREIGN_BORDER)) {
47429
+ const cn = adjacency.get(country);
47430
+ if (!cn) continue;
47431
+ for (const st of states) {
47432
+ const sn = adjacency.get(st);
47433
+ if (!sn) continue;
47434
+ cn.push(st);
47435
+ sn.push(country);
47436
+ }
47437
+ }
47438
+ }
47439
+ const { byIso, huesNeeded } = assignColors(
47440
+ [...adjacency.keys()],
47441
+ adjacency
47442
+ );
47443
+ const tints = politicalTints(palette, huesNeeded, isDark);
47444
+ for (const [iso, idx] of byIso) colorByIso.set(iso, tints[idx]);
47445
+ }
47446
+ const colorizeStroke = (fill2) => mix(fill2, palette.text, 35);
46326
47447
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46327
47448
  const fillForValue = (s) => {
46328
47449
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
46329
- const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
47450
+ const pct = RAMP_FLOOR2 + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR2);
46330
47451
  return mix(rampHue, rampBase, pct);
46331
47452
  };
46332
47453
  const tagFill = (tags, groupName) => {
@@ -46358,43 +47479,15 @@ function layoutMap(resolved, data, size, opts) {
46358
47479
  if (activeIsScore) {
46359
47480
  return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46360
47481
  }
47482
+ if (colorizeActive) return (r.iso && colorByIso.get(r.iso)) ?? neutralFill;
46361
47483
  return tagFill(r.tags, activeGroup) ?? neutralFill;
46362
47484
  };
46363
47485
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
46364
- const extentOutline = () => {
46365
- const [[w, s], [e, n]] = resolved.extent;
46366
- const N = 16;
46367
- const coords = [];
46368
- for (let i = 0; i <= N; i++) {
46369
- const t = i / N;
46370
- const lon = w + (e - w) * t;
46371
- const lat = s + (n - s) * t;
46372
- coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
46373
- }
46374
- return {
46375
- type: "Feature",
46376
- properties: {},
46377
- geometry: { type: "MultiPoint", coordinates: coords }
46378
- };
46379
- };
46380
- let fitFeatures;
46381
- if (resolved.projection === "albers-usa" && usLayer) {
46382
- fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
46383
- } else {
46384
- fitFeatures = [extentOutline()];
46385
- }
46386
- const fitTarget = { type: "FeatureCollection", features: fitFeatures };
46387
- const projection = projectionFor(resolved.projection);
46388
- if (resolved.projection !== "albers-usa") {
46389
- let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
46390
- if (centerLon > 180) centerLon -= 360;
46391
- projection.rotate([-centerLon, 0]);
46392
- }
46393
- const TITLE_GAP = 16;
47486
+ const TITLE_GAP2 = 16;
46394
47487
  let topPad = FIT_PAD;
46395
47488
  if (resolved.title && resolved.pois.length > 0) {
46396
47489
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
46397
- topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
47490
+ topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
46398
47491
  }
46399
47492
  const fitBox = [
46400
47493
  [FIT_PAD, topPad],
@@ -46404,21 +47497,20 @@ function layoutMap(resolved, data, size, opts) {
46404
47497
  ]
46405
47498
  ];
46406
47499
  projection.fitExtent(fitBox, fitTarget);
46407
- const fitGB = geoBounds2(fitTarget);
46408
- const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46409
47500
  let path;
46410
47501
  let project;
46411
47502
  let stretchParams = null;
46412
- if (fitIsGlobal) {
47503
+ if (fitIsGlobal && !opts.preferContain) {
46413
47504
  const cb = geoPath(projection).bounds(fitTarget);
46414
47505
  const bx0 = cb[0][0];
46415
47506
  const by0 = cb[0][1];
46416
47507
  const cw = cb[1][0] - bx0;
46417
47508
  const ch = cb[1][1] - by0;
46418
- const ox = fitBox[0][0];
46419
- const oy = fitBox[0][1];
46420
- const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46421
- const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47509
+ const topReserve = resolved.title && resolved.pois.length > 0 ? topPad : 0;
47510
+ const ox = 0;
47511
+ const oy = topReserve;
47512
+ const sx = cw > 0 ? width / cw : 1;
47513
+ const sy = ch > 0 ? (height - topReserve) / ch : 1;
46422
47514
  stretchParams = { sx, sy, ox, oy, bx0, by0 };
46423
47515
  const stretch = (x, y) => [
46424
47516
  ox + (x - bx0) * sx,
@@ -46451,7 +47543,9 @@ function layoutMap(resolved, data, size, opts) {
46451
47543
  const insets = [];
46452
47544
  const insetRegions = [];
46453
47545
  const insetLabelSeeds = [];
46454
- if (resolved.projection === "albers-usa" && usLayer && !resolved.directives.noInsets) {
47546
+ const akRef = resolved.regions.some((r) => r.iso === "US-AK") || resolved.pois.some((p) => inAlaska(p.lon, p.lat));
47547
+ const hiRef = resolved.regions.some((r) => r.iso === "US-HI") || resolved.pois.some((p) => inHawaii(p.lon, p.lat));
47548
+ if (resolved.projection === "albers-usa" && usLayer && (akRef || hiRef)) {
46455
47549
  const PAD = 8;
46456
47550
  const GAP = 12;
46457
47551
  const yB = height - FIT_PAD;
@@ -46516,8 +47610,18 @@ function layoutMap(resolved, data, size, opts) {
46516
47610
  );
46517
47611
  const d = geoPath(proj)(f) ?? "";
46518
47612
  if (!d) return xr;
47613
+ let contextLand;
47614
+ if (iso === "US-AK") {
47615
+ const can = worldLayer.get("CA");
47616
+ const cd = can ? geoPath(proj)(can) ?? "" : "";
47617
+ if (cd)
47618
+ contextLand = {
47619
+ d: cd,
47620
+ fill: colorizeActive ? colorByIso.get("CA") ?? foreignFill : foreignFill
47621
+ };
47622
+ }
46519
47623
  const r = regionById.get(iso);
46520
- let fill2 = neutralFill;
47624
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? neutralFill : neutralFill;
46521
47625
  let lineNumber = -1;
46522
47626
  if (r?.layer === "us-state") {
46523
47627
  fill2 = regionFill(r);
@@ -46536,13 +47640,14 @@ function layoutMap(resolved, data, size, opts) {
46536
47640
  ],
46537
47641
  // The FITTED inset projection (just fit to this box) — captured so the
46538
47642
  // geo-query can invert pixels inside the frame back to AK/HI coords.
46539
- projection: proj
47643
+ projection: proj,
47644
+ ...contextLand && { contextLand }
46540
47645
  });
46541
47646
  insetRegions.push({
46542
47647
  id: iso,
46543
47648
  d,
46544
47649
  fill: fill2,
46545
- stroke: regionStroke,
47650
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46546
47651
  lineNumber,
46547
47652
  layer: "us-state",
46548
47653
  ...r?.value !== void 0 && { value: r.value },
@@ -46555,13 +47660,16 @@ function layoutMap(resolved, data, size, opts) {
46555
47660
  }
46556
47661
  return xr;
46557
47662
  };
46558
- const akRight = placeInset(
46559
- "US-AK",
46560
- alaskaProjection(),
46561
- FIT_PAD,
46562
- width * 0.15
46563
- );
46564
- placeInset("US-HI", hawaiiProjection(), akRight + 24, width * 0.1);
47663
+ let akRight = FIT_PAD;
47664
+ if (akRef)
47665
+ akRight = placeInset("US-AK", alaskaProjection(), FIT_PAD, width * 0.15);
47666
+ if (hiRef)
47667
+ placeInset(
47668
+ "US-HI",
47669
+ hawaiiProjection(),
47670
+ akRef ? akRight + 24 : FIT_PAD,
47671
+ width * 0.1
47672
+ );
46565
47673
  }
46566
47674
  const conusFit = resolved.projection === "albers-usa" && !!usLayer;
46567
47675
  const classifyExtent = conusFit ? geoBounds2(fitTarget) : resolved.extent;
@@ -46577,15 +47685,24 @@ function layoutMap(resolved, data, size, opts) {
46577
47685
  };
46578
47686
  const ringOverlapsView = (ring) => {
46579
47687
  let loMin = Infinity, loMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
47688
+ const lons = [];
46580
47689
  for (const [rawLon] of ring) {
46581
47690
  const lon = normLon(rawLon);
47691
+ lons.push(lon);
46582
47692
  if (lon < loMin) loMin = lon;
46583
47693
  if (lon > loMax) loMax = lon;
46584
47694
  if (rawLon < rawMin) rawMin = rawLon;
46585
47695
  if (rawLon > rawMax) rawMax = rawLon;
46586
47696
  }
46587
- if (loMax - loMin > 270) return false;
46588
- if (rawMax - rawMin > 180 && loMax - loMin < 90) return false;
47697
+ lons.sort((a, b) => a - b);
47698
+ let maxGap = 0;
47699
+ for (let i = 1; i < lons.length; i++)
47700
+ maxGap = Math.max(maxGap, lons[i] - lons[i - 1]);
47701
+ if (lons.length > 1)
47702
+ maxGap = Math.max(maxGap, lons[0] + 360 - lons[lons.length - 1]);
47703
+ const occupiedArc = 360 - maxGap;
47704
+ if (occupiedArc > 270) return false;
47705
+ if (rawMax - rawMin > 180 && occupiedArc < 90) return false;
46589
47706
  let px0 = Infinity, py0 = Infinity, px1 = -Infinity, py1 = -Infinity, anyFinite = false;
46590
47707
  for (const [lon, lat] of ring) {
46591
47708
  const p = project(lon, lat);
@@ -46658,7 +47775,7 @@ function layoutMap(resolved, data, size, opts) {
46658
47775
  const regions = [];
46659
47776
  const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
46660
47777
  for (const [iso, f] of layerFeatures) {
46661
- if (layerKind === "us-state" && usContext && INSET_STATES.has(iso))
47778
+ if (layerKind === "us-state" && usContext && resolved.projection === "albers-usa" && INSET_STATES.has(iso))
46662
47779
  continue;
46663
47780
  if (layerKind === "country" && usContext && iso === "US") continue;
46664
47781
  if (layerKind === "country" && iso === "AQ" && !regionById.has("AQ"))
@@ -46666,11 +47783,13 @@ function layoutMap(resolved, data, size, opts) {
46666
47783
  const r = regionById.get(iso);
46667
47784
  const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
46668
47785
  if (!viewF) continue;
46669
- const d = path(viewF) ?? "";
47786
+ const raw = path(viewF) ?? "";
47787
+ const d = fitIsGlobal ? dropAntimeridianWrapSlivers(raw, width, height) : raw;
46670
47788
  if (!d) continue;
46671
47789
  const isThisLayer = r?.layer === layerKind;
46672
47790
  const isForeign = layerKind === "country" && usContext && iso !== "US";
46673
- let fill2 = isForeign ? foreignFill : neutralFill;
47791
+ const baseFill = isForeign ? foreignFill : neutralFill;
47792
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? baseFill : baseFill;
46674
47793
  let label;
46675
47794
  let lineNumber = -1;
46676
47795
  let layer = "base";
@@ -46679,15 +47798,21 @@ function layoutMap(resolved, data, size, opts) {
46679
47798
  lineNumber = r.lineNumber;
46680
47799
  layer = layerKind;
46681
47800
  label = r.name;
47801
+ } else {
47802
+ label = f.properties?.name;
46682
47803
  }
47804
+ const labelAnchor = WORLD_LABEL_ANCHORS[iso];
47805
+ const c = labelAnchor ? project(labelAnchor[0], labelAnchor[1]) : path.centroid(viewF);
47806
+ const hasCentroid = c != null && Number.isFinite(c[0]) && Number.isFinite(c[1]);
46683
47807
  regions.push({
46684
47808
  id: iso,
46685
47809
  d,
46686
47810
  fill: fill2,
46687
- stroke: regionStroke,
47811
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46688
47812
  lineNumber,
46689
47813
  layer,
46690
47814
  ...label !== void 0 && { label },
47815
+ ...hasCentroid && { labelX: c[0], labelY: c[1] },
46691
47816
  ...isThisLayer && r.value !== void 0 && { value: r.value },
46692
47817
  ...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
46693
47818
  });
@@ -46712,9 +47837,41 @@ function layoutMap(resolved, data, size, opts) {
46712
47837
  });
46713
47838
  }
46714
47839
  }
47840
+ const pointInRings = (px, py, rings) => {
47841
+ let inside = false;
47842
+ for (const ring of rings) {
47843
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
47844
+ const [xi, yi] = ring[i];
47845
+ const [xj, yj] = ring[j];
47846
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
47847
+ inside = !inside;
47848
+ }
47849
+ }
47850
+ return inside;
47851
+ };
47852
+ const fillHitTargets = [...regions, ...insetRegions].map((r) => ({
47853
+ fill: r.fill,
47854
+ rings: parsePathRings(r.d)
47855
+ }));
47856
+ const fillAt = (x, y) => {
47857
+ let hit = water;
47858
+ for (const t of fillHitTargets)
47859
+ if (pointInRings(x, y, t.rings)) hit = t.fill;
47860
+ return hit;
47861
+ };
47862
+ const labelOnFill = (fill2) => {
47863
+ const color = contrastRatio(fill2, palette.textOnFillDark) >= contrastRatio(fill2, palette.textOnFillLight) ? palette.textOnFillDark : palette.textOnFillLight;
47864
+ const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47865
+ return {
47866
+ color,
47867
+ halo: contrastRatio(fill2, color) < REGION_LABEL_HALO_RATIO,
47868
+ haloColor
47869
+ };
47870
+ };
47871
+ const reliefAllowed = resolved.directives.noRelief !== true;
46715
47872
  const relief = [];
46716
47873
  let reliefHatch = null;
46717
- if (resolved.directives.relief === true && data.mountainRanges) {
47874
+ if (reliefAllowed && data.mountainRanges) {
46718
47875
  for (const [, f] of decodeLayer(data.mountainRanges)) {
46719
47876
  const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
46720
47877
  if (!viewF) continue;
@@ -46730,16 +47887,32 @@ function layoutMap(resolved, data, size, opts) {
46730
47887
  if (relief.length) {
46731
47888
  const darkTone = isDark ? palette.bg : palette.text;
46732
47889
  const lightTone = isDark ? palette.text : palette.bg;
46733
- const landLum = relativeLuminance(neutralFill);
47890
+ const reliefLandRef = colorizeActive ? isDark ? palette.surface : palette.bg : neutralFill;
47891
+ const landLum = relativeLuminance(reliefLandRef);
46734
47892
  const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
46735
47893
  reliefHatch = {
46736
- color: mix(tone, neutralFill, RELIEF_HATCH_STRENGTH),
47894
+ color: mix(tone, reliefLandRef, RELIEF_HATCH_STRENGTH),
46737
47895
  spacing: RELIEF_HATCH_SPACING,
46738
47896
  width: RELIEF_HATCH_WIDTH
46739
47897
  };
46740
47898
  }
46741
47899
  }
46742
- const riverColor = mix(water, regionStroke, 16);
47900
+ let coastlineStyle = null;
47901
+ if (resolved.directives.noCoastline !== true) {
47902
+ const minDim = Math.min(width, height);
47903
+ coastlineStyle = {
47904
+ color: mix(regionStroke, water, COASTLINE_STROKE_MIX),
47905
+ // N equal-width rings: distance steps outward by COASTLINE_STEP; opacity
47906
+ // fades linearly from NEAR (innermost) to FAR (outermost).
47907
+ lines: Array.from({ length: COASTLINE_RING_COUNT }, (_, k) => ({
47908
+ d: (COASTLINE_D0 + k * COASTLINE_STEP) * minDim,
47909
+ thickness: COASTLINE_THICKNESS * minDim,
47910
+ opacity: COASTLINE_OPACITY_NEAR + (COASTLINE_OPACITY_FAR - COASTLINE_OPACITY_NEAR) * k / (COASTLINE_RING_COUNT - 1)
47911
+ })),
47912
+ minExtent: (isGlobalView ? COASTLINE_MIN_EXTENT_GLOBAL : COASTLINE_MIN_EXTENT) * minDim
47913
+ };
47914
+ }
47915
+ const riverColor = mix(palette.colors.blue, water, 32);
46743
47916
  const rivers = [];
46744
47917
  if (data.rivers) {
46745
47918
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -46795,38 +47968,108 @@ function layoutMap(resolved, data, size, opts) {
46795
47968
  const xy = project(p.lon, p.lat);
46796
47969
  if (xy) projected.push({ p, xy });
46797
47970
  }
46798
- const coloGroups = /* @__PURE__ */ new Map();
47971
+ const placePoi = (e, cx, cy, clusterId) => {
47972
+ const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
47973
+ poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
47974
+ const num = routeNumberById.get(e.p.id);
47975
+ pois.push({
47976
+ id: e.p.id,
47977
+ cx,
47978
+ cy,
47979
+ r: radiusFor(e.p),
47980
+ fill: fill2,
47981
+ stroke: stroke2,
47982
+ lineNumber: e.p.lineNumber,
47983
+ implicit: !!e.p.implicit,
47984
+ isOrigin: originIds.has(e.p.id),
47985
+ ...num !== void 0 && { routeNumber: num },
47986
+ ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags },
47987
+ ...clusterId !== void 0 && { clusterId }
47988
+ });
47989
+ };
47990
+ const clusters = [];
47991
+ const connected = /* @__PURE__ */ new Set();
47992
+ for (const e of resolved.edges) {
47993
+ connected.add(e.fromId);
47994
+ connected.add(e.toId);
47995
+ }
47996
+ for (const rt of resolved.routes) {
47997
+ rt.stopIds.forEach((id) => connected.add(id));
47998
+ }
47999
+ const radiusOf = (e) => radiusFor(e.p);
46799
48000
  for (const e of projected) {
46800
- const key = `${Math.round(e.xy[0] / COLO_EPS)},${Math.round(e.xy[1] / COLO_EPS)}`;
46801
- const arr = coloGroups.get(key);
46802
- if (arr) arr.push(e);
46803
- else coloGroups.set(key, [e]);
46804
- }
46805
- for (const group of coloGroups.values()) {
46806
- group.forEach((e, i) => {
46807
- let cx = e.xy[0];
46808
- let cy = e.xy[1];
46809
- if (group.length > 1) {
46810
- const ang = i * GOLDEN_ANGLE;
46811
- cx += Math.cos(ang) * COLO_R;
46812
- cy += Math.sin(ang) * COLO_R;
46813
- }
46814
- const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
46815
- poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
46816
- const num = routeNumberById.get(e.p.id);
46817
- pois.push({
46818
- id: e.p.id,
46819
- cx,
46820
- cy,
46821
- r: radiusFor(e.p),
46822
- fill: fill2,
46823
- stroke: stroke2,
46824
- lineNumber: e.p.lineNumber,
46825
- implicit: !!e.p.implicit,
46826
- isOrigin: originIds.has(e.p.id),
46827
- ...num !== void 0 && { routeNumber: num },
46828
- ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags }
46829
- });
48001
+ if (connected.has(e.p.id)) placePoi(e, e.xy[0], e.xy[1]);
48002
+ }
48003
+ const groups = [];
48004
+ for (const e of projected) {
48005
+ if (connected.has(e.p.id)) continue;
48006
+ const r = radiusOf(e);
48007
+ const near = groups.find(
48008
+ (g) => g.some(
48009
+ (q) => Math.hypot(q.xy[0] - e.xy[0], q.xy[1] - e.xy[1]) < (r + radiusOf(q)) * STACK_OVERLAP
48010
+ )
48011
+ );
48012
+ if (near) near.push(e);
48013
+ else groups.push([e]);
48014
+ }
48015
+ for (const g of groups) {
48016
+ if (g.length === 1) {
48017
+ placePoi(g[0], g[0].xy[0], g[0].xy[1]);
48018
+ continue;
48019
+ }
48020
+ const clusterId = g[0].p.id;
48021
+ const cx0 = g.reduce((s, e) => s + e.xy[0], 0) / g.length;
48022
+ const cy0 = g.reduce((s, e) => s + e.xy[1], 0) / g.length;
48023
+ const maxR = Math.max(...g.map(radiusOf));
48024
+ const sep = 2 * maxR + STACK_RING_GAP;
48025
+ const ringR = Math.max(
48026
+ COLO_R,
48027
+ sep / (2 * Math.sin(Math.PI / Math.max(g.length, 2)))
48028
+ );
48029
+ const positions = g.map((e, i) => {
48030
+ if (g.length <= STACK_RING_MAX) {
48031
+ const ang2 = -Math.PI / 2 + i * 2 * Math.PI / g.length;
48032
+ return {
48033
+ e,
48034
+ mx: cx0 + Math.cos(ang2) * ringR,
48035
+ my: cy0 + Math.sin(ang2) * ringR
48036
+ };
48037
+ }
48038
+ const ang = i * GOLDEN_ANGLE;
48039
+ const rr = ringR * Math.sqrt((i + 1) / g.length);
48040
+ return { e, mx: cx0 + Math.cos(ang) * rr, my: cy0 + Math.sin(ang) * rr };
48041
+ });
48042
+ let minX = cx0 - maxR;
48043
+ let maxX = cx0 + maxR;
48044
+ let minY = cy0 - maxR;
48045
+ let maxY = cy0 + maxR;
48046
+ for (const { mx, my, e } of positions) {
48047
+ const r = radiusOf(e);
48048
+ minX = Math.min(minX, mx - r);
48049
+ maxX = Math.max(maxX, mx + r);
48050
+ minY = Math.min(minY, my - r);
48051
+ maxY = Math.max(maxY, my + r);
48052
+ }
48053
+ let dx = 0;
48054
+ let dy = 0;
48055
+ if (minX + dx < 2) dx = 2 - minX;
48056
+ if (maxX + dx > width - 2) dx = width - 2 - maxX;
48057
+ if (minY + dy < 2) dy = 2 - minY;
48058
+ if (maxY + dy > height - 2) dy = height - 2 - maxY;
48059
+ const legsOut = [];
48060
+ for (const { e, mx, my } of positions) {
48061
+ const fx = mx + dx;
48062
+ const fy = my + dy;
48063
+ placePoi(e, fx, fy, clusterId);
48064
+ legsOut.push({ x2: fx, y2: fy, color: poiFill(e.p).fill });
48065
+ }
48066
+ clusters.push({
48067
+ id: clusterId,
48068
+ cx: cx0 + dx,
48069
+ cy: cy0 + dy,
48070
+ count: g.length,
48071
+ hitR: ringR + maxR + 6,
48072
+ legs: legsOut
46830
48073
  });
46831
48074
  }
46832
48075
  const legs = [];
@@ -46876,16 +48119,26 @@ function layoutMap(resolved, data, size, opts) {
46876
48119
  if (!a || !b) continue;
46877
48120
  const mx = (a.cx + b.cx) / 2;
46878
48121
  const my = (a.cy + b.cy) / 2;
48122
+ const bow = {
48123
+ curved: leg.style === "arc",
48124
+ offset: 0,
48125
+ labelX: mx,
48126
+ labelY: my - 4
48127
+ };
48128
+ const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46879
48129
  legs.push({
46880
- d: legPath(a, b, leg.style === "arc", 0),
48130
+ d: legPath(a, b, bow.curved, bow.offset),
46881
48131
  width: routeWidthFor(Number(leg.value)),
46882
48132
  color: mix(palette.text, palette.bg, 72),
46883
48133
  arrow: true,
46884
48134
  lineNumber: leg.lineNumber,
46885
48135
  ...leg.label !== void 0 && {
46886
48136
  label: leg.label,
46887
- labelX: mx,
46888
- labelY: my - 4
48137
+ labelX: bow.labelX,
48138
+ labelY: bow.labelY,
48139
+ labelColor: routeLabelStyle.color,
48140
+ labelHalo: routeLabelStyle.halo,
48141
+ labelHaloColor: routeLabelStyle.haloColor
46889
48142
  }
46890
48143
  });
46891
48144
  }
@@ -46913,20 +48166,29 @@ function layoutMap(resolved, data, size, opts) {
46913
48166
  const a = poiScreen.get(e.fromId);
46914
48167
  const b = poiScreen.get(e.toId);
46915
48168
  if (!a || !b) return;
46916
- const curved = e.style === "arc" || n > 1;
46917
- const offset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
48169
+ const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
46918
48170
  const mx = (a.cx + b.cx) / 2;
46919
48171
  const my = (a.cy + b.cy) / 2;
48172
+ const bow = {
48173
+ curved: e.style === "arc" || n > 1,
48174
+ offset: fanOffset,
48175
+ labelX: mx,
48176
+ labelY: my - 4
48177
+ };
48178
+ const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46920
48179
  legs.push({
46921
- d: legPath(a, b, curved, offset),
48180
+ d: legPath(a, b, bow.curved, bow.offset),
46922
48181
  width: widthFor(e),
46923
48182
  color: mix(palette.text, palette.bg, 66),
46924
48183
  arrow: e.directed,
46925
48184
  lineNumber: e.lineNumber,
46926
48185
  ...e.label !== void 0 && {
46927
48186
  label: e.label,
46928
- labelX: mx,
46929
- labelY: my - 4
48187
+ labelX: bow.labelX,
48188
+ labelY: bow.labelY,
48189
+ labelColor: edgeLabelStyle.color,
48190
+ labelHalo: edgeLabelStyle.halo,
48191
+ labelHaloColor: edgeLabelStyle.haloColor
46930
48192
  }
46931
48193
  });
46932
48194
  });
@@ -46968,48 +48230,73 @@ function layoutMap(resolved, data, size, opts) {
46968
48230
  }
46969
48231
  }
46970
48232
  const collides = (rect) => markers.some((m) => rectCircleOverlap(rect, m)) || obstacles.some((o) => rectsOverlap(rect, o)) || legSegments.some((s) => segmentRectOverlap(s[0], s[1], s[2], s[3], rect));
46971
- const regionLabelMode = resolved.directives.regionLabels ?? "off";
48233
+ const showRegionLabels = resolved.directives.noRegionLabels !== true;
48234
+ const isCompact = width < COMPACT_WIDTH_PX;
46972
48235
  const LABEL_PADX = 6;
46973
48236
  const LABEL_PADY = 3;
46974
- const labelW = (text) => measureLegendText(text, FONT) + 2 * LABEL_PADX;
46975
- const labelH = FONT + 2 * LABEL_PADY;
48237
+ const labelW = (text) => measureLegendText(text, FONT2) + 2 * LABEL_PADX;
48238
+ const labelH = FONT2 + 2 * LABEL_PADY;
46976
48239
  const pushRegionLabel = (x, y, text, fill2, lineNumber) => {
46977
- const color = contrastText(
46978
- fill2,
46979
- palette.textOnFillLight,
46980
- palette.textOnFillDark
48240
+ const { color, haloColor } = labelOnFill(fill2);
48241
+ const halfW = measureLegendText(text, FONT2) / 2;
48242
+ const overflows = [y - FONT2 * 0.55, y - FONT2 * 0.1].some(
48243
+ (sy) => fillAt(x - halfW, sy) !== fill2 || fillAt(x + halfW, sy) !== fill2
46981
48244
  );
46982
- const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
46983
48245
  labels.push({
46984
48246
  x,
46985
48247
  y,
46986
48248
  text,
46987
48249
  anchor: "middle",
46988
48250
  color,
46989
- halo: true,
48251
+ halo: overflows,
46990
48252
  haloColor,
46991
48253
  lineNumber
46992
48254
  });
46993
48255
  };
46994
- const WORLD_LABEL_ANCHORS = {
46995
- US: [-98.5, 39.5]
46996
- // CONUS geographic centre (near Lebanon, Kansas)
48256
+ const REGION_LABEL_GAP = 2;
48257
+ const regionLabelRect = (cx, cy, text) => {
48258
+ const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
48259
+ return { x: cx - w / 2, y: cy - FONT2 / 2, w, h: FONT2 };
46997
48260
  };
46998
- if (regionLabelMode === "full" || regionLabelMode === "abbrev") {
46999
- for (const r of regions) {
47000
- if (r.layer === "base" || r.label === void 0) continue;
47001
- const f = r.layer === "us-state" ? usLayer?.get(r.id) : worldLayer.get(r.id);
47002
- if (!f) continue;
48261
+ if (showRegionLabels) {
48262
+ const frameContainers = new Set(resolved.poiFrameContainers);
48263
+ const entries = regions.map((r) => {
48264
+ const isContainer = frameContainers.has(r.id);
48265
+ if (r.layer === "base" && !isContainer || r.label === void 0)
48266
+ return null;
48267
+ const isUsState = r.layer === "us-state" || r.id.startsWith("US-");
48268
+ const f = isUsState ? usLayer?.get(r.id) : worldLayer.get(r.id);
48269
+ if (!f) return null;
47003
48270
  const [[x0, y0], [x1, y1]] = path.bounds(f);
47004
- const text = regionLabelMode === "abbrev" ? r.id.replace(/^US-/, "") : r.label;
47005
- if (labelW(text) > x1 - x0 || labelH > y1 - y0) continue;
47006
- const anchor = r.layer !== "us-state" ? WORLD_LABEL_ANCHORS[r.id] : void 0;
48271
+ const boxW = x1 - x0;
48272
+ const boxH = y1 - y0;
48273
+ const abbrev = isUsState ? r.id.replace(/^US-/, "") : void 0;
48274
+ const candidates = abbrev !== void 0 ? isCompact ? [abbrev, r.label] : [r.label, abbrev] : [r.label];
48275
+ const anchor = !isUsState ? WORLD_LABEL_ANCHORS[r.id] : void 0;
47007
48276
  const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
47008
- if (!c || !Number.isFinite(c[0])) continue;
48277
+ if (!c || !Number.isFinite(c[0])) return null;
48278
+ return { r, c, boxW, boxH, area: boxW * boxH, candidates };
48279
+ }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
48280
+ const placedRegionRects = [];
48281
+ const POI_LABEL_PAD = 14;
48282
+ const poiObstacles = pois.map((p) => ({
48283
+ x: p.cx - p.r - POI_LABEL_PAD,
48284
+ y: p.cy - p.r - POI_LABEL_PAD,
48285
+ w: 2 * (p.r + POI_LABEL_PAD),
48286
+ h: 2 * (p.r + POI_LABEL_PAD)
48287
+ }));
48288
+ for (const { r, c, boxW, boxH, candidates } of entries) {
48289
+ const text = candidates.find((t) => {
48290
+ if (labelW(t) > boxW || labelH > boxH) return false;
48291
+ const rect = regionLabelRect(c[0], c[1], t);
48292
+ return !placedRegionRects.some((p) => rectsOverlap(rect, p)) && !poiObstacles.some((o) => rectsOverlap(rect, o));
48293
+ });
48294
+ if (text === void 0) continue;
48295
+ placedRegionRects.push(regionLabelRect(c[0], c[1], text));
47009
48296
  pushRegionLabel(c[0], c[1], text, r.fill, r.lineNumber);
47010
48297
  }
47011
48298
  for (const seed of insetLabelSeeds) {
47012
- const text = regionLabelMode === "abbrev" ? seed.iso.replace(/^US-/, "") : seed.name;
48299
+ const text = isCompact ? seed.iso.replace(/^US-/, "") : seed.name;
47013
48300
  const src = regionById.get(seed.iso);
47014
48301
  pushRegionLabel(
47015
48302
  seed.x,
@@ -47020,22 +48307,26 @@ function layoutMap(resolved, data, size, opts) {
47020
48307
  );
47021
48308
  }
47022
48309
  }
47023
- const poiLabelMode = resolved.directives.poiLabels ?? "auto";
47024
- if (poiLabelMode !== "off") {
47025
- const ordered = [...pois].sort(
47026
- (a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1)
47027
- );
48310
+ if (resolved.directives.noPoiLabels !== true) {
48311
+ const ordered = [...pois].filter((p) => p.clusterId === void 0).sort((a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1));
47028
48312
  const poiById = new Map(resolved.pois.map((q) => [q.id, q]));
47029
48313
  const labelText = (p) => {
47030
48314
  const src = poiById.get(p.id);
47031
48315
  return src?.label ?? src?.name ?? p.id;
47032
48316
  };
47033
- const poiLabH = FONT * 1.25;
48317
+ const poiLabH = FONT2 * 1.25;
47034
48318
  const labelInfo = (p) => {
47035
48319
  const text = labelText(p);
47036
- return { text, w: measureLegendText(text, FONT) };
48320
+ return { text, w: measureLegendText(text, FONT2) };
47037
48321
  };
47038
48322
  const GAP = 3;
48323
+ const clusterMembersById = /* @__PURE__ */ new Map();
48324
+ for (const p of pois) {
48325
+ if (p.clusterId === void 0) continue;
48326
+ const arr = clusterMembersById.get(p.clusterId);
48327
+ if (arr) arr.push(p);
48328
+ else clusterMembersById.set(p.clusterId, [p]);
48329
+ }
47039
48330
  const inlineRect = (p, w, side) => {
47040
48331
  switch (side) {
47041
48332
  case "right":
@@ -47065,11 +48356,11 @@ function layoutMap(resolved, data, size, opts) {
47065
48356
  const x = side === "right" ? rect.x : side === "left" ? rect.x + w : p.cx;
47066
48357
  labels.push({
47067
48358
  x,
47068
- y: rect.y + poiLabH / 2 + FONT / 3,
48359
+ y: rect.y + poiLabH / 2 + FONT2 / 3,
47069
48360
  text,
47070
48361
  anchor,
47071
48362
  color: palette.text,
47072
- halo: true,
48363
+ halo: false,
47073
48364
  haloColor: palette.bg,
47074
48365
  poiId: p.id,
47075
48366
  lineNumber: p.lineNumber
@@ -47080,43 +48371,60 @@ function layoutMap(resolved, data, size, opts) {
47080
48371
  return rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect);
47081
48372
  };
47082
48373
  const GROUP_R = 30;
47083
- const groups = [];
48374
+ const groups2 = [];
47084
48375
  for (const p of ordered) {
47085
- const near = groups.find(
48376
+ const near = groups2.find(
47086
48377
  (g) => g.some((q) => Math.hypot(q.cx - p.cx, q.cy - p.cy) < GROUP_R)
47087
48378
  );
47088
48379
  if (near) near.push(p);
47089
- else groups.push([p]);
48380
+ else groups2.push([p]);
47090
48381
  }
47091
48382
  const ROW_GAP2 = 3;
47092
48383
  const step = poiLabH + ROW_GAP2;
47093
48384
  const COL_GAP = 16;
47094
- const placeColumn = (group) => {
47095
- const items = group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48385
+ const makeItems = (group) => group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48386
+ const columnRows = (items, side) => {
47096
48387
  const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
47097
48388
  const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
47098
- const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
47099
48389
  const maxW = Math.max(...items.map((o) => o.w));
47100
- const side = right + COL_GAP + maxW <= width - 2 ? "right" : "left";
47101
- const colX = side === "right" ? right + COL_GAP : left - COL_GAP;
48390
+ const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
48391
+ const colX = side === "right" ? Math.min(right + COL_GAP, width - 2 - maxW) : Math.max(left - COL_GAP, 2 + maxW);
47102
48392
  const totalH = items.length * step;
47103
48393
  let startY = cyMid - totalH / 2;
47104
48394
  startY = Math.max(2, Math.min(startY, height - totalH - 2));
47105
- items.forEach((o, i) => {
48395
+ return items.map((o, i) => {
47106
48396
  const rowCy = startY + i * step + step / 2;
47107
- obstacles.push({
47108
- x: side === "right" ? colX : colX - o.w,
47109
- y: rowCy - poiLabH / 2,
47110
- w: o.w,
47111
- h: poiLabH
47112
- });
48397
+ return {
48398
+ o,
48399
+ colX,
48400
+ rowCy,
48401
+ rect: {
48402
+ x: side === "right" ? colX : colX - o.w,
48403
+ y: rowCy - poiLabH / 2,
48404
+ w: o.w,
48405
+ h: poiLabH
48406
+ }
48407
+ };
48408
+ });
48409
+ };
48410
+ const wouldColumnBeClean = (items, side) => columnRows(items, side).every(
48411
+ ({ rect }) => rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect)
48412
+ );
48413
+ const defaultColumnSide = (items) => {
48414
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48415
+ const maxW = Math.max(...items.map((o) => o.w));
48416
+ return right + COL_GAP + maxW <= width - 2 ? "right" : "left";
48417
+ };
48418
+ const commitColumn = (items, side, clusterId) => {
48419
+ for (const { o, colX, rowCy, rect } of columnRows(items, side)) {
48420
+ obstacles.push(rect);
47113
48421
  labels.push({
47114
48422
  x: colX,
47115
- y: rowCy + FONT / 3,
48423
+ y: rowCy + FONT2 / 3,
47116
48424
  text: o.text,
47117
48425
  anchor: side === "right" ? "start" : "end",
47118
48426
  color: palette.text,
47119
- halo: true,
48427
+ halo: false,
47120
48428
  haloColor: palette.bg,
47121
48429
  leader: {
47122
48430
  x1: o.p.cx,
@@ -47126,24 +48434,141 @@ function layoutMap(resolved, data, size, opts) {
47126
48434
  },
47127
48435
  leaderColor: o.p.fill,
47128
48436
  poiId: o.p.id,
47129
- lineNumber: o.p.lineNumber
48437
+ lineNumber: o.p.lineNumber,
48438
+ ...clusterId !== void 0 && { clusterMember: clusterId }
47130
48439
  });
48440
+ }
48441
+ };
48442
+ const pushHidden = (p) => {
48443
+ const { text, w } = labelInfo(p);
48444
+ let x = p.cx + p.r + GAP;
48445
+ let anchor = "start";
48446
+ if (x + w > width) {
48447
+ x = p.cx - p.r - GAP - w;
48448
+ anchor = "end";
48449
+ }
48450
+ const y = Math.max(0, Math.min(p.cy - poiLabH / 2, height - poiLabH));
48451
+ labels.push({
48452
+ x: anchor === "start" ? x : x + w,
48453
+ y: y + poiLabH / 2 + FONT2 / 3,
48454
+ text,
48455
+ anchor,
48456
+ color: palette.text,
48457
+ halo: false,
48458
+ haloColor: palette.bg,
48459
+ poiId: p.id,
48460
+ hidden: true,
48461
+ lineNumber: p.lineNumber
47131
48462
  });
47132
48463
  };
47133
- for (const g of groups) {
48464
+ for (const [clusterId, members] of clusterMembersById) {
48465
+ if (members.length === 0) continue;
48466
+ const items = makeItems(members);
48467
+ const side = wouldColumnBeClean(items, "right") ? "right" : wouldColumnBeClean(items, "left") ? "left" : defaultColumnSide(items);
48468
+ commitColumn(items, side, clusterId);
48469
+ }
48470
+ const maxExtent = MAX_CLUSTER_EXTENT_FACTOR * Math.min(width, height);
48471
+ const clusterPending = [];
48472
+ for (const g of groups2) {
48473
+ const items = makeItems(g);
47134
48474
  if (g.length === 1) {
47135
- const p = g[0];
47136
- const { text, w } = labelInfo(p);
48475
+ const { p, text, w } = items[0];
47137
48476
  const side = ["right", "left", "above", "below"].find(
47138
48477
  (s) => inlineFits(p, w, s)
47139
48478
  );
47140
- if (side) {
47141
- pushInline(p, text, w, side);
47142
- continue;
48479
+ if (side) pushInline(p, text, w, side);
48480
+ else commitColumn(items, defaultColumnSide(items));
48481
+ continue;
48482
+ }
48483
+ const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
48484
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48485
+ const minCy = Math.min(...items.map((o) => o.p.cy));
48486
+ const maxCy = Math.max(...items.map((o) => o.p.cy));
48487
+ const diag = Math.hypot(right - left, maxCy - minCy);
48488
+ if (diag > maxExtent || items.length > MAX_COLUMN_ROWS) {
48489
+ items.forEach((o) => pushHidden(o.p));
48490
+ } else {
48491
+ clusterPending.push(items);
48492
+ }
48493
+ }
48494
+ for (const items of clusterPending) {
48495
+ const side = ["right", "left"].find(
48496
+ (s) => wouldColumnBeClean(items, s)
48497
+ );
48498
+ if (side) commitColumn(items, side);
48499
+ else items.forEach((o) => pushHidden(o.p));
48500
+ }
48501
+ }
48502
+ if (resolved.directives.noContextLabels !== true) {
48503
+ for (const l of labels) {
48504
+ if (l.hidden) continue;
48505
+ const w = labelW(l.text);
48506
+ const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
48507
+ obstacles.push({ x, y: l.y - labelH / 2, w, h: labelH });
48508
+ }
48509
+ for (const box of insets)
48510
+ obstacles.push({ x: box.x, y: box.y, w: box.w, h: box.h });
48511
+ const countryCandidates = [];
48512
+ for (const f of worldLayer.values()) {
48513
+ const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
48514
+ if (!iso || regionById.has(iso)) continue;
48515
+ let hasReferencedSub = false;
48516
+ for (const k of regionById.keys())
48517
+ if (k.startsWith(iso + "-")) {
48518
+ hasReferencedSub = true;
48519
+ break;
47143
48520
  }
48521
+ if (hasReferencedSub) continue;
48522
+ const b = path.bounds(f);
48523
+ const [x0, y0] = b[0];
48524
+ const [x1, y1] = b[1];
48525
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48526
+ const anchorLngLat = WORLD_LABEL_ANCHORS[iso];
48527
+ const a = anchorLngLat ? project(anchorLngLat[0], anchorLngLat[1]) : path.centroid(f);
48528
+ countryCandidates.push({
48529
+ name: f.properties?.name ?? iso,
48530
+ bbox: [x0, y0, x1, y1],
48531
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48532
+ });
48533
+ }
48534
+ const framedStateContainers = (resolved.poiFrameContainers ?? []).some(
48535
+ (id) => id.startsWith("US-")
48536
+ );
48537
+ if (usLayer && framedStateContainers) {
48538
+ const containerSet = new Set(resolved.poiFrameContainers);
48539
+ for (const [iso, f] of usLayer) {
48540
+ if (containerSet.has(iso) || regionById.has(iso)) continue;
48541
+ const viewF = cullFeatureToView(f);
48542
+ if (!viewF) continue;
48543
+ const b = path.bounds(viewF);
48544
+ const [x0, y0] = b[0];
48545
+ const [x1, y1] = b[1];
48546
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48547
+ const a = path.centroid(viewF);
48548
+ countryCandidates.push({
48549
+ name: f.properties?.name ?? iso,
48550
+ bbox: [x0, y0, x1, y1],
48551
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48552
+ });
47144
48553
  }
47145
- placeColumn(g);
47146
48554
  }
48555
+ const contextLabels = placeContextLabels({
48556
+ projection: resolved.projection,
48557
+ dLonSpan,
48558
+ dLatSpan,
48559
+ width,
48560
+ height,
48561
+ waterBodies: data.waterBodies,
48562
+ countries: countryCandidates,
48563
+ palette,
48564
+ project,
48565
+ collides,
48566
+ // Water labels must stay over open water — `fillAt` returns the ocean
48567
+ // backdrop colour off-land and a region fill on-land (lakes/states count
48568
+ // as land here, which is the safe side for an ocean name).
48569
+ overLand: (x, y) => fillAt(x, y) !== water
48570
+ });
48571
+ labels.push(...contextLabels);
47147
48572
  }
47148
48573
  let legend = null;
47149
48574
  if (!resolved.directives.noLegend) {
@@ -47180,58 +48605,102 @@ function layoutMap(resolved, data, size, opts) {
47180
48605
  rivers,
47181
48606
  relief,
47182
48607
  reliefHatch,
48608
+ coastlineStyle,
47183
48609
  legs,
47184
48610
  pois,
48611
+ clusters,
47185
48612
  labels,
47186
48613
  legend,
47187
48614
  insets,
47188
48615
  insetRegions,
47189
48616
  projection,
47190
- stretch: stretchParams
48617
+ stretch: stretchParams,
48618
+ diagnostics: []
47191
48619
  };
47192
48620
  }
47193
- var FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, COLO_EPS, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, FAN_STEP, ARC_CURVE_FRAC, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, US_NON_CONUS;
48621
+ var FIT_PAD, RAMP_FLOOR2, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT2, WORLD_LABEL_ANCHORS, MAX_CLUSTER_EXTENT_FACTOR, MAX_COLUMN_ROWS, REGION_LABEL_HALO_RATIO, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, COMPACT_WIDTH_PX, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, COASTLINE_RING_COUNT, COASTLINE_D0, COASTLINE_STEP, COASTLINE_THICKNESS, COASTLINE_OPACITY_NEAR, COASTLINE_OPACITY_FAR, COASTLINE_MIN_EXTENT, COASTLINE_MIN_EXTENT_GLOBAL, COASTLINE_STROKE_MIX, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, STACK_OVERLAP, STACK_RING_MAX, STACK_RING_GAP, FAN_STEP, ARC_CURVE_FRAC, decodeCache, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, inAlaska, inHawaii, FOREIGN_BORDER, US_NON_CONUS;
47194
48622
  var init_layout15 = __esm({
47195
48623
  "src/map/layout.ts"() {
47196
48624
  "use strict";
47197
48625
  init_color_utils();
48626
+ init_geo();
48627
+ init_colorize();
47198
48628
  init_colors();
47199
48629
  init_label_layout();
47200
48630
  init_legend_constants();
47201
48631
  init_title_constants();
48632
+ init_context_labels();
47202
48633
  FIT_PAD = 24;
47203
- RAMP_FLOOR = 15;
48634
+ RAMP_FLOOR2 = 15;
47204
48635
  R_DEFAULT = 6;
47205
48636
  R_MIN = 4;
47206
48637
  R_MAX = 22;
47207
48638
  W_MIN = 1.25;
47208
48639
  W_MAX = 8;
47209
- FONT = 11;
47210
- COLO_EPS = 1.5;
48640
+ FONT2 = 11;
48641
+ WORLD_LABEL_ANCHORS = {
48642
+ US: [-98.5, 39.5]
48643
+ // CONUS geographic centre (near Lebanon, Kansas)
48644
+ };
48645
+ MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48646
+ MAX_COLUMN_ROWS = 7;
48647
+ REGION_LABEL_HALO_RATIO = 4.5;
47211
48648
  LAND_TINT_LIGHT = 12;
47212
48649
  LAND_TINT_DARK = 24;
47213
48650
  TAG_TINT_LIGHT = 60;
47214
48651
  TAG_TINT_DARK = 68;
47215
- WATER_TINT_LIGHT = 13;
47216
- WATER_TINT_DARK = 14;
48652
+ WATER_TINT_LIGHT = 24;
48653
+ WATER_TINT_DARK = 24;
47217
48654
  RIVER_WIDTH = 1.3;
48655
+ COMPACT_WIDTH_PX = 480;
47218
48656
  RELIEF_MIN_AREA = 12;
47219
48657
  RELIEF_MIN_DIM = 2;
47220
- RELIEF_HATCH_SPACING = 3;
47221
- RELIEF_HATCH_WIDTH = 0.25;
47222
- RELIEF_HATCH_STRENGTH = 32;
48658
+ RELIEF_HATCH_SPACING = 1.5;
48659
+ RELIEF_HATCH_WIDTH = 0.2;
48660
+ RELIEF_HATCH_STRENGTH = 26;
48661
+ COASTLINE_RING_COUNT = 5;
48662
+ COASTLINE_D0 = 16e-4;
48663
+ COASTLINE_STEP = 28e-4;
48664
+ COASTLINE_THICKNESS = 14e-4;
48665
+ COASTLINE_OPACITY_NEAR = 0.5;
48666
+ COASTLINE_OPACITY_FAR = 0.1;
48667
+ COASTLINE_MIN_EXTENT = 6e-4;
48668
+ COASTLINE_MIN_EXTENT_GLOBAL = 6e-4;
48669
+ COASTLINE_STROKE_MIX = 32;
47223
48670
  FOREIGN_TINT_LIGHT = 30;
47224
48671
  FOREIGN_TINT_DARK = 62;
47225
48672
  MUTED_FOREIGN_LIGHT = 28;
47226
48673
  MUTED_FOREIGN_DARK = 16;
47227
48674
  COLO_R = 9;
47228
48675
  GOLDEN_ANGLE = 2.399963229728653;
48676
+ STACK_OVERLAP = 1;
48677
+ STACK_RING_MAX = 8;
48678
+ STACK_RING_GAP = 4;
47229
48679
  FAN_STEP = 16;
47230
48680
  ARC_CURVE_FRAC = 0.18;
48681
+ decodeCache = /* @__PURE__ */ new WeakMap();
47231
48682
  usConusProjection = () => geoConicEqualArea().parallels([29.5, 45.5]).rotate([96, 0]);
47232
48683
  alaskaProjection = () => geoConicEqualArea().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
47233
48684
  hawaiiProjection = () => geoMercator();
47234
48685
  INSET_STATES = /* @__PURE__ */ new Set(["US-AK", "US-HI"]);
48686
+ inAlaska = (lon, lat) => lat >= 51 && (lon <= -129 || lon >= 172);
48687
+ inHawaii = (lon, lat) => lat >= 18 && lat <= 23 && lon >= -161 && lon <= -154;
48688
+ FOREIGN_BORDER = {
48689
+ CA: [
48690
+ "US-AK",
48691
+ "US-WA",
48692
+ "US-ID",
48693
+ "US-MT",
48694
+ "US-ND",
48695
+ "US-MN",
48696
+ "US-MI",
48697
+ "US-NY",
48698
+ "US-VT",
48699
+ "US-NH",
48700
+ "US-ME"
48701
+ ],
48702
+ MX: ["US-CA", "US-AZ", "US-NM", "US-TX"]
48703
+ };
47235
48704
  US_NON_CONUS = /* @__PURE__ */ new Set([
47236
48705
  "US-AK",
47237
48706
  "US-HI",
@@ -47251,6 +48720,98 @@ __export(renderer_exports16, {
47251
48720
  renderMapForExport: () => renderMapForExport
47252
48721
  });
47253
48722
  import * as d3Selection18 from "d3-selection";
48723
+ function pointInRing2(px, py, ring) {
48724
+ let inside = false;
48725
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
48726
+ const [xi, yi] = ring[i];
48727
+ const [xj, yj] = ring[j];
48728
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
48729
+ inside = !inside;
48730
+ }
48731
+ return inside;
48732
+ }
48733
+ function ringToPath(ring) {
48734
+ let d = "";
48735
+ for (let i = 0; i < ring.length; i++)
48736
+ d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48737
+ return d + "Z";
48738
+ }
48739
+ function polylineToPath(pts) {
48740
+ let d = "";
48741
+ for (let i = 0; i < pts.length; i++)
48742
+ d += (i ? "L" : "M") + pts[i][0] + "," + pts[i][1];
48743
+ return d;
48744
+ }
48745
+ function ringToCoastPaths(ring, frame) {
48746
+ if (!frame) return [ringToPath(ring)];
48747
+ const n = ring.length;
48748
+ const eps = 0.75;
48749
+ const onL = (x) => Math.abs(x) <= eps;
48750
+ const onR = (x) => Math.abs(x - frame.w) <= eps;
48751
+ const onT = (y) => Math.abs(y) <= eps;
48752
+ const onB = (y) => Math.abs(y - frame.h) <= eps;
48753
+ const isFrameEdge = (a, b) => onL(a[0]) && onL(b[0]) || onR(a[0]) && onR(b[0]) || onT(a[1]) && onT(b[1]) || onB(a[1]) && onB(b[1]);
48754
+ let firstBreak = -1;
48755
+ for (let i = 0; i < n; i++)
48756
+ if (isFrameEdge(ring[i], ring[(i + 1) % n])) {
48757
+ firstBreak = i;
48758
+ break;
48759
+ }
48760
+ if (firstBreak === -1) return [ringToPath(ring)];
48761
+ const paths = [];
48762
+ let cur = [];
48763
+ const start = (firstBreak + 1) % n;
48764
+ for (let k = 0; k < n; k++) {
48765
+ const i = (start + k) % n;
48766
+ const a = ring[i];
48767
+ const b = ring[(i + 1) % n];
48768
+ if (isFrameEdge(a, b)) {
48769
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
48770
+ cur = [];
48771
+ continue;
48772
+ }
48773
+ if (cur.length === 0) cur.push(a);
48774
+ cur.push(b);
48775
+ }
48776
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
48777
+ return paths;
48778
+ }
48779
+ function coastlineOuterRings(regions, minExtent, frame) {
48780
+ const paths = [];
48781
+ for (const r of regions) {
48782
+ const rings = parsePathRings(r.d);
48783
+ for (let i = 0; i < rings.length; i++) {
48784
+ const ring = rings[i];
48785
+ if (ring.length < 3) continue;
48786
+ let minX = Infinity;
48787
+ let minY = Infinity;
48788
+ let maxX = -Infinity;
48789
+ let maxY = -Infinity;
48790
+ for (const [x, y] of ring) {
48791
+ if (x < minX) minX = x;
48792
+ if (x > maxX) maxX = x;
48793
+ if (y < minY) minY = y;
48794
+ if (y > maxY) maxY = y;
48795
+ }
48796
+ if (Math.max(maxX - minX, maxY - minY) < minExtent) continue;
48797
+ const [fx, fy] = ring[0];
48798
+ let depth = 0;
48799
+ for (let j = 0; j < rings.length; j++)
48800
+ if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48801
+ if (depth % 2 === 1) continue;
48802
+ paths.push(...ringToCoastPaths(ring, frame));
48803
+ }
48804
+ }
48805
+ return paths;
48806
+ }
48807
+ function appendWaterLines(g, outerRings, style, flatWater) {
48808
+ const d = outerRings.join(" ");
48809
+ const linesOuterFirst = [...style.lines].sort((a, b) => b.d - a.d);
48810
+ for (const line12 of linesOuterFirst) {
48811
+ g.append("path").attr("d", d).attr("stroke", style.color).attr("stroke-width", 2 * (line12.d + line12.thickness)).attr("stroke-opacity", line12.opacity).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
48812
+ g.append("path").attr("d", d).attr("stroke", flatWater).attr("stroke-width", 2 * line12.d).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
48813
+ }
48814
+ }
47254
48815
  function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims, activeGroupOverride) {
47255
48816
  d3Selection18.select(container).selectAll(":not([data-d3-tooltip])").remove();
47256
48817
  const width = exportDims?.width ?? container.clientWidth;
@@ -47263,6 +48824,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47263
48824
  {
47264
48825
  palette,
47265
48826
  isDark,
48827
+ // Export-only: forward the contain-fit request from mapExportDimensions so a
48828
+ // clamped/floored (off-aspect) export canvas letterboxes instead of
48829
+ // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48830
+ // keeps the global stretch-fill.
48831
+ preferContain: exportDims?.preferContain ?? false,
47266
48832
  ...activeGroupOverride !== void 0 && {
47267
48833
  activeGroup: activeGroupOverride
47268
48834
  }
@@ -47276,6 +48842,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47276
48842
  const gRegions = svg.append("g").attr("class", "dgmo-map-regions");
47277
48843
  const drawRegion = (g, r, strokeWidth) => {
47278
48844
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
48845
+ if (r.label) p.attr("data-region-name", r.label);
48846
+ if (r.id && r.id !== "lake") p.attr("data-iso", r.id);
48847
+ if (r.labelX !== void 0 && r.labelY !== void 0) {
48848
+ p.attr("data-label-x", r.labelX).attr("data-label-y", r.labelY);
48849
+ }
47279
48850
  if (r.layer !== "base") {
47280
48851
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
47281
48852
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -47305,28 +48876,112 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47305
48876
  const landClip = defs.append("clipPath").attr("id", landClipId);
47306
48877
  for (const r of layout.regions)
47307
48878
  if (r.id !== "lake") landClip.append("path").attr("d", r.d);
47308
- const gRelief = svg.append("g").attr("clip-path", `url(#${landClipId})`).append("g").attr("class", "dgmo-map-relief").attr("clip-path", `url(#${rangeClipId})`).attr("stroke", h.color).attr("stroke-width", h.width).attr("vector-effect", "non-scaling-stroke");
48879
+ const gRelief = svg.append("g").attr("clip-path", `url(#${landClipId})`).style("pointer-events", "none").append("g").attr("class", "dgmo-map-relief").attr("clip-path", `url(#${rangeClipId})`).attr("stroke", h.color).attr("stroke-width", h.width).attr("vector-effect", "non-scaling-stroke");
47309
48880
  for (let y = h.spacing; y < height; y += h.spacing) {
47310
48881
  gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
47311
48882
  }
47312
48883
  }
48884
+ if (layout.coastlineStyle) {
48885
+ const cs = layout.coastlineStyle;
48886
+ const maskId = "dgmo-map-water-mask";
48887
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48888
+ mask.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "white");
48889
+ const landD = layout.regions.filter((r) => r.id !== "lake").map((r) => r.d).join(" ");
48890
+ const lakeD = layout.regions.filter((r) => r.id === "lake").map((r) => r.d).join(" ");
48891
+ if (landD) mask.append("path").attr("d", landD).attr("fill", "black");
48892
+ if (lakeD) mask.append("path").attr("d", lakeD).attr("fill", "white");
48893
+ if (layout.insets.length) {
48894
+ const reach = Math.max(0, ...cs.lines.map((l) => l.d + l.thickness));
48895
+ for (const box of layout.insets) {
48896
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48897
+ mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
48898
+ }
48899
+ }
48900
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").style("pointer-events", "none").attr("mask", `url(#${maskId})`);
48901
+ appendWaterLines(
48902
+ gWater,
48903
+ // Pass the canvas frame so edges collinear with it (the antimeridian on a
48904
+ // world map, regional clipExtent cuts) don't get ringed as fake coast —
48905
+ // land runs cleanly to the render-area edge.
48906
+ coastlineOuterRings(layout.regions, cs.minExtent, {
48907
+ w: width,
48908
+ h: height
48909
+ }),
48910
+ cs,
48911
+ layout.background
48912
+ );
48913
+ const byStroke = /* @__PURE__ */ new Map();
48914
+ for (const r of layout.regions) {
48915
+ const arr = byStroke.get(r.stroke);
48916
+ if (arr) arr.push(r.d);
48917
+ else byStroke.set(r.stroke, [r.d]);
48918
+ }
48919
+ for (const [stroke2, ds] of byStroke)
48920
+ gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48921
+ }
47313
48922
  if (layout.rivers.length) {
47314
- const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
48923
+ const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none").style("pointer-events", "none");
47315
48924
  for (const r of layout.rivers) {
47316
48925
  gRivers.append("path").attr("d", r.d).attr("stroke", r.color).attr("stroke-width", r.width).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
47317
48926
  }
47318
48927
  }
47319
48928
  if (layout.insets.length) {
47320
48929
  const insetG = svg.append("g").attr("class", "dgmo-map-insets");
47321
- for (const box of layout.insets) {
48930
+ layout.insets.forEach((box, bi) => {
47322
48931
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
47323
48932
  insetG.append("path").attr("d", d).attr("fill", layout.background).attr("stroke", mix(palette.text, palette.bg, 55)).attr("stroke-width", 1).attr("stroke-linejoin", "round");
47324
- }
48933
+ if (box.contextLand) {
48934
+ const clipId = `dgmo-map-inset-clip-${bi}`;
48935
+ defs.append("clipPath").attr("id", clipId).append("path").attr("d", d);
48936
+ insetG.append("path").attr("d", box.contextLand.d).attr("fill", box.contextLand.fill).attr("clip-path", `url(#${clipId})`);
48937
+ }
48938
+ });
47325
48939
  for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
47326
- }
48940
+ if (layout.coastlineStyle) {
48941
+ const cs = layout.coastlineStyle;
48942
+ const maskId = "dgmo-map-inset-water-mask";
48943
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48944
+ for (const box of layout.insets) {
48945
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48946
+ mask.append("path").attr("d", d).attr("fill", "white");
48947
+ }
48948
+ layout.insets.forEach((box, bi) => {
48949
+ if (box.contextLand)
48950
+ mask.append("path").attr("d", box.contextLand.d).attr("fill", "black").attr("clip-path", `url(#dgmo-map-inset-clip-${bi})`);
48951
+ });
48952
+ for (const r of layout.insetRegions)
48953
+ if (r.id !== "lake")
48954
+ mask.append("path").attr("d", r.d).attr("fill", "black");
48955
+ for (const r of layout.insetRegions)
48956
+ if (r.id === "lake")
48957
+ mask.append("path").attr("d", r.d).attr("fill", "white");
48958
+ const clipId = "dgmo-map-inset-water-clip";
48959
+ const clip = defs.append("clipPath").attr("id", clipId);
48960
+ for (const box of layout.insets) {
48961
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48962
+ clip.append("path").attr("d", d);
48963
+ }
48964
+ const gInsetWater = insetG.append("g").attr("clip-path", `url(#${clipId})`).append("g").attr("class", "dgmo-map-inset-water-lines").attr("fill", "none").style("pointer-events", "none").attr("mask", `url(#${maskId})`);
48965
+ appendWaterLines(
48966
+ gInsetWater,
48967
+ coastlineOuterRings(layout.insetRegions, cs.minExtent),
48968
+ cs,
48969
+ layout.background
48970
+ );
48971
+ for (const r of layout.insetRegions)
48972
+ gInsetWater.append("path").attr("d", r.d).attr("stroke", r.stroke).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48973
+ }
48974
+ }
48975
+ const wireSync = (sel, lineNumber) => {
48976
+ if (lineNumber < 1) return;
48977
+ sel.attr("data-line-number", lineNumber);
48978
+ if (onClickItem)
48979
+ sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
48980
+ };
47327
48981
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
47328
48982
  layout.legs.forEach((leg, i) => {
47329
48983
  const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round");
48984
+ wireSync(p, leg.lineNumber);
47330
48985
  if (leg.arrow) {
47331
48986
  const id = `dgmo-map-arrow-${i}`;
47332
48987
  const s = arrowSize(leg.width);
@@ -47334,25 +48989,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47334
48989
  p.attr("marker-end", `url(#${id})`);
47335
48990
  }
47336
48991
  if (leg.label !== void 0 && leg.labelX !== void 0) {
47337
- emitText(
48992
+ const lt = emitText(
47338
48993
  gLegs,
47339
48994
  leg.labelX,
47340
48995
  leg.labelY ?? 0,
47341
48996
  leg.label,
47342
48997
  "middle",
47343
- palette.textMuted,
47344
- haloColor,
47345
- true,
48998
+ leg.labelColor ?? palette.textMuted,
48999
+ leg.labelHaloColor ?? haloColor,
49000
+ leg.labelHalo ?? true,
47346
49001
  LABEL_FONT - 1
47347
49002
  );
49003
+ wireSync(lt, leg.lineNumber);
47348
49004
  }
47349
49005
  });
49006
+ const gSpider = svg.append("g").attr("class", "dgmo-map-spider");
49007
+ for (const cl of layout.clusters) {
49008
+ if (!exportDims) {
49009
+ gSpider.append("circle").attr("cx", cl.cx).attr("cy", cl.cy).attr("r", cl.hitR).attr("fill", "transparent").attr("data-cluster-hit", cl.id).style("cursor", "pointer");
49010
+ }
49011
+ for (const leg of cl.legs) {
49012
+ gSpider.append("line").attr("x1", cl.cx).attr("y1", cl.cy).attr("x2", leg.x2).attr("y2", leg.y2).attr("stroke", leg.color).attr("stroke-width", 1).attr("data-cluster-deco", cl.id).style("pointer-events", "none");
49013
+ }
49014
+ gSpider.append("circle").attr("cx", cl.cx).attr("cy", cl.cy).attr("r", 2).attr("fill", mix(palette.textMuted, palette.bg, 40)).attr("data-cluster-deco", cl.id).style("pointer-events", "none");
49015
+ }
47350
49016
  const gPois = svg.append("g").attr("class", "dgmo-map-pois");
47351
49017
  for (const poi of layout.pois) {
47352
49018
  if (poi.isOrigin) {
47353
49019
  gPois.append("circle").attr("cx", poi.cx).attr("cy", poi.cy).attr("r", poi.r + 3).attr("fill", "none").attr("stroke", poi.stroke).attr("stroke-width", 1.5);
47354
49020
  }
47355
49021
  const c = gPois.append("circle").attr("cx", poi.cx).attr("cy", poi.cy).attr("r", poi.r).attr("fill", poi.fill).attr("stroke", poi.stroke).attr("stroke-width", 1).attr("data-line-number", poi.lineNumber).attr("data-poi", poi.id);
49022
+ if (poi.clusterId !== void 0)
49023
+ c.attr("data-cluster-member", poi.clusterId);
47356
49024
  if (poi.tags) {
47357
49025
  for (const [group, value] of Object.entries(poi.tags)) {
47358
49026
  c.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
@@ -47380,12 +49048,32 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47380
49048
  }
47381
49049
  const gLabels = svg.append("g").attr("class", "dgmo-map-labels");
47382
49050
  for (const lab of layout.labels) {
49051
+ if (lab.hidden) {
49052
+ if (exportDims) continue;
49053
+ emitText(
49054
+ gLabels,
49055
+ lab.x,
49056
+ lab.y,
49057
+ lab.text,
49058
+ lab.anchor,
49059
+ lab.color,
49060
+ lab.haloColor,
49061
+ lab.halo,
49062
+ LABEL_FONT,
49063
+ lab.italic,
49064
+ lab.letterSpacing
49065
+ ).attr("data-poi", lab.poiId ?? null).attr("data-poi-hidden", "").style("opacity", 0).style("pointer-events", "none");
49066
+ continue;
49067
+ }
47383
49068
  if (lab.leader) {
47384
49069
  const line12 = gLabels.append("line").attr("x1", lab.leader.x1).attr("y1", lab.leader.y1).attr("x2", lab.leader.x2).attr("y2", lab.leader.y2).attr(
47385
49070
  "stroke",
47386
49071
  lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
47387
49072
  ).attr("stroke-width", lab.leaderColor ? 1 : 0.75);
47388
49073
  if (lab.poiId !== void 0) line12.attr("data-poi", lab.poiId);
49074
+ if (lab.clusterMember !== void 0)
49075
+ line12.attr("data-cluster-member", lab.clusterMember);
49076
+ wireSync(line12, lab.lineNumber);
47389
49077
  }
47390
49078
  const t = emitText(
47391
49079
  gLabels,
@@ -47396,11 +49084,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47396
49084
  lab.color,
47397
49085
  lab.haloColor,
47398
49086
  lab.halo,
47399
- LABEL_FONT
49087
+ LABEL_FONT,
49088
+ lab.italic,
49089
+ lab.letterSpacing,
49090
+ lab.lines
47400
49091
  );
47401
49092
  if (lab.poiId !== void 0) {
47402
49093
  t.attr("data-poi", lab.poiId).style("cursor", "default");
47403
49094
  }
49095
+ if (lab.clusterMember !== void 0) {
49096
+ t.attr("data-cluster-member", lab.clusterMember);
49097
+ }
49098
+ wireSync(t, lab.lineNumber);
49099
+ }
49100
+ if (!exportDims && layout.clusters.length) {
49101
+ const gBadge = svg.append("g").attr("class", "dgmo-map-cluster-badges");
49102
+ for (const cl of layout.clusters) {
49103
+ const g = gBadge.append("g").attr("data-cluster", cl.id).style("opacity", 0).style("pointer-events", "none");
49104
+ const R = 9;
49105
+ g.append("circle").attr("cx", cl.cx).attr("cy", cl.cy).attr("r", R).attr("fill", mix(palette.textMuted, palette.bg, 35)).attr("stroke", palette.textMuted).attr("stroke-width", 1);
49106
+ g.append("circle").attr("cx", cl.cx).attr("cy", cl.cy).attr("r", R + 2.5).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1);
49107
+ emitText(
49108
+ g,
49109
+ cl.cx,
49110
+ cl.cy + 3,
49111
+ String(cl.count),
49112
+ "middle",
49113
+ palette.text,
49114
+ palette.bg,
49115
+ false,
49116
+ LABEL_FONT
49117
+ );
49118
+ }
47404
49119
  }
47405
49120
  if (layout.legend) {
47406
49121
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
@@ -47437,7 +49152,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47437
49152
  svg.append("text").attr("class", "dgmo-map-title").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).attr("fill", palette.text).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 4).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.title);
47438
49153
  }
47439
49154
  if (layout.subtitle) {
47440
- svg.append("text").attr("x", width / 2).attr("y", TITLE_Y + TITLE_FONT_SIZE).attr("text-anchor", "middle").attr("font-size", LABEL_FONT + 1).attr("fill", palette.textMuted).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.subtitle);
49155
+ svg.append("text").attr("class", "dgmo-map-subtitle").attr("x", width / 2).attr("y", TITLE_Y + TITLE_FONT_SIZE).attr("text-anchor", "middle").attr("font-size", LABEL_FONT + 1).attr("fill", palette.textMuted).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.subtitle);
47441
49156
  }
47442
49157
  if (layout.caption) {
47443
49158
  svg.append("text").attr("x", width / 2).attr("y", height - 8).attr("text-anchor", "middle").attr("font-size", LABEL_FONT).attr("fill", palette.textMuted).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.caption);
@@ -47446,10 +49161,21 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47446
49161
  function renderMapForExport(container, resolved, data, palette, isDark, exportDims) {
47447
49162
  renderMap(container, resolved, data, palette, isDark, void 0, exportDims);
47448
49163
  }
47449
- function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize) {
47450
- const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color).text(text);
49164
+ function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize, italic, letterSpacing, lines) {
49165
+ const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color);
49166
+ if (lines && lines.length > 1) {
49167
+ const lineHeight = fontSize + 2;
49168
+ const startDy = -((lines.length - 1) / 2) * lineHeight;
49169
+ lines.forEach((ln, i) => {
49170
+ t.append("tspan").attr("x", x).attr("dy", i === 0 ? startDy : lineHeight).text(ln);
49171
+ });
49172
+ } else {
49173
+ t.text(text);
49174
+ }
49175
+ if (italic) t.attr("font-style", "italic");
49176
+ if (letterSpacing) t.attr("letter-spacing", letterSpacing);
47451
49177
  if (withHalo) {
47452
- t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7);
49178
+ t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 2).attr("stroke-linejoin", "round").attr("stroke-linecap", "round").attr("stroke-opacity", 0.55);
47453
49179
  }
47454
49180
  return t;
47455
49181
  }
@@ -47466,6 +49192,56 @@ var init_renderer16 = __esm({
47466
49192
  }
47467
49193
  });
47468
49194
 
49195
+ // src/map/dimensions.ts
49196
+ var dimensions_exports = {};
49197
+ __export(dimensions_exports, {
49198
+ mapContentAspect: () => mapContentAspect,
49199
+ mapExportDimensions: () => mapExportDimensions
49200
+ });
49201
+ import { geoPath as geoPath2 } from "d3-geo";
49202
+ function mapContentAspect(resolved, data, ref = REF) {
49203
+ const { projection, fitTarget } = buildMapProjection(resolved, data);
49204
+ projection.fitSize([ref, ref], fitTarget);
49205
+ const b = geoPath2(projection).bounds(fitTarget);
49206
+ const w = b[1][0] - b[0][0];
49207
+ const h = b[1][1] - b[0][1];
49208
+ const aspect = w / h;
49209
+ return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49210
+ }
49211
+ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49212
+ const raw = mapContentAspect(resolved, data);
49213
+ const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49214
+ const width = baseWidth;
49215
+ let height = Math.round(width / clamped);
49216
+ let chromeReserve = 0;
49217
+ if (resolved.title && resolved.pois.length > 0) {
49218
+ const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
49219
+ chromeReserve += Math.max(FIT_PAD2, bannerBottom + TITLE_GAP) - FIT_PAD2;
49220
+ }
49221
+ let floored = false;
49222
+ if (height - chromeReserve < MIN_MAP_BAND) {
49223
+ height = Math.round(chromeReserve + MIN_MAP_BAND);
49224
+ floored = true;
49225
+ }
49226
+ const preferContain = clamped !== raw || floored;
49227
+ return { width, height, preferContain };
49228
+ }
49229
+ var FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
49230
+ var init_dimensions = __esm({
49231
+ "src/map/dimensions.ts"() {
49232
+ "use strict";
49233
+ init_title_constants();
49234
+ init_layout15();
49235
+ FIT_PAD2 = 24;
49236
+ TITLE_GAP = 16;
49237
+ ASPECT_MAX = 3;
49238
+ ASPECT_MIN = 0.9;
49239
+ MIN_MAP_BAND = 200;
49240
+ FALLBACK_ASPECT = 1.5;
49241
+ REF = 1e3;
49242
+ }
49243
+ });
49244
+
47469
49245
  // src/map/load-data.ts
47470
49246
  var load_data_exports = {};
47471
49247
  __export(load_data_exports, {
@@ -47524,12 +49300,17 @@ function loadMapData() {
47524
49300
  mountainRanges,
47525
49301
  naLand,
47526
49302
  naLakes,
49303
+ waterBodies,
47527
49304
  gazetteer
47528
49305
  ] = await Promise.all([
49306
+ // worldCoarse (110m) is LOAD-BEARING but NOT a render source: the world
49307
+ // basemap renders from worldDetail (50m) at all scales (resolver pins
49308
+ // basemaps.world = 'detail'). Coarse stays as the authoritative region
49309
+ // name index + dominant-landmass bbox source in resolver.ts. Do not drop it.
47529
49310
  readJson(nb, dir, FILES.worldCoarse),
47530
49311
  readJson(nb, dir, FILES.worldDetail),
47531
49312
  readJson(nb, dir, FILES.usStates),
47532
- // Lakes/rivers/mountain/NA assets are optional — older bundles may predate them.
49313
+ // Lakes/rivers/mountain/NA/water assets are optional — older bundles may predate them.
47533
49314
  readJson(nb, dir, FILES.lakes).catch(() => void 0),
47534
49315
  readJson(nb, dir, FILES.rivers).catch(() => void 0),
47535
49316
  readJson(nb, dir, FILES.mountainRanges).catch(
@@ -47537,6 +49318,7 @@ function loadMapData() {
47537
49318
  ),
47538
49319
  readJson(nb, dir, FILES.naLand).catch(() => void 0),
47539
49320
  readJson(nb, dir, FILES.naLakes).catch(() => void 0),
49321
+ readJson(nb, dir, FILES.waterBodies).catch(() => void 0),
47540
49322
  readJson(nb, dir, FILES.gazetteer)
47541
49323
  ]);
47542
49324
  return validate({
@@ -47548,7 +49330,8 @@ function loadMapData() {
47548
49330
  ...rivers && { rivers },
47549
49331
  ...mountainRanges && { mountainRanges },
47550
49332
  ...naLand && { naLand },
47551
- ...naLakes && { naLakes }
49333
+ ...naLakes && { naLakes },
49334
+ ...waterBodies && { waterBodies }
47552
49335
  });
47553
49336
  })().catch((e) => {
47554
49337
  cache = void 0;
@@ -47569,6 +49352,7 @@ var init_load_data = __esm({
47569
49352
  mountainRanges: "mountain-ranges.json",
47570
49353
  naLand: "na-land.json",
47571
49354
  naLakes: "na-lakes.json",
49355
+ waterBodies: "water-bodies.json",
47572
49356
  gazetteer: "gazetteer.json"
47573
49357
  };
47574
49358
  CANDIDATE_DIRS = [
@@ -49582,8 +51366,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
49582
51366
  const lines = splitParticipantLabel(p.label, LABEL_MAX_CHARS);
49583
51367
  if (lines.length === 0) continue;
49584
51368
  const widest = Math.max(...lines.map((l) => l.length));
49585
- const labelWidth = widest * LABEL_CHAR_WIDTH + 10;
49586
- uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth);
51369
+ const labelWidth2 = widest * LABEL_CHAR_WIDTH + 10;
51370
+ uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth2);
49587
51371
  }
49588
51372
  uniformBoxWidth = Math.min(MAX_BOX_WIDTH, uniformBoxWidth);
49589
51373
  const effectiveGap = Math.max(PARTICIPANT_GAP, uniformBoxWidth + 30);
@@ -52282,15 +54066,15 @@ function renderArcDiagram(container, parsed, palette, _isDark, onClickItem, expo
52282
54066
  textColor,
52283
54067
  onClickItem
52284
54068
  );
52285
- const neighbors = /* @__PURE__ */ new Map();
52286
- for (const node of nodes) neighbors.set(node, /* @__PURE__ */ new Set());
54069
+ const neighbors2 = /* @__PURE__ */ new Map();
54070
+ for (const node of nodes) neighbors2.set(node, /* @__PURE__ */ new Set());
52287
54071
  for (const link of links) {
52288
- neighbors.get(link.source).add(link.target);
52289
- neighbors.get(link.target).add(link.source);
54072
+ neighbors2.get(link.source).add(link.target);
54073
+ neighbors2.get(link.target).add(link.source);
52290
54074
  }
52291
54075
  const FADE_OPACITY3 = 0.1;
52292
54076
  function handleMouseEnter(hovered) {
52293
- const connected = neighbors.get(hovered);
54077
+ const connected = neighbors2.get(hovered);
52294
54078
  g.selectAll(".arc-link").each(function() {
52295
54079
  const el = d3Selection23.select(this);
52296
54080
  const src = el.attr("data-source");
@@ -53198,10 +54982,12 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
53198
54982
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
53199
54983
  const eraLabelY = eraReserve ? -(topScaleH + markerReserve + ERA_ROW_H / 2) : 0;
53200
54984
  const innerWidth = width - margin.left - margin.right;
53201
- const innerHeight = height - margin.top - margin.bottom;
53202
- const rowH = Math.min(ctx.structural(28), innerHeight / sorted.length);
54985
+ const availInnerHeight = height - margin.top - margin.bottom;
54986
+ const rowH = Math.min(ctx.structural(28), availInnerHeight / sorted.length);
54987
+ const innerHeight = rowH * sorted.length;
54988
+ const usedHeight = margin.top + innerHeight + margin.bottom;
53203
54989
  const xScale = d3Scale2.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
53204
- const svg = d3Selection23.select(container).append("svg").attr("width", width).attr("height", height).attr("viewBox", `0 0 ${width} ${height}`).attr("preserveAspectRatio", "xMidYMin meet").style("background", bgColor);
54990
+ const svg = d3Selection23.select(container).append("svg").attr("width", width).attr("height", usedHeight).attr("viewBox", `0 0 ${width} ${usedHeight}`).attr("preserveAspectRatio", "xMidYMin meet").style("background", bgColor);
53205
54991
  if (ctx.isBelowFloor) {
53206
54992
  svg.attr("width", "100%");
53207
54993
  }
@@ -54225,7 +56011,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54225
56011
  8,
54226
56012
  Math.floor(OVERLAP_WRAP_TARGET_W / OVERLAP_CH_W)
54227
56013
  );
54228
- function wrapLabel2(text, maxChars) {
56014
+ function wrapLabel3(text, maxChars) {
54229
56015
  const words = text.split(/\s+/).filter(Boolean);
54230
56016
  const lines = [];
54231
56017
  let cur = "";
@@ -54271,7 +56057,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54271
56057
  if (!ov.label) continue;
54272
56058
  const idxs = ov.sets.map((s) => vennSets.findIndex((vs) => vs.name === s));
54273
56059
  if (idxs.some((idx) => idx < 0)) continue;
54274
- const lines = wrapLabel2(ov.label, MAX_WRAP_CHARS);
56060
+ const lines = wrapLabel3(ov.label, MAX_WRAP_CHARS);
54275
56061
  wrappedOverlapLabels.set(ov, lines);
54276
56062
  const dir = predictOverlapDirRaw(idxs);
54277
56063
  const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
@@ -55709,6 +57495,7 @@ async function renderForExport(content, theme, palette, viewState, options) {
55709
57495
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55710
57496
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55711
57497
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
57498
+ const { mapExportDimensions: mapExportDimensions2 } = await Promise.resolve().then(() => (init_dimensions(), dimensions_exports));
55712
57499
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55713
57500
  const mapParsed = parseMap2(content);
55714
57501
  let mapData = options?.mapData;
@@ -55721,14 +57508,15 @@ async function renderForExport(content, theme, palette, viewState, options) {
55721
57508
  }
55722
57509
  }
55723
57510
  const mapResolved = resolveMap2(mapParsed, mapData);
55724
- const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
57511
+ const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57512
+ const container2 = createExportContainer(dims2.width, dims2.height);
55725
57513
  renderMapForExport2(
55726
57514
  container2,
55727
57515
  mapResolved,
55728
57516
  mapData,
55729
57517
  effectivePalette2,
55730
57518
  theme === "dark",
55731
- { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
57519
+ dims2
55732
57520
  );
55733
57521
  return finalizeSvgExport(container2, theme, effectivePalette2);
55734
57522
  }
@@ -56570,7 +58358,8 @@ async function render(content, options) {
56570
58358
  ...options?.c4Container !== void 0 && {
56571
58359
  c4Container: options.c4Container
56572
58360
  },
56573
- ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup }
58361
+ ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup },
58362
+ ...options?.mapData !== void 0 && { mapData: options.mapData }
56574
58363
  });
56575
58364
  if (chartType === "map") {
56576
58365
  try {
@@ -56581,7 +58370,7 @@ async function render(content, options) {
56581
58370
  Promise.resolve().then(() => (init_load_data(), load_data_exports))
56582
58371
  ]
56583
58372
  );
56584
- const data = await loadMapData2();
58373
+ const data = options?.mapData ?? await loadMapData2();
56585
58374
  diagnostics = [...resolveMap2(parseMap2(content), data).diagnostics];
56586
58375
  } catch {
56587
58376
  }
@@ -56710,6 +58499,9 @@ var DIRECTIVE_KEYWORDS = /* @__PURE__ */ new Set([
56710
58499
  "hide",
56711
58500
  "mode",
56712
58501
  "direction",
58502
+ // Boxes-and-lines
58503
+ "box-metric",
58504
+ "show-values",
56713
58505
  // ER
56714
58506
  "notation",
56715
58507
  // Class
@@ -56749,22 +58541,20 @@ var DIRECTIVE_KEYWORDS = /* @__PURE__ */ new Set([
56749
58541
  // Sequence
56750
58542
  "activations",
56751
58543
  "no-activations",
56752
- // Map (§24B) directives
56753
- "region",
56754
- "projection",
58544
+ // Map (§24B) directives — cosmetics on by default, bare `no-*` opt-outs
56755
58545
  "region-metric",
56756
58546
  "poi-metric",
56757
58547
  "flow-metric",
56758
- "region-labels",
56759
- "poi-labels",
56760
- "default-country",
56761
- "default-state",
56762
- "no-legend",
56763
- "no-insets",
56764
- "muted",
56765
- "natural",
56766
- "subtitle",
58548
+ "locale",
58549
+ "active-tag",
56767
58550
  "caption",
58551
+ "no-legend",
58552
+ "no-coastline",
58553
+ "no-relief",
58554
+ "no-context-labels",
58555
+ "no-region-labels",
58556
+ "no-poi-labels",
58557
+ "no-colorize",
56768
58558
  "poi",
56769
58559
  "route",
56770
58560
  // Data charts
@@ -57057,7 +58847,11 @@ var ATTRIBUTE_KEYS = /* @__PURE__ */ new Set([
57057
58847
  "collapsed",
57058
58848
  "tech",
57059
58849
  "span",
57060
- "split"
58850
+ "split",
58851
+ // Map (§24B) reserved keys
58852
+ "value",
58853
+ "label",
58854
+ "style"
57061
58855
  ]);
57062
58856
  function applyAttributeKeys(tokens) {
57063
58857
  for (let i = 0; i < tokens.length - 1; i++) {
@@ -57430,7 +59224,7 @@ pre.dgmo, code.language-dgmo, pre > code.language-dgmo,
57430
59224
 
57431
59225
  // src/auto/index.ts
57432
59226
  init_safe_href();
57433
- var VERSION = "0.21.1";
59227
+ var VERSION = "0.23.0";
57434
59228
  var DEFAULTS = {
57435
59229
  theme: "auto",
57436
59230
  palette: "nord",