@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.cjs CHANGED
@@ -93,18 +93,18 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
93
93
  const results = [];
94
94
  for (let i = 0; i < points.length; i++) {
95
95
  const pt = points[i];
96
- const labelWidth = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
96
+ const labelWidth2 = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
97
97
  let best = null;
98
98
  const directions = [
99
99
  {
100
100
  // Above
101
101
  gen: (offset) => {
102
- const lx = pt.cx - labelWidth / 2;
102
+ const lx = pt.cx - labelWidth2 / 2;
103
103
  const ly = pt.cy - offset - labelHeight;
104
- if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
104
+ if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
105
105
  return null;
106
106
  return {
107
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
107
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
108
108
  textX: pt.cx,
109
109
  textY: ly + labelHeight / 2,
110
110
  anchor: "middle"
@@ -114,12 +114,12 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
114
114
  {
115
115
  // Below
116
116
  gen: (offset) => {
117
- const lx = pt.cx - labelWidth / 2;
117
+ const lx = pt.cx - labelWidth2 / 2;
118
118
  const ly = pt.cy + offset;
119
- if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
119
+ if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
120
120
  return null;
121
121
  return {
122
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
122
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
123
123
  textX: pt.cx,
124
124
  textY: ly + labelHeight / 2,
125
125
  anchor: "middle"
@@ -131,10 +131,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
131
131
  gen: (offset) => {
132
132
  const lx = pt.cx + offset;
133
133
  const ly = pt.cy - labelHeight / 2;
134
- if (lx + labelWidth > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
134
+ if (lx + labelWidth2 > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
135
135
  return null;
136
136
  return {
137
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
137
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
138
138
  textX: lx,
139
139
  textY: pt.cy,
140
140
  anchor: "start"
@@ -144,13 +144,13 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
144
144
  {
145
145
  // Left
146
146
  gen: (offset) => {
147
- const lx = pt.cx - offset - labelWidth;
147
+ const lx = pt.cx - offset - labelWidth2;
148
148
  const ly = pt.cy - labelHeight / 2;
149
149
  if (lx < chartBounds.left || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
150
150
  return null;
151
151
  return {
152
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
153
- textX: lx + labelWidth,
152
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
153
+ textX: lx + labelWidth2,
154
154
  textY: pt.cy,
155
155
  anchor: "end"
156
156
  };
@@ -200,10 +200,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
200
200
  }
201
201
  }
202
202
  if (!best) {
203
- const lx = pt.cx - labelWidth / 2;
203
+ const lx = pt.cx - labelWidth2 / 2;
204
204
  const ly = pt.cy - minGap - labelHeight;
205
205
  best = {
206
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
206
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
207
207
  textX: pt.cx,
208
208
  textY: ly + labelHeight / 2,
209
209
  anchor: "middle",
@@ -763,6 +763,9 @@ var init_reserved_key_registry = __esm({
763
763
  "value",
764
764
  "label",
765
765
  "style"
766
+ // `surface:` was removed in the 2026-06-02 defaults-on review — it is no longer
767
+ // a recognized metadata key (the route/edge surface feature was cut; §24B.7).
768
+ // A stray `surface: water` is no longer captured as a reserved key.
766
769
  ]);
767
770
  ORG_REGISTRY = staticRegistry([
768
771
  "color",
@@ -1825,77 +1828,266 @@ function getSegmentColors(palette, count) {
1825
1828
  (_, i) => hslToHex(Math.round((startHue + i * step) % 360), avgS, avgL)
1826
1829
  );
1827
1830
  }
1831
+ function politicalTints(palette, count, isDark) {
1832
+ if (count <= 0) return [];
1833
+ const base = isDark ? palette.surface : palette.bg;
1834
+ const c = palette.colors;
1835
+ const swatches = [
1836
+ .../* @__PURE__ */ new Set([
1837
+ c.green,
1838
+ c.yellow,
1839
+ c.orange,
1840
+ c.purple,
1841
+ c.red,
1842
+ c.teal,
1843
+ c.cyan,
1844
+ c.blue
1845
+ ])
1846
+ ];
1847
+ const bands = isDark ? POLITICAL_TINT_BANDS.dark : POLITICAL_TINT_BANDS.light;
1848
+ const out = [];
1849
+ for (const pct of bands) {
1850
+ if (out.length >= count) break;
1851
+ for (const s of swatches) out.push(mix(s, base, pct));
1852
+ }
1853
+ return out.slice(0, count);
1854
+ }
1855
+ var POLITICAL_TINT_BANDS;
1828
1856
  var init_color_utils = __esm({
1829
1857
  "src/palettes/color-utils.ts"() {
1830
1858
  "use strict";
1859
+ POLITICAL_TINT_BANDS = {
1860
+ light: [32, 48, 64, 80],
1861
+ dark: [44, 58, 72, 86]
1862
+ };
1831
1863
  }
1832
1864
  });
1833
1865
 
1834
- // src/palettes/bold.ts
1835
- var boldPalette;
1836
- var init_bold = __esm({
1837
- "src/palettes/bold.ts"() {
1866
+ // src/palettes/atlas.ts
1867
+ var atlasPalette;
1868
+ var init_atlas = __esm({
1869
+ "src/palettes/atlas.ts"() {
1838
1870
  "use strict";
1839
1871
  init_registry();
1840
- boldPalette = {
1841
- id: "bold",
1842
- name: "Bold",
1872
+ atlasPalette = {
1873
+ id: "atlas",
1874
+ name: "Atlas",
1843
1875
  light: {
1844
- bg: "#ffffff",
1845
- surface: "#f0f0f0",
1846
- overlay: "#f0f0f0",
1847
- border: "#cccccc",
1848
- text: "#000000",
1849
- textMuted: "#666666",
1850
- textOnFillLight: "#ffffff",
1851
- textOnFillDark: "#000000",
1852
- primary: "#0000ff",
1853
- secondary: "#ff00ff",
1854
- accent: "#00cccc",
1855
- destructive: "#ff0000",
1876
+ bg: "#f3ead3",
1877
+ // warm manila / parchment
1878
+ surface: "#ece0c0",
1879
+ // deeper paper (cards, panels)
1880
+ overlay: "#e8dab8",
1881
+ // popovers, dropdowns
1882
+ border: "#bcaa86",
1883
+ // muted sepia rule line
1884
+ text: "#463a26",
1885
+ // aged sepia-brown ink
1886
+ textMuted: "#7a6a4f",
1887
+ // faded annotation ink
1888
+ textOnFillLight: "#f7f1de",
1889
+ // parchment (light text on dark fills)
1890
+ textOnFillDark: "#3a2e1c",
1891
+ // deep ink (dark text on light fills)
1892
+ primary: "#5b7a99",
1893
+ // pull-down map ocean (steel-blue)
1894
+ secondary: "#7e9a6f",
1895
+ // lowland sage / celadon
1896
+ accent: "#b07f7c",
1897
+ // dusty rose
1898
+ destructive: "#b25a45",
1899
+ // brick / terracotta
1856
1900
  colors: {
1857
- red: "#ff0000",
1858
- orange: "#ff8000",
1859
- yellow: "#ffcc00",
1860
- green: "#00cc00",
1861
- blue: "#0000ff",
1862
- purple: "#cc00cc",
1863
- teal: "#008080",
1864
- cyan: "#00cccc",
1865
- gray: "#808080",
1866
- black: "#000000",
1867
- white: "#f0f0f0"
1901
+ red: "#bf6a52",
1902
+ // terracotta brick
1903
+ orange: "#cf9a5c",
1904
+ // map tan / ochre
1905
+ yellow: "#cdb35e",
1906
+ // straw / muted lemon
1907
+ green: "#7e9a6f",
1908
+ // sage / celadon lowland
1909
+ blue: "#5b7a99",
1910
+ // steel-blue ocean
1911
+ purple: "#9a7fa6",
1912
+ // dusty lilac / mauve
1913
+ teal: "#6fa094",
1914
+ // muted seafoam
1915
+ cyan: "#79a7b5",
1916
+ // shallow-water blue
1917
+ gray: "#8a7d68",
1918
+ // warm taupe
1919
+ black: "#463a26",
1920
+ // ink
1921
+ white: "#ece0c0"
1922
+ // paper
1868
1923
  }
1869
1924
  },
1870
1925
  dark: {
1871
- bg: "#000000",
1872
- surface: "#111111",
1873
- overlay: "#1a1a1a",
1874
- border: "#333333",
1875
- text: "#ffffff",
1876
- textMuted: "#aaaaaa",
1877
- textOnFillLight: "#ffffff",
1878
- textOnFillDark: "#000000",
1879
- primary: "#00ccff",
1880
- secondary: "#ff00ff",
1881
- accent: "#ffff00",
1882
- destructive: "#ff0000",
1926
+ bg: "#1e2a33",
1927
+ // deep map ocean (night globe)
1928
+ surface: "#27353f",
1929
+ // raised ocean
1930
+ overlay: "#2e3d48",
1931
+ // popovers, dropdowns
1932
+ border: "#3d4f5c",
1933
+ // depth-contour line
1934
+ text: "#e8dcc0",
1935
+ // parchment ink, inverted
1936
+ textMuted: "#a89a7d",
1937
+ // faded label
1938
+ textOnFillLight: "#f7f1de",
1939
+ // parchment
1940
+ textOnFillDark: "#1a242c",
1941
+ // deep ocean ink
1942
+ primary: "#7ba0bf",
1943
+ // brighter ocean
1944
+ secondary: "#9bb588",
1945
+ // sage, lifted
1946
+ accent: "#cf9a96",
1947
+ // dusty rose, lifted
1948
+ destructive: "#c9745c",
1949
+ // brick, lifted
1883
1950
  colors: {
1884
- red: "#ff0000",
1885
- orange: "#ff8000",
1886
- yellow: "#ffff00",
1887
- green: "#00ff00",
1888
- blue: "#0066ff",
1889
- purple: "#ff00ff",
1890
- teal: "#00cccc",
1891
- cyan: "#00ffff",
1892
- gray: "#808080",
1893
- black: "#111111",
1894
- white: "#ffffff"
1951
+ red: "#cf7a60",
1952
+ // terracotta
1953
+ orange: "#d9a96a",
1954
+ // tan / ochre
1955
+ yellow: "#d8c074",
1956
+ // straw
1957
+ green: "#9bb588",
1958
+ // sage lowland
1959
+ blue: "#7ba0bf",
1960
+ // ocean
1961
+ purple: "#b59ac0",
1962
+ // lilac / mauve
1963
+ teal: "#85b3a6",
1964
+ // seafoam
1965
+ cyan: "#92bccb",
1966
+ // shallow-water blue
1967
+ gray: "#9a8d76",
1968
+ // warm taupe
1969
+ black: "#27353f",
1970
+ // raised ocean
1971
+ white: "#e8dcc0"
1972
+ // parchment
1895
1973
  }
1896
1974
  }
1897
1975
  };
1898
- registerPalette(boldPalette);
1976
+ registerPalette(atlasPalette);
1977
+ }
1978
+ });
1979
+
1980
+ // src/palettes/blueprint.ts
1981
+ var blueprintPalette;
1982
+ var init_blueprint = __esm({
1983
+ "src/palettes/blueprint.ts"() {
1984
+ "use strict";
1985
+ init_registry();
1986
+ blueprintPalette = {
1987
+ id: "blueprint",
1988
+ name: "Blueprint",
1989
+ light: {
1990
+ bg: "#f4f8fb",
1991
+ // pale drafting white (faint cyan)
1992
+ surface: "#e6eef4",
1993
+ // drafting panel
1994
+ overlay: "#dde9f1",
1995
+ // popovers, dropdowns
1996
+ border: "#aac3d6",
1997
+ // pale blue grid line
1998
+ text: "#123a5e",
1999
+ // blueprint navy ink
2000
+ textMuted: "#4f7390",
2001
+ // faint draft note
2002
+ textOnFillLight: "#f4f8fb",
2003
+ // drafting white
2004
+ textOnFillDark: "#0c2f4d",
2005
+ // deep blueprint navy
2006
+ primary: "#1f5e8c",
2007
+ // blueprint blue
2008
+ secondary: "#5b7d96",
2009
+ // steel
2010
+ accent: "#b08a3e",
2011
+ // draftsman's ochre highlight
2012
+ destructive: "#c0504d",
2013
+ // correction red
2014
+ colors: {
2015
+ red: "#c25a4e",
2016
+ // correction red
2017
+ orange: "#c2823e",
2018
+ // ochre
2019
+ yellow: "#c2a843",
2020
+ // pencil gold
2021
+ green: "#4f8a6b",
2022
+ // drafting green
2023
+ blue: "#1f5e8c",
2024
+ // blueprint blue
2025
+ purple: "#6f5e96",
2026
+ // indigo pencil
2027
+ teal: "#3a8a8a",
2028
+ // teal
2029
+ cyan: "#3f8fb5",
2030
+ // cyan
2031
+ gray: "#7e8e98",
2032
+ // graphite
2033
+ black: "#123a5e",
2034
+ // navy ink
2035
+ white: "#e6eef4"
2036
+ // panel
2037
+ }
2038
+ },
2039
+ dark: {
2040
+ bg: "#103a5e",
2041
+ // deep blueprint blue (cyanotype ground)
2042
+ surface: "#16466e",
2043
+ // raised sheet
2044
+ overlay: "#1c5180",
2045
+ // popovers, dropdowns
2046
+ border: "#3a6f96",
2047
+ // grid line
2048
+ text: "#eaf2f8",
2049
+ // chalk white
2050
+ textMuted: "#9fc0d6",
2051
+ // faint chalk note
2052
+ textOnFillLight: "#eaf2f8",
2053
+ // chalk white
2054
+ textOnFillDark: "#0c2f4d",
2055
+ // deep blueprint navy
2056
+ primary: "#7fb8d8",
2057
+ // chalk cyan
2058
+ secondary: "#9fb8c8",
2059
+ // pale steel
2060
+ accent: "#d8c27a",
2061
+ // chalk amber
2062
+ destructive: "#e08a7a",
2063
+ // chalk correction red
2064
+ colors: {
2065
+ red: "#e0907e",
2066
+ // chalk red
2067
+ orange: "#e0ab78",
2068
+ // chalk amber
2069
+ yellow: "#e3d089",
2070
+ // chalk gold
2071
+ green: "#93c79e",
2072
+ // chalk green
2073
+ blue: "#8ec3e0",
2074
+ // chalk cyan-blue
2075
+ purple: "#b6a6d8",
2076
+ // chalk indigo
2077
+ teal: "#84c7c2",
2078
+ // chalk teal
2079
+ cyan: "#9fd6e0",
2080
+ // chalk cyan
2081
+ gray: "#aebecb",
2082
+ // chalk graphite
2083
+ black: "#16466e",
2084
+ // raised sheet
2085
+ white: "#eaf2f8"
2086
+ // chalk white
2087
+ }
2088
+ }
2089
+ };
2090
+ registerPalette(blueprintPalette);
1899
2091
  }
1900
2092
  });
1901
2093
 
@@ -2392,6 +2584,120 @@ var init_rose_pine = __esm({
2392
2584
  }
2393
2585
  });
2394
2586
 
2587
+ // src/palettes/slate.ts
2588
+ var slatePalette;
2589
+ var init_slate = __esm({
2590
+ "src/palettes/slate.ts"() {
2591
+ "use strict";
2592
+ init_registry();
2593
+ slatePalette = {
2594
+ id: "slate",
2595
+ name: "Slate",
2596
+ light: {
2597
+ bg: "#ffffff",
2598
+ // clean slide white
2599
+ surface: "#f3f5f8",
2600
+ // light cool-gray panel
2601
+ overlay: "#eaeef3",
2602
+ // popovers, dropdowns
2603
+ border: "#d4dae1",
2604
+ // hairline rule
2605
+ text: "#1f2933",
2606
+ // near-black slate (softer than pure black)
2607
+ textMuted: "#5b6672",
2608
+ // secondary label
2609
+ textOnFillLight: "#ffffff",
2610
+ // light text on dark fills
2611
+ textOnFillDark: "#1f2933",
2612
+ // dark text on light fills
2613
+ primary: "#3b6ea5",
2614
+ // confident corporate blue
2615
+ secondary: "#5b6672",
2616
+ // slate gray
2617
+ accent: "#3a9188",
2618
+ // muted teal accent
2619
+ destructive: "#c0504d",
2620
+ // brick red
2621
+ colors: {
2622
+ red: "#c0504d",
2623
+ // brick
2624
+ orange: "#cc7a33",
2625
+ // muted amber
2626
+ yellow: "#c9a227",
2627
+ // gold (not neon)
2628
+ green: "#5b9357",
2629
+ // forest / sage
2630
+ blue: "#3b6ea5",
2631
+ // corporate blue
2632
+ purple: "#7d5ba6",
2633
+ // muted violet
2634
+ teal: "#3a9188",
2635
+ // teal
2636
+ cyan: "#4f96c4",
2637
+ // steel cyan
2638
+ gray: "#7e8a97",
2639
+ // cool gray
2640
+ black: "#1f2933",
2641
+ // slate ink
2642
+ white: "#f3f5f8"
2643
+ // panel
2644
+ }
2645
+ },
2646
+ dark: {
2647
+ bg: "#161b22",
2648
+ // deep slate (keynote dark)
2649
+ surface: "#202833",
2650
+ // raised panel
2651
+ overlay: "#29323e",
2652
+ // popovers, dropdowns
2653
+ border: "#38424f",
2654
+ // divider
2655
+ text: "#e6eaef",
2656
+ // off-white
2657
+ textMuted: "#9aa5b1",
2658
+ // secondary label
2659
+ textOnFillLight: "#ffffff",
2660
+ // light text on dark fills
2661
+ textOnFillDark: "#161b22",
2662
+ // dark text on light fills
2663
+ primary: "#5b9bd5",
2664
+ // lifted corporate blue
2665
+ secondary: "#8593a3",
2666
+ // slate gray, lifted
2667
+ accent: "#45b3a3",
2668
+ // teal, lifted
2669
+ destructive: "#e07b6e",
2670
+ // brick, lifted
2671
+ colors: {
2672
+ red: "#e07b6e",
2673
+ // brick
2674
+ orange: "#e0975a",
2675
+ // amber
2676
+ yellow: "#d9bd5a",
2677
+ // gold
2678
+ green: "#74b56e",
2679
+ // forest / sage
2680
+ blue: "#5b9bd5",
2681
+ // corporate blue
2682
+ purple: "#a585c9",
2683
+ // violet
2684
+ teal: "#45b3a3",
2685
+ // teal
2686
+ cyan: "#62b0d9",
2687
+ // steel cyan
2688
+ gray: "#95a1ae",
2689
+ // cool gray
2690
+ black: "#202833",
2691
+ // raised panel
2692
+ white: "#e6eaef"
2693
+ // off-white
2694
+ }
2695
+ }
2696
+ };
2697
+ registerPalette(slatePalette);
2698
+ }
2699
+ });
2700
+
2395
2701
  // src/palettes/solarized.ts
2396
2702
  var solarizedPalette;
2397
2703
  var init_solarized = __esm({
@@ -2487,6 +2793,120 @@ var init_solarized = __esm({
2487
2793
  }
2488
2794
  });
2489
2795
 
2796
+ // src/palettes/tidewater.ts
2797
+ var tidewaterPalette;
2798
+ var init_tidewater = __esm({
2799
+ "src/palettes/tidewater.ts"() {
2800
+ "use strict";
2801
+ init_registry();
2802
+ tidewaterPalette = {
2803
+ id: "tidewater",
2804
+ name: "Tidewater",
2805
+ light: {
2806
+ bg: "#eceff0",
2807
+ // weathered sea-mist paper
2808
+ surface: "#e0e4e3",
2809
+ // worn deck panel
2810
+ overlay: "#dadfdf",
2811
+ // popovers, dropdowns
2812
+ border: "#a9b2b3",
2813
+ // muted slate rule
2814
+ text: "#18313f",
2815
+ // ship's-log navy ink
2816
+ textMuted: "#51636b",
2817
+ // faded log entry
2818
+ textOnFillLight: "#f3f5f3",
2819
+ // weathered white
2820
+ textOnFillDark: "#162c38",
2821
+ // deep navy
2822
+ primary: "#1f4e6b",
2823
+ // deep-sea navy
2824
+ secondary: "#b08a4f",
2825
+ // rope / manila tan
2826
+ accent: "#c69a3e",
2827
+ // brass
2828
+ destructive: "#c1433a",
2829
+ // signal-flag red
2830
+ colors: {
2831
+ red: "#c1433a",
2832
+ // signal-flag red
2833
+ orange: "#cc7a38",
2834
+ // weathered amber
2835
+ yellow: "#d6bf5a",
2836
+ // brass gold
2837
+ green: "#4f8a6b",
2838
+ // sea-glass green
2839
+ blue: "#1f4e6b",
2840
+ // deep-sea navy
2841
+ purple: "#6a5a8c",
2842
+ // twilight harbor
2843
+ teal: "#3d8c8c",
2844
+ // sea-glass teal
2845
+ cyan: "#4f9bb5",
2846
+ // shallow water
2847
+ gray: "#8a8d86",
2848
+ // driftwood gray
2849
+ black: "#18313f",
2850
+ // navy ink
2851
+ white: "#e0e4e3"
2852
+ // deck panel
2853
+ }
2854
+ },
2855
+ dark: {
2856
+ bg: "#0f2230",
2857
+ // night-harbor deep sea
2858
+ surface: "#16303f",
2859
+ // raised hull
2860
+ overlay: "#1d3a4a",
2861
+ // popovers, dropdowns
2862
+ border: "#2c4856",
2863
+ // rigging line
2864
+ text: "#e6ebe8",
2865
+ // weathered white
2866
+ textMuted: "#9aaab0",
2867
+ // faded label
2868
+ textOnFillLight: "#f3f5f3",
2869
+ // weathered white
2870
+ textOnFillDark: "#0f2230",
2871
+ // deep sea
2872
+ primary: "#4f9bc4",
2873
+ // lifted sea blue
2874
+ secondary: "#c9a46a",
2875
+ // rope tan, lifted
2876
+ accent: "#d9b25a",
2877
+ // brass, lifted
2878
+ destructive: "#e06a5e",
2879
+ // signal red, lifted
2880
+ colors: {
2881
+ red: "#e06a5e",
2882
+ // signal-flag red
2883
+ orange: "#df9a52",
2884
+ // amber
2885
+ yellow: "#e0c662",
2886
+ // brass gold
2887
+ green: "#6fb58c",
2888
+ // sea-glass green
2889
+ blue: "#4f9bc4",
2890
+ // sea blue
2891
+ purple: "#9486bf",
2892
+ // twilight harbor
2893
+ teal: "#5cb0ac",
2894
+ // sea-glass teal
2895
+ cyan: "#62b4cf",
2896
+ // shallow water
2897
+ gray: "#9aa39c",
2898
+ // driftwood gray
2899
+ black: "#16303f",
2900
+ // raised hull
2901
+ white: "#e6ebe8"
2902
+ // weathered white
2903
+ }
2904
+ }
2905
+ };
2906
+ registerPalette(tidewaterPalette);
2907
+ }
2908
+ });
2909
+
2490
2910
  // src/palettes/tokyo-night.ts
2491
2911
  var tokyoNightPalette;
2492
2912
  var init_tokyo_night = __esm({
@@ -2762,7 +3182,8 @@ var init_monokai = __esm({
2762
3182
  // src/palettes/index.ts
2763
3183
  var palettes_exports = {};
2764
3184
  __export(palettes_exports, {
2765
- boldPalette: () => boldPalette,
3185
+ atlasPalette: () => atlasPalette,
3186
+ blueprintPalette: () => blueprintPalette,
2766
3187
  catppuccinPalette: () => catppuccinPalette,
2767
3188
  contrastText: () => contrastText,
2768
3189
  draculaPalette: () => draculaPalette,
@@ -2783,7 +3204,9 @@ __export(palettes_exports, {
2783
3204
  rosePinePalette: () => rosePinePalette,
2784
3205
  shade: () => shade,
2785
3206
  shapeFill: () => shapeFill,
3207
+ slatePalette: () => slatePalette,
2786
3208
  solarizedPalette: () => solarizedPalette,
3209
+ tidewaterPalette: () => tidewaterPalette,
2787
3210
  tint: () => tint,
2788
3211
  tokyoNightPalette: () => tokyoNightPalette
2789
3212
  });
@@ -2793,17 +3216,21 @@ var init_palettes = __esm({
2793
3216
  "use strict";
2794
3217
  init_registry();
2795
3218
  init_color_utils();
2796
- init_bold();
3219
+ init_atlas();
3220
+ init_blueprint();
2797
3221
  init_catppuccin();
2798
3222
  init_gruvbox();
2799
3223
  init_nord();
2800
3224
  init_one_dark();
2801
3225
  init_rose_pine();
3226
+ init_slate();
2802
3227
  init_solarized();
3228
+ init_tidewater();
2803
3229
  init_tokyo_night();
2804
3230
  init_dracula();
2805
3231
  init_monokai();
2806
- init_bold();
3232
+ init_atlas();
3233
+ init_blueprint();
2807
3234
  init_catppuccin();
2808
3235
  init_dracula();
2809
3236
  init_gruvbox();
@@ -2811,9 +3238,15 @@ var init_palettes = __esm({
2811
3238
  init_nord();
2812
3239
  init_one_dark();
2813
3240
  init_rose_pine();
3241
+ init_slate();
2814
3242
  init_solarized();
3243
+ init_tidewater();
2815
3244
  init_tokyo_night();
2816
3245
  palettes = {
3246
+ atlas: atlasPalette,
3247
+ blueprint: blueprintPalette,
3248
+ slate: slatePalette,
3249
+ tidewater: tidewaterPalette,
2817
3250
  nord: nordPalette,
2818
3251
  catppuccin: catppuccinPalette,
2819
3252
  solarized: solarizedPalette,
@@ -2822,8 +3255,7 @@ var init_palettes = __esm({
2822
3255
  oneDark: oneDarkPalette,
2823
3256
  rosePine: rosePinePalette,
2824
3257
  dracula: draculaPalette,
2825
- monokai: monokaiPalette,
2826
- bold: boldPalette
3258
+ monokai: monokaiPalette
2827
3259
  };
2828
3260
  }
2829
3261
  });
@@ -3333,6 +3765,9 @@ function controlsGroupCapsuleWidth(toggles) {
3333
3765
  }
3334
3766
  return w;
3335
3767
  }
3768
+ function isAppHostedControls(config, isExport) {
3769
+ return !isExport && config.controlsHost === "app" && !!config.controlsGroup && config.controlsGroup.toggles.length > 0;
3770
+ }
3336
3771
  function buildControlsGroupLayout(config, state) {
3337
3772
  const cg = config.controlsGroup;
3338
3773
  if (!cg || cg.toggles.length === 0) return void 0;
@@ -3386,6 +3821,7 @@ function buildControlsGroupLayout(config, state) {
3386
3821
  function computeLegendLayout(config, state, containerWidth) {
3387
3822
  const { groups, controls: configControls, mode } = config;
3388
3823
  const isExport = mode === "export";
3824
+ const gated = isAppHostedControls(config, isExport);
3389
3825
  const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
3390
3826
  if (isExport && !activeGroupName) {
3391
3827
  return {
@@ -3396,7 +3832,7 @@ function computeLegendLayout(config, state, containerWidth) {
3396
3832
  pills: []
3397
3833
  };
3398
3834
  }
3399
- const controlsGroupLayout = isExport ? void 0 : buildControlsGroupLayout(config, state);
3835
+ const controlsGroupLayout = isExport || gated ? void 0 : buildControlsGroupLayout(config, state);
3400
3836
  const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0 || !!g.gradient);
3401
3837
  if (visibleGroups.length === 0 && (!configControls || configControls.length === 0) && !controlsGroupLayout) {
3402
3838
  return {
@@ -8276,8 +8712,8 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8276
8712
  const pt = points[i];
8277
8713
  const ptSize = pt.size ?? symbolSize;
8278
8714
  const minGap = ptSize / 2 + 4;
8279
- const labelWidth = pt.name.length * fontSize * 0.6 + 8;
8280
- const labelX = pt.px - labelWidth / 2;
8715
+ const labelWidth2 = pt.name.length * fontSize * 0.6 + 8;
8716
+ const labelX = pt.px - labelWidth2 / 2;
8281
8717
  let bestLabelY = 0;
8282
8718
  let bestOffset = Infinity;
8283
8719
  let placed = false;
@@ -8289,7 +8725,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8289
8725
  const candidate = {
8290
8726
  x: labelX,
8291
8727
  y: labelY,
8292
- w: labelWidth,
8728
+ w: labelWidth2,
8293
8729
  h: labelHeight
8294
8730
  };
8295
8731
  let collision = false;
@@ -8331,7 +8767,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8331
8767
  const labelRect = {
8332
8768
  x: labelX,
8333
8769
  y: bestLabelY,
8334
- w: labelWidth,
8770
+ w: labelWidth2,
8335
8771
  h: labelHeight
8336
8772
  };
8337
8773
  placedLabels.push(labelRect);
@@ -8367,7 +8803,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8367
8803
  shape: {
8368
8804
  x: labelX - bgPad,
8369
8805
  y: bestLabelY - bgPad,
8370
- width: labelWidth + bgPad * 2,
8806
+ width: labelWidth2 + bgPad * 2,
8371
8807
  height: labelHeight + bgPad * 2
8372
8808
  },
8373
8809
  style: { fill: bg },
@@ -15807,10 +16243,6 @@ function parseMap(content) {
15807
16243
  handleTag(trimmed, lineNumber);
15808
16244
  continue;
15809
16245
  }
15810
- if ((firstWord === "muted" || firstWord === "natural") && trimmed === firstWord) {
15811
- handleDirective(firstWord, "", lineNumber);
15812
- continue;
15813
- }
15814
16246
  if (DIRECTIVE_SET.has(firstWord) && !trimmed.slice(firstWord.length).trimStart().startsWith(":")) {
15815
16247
  handleDirective(
15816
16248
  firstWord,
@@ -15857,28 +16289,13 @@ function parseMap(content) {
15857
16289
  pushWarning(line12, `Duplicate directive "${key}" \u2014 last value wins.`);
15858
16290
  };
15859
16291
  switch (key) {
15860
- case "region":
15861
- dup(d.region);
15862
- d.region = value;
15863
- break;
15864
- case "projection":
15865
- dup(d.projection);
15866
- if (value && ![
15867
- "equirectangular",
15868
- "natural-earth",
15869
- "albers-usa",
15870
- "mercator"
15871
- ].includes(value))
15872
- pushWarning(
15873
- line12,
15874
- `Unknown projection "${value}" (expected equirectangular | natural-earth | albers-usa | mercator).`
15875
- );
15876
- d.projection = value;
15877
- break;
15878
- case "region-metric":
16292
+ case "region-metric": {
15879
16293
  dup(d.regionMetric);
15880
- d.regionMetric = value;
16294
+ const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
16295
+ d.regionMetric = rmLabel;
16296
+ if (rmColor) d.regionMetricColor = rmColor;
15881
16297
  break;
16298
+ }
15882
16299
  case "poi-metric":
15883
16300
  dup(d.poiMetric);
15884
16301
  d.poiMetric = value;
@@ -15887,85 +16304,43 @@ function parseMap(content) {
15887
16304
  dup(d.flowMetric);
15888
16305
  d.flowMetric = value;
15889
16306
  break;
15890
- case "scale":
15891
- dup(d.scale);
15892
- {
15893
- const s = parseScale(value, line12);
15894
- if (s) d.scale = s;
15895
- }
15896
- break;
15897
- case "region-labels":
15898
- dup(d.regionLabels);
15899
- if (value && !["full", "abbrev", "off"].includes(value))
15900
- pushWarning(
15901
- line12,
15902
- `Unknown region-labels "${value}" (expected full | abbrev | off).`
15903
- );
15904
- d.regionLabels = value;
15905
- break;
15906
- case "poi-labels":
15907
- dup(d.poiLabels);
15908
- if (value && !["off", "auto", "all"].includes(value))
15909
- pushWarning(
15910
- line12,
15911
- `Unknown poi-labels "${value}" (expected off | auto | all).`
15912
- );
15913
- d.poiLabels = value;
15914
- break;
15915
- case "default-country":
15916
- dup(d.defaultCountry);
15917
- d.defaultCountry = value;
15918
- break;
15919
- case "default-state":
15920
- dup(d.defaultState);
15921
- d.defaultState = value;
16307
+ case "locale":
16308
+ dup(d.locale);
16309
+ d.locale = value;
15922
16310
  break;
15923
16311
  case "active-tag":
15924
16312
  dup(d.activeTag);
15925
16313
  d.activeTag = value;
15926
16314
  break;
16315
+ case "caption":
16316
+ dup(d.caption);
16317
+ d.caption = value;
16318
+ break;
16319
+ // ── Cosmetic `no-*` opt-outs: bare flags, idempotent (mirror `no-legend`,
16320
+ // no dup warning); each defaults the feature ON when absent. ──
15927
16321
  case "no-legend":
15928
16322
  d.noLegend = true;
15929
16323
  break;
15930
- case "muted":
15931
- case "natural":
15932
- if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
15933
- pushWarning(
15934
- line12,
15935
- `Conflicting basemap dress \u2014 "${d.basemapStyle}" then "${key}"; last wins.`
15936
- );
15937
- d.basemapStyle = key;
16324
+ case "no-coastline":
16325
+ d.noCoastline = true;
15938
16326
  break;
15939
- case "subtitle":
15940
- dup(d.subtitle);
15941
- d.subtitle = value;
16327
+ case "no-relief":
16328
+ d.noRelief = true;
15942
16329
  break;
15943
- case "caption":
15944
- dup(d.caption);
15945
- d.caption = value;
16330
+ case "no-context-labels":
16331
+ d.noContextLabels = true;
16332
+ break;
16333
+ case "no-region-labels":
16334
+ d.noRegionLabels = true;
16335
+ break;
16336
+ case "no-poi-labels":
16337
+ d.noPoiLabels = true;
16338
+ break;
16339
+ case "no-colorize":
16340
+ d.noColorize = true;
15946
16341
  break;
15947
16342
  }
15948
16343
  }
15949
- function parseScale(value, line12) {
15950
- const toks = value.split(/\s+/).filter(Boolean);
15951
- const min = Number(toks[0]);
15952
- const max = Number(toks[1]);
15953
- if (!Number.isFinite(min) || !Number.isFinite(max)) {
15954
- pushError(line12, `scale requires numeric <min> <max> (got "${value}").`);
15955
- return null;
15956
- }
15957
- const scale = { min, max };
15958
- if (toks[2] === "center") {
15959
- const c = Number(toks[3]);
15960
- if (Number.isFinite(c)) scale.center = c;
15961
- else
15962
- pushError(
15963
- line12,
15964
- `scale center requires a number (got "${toks[3] ?? ""}").`
15965
- );
15966
- }
15967
- return scale;
15968
- }
15969
16344
  function handleTag(trimmed, line12) {
15970
16345
  const m = matchTagBlockHeading(trimmed);
15971
16346
  if (!m) {
@@ -16039,6 +16414,7 @@ function parseMap(content) {
16039
16414
  };
16040
16415
  if (regionScope !== void 0) region.scope = regionScope;
16041
16416
  if (valueNum !== void 0) region.value = valueNum;
16417
+ if (split.color) region.color = split.color;
16042
16418
  regions.push(region);
16043
16419
  }
16044
16420
  function handlePoi(rest, line12, indent) {
@@ -16063,6 +16439,7 @@ function parseMap(content) {
16063
16439
  const poi = { pos, tags, meta, lineNumber: line12 };
16064
16440
  if (split.alias) poi.alias = split.alias;
16065
16441
  if (label !== void 0) poi.label = label;
16442
+ if (split.color) poi.color = split.color;
16066
16443
  pois.push(poi);
16067
16444
  open.poi = { poi, indent };
16068
16445
  }
@@ -16163,13 +16540,15 @@ function parseMap(content) {
16163
16540
  pushError(line12, `Edge has an empty endpoint: "${trimmed}".`);
16164
16541
  continue;
16165
16542
  }
16166
- const meta = k === links.length - 1 ? lastSplit.meta : {};
16543
+ const isLast = k === links.length - 1;
16544
+ const meta = isLast ? lastSplit.meta : {};
16545
+ const style = links[k].style === "arc" ? "arc" : "straight";
16167
16546
  edges.push({
16168
16547
  from,
16169
16548
  to,
16170
16549
  ...links[k].label !== void 0 && { label: links[k].label },
16171
16550
  directed: links[k].directed,
16172
- style: links[k].style,
16551
+ style,
16173
16552
  meta,
16174
16553
  lineNumber: line12
16175
16554
  });
@@ -16255,20 +16634,19 @@ var init_parser12 = __esm({
16255
16634
  LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
16256
16635
  AT_RE = /(^|[\s,])at\s*:/i;
16257
16636
  DIRECTIVE_SET = /* @__PURE__ */ new Set([
16258
- "region",
16259
- "projection",
16260
16637
  "region-metric",
16261
16638
  "poi-metric",
16262
16639
  "flow-metric",
16263
- "scale",
16264
- "region-labels",
16265
- "poi-labels",
16266
- "default-country",
16267
- "default-state",
16640
+ "locale",
16268
16641
  "active-tag",
16642
+ "caption",
16269
16643
  "no-legend",
16270
- "subtitle",
16271
- "caption"
16644
+ "no-coastline",
16645
+ "no-relief",
16646
+ "no-context-labels",
16647
+ "no-region-labels",
16648
+ "no-poi-labels",
16649
+ "no-colorize"
16272
16650
  ]);
16273
16651
  }
16274
16652
  });
@@ -24189,8 +24567,8 @@ function renderKanban(container, parsed, palette, isDark, options) {
24189
24567
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24190
24568
  for (const meta of tagMeta) {
24191
24569
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(`${meta.label}: `);
24192
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24193
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24570
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24571
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24194
24572
  metaY += sCardMetaLineHeight;
24195
24573
  }
24196
24574
  for (const detail of card.details) {
@@ -24534,8 +24912,8 @@ function renderSwimlaneCard(parent, cardLayout, tagGroups, activeTagGroup, palet
24534
24912
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24535
24913
  for (const meta of tagMeta) {
24536
24914
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", palette.textMuted).text(`${meta.label}: `);
24537
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24538
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24915
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24916
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24539
24917
  metaY += sCardMetaLineHeight;
24540
24918
  }
24541
24919
  for (const detail of card.details) {
@@ -25370,8 +25748,8 @@ function classifyEREntities(tables, relationships) {
25370
25748
  }
25371
25749
  }
25372
25750
  const mmParticipants = /* @__PURE__ */ new Set();
25373
- for (const [id, neighbors] of tableStarNeighbors) {
25374
- if (neighbors.size >= 2) mmParticipants.add(id);
25751
+ for (const [id, neighbors2] of tableStarNeighbors) {
25752
+ if (neighbors2.size >= 2) mmParticipants.add(id);
25375
25753
  }
25376
25754
  const indegreeValues = Object.values(indegreeMap);
25377
25755
  const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
@@ -26042,7 +26420,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26042
26420
  controlsExpanded,
26043
26421
  onToggleDescriptions,
26044
26422
  onToggleControlsExpand,
26045
- exportMode = false
26423
+ exportMode = false,
26424
+ controlsHost
26046
26425
  } = options ?? {};
26047
26426
  d3Selection6.select(container).selectAll(":not([data-d3-tooltip])").remove();
26048
26427
  const width = exportDims?.width ?? container.clientWidth;
@@ -26060,7 +26439,11 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26060
26439
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26061
26440
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26062
26441
  const sTitleY = sctx.structural(TITLE_Y);
26063
- const sLegendHeight = sctx.structural(
26442
+ const reserveHasDescriptions = parsed.nodes.some(
26443
+ (n) => n.description && n.description.length > 0
26444
+ );
26445
+ const willRenderLegend = parsed.tagGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26446
+ const sLegendHeight = willRenderLegend ? sctx.structural(
26064
26447
  getMaxLegendReservedHeight(
26065
26448
  {
26066
26449
  groups: parsed.tagGroups,
@@ -26069,7 +26452,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26069
26452
  },
26070
26453
  width
26071
26454
  )
26072
- );
26455
+ ) : 0;
26073
26456
  const activeGroup = resolveActiveTagGroup(
26074
26457
  parsed.tagGroups,
26075
26458
  parsed.options["active-tag"],
@@ -26384,10 +26767,10 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26384
26767
  const hasDescriptions = parsed.nodes.some(
26385
26768
  (n) => n.description && n.description.length > 0
26386
26769
  );
26387
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions;
26770
+ const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26388
26771
  if (hasLegend) {
26389
26772
  let controlsGroup;
26390
- if (hasDescriptions && onToggleDescriptions) {
26773
+ if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
26391
26774
  controlsGroup = {
26392
26775
  toggles: [
26393
26776
  {
@@ -26405,7 +26788,14 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26405
26788
  groups: parsed.tagGroups,
26406
26789
  position: { placement: "top-center", titleRelation: "below-title" },
26407
26790
  mode: exportMode ? "export" : "preview",
26408
- ...controlsGroup !== void 0 && { controlsGroup }
26791
+ // Keep inactive sibling tag groups visible as collapsed pills so the user
26792
+ // can click one to flip the active colouring dimension (preview only —
26793
+ // export shows just the active group). Without this, declaring a second
26794
+ // tag group (e.g. Team) leaves it invisible whenever another group is
26795
+ // active. The app's BoxesAndLinesPreview already wires pill clicks.
26796
+ showInactivePills: true,
26797
+ ...controlsGroup !== void 0 && { controlsGroup },
26798
+ ...controlsHost !== void 0 && { controlsHost }
26409
26799
  };
26410
26800
  const legendState = {
26411
26801
  activeGroup,
@@ -27653,8 +28043,9 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27653
28043
  const containerHeight = exportDims?.height ?? (container.getBoundingClientRect().height || 600);
27654
28044
  d3Selection7.select(container).selectAll("*").remove();
27655
28045
  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);
28046
+ const appHosted = options?.controlsHost === "app";
27656
28047
  const hasControls = !!options?.onToggleColorByDepth || !!options?.onToggleDescriptions;
27657
- const hasLegend = parsed.tagGroups.length > 0 || hasControls;
28048
+ const hasLegend = parsed.tagGroups.length > 0 || hasControls && !appHosted;
27658
28049
  const fixedLegend = !isExport && hasLegend;
27659
28050
  const legendReserve = fixedLegend ? getMaxLegendReservedHeight(
27660
28051
  {
@@ -27748,7 +28139,10 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27748
28139
  }),
27749
28140
  position: { placement: "top-center", titleRelation: "below-title" },
27750
28141
  mode: options?.exportMode ? "export" : "preview",
27751
- ...controlsToggles !== void 0 && { controlsGroup: controlsToggles }
28142
+ ...controlsToggles !== void 0 && { controlsGroup: controlsToggles },
28143
+ ...options?.controlsHost !== void 0 && {
28144
+ controlsHost: options.controlsHost
28145
+ }
27752
28146
  };
27753
28147
  const legendState = {
27754
28148
  activeGroup: options?.colorByDepth ? null : activeTagGroup !== void 0 ? activeTagGroup : parsed.options["active-tag"] ?? null,
@@ -28192,8 +28586,8 @@ function computeFieldAlignX(children) {
28192
28586
  for (const child of children) {
28193
28587
  if (child.metadata["_labelField"] === "true" && child.children.length >= 2) {
28194
28588
  const labelEl = child.children[0];
28195
- const labelWidth = labelEl.label.length * CHAR_WIDTH5;
28196
- maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
28589
+ const labelWidth2 = labelEl.label.length * CHAR_WIDTH5;
28590
+ maxLabelWidth = Math.max(maxLabelWidth, labelWidth2);
28197
28591
  labelFieldCount++;
28198
28592
  }
28199
28593
  }
@@ -33157,7 +33551,7 @@ function hasRoles(node) {
33157
33551
  function computeNodeWidth2(node, expanded, options) {
33158
33552
  const badgeVal = node.computedConcurrentInvocations === 0 && node.computedInstances > 1 ? node.computedInstances : 0;
33159
33553
  const badgeLen = badgeVal > 0 ? `${badgeVal}x`.length + 2 : 0;
33160
- const labelWidth = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33554
+ const labelWidth2 = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33161
33555
  const allKeys = [];
33162
33556
  if (node.computedRps > 0) allKeys.push("RPS");
33163
33557
  if (expanded) {
@@ -33201,7 +33595,7 @@ function computeNodeWidth2(node, expanded, options) {
33201
33595
  allKeys.push("overflow");
33202
33596
  }
33203
33597
  }
33204
- if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth);
33598
+ if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth2);
33205
33599
  const maxKeyLen = Math.max(...allKeys.map((k) => k.length));
33206
33600
  let maxRowWidth = 0;
33207
33601
  if (node.computedRps > 0) {
@@ -33289,7 +33683,7 @@ function computeNodeWidth2(node, expanded, options) {
33289
33683
  truncated.length * META_CHAR_WIDTH3 + PADDING_X3
33290
33684
  );
33291
33685
  }
33292
- return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20, descWidth);
33686
+ return Math.max(MIN_NODE_WIDTH2, labelWidth2, maxRowWidth + 20, descWidth);
33293
33687
  }
33294
33688
  function computeNodeHeight2(node, expanded, options) {
33295
33689
  const propCount = countDisplayProps(node, expanded, options);
@@ -34838,8 +35232,9 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
34838
35232
  }
34839
35233
  return groups;
34840
35234
  }
34841
- function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false) {
35235
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false, controlsHost) {
34842
35236
  if (legendGroups.length === 0 && !playback) return;
35237
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34843
35238
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
34844
35239
  if (activeGroup) {
34845
35240
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
@@ -34848,14 +35243,29 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34848
35243
  name: g.name,
34849
35244
  entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
34850
35245
  }));
34851
- if (playback) {
35246
+ if (playback && !appHostedPlayback) {
34852
35247
  allGroups.push({ name: "Playback", entries: [] });
34853
35248
  }
34854
35249
  const legendConfig = {
34855
35250
  groups: allGroups,
34856
35251
  position: { placement: "top-center", titleRelation: "below-title" },
34857
35252
  mode: exportMode ? "export" : "preview",
34858
- showEmptyGroups: true
35253
+ showEmptyGroups: true,
35254
+ ...appHostedPlayback && {
35255
+ controlsHost: "app",
35256
+ controlsGroup: {
35257
+ toggles: [
35258
+ {
35259
+ id: "playback",
35260
+ type: "toggle",
35261
+ label: "Playback",
35262
+ active: true,
35263
+ onToggle: () => {
35264
+ }
35265
+ }
35266
+ ]
35267
+ }
35268
+ }
34859
35269
  };
34860
35270
  const legendState = { activeGroup };
34861
35271
  renderLegendD3(
@@ -34906,8 +35316,9 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34906
35316
  }
34907
35317
  }
34908
35318
  }
34909
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
35319
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes, controlsHost) {
34910
35320
  d3Selection11.select(container).selectAll(":not([data-d3-tooltip])").remove();
35321
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34911
35322
  const ctx = ScaleContext.identity();
34912
35323
  const sc = buildScaledConstants(ctx);
34913
35324
  const legendGroups = computeInfraLegendGroups(
@@ -34916,7 +35327,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
34916
35327
  palette,
34917
35328
  layout.edges
34918
35329
  );
34919
- const hasLegend = legendGroups.length > 0 || !!playback;
35330
+ const hasLegend = legendGroups.length > 0 || !!playback && !appHostedPlayback;
34920
35331
  const fixedLegend = !exportMode && hasLegend;
34921
35332
  const legendDynamicH = hasLegend ? getMaxLegendReservedHeight(
34922
35333
  {
@@ -35060,7 +35471,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35060
35471
  isDark,
35061
35472
  activeGroup ?? null,
35062
35473
  playback ?? void 0,
35063
- exportMode
35474
+ exportMode,
35475
+ controlsHost
35064
35476
  );
35065
35477
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
35066
35478
  } else {
@@ -35073,7 +35485,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35073
35485
  isDark,
35074
35486
  activeGroup ?? null,
35075
35487
  playback ?? void 0,
35076
- exportMode
35488
+ exportMode,
35489
+ controlsHost
35077
35490
  );
35078
35491
  }
35079
35492
  }
@@ -42708,6 +43121,9 @@ function renderTechRadar(container, parsed, palette, isDark, onClickItem, export
42708
43121
  onToggle: (active) => options.onToggleListing(active)
42709
43122
  }
42710
43123
  ]
43124
+ },
43125
+ ...options.controlsHost !== void 0 && {
43126
+ controlsHost: options.controlsHost
42711
43127
  }
42712
43128
  };
42713
43129
  const legendState = {
@@ -44531,7 +44947,7 @@ function computeCycleLayout(parsed, options) {
44531
44947
  const circleNodes = parsed.options["circle-nodes"] === "true";
44532
44948
  const nodeDims = parsed.nodes.map((node) => {
44533
44949
  const hasDesc = !hideDescriptions && node.description.length > 0;
44534
- const labelWidth = Math.max(
44950
+ const labelWidth2 = Math.max(
44535
44951
  MIN_NODE_WIDTH4,
44536
44952
  node.label.length * LABEL_CHAR_W + NODE_PAD_X * 2
44537
44953
  );
@@ -44540,12 +44956,12 @@ function computeCycleLayout(parsed, options) {
44540
44956
  }
44541
44957
  if (!hasDesc) {
44542
44958
  return {
44543
- width: Math.min(MAX_NODE_WIDTH3, labelWidth),
44959
+ width: Math.min(MAX_NODE_WIDTH3, labelWidth2),
44544
44960
  height: PLAIN_NODE_HEIGHT,
44545
44961
  wrappedDesc: []
44546
44962
  };
44547
44963
  }
44548
- return chooseDescribedRectDims(node.description, labelWidth);
44964
+ return chooseDescribedRectDims(node.description, labelWidth2);
44549
44965
  });
44550
44966
  if (circleNodes) {
44551
44967
  const maxDiam = Math.max(...nodeDims.map((d) => d.width));
@@ -44741,10 +45157,10 @@ function computeCycleLayout(parsed, options) {
44741
45157
  scale
44742
45158
  };
44743
45159
  }
44744
- function chooseDescribedRectDims(description, labelWidth) {
45160
+ function chooseDescribedRectDims(description, labelWidth2) {
44745
45161
  const minW = Math.min(
44746
45162
  MAX_NODE_WIDTH3,
44747
- Math.max(MIN_NODE_WIDTH4, labelWidth, DESC_MIN_WIDTH)
45163
+ Math.max(MIN_NODE_WIDTH4, labelWidth2, DESC_MIN_WIDTH)
44748
45164
  );
44749
45165
  let best = null;
44750
45166
  let bestScore = Infinity;
@@ -45172,7 +45588,8 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45172
45588
  const hideDescriptions = (renderOptions?.hideDescriptions ?? false) || parsed.options["no-descriptions"] === "true" || viewState?.hd === true;
45173
45589
  const showDescriptions = !hideDescriptions;
45174
45590
  const hasDescriptions = parsed.nodes.some((n) => n.description.length > 0) || parsed.edges.some((e) => e.description.length > 0);
45175
- const hasLegend = hasDescriptions && !!renderOptions?.onToggleDescriptions;
45591
+ const appHostedControls = renderOptions?.controlsHost === "app";
45592
+ const hasLegend = !appHostedControls && hasDescriptions && !!renderOptions?.onToggleDescriptions;
45176
45593
  const showTitle = !!parsed.title && parsed.options["no-title"] !== "on";
45177
45594
  const legendOffset = hasLegend ? sLegendHeight : 0;
45178
45595
  const layoutHeight = height - (showTitle ? sTitleAreaHeight : 0) - legendOffset;
@@ -45209,7 +45626,10 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45209
45626
  groups: [],
45210
45627
  position: { placement: "top-center", titleRelation: "below-title" },
45211
45628
  mode: renderOptions?.exportMode ? "export" : "preview",
45212
- controlsGroup
45629
+ controlsGroup,
45630
+ ...renderOptions?.controlsHost !== void 0 && {
45631
+ controlsHost: renderOptions.controlsHost
45632
+ }
45213
45633
  };
45214
45634
  const legendState = {
45215
45635
  activeGroup: null,
@@ -45480,6 +45900,107 @@ function featureIndex(topo) {
45480
45900
  }
45481
45901
  return idx;
45482
45902
  }
45903
+ function buildAdjacency(topo) {
45904
+ const cached = adjacencyCache.get(topo);
45905
+ if (cached) return cached;
45906
+ const geometries = geomObject(topo).geometries;
45907
+ const nb = (0, import_topojson_client.neighbors)(geometries);
45908
+ const sets = /* @__PURE__ */ new Map();
45909
+ geometries.forEach((g, i) => {
45910
+ if (!g.type || g.type === "null") return;
45911
+ let set = sets.get(g.id);
45912
+ if (!set) {
45913
+ set = /* @__PURE__ */ new Set();
45914
+ sets.set(g.id, set);
45915
+ }
45916
+ for (const j of nb[i] ?? []) {
45917
+ const nid = geometries[j]?.id;
45918
+ if (nid && nid !== g.id) set.add(nid);
45919
+ }
45920
+ });
45921
+ const out = /* @__PURE__ */ new Map();
45922
+ for (const [iso, set] of sets) out.set(iso, [...set].sort());
45923
+ adjacencyCache.set(topo, out);
45924
+ return out;
45925
+ }
45926
+ function decodeFeatures(topo) {
45927
+ return geomObject(topo).geometries.map((g) => {
45928
+ const f = (0, import_topojson_client.feature)(topo, g);
45929
+ return {
45930
+ type: "Feature",
45931
+ id: g.id,
45932
+ properties: g.properties,
45933
+ geometry: f.geometry
45934
+ };
45935
+ });
45936
+ }
45937
+ function pointInRing(lon, lat, ring) {
45938
+ let inside = false;
45939
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
45940
+ const xi = ring[i][0];
45941
+ const yi = ring[i][1];
45942
+ const xj = ring[j][0];
45943
+ const yj = ring[j][1];
45944
+ const intersect = yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi;
45945
+ if (intersect) inside = !inside;
45946
+ }
45947
+ return inside;
45948
+ }
45949
+ function pointOnRingEdge(lon, lat, ring) {
45950
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
45951
+ const xi = ring[i][0];
45952
+ const yi = ring[i][1];
45953
+ const xj = ring[j][0];
45954
+ const yj = ring[j][1];
45955
+ if (lon < Math.min(xi, xj) - EDGE_EPS || lon > Math.max(xi, xj) + EDGE_EPS)
45956
+ continue;
45957
+ if (lat < Math.min(yi, yj) - EDGE_EPS || lat > Math.max(yi, yj) + EDGE_EPS)
45958
+ continue;
45959
+ const cross = (xj - xi) * (lat - yi) - (yj - yi) * (lon - xi);
45960
+ if (Math.abs(cross) <= EDGE_EPS) return true;
45961
+ }
45962
+ return false;
45963
+ }
45964
+ function pointInGeometry(geometry, lon, lat) {
45965
+ const g = geometry;
45966
+ if (!g) return false;
45967
+ const polys = g.type === "Polygon" ? [g.coordinates] : g.type === "MultiPolygon" ? g.coordinates : [];
45968
+ for (const rings of polys) {
45969
+ if (!rings.length) continue;
45970
+ if (pointOnRingEdge(lon, lat, rings[0])) return true;
45971
+ if (!pointInRing(lon, lat, rings[0])) continue;
45972
+ let inHole = false;
45973
+ for (let h = 1; h < rings.length; h++) {
45974
+ if (pointInRing(lon, lat, rings[h]) && !pointOnRingEdge(lon, lat, rings[h])) {
45975
+ inHole = true;
45976
+ break;
45977
+ }
45978
+ }
45979
+ if (!inHole) return true;
45980
+ }
45981
+ return false;
45982
+ }
45983
+ function regionAt(lonLat, countries, states) {
45984
+ const lon = lonLat[0];
45985
+ const lat = lonLat[1];
45986
+ let country = null;
45987
+ for (const f of countries) {
45988
+ if (pointInGeometry(f.geometry, lon, lat)) {
45989
+ country = { iso: f.id, name: f.properties.name };
45990
+ break;
45991
+ }
45992
+ }
45993
+ let state = null;
45994
+ if (country?.iso === "US" && states) {
45995
+ for (const f of states) {
45996
+ if (pointInGeometry(f.geometry, lon, lat)) {
45997
+ state = { iso: f.id, name: f.properties.name };
45998
+ break;
45999
+ }
46000
+ }
46001
+ }
46002
+ return { country, state };
46003
+ }
45483
46004
  function featureBbox(topo, geomId) {
45484
46005
  const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45485
46006
  if (!geom) return null;
@@ -45491,6 +46012,74 @@ function featureBbox(topo, geomId) {
45491
46012
  [b[1][0], b[1][1]]
45492
46013
  ];
45493
46014
  }
46015
+ function explodePolygons(gj) {
46016
+ const g = gj.geometry ?? gj;
46017
+ const t = g.type;
46018
+ const coords = g.coordinates;
46019
+ if (t === "Polygon") {
46020
+ return [
46021
+ { type: "Feature", geometry: { type: "Polygon", coordinates: coords } }
46022
+ ];
46023
+ }
46024
+ if (t === "MultiPolygon") {
46025
+ return coords.map((rings) => ({
46026
+ type: "Feature",
46027
+ geometry: { type: "Polygon", coordinates: rings }
46028
+ }));
46029
+ }
46030
+ return [];
46031
+ }
46032
+ function bboxGap(a, b) {
46033
+ const lonGap = Math.max(0, a[0][0] - b[1][0], b[0][0] - a[1][0]);
46034
+ const latGap = Math.max(0, a[0][1] - b[1][1], b[0][1] - a[1][1]);
46035
+ return Math.max(lonGap, latGap);
46036
+ }
46037
+ function featureBboxPrimary(topo, geomId) {
46038
+ const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
46039
+ if (!geom) return null;
46040
+ const gj = (0, import_topojson_client.feature)(topo, geom);
46041
+ const parts = explodePolygons(gj);
46042
+ if (parts.length <= 1) return featureBbox(topo, geomId);
46043
+ const polys = parts.map((p) => {
46044
+ const b = (0, import_d3_geo.geoBounds)(p);
46045
+ if (!b || !Number.isFinite(b[0][0])) return null;
46046
+ const wraps = b[1][0] < b[0][0];
46047
+ const bbox = [
46048
+ [b[0][0], b[0][1]],
46049
+ [b[1][0], b[1][1]]
46050
+ ];
46051
+ return { bbox, area: (0, import_d3_geo.geoArea)(p), wraps };
46052
+ }).filter(
46053
+ (p) => p !== null
46054
+ );
46055
+ if (polys.length <= 1 || polys.some((p) => p.wraps))
46056
+ return featureBbox(topo, geomId);
46057
+ const maxArea = Math.max(...polys.map((p) => p.area));
46058
+ const anchor = polys.find((p) => p.area === maxArea);
46059
+ const cluster = [
46060
+ [anchor.bbox[0][0], anchor.bbox[0][1]],
46061
+ [anchor.bbox[1][0], anchor.bbox[1][1]]
46062
+ ];
46063
+ const remaining = polys.filter((p) => p !== anchor);
46064
+ let added = true;
46065
+ while (added) {
46066
+ added = false;
46067
+ for (let i = remaining.length - 1; i >= 0; i--) {
46068
+ const p = remaining[i];
46069
+ const near = bboxGap(p.bbox, cluster) <= DETACH_GAP_DEG;
46070
+ const large = p.area >= DETACH_AREA_FRAC * maxArea;
46071
+ if (near || large) {
46072
+ cluster[0][0] = Math.min(cluster[0][0], p.bbox[0][0]);
46073
+ cluster[0][1] = Math.min(cluster[0][1], p.bbox[0][1]);
46074
+ cluster[1][0] = Math.max(cluster[1][0], p.bbox[1][0]);
46075
+ cluster[1][1] = Math.max(cluster[1][1], p.bbox[1][1]);
46076
+ remaining.splice(i, 1);
46077
+ added = true;
46078
+ }
46079
+ }
46080
+ }
46081
+ return cluster;
46082
+ }
45494
46083
  function unionExtent(boxes, points) {
45495
46084
  const lats = [];
45496
46085
  const lons = [];
@@ -45529,13 +46118,17 @@ function unionLongitudes(lons) {
45529
46118
  }
45530
46119
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45531
46120
  }
45532
- var import_topojson_client, import_d3_geo, fold;
46121
+ var import_topojson_client, import_d3_geo, fold, adjacencyCache, EDGE_EPS, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45533
46122
  var init_geo = __esm({
45534
46123
  "src/map/geo.ts"() {
45535
46124
  "use strict";
45536
46125
  import_topojson_client = require("topojson-client");
45537
46126
  import_d3_geo = require("d3-geo");
45538
46127
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
46128
+ adjacencyCache = /* @__PURE__ */ new WeakMap();
46129
+ EDGE_EPS = 1e-9;
46130
+ DETACH_GAP_DEG = 10;
46131
+ DETACH_AREA_FRAC = 0.25;
45539
46132
  }
45540
46133
  });
45541
46134
 
@@ -45553,6 +46146,12 @@ function looksUS(lat, lon) {
45553
46146
  if (lat < 15 || lat > 72) return false;
45554
46147
  return lon >= -180 && lon <= -64 || lon >= 172;
45555
46148
  }
46149
+ function looksNorthAmericaNeighbor(lat, lon) {
46150
+ return lat >= 14 && lat <= 72 && lon >= -141 && lon <= -52;
46151
+ }
46152
+ function isWholeSphere(bb) {
46153
+ return bb[0][0] <= -179 && bb[1][0] >= 179 && bb[0][1] <= -89 && bb[1][1] >= 89;
46154
+ }
45556
46155
  function resolveMap(parsed, data) {
45557
46156
  const diagnostics = [...parsed.diagnostics];
45558
46157
  const err = (line12, message, code) => {
@@ -45563,9 +46162,6 @@ function resolveMap(parsed, data) {
45563
46162
  };
45564
46163
  const result = {
45565
46164
  title: parsed.title,
45566
- ...parsed.directives.subtitle !== void 0 && {
45567
- subtitle: parsed.directives.subtitle
45568
- },
45569
46165
  ...parsed.directives.caption !== void 0 && {
45570
46166
  caption: parsed.directives.caption
45571
46167
  },
@@ -45575,7 +46171,7 @@ function resolveMap(parsed, data) {
45575
46171
  // renderer's job (step 4) — the resolver only carries `tags` + `tagGroups`
45576
46172
  // through; it never resolves a tag value to a palette color (#10).
45577
46173
  directives: { ...parsed.directives },
45578
- basemaps: { world: "coarse", subdivisions: [] },
46174
+ basemaps: { world: "detail", subdivisions: [] },
45579
46175
  regions: [],
45580
46176
  pois: [],
45581
46177
  edges: [],
@@ -45584,7 +46180,8 @@ function resolveMap(parsed, data) {
45584
46180
  [-180, -85],
45585
46181
  [180, 85]
45586
46182
  ],
45587
- projection: "natural-earth",
46183
+ projection: "equirectangular",
46184
+ poiFrameContainers: [],
45588
46185
  diagnostics,
45589
46186
  error: parsed.error
45590
46187
  };
@@ -45594,7 +46191,10 @@ function resolveMap(parsed, data) {
45594
46191
  ...[...countryIndex.values()].map((v) => v.name),
45595
46192
  ...[...usStateIndex.values()].map((v) => v.name)
45596
46193
  ];
45597
- const usScoped = parsed.directives.region === "us-states" || parsed.directives.defaultCountry?.toUpperCase() === "US" || parsed.regions.some((r) => {
46194
+ const localeRaw = parsed.directives.locale?.toUpperCase();
46195
+ const localeCountry = localeRaw ? localeRaw.split("-")[0] : void 0;
46196
+ const localeSubdivision = localeRaw && /^[A-Z]{2}-/.test(localeRaw) ? localeRaw : void 0;
46197
+ const usScoped = localeCountry === "US" || parsed.regions.some((r) => {
45598
46198
  const f = fold(r.name);
45599
46199
  return usStateIndex.has(f) && !countryIndex.has(f);
45600
46200
  }) || parsed.regions.some(
@@ -45639,12 +46239,12 @@ function resolveMap(parsed, data) {
45639
46239
  chosen = { ...inState, layer: "us-state" };
45640
46240
  } else {
45641
46241
  chosen = { ...inCountry, layer: "country" };
46242
+ warn2(
46243
+ r.lineNumber,
46244
+ `"${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}").`,
46245
+ "W_MAP_REGION_AMBIGUOUS"
46246
+ );
45642
46247
  }
45643
- warn2(
45644
- r.lineNumber,
45645
- `"${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}").`,
45646
- "W_MAP_REGION_AMBIGUOUS"
45647
- );
45648
46248
  } else if (inState) {
45649
46249
  chosen = { ...inState, layer: "us-state" };
45650
46250
  } else if (inCountry) {
@@ -45668,6 +46268,7 @@ function resolveMap(parsed, data) {
45668
46268
  name: chosen.name,
45669
46269
  layer: chosen.layer,
45670
46270
  ...r.value !== void 0 && { value: r.value },
46271
+ ...r.color !== void 0 && { color: r.color },
45671
46272
  tags: r.tags,
45672
46273
  meta: r.meta,
45673
46274
  lineNumber: r.lineNumber
@@ -45744,7 +46345,7 @@ function resolveMap(parsed, data) {
45744
46345
  if (!scope)
45745
46346
  warn2(
45746
46347
  line12,
45747
- `"${name}" is ambiguous \u2014 resolved to the most-populous match.`,
46348
+ `"${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.`,
45748
46349
  "W_MAP_AMBIGUOUS_NAME"
45749
46350
  );
45750
46351
  }
@@ -45757,17 +46358,21 @@ function resolveMap(parsed, data) {
45757
46358
  return fold(pos.name);
45758
46359
  };
45759
46360
  const poiCountries = [];
45760
- let anyNonUsPoi = false;
46361
+ let anyUsPoi = false;
46362
+ let anyNonNaPoi = false;
45761
46363
  const noteCountry = (iso) => {
45762
46364
  if (iso) {
45763
46365
  poiCountries.push(iso);
45764
- if (iso !== "US") anyNonUsPoi = true;
46366
+ if (iso === "US") anyUsPoi = true;
46367
+ if (iso !== "US" && iso !== "CA" && iso !== "MX") anyNonNaPoi = true;
45765
46368
  }
45766
46369
  };
45767
46370
  const deferred = [];
45768
46371
  for (const p of parsed.pois) {
45769
46372
  if (p.pos.kind === "coords") {
45770
- if (!looksUS(p.pos.lat, p.pos.lon)) anyNonUsPoi = true;
46373
+ if (looksUS(p.pos.lat, p.pos.lon)) anyUsPoi = true;
46374
+ else if (!looksNorthAmericaNeighbor(p.pos.lat, p.pos.lon))
46375
+ anyNonNaPoi = true;
45771
46376
  addResolvedPoi(p.pos.lat, p.pos.lon, p);
45772
46377
  continue;
45773
46378
  }
@@ -45785,14 +46390,15 @@ function resolveMap(parsed, data) {
45785
46390
  deferred.push(p);
45786
46391
  }
45787
46392
  }
45788
- const inferredCountry = parsed.directives.defaultCountry?.toUpperCase() ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46393
+ const inferredCountry = localeCountry ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46394
+ const inferredScope = localeSubdivision ?? inferredCountry;
45789
46395
  for (const p of deferred) {
45790
46396
  if (p.pos.kind !== "name") continue;
45791
46397
  const got = lookupName(
45792
46398
  p.pos.name,
45793
46399
  p.pos.scope,
45794
46400
  p.lineNumber,
45795
- inferredCountry,
46401
+ inferredScope,
45796
46402
  true
45797
46403
  );
45798
46404
  if (got.kind === "ok") {
@@ -45809,6 +46415,7 @@ function resolveMap(parsed, data) {
45809
46415
  lat,
45810
46416
  lon,
45811
46417
  ...p.label !== void 0 && { label: p.label },
46418
+ ...p.color !== void 0 && { color: p.color },
45812
46419
  tags: p.tags,
45813
46420
  meta: p.meta,
45814
46421
  lineNumber: p.lineNumber
@@ -45861,7 +46468,8 @@ function resolveMap(parsed, data) {
45861
46468
  const meta = sizeValue !== void 0 ? { value: sizeValue } : {};
45862
46469
  if (pos.kind === "coords") {
45863
46470
  const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
45864
- if (!looksUS(pos.lat, pos.lon)) anyNonUsPoi = true;
46471
+ if (looksUS(pos.lat, pos.lon)) anyUsPoi = true;
46472
+ else if (!looksNorthAmericaNeighbor(pos.lat, pos.lon)) anyNonNaPoi = true;
45865
46473
  if (!registry.has(id)) {
45866
46474
  registerPoi(
45867
46475
  id,
@@ -45884,7 +46492,7 @@ function resolveMap(parsed, data) {
45884
46492
  if (registry.has(f)) return f;
45885
46493
  const aliased = declaredByName.get(f);
45886
46494
  if (aliased) return aliased;
45887
- const got = lookupName(pos.name, pos.scope, line12, inferredCountry, true);
46495
+ const got = lookupName(pos.name, pos.scope, line12, inferredScope, true);
45888
46496
  if (got.kind !== "ok") return null;
45889
46497
  noteCountry(got.iso);
45890
46498
  registerPoi(
@@ -45941,9 +46549,12 @@ function resolveMap(parsed, data) {
45941
46549
  }
45942
46550
  routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
45943
46551
  }
46552
+ const hasUsContent = usSubdivisionReferenced || anyUsPoi || localeCountry === "US";
46553
+ const usOriented = !anyNonNaPoi && !regions.some(
46554
+ (r) => r.layer === "country" && !["US", "CA", "MX"].includes(r.iso)
46555
+ ) && hasUsContent;
45944
46556
  const subdivisions = [];
45945
- if (usSubdivisionReferenced || parsed.directives.region === "us-states")
45946
- subdivisions.push("us-states");
46557
+ if (usSubdivisionReferenced || usOriented) subdivisions.push("us-states");
45947
46558
  const regionBoxes = [];
45948
46559
  for (const ref of referencedRegionIds) {
45949
46560
  const bb = featureBbox(data.usStates, ref.id);
@@ -45951,7 +46562,7 @@ function resolveMap(parsed, data) {
45951
46562
  }
45952
46563
  for (const r of regions) {
45953
46564
  if (r.layer === "country") {
45954
- const bb = featureBbox(data.worldCoarse, r.iso);
46565
+ const bb = featureBboxPrimary(data.worldCoarse, r.iso);
45955
46566
  if (bb) regionBoxes.push(bb);
45956
46567
  }
45957
46568
  }
@@ -45961,23 +46572,56 @@ function resolveMap(parsed, data) {
45961
46572
  [-180, -85],
45962
46573
  [180, 85]
45963
46574
  ];
45964
- let extent2 = unioned ? pad(unioned, PAD_FRACTION) : DEFAULT_EXTENT;
46575
+ const basePad = regions.length > 0 ? REGION_PAD_FRACTION : PAD_FRACTION;
46576
+ let extent2 = unioned ? pad(unioned, basePad) : DEFAULT_EXTENT;
46577
+ const isPoiOnly = pois.length > 0 && regions.length === 0;
46578
+ const containerRegionIds = [];
46579
+ if (isPoiOnly) {
46580
+ const countries = decodeFeatures(data.worldDetail);
46581
+ const states = decodeFeatures(data.usStates);
46582
+ const seen = /* @__PURE__ */ new Set();
46583
+ const containerBoxes = [];
46584
+ for (const p of pois) {
46585
+ const { country, state } = regionAt([p.lon, p.lat], countries, states);
46586
+ const id = state?.iso ?? country?.iso;
46587
+ if (!id || seen.has(id)) continue;
46588
+ seen.add(id);
46589
+ containerRegionIds.push(id);
46590
+ const bb = state ? featureBbox(data.usStates, id) : featureBboxPrimary(data.worldCoarse, id);
46591
+ if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46592
+ }
46593
+ const containerUnion = unionExtent(containerBoxes, points);
46594
+ if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46595
+ }
46596
+ if (isPoiOnly) {
46597
+ const cx = (extent2[0][0] + extent2[1][0]) / 2;
46598
+ const cy = (extent2[0][1] + extent2[1][1]) / 2;
46599
+ const lon = extent2[1][0] - extent2[0][0];
46600
+ const lat = extent2[1][1] - extent2[0][1];
46601
+ const longer = Math.max(lon, lat);
46602
+ if (longer > 0 && longer < POI_ZOOM_FLOOR_DEG) {
46603
+ const k = POI_ZOOM_FLOOR_DEG / longer;
46604
+ const halfLon = lon * k / 2;
46605
+ const halfLat = lat * k / 2;
46606
+ extent2 = [
46607
+ [cx - halfLon, cy - halfLat],
46608
+ [cx + halfLon, cy + halfLat]
46609
+ ];
46610
+ }
46611
+ }
45965
46612
  const lonSpan = extent2[1][0] - extent2[0][0];
45966
46613
  const latSpan = extent2[1][1] - extent2[0][1];
45967
46614
  const span = Math.max(lonSpan, latSpan);
45968
- const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46615
+ const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
45969
46616
  let projection;
45970
- const override = parsed.directives.projection;
45971
- if (override === "equirectangular" || override === "natural-earth" || override === "albers-usa" || override === "mercator") {
45972
- projection = override;
45973
- } else if (usDominant) {
46617
+ if (isPoiOnly && usOriented && lonSpan < US_NATIONAL_LON_SPAN) {
46618
+ projection = "mercator";
46619
+ } else if (usOriented) {
45974
46620
  projection = "albers-usa";
45975
- } else if (span > WORLD_SPAN) {
46621
+ } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
45976
46622
  projection = "equirectangular";
45977
- } else if (span < MERCATOR_MAX_SPAN) {
45978
- projection = "mercator";
45979
46623
  } else {
45980
- projection = "equirectangular";
46624
+ projection = "mercator";
45981
46625
  }
45982
46626
  if (lonSpan >= 180) {
45983
46627
  extent2 = [
@@ -45990,11 +46634,20 @@ function resolveMap(parsed, data) {
45990
46634
  result.edges = edges;
45991
46635
  result.routes = routes;
45992
46636
  result.basemaps = {
45993
- world: span > WORLD_SPAN ? "coarse" : "detail",
46637
+ // Tier is intentionally pinned to detail (50m) at ALL scales. Diagrammo maps
46638
+ // are presentational (palette tints, relief hachures, POI hubs), not
46639
+ // survey-grade — recognizability > generalization: 110m coarse drops the
46640
+ // Italian boot to a stump at world scale. `WORLD_SPAN` lives on only for the
46641
+ // projection decision (the `usOriented`/`span > WORLD_SPAN` chain above); it
46642
+ // no longer gates basemap resolution.
46643
+ // `worldCoarse` is still loaded — it's the authoritative name/bbox index
46644
+ // (featureIndex, featureBboxPrimary), not dead code.
46645
+ world: "detail",
45994
46646
  subdivisions
45995
46647
  };
45996
46648
  result.extent = extent2;
45997
46649
  result.projection = projection;
46650
+ result.poiFrameContainers = containerRegionIds;
45998
46651
  result.error = parsed.error ?? firstError(diagnostics);
45999
46652
  return result;
46000
46653
  }
@@ -46031,17 +46684,20 @@ function firstError(diags) {
46031
46684
  const e = diags.find((d) => d.severity === "error");
46032
46685
  return e ? formatDgmoError(e) : null;
46033
46686
  }
46034
- var WORLD_SPAN, MERCATOR_MAX_SPAN, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46687
+ 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;
46035
46688
  var init_resolver2 = __esm({
46036
46689
  "src/map/resolver.ts"() {
46037
46690
  "use strict";
46038
46691
  init_diagnostics();
46039
46692
  init_geo();
46040
46693
  WORLD_SPAN = 90;
46041
- MERCATOR_MAX_SPAN = 25;
46694
+ MERCATOR_MAX_LAT = 80;
46042
46695
  PAD_FRACTION = 0.05;
46696
+ REGION_PAD_FRACTION = 0.12;
46043
46697
  WORLD_LAT_SOUTH = -58;
46044
46698
  WORLD_LAT_NORTH = 78;
46699
+ POI_ZOOM_FLOOR_DEG = 7;
46700
+ US_NATIONAL_LON_SPAN = 48;
46045
46701
  REGION_ALIASES = {
46046
46702
  // Common everyday names → the Natural-Earth display name actually shipped.
46047
46703
  "united states": "united states of america",
@@ -46119,112 +46775,269 @@ var init_resolver2 = __esm({
46119
46775
  }
46120
46776
  });
46121
46777
 
46122
- // src/map/load-data.ts
46123
- var load_data_exports = {};
46124
- __export(load_data_exports, {
46125
- loadMapData: () => loadMapData
46778
+ // src/map/colorize.ts
46779
+ function assignColors(isos, adjacency) {
46780
+ const sorted = [...isos].sort();
46781
+ const byIso = /* @__PURE__ */ new Map();
46782
+ let maxIndex = -1;
46783
+ for (const iso of sorted) {
46784
+ const taken = /* @__PURE__ */ new Set();
46785
+ for (const n of adjacency.get(iso) ?? []) {
46786
+ const c = byIso.get(n);
46787
+ if (c !== void 0) taken.add(c);
46788
+ }
46789
+ let h = 0;
46790
+ while (taken.has(h)) h++;
46791
+ byIso.set(iso, h);
46792
+ if (h > maxIndex) maxIndex = h;
46793
+ }
46794
+ return { byIso, huesNeeded: maxIndex + 1 };
46795
+ }
46796
+ var init_colorize = __esm({
46797
+ "src/map/colorize.ts"() {
46798
+ "use strict";
46799
+ }
46126
46800
  });
46127
- async function loadNodeBuiltins() {
46128
- const [{ readFile }, { fileURLToPath }, { dirname, resolve }] = await Promise.all([
46129
- import("fs/promises"),
46130
- import("url"),
46131
- import("path")
46132
- ]);
46133
- return { readFile, fileURLToPath, dirname, resolve };
46134
- }
46135
- async function readJson(nb, dir, name) {
46136
- return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
46137
- }
46138
- async function firstExistingDir(nb, baseDir) {
46139
- for (const rel of CANDIDATE_DIRS) {
46140
- const dir = nb.resolve(baseDir, rel);
46141
- try {
46142
- await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
46143
- return dir;
46144
- } catch {
46801
+
46802
+ // src/map/context-labels.ts
46803
+ function tierBand(maxSpanDeg) {
46804
+ if (maxSpanDeg >= 90) return "world";
46805
+ if (maxSpanDeg >= 20) return "continental";
46806
+ if (maxSpanDeg >= 5) return "regional";
46807
+ return "local";
46808
+ }
46809
+ function labelBudget(width, height, band) {
46810
+ const bandCap = {
46811
+ world: 6,
46812
+ continental: 5,
46813
+ regional: 4,
46814
+ local: 3
46815
+ };
46816
+ const area2 = Math.floor(Math.sqrt(Math.max(0, width * height)) / 150);
46817
+ return Math.max(0, Math.min(area2, bandCap[band]));
46818
+ }
46819
+ function waterEligible(tier, kind, band) {
46820
+ switch (band) {
46821
+ case "world":
46822
+ return tier <= 1 && (kind === "ocean" || kind === "sea");
46823
+ case "continental":
46824
+ return tier <= 2;
46825
+ case "regional":
46826
+ return tier <= 3;
46827
+ case "local":
46828
+ return tier <= 4;
46829
+ }
46830
+ }
46831
+ function insideViewport(p, width, height) {
46832
+ return !!p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && p[0] >= 0 && p[0] <= width && p[1] >= 0 && p[1] <= height;
46833
+ }
46834
+ function labelWidth(text, letterSpacing) {
46835
+ const spacing = letterSpacing > 0 ? Math.max(0, text.length - 1) * letterSpacing : 0;
46836
+ return measureLegendText(text, FONT) + spacing + 2 * PADX;
46837
+ }
46838
+ function wrapLabel2(text, letterSpacing) {
46839
+ const words = text.split(/\s+/).filter(Boolean);
46840
+ if (words.length <= 1) return [text];
46841
+ const maxLines = words.length >= 4 ? 3 : 2;
46842
+ const n = words.length;
46843
+ let best = null;
46844
+ for (let mask = 0; mask < 1 << n - 1; mask++) {
46845
+ const lines = [];
46846
+ let cur = [words[0]];
46847
+ for (let i = 1; i < n; i++) {
46848
+ if (mask & 1 << i - 1) {
46849
+ lines.push(cur.join(" "));
46850
+ cur = [words[i]];
46851
+ } else cur.push(words[i]);
46145
46852
  }
46853
+ lines.push(cur.join(" "));
46854
+ if (lines.length > maxLines) continue;
46855
+ const cost = Math.round(
46856
+ Math.max(...lines.map((l) => labelWidth(l, letterSpacing)))
46857
+ );
46858
+ const head = labelWidth(lines[0], letterSpacing);
46859
+ if (!best || cost < best.cost || cost === best.cost && lines.length < best.lines.length || cost === best.cost && lines.length === best.lines.length && head > best.head)
46860
+ best = { lines, cost, head };
46146
46861
  }
46147
- throw new Error(
46148
- `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
46149
- );
46862
+ return best?.lines ?? [text];
46150
46863
  }
46151
- function validate(data) {
46152
- const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
46153
- if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
46154
- throw new Error("map data assets are malformed (failed shape validation)");
46155
- }
46156
- return data;
46864
+ function rectAround(cx, cy, lines, letterSpacing) {
46865
+ const w = Math.max(...lines.map((l) => labelWidth(l, letterSpacing)));
46866
+ const h = (lines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY;
46867
+ return { x: cx - w / 2, y: cy - h / 2, w, h };
46157
46868
  }
46158
- function moduleBaseDir(nb) {
46159
- try {
46160
- const url = import_meta.url;
46161
- if (url) return nb.dirname(nb.fileURLToPath(url));
46162
- } catch {
46163
- }
46164
- if (typeof __dirname !== "undefined") return __dirname;
46165
- return process.cwd();
46869
+ function rectFits(r, width, height) {
46870
+ return r.x >= 0 && r.y >= 0 && r.x + r.w <= width && r.y + r.h <= height;
46166
46871
  }
46167
- function loadMapData() {
46168
- cache ??= (async () => {
46169
- const nb = await loadNodeBuiltins();
46170
- const dir = await firstExistingDir(nb, moduleBaseDir(nb));
46171
- const [
46172
- worldCoarse,
46173
- worldDetail,
46174
- usStates,
46175
- lakes,
46176
- rivers,
46177
- naLand,
46178
- naLakes,
46179
- gazetteer
46180
- ] = await Promise.all([
46181
- readJson(nb, dir, FILES.worldCoarse),
46182
- readJson(nb, dir, FILES.worldDetail),
46183
- readJson(nb, dir, FILES.usStates),
46184
- // Lakes/rivers/NA assets are optional — older bundles may predate them.
46185
- readJson(nb, dir, FILES.lakes).catch(() => void 0),
46186
- readJson(nb, dir, FILES.rivers).catch(() => void 0),
46187
- readJson(nb, dir, FILES.naLand).catch(() => void 0),
46188
- readJson(nb, dir, FILES.naLakes).catch(() => void 0),
46189
- readJson(nb, dir, FILES.gazetteer)
46190
- ]);
46191
- return validate({
46192
- worldCoarse,
46193
- worldDetail,
46194
- usStates,
46195
- gazetteer,
46196
- ...lakes && { lakes },
46197
- ...rivers && { rivers },
46198
- ...naLand && { naLand },
46199
- ...naLakes && { naLakes }
46872
+ function overlapsPadded(a, b, pad2) {
46873
+ 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;
46874
+ }
46875
+ function placeContextLabels(args) {
46876
+ const {
46877
+ projection,
46878
+ dLonSpan,
46879
+ dLatSpan,
46880
+ width,
46881
+ height,
46882
+ waterBodies,
46883
+ countries,
46884
+ palette,
46885
+ project,
46886
+ collides,
46887
+ overLand
46888
+ } = args;
46889
+ void projection;
46890
+ const band = tierBand(Math.max(dLonSpan, dLatSpan));
46891
+ const budget = labelBudget(width, height, band);
46892
+ if (budget <= 0) return [];
46893
+ const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
46894
+ const countryColor = palette.textMuted;
46895
+ const haloColor = palette.bg;
46896
+ const candidates = [];
46897
+ const center = [width / 2, height / 2];
46898
+ for (const e of waterBodies?.entries ?? []) {
46899
+ const [lat, lon, name, tier, kind, alt] = e;
46900
+ if (!waterEligible(tier, kind, band)) continue;
46901
+ const wlines = wrapLabel2(name, WATER_LETTER_SPACING);
46902
+ const anchorsLngLat = [[lon, lat]];
46903
+ for (const a of alt ?? []) anchorsLngLat.push([a[1], a[0]]);
46904
+ let best = null;
46905
+ let bestD = Infinity;
46906
+ let nearestProj = null;
46907
+ let nearestProjD = Infinity;
46908
+ for (const [aLon, aLat] of anchorsLngLat) {
46909
+ const p = project(aLon, aLat);
46910
+ if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
46911
+ const d = (p[0] - center[0]) ** 2 + (p[1] - center[1]) ** 2;
46912
+ if (d < nearestProjD) {
46913
+ nearestProjD = d;
46914
+ nearestProj = p;
46915
+ }
46916
+ if (!insideViewport(p, width, height)) continue;
46917
+ if (d < bestD) {
46918
+ bestD = d;
46919
+ best = p;
46920
+ }
46921
+ }
46922
+ if (!best && tier === 0 && nearestProj) {
46923
+ const overX = Math.max(0, -nearestProj[0], nearestProj[0] - width);
46924
+ const overY = Math.max(0, -nearestProj[1], nearestProj[1] - height);
46925
+ if (overX <= width * EDGE_CLAMP_OVERSHOOT && overY <= height * EDGE_CLAMP_OVERSHOOT) {
46926
+ const halfW = Math.max(...wlines.map((l) => labelWidth(l, WATER_LETTER_SPACING))) / 2;
46927
+ const halfH = ((wlines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY) / 2;
46928
+ const m = EDGE_CLAMP_MARGIN;
46929
+ best = [
46930
+ Math.min(Math.max(nearestProj[0], halfW + m), width - halfW - m),
46931
+ Math.min(Math.max(nearestProj[1], halfH + m), height - halfH - m)
46932
+ ];
46933
+ }
46934
+ }
46935
+ if (!best) continue;
46936
+ candidates.push({
46937
+ text: name,
46938
+ lines: wlines,
46939
+ cx: best[0],
46940
+ cy: best[1],
46941
+ italic: true,
46942
+ letterSpacing: WATER_LETTER_SPACING,
46943
+ color: waterColor,
46944
+ // Water before any country (×1000), then by tier, then kind, then name.
46945
+ sort: tier * 10 + KIND_ORDER[kind]
46200
46946
  });
46201
- })().catch((e) => {
46202
- cache = void 0;
46203
- throw e;
46204
- });
46205
- return cache;
46947
+ }
46948
+ const ranked = countries.map((c) => {
46949
+ const [x0, y0, x1, y1] = c.bbox;
46950
+ const w = x1 - x0;
46951
+ const h = y1 - y0;
46952
+ return { c, w, h, area: w * h };
46953
+ }).filter((r) => Number.isFinite(r.area) && r.area > 0).sort((a, b) => b.area - a.area);
46954
+ let ci = 0;
46955
+ for (const r of ranked) {
46956
+ const { c, w, h } = r;
46957
+ if (w > width * 0.66 || h > height * 0.66) continue;
46958
+ if (!insideViewport(c.anchor, width, height)) continue;
46959
+ const text = c.name;
46960
+ const tw = labelWidth(text, 0);
46961
+ if (tw > w || FONT + 2 * PADY > h) continue;
46962
+ candidates.push({
46963
+ text,
46964
+ lines: [text],
46965
+ cx: c.anchor[0],
46966
+ cy: c.anchor[1],
46967
+ italic: false,
46968
+ letterSpacing: 0,
46969
+ color: countryColor,
46970
+ // Always after every water body (+1e6); larger area = earlier.
46971
+ sort: 1e6 + ci++
46972
+ });
46973
+ }
46974
+ candidates.sort((a, b) => a.sort - b.sort);
46975
+ const placed = [];
46976
+ const placedRects = [];
46977
+ for (const cand of candidates) {
46978
+ if (placed.length >= budget) break;
46979
+ const rect = rectAround(cand.cx, cand.cy, cand.lines, cand.letterSpacing);
46980
+ if (!rectFits(rect, width, height)) continue;
46981
+ if (cand.italic && overLand) {
46982
+ const inset = 2;
46983
+ const top = cand.cy - (cand.lines.length - 1) / 2 * LINE_HEIGHT;
46984
+ const touchesLand = cand.lines.some((line12, li) => {
46985
+ const lw = labelWidth(line12, cand.letterSpacing);
46986
+ const x0 = cand.cx - lw / 2 + inset;
46987
+ const x1 = cand.cx + lw / 2 - inset;
46988
+ const xs = [x0, (x0 + cand.cx) / 2, cand.cx, (cand.cx + x1) / 2, x1];
46989
+ const base = top + li * LINE_HEIGHT;
46990
+ return [base, base - FONT * 0.4, base - FONT * 0.8].some(
46991
+ (y) => xs.some((x) => overLand(x, y))
46992
+ );
46993
+ });
46994
+ if (touchesLand) continue;
46995
+ }
46996
+ if (collides(rect)) continue;
46997
+ if (placedRects.some((r) => overlapsPadded(rect, r, CONTEXT_PAD))) continue;
46998
+ placedRects.push(rect);
46999
+ placed.push({
47000
+ x: cand.cx,
47001
+ y: cand.cy,
47002
+ text: cand.text,
47003
+ anchor: "middle",
47004
+ color: cand.color,
47005
+ // No halo: the bg-coloured outline reads as a ghost box behind the text
47006
+ // over the tinted water/land. Context labels are muted enough to sit
47007
+ // cleanly on the basemap without one.
47008
+ halo: false,
47009
+ haloColor,
47010
+ italic: cand.italic,
47011
+ letterSpacing: cand.letterSpacing,
47012
+ ...cand.lines.length > 1 ? { lines: cand.lines } : {},
47013
+ lineNumber: 0
47014
+ });
47015
+ }
47016
+ return placed;
46206
47017
  }
46207
- var import_meta, FILES, CANDIDATE_DIRS, cache;
46208
- var init_load_data = __esm({
46209
- "src/map/load-data.ts"() {
47018
+ var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, KIND_ORDER;
47019
+ var init_context_labels = __esm({
47020
+ "src/map/context-labels.ts"() {
46210
47021
  "use strict";
46211
- import_meta = {};
46212
- FILES = {
46213
- worldCoarse: "world-coarse.json",
46214
- worldDetail: "world-detail.json",
46215
- usStates: "us-states.json",
46216
- lakes: "lakes.json",
46217
- rivers: "rivers.json",
46218
- naLand: "na-land.json",
46219
- naLakes: "na-lakes.json",
46220
- gazetteer: "gazetteer.json"
47022
+ init_color_utils();
47023
+ init_legend_constants();
47024
+ FONT = 11;
47025
+ LINE_HEIGHT = FONT + 2;
47026
+ PADX = 4;
47027
+ PADY = 3;
47028
+ WATER_LETTER_SPACING = 1.5;
47029
+ CONTEXT_PAD = 4;
47030
+ EDGE_CLAMP_MARGIN = 8;
47031
+ EDGE_CLAMP_OVERSHOOT = 0.35;
47032
+ KIND_ORDER = {
47033
+ ocean: 0,
47034
+ sea: 1,
47035
+ gulf: 2,
47036
+ bay: 3,
47037
+ strait: 4,
47038
+ channel: 5,
47039
+ sound: 6
46221
47040
  };
46222
- CANDIDATE_DIRS = [
46223
- "./data",
46224
- "./map-data",
46225
- "../map-data",
46226
- "../src/map/data"
46227
- ];
46228
47041
  }
46229
47042
  });
46230
47043
 
@@ -46233,12 +47046,34 @@ function geomObject2(topo) {
46233
47046
  const key = Object.keys(topo.objects)[0];
46234
47047
  return topo.objects[key];
46235
47048
  }
47049
+ function mergeFeatures(a, b) {
47050
+ const polysOf = (f) => {
47051
+ const g = f.geometry;
47052
+ if (!g) return null;
47053
+ if (g.type === "Polygon") return [g.coordinates];
47054
+ if (g.type === "MultiPolygon") return g.coordinates;
47055
+ return null;
47056
+ };
47057
+ const pa = polysOf(a);
47058
+ const pb = polysOf(b);
47059
+ if (!pa || !pb) return a;
47060
+ return {
47061
+ ...a,
47062
+ geometry: { type: "MultiPolygon", coordinates: [...pa, ...pb] }
47063
+ };
47064
+ }
46236
47065
  function decodeLayer(topo) {
47066
+ const cached = decodeCache.get(topo);
47067
+ if (cached) return cached;
46237
47068
  const out = /* @__PURE__ */ new Map();
46238
47069
  for (const g of geomObject2(topo).geometries) {
46239
47070
  const f = (0, import_topojson_client2.feature)(topo, g);
46240
- out.set(g.id, { ...f, id: g.id });
47071
+ if (!f.geometry) continue;
47072
+ const tagged = { ...f, id: g.id };
47073
+ const existing = out.get(g.id);
47074
+ out.set(g.id, existing ? mergeFeatures(existing, tagged) : tagged);
46241
47075
  }
47076
+ decodeCache.set(topo, out);
46242
47077
  return out;
46243
47078
  }
46244
47079
  function projectionFor(family) {
@@ -46247,38 +47082,35 @@ function projectionFor(family) {
46247
47082
  return usConusProjection();
46248
47083
  case "mercator":
46249
47084
  return (0, import_d3_geo2.geoMercator)();
47085
+ case "equal-earth":
47086
+ return (0, import_d3_geo2.geoEqualEarth)();
47087
+ case "equirectangular":
47088
+ return (0, import_d3_geo2.geoEquirectangular)();
46250
47089
  case "natural-earth":
46251
47090
  return (0, import_d3_geo2.geoNaturalEarth1)();
46252
- case "equirectangular":
46253
47091
  default:
46254
47092
  return (0, import_d3_geo2.geoEquirectangular)();
46255
47093
  }
46256
47094
  }
46257
- function mapBackgroundColor(palette, isDark = false, dataActive = false) {
46258
- if (dataActive)
46259
- return mix(
46260
- palette.colors.gray,
46261
- palette.bg,
46262
- isDark ? MUTED_WATER_DARK : MUTED_WATER_LIGHT
46263
- );
46264
- return mix(palette.colors.blue, palette.bg, WATER_TINT);
47095
+ function mapBackgroundColor(palette, isDark = false, _dataActive = false) {
47096
+ return mix(
47097
+ palette.colors.blue,
47098
+ palette.bg,
47099
+ isDark ? WATER_TINT_DARK : WATER_TINT_LIGHT
47100
+ );
46265
47101
  }
46266
- function mapNeutralLandColor(palette, isDark, dataActive = false) {
46267
- if (dataActive)
46268
- return isDark ? mix(palette.colors.gray, palette.bg, MUTED_LAND_DARK) : palette.bg;
47102
+ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46269
47103
  return mix(
46270
47104
  palette.colors.green,
46271
47105
  palette.bg,
46272
47106
  isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT
46273
47107
  );
46274
47108
  }
46275
- function layoutMap(resolved, data, size, opts) {
46276
- const { palette, isDark } = opts;
46277
- const { width, height } = size;
47109
+ function buildMapProjection(resolved, data) {
46278
47110
  const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
46279
- const usCrisp = resolved.projection === "albers-usa" && wantsUsStates && !!data.naLand;
47111
+ const usCrisp = (resolved.projection === "albers-usa" || resolved.projection === "mercator") && wantsUsStates && !!data.naLand;
46280
47112
  const worldTopo = usCrisp ? data.worldDetail : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46281
- const worldLayer = decodeLayer(worldTopo);
47113
+ const worldLayer = new Map(decodeLayer(worldTopo));
46282
47114
  if (usCrisp && data.naLand) {
46283
47115
  const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
46284
47116
  const crisp = decodeLayer(data.naLand);
@@ -46287,17 +47119,110 @@ function layoutMap(resolved, data, size, opts) {
46287
47119
  if (!base) continue;
46288
47120
  const [[bw, bs], [be, bn]] = (0, import_d3_geo2.geoBounds)(base);
46289
47121
  if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
46290
- worldLayer.set(iso, cf);
47122
+ worldLayer.set(iso, { ...cf, properties: base.properties });
46291
47123
  }
46292
47124
  }
46293
47125
  const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
47126
+ const extentOutline = () => {
47127
+ const [[w, s], [e, n]] = resolved.extent;
47128
+ const N = 16;
47129
+ const coords = [];
47130
+ for (let i = 0; i <= N; i++) {
47131
+ const t = i / N;
47132
+ const lon = w + (e - w) * t;
47133
+ const lat = s + (n - s) * t;
47134
+ coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
47135
+ }
47136
+ return {
47137
+ type: "Feature",
47138
+ properties: {},
47139
+ geometry: { type: "MultiPoint", coordinates: coords }
47140
+ };
47141
+ };
47142
+ let fitFeatures;
47143
+ if (resolved.projection === "albers-usa" && usLayer) {
47144
+ fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
47145
+ const neighborPoints = resolved.pois.filter((p) => !inAlaska(p.lon, p.lat) && !inHawaii(p.lon, p.lat)).map((p) => [p.lon, p.lat]);
47146
+ if (neighborPoints.length > 0) {
47147
+ fitFeatures.push({
47148
+ type: "Feature",
47149
+ properties: {},
47150
+ geometry: { type: "MultiPoint", coordinates: neighborPoints }
47151
+ });
47152
+ }
47153
+ for (const r of resolved.regions) {
47154
+ if (r.layer === "country" && (r.iso === "CA" || r.iso === "MX")) {
47155
+ const cf = worldLayer.get(r.iso);
47156
+ if (cf) fitFeatures.push(cf);
47157
+ }
47158
+ }
47159
+ } else {
47160
+ fitFeatures = [extentOutline()];
47161
+ }
47162
+ const fitTarget = { type: "FeatureCollection", features: fitFeatures };
47163
+ const projection = projectionFor(resolved.projection);
47164
+ if (resolved.projection !== "albers-usa") {
47165
+ let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
47166
+ if (centerLon > 180) centerLon -= 360;
47167
+ projection.rotate([-centerLon, 0]);
47168
+ }
47169
+ const fitGB = (0, import_d3_geo2.geoBounds)(fitTarget);
47170
+ const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
47171
+ return {
47172
+ projection,
47173
+ fitTarget,
47174
+ fitIsGlobal,
47175
+ worldLayer,
47176
+ usLayer,
47177
+ usCrisp,
47178
+ wantsUsStates,
47179
+ worldTopo
47180
+ };
47181
+ }
47182
+ function parsePathRings(d) {
47183
+ const rings = [];
47184
+ let cur = [];
47185
+ const re = /([MLZ])([^MLZ]*)/g;
47186
+ let m;
47187
+ while (m = re.exec(d)) {
47188
+ if (m[1] === "Z") {
47189
+ if (cur.length) rings.push(cur);
47190
+ cur = [];
47191
+ continue;
47192
+ }
47193
+ if (m[1] === "M" && cur.length) {
47194
+ rings.push(cur);
47195
+ cur = [];
47196
+ }
47197
+ const nums = m[2].split(/[ ,]+/).map(Number);
47198
+ for (let i = 0; i + 1 < nums.length; i += 2) {
47199
+ const x = nums[i];
47200
+ const y = nums[i + 1];
47201
+ if (Number.isFinite(x) && Number.isFinite(y)) cur.push([x, y]);
47202
+ }
47203
+ }
47204
+ if (cur.length) rings.push(cur);
47205
+ return rings;
47206
+ }
47207
+ function layoutMap(resolved, data, size, opts) {
47208
+ const { palette, isDark } = opts;
47209
+ const { width, height } = size;
47210
+ const {
47211
+ projection,
47212
+ fitTarget,
47213
+ fitIsGlobal,
47214
+ worldLayer,
47215
+ usLayer,
47216
+ usCrisp,
47217
+ worldTopo
47218
+ } = buildMapProjection(resolved, data);
46294
47219
  const usContext = usLayer !== null;
46295
47220
  const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46296
47221
  const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
46297
- const scaleOverride = resolved.directives.scale;
46298
- const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46299
- const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
46300
- const rampHue = palette.colors.red;
47222
+ const allNonNegative = values.length > 0 && values.every((v) => v >= 0);
47223
+ const rampMin = allNonNegative ? 0 : Math.min(...values);
47224
+ const rampMax = Math.max(...values);
47225
+ const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46301
47226
  const hasRamp = values.length > 0;
46302
47227
  const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
46303
47228
  const matchColorGroup = (v) => {
@@ -46317,14 +47242,48 @@ function layoutMap(resolved, data, size, opts) {
46317
47242
  activeGroup = VALUE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46318
47243
  }
46319
47244
  const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
46320
- const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
47245
+ const mutedBasemap = activeGroup !== null;
46321
47246
  const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46322
47247
  const water = mapBackgroundColor(palette, isDark, mutedBasemap);
47248
+ const lakeStroke = mix(regionStroke, water, 45);
46323
47249
  const foreignFill = mix(
46324
47250
  palette.colors.gray,
46325
47251
  palette.bg,
46326
47252
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46327
47253
  );
47254
+ const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && resolved.tagGroups.length === 0;
47255
+ const colorByIso = /* @__PURE__ */ new Map();
47256
+ if (colorizeActive) {
47257
+ const adjacency = /* @__PURE__ */ new Map();
47258
+ const addEdges = (src) => {
47259
+ for (const [iso, ns] of src) {
47260
+ const cur = adjacency.get(iso);
47261
+ if (cur) cur.push(...ns);
47262
+ else adjacency.set(iso, [...ns]);
47263
+ }
47264
+ };
47265
+ addEdges(buildAdjacency(worldTopo));
47266
+ if (usLayer) {
47267
+ addEdges(buildAdjacency(data.usStates));
47268
+ for (const [country, states] of Object.entries(FOREIGN_BORDER)) {
47269
+ const cn = adjacency.get(country);
47270
+ if (!cn) continue;
47271
+ for (const st of states) {
47272
+ const sn = adjacency.get(st);
47273
+ if (!sn) continue;
47274
+ cn.push(st);
47275
+ sn.push(country);
47276
+ }
47277
+ }
47278
+ }
47279
+ const { byIso, huesNeeded } = assignColors(
47280
+ [...adjacency.keys()],
47281
+ adjacency
47282
+ );
47283
+ const tints = politicalTints(palette, huesNeeded, isDark);
47284
+ for (const [iso, idx] of byIso) colorByIso.set(iso, tints[idx]);
47285
+ }
47286
+ const colorizeStroke = (fill2) => mix(fill2, palette.text, 35);
46328
47287
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46329
47288
  const fillForValue = (s) => {
46330
47289
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
@@ -46349,47 +47308,26 @@ function layoutMap(resolved, data, size, opts) {
46349
47308
  isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
46350
47309
  );
46351
47310
  };
47311
+ const directFill = (name) => {
47312
+ const hex = name ? resolveColor(name, palette) : null;
47313
+ if (!hex) return null;
47314
+ return mix(hex, palette.bg, isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT);
47315
+ };
46352
47316
  const regionFill = (r) => {
47317
+ const direct = directFill(r.color);
47318
+ if (direct) return direct;
46353
47319
  if (activeIsScore) {
46354
47320
  return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46355
47321
  }
47322
+ if (colorizeActive) return (r.iso && colorByIso.get(r.iso)) ?? neutralFill;
46356
47323
  return tagFill(r.tags, activeGroup) ?? neutralFill;
46357
47324
  };
46358
47325
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
46359
- const extentOutline = () => {
46360
- const [[w, s], [e, n]] = resolved.extent;
46361
- const N = 16;
46362
- const coords = [];
46363
- for (let i = 0; i <= N; i++) {
46364
- const t = i / N;
46365
- const lon = w + (e - w) * t;
46366
- const lat = s + (n - s) * t;
46367
- coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
46368
- }
46369
- return {
46370
- type: "Feature",
46371
- properties: {},
46372
- geometry: { type: "MultiPoint", coordinates: coords }
46373
- };
46374
- };
46375
- let fitFeatures;
46376
- if (resolved.projection === "albers-usa" && usLayer) {
46377
- fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
46378
- } else {
46379
- fitFeatures = [extentOutline()];
46380
- }
46381
- const fitTarget = { type: "FeatureCollection", features: fitFeatures };
46382
- const projection = projectionFor(resolved.projection);
46383
- if (resolved.projection !== "albers-usa") {
46384
- let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
46385
- if (centerLon > 180) centerLon -= 360;
46386
- projection.rotate([-centerLon, 0]);
46387
- }
46388
- const TITLE_GAP = 16;
47326
+ const TITLE_GAP2 = 16;
46389
47327
  let topPad = FIT_PAD;
46390
47328
  if (resolved.title && resolved.pois.length > 0) {
46391
47329
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
46392
- topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
47330
+ topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
46393
47331
  }
46394
47332
  const fitBox = [
46395
47333
  [FIT_PAD, topPad],
@@ -46399,11 +47337,10 @@ function layoutMap(resolved, data, size, opts) {
46399
47337
  ]
46400
47338
  ];
46401
47339
  projection.fitExtent(fitBox, fitTarget);
46402
- const fitGB = (0, import_d3_geo2.geoBounds)(fitTarget);
46403
- const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46404
47340
  let path;
46405
47341
  let project;
46406
- if (fitIsGlobal) {
47342
+ let stretchParams = null;
47343
+ if (fitIsGlobal && !opts.preferContain) {
46407
47344
  const cb = (0, import_d3_geo2.geoPath)(projection).bounds(fitTarget);
46408
47345
  const bx0 = cb[0][0];
46409
47346
  const by0 = cb[0][1];
@@ -46413,6 +47350,7 @@ function layoutMap(resolved, data, size, opts) {
46413
47350
  const oy = fitBox[0][1];
46414
47351
  const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46415
47352
  const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47353
+ stretchParams = { sx, sy, ox, oy, bx0, by0 };
46416
47354
  const stretch = (x, y) => [
46417
47355
  ox + (x - bx0) * sx,
46418
47356
  oy + (y - by0) * sy
@@ -46444,7 +47382,9 @@ function layoutMap(resolved, data, size, opts) {
46444
47382
  const insets = [];
46445
47383
  const insetRegions = [];
46446
47384
  const insetLabelSeeds = [];
46447
- if (resolved.projection === "albers-usa" && usLayer) {
47385
+ const akRef = resolved.regions.some((r) => r.iso === "US-AK") || resolved.pois.some((p) => inAlaska(p.lon, p.lat));
47386
+ const hiRef = resolved.regions.some((r) => r.iso === "US-HI") || resolved.pois.some((p) => inHawaii(p.lon, p.lat));
47387
+ if (resolved.projection === "albers-usa" && usLayer && (akRef || hiRef)) {
46448
47388
  const PAD = 8;
46449
47389
  const GAP = 12;
46450
47390
  const yB = height - FIT_PAD;
@@ -46475,38 +47415,14 @@ function layoutMap(resolved, data, size, opts) {
46475
47415
  }
46476
47416
  return y;
46477
47417
  };
46478
- const coastTop = (x0, xr) => {
47418
+ const coastFloor = (x0, xr) => {
46479
47419
  const n = 24;
46480
- const pts = [];
46481
47420
  let maxY = -Infinity;
46482
47421
  for (let i = 0; i <= n; i++) {
46483
- const x = x0 + (xr - x0) * i / n;
46484
- const y = at(x);
46485
- if (y > -Infinity) {
46486
- pts.push([x, y]);
46487
- if (y > maxY) maxY = y;
46488
- }
46489
- }
46490
- if (pts.length === 0) return () => yB - height * 0.42;
46491
- let m = 0;
46492
- if (pts.length >= 2) {
46493
- let sx = 0, sy = 0, sxx = 0, sxy = 0;
46494
- for (const [x, y] of pts) {
46495
- sx += x;
46496
- sy += y;
46497
- sxx += x * x;
46498
- sxy += x * y;
46499
- }
46500
- const den = pts.length * sxx - sx * sx;
46501
- if (den !== 0) m = (pts.length * sxy - sx * sy) / den;
46502
- }
46503
- m = Math.max(-0.35, Math.min(0.35, m));
46504
- let c = -Infinity;
46505
- for (const [x, y] of pts) {
46506
- const need = y - m * x + GAP;
46507
- if (need > c) c = need;
46508
- }
46509
- return (x) => m * x + c;
47422
+ const y = at(x0 + (xr - x0) * i / n);
47423
+ if (y > maxY) maxY = y;
47424
+ }
47425
+ return maxY;
46510
47426
  };
46511
47427
  const placeInset = (iso, proj, boxX, iwReq) => {
46512
47428
  const f = usLayer.get(iso);
@@ -46515,19 +47431,15 @@ function layoutMap(resolved, data, size, opts) {
46515
47431
  const iw = Math.min(iwReq, width - FIT_PAD - x0 - 2 * PAD);
46516
47432
  if (iw < 24) return boxX;
46517
47433
  const xr = x0 + iw + 2 * PAD;
46518
- const top = coastTop(x0, xr);
46519
- const yL = top(x0);
46520
- const yR = top(xr);
47434
+ const floor = coastFloor(x0, xr);
47435
+ const topGuess = floor > -Infinity ? floor + GAP : yB - height * 0.42;
46521
47436
  proj.fitWidth(iw, f);
46522
47437
  const bb = (0, import_d3_geo2.geoPath)(proj).bounds(f);
46523
47438
  const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
46524
47439
  const needH = sh + 2 * PAD;
46525
- let topFit = Math.max(yL, yR);
47440
+ let topFit = topGuess;
46526
47441
  const bottom = Math.min(topFit + needH, yB);
46527
47442
  if (bottom - topFit < needH) topFit = bottom - needH;
46528
- const lift = topFit - Math.max(yL, yR);
46529
- const topL = yL + lift;
46530
- const topR = yR + lift;
46531
47443
  proj.fitExtent(
46532
47444
  [
46533
47445
  [x0 + PAD, topFit + PAD],
@@ -46537,8 +47449,18 @@ function layoutMap(resolved, data, size, opts) {
46537
47449
  );
46538
47450
  const d = (0, import_d3_geo2.geoPath)(proj)(f) ?? "";
46539
47451
  if (!d) return xr;
47452
+ let contextLand;
47453
+ if (iso === "US-AK") {
47454
+ const can = worldLayer.get("CA");
47455
+ const cd = can ? (0, import_d3_geo2.geoPath)(proj)(can) ?? "" : "";
47456
+ if (cd)
47457
+ contextLand = {
47458
+ d: cd,
47459
+ fill: colorizeActive ? colorByIso.get("CA") ?? foreignFill : foreignFill
47460
+ };
47461
+ }
46540
47462
  const r = regionById.get(iso);
46541
- let fill2 = neutralFill;
47463
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? neutralFill : neutralFill;
46542
47464
  let lineNumber = -1;
46543
47465
  if (r?.layer === "us-state") {
46544
47466
  fill2 = regionFill(r);
@@ -46546,21 +47468,25 @@ function layoutMap(resolved, data, size, opts) {
46546
47468
  }
46547
47469
  insets.push({
46548
47470
  x: x0,
46549
- y: Math.min(topL, topR),
47471
+ y: topFit,
46550
47472
  w: xr - x0,
46551
- h: bottom - Math.min(topL, topR),
47473
+ h: bottom - topFit,
46552
47474
  points: [
46553
- [x0, topL],
46554
- [xr, topR],
47475
+ [x0, topFit],
47476
+ [xr, topFit],
46555
47477
  [xr, bottom],
46556
47478
  [x0, bottom]
46557
- ]
47479
+ ],
47480
+ // The FITTED inset projection (just fit to this box) — captured so the
47481
+ // geo-query can invert pixels inside the frame back to AK/HI coords.
47482
+ projection: proj,
47483
+ ...contextLand && { contextLand }
46558
47484
  });
46559
47485
  insetRegions.push({
46560
47486
  id: iso,
46561
47487
  d,
46562
47488
  fill: fill2,
46563
- stroke: regionStroke,
47489
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46564
47490
  lineNumber,
46565
47491
  layer: "us-state",
46566
47492
  ...r?.value !== void 0 && { value: r.value },
@@ -46573,13 +47499,16 @@ function layoutMap(resolved, data, size, opts) {
46573
47499
  }
46574
47500
  return xr;
46575
47501
  };
46576
- const akRight = placeInset(
46577
- "US-AK",
46578
- alaskaProjection(),
46579
- FIT_PAD,
46580
- width * 0.15
46581
- );
46582
- placeInset("US-HI", hawaiiProjection(), akRight + 24, width * 0.1);
47502
+ let akRight = FIT_PAD;
47503
+ if (akRef)
47504
+ akRight = placeInset("US-AK", alaskaProjection(), FIT_PAD, width * 0.15);
47505
+ if (hiRef)
47506
+ placeInset(
47507
+ "US-HI",
47508
+ hawaiiProjection(),
47509
+ akRef ? akRight + 24 : FIT_PAD,
47510
+ width * 0.1
47511
+ );
46583
47512
  }
46584
47513
  const conusFit = resolved.projection === "albers-usa" && !!usLayer;
46585
47514
  const classifyExtent = conusFit ? (0, import_d3_geo2.geoBounds)(fitTarget) : resolved.extent;
@@ -46595,15 +47524,24 @@ function layoutMap(resolved, data, size, opts) {
46595
47524
  };
46596
47525
  const ringOverlapsView = (ring) => {
46597
47526
  let loMin = Infinity, loMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
47527
+ const lons = [];
46598
47528
  for (const [rawLon] of ring) {
46599
47529
  const lon = normLon(rawLon);
47530
+ lons.push(lon);
46600
47531
  if (lon < loMin) loMin = lon;
46601
47532
  if (lon > loMax) loMax = lon;
46602
47533
  if (rawLon < rawMin) rawMin = rawLon;
46603
47534
  if (rawLon > rawMax) rawMax = rawLon;
46604
47535
  }
46605
- if (loMax - loMin > 270) return false;
46606
- if (rawMax - rawMin > 180 && loMax - loMin < 90) return false;
47536
+ lons.sort((a, b) => a - b);
47537
+ let maxGap = 0;
47538
+ for (let i = 1; i < lons.length; i++)
47539
+ maxGap = Math.max(maxGap, lons[i] - lons[i - 1]);
47540
+ if (lons.length > 1)
47541
+ maxGap = Math.max(maxGap, lons[0] + 360 - lons[lons.length - 1]);
47542
+ const occupiedArc = 360 - maxGap;
47543
+ if (occupiedArc > 270) return false;
47544
+ if (rawMax - rawMin > 180 && occupiedArc < 90) return false;
46607
47545
  let px0 = Infinity, py0 = Infinity, px1 = -Infinity, py1 = -Infinity, anyFinite = false;
46608
47546
  for (const [lon, lat] of ring) {
46609
47547
  const p = project(lon, lat);
@@ -46676,7 +47614,7 @@ function layoutMap(resolved, data, size, opts) {
46676
47614
  const regions = [];
46677
47615
  const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
46678
47616
  for (const [iso, f] of layerFeatures) {
46679
- if (layerKind === "us-state" && usContext && INSET_STATES.has(iso))
47617
+ if (layerKind === "us-state" && usContext && resolved.projection === "albers-usa" && INSET_STATES.has(iso))
46680
47618
  continue;
46681
47619
  if (layerKind === "country" && usContext && iso === "US") continue;
46682
47620
  if (layerKind === "country" && iso === "AQ" && !regionById.has("AQ"))
@@ -46688,7 +47626,8 @@ function layoutMap(resolved, data, size, opts) {
46688
47626
  if (!d) continue;
46689
47627
  const isThisLayer = r?.layer === layerKind;
46690
47628
  const isForeign = layerKind === "country" && usContext && iso !== "US";
46691
- let fill2 = isForeign ? foreignFill : neutralFill;
47629
+ const baseFill = isForeign ? foreignFill : neutralFill;
47630
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? baseFill : baseFill;
46692
47631
  let label;
46693
47632
  let lineNumber = -1;
46694
47633
  let layer = "base";
@@ -46697,12 +47636,14 @@ function layoutMap(resolved, data, size, opts) {
46697
47636
  lineNumber = r.lineNumber;
46698
47637
  layer = layerKind;
46699
47638
  label = r.name;
47639
+ } else {
47640
+ label = f.properties?.name;
46700
47641
  }
46701
47642
  regions.push({
46702
47643
  id: iso,
46703
47644
  d,
46704
47645
  fill: fill2,
46705
- stroke: regionStroke,
47646
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46706
47647
  lineNumber,
46707
47648
  layer,
46708
47649
  ...label !== void 0 && { label },
@@ -46724,13 +47665,88 @@ function layoutMap(resolved, data, size, opts) {
46724
47665
  id: "lake",
46725
47666
  d,
46726
47667
  fill: water,
46727
- stroke: "none",
47668
+ stroke: lakeStroke,
46728
47669
  lineNumber: -1,
46729
47670
  layer: "base"
46730
47671
  });
46731
47672
  }
46732
47673
  }
46733
- const riverColor = water;
47674
+ const pointInRings = (px, py, rings) => {
47675
+ let inside = false;
47676
+ for (const ring of rings) {
47677
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
47678
+ const [xi, yi] = ring[i];
47679
+ const [xj, yj] = ring[j];
47680
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
47681
+ inside = !inside;
47682
+ }
47683
+ }
47684
+ return inside;
47685
+ };
47686
+ const fillHitTargets = [...regions, ...insetRegions].map((r) => ({
47687
+ fill: r.fill,
47688
+ rings: parsePathRings(r.d)
47689
+ }));
47690
+ const fillAt = (x, y) => {
47691
+ let hit = water;
47692
+ for (const t of fillHitTargets)
47693
+ if (pointInRings(x, y, t.rings)) hit = t.fill;
47694
+ return hit;
47695
+ };
47696
+ const labelOnFill = (fill2) => {
47697
+ const color = contrastRatio(fill2, palette.textOnFillDark) >= contrastRatio(fill2, palette.textOnFillLight) ? palette.textOnFillDark : palette.textOnFillLight;
47698
+ const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47699
+ return {
47700
+ color,
47701
+ halo: contrastRatio(fill2, color) < REGION_LABEL_HALO_RATIO,
47702
+ haloColor
47703
+ };
47704
+ };
47705
+ const reliefAllowed = resolved.directives.noRelief !== true;
47706
+ const relief = [];
47707
+ let reliefHatch = null;
47708
+ if (reliefAllowed && data.mountainRanges) {
47709
+ for (const [, f] of decodeLayer(data.mountainRanges)) {
47710
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
47711
+ if (!viewF) continue;
47712
+ const area2 = path.area(viewF);
47713
+ if (!Number.isFinite(area2) || area2 < RELIEF_MIN_AREA) continue;
47714
+ const box = path.bounds(viewF);
47715
+ if (box[1][0] - box[0][0] < RELIEF_MIN_DIM || box[1][1] - box[0][1] < RELIEF_MIN_DIM)
47716
+ continue;
47717
+ const d = path(viewF) ?? "";
47718
+ if (!d) continue;
47719
+ relief.push({ d });
47720
+ }
47721
+ if (relief.length) {
47722
+ const darkTone = isDark ? palette.bg : palette.text;
47723
+ const lightTone = isDark ? palette.text : palette.bg;
47724
+ const reliefLandRef = colorizeActive ? isDark ? palette.surface : palette.bg : neutralFill;
47725
+ const landLum = relativeLuminance(reliefLandRef);
47726
+ const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
47727
+ reliefHatch = {
47728
+ color: mix(tone, reliefLandRef, RELIEF_HATCH_STRENGTH),
47729
+ spacing: RELIEF_HATCH_SPACING,
47730
+ width: RELIEF_HATCH_WIDTH
47731
+ };
47732
+ }
47733
+ }
47734
+ let coastlineStyle = null;
47735
+ if (resolved.directives.noCoastline !== true) {
47736
+ const minDim = Math.min(width, height);
47737
+ coastlineStyle = {
47738
+ color: mix(regionStroke, water, COASTLINE_STROKE_MIX),
47739
+ // N equal-width rings: distance steps outward by COASTLINE_STEP; opacity
47740
+ // fades linearly from NEAR (innermost) to FAR (outermost).
47741
+ lines: Array.from({ length: COASTLINE_RING_COUNT }, (_, k) => ({
47742
+ d: (COASTLINE_D0 + k * COASTLINE_STEP) * minDim,
47743
+ thickness: COASTLINE_THICKNESS * minDim,
47744
+ opacity: COASTLINE_OPACITY_NEAR + (COASTLINE_OPACITY_FAR - COASTLINE_OPACITY_NEAR) * k / (COASTLINE_RING_COUNT - 1)
47745
+ })),
47746
+ minExtent: (isGlobalView ? COASTLINE_MIN_EXTENT_GLOBAL : COASTLINE_MIN_EXTENT) * minDim
47747
+ };
47748
+ }
47749
+ const riverColor = mix(palette.colors.blue, water, 32);
46734
47750
  const rivers = [];
46735
47751
  if (data.rivers) {
46736
47752
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -46751,6 +47767,9 @@ function layoutMap(resolved, data, size, opts) {
46751
47767
  return R_MIN + Math.max(0, Math.min(1, t)) * (R_MAX - R_MIN);
46752
47768
  };
46753
47769
  const poiFill = (p) => {
47770
+ const directHex = p.color ? resolveColor(p.color, palette) : null;
47771
+ if (directHex)
47772
+ return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
46754
47773
  for (const group of resolved.tagGroups) {
46755
47774
  const val = p.tags[group.name.toLowerCase()];
46756
47775
  if (!val) continue;
@@ -46783,38 +47802,108 @@ function layoutMap(resolved, data, size, opts) {
46783
47802
  const xy = project(p.lon, p.lat);
46784
47803
  if (xy) projected.push({ p, xy });
46785
47804
  }
46786
- const coloGroups = /* @__PURE__ */ new Map();
47805
+ const placePoi = (e, cx, cy, clusterId) => {
47806
+ const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
47807
+ poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
47808
+ const num = routeNumberById.get(e.p.id);
47809
+ pois.push({
47810
+ id: e.p.id,
47811
+ cx,
47812
+ cy,
47813
+ r: radiusFor(e.p),
47814
+ fill: fill2,
47815
+ stroke: stroke2,
47816
+ lineNumber: e.p.lineNumber,
47817
+ implicit: !!e.p.implicit,
47818
+ isOrigin: originIds.has(e.p.id),
47819
+ ...num !== void 0 && { routeNumber: num },
47820
+ ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags },
47821
+ ...clusterId !== void 0 && { clusterId }
47822
+ });
47823
+ };
47824
+ const clusters = [];
47825
+ const connected = /* @__PURE__ */ new Set();
47826
+ for (const e of resolved.edges) {
47827
+ connected.add(e.fromId);
47828
+ connected.add(e.toId);
47829
+ }
47830
+ for (const rt of resolved.routes) {
47831
+ rt.stopIds.forEach((id) => connected.add(id));
47832
+ }
47833
+ const radiusOf = (e) => radiusFor(e.p);
46787
47834
  for (const e of projected) {
46788
- const key = `${Math.round(e.xy[0] / COLO_EPS)},${Math.round(e.xy[1] / COLO_EPS)}`;
46789
- const arr = coloGroups.get(key);
46790
- if (arr) arr.push(e);
46791
- else coloGroups.set(key, [e]);
46792
- }
46793
- for (const group of coloGroups.values()) {
46794
- group.forEach((e, i) => {
46795
- let cx = e.xy[0];
46796
- let cy = e.xy[1];
46797
- if (group.length > 1) {
46798
- const ang = i * GOLDEN_ANGLE;
46799
- cx += Math.cos(ang) * COLO_R;
46800
- cy += Math.sin(ang) * COLO_R;
46801
- }
46802
- const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
46803
- poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
46804
- const num = routeNumberById.get(e.p.id);
46805
- pois.push({
46806
- id: e.p.id,
46807
- cx,
46808
- cy,
46809
- r: radiusFor(e.p),
46810
- fill: fill2,
46811
- stroke: stroke2,
46812
- lineNumber: e.p.lineNumber,
46813
- implicit: !!e.p.implicit,
46814
- isOrigin: originIds.has(e.p.id),
46815
- ...num !== void 0 && { routeNumber: num },
46816
- ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags }
46817
- });
47835
+ if (connected.has(e.p.id)) placePoi(e, e.xy[0], e.xy[1]);
47836
+ }
47837
+ const groups = [];
47838
+ for (const e of projected) {
47839
+ if (connected.has(e.p.id)) continue;
47840
+ const r = radiusOf(e);
47841
+ const near = groups.find(
47842
+ (g) => g.some(
47843
+ (q) => Math.hypot(q.xy[0] - e.xy[0], q.xy[1] - e.xy[1]) < (r + radiusOf(q)) * STACK_OVERLAP
47844
+ )
47845
+ );
47846
+ if (near) near.push(e);
47847
+ else groups.push([e]);
47848
+ }
47849
+ for (const g of groups) {
47850
+ if (g.length === 1) {
47851
+ placePoi(g[0], g[0].xy[0], g[0].xy[1]);
47852
+ continue;
47853
+ }
47854
+ const clusterId = g[0].p.id;
47855
+ const cx0 = g.reduce((s, e) => s + e.xy[0], 0) / g.length;
47856
+ const cy0 = g.reduce((s, e) => s + e.xy[1], 0) / g.length;
47857
+ const maxR = Math.max(...g.map(radiusOf));
47858
+ const sep = 2 * maxR + STACK_RING_GAP;
47859
+ const ringR = Math.max(
47860
+ COLO_R,
47861
+ sep / (2 * Math.sin(Math.PI / Math.max(g.length, 2)))
47862
+ );
47863
+ const positions = g.map((e, i) => {
47864
+ if (g.length <= STACK_RING_MAX) {
47865
+ const ang2 = -Math.PI / 2 + i * 2 * Math.PI / g.length;
47866
+ return {
47867
+ e,
47868
+ mx: cx0 + Math.cos(ang2) * ringR,
47869
+ my: cy0 + Math.sin(ang2) * ringR
47870
+ };
47871
+ }
47872
+ const ang = i * GOLDEN_ANGLE;
47873
+ const rr = ringR * Math.sqrt((i + 1) / g.length);
47874
+ return { e, mx: cx0 + Math.cos(ang) * rr, my: cy0 + Math.sin(ang) * rr };
47875
+ });
47876
+ let minX = cx0 - maxR;
47877
+ let maxX = cx0 + maxR;
47878
+ let minY = cy0 - maxR;
47879
+ let maxY = cy0 + maxR;
47880
+ for (const { mx, my, e } of positions) {
47881
+ const r = radiusOf(e);
47882
+ minX = Math.min(minX, mx - r);
47883
+ maxX = Math.max(maxX, mx + r);
47884
+ minY = Math.min(minY, my - r);
47885
+ maxY = Math.max(maxY, my + r);
47886
+ }
47887
+ let dx = 0;
47888
+ let dy = 0;
47889
+ if (minX + dx < 2) dx = 2 - minX;
47890
+ if (maxX + dx > width - 2) dx = width - 2 - maxX;
47891
+ if (minY + dy < 2) dy = 2 - minY;
47892
+ if (maxY + dy > height - 2) dy = height - 2 - maxY;
47893
+ const legsOut = [];
47894
+ for (const { e, mx, my } of positions) {
47895
+ const fx = mx + dx;
47896
+ const fy = my + dy;
47897
+ placePoi(e, fx, fy, clusterId);
47898
+ legsOut.push({ x2: fx, y2: fy, color: poiFill(e.p).fill });
47899
+ }
47900
+ clusters.push({
47901
+ id: clusterId,
47902
+ cx: cx0 + dx,
47903
+ cy: cy0 + dy,
47904
+ count: g.length,
47905
+ hitR: ringR + maxR + 6,
47906
+ legs: legsOut
46818
47907
  });
46819
47908
  }
46820
47909
  const legs = [];
@@ -46864,16 +47953,26 @@ function layoutMap(resolved, data, size, opts) {
46864
47953
  if (!a || !b) continue;
46865
47954
  const mx = (a.cx + b.cx) / 2;
46866
47955
  const my = (a.cy + b.cy) / 2;
47956
+ const bow = {
47957
+ curved: leg.style === "arc",
47958
+ offset: 0,
47959
+ labelX: mx,
47960
+ labelY: my - 4
47961
+ };
47962
+ const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46867
47963
  legs.push({
46868
- d: legPath(a, b, leg.style === "arc", 0),
47964
+ d: legPath(a, b, bow.curved, bow.offset),
46869
47965
  width: routeWidthFor(Number(leg.value)),
46870
47966
  color: mix(palette.text, palette.bg, 72),
46871
47967
  arrow: true,
46872
47968
  lineNumber: leg.lineNumber,
46873
47969
  ...leg.label !== void 0 && {
46874
47970
  label: leg.label,
46875
- labelX: mx,
46876
- labelY: my - 4
47971
+ labelX: bow.labelX,
47972
+ labelY: bow.labelY,
47973
+ labelColor: routeLabelStyle.color,
47974
+ labelHalo: routeLabelStyle.halo,
47975
+ labelHaloColor: routeLabelStyle.haloColor
46877
47976
  }
46878
47977
  });
46879
47978
  }
@@ -46901,20 +48000,29 @@ function layoutMap(resolved, data, size, opts) {
46901
48000
  const a = poiScreen.get(e.fromId);
46902
48001
  const b = poiScreen.get(e.toId);
46903
48002
  if (!a || !b) return;
46904
- const curved = e.style === "arc" || n > 1;
46905
- const offset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
48003
+ const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
46906
48004
  const mx = (a.cx + b.cx) / 2;
46907
48005
  const my = (a.cy + b.cy) / 2;
48006
+ const bow = {
48007
+ curved: e.style === "arc" || n > 1,
48008
+ offset: fanOffset,
48009
+ labelX: mx,
48010
+ labelY: my - 4
48011
+ };
48012
+ const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46908
48013
  legs.push({
46909
- d: legPath(a, b, curved, offset),
48014
+ d: legPath(a, b, bow.curved, bow.offset),
46910
48015
  width: widthFor(e),
46911
48016
  color: mix(palette.text, palette.bg, 66),
46912
48017
  arrow: e.directed,
46913
48018
  lineNumber: e.lineNumber,
46914
48019
  ...e.label !== void 0 && {
46915
48020
  label: e.label,
46916
- labelX: mx,
46917
- labelY: my - 4
48021
+ labelX: bow.labelX,
48022
+ labelY: bow.labelY,
48023
+ labelColor: edgeLabelStyle.color,
48024
+ labelHalo: edgeLabelStyle.halo,
48025
+ labelHaloColor: edgeLabelStyle.haloColor
46918
48026
  }
46919
48027
  });
46920
48028
  });
@@ -46956,25 +48064,25 @@ function layoutMap(resolved, data, size, opts) {
46956
48064
  }
46957
48065
  }
46958
48066
  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));
46959
- const regionLabelMode = resolved.directives.regionLabels ?? "off";
48067
+ const showRegionLabels = resolved.directives.noRegionLabels !== true;
48068
+ const isCompact = width < COMPACT_WIDTH_PX;
46960
48069
  const LABEL_PADX = 6;
46961
48070
  const LABEL_PADY = 3;
46962
- const labelW = (text) => measureLegendText(text, FONT) + 2 * LABEL_PADX;
46963
- const labelH = FONT + 2 * LABEL_PADY;
48071
+ const labelW = (text) => measureLegendText(text, FONT2) + 2 * LABEL_PADX;
48072
+ const labelH = FONT2 + 2 * LABEL_PADY;
46964
48073
  const pushRegionLabel = (x, y, text, fill2, lineNumber) => {
46965
- const color = contrastText(
46966
- fill2,
46967
- palette.textOnFillLight,
46968
- palette.textOnFillDark
48074
+ const { color, haloColor } = labelOnFill(fill2);
48075
+ const halfW = measureLegendText(text, FONT2) / 2;
48076
+ const overflows = [y - FONT2 * 0.55, y - FONT2 * 0.1].some(
48077
+ (sy) => fillAt(x - halfW, sy) !== fill2 || fillAt(x + halfW, sy) !== fill2
46969
48078
  );
46970
- const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
46971
48079
  labels.push({
46972
48080
  x,
46973
48081
  y,
46974
48082
  text,
46975
48083
  anchor: "middle",
46976
48084
  color,
46977
- halo: true,
48085
+ halo: overflows,
46978
48086
  haloColor,
46979
48087
  lineNumber
46980
48088
  });
@@ -46983,21 +48091,50 @@ function layoutMap(resolved, data, size, opts) {
46983
48091
  US: [-98.5, 39.5]
46984
48092
  // CONUS geographic centre (near Lebanon, Kansas)
46985
48093
  };
46986
- if (regionLabelMode === "full" || regionLabelMode === "abbrev") {
46987
- for (const r of regions) {
46988
- if (r.layer === "base" || r.label === void 0) continue;
46989
- const f = r.layer === "us-state" ? usLayer?.get(r.id) : worldLayer.get(r.id);
46990
- if (!f) continue;
48094
+ const REGION_LABEL_GAP = 2;
48095
+ const regionLabelRect = (cx, cy, text) => {
48096
+ const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
48097
+ return { x: cx - w / 2, y: cy - FONT2 / 2, w, h: FONT2 };
48098
+ };
48099
+ if (showRegionLabels) {
48100
+ const frameContainers = new Set(resolved.poiFrameContainers);
48101
+ const entries = regions.map((r) => {
48102
+ const isContainer = frameContainers.has(r.id);
48103
+ if (r.layer === "base" && !isContainer || r.label === void 0)
48104
+ return null;
48105
+ const isUsState = r.layer === "us-state" || r.id.startsWith("US-");
48106
+ const f = isUsState ? usLayer?.get(r.id) : worldLayer.get(r.id);
48107
+ if (!f) return null;
46991
48108
  const [[x0, y0], [x1, y1]] = path.bounds(f);
46992
- const text = regionLabelMode === "abbrev" ? r.id.replace(/^US-/, "") : r.label;
46993
- if (labelW(text) > x1 - x0 || labelH > y1 - y0) continue;
46994
- const anchor = r.layer !== "us-state" ? WORLD_LABEL_ANCHORS[r.id] : void 0;
48109
+ const boxW = x1 - x0;
48110
+ const boxH = y1 - y0;
48111
+ const abbrev = isUsState ? r.id.replace(/^US-/, "") : void 0;
48112
+ const candidates = abbrev !== void 0 ? isCompact ? [abbrev, r.label] : [r.label, abbrev] : [r.label];
48113
+ const anchor = !isUsState ? WORLD_LABEL_ANCHORS[r.id] : void 0;
46995
48114
  const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
46996
- if (!c || !Number.isFinite(c[0])) continue;
48115
+ if (!c || !Number.isFinite(c[0])) return null;
48116
+ return { r, c, boxW, boxH, area: boxW * boxH, candidates };
48117
+ }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
48118
+ const placedRegionRects = [];
48119
+ const POI_LABEL_PAD = 14;
48120
+ const poiObstacles = pois.map((p) => ({
48121
+ x: p.cx - p.r - POI_LABEL_PAD,
48122
+ y: p.cy - p.r - POI_LABEL_PAD,
48123
+ w: 2 * (p.r + POI_LABEL_PAD),
48124
+ h: 2 * (p.r + POI_LABEL_PAD)
48125
+ }));
48126
+ for (const { r, c, boxW, boxH, candidates } of entries) {
48127
+ const text = candidates.find((t) => {
48128
+ if (labelW(t) > boxW || labelH > boxH) return false;
48129
+ const rect = regionLabelRect(c[0], c[1], t);
48130
+ return !placedRegionRects.some((p) => rectsOverlap(rect, p)) && !poiObstacles.some((o) => rectsOverlap(rect, o));
48131
+ });
48132
+ if (text === void 0) continue;
48133
+ placedRegionRects.push(regionLabelRect(c[0], c[1], text));
46997
48134
  pushRegionLabel(c[0], c[1], text, r.fill, r.lineNumber);
46998
48135
  }
46999
48136
  for (const seed of insetLabelSeeds) {
47000
- const text = regionLabelMode === "abbrev" ? seed.iso.replace(/^US-/, "") : seed.name;
48137
+ const text = isCompact ? seed.iso.replace(/^US-/, "") : seed.name;
47001
48138
  const src = regionById.get(seed.iso);
47002
48139
  pushRegionLabel(
47003
48140
  seed.x,
@@ -47008,22 +48145,26 @@ function layoutMap(resolved, data, size, opts) {
47008
48145
  );
47009
48146
  }
47010
48147
  }
47011
- const poiLabelMode = resolved.directives.poiLabels ?? "auto";
47012
- if (poiLabelMode !== "off") {
47013
- const ordered = [...pois].sort(
47014
- (a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1)
47015
- );
48148
+ if (resolved.directives.noPoiLabels !== true) {
48149
+ const ordered = [...pois].filter((p) => p.clusterId === void 0).sort((a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1));
47016
48150
  const poiById = new Map(resolved.pois.map((q) => [q.id, q]));
47017
48151
  const labelText = (p) => {
47018
48152
  const src = poiById.get(p.id);
47019
48153
  return src?.label ?? src?.name ?? p.id;
47020
48154
  };
47021
- const poiLabH = FONT * 1.25;
48155
+ const poiLabH = FONT2 * 1.25;
47022
48156
  const labelInfo = (p) => {
47023
48157
  const text = labelText(p);
47024
- return { text, w: measureLegendText(text, FONT) };
48158
+ return { text, w: measureLegendText(text, FONT2) };
47025
48159
  };
47026
48160
  const GAP = 3;
48161
+ const clusterMembersById = /* @__PURE__ */ new Map();
48162
+ for (const p of pois) {
48163
+ if (p.clusterId === void 0) continue;
48164
+ const arr = clusterMembersById.get(p.clusterId);
48165
+ if (arr) arr.push(p);
48166
+ else clusterMembersById.set(p.clusterId, [p]);
48167
+ }
47027
48168
  const inlineRect = (p, w, side) => {
47028
48169
  switch (side) {
47029
48170
  case "right":
@@ -47053,11 +48194,11 @@ function layoutMap(resolved, data, size, opts) {
47053
48194
  const x = side === "right" ? rect.x : side === "left" ? rect.x + w : p.cx;
47054
48195
  labels.push({
47055
48196
  x,
47056
- y: rect.y + poiLabH / 2 + FONT / 3,
48197
+ y: rect.y + poiLabH / 2 + FONT2 / 3,
47057
48198
  text,
47058
48199
  anchor,
47059
48200
  color: palette.text,
47060
- halo: true,
48201
+ halo: false,
47061
48202
  haloColor: palette.bg,
47062
48203
  poiId: p.id,
47063
48204
  lineNumber: p.lineNumber
@@ -47068,43 +48209,60 @@ function layoutMap(resolved, data, size, opts) {
47068
48209
  return rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect);
47069
48210
  };
47070
48211
  const GROUP_R = 30;
47071
- const groups = [];
48212
+ const groups2 = [];
47072
48213
  for (const p of ordered) {
47073
- const near = groups.find(
48214
+ const near = groups2.find(
47074
48215
  (g) => g.some((q) => Math.hypot(q.cx - p.cx, q.cy - p.cy) < GROUP_R)
47075
48216
  );
47076
48217
  if (near) near.push(p);
47077
- else groups.push([p]);
48218
+ else groups2.push([p]);
47078
48219
  }
47079
48220
  const ROW_GAP2 = 3;
47080
48221
  const step = poiLabH + ROW_GAP2;
47081
48222
  const COL_GAP = 16;
47082
- const placeColumn = (group) => {
47083
- const items = group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48223
+ const makeItems = (group) => group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48224
+ const columnRows = (items, side) => {
47084
48225
  const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
47085
48226
  const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
47086
- const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
47087
48227
  const maxW = Math.max(...items.map((o) => o.w));
47088
- const side = right + COL_GAP + maxW <= width - 2 ? "right" : "left";
47089
- const colX = side === "right" ? right + COL_GAP : left - COL_GAP;
48228
+ const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
48229
+ const colX = side === "right" ? Math.min(right + COL_GAP, width - 2 - maxW) : Math.max(left - COL_GAP, 2 + maxW);
47090
48230
  const totalH = items.length * step;
47091
48231
  let startY = cyMid - totalH / 2;
47092
48232
  startY = Math.max(2, Math.min(startY, height - totalH - 2));
47093
- items.forEach((o, i) => {
48233
+ return items.map((o, i) => {
47094
48234
  const rowCy = startY + i * step + step / 2;
47095
- obstacles.push({
47096
- x: side === "right" ? colX : colX - o.w,
47097
- y: rowCy - poiLabH / 2,
47098
- w: o.w,
47099
- h: poiLabH
47100
- });
48235
+ return {
48236
+ o,
48237
+ colX,
48238
+ rowCy,
48239
+ rect: {
48240
+ x: side === "right" ? colX : colX - o.w,
48241
+ y: rowCy - poiLabH / 2,
48242
+ w: o.w,
48243
+ h: poiLabH
48244
+ }
48245
+ };
48246
+ });
48247
+ };
48248
+ const wouldColumnBeClean = (items, side) => columnRows(items, side).every(
48249
+ ({ rect }) => rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect)
48250
+ );
48251
+ const defaultColumnSide = (items) => {
48252
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48253
+ const maxW = Math.max(...items.map((o) => o.w));
48254
+ return right + COL_GAP + maxW <= width - 2 ? "right" : "left";
48255
+ };
48256
+ const commitColumn = (items, side, clusterId) => {
48257
+ for (const { o, colX, rowCy, rect } of columnRows(items, side)) {
48258
+ obstacles.push(rect);
47101
48259
  labels.push({
47102
48260
  x: colX,
47103
- y: rowCy + FONT / 3,
48261
+ y: rowCy + FONT2 / 3,
47104
48262
  text: o.text,
47105
48263
  anchor: side === "right" ? "start" : "end",
47106
48264
  color: palette.text,
47107
- halo: true,
48265
+ halo: false,
47108
48266
  haloColor: palette.bg,
47109
48267
  leader: {
47110
48268
  x1: o.p.cx,
@@ -47114,24 +48272,141 @@ function layoutMap(resolved, data, size, opts) {
47114
48272
  },
47115
48273
  leaderColor: o.p.fill,
47116
48274
  poiId: o.p.id,
47117
- lineNumber: o.p.lineNumber
48275
+ lineNumber: o.p.lineNumber,
48276
+ ...clusterId !== void 0 && { clusterMember: clusterId }
47118
48277
  });
48278
+ }
48279
+ };
48280
+ const pushHidden = (p) => {
48281
+ const { text, w } = labelInfo(p);
48282
+ let x = p.cx + p.r + GAP;
48283
+ let anchor = "start";
48284
+ if (x + w > width) {
48285
+ x = p.cx - p.r - GAP - w;
48286
+ anchor = "end";
48287
+ }
48288
+ const y = Math.max(0, Math.min(p.cy - poiLabH / 2, height - poiLabH));
48289
+ labels.push({
48290
+ x: anchor === "start" ? x : x + w,
48291
+ y: y + poiLabH / 2 + FONT2 / 3,
48292
+ text,
48293
+ anchor,
48294
+ color: palette.text,
48295
+ halo: false,
48296
+ haloColor: palette.bg,
48297
+ poiId: p.id,
48298
+ hidden: true,
48299
+ lineNumber: p.lineNumber
47119
48300
  });
47120
48301
  };
47121
- for (const g of groups) {
48302
+ for (const [clusterId, members] of clusterMembersById) {
48303
+ if (members.length === 0) continue;
48304
+ const items = makeItems(members);
48305
+ const side = wouldColumnBeClean(items, "right") ? "right" : wouldColumnBeClean(items, "left") ? "left" : defaultColumnSide(items);
48306
+ commitColumn(items, side, clusterId);
48307
+ }
48308
+ const maxExtent = MAX_CLUSTER_EXTENT_FACTOR * Math.min(width, height);
48309
+ const clusterPending = [];
48310
+ for (const g of groups2) {
48311
+ const items = makeItems(g);
47122
48312
  if (g.length === 1) {
47123
- const p = g[0];
47124
- const { text, w } = labelInfo(p);
48313
+ const { p, text, w } = items[0];
47125
48314
  const side = ["right", "left", "above", "below"].find(
47126
48315
  (s) => inlineFits(p, w, s)
47127
48316
  );
47128
- if (side) {
47129
- pushInline(p, text, w, side);
47130
- continue;
48317
+ if (side) pushInline(p, text, w, side);
48318
+ else commitColumn(items, defaultColumnSide(items));
48319
+ continue;
48320
+ }
48321
+ const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
48322
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48323
+ const minCy = Math.min(...items.map((o) => o.p.cy));
48324
+ const maxCy = Math.max(...items.map((o) => o.p.cy));
48325
+ const diag = Math.hypot(right - left, maxCy - minCy);
48326
+ if (diag > maxExtent || items.length > MAX_COLUMN_ROWS) {
48327
+ items.forEach((o) => pushHidden(o.p));
48328
+ } else {
48329
+ clusterPending.push(items);
48330
+ }
48331
+ }
48332
+ for (const items of clusterPending) {
48333
+ const side = ["right", "left"].find(
48334
+ (s) => wouldColumnBeClean(items, s)
48335
+ );
48336
+ if (side) commitColumn(items, side);
48337
+ else items.forEach((o) => pushHidden(o.p));
48338
+ }
48339
+ }
48340
+ if (resolved.directives.noContextLabels !== true) {
48341
+ for (const l of labels) {
48342
+ if (l.hidden) continue;
48343
+ const w = labelW(l.text);
48344
+ const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
48345
+ obstacles.push({ x, y: l.y - labelH / 2, w, h: labelH });
48346
+ }
48347
+ for (const box of insets)
48348
+ obstacles.push({ x: box.x, y: box.y, w: box.w, h: box.h });
48349
+ const countryCandidates = [];
48350
+ for (const f of worldLayer.values()) {
48351
+ const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
48352
+ if (!iso || regionById.has(iso)) continue;
48353
+ let hasReferencedSub = false;
48354
+ for (const k of regionById.keys())
48355
+ if (k.startsWith(iso + "-")) {
48356
+ hasReferencedSub = true;
48357
+ break;
47131
48358
  }
48359
+ if (hasReferencedSub) continue;
48360
+ const b = path.bounds(f);
48361
+ const [x0, y0] = b[0];
48362
+ const [x1, y1] = b[1];
48363
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48364
+ const anchorLngLat = WORLD_LABEL_ANCHORS[iso];
48365
+ const a = anchorLngLat ? project(anchorLngLat[0], anchorLngLat[1]) : path.centroid(f);
48366
+ countryCandidates.push({
48367
+ name: f.properties?.name ?? iso,
48368
+ bbox: [x0, y0, x1, y1],
48369
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48370
+ });
48371
+ }
48372
+ const framedStateContainers = (resolved.poiFrameContainers ?? []).some(
48373
+ (id) => id.startsWith("US-")
48374
+ );
48375
+ if (usLayer && framedStateContainers) {
48376
+ const containerSet = new Set(resolved.poiFrameContainers);
48377
+ for (const [iso, f] of usLayer) {
48378
+ if (containerSet.has(iso) || regionById.has(iso)) continue;
48379
+ const viewF = cullFeatureToView(f);
48380
+ if (!viewF) continue;
48381
+ const b = path.bounds(viewF);
48382
+ const [x0, y0] = b[0];
48383
+ const [x1, y1] = b[1];
48384
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48385
+ const a = path.centroid(viewF);
48386
+ countryCandidates.push({
48387
+ name: f.properties?.name ?? iso,
48388
+ bbox: [x0, y0, x1, y1],
48389
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48390
+ });
47132
48391
  }
47133
- placeColumn(g);
47134
48392
  }
48393
+ const contextLabels = placeContextLabels({
48394
+ projection: resolved.projection,
48395
+ dLonSpan,
48396
+ dLatSpan,
48397
+ width,
48398
+ height,
48399
+ waterBodies: data.waterBodies,
48400
+ countries: countryCandidates,
48401
+ palette,
48402
+ project,
48403
+ collides,
48404
+ // Water labels must stay over open water — `fillAt` returns the ocean
48405
+ // backdrop colour off-land and a region fill on-land (lakes/states count
48406
+ // as land here, which is the safe side for an ocean name).
48407
+ overLand: (x, y) => fillAt(x, y) !== water
48408
+ });
48409
+ labels.push(...contextLabels);
47135
48410
  }
47136
48411
  let legend = null;
47137
48412
  if (!resolved.directives.noLegend) {
@@ -47166,24 +48441,35 @@ function layoutMap(resolved, data, size, opts) {
47166
48441
  ...resolved.caption !== void 0 && { caption: resolved.caption },
47167
48442
  regions,
47168
48443
  rivers,
48444
+ relief,
48445
+ reliefHatch,
48446
+ coastlineStyle,
47169
48447
  legs,
47170
48448
  pois,
48449
+ clusters,
47171
48450
  labels,
47172
48451
  legend,
47173
48452
  insets,
47174
- insetRegions
48453
+ insetRegions,
48454
+ projection,
48455
+ stretch: stretchParams,
48456
+ diagnostics: []
47175
48457
  };
47176
48458
  }
47177
- var import_d3_geo2, import_topojson_client2, 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;
48459
+ var import_d3_geo2, import_topojson_client2, 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;
47178
48460
  var init_layout15 = __esm({
47179
48461
  "src/map/layout.ts"() {
47180
48462
  "use strict";
47181
48463
  import_d3_geo2 = require("d3-geo");
47182
48464
  import_topojson_client2 = require("topojson-client");
47183
48465
  init_color_utils();
48466
+ init_geo();
48467
+ init_colorize();
48468
+ init_colors();
47184
48469
  init_label_layout();
47185
48470
  init_legend_constants();
47186
48471
  init_title_constants();
48472
+ init_context_labels();
47187
48473
  FIT_PAD = 24;
47188
48474
  RAMP_FLOOR = 15;
47189
48475
  R_DEFAULT = 6;
@@ -47191,29 +48477,66 @@ var init_layout15 = __esm({
47191
48477
  R_MAX = 22;
47192
48478
  W_MIN = 1.25;
47193
48479
  W_MAX = 8;
47194
- FONT = 11;
47195
- COLO_EPS = 1.5;
47196
- LAND_TINT_LIGHT = 58;
47197
- LAND_TINT_DARK = 75;
48480
+ FONT2 = 11;
48481
+ MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48482
+ MAX_COLUMN_ROWS = 7;
48483
+ REGION_LABEL_HALO_RATIO = 4.5;
48484
+ LAND_TINT_LIGHT = 12;
48485
+ LAND_TINT_DARK = 24;
47198
48486
  TAG_TINT_LIGHT = 60;
47199
48487
  TAG_TINT_DARK = 68;
47200
- WATER_TINT = 55;
48488
+ WATER_TINT_LIGHT = 24;
48489
+ WATER_TINT_DARK = 24;
47201
48490
  RIVER_WIDTH = 1.3;
48491
+ COMPACT_WIDTH_PX = 480;
48492
+ RELIEF_MIN_AREA = 12;
48493
+ RELIEF_MIN_DIM = 2;
48494
+ RELIEF_HATCH_SPACING = 2;
48495
+ RELIEF_HATCH_WIDTH = 0.15;
48496
+ RELIEF_HATCH_STRENGTH = 32;
48497
+ COASTLINE_RING_COUNT = 5;
48498
+ COASTLINE_D0 = 16e-4;
48499
+ COASTLINE_STEP = 28e-4;
48500
+ COASTLINE_THICKNESS = 14e-4;
48501
+ COASTLINE_OPACITY_NEAR = 0.5;
48502
+ COASTLINE_OPACITY_FAR = 0.1;
48503
+ COASTLINE_MIN_EXTENT = 6e-4;
48504
+ COASTLINE_MIN_EXTENT_GLOBAL = 6e-4;
48505
+ COASTLINE_STROKE_MIX = 32;
47202
48506
  FOREIGN_TINT_LIGHT = 30;
47203
48507
  FOREIGN_TINT_DARK = 62;
47204
- MUTED_WATER_LIGHT = 14;
47205
- MUTED_WATER_DARK = 10;
47206
48508
  MUTED_FOREIGN_LIGHT = 28;
47207
48509
  MUTED_FOREIGN_DARK = 16;
47208
- MUTED_LAND_DARK = 24;
47209
48510
  COLO_R = 9;
47210
48511
  GOLDEN_ANGLE = 2.399963229728653;
48512
+ STACK_OVERLAP = 1;
48513
+ STACK_RING_MAX = 8;
48514
+ STACK_RING_GAP = 4;
47211
48515
  FAN_STEP = 16;
47212
48516
  ARC_CURVE_FRAC = 0.18;
48517
+ decodeCache = /* @__PURE__ */ new WeakMap();
47213
48518
  usConusProjection = () => (0, import_d3_geo2.geoConicEqualArea)().parallels([29.5, 45.5]).rotate([96, 0]);
47214
48519
  alaskaProjection = () => (0, import_d3_geo2.geoConicEqualArea)().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
47215
48520
  hawaiiProjection = () => (0, import_d3_geo2.geoMercator)();
47216
48521
  INSET_STATES = /* @__PURE__ */ new Set(["US-AK", "US-HI"]);
48522
+ inAlaska = (lon, lat) => lat >= 51 && (lon <= -129 || lon >= 172);
48523
+ inHawaii = (lon, lat) => lat >= 18 && lat <= 23 && lon >= -161 && lon <= -154;
48524
+ FOREIGN_BORDER = {
48525
+ CA: [
48526
+ "US-AK",
48527
+ "US-WA",
48528
+ "US-ID",
48529
+ "US-MT",
48530
+ "US-ND",
48531
+ "US-MN",
48532
+ "US-MI",
48533
+ "US-NY",
48534
+ "US-VT",
48535
+ "US-NH",
48536
+ "US-ME"
48537
+ ],
48538
+ MX: ["US-CA", "US-AZ", "US-NM", "US-TX"]
48539
+ };
47217
48540
  US_NON_CONUS = /* @__PURE__ */ new Set([
47218
48541
  "US-AK",
47219
48542
  "US-HI",
@@ -47232,6 +48555,58 @@ __export(renderer_exports16, {
47232
48555
  renderMap: () => renderMap,
47233
48556
  renderMapForExport: () => renderMapForExport
47234
48557
  });
48558
+ function pointInRing2(px, py, ring) {
48559
+ let inside = false;
48560
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
48561
+ const [xi, yi] = ring[i];
48562
+ const [xj, yj] = ring[j];
48563
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
48564
+ inside = !inside;
48565
+ }
48566
+ return inside;
48567
+ }
48568
+ function ringToPath(ring) {
48569
+ let d = "";
48570
+ for (let i = 0; i < ring.length; i++)
48571
+ d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48572
+ return d + "Z";
48573
+ }
48574
+ function coastlineOuterRings(regions, minExtent) {
48575
+ const paths = [];
48576
+ for (const r of regions) {
48577
+ const rings = parsePathRings(r.d);
48578
+ for (let i = 0; i < rings.length; i++) {
48579
+ const ring = rings[i];
48580
+ if (ring.length < 3) continue;
48581
+ let minX = Infinity;
48582
+ let minY = Infinity;
48583
+ let maxX = -Infinity;
48584
+ let maxY = -Infinity;
48585
+ for (const [x, y] of ring) {
48586
+ if (x < minX) minX = x;
48587
+ if (x > maxX) maxX = x;
48588
+ if (y < minY) minY = y;
48589
+ if (y > maxY) maxY = y;
48590
+ }
48591
+ if (Math.max(maxX - minX, maxY - minY) < minExtent) continue;
48592
+ const [fx, fy] = ring[0];
48593
+ let depth = 0;
48594
+ for (let j = 0; j < rings.length; j++)
48595
+ if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48596
+ if (depth % 2 === 1) continue;
48597
+ paths.push(ringToPath(ring));
48598
+ }
48599
+ }
48600
+ return paths;
48601
+ }
48602
+ function appendWaterLines(g, outerRings, style, flatWater) {
48603
+ const d = outerRings.join(" ");
48604
+ const linesOuterFirst = [...style.lines].sort((a, b) => b.d - a.d);
48605
+ for (const line12 of linesOuterFirst) {
48606
+ 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");
48607
+ g.append("path").attr("d", d).attr("stroke", flatWater).attr("stroke-width", 2 * line12.d).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
48608
+ }
48609
+ }
47235
48610
  function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims, activeGroupOverride) {
47236
48611
  d3Selection18.select(container).selectAll(":not([data-d3-tooltip])").remove();
47237
48612
  const width = exportDims?.width ?? container.clientWidth;
@@ -47244,6 +48619,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47244
48619
  {
47245
48620
  palette,
47246
48621
  isDark,
48622
+ // Export-only: forward the contain-fit request from mapExportDimensions so a
48623
+ // clamped/floored (off-aspect) export canvas letterboxes instead of
48624
+ // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48625
+ // keeps the global stretch-fill.
48626
+ preferContain: exportDims?.preferContain ?? false,
47247
48627
  ...activeGroupOverride !== void 0 && {
47248
48628
  activeGroup: activeGroupOverride
47249
48629
  }
@@ -47257,6 +48637,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47257
48637
  const gRegions = svg.append("g").attr("class", "dgmo-map-regions");
47258
48638
  const drawRegion = (g, r, strokeWidth) => {
47259
48639
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
48640
+ if (r.label) p.attr("data-region-name", r.label);
47260
48641
  if (r.layer !== "base") {
47261
48642
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
47262
48643
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -47277,6 +48658,52 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47277
48658
  }
47278
48659
  };
47279
48660
  for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
48661
+ if (layout.relief.length && layout.reliefHatch) {
48662
+ const h = layout.reliefHatch;
48663
+ const rangeClipId = "dgmo-relief-clip";
48664
+ const landClipId = "dgmo-relief-land";
48665
+ const rangeClip = defs.append("clipPath").attr("id", rangeClipId);
48666
+ for (const s of layout.relief) rangeClip.append("path").attr("d", s.d);
48667
+ const landClip = defs.append("clipPath").attr("id", landClipId);
48668
+ for (const r of layout.regions)
48669
+ if (r.id !== "lake") landClip.append("path").attr("d", r.d);
48670
+ 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");
48671
+ for (let y = h.spacing; y < height; y += h.spacing) {
48672
+ gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
48673
+ }
48674
+ }
48675
+ if (layout.coastlineStyle) {
48676
+ const cs = layout.coastlineStyle;
48677
+ const maskId = "dgmo-map-water-mask";
48678
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48679
+ mask.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "white");
48680
+ const landD = layout.regions.filter((r) => r.id !== "lake").map((r) => r.d).join(" ");
48681
+ const lakeD = layout.regions.filter((r) => r.id === "lake").map((r) => r.d).join(" ");
48682
+ if (landD) mask.append("path").attr("d", landD).attr("fill", "black");
48683
+ if (lakeD) mask.append("path").attr("d", lakeD).attr("fill", "white");
48684
+ if (layout.insets.length) {
48685
+ const reach = Math.max(0, ...cs.lines.map((l) => l.d + l.thickness));
48686
+ for (const box of layout.insets) {
48687
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48688
+ mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
48689
+ }
48690
+ }
48691
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
48692
+ appendWaterLines(
48693
+ gWater,
48694
+ coastlineOuterRings(layout.regions, cs.minExtent),
48695
+ cs,
48696
+ layout.background
48697
+ );
48698
+ const byStroke = /* @__PURE__ */ new Map();
48699
+ for (const r of layout.regions) {
48700
+ const arr = byStroke.get(r.stroke);
48701
+ if (arr) arr.push(r.d);
48702
+ else byStroke.set(r.stroke, [r.d]);
48703
+ }
48704
+ for (const [stroke2, ds] of byStroke)
48705
+ gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48706
+ }
47280
48707
  if (layout.rivers.length) {
47281
48708
  const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47282
48709
  for (const r of layout.rivers) {
@@ -47285,15 +48712,61 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47285
48712
  }
47286
48713
  if (layout.insets.length) {
47287
48714
  const insetG = svg.append("g").attr("class", "dgmo-map-insets");
47288
- for (const box of layout.insets) {
48715
+ layout.insets.forEach((box, bi) => {
47289
48716
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
47290
48717
  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");
47291
- }
48718
+ if (box.contextLand) {
48719
+ const clipId = `dgmo-map-inset-clip-${bi}`;
48720
+ defs.append("clipPath").attr("id", clipId).append("path").attr("d", d);
48721
+ insetG.append("path").attr("d", box.contextLand.d).attr("fill", box.contextLand.fill).attr("clip-path", `url(#${clipId})`);
48722
+ }
48723
+ });
47292
48724
  for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
47293
- }
48725
+ if (layout.coastlineStyle) {
48726
+ const cs = layout.coastlineStyle;
48727
+ const maskId = "dgmo-map-inset-water-mask";
48728
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48729
+ for (const box of layout.insets) {
48730
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48731
+ mask.append("path").attr("d", d).attr("fill", "white");
48732
+ }
48733
+ layout.insets.forEach((box, bi) => {
48734
+ if (box.contextLand)
48735
+ mask.append("path").attr("d", box.contextLand.d).attr("fill", "black").attr("clip-path", `url(#dgmo-map-inset-clip-${bi})`);
48736
+ });
48737
+ for (const r of layout.insetRegions)
48738
+ if (r.id !== "lake")
48739
+ mask.append("path").attr("d", r.d).attr("fill", "black");
48740
+ for (const r of layout.insetRegions)
48741
+ if (r.id === "lake")
48742
+ mask.append("path").attr("d", r.d).attr("fill", "white");
48743
+ const clipId = "dgmo-map-inset-water-clip";
48744
+ const clip = defs.append("clipPath").attr("id", clipId);
48745
+ for (const box of layout.insets) {
48746
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48747
+ clip.append("path").attr("d", d);
48748
+ }
48749
+ 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})`);
48750
+ appendWaterLines(
48751
+ gInsetWater,
48752
+ coastlineOuterRings(layout.insetRegions, cs.minExtent),
48753
+ cs,
48754
+ layout.background
48755
+ );
48756
+ for (const r of layout.insetRegions)
48757
+ gInsetWater.append("path").attr("d", r.d).attr("stroke", r.stroke).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48758
+ }
48759
+ }
48760
+ const wireSync = (sel, lineNumber) => {
48761
+ if (lineNumber < 1) return;
48762
+ sel.attr("data-line-number", lineNumber);
48763
+ if (onClickItem)
48764
+ sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
48765
+ };
47294
48766
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
47295
48767
  layout.legs.forEach((leg, i) => {
47296
48768
  const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round");
48769
+ wireSync(p, leg.lineNumber);
47297
48770
  if (leg.arrow) {
47298
48771
  const id = `dgmo-map-arrow-${i}`;
47299
48772
  const s = arrowSize(leg.width);
@@ -47301,25 +48774,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47301
48774
  p.attr("marker-end", `url(#${id})`);
47302
48775
  }
47303
48776
  if (leg.label !== void 0 && leg.labelX !== void 0) {
47304
- emitText(
48777
+ const lt = emitText(
47305
48778
  gLegs,
47306
48779
  leg.labelX,
47307
48780
  leg.labelY ?? 0,
47308
48781
  leg.label,
47309
48782
  "middle",
47310
- palette.textMuted,
47311
- haloColor,
47312
- true,
48783
+ leg.labelColor ?? palette.textMuted,
48784
+ leg.labelHaloColor ?? haloColor,
48785
+ leg.labelHalo ?? true,
47313
48786
  LABEL_FONT - 1
47314
48787
  );
48788
+ wireSync(lt, leg.lineNumber);
47315
48789
  }
47316
48790
  });
48791
+ const gSpider = svg.append("g").attr("class", "dgmo-map-spider");
48792
+ for (const cl of layout.clusters) {
48793
+ if (!exportDims) {
48794
+ 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");
48795
+ }
48796
+ for (const leg of cl.legs) {
48797
+ 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");
48798
+ }
48799
+ 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");
48800
+ }
47317
48801
  const gPois = svg.append("g").attr("class", "dgmo-map-pois");
47318
48802
  for (const poi of layout.pois) {
47319
48803
  if (poi.isOrigin) {
47320
48804
  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);
47321
48805
  }
47322
48806
  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);
48807
+ if (poi.clusterId !== void 0)
48808
+ c.attr("data-cluster-member", poi.clusterId);
47323
48809
  if (poi.tags) {
47324
48810
  for (const [group, value] of Object.entries(poi.tags)) {
47325
48811
  c.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
@@ -47347,12 +48833,32 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47347
48833
  }
47348
48834
  const gLabels = svg.append("g").attr("class", "dgmo-map-labels");
47349
48835
  for (const lab of layout.labels) {
48836
+ if (lab.hidden) {
48837
+ if (exportDims) continue;
48838
+ emitText(
48839
+ gLabels,
48840
+ lab.x,
48841
+ lab.y,
48842
+ lab.text,
48843
+ lab.anchor,
48844
+ lab.color,
48845
+ lab.haloColor,
48846
+ lab.halo,
48847
+ LABEL_FONT,
48848
+ lab.italic,
48849
+ lab.letterSpacing
48850
+ ).attr("data-poi", lab.poiId ?? null).attr("data-poi-hidden", "").style("opacity", 0).style("pointer-events", "none");
48851
+ continue;
48852
+ }
47350
48853
  if (lab.leader) {
47351
48854
  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(
47352
48855
  "stroke",
47353
48856
  lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
47354
48857
  ).attr("stroke-width", lab.leaderColor ? 1 : 0.75);
47355
48858
  if (lab.poiId !== void 0) line12.attr("data-poi", lab.poiId);
48859
+ if (lab.clusterMember !== void 0)
48860
+ line12.attr("data-cluster-member", lab.clusterMember);
48861
+ wireSync(line12, lab.lineNumber);
47356
48862
  }
47357
48863
  const t = emitText(
47358
48864
  gLabels,
@@ -47363,11 +48869,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47363
48869
  lab.color,
47364
48870
  lab.haloColor,
47365
48871
  lab.halo,
47366
- LABEL_FONT
48872
+ LABEL_FONT,
48873
+ lab.italic,
48874
+ lab.letterSpacing,
48875
+ lab.lines
47367
48876
  );
47368
48877
  if (lab.poiId !== void 0) {
47369
48878
  t.attr("data-poi", lab.poiId).style("cursor", "default");
47370
48879
  }
48880
+ if (lab.clusterMember !== void 0) {
48881
+ t.attr("data-cluster-member", lab.clusterMember);
48882
+ }
48883
+ wireSync(t, lab.lineNumber);
48884
+ }
48885
+ if (!exportDims && layout.clusters.length) {
48886
+ const gBadge = svg.append("g").attr("class", "dgmo-map-cluster-badges");
48887
+ for (const cl of layout.clusters) {
48888
+ const g = gBadge.append("g").attr("data-cluster", cl.id).style("opacity", 0).style("pointer-events", "none");
48889
+ const R = 9;
48890
+ 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);
48891
+ 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);
48892
+ emitText(
48893
+ g,
48894
+ cl.cx,
48895
+ cl.cy + 3,
48896
+ String(cl.count),
48897
+ "middle",
48898
+ palette.text,
48899
+ palette.bg,
48900
+ false,
48901
+ LABEL_FONT
48902
+ );
48903
+ }
47371
48904
  }
47372
48905
  if (layout.legend) {
47373
48906
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
@@ -47401,10 +48934,10 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47401
48934
  }
47402
48935
  }
47403
48936
  if (layout.title) {
47404
- 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);
48937
+ 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);
47405
48938
  }
47406
48939
  if (layout.subtitle) {
47407
- 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);
48940
+ 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);
47408
48941
  }
47409
48942
  if (layout.caption) {
47410
48943
  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);
@@ -47413,10 +48946,21 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47413
48946
  function renderMapForExport(container, resolved, data, palette, isDark, exportDims) {
47414
48947
  renderMap(container, resolved, data, palette, isDark, void 0, exportDims);
47415
48948
  }
47416
- function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize) {
47417
- const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color).text(text);
48949
+ function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize, italic, letterSpacing, lines) {
48950
+ const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color);
48951
+ if (lines && lines.length > 1) {
48952
+ const lineHeight = fontSize + 2;
48953
+ const startDy = -((lines.length - 1) / 2) * lineHeight;
48954
+ lines.forEach((ln, i) => {
48955
+ t.append("tspan").attr("x", x).attr("dy", i === 0 ? startDy : lineHeight).text(ln);
48956
+ });
48957
+ } else {
48958
+ t.text(text);
48959
+ }
48960
+ if (italic) t.attr("font-style", "italic");
48961
+ if (letterSpacing) t.attr("letter-spacing", letterSpacing);
47418
48962
  if (withHalo) {
47419
- t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7);
48963
+ 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);
47420
48964
  }
47421
48965
  return t;
47422
48966
  }
@@ -47434,6 +48978,179 @@ var init_renderer16 = __esm({
47434
48978
  }
47435
48979
  });
47436
48980
 
48981
+ // src/map/dimensions.ts
48982
+ var dimensions_exports = {};
48983
+ __export(dimensions_exports, {
48984
+ mapContentAspect: () => mapContentAspect,
48985
+ mapExportDimensions: () => mapExportDimensions
48986
+ });
48987
+ function mapContentAspect(resolved, data, ref = REF) {
48988
+ const { projection, fitTarget } = buildMapProjection(resolved, data);
48989
+ projection.fitSize([ref, ref], fitTarget);
48990
+ const b = (0, import_d3_geo3.geoPath)(projection).bounds(fitTarget);
48991
+ const w = b[1][0] - b[0][0];
48992
+ const h = b[1][1] - b[0][1];
48993
+ const aspect = w / h;
48994
+ return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
48995
+ }
48996
+ function mapExportDimensions(resolved, data, baseWidth = 1200) {
48997
+ const raw = mapContentAspect(resolved, data);
48998
+ const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
48999
+ const width = baseWidth;
49000
+ let height = Math.round(width / clamped);
49001
+ let chromeReserve = 0;
49002
+ if (resolved.title && resolved.pois.length > 0) {
49003
+ const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
49004
+ chromeReserve += Math.max(FIT_PAD2, bannerBottom + TITLE_GAP) - FIT_PAD2;
49005
+ }
49006
+ let floored = false;
49007
+ if (height - chromeReserve < MIN_MAP_BAND) {
49008
+ height = Math.round(chromeReserve + MIN_MAP_BAND);
49009
+ floored = true;
49010
+ }
49011
+ const preferContain = clamped !== raw || floored;
49012
+ return { width, height, preferContain };
49013
+ }
49014
+ var import_d3_geo3, FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
49015
+ var init_dimensions = __esm({
49016
+ "src/map/dimensions.ts"() {
49017
+ "use strict";
49018
+ import_d3_geo3 = require("d3-geo");
49019
+ init_title_constants();
49020
+ init_layout15();
49021
+ FIT_PAD2 = 24;
49022
+ TITLE_GAP = 16;
49023
+ ASPECT_MAX = 3;
49024
+ ASPECT_MIN = 0.9;
49025
+ MIN_MAP_BAND = 200;
49026
+ FALLBACK_ASPECT = 1.5;
49027
+ REF = 1e3;
49028
+ }
49029
+ });
49030
+
49031
+ // src/map/load-data.ts
49032
+ var load_data_exports = {};
49033
+ __export(load_data_exports, {
49034
+ loadMapData: () => loadMapData
49035
+ });
49036
+ async function loadNodeBuiltins() {
49037
+ const [{ readFile }, { fileURLToPath }, { dirname, resolve }] = await Promise.all([
49038
+ import("fs/promises"),
49039
+ import("url"),
49040
+ import("path")
49041
+ ]);
49042
+ return { readFile, fileURLToPath, dirname, resolve };
49043
+ }
49044
+ async function readJson(nb, dir, name) {
49045
+ return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
49046
+ }
49047
+ async function firstExistingDir(nb, baseDir) {
49048
+ for (const rel of CANDIDATE_DIRS) {
49049
+ const dir = nb.resolve(baseDir, rel);
49050
+ try {
49051
+ await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
49052
+ return dir;
49053
+ } catch {
49054
+ }
49055
+ }
49056
+ throw new Error(
49057
+ `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
49058
+ );
49059
+ }
49060
+ function validate(data) {
49061
+ const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
49062
+ if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
49063
+ throw new Error("map data assets are malformed (failed shape validation)");
49064
+ }
49065
+ return data;
49066
+ }
49067
+ function moduleBaseDir(nb) {
49068
+ try {
49069
+ const url = import_meta.url;
49070
+ if (url) return nb.dirname(nb.fileURLToPath(url));
49071
+ } catch {
49072
+ }
49073
+ if (typeof __dirname !== "undefined") return __dirname;
49074
+ return process.cwd();
49075
+ }
49076
+ function loadMapData() {
49077
+ cache ??= (async () => {
49078
+ const nb = await loadNodeBuiltins();
49079
+ const dir = await firstExistingDir(nb, moduleBaseDir(nb));
49080
+ const [
49081
+ worldCoarse,
49082
+ worldDetail,
49083
+ usStates,
49084
+ lakes,
49085
+ rivers,
49086
+ mountainRanges,
49087
+ naLand,
49088
+ naLakes,
49089
+ waterBodies,
49090
+ gazetteer
49091
+ ] = await Promise.all([
49092
+ // worldCoarse (110m) is LOAD-BEARING but NOT a render source: the world
49093
+ // basemap renders from worldDetail (50m) at all scales (resolver pins
49094
+ // basemaps.world = 'detail'). Coarse stays as the authoritative region
49095
+ // name index + dominant-landmass bbox source in resolver.ts. Do not drop it.
49096
+ readJson(nb, dir, FILES.worldCoarse),
49097
+ readJson(nb, dir, FILES.worldDetail),
49098
+ readJson(nb, dir, FILES.usStates),
49099
+ // Lakes/rivers/mountain/NA/water assets are optional — older bundles may predate them.
49100
+ readJson(nb, dir, FILES.lakes).catch(() => void 0),
49101
+ readJson(nb, dir, FILES.rivers).catch(() => void 0),
49102
+ readJson(nb, dir, FILES.mountainRanges).catch(
49103
+ () => void 0
49104
+ ),
49105
+ readJson(nb, dir, FILES.naLand).catch(() => void 0),
49106
+ readJson(nb, dir, FILES.naLakes).catch(() => void 0),
49107
+ readJson(nb, dir, FILES.waterBodies).catch(() => void 0),
49108
+ readJson(nb, dir, FILES.gazetteer)
49109
+ ]);
49110
+ return validate({
49111
+ worldCoarse,
49112
+ worldDetail,
49113
+ usStates,
49114
+ gazetteer,
49115
+ ...lakes && { lakes },
49116
+ ...rivers && { rivers },
49117
+ ...mountainRanges && { mountainRanges },
49118
+ ...naLand && { naLand },
49119
+ ...naLakes && { naLakes },
49120
+ ...waterBodies && { waterBodies }
49121
+ });
49122
+ })().catch((e) => {
49123
+ cache = void 0;
49124
+ throw e;
49125
+ });
49126
+ return cache;
49127
+ }
49128
+ var import_meta, FILES, CANDIDATE_DIRS, cache;
49129
+ var init_load_data = __esm({
49130
+ "src/map/load-data.ts"() {
49131
+ "use strict";
49132
+ import_meta = {};
49133
+ FILES = {
49134
+ worldCoarse: "world-coarse.json",
49135
+ worldDetail: "world-detail.json",
49136
+ usStates: "us-states.json",
49137
+ lakes: "lakes.json",
49138
+ rivers: "rivers.json",
49139
+ mountainRanges: "mountain-ranges.json",
49140
+ naLand: "na-land.json",
49141
+ naLakes: "na-lakes.json",
49142
+ waterBodies: "water-bodies.json",
49143
+ gazetteer: "gazetteer.json"
49144
+ };
49145
+ CANDIDATE_DIRS = [
49146
+ "./data",
49147
+ "./map-data",
49148
+ "../map-data",
49149
+ "../src/map/data"
49150
+ ];
49151
+ }
49152
+ });
49153
+
47437
49154
  // src/pyramid/renderer.ts
47438
49155
  var renderer_exports17 = {};
47439
49156
  __export(renderer_exports17, {
@@ -49435,8 +51152,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
49435
51152
  const lines = splitParticipantLabel(p.label, LABEL_MAX_CHARS);
49436
51153
  if (lines.length === 0) continue;
49437
51154
  const widest = Math.max(...lines.map((l) => l.length));
49438
- const labelWidth = widest * LABEL_CHAR_WIDTH + 10;
49439
- uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth);
51155
+ const labelWidth2 = widest * LABEL_CHAR_WIDTH + 10;
51156
+ uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth2);
49440
51157
  }
49441
51158
  uniformBoxWidth = Math.min(MAX_BOX_WIDTH, uniformBoxWidth);
49442
51159
  const effectiveGap = Math.max(PARTICIPANT_GAP, uniformBoxWidth + 30);
@@ -52131,15 +53848,15 @@ function renderArcDiagram(container, parsed, palette, _isDark, onClickItem, expo
52131
53848
  textColor,
52132
53849
  onClickItem
52133
53850
  );
52134
- const neighbors = /* @__PURE__ */ new Map();
52135
- for (const node of nodes) neighbors.set(node, /* @__PURE__ */ new Set());
53851
+ const neighbors2 = /* @__PURE__ */ new Map();
53852
+ for (const node of nodes) neighbors2.set(node, /* @__PURE__ */ new Set());
52136
53853
  for (const link of links) {
52137
- neighbors.get(link.source).add(link.target);
52138
- neighbors.get(link.target).add(link.source);
53854
+ neighbors2.get(link.source).add(link.target);
53855
+ neighbors2.get(link.target).add(link.source);
52139
53856
  }
52140
53857
  const FADE_OPACITY3 = 0.1;
52141
53858
  function handleMouseEnter(hovered) {
52142
- const connected = neighbors.get(hovered);
53859
+ const connected = neighbors2.get(hovered);
52143
53860
  g.selectAll(".arc-link").each(function() {
52144
53861
  const el = d3Selection23.select(this);
52145
53862
  const src = el.attr("data-source");
@@ -54074,7 +55791,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54074
55791
  8,
54075
55792
  Math.floor(OVERLAP_WRAP_TARGET_W / OVERLAP_CH_W)
54076
55793
  );
54077
- function wrapLabel2(text, maxChars) {
55794
+ function wrapLabel3(text, maxChars) {
54078
55795
  const words = text.split(/\s+/).filter(Boolean);
54079
55796
  const lines = [];
54080
55797
  let cur = "";
@@ -54120,7 +55837,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54120
55837
  if (!ov.label) continue;
54121
55838
  const idxs = ov.sets.map((s) => vennSets.findIndex((vs) => vs.name === s));
54122
55839
  if (idxs.some((idx) => idx < 0)) continue;
54123
- const lines = wrapLabel2(ov.label, MAX_WRAP_CHARS);
55840
+ const lines = wrapLabel3(ov.label, MAX_WRAP_CHARS);
54124
55841
  wrappedOverlapLabels.set(ov, lines);
54125
55842
  const dir = predictOverlapDirRaw(idxs);
54126
55843
  const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
@@ -55557,25 +57274,29 @@ async function renderForExport(content, theme, palette, viewState, options) {
55557
57274
  if (detectedType === "map") {
55558
57275
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55559
57276
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55560
- const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
55561
57277
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
57278
+ const { mapExportDimensions: mapExportDimensions2 } = await Promise.resolve().then(() => (init_dimensions(), dimensions_exports));
55562
57279
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55563
57280
  const mapParsed = parseMap2(content);
55564
- let mapData;
55565
- try {
55566
- mapData = await loadMapData2();
55567
- } catch {
55568
- return "";
57281
+ let mapData = options?.mapData;
57282
+ if (!mapData) {
57283
+ const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
57284
+ try {
57285
+ mapData = await loadMapData2();
57286
+ } catch {
57287
+ return "";
57288
+ }
55569
57289
  }
55570
57290
  const mapResolved = resolveMap2(mapParsed, mapData);
55571
- const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
57291
+ const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57292
+ const container2 = createExportContainer(dims2.width, dims2.height);
55572
57293
  renderMapForExport2(
55573
57294
  container2,
55574
57295
  mapResolved,
55575
57296
  mapData,
55576
57297
  effectivePalette2,
55577
57298
  theme === "dark",
55578
- { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
57299
+ dims2
55579
57300
  );
55580
57301
  return finalizeSvgExport(container2, theme, effectivePalette2);
55581
57302
  }
@@ -56436,7 +58157,8 @@ async function render(content, options) {
56436
58157
  ...options?.c4Container !== void 0 && {
56437
58158
  c4Container: options.c4Container
56438
58159
  },
56439
- ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup }
58160
+ ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup },
58161
+ ...options?.mapData !== void 0 && { mapData: options.mapData }
56440
58162
  });
56441
58163
  if (chartType === "map") {
56442
58164
  try {
@@ -56447,7 +58169,7 @@ async function render(content, options) {
56447
58169
  Promise.resolve().then(() => (init_load_data(), load_data_exports))
56448
58170
  ]
56449
58171
  );
56450
- const data = await loadMapData2();
58172
+ const data = options?.mapData ?? await loadMapData2();
56451
58173
  diagnostics = [...resolveMap2(parseMap2(content), data).diagnostics];
56452
58174
  } catch {
56453
58175
  }
@@ -56615,21 +58337,20 @@ var DIRECTIVE_KEYWORDS = /* @__PURE__ */ new Set([
56615
58337
  // Sequence
56616
58338
  "activations",
56617
58339
  "no-activations",
56618
- // Map (§24B) directives
56619
- "region",
56620
- "projection",
58340
+ // Map (§24B) directives — cosmetics on by default, bare `no-*` opt-outs
56621
58341
  "region-metric",
56622
58342
  "poi-metric",
56623
58343
  "flow-metric",
56624
- "region-labels",
56625
- "poi-labels",
56626
- "default-country",
56627
- "default-state",
56628
- "no-legend",
56629
- "muted",
56630
- "natural",
56631
- "subtitle",
58344
+ "locale",
58345
+ "active-tag",
56632
58346
  "caption",
58347
+ "no-legend",
58348
+ "no-coastline",
58349
+ "no-relief",
58350
+ "no-context-labels",
58351
+ "no-region-labels",
58352
+ "no-poi-labels",
58353
+ "no-colorize",
56633
58354
  "poi",
56634
58355
  "route",
56635
58356
  // Data charts
@@ -56922,7 +58643,11 @@ var ATTRIBUTE_KEYS = /* @__PURE__ */ new Set([
56922
58643
  "collapsed",
56923
58644
  "tech",
56924
58645
  "span",
56925
- "split"
58646
+ "split",
58647
+ // Map (§24B) reserved keys
58648
+ "value",
58649
+ "label",
58650
+ "style"
56926
58651
  ]);
56927
58652
  function applyAttributeKeys(tokens) {
56928
58653
  for (let i = 0; i < tokens.length - 1; i++) {
@@ -57295,7 +59020,7 @@ pre.dgmo, code.language-dgmo, pre > code.language-dgmo,
57295
59020
 
57296
59021
  // src/auto/index.ts
57297
59022
  init_safe_href();
57298
- var VERSION = "0.21.0";
59023
+ var VERSION = "0.22.0";
57299
59024
  var DEFAULTS = {
57300
59025
  theme: "auto",
57301
59026
  palette: "nord",