@diagrammo/dgmo 0.21.0 → 0.22.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 (76) hide show
  1. package/README.md +16 -6
  2. package/dist/advanced.cjs +2521 -623
  3. package/dist/advanced.d.cts +917 -534
  4. package/dist/advanced.d.ts +917 -534
  5. package/dist/advanced.js +2516 -623
  6. package/dist/auto.cjs +2333 -608
  7. package/dist/auto.js +119 -119
  8. package/dist/auto.mjs +2335 -609
  9. package/dist/cli.cjs +168 -168
  10. package/dist/editor.cjs +13 -15
  11. package/dist/editor.js +13 -15
  12. package/dist/highlight.cjs +15 -12
  13. package/dist/highlight.js +15 -12
  14. package/dist/index.cjs +2317 -595
  15. package/dist/index.d.cts +4 -1
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.js +2319 -596
  18. package/dist/internal.cjs +2521 -623
  19. package/dist/internal.d.cts +917 -534
  20. package/dist/internal.d.ts +917 -534
  21. package/dist/internal.js +2516 -623
  22. package/dist/map-data/PROVENANCE.json +1 -1
  23. package/dist/map-data/mountain-ranges.json +1 -0
  24. package/dist/map-data/water-bodies.json +1 -0
  25. package/docs/language-reference.md +44 -31
  26. package/gallery/fixtures/map-categorical-world.dgmo +16 -0
  27. package/gallery/fixtures/map-categorical.dgmo +0 -1
  28. package/gallery/fixtures/map-choropleth.dgmo +0 -1
  29. package/gallery/fixtures/map-coastline.dgmo +7 -0
  30. package/gallery/fixtures/map-colorize.dgmo +11 -0
  31. package/gallery/fixtures/map-direct-color.dgmo +9 -0
  32. package/gallery/fixtures/map-reference-world.dgmo +11 -0
  33. package/gallery/fixtures/map-region-scope.dgmo +0 -3
  34. package/gallery/fixtures/map-route.dgmo +0 -1
  35. package/package.json +1 -1
  36. package/src/advanced.ts +26 -1
  37. package/src/boxes-and-lines/renderer.ts +39 -12
  38. package/src/cli.ts +1 -1
  39. package/src/completion.ts +32 -24
  40. package/src/cycle/renderer.ts +14 -1
  41. package/src/d3.ts +23 -11
  42. package/src/editor/highlight-api.ts +4 -0
  43. package/src/editor/keywords.ts +13 -15
  44. package/src/infra/renderer.ts +35 -7
  45. package/src/map/colorize.ts +54 -0
  46. package/src/map/context-labels.ts +429 -0
  47. package/src/map/data/PROVENANCE.json +1 -1
  48. package/src/map/data/mountain-ranges.json +1 -0
  49. package/src/map/data/types.ts +34 -0
  50. package/src/map/data/water-bodies.json +1 -0
  51. package/src/map/dimensions.ts +117 -0
  52. package/src/map/geo-query.ts +295 -0
  53. package/src/map/geo.ts +305 -2
  54. package/src/map/invert.ts +111 -0
  55. package/src/map/layout.ts +1504 -335
  56. package/src/map/load-data.ts +16 -2
  57. package/src/map/parser.ts +57 -111
  58. package/src/map/renderer.ts +556 -13
  59. package/src/map/resolved-types.ts +24 -2
  60. package/src/map/resolver.ts +237 -67
  61. package/src/map/types.ts +39 -23
  62. package/src/mindmap/renderer.ts +10 -1
  63. package/src/palettes/atlas.ts +77 -0
  64. package/src/palettes/blueprint.ts +73 -0
  65. package/src/palettes/color-utils.ts +58 -1
  66. package/src/palettes/index.ts +12 -3
  67. package/src/palettes/slate.ts +73 -0
  68. package/src/palettes/tidewater.ts +73 -0
  69. package/src/render.ts +8 -1
  70. package/src/tech-radar/renderer.ts +3 -0
  71. package/src/tech-radar/types.ts +3 -0
  72. package/src/utils/d3-types.ts +5 -0
  73. package/src/utils/legend-layout.ts +21 -4
  74. package/src/utils/legend-types.ts +7 -0
  75. package/src/utils/reserved-key-registry.ts +3 -0
  76. 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",
@@ -1823,77 +1826,266 @@ function getSegmentColors(palette, count) {
1823
1826
  (_, i) => hslToHex(Math.round((startHue + i * step) % 360), avgS, avgL)
1824
1827
  );
1825
1828
  }
1829
+ function politicalTints(palette, count, isDark) {
1830
+ if (count <= 0) return [];
1831
+ const base = isDark ? palette.surface : palette.bg;
1832
+ const c = palette.colors;
1833
+ const swatches = [
1834
+ .../* @__PURE__ */ new Set([
1835
+ c.green,
1836
+ c.yellow,
1837
+ c.orange,
1838
+ c.purple,
1839
+ c.red,
1840
+ c.teal,
1841
+ c.cyan,
1842
+ c.blue
1843
+ ])
1844
+ ];
1845
+ const bands = isDark ? POLITICAL_TINT_BANDS.dark : POLITICAL_TINT_BANDS.light;
1846
+ const out = [];
1847
+ for (const pct of bands) {
1848
+ if (out.length >= count) break;
1849
+ for (const s of swatches) out.push(mix(s, base, pct));
1850
+ }
1851
+ return out.slice(0, count);
1852
+ }
1853
+ var POLITICAL_TINT_BANDS;
1826
1854
  var init_color_utils = __esm({
1827
1855
  "src/palettes/color-utils.ts"() {
1828
1856
  "use strict";
1857
+ POLITICAL_TINT_BANDS = {
1858
+ light: [32, 48, 64, 80],
1859
+ dark: [44, 58, 72, 86]
1860
+ };
1829
1861
  }
1830
1862
  });
1831
1863
 
1832
- // src/palettes/bold.ts
1833
- var boldPalette;
1834
- var init_bold = __esm({
1835
- "src/palettes/bold.ts"() {
1864
+ // src/palettes/atlas.ts
1865
+ var atlasPalette;
1866
+ var init_atlas = __esm({
1867
+ "src/palettes/atlas.ts"() {
1836
1868
  "use strict";
1837
1869
  init_registry();
1838
- boldPalette = {
1839
- id: "bold",
1840
- name: "Bold",
1870
+ atlasPalette = {
1871
+ id: "atlas",
1872
+ name: "Atlas",
1841
1873
  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",
1874
+ bg: "#f3ead3",
1875
+ // warm manila / parchment
1876
+ surface: "#ece0c0",
1877
+ // deeper paper (cards, panels)
1878
+ overlay: "#e8dab8",
1879
+ // popovers, dropdowns
1880
+ border: "#bcaa86",
1881
+ // muted sepia rule line
1882
+ text: "#463a26",
1883
+ // aged sepia-brown ink
1884
+ textMuted: "#7a6a4f",
1885
+ // faded annotation ink
1886
+ textOnFillLight: "#f7f1de",
1887
+ // parchment (light text on dark fills)
1888
+ textOnFillDark: "#3a2e1c",
1889
+ // deep ink (dark text on light fills)
1890
+ primary: "#5b7a99",
1891
+ // pull-down map ocean (steel-blue)
1892
+ secondary: "#7e9a6f",
1893
+ // lowland sage / celadon
1894
+ accent: "#b07f7c",
1895
+ // dusty rose
1896
+ destructive: "#b25a45",
1897
+ // brick / terracotta
1854
1898
  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"
1899
+ red: "#bf6a52",
1900
+ // terracotta brick
1901
+ orange: "#cf9a5c",
1902
+ // map tan / ochre
1903
+ yellow: "#cdb35e",
1904
+ // straw / muted lemon
1905
+ green: "#7e9a6f",
1906
+ // sage / celadon lowland
1907
+ blue: "#5b7a99",
1908
+ // steel-blue ocean
1909
+ purple: "#9a7fa6",
1910
+ // dusty lilac / mauve
1911
+ teal: "#6fa094",
1912
+ // muted seafoam
1913
+ cyan: "#79a7b5",
1914
+ // shallow-water blue
1915
+ gray: "#8a7d68",
1916
+ // warm taupe
1917
+ black: "#463a26",
1918
+ // ink
1919
+ white: "#ece0c0"
1920
+ // paper
1866
1921
  }
1867
1922
  },
1868
1923
  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",
1924
+ bg: "#1e2a33",
1925
+ // deep map ocean (night globe)
1926
+ surface: "#27353f",
1927
+ // raised ocean
1928
+ overlay: "#2e3d48",
1929
+ // popovers, dropdowns
1930
+ border: "#3d4f5c",
1931
+ // depth-contour line
1932
+ text: "#e8dcc0",
1933
+ // parchment ink, inverted
1934
+ textMuted: "#a89a7d",
1935
+ // faded label
1936
+ textOnFillLight: "#f7f1de",
1937
+ // parchment
1938
+ textOnFillDark: "#1a242c",
1939
+ // deep ocean ink
1940
+ primary: "#7ba0bf",
1941
+ // brighter ocean
1942
+ secondary: "#9bb588",
1943
+ // sage, lifted
1944
+ accent: "#cf9a96",
1945
+ // dusty rose, lifted
1946
+ destructive: "#c9745c",
1947
+ // brick, lifted
1881
1948
  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"
1949
+ red: "#cf7a60",
1950
+ // terracotta
1951
+ orange: "#d9a96a",
1952
+ // tan / ochre
1953
+ yellow: "#d8c074",
1954
+ // straw
1955
+ green: "#9bb588",
1956
+ // sage lowland
1957
+ blue: "#7ba0bf",
1958
+ // ocean
1959
+ purple: "#b59ac0",
1960
+ // lilac / mauve
1961
+ teal: "#85b3a6",
1962
+ // seafoam
1963
+ cyan: "#92bccb",
1964
+ // shallow-water blue
1965
+ gray: "#9a8d76",
1966
+ // warm taupe
1967
+ black: "#27353f",
1968
+ // raised ocean
1969
+ white: "#e8dcc0"
1970
+ // parchment
1893
1971
  }
1894
1972
  }
1895
1973
  };
1896
- registerPalette(boldPalette);
1974
+ registerPalette(atlasPalette);
1975
+ }
1976
+ });
1977
+
1978
+ // src/palettes/blueprint.ts
1979
+ var blueprintPalette;
1980
+ var init_blueprint = __esm({
1981
+ "src/palettes/blueprint.ts"() {
1982
+ "use strict";
1983
+ init_registry();
1984
+ blueprintPalette = {
1985
+ id: "blueprint",
1986
+ name: "Blueprint",
1987
+ light: {
1988
+ bg: "#f4f8fb",
1989
+ // pale drafting white (faint cyan)
1990
+ surface: "#e6eef4",
1991
+ // drafting panel
1992
+ overlay: "#dde9f1",
1993
+ // popovers, dropdowns
1994
+ border: "#aac3d6",
1995
+ // pale blue grid line
1996
+ text: "#123a5e",
1997
+ // blueprint navy ink
1998
+ textMuted: "#4f7390",
1999
+ // faint draft note
2000
+ textOnFillLight: "#f4f8fb",
2001
+ // drafting white
2002
+ textOnFillDark: "#0c2f4d",
2003
+ // deep blueprint navy
2004
+ primary: "#1f5e8c",
2005
+ // blueprint blue
2006
+ secondary: "#5b7d96",
2007
+ // steel
2008
+ accent: "#b08a3e",
2009
+ // draftsman's ochre highlight
2010
+ destructive: "#c0504d",
2011
+ // correction red
2012
+ colors: {
2013
+ red: "#c25a4e",
2014
+ // correction red
2015
+ orange: "#c2823e",
2016
+ // ochre
2017
+ yellow: "#c2a843",
2018
+ // pencil gold
2019
+ green: "#4f8a6b",
2020
+ // drafting green
2021
+ blue: "#1f5e8c",
2022
+ // blueprint blue
2023
+ purple: "#6f5e96",
2024
+ // indigo pencil
2025
+ teal: "#3a8a8a",
2026
+ // teal
2027
+ cyan: "#3f8fb5",
2028
+ // cyan
2029
+ gray: "#7e8e98",
2030
+ // graphite
2031
+ black: "#123a5e",
2032
+ // navy ink
2033
+ white: "#e6eef4"
2034
+ // panel
2035
+ }
2036
+ },
2037
+ dark: {
2038
+ bg: "#103a5e",
2039
+ // deep blueprint blue (cyanotype ground)
2040
+ surface: "#16466e",
2041
+ // raised sheet
2042
+ overlay: "#1c5180",
2043
+ // popovers, dropdowns
2044
+ border: "#3a6f96",
2045
+ // grid line
2046
+ text: "#eaf2f8",
2047
+ // chalk white
2048
+ textMuted: "#9fc0d6",
2049
+ // faint chalk note
2050
+ textOnFillLight: "#eaf2f8",
2051
+ // chalk white
2052
+ textOnFillDark: "#0c2f4d",
2053
+ // deep blueprint navy
2054
+ primary: "#7fb8d8",
2055
+ // chalk cyan
2056
+ secondary: "#9fb8c8",
2057
+ // pale steel
2058
+ accent: "#d8c27a",
2059
+ // chalk amber
2060
+ destructive: "#e08a7a",
2061
+ // chalk correction red
2062
+ colors: {
2063
+ red: "#e0907e",
2064
+ // chalk red
2065
+ orange: "#e0ab78",
2066
+ // chalk amber
2067
+ yellow: "#e3d089",
2068
+ // chalk gold
2069
+ green: "#93c79e",
2070
+ // chalk green
2071
+ blue: "#8ec3e0",
2072
+ // chalk cyan-blue
2073
+ purple: "#b6a6d8",
2074
+ // chalk indigo
2075
+ teal: "#84c7c2",
2076
+ // chalk teal
2077
+ cyan: "#9fd6e0",
2078
+ // chalk cyan
2079
+ gray: "#aebecb",
2080
+ // chalk graphite
2081
+ black: "#16466e",
2082
+ // raised sheet
2083
+ white: "#eaf2f8"
2084
+ // chalk white
2085
+ }
2086
+ }
2087
+ };
2088
+ registerPalette(blueprintPalette);
1897
2089
  }
1898
2090
  });
1899
2091
 
@@ -2390,6 +2582,120 @@ var init_rose_pine = __esm({
2390
2582
  }
2391
2583
  });
2392
2584
 
2585
+ // src/palettes/slate.ts
2586
+ var slatePalette;
2587
+ var init_slate = __esm({
2588
+ "src/palettes/slate.ts"() {
2589
+ "use strict";
2590
+ init_registry();
2591
+ slatePalette = {
2592
+ id: "slate",
2593
+ name: "Slate",
2594
+ light: {
2595
+ bg: "#ffffff",
2596
+ // clean slide white
2597
+ surface: "#f3f5f8",
2598
+ // light cool-gray panel
2599
+ overlay: "#eaeef3",
2600
+ // popovers, dropdowns
2601
+ border: "#d4dae1",
2602
+ // hairline rule
2603
+ text: "#1f2933",
2604
+ // near-black slate (softer than pure black)
2605
+ textMuted: "#5b6672",
2606
+ // secondary label
2607
+ textOnFillLight: "#ffffff",
2608
+ // light text on dark fills
2609
+ textOnFillDark: "#1f2933",
2610
+ // dark text on light fills
2611
+ primary: "#3b6ea5",
2612
+ // confident corporate blue
2613
+ secondary: "#5b6672",
2614
+ // slate gray
2615
+ accent: "#3a9188",
2616
+ // muted teal accent
2617
+ destructive: "#c0504d",
2618
+ // brick red
2619
+ colors: {
2620
+ red: "#c0504d",
2621
+ // brick
2622
+ orange: "#cc7a33",
2623
+ // muted amber
2624
+ yellow: "#c9a227",
2625
+ // gold (not neon)
2626
+ green: "#5b9357",
2627
+ // forest / sage
2628
+ blue: "#3b6ea5",
2629
+ // corporate blue
2630
+ purple: "#7d5ba6",
2631
+ // muted violet
2632
+ teal: "#3a9188",
2633
+ // teal
2634
+ cyan: "#4f96c4",
2635
+ // steel cyan
2636
+ gray: "#7e8a97",
2637
+ // cool gray
2638
+ black: "#1f2933",
2639
+ // slate ink
2640
+ white: "#f3f5f8"
2641
+ // panel
2642
+ }
2643
+ },
2644
+ dark: {
2645
+ bg: "#161b22",
2646
+ // deep slate (keynote dark)
2647
+ surface: "#202833",
2648
+ // raised panel
2649
+ overlay: "#29323e",
2650
+ // popovers, dropdowns
2651
+ border: "#38424f",
2652
+ // divider
2653
+ text: "#e6eaef",
2654
+ // off-white
2655
+ textMuted: "#9aa5b1",
2656
+ // secondary label
2657
+ textOnFillLight: "#ffffff",
2658
+ // light text on dark fills
2659
+ textOnFillDark: "#161b22",
2660
+ // dark text on light fills
2661
+ primary: "#5b9bd5",
2662
+ // lifted corporate blue
2663
+ secondary: "#8593a3",
2664
+ // slate gray, lifted
2665
+ accent: "#45b3a3",
2666
+ // teal, lifted
2667
+ destructive: "#e07b6e",
2668
+ // brick, lifted
2669
+ colors: {
2670
+ red: "#e07b6e",
2671
+ // brick
2672
+ orange: "#e0975a",
2673
+ // amber
2674
+ yellow: "#d9bd5a",
2675
+ // gold
2676
+ green: "#74b56e",
2677
+ // forest / sage
2678
+ blue: "#5b9bd5",
2679
+ // corporate blue
2680
+ purple: "#a585c9",
2681
+ // violet
2682
+ teal: "#45b3a3",
2683
+ // teal
2684
+ cyan: "#62b0d9",
2685
+ // steel cyan
2686
+ gray: "#95a1ae",
2687
+ // cool gray
2688
+ black: "#202833",
2689
+ // raised panel
2690
+ white: "#e6eaef"
2691
+ // off-white
2692
+ }
2693
+ }
2694
+ };
2695
+ registerPalette(slatePalette);
2696
+ }
2697
+ });
2698
+
2393
2699
  // src/palettes/solarized.ts
2394
2700
  var solarizedPalette;
2395
2701
  var init_solarized = __esm({
@@ -2485,6 +2791,120 @@ var init_solarized = __esm({
2485
2791
  }
2486
2792
  });
2487
2793
 
2794
+ // src/palettes/tidewater.ts
2795
+ var tidewaterPalette;
2796
+ var init_tidewater = __esm({
2797
+ "src/palettes/tidewater.ts"() {
2798
+ "use strict";
2799
+ init_registry();
2800
+ tidewaterPalette = {
2801
+ id: "tidewater",
2802
+ name: "Tidewater",
2803
+ light: {
2804
+ bg: "#eceff0",
2805
+ // weathered sea-mist paper
2806
+ surface: "#e0e4e3",
2807
+ // worn deck panel
2808
+ overlay: "#dadfdf",
2809
+ // popovers, dropdowns
2810
+ border: "#a9b2b3",
2811
+ // muted slate rule
2812
+ text: "#18313f",
2813
+ // ship's-log navy ink
2814
+ textMuted: "#51636b",
2815
+ // faded log entry
2816
+ textOnFillLight: "#f3f5f3",
2817
+ // weathered white
2818
+ textOnFillDark: "#162c38",
2819
+ // deep navy
2820
+ primary: "#1f4e6b",
2821
+ // deep-sea navy
2822
+ secondary: "#b08a4f",
2823
+ // rope / manila tan
2824
+ accent: "#c69a3e",
2825
+ // brass
2826
+ destructive: "#c1433a",
2827
+ // signal-flag red
2828
+ colors: {
2829
+ red: "#c1433a",
2830
+ // signal-flag red
2831
+ orange: "#cc7a38",
2832
+ // weathered amber
2833
+ yellow: "#d6bf5a",
2834
+ // brass gold
2835
+ green: "#4f8a6b",
2836
+ // sea-glass green
2837
+ blue: "#1f4e6b",
2838
+ // deep-sea navy
2839
+ purple: "#6a5a8c",
2840
+ // twilight harbor
2841
+ teal: "#3d8c8c",
2842
+ // sea-glass teal
2843
+ cyan: "#4f9bb5",
2844
+ // shallow water
2845
+ gray: "#8a8d86",
2846
+ // driftwood gray
2847
+ black: "#18313f",
2848
+ // navy ink
2849
+ white: "#e0e4e3"
2850
+ // deck panel
2851
+ }
2852
+ },
2853
+ dark: {
2854
+ bg: "#0f2230",
2855
+ // night-harbor deep sea
2856
+ surface: "#16303f",
2857
+ // raised hull
2858
+ overlay: "#1d3a4a",
2859
+ // popovers, dropdowns
2860
+ border: "#2c4856",
2861
+ // rigging line
2862
+ text: "#e6ebe8",
2863
+ // weathered white
2864
+ textMuted: "#9aaab0",
2865
+ // faded label
2866
+ textOnFillLight: "#f3f5f3",
2867
+ // weathered white
2868
+ textOnFillDark: "#0f2230",
2869
+ // deep sea
2870
+ primary: "#4f9bc4",
2871
+ // lifted sea blue
2872
+ secondary: "#c9a46a",
2873
+ // rope tan, lifted
2874
+ accent: "#d9b25a",
2875
+ // brass, lifted
2876
+ destructive: "#e06a5e",
2877
+ // signal red, lifted
2878
+ colors: {
2879
+ red: "#e06a5e",
2880
+ // signal-flag red
2881
+ orange: "#df9a52",
2882
+ // amber
2883
+ yellow: "#e0c662",
2884
+ // brass gold
2885
+ green: "#6fb58c",
2886
+ // sea-glass green
2887
+ blue: "#4f9bc4",
2888
+ // sea blue
2889
+ purple: "#9486bf",
2890
+ // twilight harbor
2891
+ teal: "#5cb0ac",
2892
+ // sea-glass teal
2893
+ cyan: "#62b4cf",
2894
+ // shallow water
2895
+ gray: "#9aa39c",
2896
+ // driftwood gray
2897
+ black: "#16303f",
2898
+ // raised hull
2899
+ white: "#e6ebe8"
2900
+ // weathered white
2901
+ }
2902
+ }
2903
+ };
2904
+ registerPalette(tidewaterPalette);
2905
+ }
2906
+ });
2907
+
2488
2908
  // src/palettes/tokyo-night.ts
2489
2909
  var tokyoNightPalette;
2490
2910
  var init_tokyo_night = __esm({
@@ -2760,7 +3180,8 @@ var init_monokai = __esm({
2760
3180
  // src/palettes/index.ts
2761
3181
  var palettes_exports = {};
2762
3182
  __export(palettes_exports, {
2763
- boldPalette: () => boldPalette,
3183
+ atlasPalette: () => atlasPalette,
3184
+ blueprintPalette: () => blueprintPalette,
2764
3185
  catppuccinPalette: () => catppuccinPalette,
2765
3186
  contrastText: () => contrastText,
2766
3187
  draculaPalette: () => draculaPalette,
@@ -2781,7 +3202,9 @@ __export(palettes_exports, {
2781
3202
  rosePinePalette: () => rosePinePalette,
2782
3203
  shade: () => shade,
2783
3204
  shapeFill: () => shapeFill,
3205
+ slatePalette: () => slatePalette,
2784
3206
  solarizedPalette: () => solarizedPalette,
3207
+ tidewaterPalette: () => tidewaterPalette,
2785
3208
  tint: () => tint,
2786
3209
  tokyoNightPalette: () => tokyoNightPalette
2787
3210
  });
@@ -2791,17 +3214,21 @@ var init_palettes = __esm({
2791
3214
  "use strict";
2792
3215
  init_registry();
2793
3216
  init_color_utils();
2794
- init_bold();
3217
+ init_atlas();
3218
+ init_blueprint();
2795
3219
  init_catppuccin();
2796
3220
  init_gruvbox();
2797
3221
  init_nord();
2798
3222
  init_one_dark();
2799
3223
  init_rose_pine();
3224
+ init_slate();
2800
3225
  init_solarized();
3226
+ init_tidewater();
2801
3227
  init_tokyo_night();
2802
3228
  init_dracula();
2803
3229
  init_monokai();
2804
- init_bold();
3230
+ init_atlas();
3231
+ init_blueprint();
2805
3232
  init_catppuccin();
2806
3233
  init_dracula();
2807
3234
  init_gruvbox();
@@ -2809,9 +3236,15 @@ var init_palettes = __esm({
2809
3236
  init_nord();
2810
3237
  init_one_dark();
2811
3238
  init_rose_pine();
3239
+ init_slate();
2812
3240
  init_solarized();
3241
+ init_tidewater();
2813
3242
  init_tokyo_night();
2814
3243
  palettes = {
3244
+ atlas: atlasPalette,
3245
+ blueprint: blueprintPalette,
3246
+ slate: slatePalette,
3247
+ tidewater: tidewaterPalette,
2815
3248
  nord: nordPalette,
2816
3249
  catppuccin: catppuccinPalette,
2817
3250
  solarized: solarizedPalette,
@@ -2820,8 +3253,7 @@ var init_palettes = __esm({
2820
3253
  oneDark: oneDarkPalette,
2821
3254
  rosePine: rosePinePalette,
2822
3255
  dracula: draculaPalette,
2823
- monokai: monokaiPalette,
2824
- bold: boldPalette
3256
+ monokai: monokaiPalette
2825
3257
  };
2826
3258
  }
2827
3259
  });
@@ -3331,6 +3763,9 @@ function controlsGroupCapsuleWidth(toggles) {
3331
3763
  }
3332
3764
  return w;
3333
3765
  }
3766
+ function isAppHostedControls(config, isExport) {
3767
+ return !isExport && config.controlsHost === "app" && !!config.controlsGroup && config.controlsGroup.toggles.length > 0;
3768
+ }
3334
3769
  function buildControlsGroupLayout(config, state) {
3335
3770
  const cg = config.controlsGroup;
3336
3771
  if (!cg || cg.toggles.length === 0) return void 0;
@@ -3384,6 +3819,7 @@ function buildControlsGroupLayout(config, state) {
3384
3819
  function computeLegendLayout(config, state, containerWidth) {
3385
3820
  const { groups, controls: configControls, mode } = config;
3386
3821
  const isExport = mode === "export";
3822
+ const gated = isAppHostedControls(config, isExport);
3387
3823
  const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
3388
3824
  if (isExport && !activeGroupName) {
3389
3825
  return {
@@ -3394,7 +3830,7 @@ function computeLegendLayout(config, state, containerWidth) {
3394
3830
  pills: []
3395
3831
  };
3396
3832
  }
3397
- const controlsGroupLayout = isExport ? void 0 : buildControlsGroupLayout(config, state);
3833
+ const controlsGroupLayout = isExport || gated ? void 0 : buildControlsGroupLayout(config, state);
3398
3834
  const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0 || !!g.gradient);
3399
3835
  if (visibleGroups.length === 0 && (!configControls || configControls.length === 0) && !controlsGroupLayout) {
3400
3836
  return {
@@ -8296,8 +8732,8 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8296
8732
  const pt = points[i];
8297
8733
  const ptSize = pt.size ?? symbolSize;
8298
8734
  const minGap = ptSize / 2 + 4;
8299
- const labelWidth = pt.name.length * fontSize * 0.6 + 8;
8300
- const labelX = pt.px - labelWidth / 2;
8735
+ const labelWidth2 = pt.name.length * fontSize * 0.6 + 8;
8736
+ const labelX = pt.px - labelWidth2 / 2;
8301
8737
  let bestLabelY = 0;
8302
8738
  let bestOffset = Infinity;
8303
8739
  let placed = false;
@@ -8309,7 +8745,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8309
8745
  const candidate = {
8310
8746
  x: labelX,
8311
8747
  y: labelY,
8312
- w: labelWidth,
8748
+ w: labelWidth2,
8313
8749
  h: labelHeight
8314
8750
  };
8315
8751
  let collision = false;
@@ -8351,7 +8787,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8351
8787
  const labelRect = {
8352
8788
  x: labelX,
8353
8789
  y: bestLabelY,
8354
- w: labelWidth,
8790
+ w: labelWidth2,
8355
8791
  h: labelHeight
8356
8792
  };
8357
8793
  placedLabels.push(labelRect);
@@ -8387,7 +8823,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8387
8823
  shape: {
8388
8824
  x: labelX - bgPad,
8389
8825
  y: bestLabelY - bgPad,
8390
- width: labelWidth + bgPad * 2,
8826
+ width: labelWidth2 + bgPad * 2,
8391
8827
  height: labelHeight + bgPad * 2
8392
8828
  },
8393
8829
  style: { fill: bg },
@@ -15823,10 +16259,6 @@ function parseMap(content) {
15823
16259
  handleTag(trimmed, lineNumber);
15824
16260
  continue;
15825
16261
  }
15826
- if ((firstWord === "muted" || firstWord === "natural") && trimmed === firstWord) {
15827
- handleDirective(firstWord, "", lineNumber);
15828
- continue;
15829
- }
15830
16262
  if (DIRECTIVE_SET.has(firstWord) && !trimmed.slice(firstWord.length).trimStart().startsWith(":")) {
15831
16263
  handleDirective(
15832
16264
  firstWord,
@@ -15873,28 +16305,13 @@ function parseMap(content) {
15873
16305
  pushWarning(line12, `Duplicate directive "${key}" \u2014 last value wins.`);
15874
16306
  };
15875
16307
  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
- case "region-metric":
16308
+ case "region-metric": {
15895
16309
  dup(d.regionMetric);
15896
- d.regionMetric = value;
16310
+ const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
16311
+ d.regionMetric = rmLabel;
16312
+ if (rmColor) d.regionMetricColor = rmColor;
15897
16313
  break;
16314
+ }
15898
16315
  case "poi-metric":
15899
16316
  dup(d.poiMetric);
15900
16317
  d.poiMetric = value;
@@ -15903,85 +16320,43 @@ function parseMap(content) {
15903
16320
  dup(d.flowMetric);
15904
16321
  d.flowMetric = value;
15905
16322
  break;
15906
- case "scale":
15907
- dup(d.scale);
15908
- {
15909
- const s = parseScale(value, line12);
15910
- if (s) d.scale = s;
15911
- }
15912
- break;
15913
- case "region-labels":
15914
- dup(d.regionLabels);
15915
- if (value && !["full", "abbrev", "off"].includes(value))
15916
- pushWarning(
15917
- line12,
15918
- `Unknown region-labels "${value}" (expected full | abbrev | off).`
15919
- );
15920
- d.regionLabels = value;
15921
- break;
15922
- case "poi-labels":
15923
- dup(d.poiLabels);
15924
- if (value && !["off", "auto", "all"].includes(value))
15925
- pushWarning(
15926
- line12,
15927
- `Unknown poi-labels "${value}" (expected off | auto | all).`
15928
- );
15929
- d.poiLabels = value;
15930
- break;
15931
- case "default-country":
15932
- dup(d.defaultCountry);
15933
- d.defaultCountry = value;
15934
- break;
15935
- case "default-state":
15936
- dup(d.defaultState);
15937
- d.defaultState = value;
16323
+ case "locale":
16324
+ dup(d.locale);
16325
+ d.locale = value;
15938
16326
  break;
15939
16327
  case "active-tag":
15940
16328
  dup(d.activeTag);
15941
16329
  d.activeTag = value;
15942
16330
  break;
16331
+ case "caption":
16332
+ dup(d.caption);
16333
+ d.caption = value;
16334
+ break;
16335
+ // ── Cosmetic `no-*` opt-outs: bare flags, idempotent (mirror `no-legend`,
16336
+ // no dup warning); each defaults the feature ON when absent. ──
15943
16337
  case "no-legend":
15944
16338
  d.noLegend = true;
15945
16339
  break;
15946
- case "muted":
15947
- case "natural":
15948
- if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
15949
- pushWarning(
15950
- line12,
15951
- `Conflicting basemap dress \u2014 "${d.basemapStyle}" then "${key}"; last wins.`
15952
- );
15953
- d.basemapStyle = key;
16340
+ case "no-coastline":
16341
+ d.noCoastline = true;
15954
16342
  break;
15955
- case "subtitle":
15956
- dup(d.subtitle);
15957
- d.subtitle = value;
16343
+ case "no-relief":
16344
+ d.noRelief = true;
15958
16345
  break;
15959
- case "caption":
15960
- dup(d.caption);
15961
- d.caption = value;
16346
+ case "no-context-labels":
16347
+ d.noContextLabels = true;
16348
+ break;
16349
+ case "no-region-labels":
16350
+ d.noRegionLabels = true;
16351
+ break;
16352
+ case "no-poi-labels":
16353
+ d.noPoiLabels = true;
16354
+ break;
16355
+ case "no-colorize":
16356
+ d.noColorize = true;
15962
16357
  break;
15963
16358
  }
15964
16359
  }
15965
- function parseScale(value, line12) {
15966
- const toks = value.split(/\s+/).filter(Boolean);
15967
- const min = Number(toks[0]);
15968
- const max = Number(toks[1]);
15969
- if (!Number.isFinite(min) || !Number.isFinite(max)) {
15970
- pushError(line12, `scale requires numeric <min> <max> (got "${value}").`);
15971
- return null;
15972
- }
15973
- const scale = { min, max };
15974
- if (toks[2] === "center") {
15975
- const c = Number(toks[3]);
15976
- if (Number.isFinite(c)) scale.center = c;
15977
- else
15978
- pushError(
15979
- line12,
15980
- `scale center requires a number (got "${toks[3] ?? ""}").`
15981
- );
15982
- }
15983
- return scale;
15984
- }
15985
16360
  function handleTag(trimmed, line12) {
15986
16361
  const m = matchTagBlockHeading(trimmed);
15987
16362
  if (!m) {
@@ -16055,6 +16430,7 @@ function parseMap(content) {
16055
16430
  };
16056
16431
  if (regionScope !== void 0) region.scope = regionScope;
16057
16432
  if (valueNum !== void 0) region.value = valueNum;
16433
+ if (split.color) region.color = split.color;
16058
16434
  regions.push(region);
16059
16435
  }
16060
16436
  function handlePoi(rest, line12, indent) {
@@ -16079,6 +16455,7 @@ function parseMap(content) {
16079
16455
  const poi = { pos, tags, meta, lineNumber: line12 };
16080
16456
  if (split.alias) poi.alias = split.alias;
16081
16457
  if (label !== void 0) poi.label = label;
16458
+ if (split.color) poi.color = split.color;
16082
16459
  pois.push(poi);
16083
16460
  open.poi = { poi, indent };
16084
16461
  }
@@ -16179,13 +16556,15 @@ function parseMap(content) {
16179
16556
  pushError(line12, `Edge has an empty endpoint: "${trimmed}".`);
16180
16557
  continue;
16181
16558
  }
16182
- const meta = k === links.length - 1 ? lastSplit.meta : {};
16559
+ const isLast = k === links.length - 1;
16560
+ const meta = isLast ? lastSplit.meta : {};
16561
+ const style = links[k].style === "arc" ? "arc" : "straight";
16183
16562
  edges.push({
16184
16563
  from,
16185
16564
  to,
16186
16565
  ...links[k].label !== void 0 && { label: links[k].label },
16187
16566
  directed: links[k].directed,
16188
- style: links[k].style,
16567
+ style,
16189
16568
  meta,
16190
16569
  lineNumber: line12
16191
16570
  });
@@ -16271,20 +16650,19 @@ var init_parser12 = __esm({
16271
16650
  LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
16272
16651
  AT_RE = /(^|[\s,])at\s*:/i;
16273
16652
  DIRECTIVE_SET = /* @__PURE__ */ new Set([
16274
- "region",
16275
- "projection",
16276
16653
  "region-metric",
16277
16654
  "poi-metric",
16278
16655
  "flow-metric",
16279
- "scale",
16280
- "region-labels",
16281
- "poi-labels",
16282
- "default-country",
16283
- "default-state",
16656
+ "locale",
16284
16657
  "active-tag",
16658
+ "caption",
16285
16659
  "no-legend",
16286
- "subtitle",
16287
- "caption"
16660
+ "no-coastline",
16661
+ "no-relief",
16662
+ "no-context-labels",
16663
+ "no-region-labels",
16664
+ "no-poi-labels",
16665
+ "no-colorize"
16288
16666
  ]);
16289
16667
  }
16290
16668
  });
@@ -24206,8 +24584,8 @@ function renderKanban(container, parsed, palette, isDark, options) {
24206
24584
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24207
24585
  for (const meta of tagMeta) {
24208
24586
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(`${meta.label}: `);
24209
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24210
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24587
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24588
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24211
24589
  metaY += sCardMetaLineHeight;
24212
24590
  }
24213
24591
  for (const detail of card.details) {
@@ -24551,8 +24929,8 @@ function renderSwimlaneCard(parent, cardLayout, tagGroups, activeTagGroup, palet
24551
24929
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24552
24930
  for (const meta of tagMeta) {
24553
24931
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", palette.textMuted).text(`${meta.label}: `);
24554
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24555
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24932
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24933
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24556
24934
  metaY += sCardMetaLineHeight;
24557
24935
  }
24558
24936
  for (const detail of card.details) {
@@ -25386,8 +25764,8 @@ function classifyEREntities(tables, relationships) {
25386
25764
  }
25387
25765
  }
25388
25766
  const mmParticipants = /* @__PURE__ */ new Set();
25389
- for (const [id, neighbors] of tableStarNeighbors) {
25390
- if (neighbors.size >= 2) mmParticipants.add(id);
25767
+ for (const [id, neighbors2] of tableStarNeighbors) {
25768
+ if (neighbors2.size >= 2) mmParticipants.add(id);
25391
25769
  }
25392
25770
  const indegreeValues = Object.values(indegreeMap);
25393
25771
  const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
@@ -26060,7 +26438,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26060
26438
  controlsExpanded,
26061
26439
  onToggleDescriptions,
26062
26440
  onToggleControlsExpand,
26063
- exportMode = false
26441
+ exportMode = false,
26442
+ controlsHost
26064
26443
  } = options ?? {};
26065
26444
  d3Selection6.select(container).selectAll(":not([data-d3-tooltip])").remove();
26066
26445
  const width = exportDims?.width ?? container.clientWidth;
@@ -26078,7 +26457,11 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26078
26457
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26079
26458
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26080
26459
  const sTitleY = sctx.structural(TITLE_Y);
26081
- const sLegendHeight = sctx.structural(
26460
+ const reserveHasDescriptions = parsed.nodes.some(
26461
+ (n) => n.description && n.description.length > 0
26462
+ );
26463
+ const willRenderLegend = parsed.tagGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26464
+ const sLegendHeight = willRenderLegend ? sctx.structural(
26082
26465
  getMaxLegendReservedHeight(
26083
26466
  {
26084
26467
  groups: parsed.tagGroups,
@@ -26087,7 +26470,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26087
26470
  },
26088
26471
  width
26089
26472
  )
26090
- );
26473
+ ) : 0;
26091
26474
  const activeGroup = resolveActiveTagGroup(
26092
26475
  parsed.tagGroups,
26093
26476
  parsed.options["active-tag"],
@@ -26402,10 +26785,10 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26402
26785
  const hasDescriptions = parsed.nodes.some(
26403
26786
  (n) => n.description && n.description.length > 0
26404
26787
  );
26405
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions;
26788
+ const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26406
26789
  if (hasLegend) {
26407
26790
  let controlsGroup;
26408
- if (hasDescriptions && onToggleDescriptions) {
26791
+ if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
26409
26792
  controlsGroup = {
26410
26793
  toggles: [
26411
26794
  {
@@ -26423,7 +26806,14 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26423
26806
  groups: parsed.tagGroups,
26424
26807
  position: { placement: "top-center", titleRelation: "below-title" },
26425
26808
  mode: exportMode ? "export" : "preview",
26426
- ...controlsGroup !== void 0 && { controlsGroup }
26809
+ // Keep inactive sibling tag groups visible as collapsed pills so the user
26810
+ // can click one to flip the active colouring dimension (preview only —
26811
+ // export shows just the active group). Without this, declaring a second
26812
+ // tag group (e.g. Team) leaves it invisible whenever another group is
26813
+ // active. The app's BoxesAndLinesPreview already wires pill clicks.
26814
+ showInactivePills: true,
26815
+ ...controlsGroup !== void 0 && { controlsGroup },
26816
+ ...controlsHost !== void 0 && { controlsHost }
26427
26817
  };
26428
26818
  const legendState = {
26429
26819
  activeGroup,
@@ -27670,8 +28060,9 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27670
28060
  const containerHeight = exportDims?.height ?? (container.getBoundingClientRect().height || 600);
27671
28061
  d3Selection7.select(container).selectAll("*").remove();
27672
28062
  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);
28063
+ const appHosted = options?.controlsHost === "app";
27673
28064
  const hasControls = !!options?.onToggleColorByDepth || !!options?.onToggleDescriptions;
27674
- const hasLegend = parsed.tagGroups.length > 0 || hasControls;
28065
+ const hasLegend = parsed.tagGroups.length > 0 || hasControls && !appHosted;
27675
28066
  const fixedLegend = !isExport && hasLegend;
27676
28067
  const legendReserve = fixedLegend ? getMaxLegendReservedHeight(
27677
28068
  {
@@ -27765,7 +28156,10 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27765
28156
  }),
27766
28157
  position: { placement: "top-center", titleRelation: "below-title" },
27767
28158
  mode: options?.exportMode ? "export" : "preview",
27768
- ...controlsToggles !== void 0 && { controlsGroup: controlsToggles }
28159
+ ...controlsToggles !== void 0 && { controlsGroup: controlsToggles },
28160
+ ...options?.controlsHost !== void 0 && {
28161
+ controlsHost: options.controlsHost
28162
+ }
27769
28163
  };
27770
28164
  const legendState = {
27771
28165
  activeGroup: options?.colorByDepth ? null : activeTagGroup !== void 0 ? activeTagGroup : parsed.options["active-tag"] ?? null,
@@ -28208,8 +28602,8 @@ function computeFieldAlignX(children) {
28208
28602
  for (const child of children) {
28209
28603
  if (child.metadata["_labelField"] === "true" && child.children.length >= 2) {
28210
28604
  const labelEl = child.children[0];
28211
- const labelWidth = labelEl.label.length * CHAR_WIDTH5;
28212
- maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
28605
+ const labelWidth2 = labelEl.label.length * CHAR_WIDTH5;
28606
+ maxLabelWidth = Math.max(maxLabelWidth, labelWidth2);
28213
28607
  labelFieldCount++;
28214
28608
  }
28215
28609
  }
@@ -33174,7 +33568,7 @@ function hasRoles(node) {
33174
33568
  function computeNodeWidth2(node, expanded, options) {
33175
33569
  const badgeVal = node.computedConcurrentInvocations === 0 && node.computedInstances > 1 ? node.computedInstances : 0;
33176
33570
  const badgeLen = badgeVal > 0 ? `${badgeVal}x`.length + 2 : 0;
33177
- const labelWidth = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33571
+ const labelWidth2 = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33178
33572
  const allKeys = [];
33179
33573
  if (node.computedRps > 0) allKeys.push("RPS");
33180
33574
  if (expanded) {
@@ -33218,7 +33612,7 @@ function computeNodeWidth2(node, expanded, options) {
33218
33612
  allKeys.push("overflow");
33219
33613
  }
33220
33614
  }
33221
- if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth);
33615
+ if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth2);
33222
33616
  const maxKeyLen = Math.max(...allKeys.map((k) => k.length));
33223
33617
  let maxRowWidth = 0;
33224
33618
  if (node.computedRps > 0) {
@@ -33306,7 +33700,7 @@ function computeNodeWidth2(node, expanded, options) {
33306
33700
  truncated.length * META_CHAR_WIDTH3 + PADDING_X3
33307
33701
  );
33308
33702
  }
33309
- return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20, descWidth);
33703
+ return Math.max(MIN_NODE_WIDTH2, labelWidth2, maxRowWidth + 20, descWidth);
33310
33704
  }
33311
33705
  function computeNodeHeight2(node, expanded, options) {
33312
33706
  const propCount = countDisplayProps(node, expanded, options);
@@ -34856,8 +35250,9 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
34856
35250
  }
34857
35251
  return groups;
34858
35252
  }
34859
- function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false) {
35253
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false, controlsHost) {
34860
35254
  if (legendGroups.length === 0 && !playback) return;
35255
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34861
35256
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
34862
35257
  if (activeGroup) {
34863
35258
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
@@ -34866,14 +35261,29 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34866
35261
  name: g.name,
34867
35262
  entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
34868
35263
  }));
34869
- if (playback) {
35264
+ if (playback && !appHostedPlayback) {
34870
35265
  allGroups.push({ name: "Playback", entries: [] });
34871
35266
  }
34872
35267
  const legendConfig = {
34873
35268
  groups: allGroups,
34874
35269
  position: { placement: "top-center", titleRelation: "below-title" },
34875
35270
  mode: exportMode ? "export" : "preview",
34876
- showEmptyGroups: true
35271
+ showEmptyGroups: true,
35272
+ ...appHostedPlayback && {
35273
+ controlsHost: "app",
35274
+ controlsGroup: {
35275
+ toggles: [
35276
+ {
35277
+ id: "playback",
35278
+ type: "toggle",
35279
+ label: "Playback",
35280
+ active: true,
35281
+ onToggle: () => {
35282
+ }
35283
+ }
35284
+ ]
35285
+ }
35286
+ }
34877
35287
  };
34878
35288
  const legendState = { activeGroup };
34879
35289
  renderLegendD3(
@@ -34924,8 +35334,9 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34924
35334
  }
34925
35335
  }
34926
35336
  }
34927
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
35337
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes, controlsHost) {
34928
35338
  d3Selection11.select(container).selectAll(":not([data-d3-tooltip])").remove();
35339
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34929
35340
  const ctx = ScaleContext.identity();
34930
35341
  const sc = buildScaledConstants(ctx);
34931
35342
  const legendGroups = computeInfraLegendGroups(
@@ -34934,7 +35345,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
34934
35345
  palette,
34935
35346
  layout.edges
34936
35347
  );
34937
- const hasLegend = legendGroups.length > 0 || !!playback;
35348
+ const hasLegend = legendGroups.length > 0 || !!playback && !appHostedPlayback;
34938
35349
  const fixedLegend = !exportMode && hasLegend;
34939
35350
  const legendDynamicH = hasLegend ? getMaxLegendReservedHeight(
34940
35351
  {
@@ -35078,7 +35489,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35078
35489
  isDark,
35079
35490
  activeGroup ?? null,
35080
35491
  playback ?? void 0,
35081
- exportMode
35492
+ exportMode,
35493
+ controlsHost
35082
35494
  );
35083
35495
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
35084
35496
  } else {
@@ -35091,7 +35503,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35091
35503
  isDark,
35092
35504
  activeGroup ?? null,
35093
35505
  playback ?? void 0,
35094
- exportMode
35506
+ exportMode,
35507
+ controlsHost
35095
35508
  );
35096
35509
  }
35097
35510
  }
@@ -42725,6 +43138,9 @@ function renderTechRadar(container, parsed, palette, isDark, onClickItem, export
42725
43138
  onToggle: (active) => options.onToggleListing(active)
42726
43139
  }
42727
43140
  ]
43141
+ },
43142
+ ...options.controlsHost !== void 0 && {
43143
+ controlsHost: options.controlsHost
42728
43144
  }
42729
43145
  };
42730
43146
  const legendState = {
@@ -44547,7 +44963,7 @@ function computeCycleLayout(parsed, options) {
44547
44963
  const circleNodes = parsed.options["circle-nodes"] === "true";
44548
44964
  const nodeDims = parsed.nodes.map((node) => {
44549
44965
  const hasDesc = !hideDescriptions && node.description.length > 0;
44550
- const labelWidth = Math.max(
44966
+ const labelWidth2 = Math.max(
44551
44967
  MIN_NODE_WIDTH4,
44552
44968
  node.label.length * LABEL_CHAR_W + NODE_PAD_X * 2
44553
44969
  );
@@ -44556,12 +44972,12 @@ function computeCycleLayout(parsed, options) {
44556
44972
  }
44557
44973
  if (!hasDesc) {
44558
44974
  return {
44559
- width: Math.min(MAX_NODE_WIDTH3, labelWidth),
44975
+ width: Math.min(MAX_NODE_WIDTH3, labelWidth2),
44560
44976
  height: PLAIN_NODE_HEIGHT,
44561
44977
  wrappedDesc: []
44562
44978
  };
44563
44979
  }
44564
- return chooseDescribedRectDims(node.description, labelWidth);
44980
+ return chooseDescribedRectDims(node.description, labelWidth2);
44565
44981
  });
44566
44982
  if (circleNodes) {
44567
44983
  const maxDiam = Math.max(...nodeDims.map((d) => d.width));
@@ -44757,10 +45173,10 @@ function computeCycleLayout(parsed, options) {
44757
45173
  scale
44758
45174
  };
44759
45175
  }
44760
- function chooseDescribedRectDims(description, labelWidth) {
45176
+ function chooseDescribedRectDims(description, labelWidth2) {
44761
45177
  const minW = Math.min(
44762
45178
  MAX_NODE_WIDTH3,
44763
- Math.max(MIN_NODE_WIDTH4, labelWidth, DESC_MIN_WIDTH)
45179
+ Math.max(MIN_NODE_WIDTH4, labelWidth2, DESC_MIN_WIDTH)
44764
45180
  );
44765
45181
  let best = null;
44766
45182
  let bestScore = Infinity;
@@ -45189,7 +45605,8 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45189
45605
  const hideDescriptions = (renderOptions?.hideDescriptions ?? false) || parsed.options["no-descriptions"] === "true" || viewState?.hd === true;
45190
45606
  const showDescriptions = !hideDescriptions;
45191
45607
  const hasDescriptions = parsed.nodes.some((n) => n.description.length > 0) || parsed.edges.some((e) => e.description.length > 0);
45192
- const hasLegend = hasDescriptions && !!renderOptions?.onToggleDescriptions;
45608
+ const appHostedControls = renderOptions?.controlsHost === "app";
45609
+ const hasLegend = !appHostedControls && hasDescriptions && !!renderOptions?.onToggleDescriptions;
45193
45610
  const showTitle = !!parsed.title && parsed.options["no-title"] !== "on";
45194
45611
  const legendOffset = hasLegend ? sLegendHeight : 0;
45195
45612
  const layoutHeight = height - (showTitle ? sTitleAreaHeight : 0) - legendOffset;
@@ -45226,7 +45643,10 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45226
45643
  groups: [],
45227
45644
  position: { placement: "top-center", titleRelation: "below-title" },
45228
45645
  mode: renderOptions?.exportMode ? "export" : "preview",
45229
- controlsGroup
45646
+ controlsGroup,
45647
+ ...renderOptions?.controlsHost !== void 0 && {
45648
+ controlsHost: renderOptions.controlsHost
45649
+ }
45230
45650
  };
45231
45651
  const legendState = {
45232
45652
  activeGroup: null,
@@ -45480,8 +45900,8 @@ var init_renderer15 = __esm({
45480
45900
  });
45481
45901
 
45482
45902
  // src/map/geo.ts
45483
- import { feature } from "topojson-client";
45484
- import { geoBounds } from "d3-geo";
45903
+ import { feature, neighbors } from "topojson-client";
45904
+ import { geoBounds, geoArea } from "d3-geo";
45485
45905
  function geomObject(topo) {
45486
45906
  const key = Object.keys(topo.objects)[0];
45487
45907
  return topo.objects[key];
@@ -45498,6 +45918,107 @@ function featureIndex(topo) {
45498
45918
  }
45499
45919
  return idx;
45500
45920
  }
45921
+ function buildAdjacency(topo) {
45922
+ const cached = adjacencyCache.get(topo);
45923
+ if (cached) return cached;
45924
+ const geometries = geomObject(topo).geometries;
45925
+ const nb = neighbors(geometries);
45926
+ const sets = /* @__PURE__ */ new Map();
45927
+ geometries.forEach((g, i) => {
45928
+ if (!g.type || g.type === "null") return;
45929
+ let set = sets.get(g.id);
45930
+ if (!set) {
45931
+ set = /* @__PURE__ */ new Set();
45932
+ sets.set(g.id, set);
45933
+ }
45934
+ for (const j of nb[i] ?? []) {
45935
+ const nid = geometries[j]?.id;
45936
+ if (nid && nid !== g.id) set.add(nid);
45937
+ }
45938
+ });
45939
+ const out = /* @__PURE__ */ new Map();
45940
+ for (const [iso, set] of sets) out.set(iso, [...set].sort());
45941
+ adjacencyCache.set(topo, out);
45942
+ return out;
45943
+ }
45944
+ function decodeFeatures(topo) {
45945
+ return geomObject(topo).geometries.map((g) => {
45946
+ const f = feature(topo, g);
45947
+ return {
45948
+ type: "Feature",
45949
+ id: g.id,
45950
+ properties: g.properties,
45951
+ geometry: f.geometry
45952
+ };
45953
+ });
45954
+ }
45955
+ function pointInRing(lon, lat, ring) {
45956
+ let inside = false;
45957
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
45958
+ const xi = ring[i][0];
45959
+ const yi = ring[i][1];
45960
+ const xj = ring[j][0];
45961
+ const yj = ring[j][1];
45962
+ const intersect = yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi;
45963
+ if (intersect) inside = !inside;
45964
+ }
45965
+ return inside;
45966
+ }
45967
+ function pointOnRingEdge(lon, lat, ring) {
45968
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
45969
+ const xi = ring[i][0];
45970
+ const yi = ring[i][1];
45971
+ const xj = ring[j][0];
45972
+ const yj = ring[j][1];
45973
+ if (lon < Math.min(xi, xj) - EDGE_EPS || lon > Math.max(xi, xj) + EDGE_EPS)
45974
+ continue;
45975
+ if (lat < Math.min(yi, yj) - EDGE_EPS || lat > Math.max(yi, yj) + EDGE_EPS)
45976
+ continue;
45977
+ const cross = (xj - xi) * (lat - yi) - (yj - yi) * (lon - xi);
45978
+ if (Math.abs(cross) <= EDGE_EPS) return true;
45979
+ }
45980
+ return false;
45981
+ }
45982
+ function pointInGeometry(geometry, lon, lat) {
45983
+ const g = geometry;
45984
+ if (!g) return false;
45985
+ const polys = g.type === "Polygon" ? [g.coordinates] : g.type === "MultiPolygon" ? g.coordinates : [];
45986
+ for (const rings of polys) {
45987
+ if (!rings.length) continue;
45988
+ if (pointOnRingEdge(lon, lat, rings[0])) return true;
45989
+ if (!pointInRing(lon, lat, rings[0])) continue;
45990
+ let inHole = false;
45991
+ for (let h = 1; h < rings.length; h++) {
45992
+ if (pointInRing(lon, lat, rings[h]) && !pointOnRingEdge(lon, lat, rings[h])) {
45993
+ inHole = true;
45994
+ break;
45995
+ }
45996
+ }
45997
+ if (!inHole) return true;
45998
+ }
45999
+ return false;
46000
+ }
46001
+ function regionAt(lonLat, countries, states) {
46002
+ const lon = lonLat[0];
46003
+ const lat = lonLat[1];
46004
+ let country = null;
46005
+ for (const f of countries) {
46006
+ if (pointInGeometry(f.geometry, lon, lat)) {
46007
+ country = { iso: f.id, name: f.properties.name };
46008
+ break;
46009
+ }
46010
+ }
46011
+ let state = null;
46012
+ if (country?.iso === "US" && states) {
46013
+ for (const f of states) {
46014
+ if (pointInGeometry(f.geometry, lon, lat)) {
46015
+ state = { iso: f.id, name: f.properties.name };
46016
+ break;
46017
+ }
46018
+ }
46019
+ }
46020
+ return { country, state };
46021
+ }
45501
46022
  function featureBbox(topo, geomId) {
45502
46023
  const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45503
46024
  if (!geom) return null;
@@ -45509,6 +46030,74 @@ function featureBbox(topo, geomId) {
45509
46030
  [b[1][0], b[1][1]]
45510
46031
  ];
45511
46032
  }
46033
+ function explodePolygons(gj) {
46034
+ const g = gj.geometry ?? gj;
46035
+ const t = g.type;
46036
+ const coords = g.coordinates;
46037
+ if (t === "Polygon") {
46038
+ return [
46039
+ { type: "Feature", geometry: { type: "Polygon", coordinates: coords } }
46040
+ ];
46041
+ }
46042
+ if (t === "MultiPolygon") {
46043
+ return coords.map((rings) => ({
46044
+ type: "Feature",
46045
+ geometry: { type: "Polygon", coordinates: rings }
46046
+ }));
46047
+ }
46048
+ return [];
46049
+ }
46050
+ function bboxGap(a, b) {
46051
+ const lonGap = Math.max(0, a[0][0] - b[1][0], b[0][0] - a[1][0]);
46052
+ const latGap = Math.max(0, a[0][1] - b[1][1], b[0][1] - a[1][1]);
46053
+ return Math.max(lonGap, latGap);
46054
+ }
46055
+ function featureBboxPrimary(topo, geomId) {
46056
+ const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
46057
+ if (!geom) return null;
46058
+ const gj = feature(topo, geom);
46059
+ const parts = explodePolygons(gj);
46060
+ if (parts.length <= 1) return featureBbox(topo, geomId);
46061
+ const polys = parts.map((p) => {
46062
+ const b = geoBounds(p);
46063
+ if (!b || !Number.isFinite(b[0][0])) return null;
46064
+ const wraps = b[1][0] < b[0][0];
46065
+ const bbox = [
46066
+ [b[0][0], b[0][1]],
46067
+ [b[1][0], b[1][1]]
46068
+ ];
46069
+ return { bbox, area: geoArea(p), wraps };
46070
+ }).filter(
46071
+ (p) => p !== null
46072
+ );
46073
+ if (polys.length <= 1 || polys.some((p) => p.wraps))
46074
+ return featureBbox(topo, geomId);
46075
+ const maxArea = Math.max(...polys.map((p) => p.area));
46076
+ const anchor = polys.find((p) => p.area === maxArea);
46077
+ const cluster = [
46078
+ [anchor.bbox[0][0], anchor.bbox[0][1]],
46079
+ [anchor.bbox[1][0], anchor.bbox[1][1]]
46080
+ ];
46081
+ const remaining = polys.filter((p) => p !== anchor);
46082
+ let added = true;
46083
+ while (added) {
46084
+ added = false;
46085
+ for (let i = remaining.length - 1; i >= 0; i--) {
46086
+ const p = remaining[i];
46087
+ const near = bboxGap(p.bbox, cluster) <= DETACH_GAP_DEG;
46088
+ const large = p.area >= DETACH_AREA_FRAC * maxArea;
46089
+ if (near || large) {
46090
+ cluster[0][0] = Math.min(cluster[0][0], p.bbox[0][0]);
46091
+ cluster[0][1] = Math.min(cluster[0][1], p.bbox[0][1]);
46092
+ cluster[1][0] = Math.max(cluster[1][0], p.bbox[1][0]);
46093
+ cluster[1][1] = Math.max(cluster[1][1], p.bbox[1][1]);
46094
+ remaining.splice(i, 1);
46095
+ added = true;
46096
+ }
46097
+ }
46098
+ }
46099
+ return cluster;
46100
+ }
45512
46101
  function unionExtent(boxes, points) {
45513
46102
  const lats = [];
45514
46103
  const lons = [];
@@ -45547,11 +46136,15 @@ function unionLongitudes(lons) {
45547
46136
  }
45548
46137
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45549
46138
  }
45550
- var fold;
46139
+ var fold, adjacencyCache, EDGE_EPS, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45551
46140
  var init_geo = __esm({
45552
46141
  "src/map/geo.ts"() {
45553
46142
  "use strict";
45554
46143
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
46144
+ adjacencyCache = /* @__PURE__ */ new WeakMap();
46145
+ EDGE_EPS = 1e-9;
46146
+ DETACH_GAP_DEG = 10;
46147
+ DETACH_AREA_FRAC = 0.25;
45555
46148
  }
45556
46149
  });
45557
46150
 
@@ -45569,6 +46162,12 @@ function looksUS(lat, lon) {
45569
46162
  if (lat < 15 || lat > 72) return false;
45570
46163
  return lon >= -180 && lon <= -64 || lon >= 172;
45571
46164
  }
46165
+ function looksNorthAmericaNeighbor(lat, lon) {
46166
+ return lat >= 14 && lat <= 72 && lon >= -141 && lon <= -52;
46167
+ }
46168
+ function isWholeSphere(bb) {
46169
+ return bb[0][0] <= -179 && bb[1][0] >= 179 && bb[0][1] <= -89 && bb[1][1] >= 89;
46170
+ }
45572
46171
  function resolveMap(parsed, data) {
45573
46172
  const diagnostics = [...parsed.diagnostics];
45574
46173
  const err = (line12, message, code) => {
@@ -45579,9 +46178,6 @@ function resolveMap(parsed, data) {
45579
46178
  };
45580
46179
  const result = {
45581
46180
  title: parsed.title,
45582
- ...parsed.directives.subtitle !== void 0 && {
45583
- subtitle: parsed.directives.subtitle
45584
- },
45585
46181
  ...parsed.directives.caption !== void 0 && {
45586
46182
  caption: parsed.directives.caption
45587
46183
  },
@@ -45591,7 +46187,7 @@ function resolveMap(parsed, data) {
45591
46187
  // renderer's job (step 4) — the resolver only carries `tags` + `tagGroups`
45592
46188
  // through; it never resolves a tag value to a palette color (#10).
45593
46189
  directives: { ...parsed.directives },
45594
- basemaps: { world: "coarse", subdivisions: [] },
46190
+ basemaps: { world: "detail", subdivisions: [] },
45595
46191
  regions: [],
45596
46192
  pois: [],
45597
46193
  edges: [],
@@ -45600,7 +46196,8 @@ function resolveMap(parsed, data) {
45600
46196
  [-180, -85],
45601
46197
  [180, 85]
45602
46198
  ],
45603
- projection: "natural-earth",
46199
+ projection: "equirectangular",
46200
+ poiFrameContainers: [],
45604
46201
  diagnostics,
45605
46202
  error: parsed.error
45606
46203
  };
@@ -45610,7 +46207,10 @@ function resolveMap(parsed, data) {
45610
46207
  ...[...countryIndex.values()].map((v) => v.name),
45611
46208
  ...[...usStateIndex.values()].map((v) => v.name)
45612
46209
  ];
45613
- const usScoped = parsed.directives.region === "us-states" || parsed.directives.defaultCountry?.toUpperCase() === "US" || parsed.regions.some((r) => {
46210
+ const localeRaw = parsed.directives.locale?.toUpperCase();
46211
+ const localeCountry = localeRaw ? localeRaw.split("-")[0] : void 0;
46212
+ const localeSubdivision = localeRaw && /^[A-Z]{2}-/.test(localeRaw) ? localeRaw : void 0;
46213
+ const usScoped = localeCountry === "US" || parsed.regions.some((r) => {
45614
46214
  const f = fold(r.name);
45615
46215
  return usStateIndex.has(f) && !countryIndex.has(f);
45616
46216
  }) || parsed.regions.some(
@@ -45655,12 +46255,12 @@ function resolveMap(parsed, data) {
45655
46255
  chosen = { ...inState, layer: "us-state" };
45656
46256
  } else {
45657
46257
  chosen = { ...inCountry, layer: "country" };
46258
+ warn2(
46259
+ r.lineNumber,
46260
+ `"${r.name}" is both a country and a US state \u2014 resolved as ${chosen.layer} (${chosen.id}). Pin it with an ISO code (${inState.id} / ${inCountry.id}) or name + scope ("${r.name} US" / "${r.name} ${inCountry.id}").`,
46261
+ "W_MAP_REGION_AMBIGUOUS"
46262
+ );
45658
46263
  }
45659
- warn2(
45660
- r.lineNumber,
45661
- `"${r.name}" is both a country and a US state \u2014 resolved as ${chosen.layer} (${chosen.id}). Pin it with an ISO code (${inState.id} / ${inCountry.id}) or name + scope ("${r.name} US" / "${r.name} ${inCountry.id}").`,
45662
- "W_MAP_REGION_AMBIGUOUS"
45663
- );
45664
46264
  } else if (inState) {
45665
46265
  chosen = { ...inState, layer: "us-state" };
45666
46266
  } else if (inCountry) {
@@ -45684,6 +46284,7 @@ function resolveMap(parsed, data) {
45684
46284
  name: chosen.name,
45685
46285
  layer: chosen.layer,
45686
46286
  ...r.value !== void 0 && { value: r.value },
46287
+ ...r.color !== void 0 && { color: r.color },
45687
46288
  tags: r.tags,
45688
46289
  meta: r.meta,
45689
46290
  lineNumber: r.lineNumber
@@ -45760,7 +46361,7 @@ function resolveMap(parsed, data) {
45760
46361
  if (!scope)
45761
46362
  warn2(
45762
46363
  line12,
45763
- `"${name}" is ambiguous \u2014 resolved to the most-populous match.`,
46364
+ `"${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.`,
45764
46365
  "W_MAP_AMBIGUOUS_NAME"
45765
46366
  );
45766
46367
  }
@@ -45773,17 +46374,21 @@ function resolveMap(parsed, data) {
45773
46374
  return fold(pos.name);
45774
46375
  };
45775
46376
  const poiCountries = [];
45776
- let anyNonUsPoi = false;
46377
+ let anyUsPoi = false;
46378
+ let anyNonNaPoi = false;
45777
46379
  const noteCountry = (iso) => {
45778
46380
  if (iso) {
45779
46381
  poiCountries.push(iso);
45780
- if (iso !== "US") anyNonUsPoi = true;
46382
+ if (iso === "US") anyUsPoi = true;
46383
+ if (iso !== "US" && iso !== "CA" && iso !== "MX") anyNonNaPoi = true;
45781
46384
  }
45782
46385
  };
45783
46386
  const deferred = [];
45784
46387
  for (const p of parsed.pois) {
45785
46388
  if (p.pos.kind === "coords") {
45786
- if (!looksUS(p.pos.lat, p.pos.lon)) anyNonUsPoi = true;
46389
+ if (looksUS(p.pos.lat, p.pos.lon)) anyUsPoi = true;
46390
+ else if (!looksNorthAmericaNeighbor(p.pos.lat, p.pos.lon))
46391
+ anyNonNaPoi = true;
45787
46392
  addResolvedPoi(p.pos.lat, p.pos.lon, p);
45788
46393
  continue;
45789
46394
  }
@@ -45801,14 +46406,15 @@ function resolveMap(parsed, data) {
45801
46406
  deferred.push(p);
45802
46407
  }
45803
46408
  }
45804
- const inferredCountry = parsed.directives.defaultCountry?.toUpperCase() ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46409
+ const inferredCountry = localeCountry ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46410
+ const inferredScope = localeSubdivision ?? inferredCountry;
45805
46411
  for (const p of deferred) {
45806
46412
  if (p.pos.kind !== "name") continue;
45807
46413
  const got = lookupName(
45808
46414
  p.pos.name,
45809
46415
  p.pos.scope,
45810
46416
  p.lineNumber,
45811
- inferredCountry,
46417
+ inferredScope,
45812
46418
  true
45813
46419
  );
45814
46420
  if (got.kind === "ok") {
@@ -45825,6 +46431,7 @@ function resolveMap(parsed, data) {
45825
46431
  lat,
45826
46432
  lon,
45827
46433
  ...p.label !== void 0 && { label: p.label },
46434
+ ...p.color !== void 0 && { color: p.color },
45828
46435
  tags: p.tags,
45829
46436
  meta: p.meta,
45830
46437
  lineNumber: p.lineNumber
@@ -45877,7 +46484,8 @@ function resolveMap(parsed, data) {
45877
46484
  const meta = sizeValue !== void 0 ? { value: sizeValue } : {};
45878
46485
  if (pos.kind === "coords") {
45879
46486
  const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
45880
- if (!looksUS(pos.lat, pos.lon)) anyNonUsPoi = true;
46487
+ if (looksUS(pos.lat, pos.lon)) anyUsPoi = true;
46488
+ else if (!looksNorthAmericaNeighbor(pos.lat, pos.lon)) anyNonNaPoi = true;
45881
46489
  if (!registry.has(id)) {
45882
46490
  registerPoi(
45883
46491
  id,
@@ -45900,7 +46508,7 @@ function resolveMap(parsed, data) {
45900
46508
  if (registry.has(f)) return f;
45901
46509
  const aliased = declaredByName.get(f);
45902
46510
  if (aliased) return aliased;
45903
- const got = lookupName(pos.name, pos.scope, line12, inferredCountry, true);
46511
+ const got = lookupName(pos.name, pos.scope, line12, inferredScope, true);
45904
46512
  if (got.kind !== "ok") return null;
45905
46513
  noteCountry(got.iso);
45906
46514
  registerPoi(
@@ -45957,9 +46565,12 @@ function resolveMap(parsed, data) {
45957
46565
  }
45958
46566
  routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
45959
46567
  }
46568
+ const hasUsContent = usSubdivisionReferenced || anyUsPoi || localeCountry === "US";
46569
+ const usOriented = !anyNonNaPoi && !regions.some(
46570
+ (r) => r.layer === "country" && !["US", "CA", "MX"].includes(r.iso)
46571
+ ) && hasUsContent;
45960
46572
  const subdivisions = [];
45961
- if (usSubdivisionReferenced || parsed.directives.region === "us-states")
45962
- subdivisions.push("us-states");
46573
+ if (usSubdivisionReferenced || usOriented) subdivisions.push("us-states");
45963
46574
  const regionBoxes = [];
45964
46575
  for (const ref of referencedRegionIds) {
45965
46576
  const bb = featureBbox(data.usStates, ref.id);
@@ -45967,7 +46578,7 @@ function resolveMap(parsed, data) {
45967
46578
  }
45968
46579
  for (const r of regions) {
45969
46580
  if (r.layer === "country") {
45970
- const bb = featureBbox(data.worldCoarse, r.iso);
46581
+ const bb = featureBboxPrimary(data.worldCoarse, r.iso);
45971
46582
  if (bb) regionBoxes.push(bb);
45972
46583
  }
45973
46584
  }
@@ -45977,23 +46588,56 @@ function resolveMap(parsed, data) {
45977
46588
  [-180, -85],
45978
46589
  [180, 85]
45979
46590
  ];
45980
- let extent2 = unioned ? pad(unioned, PAD_FRACTION) : DEFAULT_EXTENT;
46591
+ const basePad = regions.length > 0 ? REGION_PAD_FRACTION : PAD_FRACTION;
46592
+ let extent2 = unioned ? pad(unioned, basePad) : DEFAULT_EXTENT;
46593
+ const isPoiOnly = pois.length > 0 && regions.length === 0;
46594
+ const containerRegionIds = [];
46595
+ if (isPoiOnly) {
46596
+ const countries = decodeFeatures(data.worldDetail);
46597
+ const states = decodeFeatures(data.usStates);
46598
+ const seen = /* @__PURE__ */ new Set();
46599
+ const containerBoxes = [];
46600
+ for (const p of pois) {
46601
+ const { country, state } = regionAt([p.lon, p.lat], countries, states);
46602
+ const id = state?.iso ?? country?.iso;
46603
+ if (!id || seen.has(id)) continue;
46604
+ seen.add(id);
46605
+ containerRegionIds.push(id);
46606
+ const bb = state ? featureBbox(data.usStates, id) : featureBboxPrimary(data.worldCoarse, id);
46607
+ if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46608
+ }
46609
+ const containerUnion = unionExtent(containerBoxes, points);
46610
+ if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46611
+ }
46612
+ if (isPoiOnly) {
46613
+ const cx = (extent2[0][0] + extent2[1][0]) / 2;
46614
+ const cy = (extent2[0][1] + extent2[1][1]) / 2;
46615
+ const lon = extent2[1][0] - extent2[0][0];
46616
+ const lat = extent2[1][1] - extent2[0][1];
46617
+ const longer = Math.max(lon, lat);
46618
+ if (longer > 0 && longer < POI_ZOOM_FLOOR_DEG) {
46619
+ const k = POI_ZOOM_FLOOR_DEG / longer;
46620
+ const halfLon = lon * k / 2;
46621
+ const halfLat = lat * k / 2;
46622
+ extent2 = [
46623
+ [cx - halfLon, cy - halfLat],
46624
+ [cx + halfLon, cy + halfLat]
46625
+ ];
46626
+ }
46627
+ }
45981
46628
  const lonSpan = extent2[1][0] - extent2[0][0];
45982
46629
  const latSpan = extent2[1][1] - extent2[0][1];
45983
46630
  const span = Math.max(lonSpan, latSpan);
45984
- const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46631
+ const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
45985
46632
  let projection;
45986
- const override = parsed.directives.projection;
45987
- if (override === "equirectangular" || override === "natural-earth" || override === "albers-usa" || override === "mercator") {
45988
- projection = override;
45989
- } else if (usDominant) {
46633
+ if (isPoiOnly && usOriented && lonSpan < US_NATIONAL_LON_SPAN) {
46634
+ projection = "mercator";
46635
+ } else if (usOriented) {
45990
46636
  projection = "albers-usa";
45991
- } else if (span > WORLD_SPAN) {
46637
+ } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
45992
46638
  projection = "equirectangular";
45993
- } else if (span < MERCATOR_MAX_SPAN) {
45994
- projection = "mercator";
45995
46639
  } else {
45996
- projection = "equirectangular";
46640
+ projection = "mercator";
45997
46641
  }
45998
46642
  if (lonSpan >= 180) {
45999
46643
  extent2 = [
@@ -46006,11 +46650,20 @@ function resolveMap(parsed, data) {
46006
46650
  result.edges = edges;
46007
46651
  result.routes = routes;
46008
46652
  result.basemaps = {
46009
- world: span > WORLD_SPAN ? "coarse" : "detail",
46653
+ // Tier is intentionally pinned to detail (50m) at ALL scales. Diagrammo maps
46654
+ // are presentational (palette tints, relief hachures, POI hubs), not
46655
+ // survey-grade — recognizability > generalization: 110m coarse drops the
46656
+ // Italian boot to a stump at world scale. `WORLD_SPAN` lives on only for the
46657
+ // projection decision (the `usOriented`/`span > WORLD_SPAN` chain above); it
46658
+ // no longer gates basemap resolution.
46659
+ // `worldCoarse` is still loaded — it's the authoritative name/bbox index
46660
+ // (featureIndex, featureBboxPrimary), not dead code.
46661
+ world: "detail",
46010
46662
  subdivisions
46011
46663
  };
46012
46664
  result.extent = extent2;
46013
46665
  result.projection = projection;
46666
+ result.poiFrameContainers = containerRegionIds;
46014
46667
  result.error = parsed.error ?? firstError(diagnostics);
46015
46668
  return result;
46016
46669
  }
@@ -46047,17 +46700,20 @@ function firstError(diags) {
46047
46700
  const e = diags.find((d) => d.severity === "error");
46048
46701
  return e ? formatDgmoError(e) : null;
46049
46702
  }
46050
- var WORLD_SPAN, MERCATOR_MAX_SPAN, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46703
+ 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;
46051
46704
  var init_resolver2 = __esm({
46052
46705
  "src/map/resolver.ts"() {
46053
46706
  "use strict";
46054
46707
  init_diagnostics();
46055
46708
  init_geo();
46056
46709
  WORLD_SPAN = 90;
46057
- MERCATOR_MAX_SPAN = 25;
46710
+ MERCATOR_MAX_LAT = 80;
46058
46711
  PAD_FRACTION = 0.05;
46712
+ REGION_PAD_FRACTION = 0.12;
46059
46713
  WORLD_LAT_SOUTH = -58;
46060
46714
  WORLD_LAT_NORTH = 78;
46715
+ POI_ZOOM_FLOOR_DEG = 7;
46716
+ US_NATIONAL_LON_SPAN = 48;
46061
46717
  REGION_ALIASES = {
46062
46718
  // Common everyday names → the Natural-Earth display name actually shipped.
46063
46719
  "united states": "united states of america",
@@ -46135,111 +46791,269 @@ var init_resolver2 = __esm({
46135
46791
  }
46136
46792
  });
46137
46793
 
46138
- // src/map/load-data.ts
46139
- var load_data_exports = {};
46140
- __export(load_data_exports, {
46141
- loadMapData: () => loadMapData
46794
+ // src/map/colorize.ts
46795
+ function assignColors(isos, adjacency) {
46796
+ const sorted = [...isos].sort();
46797
+ const byIso = /* @__PURE__ */ new Map();
46798
+ let maxIndex = -1;
46799
+ for (const iso of sorted) {
46800
+ const taken = /* @__PURE__ */ new Set();
46801
+ for (const n of adjacency.get(iso) ?? []) {
46802
+ const c = byIso.get(n);
46803
+ if (c !== void 0) taken.add(c);
46804
+ }
46805
+ let h = 0;
46806
+ while (taken.has(h)) h++;
46807
+ byIso.set(iso, h);
46808
+ if (h > maxIndex) maxIndex = h;
46809
+ }
46810
+ return { byIso, huesNeeded: maxIndex + 1 };
46811
+ }
46812
+ var init_colorize = __esm({
46813
+ "src/map/colorize.ts"() {
46814
+ "use strict";
46815
+ }
46142
46816
  });
46143
- async function loadNodeBuiltins() {
46144
- const [{ readFile }, { fileURLToPath }, { dirname, resolve }] = await Promise.all([
46145
- import("fs/promises"),
46146
- import("url"),
46147
- import("path")
46148
- ]);
46149
- return { readFile, fileURLToPath, dirname, resolve };
46150
- }
46151
- async function readJson(nb, dir, name) {
46152
- return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
46153
- }
46154
- async function firstExistingDir(nb, baseDir) {
46155
- for (const rel of CANDIDATE_DIRS) {
46156
- const dir = nb.resolve(baseDir, rel);
46157
- try {
46158
- await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
46159
- return dir;
46160
- } catch {
46817
+
46818
+ // src/map/context-labels.ts
46819
+ function tierBand(maxSpanDeg) {
46820
+ if (maxSpanDeg >= 90) return "world";
46821
+ if (maxSpanDeg >= 20) return "continental";
46822
+ if (maxSpanDeg >= 5) return "regional";
46823
+ return "local";
46824
+ }
46825
+ function labelBudget(width, height, band) {
46826
+ const bandCap = {
46827
+ world: 6,
46828
+ continental: 5,
46829
+ regional: 4,
46830
+ local: 3
46831
+ };
46832
+ const area2 = Math.floor(Math.sqrt(Math.max(0, width * height)) / 150);
46833
+ return Math.max(0, Math.min(area2, bandCap[band]));
46834
+ }
46835
+ function waterEligible(tier, kind, band) {
46836
+ switch (band) {
46837
+ case "world":
46838
+ return tier <= 1 && (kind === "ocean" || kind === "sea");
46839
+ case "continental":
46840
+ return tier <= 2;
46841
+ case "regional":
46842
+ return tier <= 3;
46843
+ case "local":
46844
+ return tier <= 4;
46845
+ }
46846
+ }
46847
+ function insideViewport(p, width, height) {
46848
+ return !!p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && p[0] >= 0 && p[0] <= width && p[1] >= 0 && p[1] <= height;
46849
+ }
46850
+ function labelWidth(text, letterSpacing) {
46851
+ const spacing = letterSpacing > 0 ? Math.max(0, text.length - 1) * letterSpacing : 0;
46852
+ return measureLegendText(text, FONT) + spacing + 2 * PADX;
46853
+ }
46854
+ function wrapLabel2(text, letterSpacing) {
46855
+ const words = text.split(/\s+/).filter(Boolean);
46856
+ if (words.length <= 1) return [text];
46857
+ const maxLines = words.length >= 4 ? 3 : 2;
46858
+ const n = words.length;
46859
+ let best = null;
46860
+ for (let mask = 0; mask < 1 << n - 1; mask++) {
46861
+ const lines = [];
46862
+ let cur = [words[0]];
46863
+ for (let i = 1; i < n; i++) {
46864
+ if (mask & 1 << i - 1) {
46865
+ lines.push(cur.join(" "));
46866
+ cur = [words[i]];
46867
+ } else cur.push(words[i]);
46161
46868
  }
46869
+ lines.push(cur.join(" "));
46870
+ if (lines.length > maxLines) continue;
46871
+ const cost = Math.round(
46872
+ Math.max(...lines.map((l) => labelWidth(l, letterSpacing)))
46873
+ );
46874
+ const head = labelWidth(lines[0], letterSpacing);
46875
+ if (!best || cost < best.cost || cost === best.cost && lines.length < best.lines.length || cost === best.cost && lines.length === best.lines.length && head > best.head)
46876
+ best = { lines, cost, head };
46162
46877
  }
46163
- throw new Error(
46164
- `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
46165
- );
46878
+ return best?.lines ?? [text];
46166
46879
  }
46167
- function validate(data) {
46168
- const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
46169
- if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
46170
- throw new Error("map data assets are malformed (failed shape validation)");
46171
- }
46172
- return data;
46880
+ function rectAround(cx, cy, lines, letterSpacing) {
46881
+ const w = Math.max(...lines.map((l) => labelWidth(l, letterSpacing)));
46882
+ const h = (lines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY;
46883
+ return { x: cx - w / 2, y: cy - h / 2, w, h };
46173
46884
  }
46174
- function moduleBaseDir(nb) {
46175
- try {
46176
- const url = import.meta.url;
46177
- if (url) return nb.dirname(nb.fileURLToPath(url));
46178
- } catch {
46179
- }
46180
- if (typeof __dirname !== "undefined") return __dirname;
46181
- return process.cwd();
46885
+ function rectFits(r, width, height) {
46886
+ return r.x >= 0 && r.y >= 0 && r.x + r.w <= width && r.y + r.h <= height;
46182
46887
  }
46183
- function loadMapData() {
46184
- cache ??= (async () => {
46185
- const nb = await loadNodeBuiltins();
46186
- const dir = await firstExistingDir(nb, moduleBaseDir(nb));
46187
- const [
46188
- worldCoarse,
46189
- worldDetail,
46190
- usStates,
46191
- lakes,
46192
- rivers,
46193
- naLand,
46194
- naLakes,
46195
- gazetteer
46196
- ] = await Promise.all([
46197
- readJson(nb, dir, FILES.worldCoarse),
46198
- readJson(nb, dir, FILES.worldDetail),
46199
- readJson(nb, dir, FILES.usStates),
46200
- // Lakes/rivers/NA assets are optional — older bundles may predate them.
46201
- readJson(nb, dir, FILES.lakes).catch(() => void 0),
46202
- readJson(nb, dir, FILES.rivers).catch(() => void 0),
46203
- readJson(nb, dir, FILES.naLand).catch(() => void 0),
46204
- readJson(nb, dir, FILES.naLakes).catch(() => void 0),
46205
- readJson(nb, dir, FILES.gazetteer)
46206
- ]);
46207
- return validate({
46208
- worldCoarse,
46209
- worldDetail,
46210
- usStates,
46211
- gazetteer,
46212
- ...lakes && { lakes },
46213
- ...rivers && { rivers },
46214
- ...naLand && { naLand },
46215
- ...naLakes && { naLakes }
46888
+ function overlapsPadded(a, b, pad2) {
46889
+ 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;
46890
+ }
46891
+ function placeContextLabels(args) {
46892
+ const {
46893
+ projection,
46894
+ dLonSpan,
46895
+ dLatSpan,
46896
+ width,
46897
+ height,
46898
+ waterBodies,
46899
+ countries,
46900
+ palette,
46901
+ project,
46902
+ collides,
46903
+ overLand
46904
+ } = args;
46905
+ void projection;
46906
+ const band = tierBand(Math.max(dLonSpan, dLatSpan));
46907
+ const budget = labelBudget(width, height, band);
46908
+ if (budget <= 0) return [];
46909
+ const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
46910
+ const countryColor = palette.textMuted;
46911
+ const haloColor = palette.bg;
46912
+ const candidates = [];
46913
+ const center = [width / 2, height / 2];
46914
+ for (const e of waterBodies?.entries ?? []) {
46915
+ const [lat, lon, name, tier, kind, alt] = e;
46916
+ if (!waterEligible(tier, kind, band)) continue;
46917
+ const wlines = wrapLabel2(name, WATER_LETTER_SPACING);
46918
+ const anchorsLngLat = [[lon, lat]];
46919
+ for (const a of alt ?? []) anchorsLngLat.push([a[1], a[0]]);
46920
+ let best = null;
46921
+ let bestD = Infinity;
46922
+ let nearestProj = null;
46923
+ let nearestProjD = Infinity;
46924
+ for (const [aLon, aLat] of anchorsLngLat) {
46925
+ const p = project(aLon, aLat);
46926
+ if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
46927
+ const d = (p[0] - center[0]) ** 2 + (p[1] - center[1]) ** 2;
46928
+ if (d < nearestProjD) {
46929
+ nearestProjD = d;
46930
+ nearestProj = p;
46931
+ }
46932
+ if (!insideViewport(p, width, height)) continue;
46933
+ if (d < bestD) {
46934
+ bestD = d;
46935
+ best = p;
46936
+ }
46937
+ }
46938
+ if (!best && tier === 0 && nearestProj) {
46939
+ const overX = Math.max(0, -nearestProj[0], nearestProj[0] - width);
46940
+ const overY = Math.max(0, -nearestProj[1], nearestProj[1] - height);
46941
+ if (overX <= width * EDGE_CLAMP_OVERSHOOT && overY <= height * EDGE_CLAMP_OVERSHOOT) {
46942
+ const halfW = Math.max(...wlines.map((l) => labelWidth(l, WATER_LETTER_SPACING))) / 2;
46943
+ const halfH = ((wlines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY) / 2;
46944
+ const m = EDGE_CLAMP_MARGIN;
46945
+ best = [
46946
+ Math.min(Math.max(nearestProj[0], halfW + m), width - halfW - m),
46947
+ Math.min(Math.max(nearestProj[1], halfH + m), height - halfH - m)
46948
+ ];
46949
+ }
46950
+ }
46951
+ if (!best) continue;
46952
+ candidates.push({
46953
+ text: name,
46954
+ lines: wlines,
46955
+ cx: best[0],
46956
+ cy: best[1],
46957
+ italic: true,
46958
+ letterSpacing: WATER_LETTER_SPACING,
46959
+ color: waterColor,
46960
+ // Water before any country (×1000), then by tier, then kind, then name.
46961
+ sort: tier * 10 + KIND_ORDER[kind]
46216
46962
  });
46217
- })().catch((e) => {
46218
- cache = void 0;
46219
- throw e;
46220
- });
46221
- return cache;
46963
+ }
46964
+ const ranked = countries.map((c) => {
46965
+ const [x0, y0, x1, y1] = c.bbox;
46966
+ const w = x1 - x0;
46967
+ const h = y1 - y0;
46968
+ return { c, w, h, area: w * h };
46969
+ }).filter((r) => Number.isFinite(r.area) && r.area > 0).sort((a, b) => b.area - a.area);
46970
+ let ci = 0;
46971
+ for (const r of ranked) {
46972
+ const { c, w, h } = r;
46973
+ if (w > width * 0.66 || h > height * 0.66) continue;
46974
+ if (!insideViewport(c.anchor, width, height)) continue;
46975
+ const text = c.name;
46976
+ const tw = labelWidth(text, 0);
46977
+ if (tw > w || FONT + 2 * PADY > h) continue;
46978
+ candidates.push({
46979
+ text,
46980
+ lines: [text],
46981
+ cx: c.anchor[0],
46982
+ cy: c.anchor[1],
46983
+ italic: false,
46984
+ letterSpacing: 0,
46985
+ color: countryColor,
46986
+ // Always after every water body (+1e6); larger area = earlier.
46987
+ sort: 1e6 + ci++
46988
+ });
46989
+ }
46990
+ candidates.sort((a, b) => a.sort - b.sort);
46991
+ const placed = [];
46992
+ const placedRects = [];
46993
+ for (const cand of candidates) {
46994
+ if (placed.length >= budget) break;
46995
+ const rect = rectAround(cand.cx, cand.cy, cand.lines, cand.letterSpacing);
46996
+ if (!rectFits(rect, width, height)) continue;
46997
+ if (cand.italic && overLand) {
46998
+ const inset = 2;
46999
+ const top = cand.cy - (cand.lines.length - 1) / 2 * LINE_HEIGHT;
47000
+ const touchesLand = cand.lines.some((line12, li) => {
47001
+ const lw = labelWidth(line12, cand.letterSpacing);
47002
+ const x0 = cand.cx - lw / 2 + inset;
47003
+ const x1 = cand.cx + lw / 2 - inset;
47004
+ const xs = [x0, (x0 + cand.cx) / 2, cand.cx, (cand.cx + x1) / 2, x1];
47005
+ const base = top + li * LINE_HEIGHT;
47006
+ return [base, base - FONT * 0.4, base - FONT * 0.8].some(
47007
+ (y) => xs.some((x) => overLand(x, y))
47008
+ );
47009
+ });
47010
+ if (touchesLand) continue;
47011
+ }
47012
+ if (collides(rect)) continue;
47013
+ if (placedRects.some((r) => overlapsPadded(rect, r, CONTEXT_PAD))) continue;
47014
+ placedRects.push(rect);
47015
+ placed.push({
47016
+ x: cand.cx,
47017
+ y: cand.cy,
47018
+ text: cand.text,
47019
+ anchor: "middle",
47020
+ color: cand.color,
47021
+ // No halo: the bg-coloured outline reads as a ghost box behind the text
47022
+ // over the tinted water/land. Context labels are muted enough to sit
47023
+ // cleanly on the basemap without one.
47024
+ halo: false,
47025
+ haloColor,
47026
+ italic: cand.italic,
47027
+ letterSpacing: cand.letterSpacing,
47028
+ ...cand.lines.length > 1 ? { lines: cand.lines } : {},
47029
+ lineNumber: 0
47030
+ });
47031
+ }
47032
+ return placed;
46222
47033
  }
46223
- var FILES, CANDIDATE_DIRS, cache;
46224
- var init_load_data = __esm({
46225
- "src/map/load-data.ts"() {
47034
+ var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, KIND_ORDER;
47035
+ var init_context_labels = __esm({
47036
+ "src/map/context-labels.ts"() {
46226
47037
  "use strict";
46227
- FILES = {
46228
- worldCoarse: "world-coarse.json",
46229
- worldDetail: "world-detail.json",
46230
- usStates: "us-states.json",
46231
- lakes: "lakes.json",
46232
- rivers: "rivers.json",
46233
- naLand: "na-land.json",
46234
- naLakes: "na-lakes.json",
46235
- gazetteer: "gazetteer.json"
47038
+ init_color_utils();
47039
+ init_legend_constants();
47040
+ FONT = 11;
47041
+ LINE_HEIGHT = FONT + 2;
47042
+ PADX = 4;
47043
+ PADY = 3;
47044
+ WATER_LETTER_SPACING = 1.5;
47045
+ CONTEXT_PAD = 4;
47046
+ EDGE_CLAMP_MARGIN = 8;
47047
+ EDGE_CLAMP_OVERSHOOT = 0.35;
47048
+ KIND_ORDER = {
47049
+ ocean: 0,
47050
+ sea: 1,
47051
+ gulf: 2,
47052
+ bay: 3,
47053
+ strait: 4,
47054
+ channel: 5,
47055
+ sound: 6
46236
47056
  };
46237
- CANDIDATE_DIRS = [
46238
- "./data",
46239
- "./map-data",
46240
- "../map-data",
46241
- "../src/map/data"
46242
- ];
46243
47057
  }
46244
47058
  });
46245
47059
 
@@ -46247,6 +47061,7 @@ var init_load_data = __esm({
46247
47061
  import {
46248
47062
  geoPath,
46249
47063
  geoNaturalEarth1,
47064
+ geoEqualEarth,
46250
47065
  geoEquirectangular,
46251
47066
  geoConicEqualArea,
46252
47067
  geoMercator,
@@ -46258,12 +47073,34 @@ function geomObject2(topo) {
46258
47073
  const key = Object.keys(topo.objects)[0];
46259
47074
  return topo.objects[key];
46260
47075
  }
47076
+ function mergeFeatures(a, b) {
47077
+ const polysOf = (f) => {
47078
+ const g = f.geometry;
47079
+ if (!g) return null;
47080
+ if (g.type === "Polygon") return [g.coordinates];
47081
+ if (g.type === "MultiPolygon") return g.coordinates;
47082
+ return null;
47083
+ };
47084
+ const pa = polysOf(a);
47085
+ const pb = polysOf(b);
47086
+ if (!pa || !pb) return a;
47087
+ return {
47088
+ ...a,
47089
+ geometry: { type: "MultiPolygon", coordinates: [...pa, ...pb] }
47090
+ };
47091
+ }
46261
47092
  function decodeLayer(topo) {
47093
+ const cached = decodeCache.get(topo);
47094
+ if (cached) return cached;
46262
47095
  const out = /* @__PURE__ */ new Map();
46263
47096
  for (const g of geomObject2(topo).geometries) {
46264
47097
  const f = feature2(topo, g);
46265
- out.set(g.id, { ...f, id: g.id });
47098
+ if (!f.geometry) continue;
47099
+ const tagged = { ...f, id: g.id };
47100
+ const existing = out.get(g.id);
47101
+ out.set(g.id, existing ? mergeFeatures(existing, tagged) : tagged);
46266
47102
  }
47103
+ decodeCache.set(topo, out);
46267
47104
  return out;
46268
47105
  }
46269
47106
  function projectionFor(family) {
@@ -46272,38 +47109,35 @@ function projectionFor(family) {
46272
47109
  return usConusProjection();
46273
47110
  case "mercator":
46274
47111
  return geoMercator();
47112
+ case "equal-earth":
47113
+ return geoEqualEarth();
47114
+ case "equirectangular":
47115
+ return geoEquirectangular();
46275
47116
  case "natural-earth":
46276
47117
  return geoNaturalEarth1();
46277
- case "equirectangular":
46278
47118
  default:
46279
47119
  return geoEquirectangular();
46280
47120
  }
46281
47121
  }
46282
- function mapBackgroundColor(palette, isDark = false, dataActive = false) {
46283
- if (dataActive)
46284
- return mix(
46285
- palette.colors.gray,
46286
- palette.bg,
46287
- isDark ? MUTED_WATER_DARK : MUTED_WATER_LIGHT
46288
- );
46289
- return mix(palette.colors.blue, palette.bg, WATER_TINT);
47122
+ function mapBackgroundColor(palette, isDark = false, _dataActive = false) {
47123
+ return mix(
47124
+ palette.colors.blue,
47125
+ palette.bg,
47126
+ isDark ? WATER_TINT_DARK : WATER_TINT_LIGHT
47127
+ );
46290
47128
  }
46291
- function mapNeutralLandColor(palette, isDark, dataActive = false) {
46292
- if (dataActive)
46293
- return isDark ? mix(palette.colors.gray, palette.bg, MUTED_LAND_DARK) : palette.bg;
47129
+ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46294
47130
  return mix(
46295
47131
  palette.colors.green,
46296
47132
  palette.bg,
46297
47133
  isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT
46298
47134
  );
46299
47135
  }
46300
- function layoutMap(resolved, data, size, opts) {
46301
- const { palette, isDark } = opts;
46302
- const { width, height } = size;
47136
+ function buildMapProjection(resolved, data) {
46303
47137
  const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
46304
- const usCrisp = resolved.projection === "albers-usa" && wantsUsStates && !!data.naLand;
47138
+ const usCrisp = (resolved.projection === "albers-usa" || resolved.projection === "mercator") && wantsUsStates && !!data.naLand;
46305
47139
  const worldTopo = usCrisp ? data.worldDetail : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46306
- const worldLayer = decodeLayer(worldTopo);
47140
+ const worldLayer = new Map(decodeLayer(worldTopo));
46307
47141
  if (usCrisp && data.naLand) {
46308
47142
  const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
46309
47143
  const crisp = decodeLayer(data.naLand);
@@ -46312,17 +47146,110 @@ function layoutMap(resolved, data, size, opts) {
46312
47146
  if (!base) continue;
46313
47147
  const [[bw, bs], [be, bn]] = geoBounds2(base);
46314
47148
  if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
46315
- worldLayer.set(iso, cf);
47149
+ worldLayer.set(iso, { ...cf, properties: base.properties });
46316
47150
  }
46317
47151
  }
46318
47152
  const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
47153
+ const extentOutline = () => {
47154
+ const [[w, s], [e, n]] = resolved.extent;
47155
+ const N = 16;
47156
+ const coords = [];
47157
+ for (let i = 0; i <= N; i++) {
47158
+ const t = i / N;
47159
+ const lon = w + (e - w) * t;
47160
+ const lat = s + (n - s) * t;
47161
+ coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
47162
+ }
47163
+ return {
47164
+ type: "Feature",
47165
+ properties: {},
47166
+ geometry: { type: "MultiPoint", coordinates: coords }
47167
+ };
47168
+ };
47169
+ let fitFeatures;
47170
+ if (resolved.projection === "albers-usa" && usLayer) {
47171
+ fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
47172
+ const neighborPoints = resolved.pois.filter((p) => !inAlaska(p.lon, p.lat) && !inHawaii(p.lon, p.lat)).map((p) => [p.lon, p.lat]);
47173
+ if (neighborPoints.length > 0) {
47174
+ fitFeatures.push({
47175
+ type: "Feature",
47176
+ properties: {},
47177
+ geometry: { type: "MultiPoint", coordinates: neighborPoints }
47178
+ });
47179
+ }
47180
+ for (const r of resolved.regions) {
47181
+ if (r.layer === "country" && (r.iso === "CA" || r.iso === "MX")) {
47182
+ const cf = worldLayer.get(r.iso);
47183
+ if (cf) fitFeatures.push(cf);
47184
+ }
47185
+ }
47186
+ } else {
47187
+ fitFeatures = [extentOutline()];
47188
+ }
47189
+ const fitTarget = { type: "FeatureCollection", features: fitFeatures };
47190
+ const projection = projectionFor(resolved.projection);
47191
+ if (resolved.projection !== "albers-usa") {
47192
+ let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
47193
+ if (centerLon > 180) centerLon -= 360;
47194
+ projection.rotate([-centerLon, 0]);
47195
+ }
47196
+ const fitGB = geoBounds2(fitTarget);
47197
+ const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
47198
+ return {
47199
+ projection,
47200
+ fitTarget,
47201
+ fitIsGlobal,
47202
+ worldLayer,
47203
+ usLayer,
47204
+ usCrisp,
47205
+ wantsUsStates,
47206
+ worldTopo
47207
+ };
47208
+ }
47209
+ function parsePathRings(d) {
47210
+ const rings = [];
47211
+ let cur = [];
47212
+ const re = /([MLZ])([^MLZ]*)/g;
47213
+ let m;
47214
+ while (m = re.exec(d)) {
47215
+ if (m[1] === "Z") {
47216
+ if (cur.length) rings.push(cur);
47217
+ cur = [];
47218
+ continue;
47219
+ }
47220
+ if (m[1] === "M" && cur.length) {
47221
+ rings.push(cur);
47222
+ cur = [];
47223
+ }
47224
+ const nums = m[2].split(/[ ,]+/).map(Number);
47225
+ for (let i = 0; i + 1 < nums.length; i += 2) {
47226
+ const x = nums[i];
47227
+ const y = nums[i + 1];
47228
+ if (Number.isFinite(x) && Number.isFinite(y)) cur.push([x, y]);
47229
+ }
47230
+ }
47231
+ if (cur.length) rings.push(cur);
47232
+ return rings;
47233
+ }
47234
+ function layoutMap(resolved, data, size, opts) {
47235
+ const { palette, isDark } = opts;
47236
+ const { width, height } = size;
47237
+ const {
47238
+ projection,
47239
+ fitTarget,
47240
+ fitIsGlobal,
47241
+ worldLayer,
47242
+ usLayer,
47243
+ usCrisp,
47244
+ worldTopo
47245
+ } = buildMapProjection(resolved, data);
46319
47246
  const usContext = usLayer !== null;
46320
47247
  const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46321
47248
  const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
46322
- const scaleOverride = resolved.directives.scale;
46323
- const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46324
- const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
46325
- const rampHue = palette.colors.red;
47249
+ const allNonNegative = values.length > 0 && values.every((v) => v >= 0);
47250
+ const rampMin = allNonNegative ? 0 : Math.min(...values);
47251
+ const rampMax = Math.max(...values);
47252
+ const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46326
47253
  const hasRamp = values.length > 0;
46327
47254
  const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
46328
47255
  const matchColorGroup = (v) => {
@@ -46342,14 +47269,48 @@ function layoutMap(resolved, data, size, opts) {
46342
47269
  activeGroup = VALUE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46343
47270
  }
46344
47271
  const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
46345
- const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
47272
+ const mutedBasemap = activeGroup !== null;
46346
47273
  const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46347
47274
  const water = mapBackgroundColor(palette, isDark, mutedBasemap);
47275
+ const lakeStroke = mix(regionStroke, water, 45);
46348
47276
  const foreignFill = mix(
46349
47277
  palette.colors.gray,
46350
47278
  palette.bg,
46351
47279
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46352
47280
  );
47281
+ const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && resolved.tagGroups.length === 0;
47282
+ const colorByIso = /* @__PURE__ */ new Map();
47283
+ if (colorizeActive) {
47284
+ const adjacency = /* @__PURE__ */ new Map();
47285
+ const addEdges = (src) => {
47286
+ for (const [iso, ns] of src) {
47287
+ const cur = adjacency.get(iso);
47288
+ if (cur) cur.push(...ns);
47289
+ else adjacency.set(iso, [...ns]);
47290
+ }
47291
+ };
47292
+ addEdges(buildAdjacency(worldTopo));
47293
+ if (usLayer) {
47294
+ addEdges(buildAdjacency(data.usStates));
47295
+ for (const [country, states] of Object.entries(FOREIGN_BORDER)) {
47296
+ const cn = adjacency.get(country);
47297
+ if (!cn) continue;
47298
+ for (const st of states) {
47299
+ const sn = adjacency.get(st);
47300
+ if (!sn) continue;
47301
+ cn.push(st);
47302
+ sn.push(country);
47303
+ }
47304
+ }
47305
+ }
47306
+ const { byIso, huesNeeded } = assignColors(
47307
+ [...adjacency.keys()],
47308
+ adjacency
47309
+ );
47310
+ const tints = politicalTints(palette, huesNeeded, isDark);
47311
+ for (const [iso, idx] of byIso) colorByIso.set(iso, tints[idx]);
47312
+ }
47313
+ const colorizeStroke = (fill2) => mix(fill2, palette.text, 35);
46353
47314
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46354
47315
  const fillForValue = (s) => {
46355
47316
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
@@ -46374,47 +47335,26 @@ function layoutMap(resolved, data, size, opts) {
46374
47335
  isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
46375
47336
  );
46376
47337
  };
47338
+ const directFill = (name) => {
47339
+ const hex = name ? resolveColor(name, palette) : null;
47340
+ if (!hex) return null;
47341
+ return mix(hex, palette.bg, isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT);
47342
+ };
46377
47343
  const regionFill = (r) => {
47344
+ const direct = directFill(r.color);
47345
+ if (direct) return direct;
46378
47346
  if (activeIsScore) {
46379
47347
  return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46380
47348
  }
47349
+ if (colorizeActive) return (r.iso && colorByIso.get(r.iso)) ?? neutralFill;
46381
47350
  return tagFill(r.tags, activeGroup) ?? neutralFill;
46382
47351
  };
46383
47352
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
46384
- const extentOutline = () => {
46385
- const [[w, s], [e, n]] = resolved.extent;
46386
- const N = 16;
46387
- const coords = [];
46388
- for (let i = 0; i <= N; i++) {
46389
- const t = i / N;
46390
- const lon = w + (e - w) * t;
46391
- const lat = s + (n - s) * t;
46392
- coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
46393
- }
46394
- return {
46395
- type: "Feature",
46396
- properties: {},
46397
- geometry: { type: "MultiPoint", coordinates: coords }
46398
- };
46399
- };
46400
- let fitFeatures;
46401
- if (resolved.projection === "albers-usa" && usLayer) {
46402
- fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
46403
- } else {
46404
- fitFeatures = [extentOutline()];
46405
- }
46406
- const fitTarget = { type: "FeatureCollection", features: fitFeatures };
46407
- const projection = projectionFor(resolved.projection);
46408
- if (resolved.projection !== "albers-usa") {
46409
- let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
46410
- if (centerLon > 180) centerLon -= 360;
46411
- projection.rotate([-centerLon, 0]);
46412
- }
46413
- const TITLE_GAP = 16;
47353
+ const TITLE_GAP2 = 16;
46414
47354
  let topPad = FIT_PAD;
46415
47355
  if (resolved.title && resolved.pois.length > 0) {
46416
47356
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
46417
- topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
47357
+ topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
46418
47358
  }
46419
47359
  const fitBox = [
46420
47360
  [FIT_PAD, topPad],
@@ -46424,11 +47364,10 @@ function layoutMap(resolved, data, size, opts) {
46424
47364
  ]
46425
47365
  ];
46426
47366
  projection.fitExtent(fitBox, fitTarget);
46427
- const fitGB = geoBounds2(fitTarget);
46428
- const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46429
47367
  let path;
46430
47368
  let project;
46431
- if (fitIsGlobal) {
47369
+ let stretchParams = null;
47370
+ if (fitIsGlobal && !opts.preferContain) {
46432
47371
  const cb = geoPath(projection).bounds(fitTarget);
46433
47372
  const bx0 = cb[0][0];
46434
47373
  const by0 = cb[0][1];
@@ -46438,6 +47377,7 @@ function layoutMap(resolved, data, size, opts) {
46438
47377
  const oy = fitBox[0][1];
46439
47378
  const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46440
47379
  const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47380
+ stretchParams = { sx, sy, ox, oy, bx0, by0 };
46441
47381
  const stretch = (x, y) => [
46442
47382
  ox + (x - bx0) * sx,
46443
47383
  oy + (y - by0) * sy
@@ -46469,7 +47409,9 @@ function layoutMap(resolved, data, size, opts) {
46469
47409
  const insets = [];
46470
47410
  const insetRegions = [];
46471
47411
  const insetLabelSeeds = [];
46472
- if (resolved.projection === "albers-usa" && usLayer) {
47412
+ const akRef = resolved.regions.some((r) => r.iso === "US-AK") || resolved.pois.some((p) => inAlaska(p.lon, p.lat));
47413
+ const hiRef = resolved.regions.some((r) => r.iso === "US-HI") || resolved.pois.some((p) => inHawaii(p.lon, p.lat));
47414
+ if (resolved.projection === "albers-usa" && usLayer && (akRef || hiRef)) {
46473
47415
  const PAD = 8;
46474
47416
  const GAP = 12;
46475
47417
  const yB = height - FIT_PAD;
@@ -46500,38 +47442,14 @@ function layoutMap(resolved, data, size, opts) {
46500
47442
  }
46501
47443
  return y;
46502
47444
  };
46503
- const coastTop = (x0, xr) => {
47445
+ const coastFloor = (x0, xr) => {
46504
47446
  const n = 24;
46505
- const pts = [];
46506
47447
  let maxY = -Infinity;
46507
47448
  for (let i = 0; i <= n; i++) {
46508
- const x = x0 + (xr - x0) * i / n;
46509
- const y = at(x);
46510
- if (y > -Infinity) {
46511
- pts.push([x, y]);
46512
- if (y > maxY) maxY = y;
46513
- }
46514
- }
46515
- if (pts.length === 0) return () => yB - height * 0.42;
46516
- let m = 0;
46517
- if (pts.length >= 2) {
46518
- let sx = 0, sy = 0, sxx = 0, sxy = 0;
46519
- for (const [x, y] of pts) {
46520
- sx += x;
46521
- sy += y;
46522
- sxx += x * x;
46523
- sxy += x * y;
46524
- }
46525
- const den = pts.length * sxx - sx * sx;
46526
- if (den !== 0) m = (pts.length * sxy - sx * sy) / den;
46527
- }
46528
- m = Math.max(-0.35, Math.min(0.35, m));
46529
- let c = -Infinity;
46530
- for (const [x, y] of pts) {
46531
- const need = y - m * x + GAP;
46532
- if (need > c) c = need;
46533
- }
46534
- return (x) => m * x + c;
47449
+ const y = at(x0 + (xr - x0) * i / n);
47450
+ if (y > maxY) maxY = y;
47451
+ }
47452
+ return maxY;
46535
47453
  };
46536
47454
  const placeInset = (iso, proj, boxX, iwReq) => {
46537
47455
  const f = usLayer.get(iso);
@@ -46540,19 +47458,15 @@ function layoutMap(resolved, data, size, opts) {
46540
47458
  const iw = Math.min(iwReq, width - FIT_PAD - x0 - 2 * PAD);
46541
47459
  if (iw < 24) return boxX;
46542
47460
  const xr = x0 + iw + 2 * PAD;
46543
- const top = coastTop(x0, xr);
46544
- const yL = top(x0);
46545
- const yR = top(xr);
47461
+ const floor = coastFloor(x0, xr);
47462
+ const topGuess = floor > -Infinity ? floor + GAP : yB - height * 0.42;
46546
47463
  proj.fitWidth(iw, f);
46547
47464
  const bb = geoPath(proj).bounds(f);
46548
47465
  const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
46549
47466
  const needH = sh + 2 * PAD;
46550
- let topFit = Math.max(yL, yR);
47467
+ let topFit = topGuess;
46551
47468
  const bottom = Math.min(topFit + needH, yB);
46552
47469
  if (bottom - topFit < needH) topFit = bottom - needH;
46553
- const lift = topFit - Math.max(yL, yR);
46554
- const topL = yL + lift;
46555
- const topR = yR + lift;
46556
47470
  proj.fitExtent(
46557
47471
  [
46558
47472
  [x0 + PAD, topFit + PAD],
@@ -46562,8 +47476,18 @@ function layoutMap(resolved, data, size, opts) {
46562
47476
  );
46563
47477
  const d = geoPath(proj)(f) ?? "";
46564
47478
  if (!d) return xr;
47479
+ let contextLand;
47480
+ if (iso === "US-AK") {
47481
+ const can = worldLayer.get("CA");
47482
+ const cd = can ? geoPath(proj)(can) ?? "" : "";
47483
+ if (cd)
47484
+ contextLand = {
47485
+ d: cd,
47486
+ fill: colorizeActive ? colorByIso.get("CA") ?? foreignFill : foreignFill
47487
+ };
47488
+ }
46565
47489
  const r = regionById.get(iso);
46566
- let fill2 = neutralFill;
47490
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? neutralFill : neutralFill;
46567
47491
  let lineNumber = -1;
46568
47492
  if (r?.layer === "us-state") {
46569
47493
  fill2 = regionFill(r);
@@ -46571,21 +47495,25 @@ function layoutMap(resolved, data, size, opts) {
46571
47495
  }
46572
47496
  insets.push({
46573
47497
  x: x0,
46574
- y: Math.min(topL, topR),
47498
+ y: topFit,
46575
47499
  w: xr - x0,
46576
- h: bottom - Math.min(topL, topR),
47500
+ h: bottom - topFit,
46577
47501
  points: [
46578
- [x0, topL],
46579
- [xr, topR],
47502
+ [x0, topFit],
47503
+ [xr, topFit],
46580
47504
  [xr, bottom],
46581
47505
  [x0, bottom]
46582
- ]
47506
+ ],
47507
+ // The FITTED inset projection (just fit to this box) — captured so the
47508
+ // geo-query can invert pixels inside the frame back to AK/HI coords.
47509
+ projection: proj,
47510
+ ...contextLand && { contextLand }
46583
47511
  });
46584
47512
  insetRegions.push({
46585
47513
  id: iso,
46586
47514
  d,
46587
47515
  fill: fill2,
46588
- stroke: regionStroke,
47516
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46589
47517
  lineNumber,
46590
47518
  layer: "us-state",
46591
47519
  ...r?.value !== void 0 && { value: r.value },
@@ -46598,13 +47526,16 @@ function layoutMap(resolved, data, size, opts) {
46598
47526
  }
46599
47527
  return xr;
46600
47528
  };
46601
- const akRight = placeInset(
46602
- "US-AK",
46603
- alaskaProjection(),
46604
- FIT_PAD,
46605
- width * 0.15
46606
- );
46607
- placeInset("US-HI", hawaiiProjection(), akRight + 24, width * 0.1);
47529
+ let akRight = FIT_PAD;
47530
+ if (akRef)
47531
+ akRight = placeInset("US-AK", alaskaProjection(), FIT_PAD, width * 0.15);
47532
+ if (hiRef)
47533
+ placeInset(
47534
+ "US-HI",
47535
+ hawaiiProjection(),
47536
+ akRef ? akRight + 24 : FIT_PAD,
47537
+ width * 0.1
47538
+ );
46608
47539
  }
46609
47540
  const conusFit = resolved.projection === "albers-usa" && !!usLayer;
46610
47541
  const classifyExtent = conusFit ? geoBounds2(fitTarget) : resolved.extent;
@@ -46620,15 +47551,24 @@ function layoutMap(resolved, data, size, opts) {
46620
47551
  };
46621
47552
  const ringOverlapsView = (ring) => {
46622
47553
  let loMin = Infinity, loMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
47554
+ const lons = [];
46623
47555
  for (const [rawLon] of ring) {
46624
47556
  const lon = normLon(rawLon);
47557
+ lons.push(lon);
46625
47558
  if (lon < loMin) loMin = lon;
46626
47559
  if (lon > loMax) loMax = lon;
46627
47560
  if (rawLon < rawMin) rawMin = rawLon;
46628
47561
  if (rawLon > rawMax) rawMax = rawLon;
46629
47562
  }
46630
- if (loMax - loMin > 270) return false;
46631
- if (rawMax - rawMin > 180 && loMax - loMin < 90) return false;
47563
+ lons.sort((a, b) => a - b);
47564
+ let maxGap = 0;
47565
+ for (let i = 1; i < lons.length; i++)
47566
+ maxGap = Math.max(maxGap, lons[i] - lons[i - 1]);
47567
+ if (lons.length > 1)
47568
+ maxGap = Math.max(maxGap, lons[0] + 360 - lons[lons.length - 1]);
47569
+ const occupiedArc = 360 - maxGap;
47570
+ if (occupiedArc > 270) return false;
47571
+ if (rawMax - rawMin > 180 && occupiedArc < 90) return false;
46632
47572
  let px0 = Infinity, py0 = Infinity, px1 = -Infinity, py1 = -Infinity, anyFinite = false;
46633
47573
  for (const [lon, lat] of ring) {
46634
47574
  const p = project(lon, lat);
@@ -46701,7 +47641,7 @@ function layoutMap(resolved, data, size, opts) {
46701
47641
  const regions = [];
46702
47642
  const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
46703
47643
  for (const [iso, f] of layerFeatures) {
46704
- if (layerKind === "us-state" && usContext && INSET_STATES.has(iso))
47644
+ if (layerKind === "us-state" && usContext && resolved.projection === "albers-usa" && INSET_STATES.has(iso))
46705
47645
  continue;
46706
47646
  if (layerKind === "country" && usContext && iso === "US") continue;
46707
47647
  if (layerKind === "country" && iso === "AQ" && !regionById.has("AQ"))
@@ -46713,7 +47653,8 @@ function layoutMap(resolved, data, size, opts) {
46713
47653
  if (!d) continue;
46714
47654
  const isThisLayer = r?.layer === layerKind;
46715
47655
  const isForeign = layerKind === "country" && usContext && iso !== "US";
46716
- let fill2 = isForeign ? foreignFill : neutralFill;
47656
+ const baseFill = isForeign ? foreignFill : neutralFill;
47657
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? baseFill : baseFill;
46717
47658
  let label;
46718
47659
  let lineNumber = -1;
46719
47660
  let layer = "base";
@@ -46722,12 +47663,14 @@ function layoutMap(resolved, data, size, opts) {
46722
47663
  lineNumber = r.lineNumber;
46723
47664
  layer = layerKind;
46724
47665
  label = r.name;
47666
+ } else {
47667
+ label = f.properties?.name;
46725
47668
  }
46726
47669
  regions.push({
46727
47670
  id: iso,
46728
47671
  d,
46729
47672
  fill: fill2,
46730
- stroke: regionStroke,
47673
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46731
47674
  lineNumber,
46732
47675
  layer,
46733
47676
  ...label !== void 0 && { label },
@@ -46749,13 +47692,88 @@ function layoutMap(resolved, data, size, opts) {
46749
47692
  id: "lake",
46750
47693
  d,
46751
47694
  fill: water,
46752
- stroke: "none",
47695
+ stroke: lakeStroke,
46753
47696
  lineNumber: -1,
46754
47697
  layer: "base"
46755
47698
  });
46756
47699
  }
46757
47700
  }
46758
- const riverColor = water;
47701
+ const pointInRings = (px, py, rings) => {
47702
+ let inside = false;
47703
+ for (const ring of rings) {
47704
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
47705
+ const [xi, yi] = ring[i];
47706
+ const [xj, yj] = ring[j];
47707
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
47708
+ inside = !inside;
47709
+ }
47710
+ }
47711
+ return inside;
47712
+ };
47713
+ const fillHitTargets = [...regions, ...insetRegions].map((r) => ({
47714
+ fill: r.fill,
47715
+ rings: parsePathRings(r.d)
47716
+ }));
47717
+ const fillAt = (x, y) => {
47718
+ let hit = water;
47719
+ for (const t of fillHitTargets)
47720
+ if (pointInRings(x, y, t.rings)) hit = t.fill;
47721
+ return hit;
47722
+ };
47723
+ const labelOnFill = (fill2) => {
47724
+ const color = contrastRatio(fill2, palette.textOnFillDark) >= contrastRatio(fill2, palette.textOnFillLight) ? palette.textOnFillDark : palette.textOnFillLight;
47725
+ const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47726
+ return {
47727
+ color,
47728
+ halo: contrastRatio(fill2, color) < REGION_LABEL_HALO_RATIO,
47729
+ haloColor
47730
+ };
47731
+ };
47732
+ const reliefAllowed = resolved.directives.noRelief !== true;
47733
+ const relief = [];
47734
+ let reliefHatch = null;
47735
+ if (reliefAllowed && data.mountainRanges) {
47736
+ for (const [, f] of decodeLayer(data.mountainRanges)) {
47737
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
47738
+ if (!viewF) continue;
47739
+ const area2 = path.area(viewF);
47740
+ if (!Number.isFinite(area2) || area2 < RELIEF_MIN_AREA) continue;
47741
+ const box = path.bounds(viewF);
47742
+ if (box[1][0] - box[0][0] < RELIEF_MIN_DIM || box[1][1] - box[0][1] < RELIEF_MIN_DIM)
47743
+ continue;
47744
+ const d = path(viewF) ?? "";
47745
+ if (!d) continue;
47746
+ relief.push({ d });
47747
+ }
47748
+ if (relief.length) {
47749
+ const darkTone = isDark ? palette.bg : palette.text;
47750
+ const lightTone = isDark ? palette.text : palette.bg;
47751
+ const reliefLandRef = colorizeActive ? isDark ? palette.surface : palette.bg : neutralFill;
47752
+ const landLum = relativeLuminance(reliefLandRef);
47753
+ const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
47754
+ reliefHatch = {
47755
+ color: mix(tone, reliefLandRef, RELIEF_HATCH_STRENGTH),
47756
+ spacing: RELIEF_HATCH_SPACING,
47757
+ width: RELIEF_HATCH_WIDTH
47758
+ };
47759
+ }
47760
+ }
47761
+ let coastlineStyle = null;
47762
+ if (resolved.directives.noCoastline !== true) {
47763
+ const minDim = Math.min(width, height);
47764
+ coastlineStyle = {
47765
+ color: mix(regionStroke, water, COASTLINE_STROKE_MIX),
47766
+ // N equal-width rings: distance steps outward by COASTLINE_STEP; opacity
47767
+ // fades linearly from NEAR (innermost) to FAR (outermost).
47768
+ lines: Array.from({ length: COASTLINE_RING_COUNT }, (_, k) => ({
47769
+ d: (COASTLINE_D0 + k * COASTLINE_STEP) * minDim,
47770
+ thickness: COASTLINE_THICKNESS * minDim,
47771
+ opacity: COASTLINE_OPACITY_NEAR + (COASTLINE_OPACITY_FAR - COASTLINE_OPACITY_NEAR) * k / (COASTLINE_RING_COUNT - 1)
47772
+ })),
47773
+ minExtent: (isGlobalView ? COASTLINE_MIN_EXTENT_GLOBAL : COASTLINE_MIN_EXTENT) * minDim
47774
+ };
47775
+ }
47776
+ const riverColor = mix(palette.colors.blue, water, 32);
46759
47777
  const rivers = [];
46760
47778
  if (data.rivers) {
46761
47779
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -46776,6 +47794,9 @@ function layoutMap(resolved, data, size, opts) {
46776
47794
  return R_MIN + Math.max(0, Math.min(1, t)) * (R_MAX - R_MIN);
46777
47795
  };
46778
47796
  const poiFill = (p) => {
47797
+ const directHex = p.color ? resolveColor(p.color, palette) : null;
47798
+ if (directHex)
47799
+ return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
46779
47800
  for (const group of resolved.tagGroups) {
46780
47801
  const val = p.tags[group.name.toLowerCase()];
46781
47802
  if (!val) continue;
@@ -46808,38 +47829,108 @@ function layoutMap(resolved, data, size, opts) {
46808
47829
  const xy = project(p.lon, p.lat);
46809
47830
  if (xy) projected.push({ p, xy });
46810
47831
  }
46811
- const coloGroups = /* @__PURE__ */ new Map();
47832
+ const placePoi = (e, cx, cy, clusterId) => {
47833
+ const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
47834
+ poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
47835
+ const num = routeNumberById.get(e.p.id);
47836
+ pois.push({
47837
+ id: e.p.id,
47838
+ cx,
47839
+ cy,
47840
+ r: radiusFor(e.p),
47841
+ fill: fill2,
47842
+ stroke: stroke2,
47843
+ lineNumber: e.p.lineNumber,
47844
+ implicit: !!e.p.implicit,
47845
+ isOrigin: originIds.has(e.p.id),
47846
+ ...num !== void 0 && { routeNumber: num },
47847
+ ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags },
47848
+ ...clusterId !== void 0 && { clusterId }
47849
+ });
47850
+ };
47851
+ const clusters = [];
47852
+ const connected = /* @__PURE__ */ new Set();
47853
+ for (const e of resolved.edges) {
47854
+ connected.add(e.fromId);
47855
+ connected.add(e.toId);
47856
+ }
47857
+ for (const rt of resolved.routes) {
47858
+ rt.stopIds.forEach((id) => connected.add(id));
47859
+ }
47860
+ const radiusOf = (e) => radiusFor(e.p);
46812
47861
  for (const e of projected) {
46813
- const key = `${Math.round(e.xy[0] / COLO_EPS)},${Math.round(e.xy[1] / COLO_EPS)}`;
46814
- const arr = coloGroups.get(key);
46815
- if (arr) arr.push(e);
46816
- else coloGroups.set(key, [e]);
46817
- }
46818
- for (const group of coloGroups.values()) {
46819
- group.forEach((e, i) => {
46820
- let cx = e.xy[0];
46821
- let cy = e.xy[1];
46822
- if (group.length > 1) {
46823
- const ang = i * GOLDEN_ANGLE;
46824
- cx += Math.cos(ang) * COLO_R;
46825
- cy += Math.sin(ang) * COLO_R;
46826
- }
46827
- const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
46828
- poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
46829
- const num = routeNumberById.get(e.p.id);
46830
- pois.push({
46831
- id: e.p.id,
46832
- cx,
46833
- cy,
46834
- r: radiusFor(e.p),
46835
- fill: fill2,
46836
- stroke: stroke2,
46837
- lineNumber: e.p.lineNumber,
46838
- implicit: !!e.p.implicit,
46839
- isOrigin: originIds.has(e.p.id),
46840
- ...num !== void 0 && { routeNumber: num },
46841
- ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags }
46842
- });
47862
+ if (connected.has(e.p.id)) placePoi(e, e.xy[0], e.xy[1]);
47863
+ }
47864
+ const groups = [];
47865
+ for (const e of projected) {
47866
+ if (connected.has(e.p.id)) continue;
47867
+ const r = radiusOf(e);
47868
+ const near = groups.find(
47869
+ (g) => g.some(
47870
+ (q) => Math.hypot(q.xy[0] - e.xy[0], q.xy[1] - e.xy[1]) < (r + radiusOf(q)) * STACK_OVERLAP
47871
+ )
47872
+ );
47873
+ if (near) near.push(e);
47874
+ else groups.push([e]);
47875
+ }
47876
+ for (const g of groups) {
47877
+ if (g.length === 1) {
47878
+ placePoi(g[0], g[0].xy[0], g[0].xy[1]);
47879
+ continue;
47880
+ }
47881
+ const clusterId = g[0].p.id;
47882
+ const cx0 = g.reduce((s, e) => s + e.xy[0], 0) / g.length;
47883
+ const cy0 = g.reduce((s, e) => s + e.xy[1], 0) / g.length;
47884
+ const maxR = Math.max(...g.map(radiusOf));
47885
+ const sep = 2 * maxR + STACK_RING_GAP;
47886
+ const ringR = Math.max(
47887
+ COLO_R,
47888
+ sep / (2 * Math.sin(Math.PI / Math.max(g.length, 2)))
47889
+ );
47890
+ const positions = g.map((e, i) => {
47891
+ if (g.length <= STACK_RING_MAX) {
47892
+ const ang2 = -Math.PI / 2 + i * 2 * Math.PI / g.length;
47893
+ return {
47894
+ e,
47895
+ mx: cx0 + Math.cos(ang2) * ringR,
47896
+ my: cy0 + Math.sin(ang2) * ringR
47897
+ };
47898
+ }
47899
+ const ang = i * GOLDEN_ANGLE;
47900
+ const rr = ringR * Math.sqrt((i + 1) / g.length);
47901
+ return { e, mx: cx0 + Math.cos(ang) * rr, my: cy0 + Math.sin(ang) * rr };
47902
+ });
47903
+ let minX = cx0 - maxR;
47904
+ let maxX = cx0 + maxR;
47905
+ let minY = cy0 - maxR;
47906
+ let maxY = cy0 + maxR;
47907
+ for (const { mx, my, e } of positions) {
47908
+ const r = radiusOf(e);
47909
+ minX = Math.min(minX, mx - r);
47910
+ maxX = Math.max(maxX, mx + r);
47911
+ minY = Math.min(minY, my - r);
47912
+ maxY = Math.max(maxY, my + r);
47913
+ }
47914
+ let dx = 0;
47915
+ let dy = 0;
47916
+ if (minX + dx < 2) dx = 2 - minX;
47917
+ if (maxX + dx > width - 2) dx = width - 2 - maxX;
47918
+ if (minY + dy < 2) dy = 2 - minY;
47919
+ if (maxY + dy > height - 2) dy = height - 2 - maxY;
47920
+ const legsOut = [];
47921
+ for (const { e, mx, my } of positions) {
47922
+ const fx = mx + dx;
47923
+ const fy = my + dy;
47924
+ placePoi(e, fx, fy, clusterId);
47925
+ legsOut.push({ x2: fx, y2: fy, color: poiFill(e.p).fill });
47926
+ }
47927
+ clusters.push({
47928
+ id: clusterId,
47929
+ cx: cx0 + dx,
47930
+ cy: cy0 + dy,
47931
+ count: g.length,
47932
+ hitR: ringR + maxR + 6,
47933
+ legs: legsOut
46843
47934
  });
46844
47935
  }
46845
47936
  const legs = [];
@@ -46889,16 +47980,26 @@ function layoutMap(resolved, data, size, opts) {
46889
47980
  if (!a || !b) continue;
46890
47981
  const mx = (a.cx + b.cx) / 2;
46891
47982
  const my = (a.cy + b.cy) / 2;
47983
+ const bow = {
47984
+ curved: leg.style === "arc",
47985
+ offset: 0,
47986
+ labelX: mx,
47987
+ labelY: my - 4
47988
+ };
47989
+ const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46892
47990
  legs.push({
46893
- d: legPath(a, b, leg.style === "arc", 0),
47991
+ d: legPath(a, b, bow.curved, bow.offset),
46894
47992
  width: routeWidthFor(Number(leg.value)),
46895
47993
  color: mix(palette.text, palette.bg, 72),
46896
47994
  arrow: true,
46897
47995
  lineNumber: leg.lineNumber,
46898
47996
  ...leg.label !== void 0 && {
46899
47997
  label: leg.label,
46900
- labelX: mx,
46901
- labelY: my - 4
47998
+ labelX: bow.labelX,
47999
+ labelY: bow.labelY,
48000
+ labelColor: routeLabelStyle.color,
48001
+ labelHalo: routeLabelStyle.halo,
48002
+ labelHaloColor: routeLabelStyle.haloColor
46902
48003
  }
46903
48004
  });
46904
48005
  }
@@ -46926,20 +48027,29 @@ function layoutMap(resolved, data, size, opts) {
46926
48027
  const a = poiScreen.get(e.fromId);
46927
48028
  const b = poiScreen.get(e.toId);
46928
48029
  if (!a || !b) return;
46929
- const curved = e.style === "arc" || n > 1;
46930
- const offset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
48030
+ const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
46931
48031
  const mx = (a.cx + b.cx) / 2;
46932
48032
  const my = (a.cy + b.cy) / 2;
48033
+ const bow = {
48034
+ curved: e.style === "arc" || n > 1,
48035
+ offset: fanOffset,
48036
+ labelX: mx,
48037
+ labelY: my - 4
48038
+ };
48039
+ const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46933
48040
  legs.push({
46934
- d: legPath(a, b, curved, offset),
48041
+ d: legPath(a, b, bow.curved, bow.offset),
46935
48042
  width: widthFor(e),
46936
48043
  color: mix(palette.text, palette.bg, 66),
46937
48044
  arrow: e.directed,
46938
48045
  lineNumber: e.lineNumber,
46939
48046
  ...e.label !== void 0 && {
46940
48047
  label: e.label,
46941
- labelX: mx,
46942
- labelY: my - 4
48048
+ labelX: bow.labelX,
48049
+ labelY: bow.labelY,
48050
+ labelColor: edgeLabelStyle.color,
48051
+ labelHalo: edgeLabelStyle.halo,
48052
+ labelHaloColor: edgeLabelStyle.haloColor
46943
48053
  }
46944
48054
  });
46945
48055
  });
@@ -46981,25 +48091,25 @@ function layoutMap(resolved, data, size, opts) {
46981
48091
  }
46982
48092
  }
46983
48093
  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));
46984
- const regionLabelMode = resolved.directives.regionLabels ?? "off";
48094
+ const showRegionLabels = resolved.directives.noRegionLabels !== true;
48095
+ const isCompact = width < COMPACT_WIDTH_PX;
46985
48096
  const LABEL_PADX = 6;
46986
48097
  const LABEL_PADY = 3;
46987
- const labelW = (text) => measureLegendText(text, FONT) + 2 * LABEL_PADX;
46988
- const labelH = FONT + 2 * LABEL_PADY;
48098
+ const labelW = (text) => measureLegendText(text, FONT2) + 2 * LABEL_PADX;
48099
+ const labelH = FONT2 + 2 * LABEL_PADY;
46989
48100
  const pushRegionLabel = (x, y, text, fill2, lineNumber) => {
46990
- const color = contrastText(
46991
- fill2,
46992
- palette.textOnFillLight,
46993
- palette.textOnFillDark
48101
+ const { color, haloColor } = labelOnFill(fill2);
48102
+ const halfW = measureLegendText(text, FONT2) / 2;
48103
+ const overflows = [y - FONT2 * 0.55, y - FONT2 * 0.1].some(
48104
+ (sy) => fillAt(x - halfW, sy) !== fill2 || fillAt(x + halfW, sy) !== fill2
46994
48105
  );
46995
- const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
46996
48106
  labels.push({
46997
48107
  x,
46998
48108
  y,
46999
48109
  text,
47000
48110
  anchor: "middle",
47001
48111
  color,
47002
- halo: true,
48112
+ halo: overflows,
47003
48113
  haloColor,
47004
48114
  lineNumber
47005
48115
  });
@@ -47008,21 +48118,50 @@ function layoutMap(resolved, data, size, opts) {
47008
48118
  US: [-98.5, 39.5]
47009
48119
  // CONUS geographic centre (near Lebanon, Kansas)
47010
48120
  };
47011
- if (regionLabelMode === "full" || regionLabelMode === "abbrev") {
47012
- for (const r of regions) {
47013
- if (r.layer === "base" || r.label === void 0) continue;
47014
- const f = r.layer === "us-state" ? usLayer?.get(r.id) : worldLayer.get(r.id);
47015
- if (!f) continue;
48121
+ const REGION_LABEL_GAP = 2;
48122
+ const regionLabelRect = (cx, cy, text) => {
48123
+ const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
48124
+ return { x: cx - w / 2, y: cy - FONT2 / 2, w, h: FONT2 };
48125
+ };
48126
+ if (showRegionLabels) {
48127
+ const frameContainers = new Set(resolved.poiFrameContainers);
48128
+ const entries = regions.map((r) => {
48129
+ const isContainer = frameContainers.has(r.id);
48130
+ if (r.layer === "base" && !isContainer || r.label === void 0)
48131
+ return null;
48132
+ const isUsState = r.layer === "us-state" || r.id.startsWith("US-");
48133
+ const f = isUsState ? usLayer?.get(r.id) : worldLayer.get(r.id);
48134
+ if (!f) return null;
47016
48135
  const [[x0, y0], [x1, y1]] = path.bounds(f);
47017
- const text = regionLabelMode === "abbrev" ? r.id.replace(/^US-/, "") : r.label;
47018
- if (labelW(text) > x1 - x0 || labelH > y1 - y0) continue;
47019
- const anchor = r.layer !== "us-state" ? WORLD_LABEL_ANCHORS[r.id] : void 0;
48136
+ const boxW = x1 - x0;
48137
+ const boxH = y1 - y0;
48138
+ const abbrev = isUsState ? r.id.replace(/^US-/, "") : void 0;
48139
+ const candidates = abbrev !== void 0 ? isCompact ? [abbrev, r.label] : [r.label, abbrev] : [r.label];
48140
+ const anchor = !isUsState ? WORLD_LABEL_ANCHORS[r.id] : void 0;
47020
48141
  const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
47021
- if (!c || !Number.isFinite(c[0])) continue;
48142
+ if (!c || !Number.isFinite(c[0])) return null;
48143
+ return { r, c, boxW, boxH, area: boxW * boxH, candidates };
48144
+ }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
48145
+ const placedRegionRects = [];
48146
+ const POI_LABEL_PAD = 14;
48147
+ const poiObstacles = pois.map((p) => ({
48148
+ x: p.cx - p.r - POI_LABEL_PAD,
48149
+ y: p.cy - p.r - POI_LABEL_PAD,
48150
+ w: 2 * (p.r + POI_LABEL_PAD),
48151
+ h: 2 * (p.r + POI_LABEL_PAD)
48152
+ }));
48153
+ for (const { r, c, boxW, boxH, candidates } of entries) {
48154
+ const text = candidates.find((t) => {
48155
+ if (labelW(t) > boxW || labelH > boxH) return false;
48156
+ const rect = regionLabelRect(c[0], c[1], t);
48157
+ return !placedRegionRects.some((p) => rectsOverlap(rect, p)) && !poiObstacles.some((o) => rectsOverlap(rect, o));
48158
+ });
48159
+ if (text === void 0) continue;
48160
+ placedRegionRects.push(regionLabelRect(c[0], c[1], text));
47022
48161
  pushRegionLabel(c[0], c[1], text, r.fill, r.lineNumber);
47023
48162
  }
47024
48163
  for (const seed of insetLabelSeeds) {
47025
- const text = regionLabelMode === "abbrev" ? seed.iso.replace(/^US-/, "") : seed.name;
48164
+ const text = isCompact ? seed.iso.replace(/^US-/, "") : seed.name;
47026
48165
  const src = regionById.get(seed.iso);
47027
48166
  pushRegionLabel(
47028
48167
  seed.x,
@@ -47033,22 +48172,26 @@ function layoutMap(resolved, data, size, opts) {
47033
48172
  );
47034
48173
  }
47035
48174
  }
47036
- const poiLabelMode = resolved.directives.poiLabels ?? "auto";
47037
- if (poiLabelMode !== "off") {
47038
- const ordered = [...pois].sort(
47039
- (a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1)
47040
- );
48175
+ if (resolved.directives.noPoiLabels !== true) {
48176
+ const ordered = [...pois].filter((p) => p.clusterId === void 0).sort((a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1));
47041
48177
  const poiById = new Map(resolved.pois.map((q) => [q.id, q]));
47042
48178
  const labelText = (p) => {
47043
48179
  const src = poiById.get(p.id);
47044
48180
  return src?.label ?? src?.name ?? p.id;
47045
48181
  };
47046
- const poiLabH = FONT * 1.25;
48182
+ const poiLabH = FONT2 * 1.25;
47047
48183
  const labelInfo = (p) => {
47048
48184
  const text = labelText(p);
47049
- return { text, w: measureLegendText(text, FONT) };
48185
+ return { text, w: measureLegendText(text, FONT2) };
47050
48186
  };
47051
48187
  const GAP = 3;
48188
+ const clusterMembersById = /* @__PURE__ */ new Map();
48189
+ for (const p of pois) {
48190
+ if (p.clusterId === void 0) continue;
48191
+ const arr = clusterMembersById.get(p.clusterId);
48192
+ if (arr) arr.push(p);
48193
+ else clusterMembersById.set(p.clusterId, [p]);
48194
+ }
47052
48195
  const inlineRect = (p, w, side) => {
47053
48196
  switch (side) {
47054
48197
  case "right":
@@ -47078,11 +48221,11 @@ function layoutMap(resolved, data, size, opts) {
47078
48221
  const x = side === "right" ? rect.x : side === "left" ? rect.x + w : p.cx;
47079
48222
  labels.push({
47080
48223
  x,
47081
- y: rect.y + poiLabH / 2 + FONT / 3,
48224
+ y: rect.y + poiLabH / 2 + FONT2 / 3,
47082
48225
  text,
47083
48226
  anchor,
47084
48227
  color: palette.text,
47085
- halo: true,
48228
+ halo: false,
47086
48229
  haloColor: palette.bg,
47087
48230
  poiId: p.id,
47088
48231
  lineNumber: p.lineNumber
@@ -47093,43 +48236,60 @@ function layoutMap(resolved, data, size, opts) {
47093
48236
  return rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect);
47094
48237
  };
47095
48238
  const GROUP_R = 30;
47096
- const groups = [];
48239
+ const groups2 = [];
47097
48240
  for (const p of ordered) {
47098
- const near = groups.find(
48241
+ const near = groups2.find(
47099
48242
  (g) => g.some((q) => Math.hypot(q.cx - p.cx, q.cy - p.cy) < GROUP_R)
47100
48243
  );
47101
48244
  if (near) near.push(p);
47102
- else groups.push([p]);
48245
+ else groups2.push([p]);
47103
48246
  }
47104
48247
  const ROW_GAP2 = 3;
47105
48248
  const step = poiLabH + ROW_GAP2;
47106
48249
  const COL_GAP = 16;
47107
- const placeColumn = (group) => {
47108
- const items = group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48250
+ const makeItems = (group) => group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48251
+ const columnRows = (items, side) => {
47109
48252
  const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
47110
48253
  const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
47111
- const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
47112
48254
  const maxW = Math.max(...items.map((o) => o.w));
47113
- const side = right + COL_GAP + maxW <= width - 2 ? "right" : "left";
47114
- const colX = side === "right" ? right + COL_GAP : left - COL_GAP;
48255
+ const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
48256
+ const colX = side === "right" ? Math.min(right + COL_GAP, width - 2 - maxW) : Math.max(left - COL_GAP, 2 + maxW);
47115
48257
  const totalH = items.length * step;
47116
48258
  let startY = cyMid - totalH / 2;
47117
48259
  startY = Math.max(2, Math.min(startY, height - totalH - 2));
47118
- items.forEach((o, i) => {
48260
+ return items.map((o, i) => {
47119
48261
  const rowCy = startY + i * step + step / 2;
47120
- obstacles.push({
47121
- x: side === "right" ? colX : colX - o.w,
47122
- y: rowCy - poiLabH / 2,
47123
- w: o.w,
47124
- h: poiLabH
47125
- });
48262
+ return {
48263
+ o,
48264
+ colX,
48265
+ rowCy,
48266
+ rect: {
48267
+ x: side === "right" ? colX : colX - o.w,
48268
+ y: rowCy - poiLabH / 2,
48269
+ w: o.w,
48270
+ h: poiLabH
48271
+ }
48272
+ };
48273
+ });
48274
+ };
48275
+ const wouldColumnBeClean = (items, side) => columnRows(items, side).every(
48276
+ ({ rect }) => rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect)
48277
+ );
48278
+ const defaultColumnSide = (items) => {
48279
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48280
+ const maxW = Math.max(...items.map((o) => o.w));
48281
+ return right + COL_GAP + maxW <= width - 2 ? "right" : "left";
48282
+ };
48283
+ const commitColumn = (items, side, clusterId) => {
48284
+ for (const { o, colX, rowCy, rect } of columnRows(items, side)) {
48285
+ obstacles.push(rect);
47126
48286
  labels.push({
47127
48287
  x: colX,
47128
- y: rowCy + FONT / 3,
48288
+ y: rowCy + FONT2 / 3,
47129
48289
  text: o.text,
47130
48290
  anchor: side === "right" ? "start" : "end",
47131
48291
  color: palette.text,
47132
- halo: true,
48292
+ halo: false,
47133
48293
  haloColor: palette.bg,
47134
48294
  leader: {
47135
48295
  x1: o.p.cx,
@@ -47139,24 +48299,141 @@ function layoutMap(resolved, data, size, opts) {
47139
48299
  },
47140
48300
  leaderColor: o.p.fill,
47141
48301
  poiId: o.p.id,
47142
- lineNumber: o.p.lineNumber
48302
+ lineNumber: o.p.lineNumber,
48303
+ ...clusterId !== void 0 && { clusterMember: clusterId }
47143
48304
  });
48305
+ }
48306
+ };
48307
+ const pushHidden = (p) => {
48308
+ const { text, w } = labelInfo(p);
48309
+ let x = p.cx + p.r + GAP;
48310
+ let anchor = "start";
48311
+ if (x + w > width) {
48312
+ x = p.cx - p.r - GAP - w;
48313
+ anchor = "end";
48314
+ }
48315
+ const y = Math.max(0, Math.min(p.cy - poiLabH / 2, height - poiLabH));
48316
+ labels.push({
48317
+ x: anchor === "start" ? x : x + w,
48318
+ y: y + poiLabH / 2 + FONT2 / 3,
48319
+ text,
48320
+ anchor,
48321
+ color: palette.text,
48322
+ halo: false,
48323
+ haloColor: palette.bg,
48324
+ poiId: p.id,
48325
+ hidden: true,
48326
+ lineNumber: p.lineNumber
47144
48327
  });
47145
48328
  };
47146
- for (const g of groups) {
48329
+ for (const [clusterId, members] of clusterMembersById) {
48330
+ if (members.length === 0) continue;
48331
+ const items = makeItems(members);
48332
+ const side = wouldColumnBeClean(items, "right") ? "right" : wouldColumnBeClean(items, "left") ? "left" : defaultColumnSide(items);
48333
+ commitColumn(items, side, clusterId);
48334
+ }
48335
+ const maxExtent = MAX_CLUSTER_EXTENT_FACTOR * Math.min(width, height);
48336
+ const clusterPending = [];
48337
+ for (const g of groups2) {
48338
+ const items = makeItems(g);
47147
48339
  if (g.length === 1) {
47148
- const p = g[0];
47149
- const { text, w } = labelInfo(p);
48340
+ const { p, text, w } = items[0];
47150
48341
  const side = ["right", "left", "above", "below"].find(
47151
48342
  (s) => inlineFits(p, w, s)
47152
48343
  );
47153
- if (side) {
47154
- pushInline(p, text, w, side);
47155
- continue;
48344
+ if (side) pushInline(p, text, w, side);
48345
+ else commitColumn(items, defaultColumnSide(items));
48346
+ continue;
48347
+ }
48348
+ const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
48349
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48350
+ const minCy = Math.min(...items.map((o) => o.p.cy));
48351
+ const maxCy = Math.max(...items.map((o) => o.p.cy));
48352
+ const diag = Math.hypot(right - left, maxCy - minCy);
48353
+ if (diag > maxExtent || items.length > MAX_COLUMN_ROWS) {
48354
+ items.forEach((o) => pushHidden(o.p));
48355
+ } else {
48356
+ clusterPending.push(items);
48357
+ }
48358
+ }
48359
+ for (const items of clusterPending) {
48360
+ const side = ["right", "left"].find(
48361
+ (s) => wouldColumnBeClean(items, s)
48362
+ );
48363
+ if (side) commitColumn(items, side);
48364
+ else items.forEach((o) => pushHidden(o.p));
48365
+ }
48366
+ }
48367
+ if (resolved.directives.noContextLabels !== true) {
48368
+ for (const l of labels) {
48369
+ if (l.hidden) continue;
48370
+ const w = labelW(l.text);
48371
+ const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
48372
+ obstacles.push({ x, y: l.y - labelH / 2, w, h: labelH });
48373
+ }
48374
+ for (const box of insets)
48375
+ obstacles.push({ x: box.x, y: box.y, w: box.w, h: box.h });
48376
+ const countryCandidates = [];
48377
+ for (const f of worldLayer.values()) {
48378
+ const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
48379
+ if (!iso || regionById.has(iso)) continue;
48380
+ let hasReferencedSub = false;
48381
+ for (const k of regionById.keys())
48382
+ if (k.startsWith(iso + "-")) {
48383
+ hasReferencedSub = true;
48384
+ break;
47156
48385
  }
48386
+ if (hasReferencedSub) continue;
48387
+ const b = path.bounds(f);
48388
+ const [x0, y0] = b[0];
48389
+ const [x1, y1] = b[1];
48390
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48391
+ const anchorLngLat = WORLD_LABEL_ANCHORS[iso];
48392
+ const a = anchorLngLat ? project(anchorLngLat[0], anchorLngLat[1]) : path.centroid(f);
48393
+ countryCandidates.push({
48394
+ name: f.properties?.name ?? iso,
48395
+ bbox: [x0, y0, x1, y1],
48396
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48397
+ });
48398
+ }
48399
+ const framedStateContainers = (resolved.poiFrameContainers ?? []).some(
48400
+ (id) => id.startsWith("US-")
48401
+ );
48402
+ if (usLayer && framedStateContainers) {
48403
+ const containerSet = new Set(resolved.poiFrameContainers);
48404
+ for (const [iso, f] of usLayer) {
48405
+ if (containerSet.has(iso) || regionById.has(iso)) continue;
48406
+ const viewF = cullFeatureToView(f);
48407
+ if (!viewF) continue;
48408
+ const b = path.bounds(viewF);
48409
+ const [x0, y0] = b[0];
48410
+ const [x1, y1] = b[1];
48411
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48412
+ const a = path.centroid(viewF);
48413
+ countryCandidates.push({
48414
+ name: f.properties?.name ?? iso,
48415
+ bbox: [x0, y0, x1, y1],
48416
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48417
+ });
47157
48418
  }
47158
- placeColumn(g);
47159
48419
  }
48420
+ const contextLabels = placeContextLabels({
48421
+ projection: resolved.projection,
48422
+ dLonSpan,
48423
+ dLatSpan,
48424
+ width,
48425
+ height,
48426
+ waterBodies: data.waterBodies,
48427
+ countries: countryCandidates,
48428
+ palette,
48429
+ project,
48430
+ collides,
48431
+ // Water labels must stay over open water — `fillAt` returns the ocean
48432
+ // backdrop colour off-land and a region fill on-land (lakes/states count
48433
+ // as land here, which is the safe side for an ocean name).
48434
+ overLand: (x, y) => fillAt(x, y) !== water
48435
+ });
48436
+ labels.push(...contextLabels);
47160
48437
  }
47161
48438
  let legend = null;
47162
48439
  if (!resolved.directives.noLegend) {
@@ -47191,22 +48468,33 @@ function layoutMap(resolved, data, size, opts) {
47191
48468
  ...resolved.caption !== void 0 && { caption: resolved.caption },
47192
48469
  regions,
47193
48470
  rivers,
48471
+ relief,
48472
+ reliefHatch,
48473
+ coastlineStyle,
47194
48474
  legs,
47195
48475
  pois,
48476
+ clusters,
47196
48477
  labels,
47197
48478
  legend,
47198
48479
  insets,
47199
- insetRegions
48480
+ insetRegions,
48481
+ projection,
48482
+ stretch: stretchParams,
48483
+ diagnostics: []
47200
48484
  };
47201
48485
  }
47202
- 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, RIVER_WIDTH, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_WATER_LIGHT, MUTED_WATER_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, MUTED_LAND_DARK, COLO_R, GOLDEN_ANGLE, FAN_STEP, ARC_CURVE_FRAC, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, US_NON_CONUS;
48486
+ var FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT2, 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;
47203
48487
  var init_layout15 = __esm({
47204
48488
  "src/map/layout.ts"() {
47205
48489
  "use strict";
47206
48490
  init_color_utils();
48491
+ init_geo();
48492
+ init_colorize();
48493
+ init_colors();
47207
48494
  init_label_layout();
47208
48495
  init_legend_constants();
47209
48496
  init_title_constants();
48497
+ init_context_labels();
47210
48498
  FIT_PAD = 24;
47211
48499
  RAMP_FLOOR = 15;
47212
48500
  R_DEFAULT = 6;
@@ -47214,29 +48502,66 @@ var init_layout15 = __esm({
47214
48502
  R_MAX = 22;
47215
48503
  W_MIN = 1.25;
47216
48504
  W_MAX = 8;
47217
- FONT = 11;
47218
- COLO_EPS = 1.5;
47219
- LAND_TINT_LIGHT = 58;
47220
- LAND_TINT_DARK = 75;
48505
+ FONT2 = 11;
48506
+ MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48507
+ MAX_COLUMN_ROWS = 7;
48508
+ REGION_LABEL_HALO_RATIO = 4.5;
48509
+ LAND_TINT_LIGHT = 12;
48510
+ LAND_TINT_DARK = 24;
47221
48511
  TAG_TINT_LIGHT = 60;
47222
48512
  TAG_TINT_DARK = 68;
47223
- WATER_TINT = 55;
48513
+ WATER_TINT_LIGHT = 24;
48514
+ WATER_TINT_DARK = 24;
47224
48515
  RIVER_WIDTH = 1.3;
48516
+ COMPACT_WIDTH_PX = 480;
48517
+ RELIEF_MIN_AREA = 12;
48518
+ RELIEF_MIN_DIM = 2;
48519
+ RELIEF_HATCH_SPACING = 2;
48520
+ RELIEF_HATCH_WIDTH = 0.15;
48521
+ RELIEF_HATCH_STRENGTH = 32;
48522
+ COASTLINE_RING_COUNT = 5;
48523
+ COASTLINE_D0 = 16e-4;
48524
+ COASTLINE_STEP = 28e-4;
48525
+ COASTLINE_THICKNESS = 14e-4;
48526
+ COASTLINE_OPACITY_NEAR = 0.5;
48527
+ COASTLINE_OPACITY_FAR = 0.1;
48528
+ COASTLINE_MIN_EXTENT = 6e-4;
48529
+ COASTLINE_MIN_EXTENT_GLOBAL = 6e-4;
48530
+ COASTLINE_STROKE_MIX = 32;
47225
48531
  FOREIGN_TINT_LIGHT = 30;
47226
48532
  FOREIGN_TINT_DARK = 62;
47227
- MUTED_WATER_LIGHT = 14;
47228
- MUTED_WATER_DARK = 10;
47229
48533
  MUTED_FOREIGN_LIGHT = 28;
47230
48534
  MUTED_FOREIGN_DARK = 16;
47231
- MUTED_LAND_DARK = 24;
47232
48535
  COLO_R = 9;
47233
48536
  GOLDEN_ANGLE = 2.399963229728653;
48537
+ STACK_OVERLAP = 1;
48538
+ STACK_RING_MAX = 8;
48539
+ STACK_RING_GAP = 4;
47234
48540
  FAN_STEP = 16;
47235
48541
  ARC_CURVE_FRAC = 0.18;
48542
+ decodeCache = /* @__PURE__ */ new WeakMap();
47236
48543
  usConusProjection = () => geoConicEqualArea().parallels([29.5, 45.5]).rotate([96, 0]);
47237
48544
  alaskaProjection = () => geoConicEqualArea().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
47238
48545
  hawaiiProjection = () => geoMercator();
47239
48546
  INSET_STATES = /* @__PURE__ */ new Set(["US-AK", "US-HI"]);
48547
+ inAlaska = (lon, lat) => lat >= 51 && (lon <= -129 || lon >= 172);
48548
+ inHawaii = (lon, lat) => lat >= 18 && lat <= 23 && lon >= -161 && lon <= -154;
48549
+ FOREIGN_BORDER = {
48550
+ CA: [
48551
+ "US-AK",
48552
+ "US-WA",
48553
+ "US-ID",
48554
+ "US-MT",
48555
+ "US-ND",
48556
+ "US-MN",
48557
+ "US-MI",
48558
+ "US-NY",
48559
+ "US-VT",
48560
+ "US-NH",
48561
+ "US-ME"
48562
+ ],
48563
+ MX: ["US-CA", "US-AZ", "US-NM", "US-TX"]
48564
+ };
47240
48565
  US_NON_CONUS = /* @__PURE__ */ new Set([
47241
48566
  "US-AK",
47242
48567
  "US-HI",
@@ -47256,6 +48581,58 @@ __export(renderer_exports16, {
47256
48581
  renderMapForExport: () => renderMapForExport
47257
48582
  });
47258
48583
  import * as d3Selection18 from "d3-selection";
48584
+ function pointInRing2(px, py, ring) {
48585
+ let inside = false;
48586
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
48587
+ const [xi, yi] = ring[i];
48588
+ const [xj, yj] = ring[j];
48589
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
48590
+ inside = !inside;
48591
+ }
48592
+ return inside;
48593
+ }
48594
+ function ringToPath(ring) {
48595
+ let d = "";
48596
+ for (let i = 0; i < ring.length; i++)
48597
+ d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48598
+ return d + "Z";
48599
+ }
48600
+ function coastlineOuterRings(regions, minExtent) {
48601
+ const paths = [];
48602
+ for (const r of regions) {
48603
+ const rings = parsePathRings(r.d);
48604
+ for (let i = 0; i < rings.length; i++) {
48605
+ const ring = rings[i];
48606
+ if (ring.length < 3) continue;
48607
+ let minX = Infinity;
48608
+ let minY = Infinity;
48609
+ let maxX = -Infinity;
48610
+ let maxY = -Infinity;
48611
+ for (const [x, y] of ring) {
48612
+ if (x < minX) minX = x;
48613
+ if (x > maxX) maxX = x;
48614
+ if (y < minY) minY = y;
48615
+ if (y > maxY) maxY = y;
48616
+ }
48617
+ if (Math.max(maxX - minX, maxY - minY) < minExtent) continue;
48618
+ const [fx, fy] = ring[0];
48619
+ let depth = 0;
48620
+ for (let j = 0; j < rings.length; j++)
48621
+ if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48622
+ if (depth % 2 === 1) continue;
48623
+ paths.push(ringToPath(ring));
48624
+ }
48625
+ }
48626
+ return paths;
48627
+ }
48628
+ function appendWaterLines(g, outerRings, style, flatWater) {
48629
+ const d = outerRings.join(" ");
48630
+ const linesOuterFirst = [...style.lines].sort((a, b) => b.d - a.d);
48631
+ for (const line12 of linesOuterFirst) {
48632
+ 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");
48633
+ g.append("path").attr("d", d).attr("stroke", flatWater).attr("stroke-width", 2 * line12.d).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
48634
+ }
48635
+ }
47259
48636
  function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims, activeGroupOverride) {
47260
48637
  d3Selection18.select(container).selectAll(":not([data-d3-tooltip])").remove();
47261
48638
  const width = exportDims?.width ?? container.clientWidth;
@@ -47268,6 +48645,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47268
48645
  {
47269
48646
  palette,
47270
48647
  isDark,
48648
+ // Export-only: forward the contain-fit request from mapExportDimensions so a
48649
+ // clamped/floored (off-aspect) export canvas letterboxes instead of
48650
+ // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48651
+ // keeps the global stretch-fill.
48652
+ preferContain: exportDims?.preferContain ?? false,
47271
48653
  ...activeGroupOverride !== void 0 && {
47272
48654
  activeGroup: activeGroupOverride
47273
48655
  }
@@ -47281,6 +48663,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47281
48663
  const gRegions = svg.append("g").attr("class", "dgmo-map-regions");
47282
48664
  const drawRegion = (g, r, strokeWidth) => {
47283
48665
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
48666
+ if (r.label) p.attr("data-region-name", r.label);
47284
48667
  if (r.layer !== "base") {
47285
48668
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
47286
48669
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -47301,6 +48684,52 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47301
48684
  }
47302
48685
  };
47303
48686
  for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
48687
+ if (layout.relief.length && layout.reliefHatch) {
48688
+ const h = layout.reliefHatch;
48689
+ const rangeClipId = "dgmo-relief-clip";
48690
+ const landClipId = "dgmo-relief-land";
48691
+ const rangeClip = defs.append("clipPath").attr("id", rangeClipId);
48692
+ for (const s of layout.relief) rangeClip.append("path").attr("d", s.d);
48693
+ const landClip = defs.append("clipPath").attr("id", landClipId);
48694
+ for (const r of layout.regions)
48695
+ if (r.id !== "lake") landClip.append("path").attr("d", r.d);
48696
+ 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");
48697
+ for (let y = h.spacing; y < height; y += h.spacing) {
48698
+ gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
48699
+ }
48700
+ }
48701
+ if (layout.coastlineStyle) {
48702
+ const cs = layout.coastlineStyle;
48703
+ const maskId = "dgmo-map-water-mask";
48704
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48705
+ mask.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "white");
48706
+ const landD = layout.regions.filter((r) => r.id !== "lake").map((r) => r.d).join(" ");
48707
+ const lakeD = layout.regions.filter((r) => r.id === "lake").map((r) => r.d).join(" ");
48708
+ if (landD) mask.append("path").attr("d", landD).attr("fill", "black");
48709
+ if (lakeD) mask.append("path").attr("d", lakeD).attr("fill", "white");
48710
+ if (layout.insets.length) {
48711
+ const reach = Math.max(0, ...cs.lines.map((l) => l.d + l.thickness));
48712
+ for (const box of layout.insets) {
48713
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48714
+ mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
48715
+ }
48716
+ }
48717
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
48718
+ appendWaterLines(
48719
+ gWater,
48720
+ coastlineOuterRings(layout.regions, cs.minExtent),
48721
+ cs,
48722
+ layout.background
48723
+ );
48724
+ const byStroke = /* @__PURE__ */ new Map();
48725
+ for (const r of layout.regions) {
48726
+ const arr = byStroke.get(r.stroke);
48727
+ if (arr) arr.push(r.d);
48728
+ else byStroke.set(r.stroke, [r.d]);
48729
+ }
48730
+ for (const [stroke2, ds] of byStroke)
48731
+ gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48732
+ }
47304
48733
  if (layout.rivers.length) {
47305
48734
  const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47306
48735
  for (const r of layout.rivers) {
@@ -47309,15 +48738,61 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47309
48738
  }
47310
48739
  if (layout.insets.length) {
47311
48740
  const insetG = svg.append("g").attr("class", "dgmo-map-insets");
47312
- for (const box of layout.insets) {
48741
+ layout.insets.forEach((box, bi) => {
47313
48742
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
47314
48743
  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");
47315
- }
48744
+ if (box.contextLand) {
48745
+ const clipId = `dgmo-map-inset-clip-${bi}`;
48746
+ defs.append("clipPath").attr("id", clipId).append("path").attr("d", d);
48747
+ insetG.append("path").attr("d", box.contextLand.d).attr("fill", box.contextLand.fill).attr("clip-path", `url(#${clipId})`);
48748
+ }
48749
+ });
47316
48750
  for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
47317
- }
48751
+ if (layout.coastlineStyle) {
48752
+ const cs = layout.coastlineStyle;
48753
+ const maskId = "dgmo-map-inset-water-mask";
48754
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48755
+ for (const box of layout.insets) {
48756
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48757
+ mask.append("path").attr("d", d).attr("fill", "white");
48758
+ }
48759
+ layout.insets.forEach((box, bi) => {
48760
+ if (box.contextLand)
48761
+ mask.append("path").attr("d", box.contextLand.d).attr("fill", "black").attr("clip-path", `url(#dgmo-map-inset-clip-${bi})`);
48762
+ });
48763
+ for (const r of layout.insetRegions)
48764
+ if (r.id !== "lake")
48765
+ mask.append("path").attr("d", r.d).attr("fill", "black");
48766
+ for (const r of layout.insetRegions)
48767
+ if (r.id === "lake")
48768
+ mask.append("path").attr("d", r.d).attr("fill", "white");
48769
+ const clipId = "dgmo-map-inset-water-clip";
48770
+ const clip = defs.append("clipPath").attr("id", clipId);
48771
+ for (const box of layout.insets) {
48772
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48773
+ clip.append("path").attr("d", d);
48774
+ }
48775
+ const gInsetWater = insetG.append("g").attr("clip-path", `url(#${clipId})`).append("g").attr("class", "dgmo-map-inset-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
48776
+ appendWaterLines(
48777
+ gInsetWater,
48778
+ coastlineOuterRings(layout.insetRegions, cs.minExtent),
48779
+ cs,
48780
+ layout.background
48781
+ );
48782
+ for (const r of layout.insetRegions)
48783
+ gInsetWater.append("path").attr("d", r.d).attr("stroke", r.stroke).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48784
+ }
48785
+ }
48786
+ const wireSync = (sel, lineNumber) => {
48787
+ if (lineNumber < 1) return;
48788
+ sel.attr("data-line-number", lineNumber);
48789
+ if (onClickItem)
48790
+ sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
48791
+ };
47318
48792
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
47319
48793
  layout.legs.forEach((leg, i) => {
47320
48794
  const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round");
48795
+ wireSync(p, leg.lineNumber);
47321
48796
  if (leg.arrow) {
47322
48797
  const id = `dgmo-map-arrow-${i}`;
47323
48798
  const s = arrowSize(leg.width);
@@ -47325,25 +48800,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47325
48800
  p.attr("marker-end", `url(#${id})`);
47326
48801
  }
47327
48802
  if (leg.label !== void 0 && leg.labelX !== void 0) {
47328
- emitText(
48803
+ const lt = emitText(
47329
48804
  gLegs,
47330
48805
  leg.labelX,
47331
48806
  leg.labelY ?? 0,
47332
48807
  leg.label,
47333
48808
  "middle",
47334
- palette.textMuted,
47335
- haloColor,
47336
- true,
48809
+ leg.labelColor ?? palette.textMuted,
48810
+ leg.labelHaloColor ?? haloColor,
48811
+ leg.labelHalo ?? true,
47337
48812
  LABEL_FONT - 1
47338
48813
  );
48814
+ wireSync(lt, leg.lineNumber);
47339
48815
  }
47340
48816
  });
48817
+ const gSpider = svg.append("g").attr("class", "dgmo-map-spider");
48818
+ for (const cl of layout.clusters) {
48819
+ if (!exportDims) {
48820
+ 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");
48821
+ }
48822
+ for (const leg of cl.legs) {
48823
+ 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");
48824
+ }
48825
+ 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");
48826
+ }
47341
48827
  const gPois = svg.append("g").attr("class", "dgmo-map-pois");
47342
48828
  for (const poi of layout.pois) {
47343
48829
  if (poi.isOrigin) {
47344
48830
  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);
47345
48831
  }
47346
48832
  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);
48833
+ if (poi.clusterId !== void 0)
48834
+ c.attr("data-cluster-member", poi.clusterId);
47347
48835
  if (poi.tags) {
47348
48836
  for (const [group, value] of Object.entries(poi.tags)) {
47349
48837
  c.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
@@ -47371,12 +48859,32 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47371
48859
  }
47372
48860
  const gLabels = svg.append("g").attr("class", "dgmo-map-labels");
47373
48861
  for (const lab of layout.labels) {
48862
+ if (lab.hidden) {
48863
+ if (exportDims) continue;
48864
+ emitText(
48865
+ gLabels,
48866
+ lab.x,
48867
+ lab.y,
48868
+ lab.text,
48869
+ lab.anchor,
48870
+ lab.color,
48871
+ lab.haloColor,
48872
+ lab.halo,
48873
+ LABEL_FONT,
48874
+ lab.italic,
48875
+ lab.letterSpacing
48876
+ ).attr("data-poi", lab.poiId ?? null).attr("data-poi-hidden", "").style("opacity", 0).style("pointer-events", "none");
48877
+ continue;
48878
+ }
47374
48879
  if (lab.leader) {
47375
48880
  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(
47376
48881
  "stroke",
47377
48882
  lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
47378
48883
  ).attr("stroke-width", lab.leaderColor ? 1 : 0.75);
47379
48884
  if (lab.poiId !== void 0) line12.attr("data-poi", lab.poiId);
48885
+ if (lab.clusterMember !== void 0)
48886
+ line12.attr("data-cluster-member", lab.clusterMember);
48887
+ wireSync(line12, lab.lineNumber);
47380
48888
  }
47381
48889
  const t = emitText(
47382
48890
  gLabels,
@@ -47387,11 +48895,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47387
48895
  lab.color,
47388
48896
  lab.haloColor,
47389
48897
  lab.halo,
47390
- LABEL_FONT
48898
+ LABEL_FONT,
48899
+ lab.italic,
48900
+ lab.letterSpacing,
48901
+ lab.lines
47391
48902
  );
47392
48903
  if (lab.poiId !== void 0) {
47393
48904
  t.attr("data-poi", lab.poiId).style("cursor", "default");
47394
48905
  }
48906
+ if (lab.clusterMember !== void 0) {
48907
+ t.attr("data-cluster-member", lab.clusterMember);
48908
+ }
48909
+ wireSync(t, lab.lineNumber);
48910
+ }
48911
+ if (!exportDims && layout.clusters.length) {
48912
+ const gBadge = svg.append("g").attr("class", "dgmo-map-cluster-badges");
48913
+ for (const cl of layout.clusters) {
48914
+ const g = gBadge.append("g").attr("data-cluster", cl.id).style("opacity", 0).style("pointer-events", "none");
48915
+ const R = 9;
48916
+ 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);
48917
+ 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);
48918
+ emitText(
48919
+ g,
48920
+ cl.cx,
48921
+ cl.cy + 3,
48922
+ String(cl.count),
48923
+ "middle",
48924
+ palette.text,
48925
+ palette.bg,
48926
+ false,
48927
+ LABEL_FONT
48928
+ );
48929
+ }
47395
48930
  }
47396
48931
  if (layout.legend) {
47397
48932
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
@@ -47425,10 +48960,10 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47425
48960
  }
47426
48961
  }
47427
48962
  if (layout.title) {
47428
- svg.append("text").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);
48963
+ 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);
47429
48964
  }
47430
48965
  if (layout.subtitle) {
47431
- 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);
48966
+ 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);
47432
48967
  }
47433
48968
  if (layout.caption) {
47434
48969
  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);
@@ -47437,10 +48972,21 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47437
48972
  function renderMapForExport(container, resolved, data, palette, isDark, exportDims) {
47438
48973
  renderMap(container, resolved, data, palette, isDark, void 0, exportDims);
47439
48974
  }
47440
- function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize) {
47441
- const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color).text(text);
48975
+ function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize, italic, letterSpacing, lines) {
48976
+ const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color);
48977
+ if (lines && lines.length > 1) {
48978
+ const lineHeight = fontSize + 2;
48979
+ const startDy = -((lines.length - 1) / 2) * lineHeight;
48980
+ lines.forEach((ln, i) => {
48981
+ t.append("tspan").attr("x", x).attr("dy", i === 0 ? startDy : lineHeight).text(ln);
48982
+ });
48983
+ } else {
48984
+ t.text(text);
48985
+ }
48986
+ if (italic) t.attr("font-style", "italic");
48987
+ if (letterSpacing) t.attr("letter-spacing", letterSpacing);
47442
48988
  if (withHalo) {
47443
- t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7);
48989
+ 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);
47444
48990
  }
47445
48991
  return t;
47446
48992
  }
@@ -47457,6 +49003,178 @@ var init_renderer16 = __esm({
47457
49003
  }
47458
49004
  });
47459
49005
 
49006
+ // src/map/dimensions.ts
49007
+ var dimensions_exports = {};
49008
+ __export(dimensions_exports, {
49009
+ mapContentAspect: () => mapContentAspect,
49010
+ mapExportDimensions: () => mapExportDimensions
49011
+ });
49012
+ import { geoPath as geoPath2 } from "d3-geo";
49013
+ function mapContentAspect(resolved, data, ref = REF) {
49014
+ const { projection, fitTarget } = buildMapProjection(resolved, data);
49015
+ projection.fitSize([ref, ref], fitTarget);
49016
+ const b = geoPath2(projection).bounds(fitTarget);
49017
+ const w = b[1][0] - b[0][0];
49018
+ const h = b[1][1] - b[0][1];
49019
+ const aspect = w / h;
49020
+ return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49021
+ }
49022
+ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49023
+ const raw = mapContentAspect(resolved, data);
49024
+ const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49025
+ const width = baseWidth;
49026
+ let height = Math.round(width / clamped);
49027
+ let chromeReserve = 0;
49028
+ if (resolved.title && resolved.pois.length > 0) {
49029
+ const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
49030
+ chromeReserve += Math.max(FIT_PAD2, bannerBottom + TITLE_GAP) - FIT_PAD2;
49031
+ }
49032
+ let floored = false;
49033
+ if (height - chromeReserve < MIN_MAP_BAND) {
49034
+ height = Math.round(chromeReserve + MIN_MAP_BAND);
49035
+ floored = true;
49036
+ }
49037
+ const preferContain = clamped !== raw || floored;
49038
+ return { width, height, preferContain };
49039
+ }
49040
+ var FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
49041
+ var init_dimensions = __esm({
49042
+ "src/map/dimensions.ts"() {
49043
+ "use strict";
49044
+ init_title_constants();
49045
+ init_layout15();
49046
+ FIT_PAD2 = 24;
49047
+ TITLE_GAP = 16;
49048
+ ASPECT_MAX = 3;
49049
+ ASPECT_MIN = 0.9;
49050
+ MIN_MAP_BAND = 200;
49051
+ FALLBACK_ASPECT = 1.5;
49052
+ REF = 1e3;
49053
+ }
49054
+ });
49055
+
49056
+ // src/map/load-data.ts
49057
+ var load_data_exports = {};
49058
+ __export(load_data_exports, {
49059
+ loadMapData: () => loadMapData
49060
+ });
49061
+ async function loadNodeBuiltins() {
49062
+ const [{ readFile }, { fileURLToPath }, { dirname, resolve }] = await Promise.all([
49063
+ import("fs/promises"),
49064
+ import("url"),
49065
+ import("path")
49066
+ ]);
49067
+ return { readFile, fileURLToPath, dirname, resolve };
49068
+ }
49069
+ async function readJson(nb, dir, name) {
49070
+ return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
49071
+ }
49072
+ async function firstExistingDir(nb, baseDir) {
49073
+ for (const rel of CANDIDATE_DIRS) {
49074
+ const dir = nb.resolve(baseDir, rel);
49075
+ try {
49076
+ await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
49077
+ return dir;
49078
+ } catch {
49079
+ }
49080
+ }
49081
+ throw new Error(
49082
+ `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
49083
+ );
49084
+ }
49085
+ function validate(data) {
49086
+ const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
49087
+ if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
49088
+ throw new Error("map data assets are malformed (failed shape validation)");
49089
+ }
49090
+ return data;
49091
+ }
49092
+ function moduleBaseDir(nb) {
49093
+ try {
49094
+ const url = import.meta.url;
49095
+ if (url) return nb.dirname(nb.fileURLToPath(url));
49096
+ } catch {
49097
+ }
49098
+ if (typeof __dirname !== "undefined") return __dirname;
49099
+ return process.cwd();
49100
+ }
49101
+ function loadMapData() {
49102
+ cache ??= (async () => {
49103
+ const nb = await loadNodeBuiltins();
49104
+ const dir = await firstExistingDir(nb, moduleBaseDir(nb));
49105
+ const [
49106
+ worldCoarse,
49107
+ worldDetail,
49108
+ usStates,
49109
+ lakes,
49110
+ rivers,
49111
+ mountainRanges,
49112
+ naLand,
49113
+ naLakes,
49114
+ waterBodies,
49115
+ gazetteer
49116
+ ] = await Promise.all([
49117
+ // worldCoarse (110m) is LOAD-BEARING but NOT a render source: the world
49118
+ // basemap renders from worldDetail (50m) at all scales (resolver pins
49119
+ // basemaps.world = 'detail'). Coarse stays as the authoritative region
49120
+ // name index + dominant-landmass bbox source in resolver.ts. Do not drop it.
49121
+ readJson(nb, dir, FILES.worldCoarse),
49122
+ readJson(nb, dir, FILES.worldDetail),
49123
+ readJson(nb, dir, FILES.usStates),
49124
+ // Lakes/rivers/mountain/NA/water assets are optional — older bundles may predate them.
49125
+ readJson(nb, dir, FILES.lakes).catch(() => void 0),
49126
+ readJson(nb, dir, FILES.rivers).catch(() => void 0),
49127
+ readJson(nb, dir, FILES.mountainRanges).catch(
49128
+ () => void 0
49129
+ ),
49130
+ readJson(nb, dir, FILES.naLand).catch(() => void 0),
49131
+ readJson(nb, dir, FILES.naLakes).catch(() => void 0),
49132
+ readJson(nb, dir, FILES.waterBodies).catch(() => void 0),
49133
+ readJson(nb, dir, FILES.gazetteer)
49134
+ ]);
49135
+ return validate({
49136
+ worldCoarse,
49137
+ worldDetail,
49138
+ usStates,
49139
+ gazetteer,
49140
+ ...lakes && { lakes },
49141
+ ...rivers && { rivers },
49142
+ ...mountainRanges && { mountainRanges },
49143
+ ...naLand && { naLand },
49144
+ ...naLakes && { naLakes },
49145
+ ...waterBodies && { waterBodies }
49146
+ });
49147
+ })().catch((e) => {
49148
+ cache = void 0;
49149
+ throw e;
49150
+ });
49151
+ return cache;
49152
+ }
49153
+ var FILES, CANDIDATE_DIRS, cache;
49154
+ var init_load_data = __esm({
49155
+ "src/map/load-data.ts"() {
49156
+ "use strict";
49157
+ FILES = {
49158
+ worldCoarse: "world-coarse.json",
49159
+ worldDetail: "world-detail.json",
49160
+ usStates: "us-states.json",
49161
+ lakes: "lakes.json",
49162
+ rivers: "rivers.json",
49163
+ mountainRanges: "mountain-ranges.json",
49164
+ naLand: "na-land.json",
49165
+ naLakes: "na-lakes.json",
49166
+ waterBodies: "water-bodies.json",
49167
+ gazetteer: "gazetteer.json"
49168
+ };
49169
+ CANDIDATE_DIRS = [
49170
+ "./data",
49171
+ "./map-data",
49172
+ "../map-data",
49173
+ "../src/map/data"
49174
+ ];
49175
+ }
49176
+ });
49177
+
47460
49178
  // src/pyramid/renderer.ts
47461
49179
  var renderer_exports17 = {};
47462
49180
  __export(renderer_exports17, {
@@ -49459,8 +51177,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
49459
51177
  const lines = splitParticipantLabel(p.label, LABEL_MAX_CHARS);
49460
51178
  if (lines.length === 0) continue;
49461
51179
  const widest = Math.max(...lines.map((l) => l.length));
49462
- const labelWidth = widest * LABEL_CHAR_WIDTH + 10;
49463
- uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth);
51180
+ const labelWidth2 = widest * LABEL_CHAR_WIDTH + 10;
51181
+ uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth2);
49464
51182
  }
49465
51183
  uniformBoxWidth = Math.min(MAX_BOX_WIDTH, uniformBoxWidth);
49466
51184
  const effectiveGap = Math.max(PARTICIPANT_GAP, uniformBoxWidth + 30);
@@ -52159,15 +53877,15 @@ function renderArcDiagram(container, parsed, palette, _isDark, onClickItem, expo
52159
53877
  textColor,
52160
53878
  onClickItem
52161
53879
  );
52162
- const neighbors = /* @__PURE__ */ new Map();
52163
- for (const node of nodes) neighbors.set(node, /* @__PURE__ */ new Set());
53880
+ const neighbors2 = /* @__PURE__ */ new Map();
53881
+ for (const node of nodes) neighbors2.set(node, /* @__PURE__ */ new Set());
52164
53882
  for (const link of links) {
52165
- neighbors.get(link.source).add(link.target);
52166
- neighbors.get(link.target).add(link.source);
53883
+ neighbors2.get(link.source).add(link.target);
53884
+ neighbors2.get(link.target).add(link.source);
52167
53885
  }
52168
53886
  const FADE_OPACITY3 = 0.1;
52169
53887
  function handleMouseEnter(hovered) {
52170
- const connected = neighbors.get(hovered);
53888
+ const connected = neighbors2.get(hovered);
52171
53889
  g.selectAll(".arc-link").each(function() {
52172
53890
  const el = d3Selection23.select(this);
52173
53891
  const src = el.attr("data-source");
@@ -54102,7 +55820,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54102
55820
  8,
54103
55821
  Math.floor(OVERLAP_WRAP_TARGET_W / OVERLAP_CH_W)
54104
55822
  );
54105
- function wrapLabel2(text, maxChars) {
55823
+ function wrapLabel3(text, maxChars) {
54106
55824
  const words = text.split(/\s+/).filter(Boolean);
54107
55825
  const lines = [];
54108
55826
  let cur = "";
@@ -54148,7 +55866,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54148
55866
  if (!ov.label) continue;
54149
55867
  const idxs = ov.sets.map((s) => vennSets.findIndex((vs) => vs.name === s));
54150
55868
  if (idxs.some((idx) => idx < 0)) continue;
54151
- const lines = wrapLabel2(ov.label, MAX_WRAP_CHARS);
55869
+ const lines = wrapLabel3(ov.label, MAX_WRAP_CHARS);
54152
55870
  wrappedOverlapLabels.set(ov, lines);
54153
55871
  const dir = predictOverlapDirRaw(idxs);
54154
55872
  const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
@@ -55585,25 +57303,29 @@ async function renderForExport(content, theme, palette, viewState, options) {
55585
57303
  if (detectedType === "map") {
55586
57304
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55587
57305
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55588
- const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
55589
57306
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
57307
+ const { mapExportDimensions: mapExportDimensions2 } = await Promise.resolve().then(() => (init_dimensions(), dimensions_exports));
55590
57308
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55591
57309
  const mapParsed = parseMap2(content);
55592
- let mapData;
55593
- try {
55594
- mapData = await loadMapData2();
55595
- } catch {
55596
- return "";
57310
+ let mapData = options?.mapData;
57311
+ if (!mapData) {
57312
+ const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
57313
+ try {
57314
+ mapData = await loadMapData2();
57315
+ } catch {
57316
+ return "";
57317
+ }
55597
57318
  }
55598
57319
  const mapResolved = resolveMap2(mapParsed, mapData);
55599
- const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
57320
+ const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57321
+ const container2 = createExportContainer(dims2.width, dims2.height);
55600
57322
  renderMapForExport2(
55601
57323
  container2,
55602
57324
  mapResolved,
55603
57325
  mapData,
55604
57326
  effectivePalette2,
55605
57327
  theme === "dark",
55606
- { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
57328
+ dims2
55607
57329
  );
55608
57330
  return finalizeSvgExport(container2, theme, effectivePalette2);
55609
57331
  }
@@ -56445,7 +58167,8 @@ async function render(content, options) {
56445
58167
  ...options?.c4Container !== void 0 && {
56446
58168
  c4Container: options.c4Container
56447
58169
  },
56448
- ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup }
58170
+ ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup },
58171
+ ...options?.mapData !== void 0 && { mapData: options.mapData }
56449
58172
  });
56450
58173
  if (chartType === "map") {
56451
58174
  try {
@@ -56456,7 +58179,7 @@ async function render(content, options) {
56456
58179
  Promise.resolve().then(() => (init_load_data(), load_data_exports))
56457
58180
  ]
56458
58181
  );
56459
- const data = await loadMapData2();
58182
+ const data = options?.mapData ?? await loadMapData2();
56460
58183
  diagnostics = [...resolveMap2(parseMap2(content), data).diagnostics];
56461
58184
  } catch {
56462
58185
  }
@@ -56624,21 +58347,20 @@ var DIRECTIVE_KEYWORDS = /* @__PURE__ */ new Set([
56624
58347
  // Sequence
56625
58348
  "activations",
56626
58349
  "no-activations",
56627
- // Map (§24B) directives
56628
- "region",
56629
- "projection",
58350
+ // Map (§24B) directives — cosmetics on by default, bare `no-*` opt-outs
56630
58351
  "region-metric",
56631
58352
  "poi-metric",
56632
58353
  "flow-metric",
56633
- "region-labels",
56634
- "poi-labels",
56635
- "default-country",
56636
- "default-state",
56637
- "no-legend",
56638
- "muted",
56639
- "natural",
56640
- "subtitle",
58354
+ "locale",
58355
+ "active-tag",
56641
58356
  "caption",
58357
+ "no-legend",
58358
+ "no-coastline",
58359
+ "no-relief",
58360
+ "no-context-labels",
58361
+ "no-region-labels",
58362
+ "no-poi-labels",
58363
+ "no-colorize",
56642
58364
  "poi",
56643
58365
  "route",
56644
58366
  // Data charts
@@ -56931,7 +58653,11 @@ var ATTRIBUTE_KEYS = /* @__PURE__ */ new Set([
56931
58653
  "collapsed",
56932
58654
  "tech",
56933
58655
  "span",
56934
- "split"
58656
+ "split",
58657
+ // Map (§24B) reserved keys
58658
+ "value",
58659
+ "label",
58660
+ "style"
56935
58661
  ]);
56936
58662
  function applyAttributeKeys(tokens) {
56937
58663
  for (let i = 0; i < tokens.length - 1; i++) {
@@ -57304,7 +59030,7 @@ pre.dgmo, code.language-dgmo, pre > code.language-dgmo,
57304
59030
 
57305
59031
  // src/auto/index.ts
57306
59032
  init_safe_href();
57307
- var VERSION = "0.21.0";
59033
+ var VERSION = "0.22.0";
57308
59034
  var DEFAULTS = {
57309
59035
  theme: "auto",
57310
59036
  palette: "nord",