@diagrammo/dgmo 0.21.1 → 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 (73) hide show
  1. package/README.md +16 -6
  2. package/dist/advanced.cjs +2003 -466
  3. package/dist/advanced.d.cts +5714 -0
  4. package/dist/advanced.d.ts +5714 -0
  5. package/dist/advanced.js +1999 -466
  6. package/dist/auto.cjs +2048 -449
  7. package/dist/auto.d.cts +39 -0
  8. package/dist/auto.d.ts +39 -0
  9. package/dist/auto.js +121 -121
  10. package/dist/auto.mjs +2050 -450
  11. package/dist/cli.cjs +170 -170
  12. package/dist/editor.cjs +13 -16
  13. package/dist/editor.js +13 -16
  14. package/dist/highlight.cjs +15 -13
  15. package/dist/highlight.js +15 -13
  16. package/dist/index.cjs +2032 -435
  17. package/dist/index.d.cts +339 -0
  18. package/dist/index.d.ts +339 -0
  19. package/dist/index.js +2034 -436
  20. package/dist/internal.cjs +2003 -466
  21. package/dist/internal.d.cts +5714 -0
  22. package/dist/internal.d.ts +5714 -0
  23. package/dist/internal.js +1999 -466
  24. package/dist/map-data/water-bodies.json +1 -0
  25. package/docs/language-reference.md +20 -9
  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 +0 -1
  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 +12 -1
  37. package/src/boxes-and-lines/renderer.ts +39 -12
  38. package/src/cli.ts +1 -1
  39. package/src/completion.ts +32 -25
  40. package/src/cycle/renderer.ts +14 -1
  41. package/src/d3.ts +8 -2
  42. package/src/editor/highlight-api.ts +4 -0
  43. package/src/editor/keywords.ts +13 -16
  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/types.ts +34 -0
  48. package/src/map/data/water-bodies.json +1 -0
  49. package/src/map/dimensions.ts +117 -0
  50. package/src/map/geo-query.ts +21 -3
  51. package/src/map/geo.ts +47 -1
  52. package/src/map/layout.ts +1300 -251
  53. package/src/map/load-data.ts +10 -2
  54. package/src/map/parser.ts +42 -116
  55. package/src/map/renderer.ts +512 -13
  56. package/src/map/resolved-types.ts +16 -2
  57. package/src/map/resolver.ts +208 -59
  58. package/src/map/types.ts +30 -32
  59. package/src/mindmap/renderer.ts +10 -1
  60. package/src/palettes/atlas.ts +77 -0
  61. package/src/palettes/blueprint.ts +73 -0
  62. package/src/palettes/color-utils.ts +58 -1
  63. package/src/palettes/index.ts +12 -3
  64. package/src/palettes/slate.ts +73 -0
  65. package/src/palettes/tidewater.ts +73 -0
  66. package/src/render.ts +8 -1
  67. package/src/tech-radar/renderer.ts +3 -0
  68. package/src/tech-radar/types.ts +3 -0
  69. package/src/utils/d3-types.ts +5 -0
  70. package/src/utils/legend-layout.ts +21 -4
  71. package/src/utils/legend-types.ts +7 -0
  72. package/src/utils/reserved-key-registry.ts +3 -0
  73. package/src/palettes/bold.ts +0 -67
package/dist/index.js CHANGED
@@ -91,18 +91,18 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
91
91
  const results = [];
92
92
  for (let i = 0; i < points.length; i++) {
93
93
  const pt = points[i];
94
- const labelWidth = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
94
+ const labelWidth2 = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
95
95
  let best = null;
96
96
  const directions = [
97
97
  {
98
98
  // Above
99
99
  gen: (offset) => {
100
- const lx = pt.cx - labelWidth / 2;
100
+ const lx = pt.cx - labelWidth2 / 2;
101
101
  const ly = pt.cy - offset - labelHeight;
102
- if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
102
+ if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
103
103
  return null;
104
104
  return {
105
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
105
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
106
106
  textX: pt.cx,
107
107
  textY: ly + labelHeight / 2,
108
108
  anchor: "middle"
@@ -112,12 +112,12 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
112
112
  {
113
113
  // Below
114
114
  gen: (offset) => {
115
- const lx = pt.cx - labelWidth / 2;
115
+ const lx = pt.cx - labelWidth2 / 2;
116
116
  const ly = pt.cy + offset;
117
- if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
117
+ if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
118
118
  return null;
119
119
  return {
120
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
120
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
121
121
  textX: pt.cx,
122
122
  textY: ly + labelHeight / 2,
123
123
  anchor: "middle"
@@ -129,10 +129,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
129
129
  gen: (offset) => {
130
130
  const lx = pt.cx + offset;
131
131
  const ly = pt.cy - labelHeight / 2;
132
- if (lx + labelWidth > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
132
+ if (lx + labelWidth2 > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
133
133
  return null;
134
134
  return {
135
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
135
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
136
136
  textX: lx,
137
137
  textY: pt.cy,
138
138
  anchor: "start"
@@ -142,13 +142,13 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
142
142
  {
143
143
  // Left
144
144
  gen: (offset) => {
145
- const lx = pt.cx - offset - labelWidth;
145
+ const lx = pt.cx - offset - labelWidth2;
146
146
  const ly = pt.cy - labelHeight / 2;
147
147
  if (lx < chartBounds.left || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
148
148
  return null;
149
149
  return {
150
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
151
- textX: lx + labelWidth,
150
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
151
+ textX: lx + labelWidth2,
152
152
  textY: pt.cy,
153
153
  anchor: "end"
154
154
  };
@@ -198,10 +198,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
198
198
  }
199
199
  }
200
200
  if (!best) {
201
- const lx = pt.cx - labelWidth / 2;
201
+ const lx = pt.cx - labelWidth2 / 2;
202
202
  const ly = pt.cy - minGap - labelHeight;
203
203
  best = {
204
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
204
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
205
205
  textX: pt.cx,
206
206
  textY: ly + labelHeight / 2,
207
207
  anchor: "middle",
@@ -836,6 +836,9 @@ var init_reserved_key_registry = __esm({
836
836
  "value",
837
837
  "label",
838
838
  "style"
839
+ // `surface:` was removed in the 2026-06-02 defaults-on review — it is no longer
840
+ // a recognized metadata key (the route/edge surface feature was cut; §24B.7).
841
+ // A stray `surface: water` is no longer captured as a reserved key.
839
842
  ]);
840
843
  ORG_REGISTRY = staticRegistry([
841
844
  "color",
@@ -1898,77 +1901,266 @@ function getSegmentColors(palette, count) {
1898
1901
  (_, i) => hslToHex(Math.round((startHue + i * step) % 360), avgS, avgL)
1899
1902
  );
1900
1903
  }
1904
+ function politicalTints(palette, count, isDark) {
1905
+ if (count <= 0) return [];
1906
+ const base = isDark ? palette.surface : palette.bg;
1907
+ const c = palette.colors;
1908
+ const swatches = [
1909
+ .../* @__PURE__ */ new Set([
1910
+ c.green,
1911
+ c.yellow,
1912
+ c.orange,
1913
+ c.purple,
1914
+ c.red,
1915
+ c.teal,
1916
+ c.cyan,
1917
+ c.blue
1918
+ ])
1919
+ ];
1920
+ const bands = isDark ? POLITICAL_TINT_BANDS.dark : POLITICAL_TINT_BANDS.light;
1921
+ const out = [];
1922
+ for (const pct of bands) {
1923
+ if (out.length >= count) break;
1924
+ for (const s of swatches) out.push(mix(s, base, pct));
1925
+ }
1926
+ return out.slice(0, count);
1927
+ }
1928
+ var POLITICAL_TINT_BANDS;
1901
1929
  var init_color_utils = __esm({
1902
1930
  "src/palettes/color-utils.ts"() {
1903
1931
  "use strict";
1932
+ POLITICAL_TINT_BANDS = {
1933
+ light: [32, 48, 64, 80],
1934
+ dark: [44, 58, 72, 86]
1935
+ };
1904
1936
  }
1905
1937
  });
1906
1938
 
1907
- // src/palettes/bold.ts
1908
- var boldPalette;
1909
- var init_bold = __esm({
1910
- "src/palettes/bold.ts"() {
1939
+ // src/palettes/atlas.ts
1940
+ var atlasPalette;
1941
+ var init_atlas = __esm({
1942
+ "src/palettes/atlas.ts"() {
1911
1943
  "use strict";
1912
1944
  init_registry();
1913
- boldPalette = {
1914
- id: "bold",
1915
- name: "Bold",
1945
+ atlasPalette = {
1946
+ id: "atlas",
1947
+ name: "Atlas",
1916
1948
  light: {
1917
- bg: "#ffffff",
1918
- surface: "#f0f0f0",
1919
- overlay: "#f0f0f0",
1920
- border: "#cccccc",
1921
- text: "#000000",
1922
- textMuted: "#666666",
1923
- textOnFillLight: "#ffffff",
1924
- textOnFillDark: "#000000",
1925
- primary: "#0000ff",
1926
- secondary: "#ff00ff",
1927
- accent: "#00cccc",
1928
- destructive: "#ff0000",
1949
+ bg: "#f3ead3",
1950
+ // warm manila / parchment
1951
+ surface: "#ece0c0",
1952
+ // deeper paper (cards, panels)
1953
+ overlay: "#e8dab8",
1954
+ // popovers, dropdowns
1955
+ border: "#bcaa86",
1956
+ // muted sepia rule line
1957
+ text: "#463a26",
1958
+ // aged sepia-brown ink
1959
+ textMuted: "#7a6a4f",
1960
+ // faded annotation ink
1961
+ textOnFillLight: "#f7f1de",
1962
+ // parchment (light text on dark fills)
1963
+ textOnFillDark: "#3a2e1c",
1964
+ // deep ink (dark text on light fills)
1965
+ primary: "#5b7a99",
1966
+ // pull-down map ocean (steel-blue)
1967
+ secondary: "#7e9a6f",
1968
+ // lowland sage / celadon
1969
+ accent: "#b07f7c",
1970
+ // dusty rose
1971
+ destructive: "#b25a45",
1972
+ // brick / terracotta
1929
1973
  colors: {
1930
- red: "#ff0000",
1931
- orange: "#ff8000",
1932
- yellow: "#ffcc00",
1933
- green: "#00cc00",
1934
- blue: "#0000ff",
1935
- purple: "#cc00cc",
1936
- teal: "#008080",
1937
- cyan: "#00cccc",
1938
- gray: "#808080",
1939
- black: "#000000",
1940
- white: "#f0f0f0"
1974
+ red: "#bf6a52",
1975
+ // terracotta brick
1976
+ orange: "#cf9a5c",
1977
+ // map tan / ochre
1978
+ yellow: "#cdb35e",
1979
+ // straw / muted lemon
1980
+ green: "#7e9a6f",
1981
+ // sage / celadon lowland
1982
+ blue: "#5b7a99",
1983
+ // steel-blue ocean
1984
+ purple: "#9a7fa6",
1985
+ // dusty lilac / mauve
1986
+ teal: "#6fa094",
1987
+ // muted seafoam
1988
+ cyan: "#79a7b5",
1989
+ // shallow-water blue
1990
+ gray: "#8a7d68",
1991
+ // warm taupe
1992
+ black: "#463a26",
1993
+ // ink
1994
+ white: "#ece0c0"
1995
+ // paper
1941
1996
  }
1942
1997
  },
1943
1998
  dark: {
1944
- bg: "#000000",
1945
- surface: "#111111",
1946
- overlay: "#1a1a1a",
1947
- border: "#333333",
1948
- text: "#ffffff",
1949
- textMuted: "#aaaaaa",
1950
- textOnFillLight: "#ffffff",
1951
- textOnFillDark: "#000000",
1952
- primary: "#00ccff",
1953
- secondary: "#ff00ff",
1954
- accent: "#ffff00",
1955
- destructive: "#ff0000",
1999
+ bg: "#1e2a33",
2000
+ // deep map ocean (night globe)
2001
+ surface: "#27353f",
2002
+ // raised ocean
2003
+ overlay: "#2e3d48",
2004
+ // popovers, dropdowns
2005
+ border: "#3d4f5c",
2006
+ // depth-contour line
2007
+ text: "#e8dcc0",
2008
+ // parchment ink, inverted
2009
+ textMuted: "#a89a7d",
2010
+ // faded label
2011
+ textOnFillLight: "#f7f1de",
2012
+ // parchment
2013
+ textOnFillDark: "#1a242c",
2014
+ // deep ocean ink
2015
+ primary: "#7ba0bf",
2016
+ // brighter ocean
2017
+ secondary: "#9bb588",
2018
+ // sage, lifted
2019
+ accent: "#cf9a96",
2020
+ // dusty rose, lifted
2021
+ destructive: "#c9745c",
2022
+ // brick, lifted
1956
2023
  colors: {
1957
- red: "#ff0000",
1958
- orange: "#ff8000",
1959
- yellow: "#ffff00",
1960
- green: "#00ff00",
1961
- blue: "#0066ff",
1962
- purple: "#ff00ff",
1963
- teal: "#00cccc",
1964
- cyan: "#00ffff",
1965
- gray: "#808080",
1966
- black: "#111111",
1967
- white: "#ffffff"
2024
+ red: "#cf7a60",
2025
+ // terracotta
2026
+ orange: "#d9a96a",
2027
+ // tan / ochre
2028
+ yellow: "#d8c074",
2029
+ // straw
2030
+ green: "#9bb588",
2031
+ // sage lowland
2032
+ blue: "#7ba0bf",
2033
+ // ocean
2034
+ purple: "#b59ac0",
2035
+ // lilac / mauve
2036
+ teal: "#85b3a6",
2037
+ // seafoam
2038
+ cyan: "#92bccb",
2039
+ // shallow-water blue
2040
+ gray: "#9a8d76",
2041
+ // warm taupe
2042
+ black: "#27353f",
2043
+ // raised ocean
2044
+ white: "#e8dcc0"
2045
+ // parchment
1968
2046
  }
1969
2047
  }
1970
2048
  };
1971
- registerPalette(boldPalette);
2049
+ registerPalette(atlasPalette);
2050
+ }
2051
+ });
2052
+
2053
+ // src/palettes/blueprint.ts
2054
+ var blueprintPalette;
2055
+ var init_blueprint = __esm({
2056
+ "src/palettes/blueprint.ts"() {
2057
+ "use strict";
2058
+ init_registry();
2059
+ blueprintPalette = {
2060
+ id: "blueprint",
2061
+ name: "Blueprint",
2062
+ light: {
2063
+ bg: "#f4f8fb",
2064
+ // pale drafting white (faint cyan)
2065
+ surface: "#e6eef4",
2066
+ // drafting panel
2067
+ overlay: "#dde9f1",
2068
+ // popovers, dropdowns
2069
+ border: "#aac3d6",
2070
+ // pale blue grid line
2071
+ text: "#123a5e",
2072
+ // blueprint navy ink
2073
+ textMuted: "#4f7390",
2074
+ // faint draft note
2075
+ textOnFillLight: "#f4f8fb",
2076
+ // drafting white
2077
+ textOnFillDark: "#0c2f4d",
2078
+ // deep blueprint navy
2079
+ primary: "#1f5e8c",
2080
+ // blueprint blue
2081
+ secondary: "#5b7d96",
2082
+ // steel
2083
+ accent: "#b08a3e",
2084
+ // draftsman's ochre highlight
2085
+ destructive: "#c0504d",
2086
+ // correction red
2087
+ colors: {
2088
+ red: "#c25a4e",
2089
+ // correction red
2090
+ orange: "#c2823e",
2091
+ // ochre
2092
+ yellow: "#c2a843",
2093
+ // pencil gold
2094
+ green: "#4f8a6b",
2095
+ // drafting green
2096
+ blue: "#1f5e8c",
2097
+ // blueprint blue
2098
+ purple: "#6f5e96",
2099
+ // indigo pencil
2100
+ teal: "#3a8a8a",
2101
+ // teal
2102
+ cyan: "#3f8fb5",
2103
+ // cyan
2104
+ gray: "#7e8e98",
2105
+ // graphite
2106
+ black: "#123a5e",
2107
+ // navy ink
2108
+ white: "#e6eef4"
2109
+ // panel
2110
+ }
2111
+ },
2112
+ dark: {
2113
+ bg: "#103a5e",
2114
+ // deep blueprint blue (cyanotype ground)
2115
+ surface: "#16466e",
2116
+ // raised sheet
2117
+ overlay: "#1c5180",
2118
+ // popovers, dropdowns
2119
+ border: "#3a6f96",
2120
+ // grid line
2121
+ text: "#eaf2f8",
2122
+ // chalk white
2123
+ textMuted: "#9fc0d6",
2124
+ // faint chalk note
2125
+ textOnFillLight: "#eaf2f8",
2126
+ // chalk white
2127
+ textOnFillDark: "#0c2f4d",
2128
+ // deep blueprint navy
2129
+ primary: "#7fb8d8",
2130
+ // chalk cyan
2131
+ secondary: "#9fb8c8",
2132
+ // pale steel
2133
+ accent: "#d8c27a",
2134
+ // chalk amber
2135
+ destructive: "#e08a7a",
2136
+ // chalk correction red
2137
+ colors: {
2138
+ red: "#e0907e",
2139
+ // chalk red
2140
+ orange: "#e0ab78",
2141
+ // chalk amber
2142
+ yellow: "#e3d089",
2143
+ // chalk gold
2144
+ green: "#93c79e",
2145
+ // chalk green
2146
+ blue: "#8ec3e0",
2147
+ // chalk cyan-blue
2148
+ purple: "#b6a6d8",
2149
+ // chalk indigo
2150
+ teal: "#84c7c2",
2151
+ // chalk teal
2152
+ cyan: "#9fd6e0",
2153
+ // chalk cyan
2154
+ gray: "#aebecb",
2155
+ // chalk graphite
2156
+ black: "#16466e",
2157
+ // raised sheet
2158
+ white: "#eaf2f8"
2159
+ // chalk white
2160
+ }
2161
+ }
2162
+ };
2163
+ registerPalette(blueprintPalette);
1972
2164
  }
1973
2165
  });
1974
2166
 
@@ -2465,6 +2657,120 @@ var init_rose_pine = __esm({
2465
2657
  }
2466
2658
  });
2467
2659
 
2660
+ // src/palettes/slate.ts
2661
+ var slatePalette;
2662
+ var init_slate = __esm({
2663
+ "src/palettes/slate.ts"() {
2664
+ "use strict";
2665
+ init_registry();
2666
+ slatePalette = {
2667
+ id: "slate",
2668
+ name: "Slate",
2669
+ light: {
2670
+ bg: "#ffffff",
2671
+ // clean slide white
2672
+ surface: "#f3f5f8",
2673
+ // light cool-gray panel
2674
+ overlay: "#eaeef3",
2675
+ // popovers, dropdowns
2676
+ border: "#d4dae1",
2677
+ // hairline rule
2678
+ text: "#1f2933",
2679
+ // near-black slate (softer than pure black)
2680
+ textMuted: "#5b6672",
2681
+ // secondary label
2682
+ textOnFillLight: "#ffffff",
2683
+ // light text on dark fills
2684
+ textOnFillDark: "#1f2933",
2685
+ // dark text on light fills
2686
+ primary: "#3b6ea5",
2687
+ // confident corporate blue
2688
+ secondary: "#5b6672",
2689
+ // slate gray
2690
+ accent: "#3a9188",
2691
+ // muted teal accent
2692
+ destructive: "#c0504d",
2693
+ // brick red
2694
+ colors: {
2695
+ red: "#c0504d",
2696
+ // brick
2697
+ orange: "#cc7a33",
2698
+ // muted amber
2699
+ yellow: "#c9a227",
2700
+ // gold (not neon)
2701
+ green: "#5b9357",
2702
+ // forest / sage
2703
+ blue: "#3b6ea5",
2704
+ // corporate blue
2705
+ purple: "#7d5ba6",
2706
+ // muted violet
2707
+ teal: "#3a9188",
2708
+ // teal
2709
+ cyan: "#4f96c4",
2710
+ // steel cyan
2711
+ gray: "#7e8a97",
2712
+ // cool gray
2713
+ black: "#1f2933",
2714
+ // slate ink
2715
+ white: "#f3f5f8"
2716
+ // panel
2717
+ }
2718
+ },
2719
+ dark: {
2720
+ bg: "#161b22",
2721
+ // deep slate (keynote dark)
2722
+ surface: "#202833",
2723
+ // raised panel
2724
+ overlay: "#29323e",
2725
+ // popovers, dropdowns
2726
+ border: "#38424f",
2727
+ // divider
2728
+ text: "#e6eaef",
2729
+ // off-white
2730
+ textMuted: "#9aa5b1",
2731
+ // secondary label
2732
+ textOnFillLight: "#ffffff",
2733
+ // light text on dark fills
2734
+ textOnFillDark: "#161b22",
2735
+ // dark text on light fills
2736
+ primary: "#5b9bd5",
2737
+ // lifted corporate blue
2738
+ secondary: "#8593a3",
2739
+ // slate gray, lifted
2740
+ accent: "#45b3a3",
2741
+ // teal, lifted
2742
+ destructive: "#e07b6e",
2743
+ // brick, lifted
2744
+ colors: {
2745
+ red: "#e07b6e",
2746
+ // brick
2747
+ orange: "#e0975a",
2748
+ // amber
2749
+ yellow: "#d9bd5a",
2750
+ // gold
2751
+ green: "#74b56e",
2752
+ // forest / sage
2753
+ blue: "#5b9bd5",
2754
+ // corporate blue
2755
+ purple: "#a585c9",
2756
+ // violet
2757
+ teal: "#45b3a3",
2758
+ // teal
2759
+ cyan: "#62b0d9",
2760
+ // steel cyan
2761
+ gray: "#95a1ae",
2762
+ // cool gray
2763
+ black: "#202833",
2764
+ // raised panel
2765
+ white: "#e6eaef"
2766
+ // off-white
2767
+ }
2768
+ }
2769
+ };
2770
+ registerPalette(slatePalette);
2771
+ }
2772
+ });
2773
+
2468
2774
  // src/palettes/solarized.ts
2469
2775
  var solarizedPalette;
2470
2776
  var init_solarized = __esm({
@@ -2560,6 +2866,120 @@ var init_solarized = __esm({
2560
2866
  }
2561
2867
  });
2562
2868
 
2869
+ // src/palettes/tidewater.ts
2870
+ var tidewaterPalette;
2871
+ var init_tidewater = __esm({
2872
+ "src/palettes/tidewater.ts"() {
2873
+ "use strict";
2874
+ init_registry();
2875
+ tidewaterPalette = {
2876
+ id: "tidewater",
2877
+ name: "Tidewater",
2878
+ light: {
2879
+ bg: "#eceff0",
2880
+ // weathered sea-mist paper
2881
+ surface: "#e0e4e3",
2882
+ // worn deck panel
2883
+ overlay: "#dadfdf",
2884
+ // popovers, dropdowns
2885
+ border: "#a9b2b3",
2886
+ // muted slate rule
2887
+ text: "#18313f",
2888
+ // ship's-log navy ink
2889
+ textMuted: "#51636b",
2890
+ // faded log entry
2891
+ textOnFillLight: "#f3f5f3",
2892
+ // weathered white
2893
+ textOnFillDark: "#162c38",
2894
+ // deep navy
2895
+ primary: "#1f4e6b",
2896
+ // deep-sea navy
2897
+ secondary: "#b08a4f",
2898
+ // rope / manila tan
2899
+ accent: "#c69a3e",
2900
+ // brass
2901
+ destructive: "#c1433a",
2902
+ // signal-flag red
2903
+ colors: {
2904
+ red: "#c1433a",
2905
+ // signal-flag red
2906
+ orange: "#cc7a38",
2907
+ // weathered amber
2908
+ yellow: "#d6bf5a",
2909
+ // brass gold
2910
+ green: "#4f8a6b",
2911
+ // sea-glass green
2912
+ blue: "#1f4e6b",
2913
+ // deep-sea navy
2914
+ purple: "#6a5a8c",
2915
+ // twilight harbor
2916
+ teal: "#3d8c8c",
2917
+ // sea-glass teal
2918
+ cyan: "#4f9bb5",
2919
+ // shallow water
2920
+ gray: "#8a8d86",
2921
+ // driftwood gray
2922
+ black: "#18313f",
2923
+ // navy ink
2924
+ white: "#e0e4e3"
2925
+ // deck panel
2926
+ }
2927
+ },
2928
+ dark: {
2929
+ bg: "#0f2230",
2930
+ // night-harbor deep sea
2931
+ surface: "#16303f",
2932
+ // raised hull
2933
+ overlay: "#1d3a4a",
2934
+ // popovers, dropdowns
2935
+ border: "#2c4856",
2936
+ // rigging line
2937
+ text: "#e6ebe8",
2938
+ // weathered white
2939
+ textMuted: "#9aaab0",
2940
+ // faded label
2941
+ textOnFillLight: "#f3f5f3",
2942
+ // weathered white
2943
+ textOnFillDark: "#0f2230",
2944
+ // deep sea
2945
+ primary: "#4f9bc4",
2946
+ // lifted sea blue
2947
+ secondary: "#c9a46a",
2948
+ // rope tan, lifted
2949
+ accent: "#d9b25a",
2950
+ // brass, lifted
2951
+ destructive: "#e06a5e",
2952
+ // signal red, lifted
2953
+ colors: {
2954
+ red: "#e06a5e",
2955
+ // signal-flag red
2956
+ orange: "#df9a52",
2957
+ // amber
2958
+ yellow: "#e0c662",
2959
+ // brass gold
2960
+ green: "#6fb58c",
2961
+ // sea-glass green
2962
+ blue: "#4f9bc4",
2963
+ // sea blue
2964
+ purple: "#9486bf",
2965
+ // twilight harbor
2966
+ teal: "#5cb0ac",
2967
+ // sea-glass teal
2968
+ cyan: "#62b4cf",
2969
+ // shallow water
2970
+ gray: "#9aa39c",
2971
+ // driftwood gray
2972
+ black: "#16303f",
2973
+ // raised hull
2974
+ white: "#e6ebe8"
2975
+ // weathered white
2976
+ }
2977
+ }
2978
+ };
2979
+ registerPalette(tidewaterPalette);
2980
+ }
2981
+ });
2982
+
2563
2983
  // src/palettes/tokyo-night.ts
2564
2984
  var tokyoNightPalette;
2565
2985
  var init_tokyo_night = __esm({
@@ -2835,7 +3255,8 @@ var init_monokai = __esm({
2835
3255
  // src/palettes/index.ts
2836
3256
  var palettes_exports = {};
2837
3257
  __export(palettes_exports, {
2838
- boldPalette: () => boldPalette,
3258
+ atlasPalette: () => atlasPalette,
3259
+ blueprintPalette: () => blueprintPalette,
2839
3260
  catppuccinPalette: () => catppuccinPalette,
2840
3261
  contrastText: () => contrastText,
2841
3262
  draculaPalette: () => draculaPalette,
@@ -2856,7 +3277,9 @@ __export(palettes_exports, {
2856
3277
  rosePinePalette: () => rosePinePalette,
2857
3278
  shade: () => shade,
2858
3279
  shapeFill: () => shapeFill,
3280
+ slatePalette: () => slatePalette,
2859
3281
  solarizedPalette: () => solarizedPalette,
3282
+ tidewaterPalette: () => tidewaterPalette,
2860
3283
  tint: () => tint,
2861
3284
  tokyoNightPalette: () => tokyoNightPalette
2862
3285
  });
@@ -2866,17 +3289,21 @@ var init_palettes = __esm({
2866
3289
  "use strict";
2867
3290
  init_registry();
2868
3291
  init_color_utils();
2869
- init_bold();
3292
+ init_atlas();
3293
+ init_blueprint();
2870
3294
  init_catppuccin();
2871
3295
  init_gruvbox();
2872
3296
  init_nord();
2873
3297
  init_one_dark();
2874
3298
  init_rose_pine();
3299
+ init_slate();
2875
3300
  init_solarized();
3301
+ init_tidewater();
2876
3302
  init_tokyo_night();
2877
3303
  init_dracula();
2878
3304
  init_monokai();
2879
- init_bold();
3305
+ init_atlas();
3306
+ init_blueprint();
2880
3307
  init_catppuccin();
2881
3308
  init_dracula();
2882
3309
  init_gruvbox();
@@ -2884,9 +3311,15 @@ var init_palettes = __esm({
2884
3311
  init_nord();
2885
3312
  init_one_dark();
2886
3313
  init_rose_pine();
3314
+ init_slate();
2887
3315
  init_solarized();
3316
+ init_tidewater();
2888
3317
  init_tokyo_night();
2889
3318
  palettes = {
3319
+ atlas: atlasPalette,
3320
+ blueprint: blueprintPalette,
3321
+ slate: slatePalette,
3322
+ tidewater: tidewaterPalette,
2890
3323
  nord: nordPalette,
2891
3324
  catppuccin: catppuccinPalette,
2892
3325
  solarized: solarizedPalette,
@@ -2895,8 +3328,7 @@ var init_palettes = __esm({
2895
3328
  oneDark: oneDarkPalette,
2896
3329
  rosePine: rosePinePalette,
2897
3330
  dracula: draculaPalette,
2898
- monokai: monokaiPalette,
2899
- bold: boldPalette
3331
+ monokai: monokaiPalette
2900
3332
  };
2901
3333
  }
2902
3334
  });
@@ -3406,6 +3838,9 @@ function controlsGroupCapsuleWidth(toggles) {
3406
3838
  }
3407
3839
  return w;
3408
3840
  }
3841
+ function isAppHostedControls(config, isExport) {
3842
+ return !isExport && config.controlsHost === "app" && !!config.controlsGroup && config.controlsGroup.toggles.length > 0;
3843
+ }
3409
3844
  function buildControlsGroupLayout(config, state) {
3410
3845
  const cg = config.controlsGroup;
3411
3846
  if (!cg || cg.toggles.length === 0) return void 0;
@@ -3459,6 +3894,7 @@ function buildControlsGroupLayout(config, state) {
3459
3894
  function computeLegendLayout(config, state, containerWidth) {
3460
3895
  const { groups, controls: configControls, mode } = config;
3461
3896
  const isExport = mode === "export";
3897
+ const gated = isAppHostedControls(config, isExport);
3462
3898
  const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
3463
3899
  if (isExport && !activeGroupName) {
3464
3900
  return {
@@ -3469,7 +3905,7 @@ function computeLegendLayout(config, state, containerWidth) {
3469
3905
  pills: []
3470
3906
  };
3471
3907
  }
3472
- const controlsGroupLayout = isExport ? void 0 : buildControlsGroupLayout(config, state);
3908
+ const controlsGroupLayout = isExport || gated ? void 0 : buildControlsGroupLayout(config, state);
3473
3909
  const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0 || !!g.gradient);
3474
3910
  if (visibleGroups.length === 0 && (!configControls || configControls.length === 0) && !controlsGroupLayout) {
3475
3911
  return {
@@ -8371,8 +8807,8 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8371
8807
  const pt = points[i];
8372
8808
  const ptSize = pt.size ?? symbolSize;
8373
8809
  const minGap = ptSize / 2 + 4;
8374
- const labelWidth = pt.name.length * fontSize * 0.6 + 8;
8375
- const labelX = pt.px - labelWidth / 2;
8810
+ const labelWidth2 = pt.name.length * fontSize * 0.6 + 8;
8811
+ const labelX = pt.px - labelWidth2 / 2;
8376
8812
  let bestLabelY = 0;
8377
8813
  let bestOffset = Infinity;
8378
8814
  let placed = false;
@@ -8384,7 +8820,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8384
8820
  const candidate = {
8385
8821
  x: labelX,
8386
8822
  y: labelY,
8387
- w: labelWidth,
8823
+ w: labelWidth2,
8388
8824
  h: labelHeight
8389
8825
  };
8390
8826
  let collision = false;
@@ -8426,7 +8862,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8426
8862
  const labelRect = {
8427
8863
  x: labelX,
8428
8864
  y: bestLabelY,
8429
- w: labelWidth,
8865
+ w: labelWidth2,
8430
8866
  h: labelHeight
8431
8867
  };
8432
8868
  placedLabels.push(labelRect);
@@ -8462,7 +8898,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8462
8898
  shape: {
8463
8899
  x: labelX - bgPad,
8464
8900
  y: bestLabelY - bgPad,
8465
- width: labelWidth + bgPad * 2,
8901
+ width: labelWidth2 + bgPad * 2,
8466
8902
  height: labelHeight + bgPad * 2
8467
8903
  },
8468
8904
  style: { fill: bg },
@@ -15898,10 +16334,6 @@ function parseMap(content) {
15898
16334
  handleTag(trimmed, lineNumber);
15899
16335
  continue;
15900
16336
  }
15901
- if ((firstWord === "muted" || firstWord === "natural") && trimmed === firstWord) {
15902
- handleDirective(firstWord, "", lineNumber);
15903
- continue;
15904
- }
15905
16337
  if (DIRECTIVE_SET.has(firstWord) && !trimmed.slice(firstWord.length).trimStart().startsWith(":")) {
15906
16338
  handleDirective(
15907
16339
  firstWord,
@@ -15948,24 +16380,6 @@ function parseMap(content) {
15948
16380
  pushWarning(line12, `Duplicate directive "${key}" \u2014 last value wins.`);
15949
16381
  };
15950
16382
  switch (key) {
15951
- case "region":
15952
- dup(d.region);
15953
- d.region = value;
15954
- break;
15955
- case "projection":
15956
- dup(d.projection);
15957
- if (value && ![
15958
- "equirectangular",
15959
- "natural-earth",
15960
- "albers-usa",
15961
- "mercator"
15962
- ].includes(value))
15963
- pushWarning(
15964
- line12,
15965
- `Unknown projection "${value}" (expected equirectangular | natural-earth | albers-usa | mercator).`
15966
- );
15967
- d.projection = value;
15968
- break;
15969
16383
  case "region-metric": {
15970
16384
  dup(d.regionMetric);
15971
16385
  const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
@@ -15981,91 +16395,43 @@ function parseMap(content) {
15981
16395
  dup(d.flowMetric);
15982
16396
  d.flowMetric = value;
15983
16397
  break;
15984
- case "scale":
15985
- dup(d.scale);
15986
- {
15987
- const s = parseScale(value, line12);
15988
- if (s) d.scale = s;
15989
- }
15990
- break;
15991
- case "region-labels":
15992
- dup(d.regionLabels);
15993
- if (value && !["full", "abbrev", "off"].includes(value))
15994
- pushWarning(
15995
- line12,
15996
- `Unknown region-labels "${value}" (expected full | abbrev | off).`
15997
- );
15998
- d.regionLabels = value;
15999
- break;
16000
- case "poi-labels":
16001
- dup(d.poiLabels);
16002
- if (value && !["off", "auto", "all"].includes(value))
16003
- pushWarning(
16004
- line12,
16005
- `Unknown poi-labels "${value}" (expected off | auto | all).`
16006
- );
16007
- d.poiLabels = value;
16008
- break;
16009
- case "default-country":
16010
- dup(d.defaultCountry);
16011
- d.defaultCountry = value;
16012
- break;
16013
- case "default-state":
16014
- dup(d.defaultState);
16015
- d.defaultState = value;
16398
+ case "locale":
16399
+ dup(d.locale);
16400
+ d.locale = value;
16016
16401
  break;
16017
16402
  case "active-tag":
16018
16403
  dup(d.activeTag);
16019
16404
  d.activeTag = value;
16020
16405
  break;
16406
+ case "caption":
16407
+ dup(d.caption);
16408
+ d.caption = value;
16409
+ break;
16410
+ // ── Cosmetic `no-*` opt-outs: bare flags, idempotent (mirror `no-legend`,
16411
+ // no dup warning); each defaults the feature ON when absent. ──
16021
16412
  case "no-legend":
16022
16413
  d.noLegend = true;
16023
16414
  break;
16024
- case "no-insets":
16025
- d.noInsets = true;
16415
+ case "no-coastline":
16416
+ d.noCoastline = true;
16026
16417
  break;
16027
- case "relief":
16028
- d.relief = true;
16418
+ case "no-relief":
16419
+ d.noRelief = true;
16029
16420
  break;
16030
- case "muted":
16031
- case "natural":
16032
- if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
16033
- pushWarning(
16034
- line12,
16035
- `Conflicting basemap dress \u2014 "${d.basemapStyle}" then "${key}"; last wins.`
16036
- );
16037
- d.basemapStyle = key;
16421
+ case "no-context-labels":
16422
+ d.noContextLabels = true;
16038
16423
  break;
16039
- case "subtitle":
16040
- dup(d.subtitle);
16041
- d.subtitle = value;
16424
+ case "no-region-labels":
16425
+ d.noRegionLabels = true;
16042
16426
  break;
16043
- case "caption":
16044
- dup(d.caption);
16045
- d.caption = value;
16427
+ case "no-poi-labels":
16428
+ d.noPoiLabels = true;
16429
+ break;
16430
+ case "no-colorize":
16431
+ d.noColorize = true;
16046
16432
  break;
16047
16433
  }
16048
16434
  }
16049
- function parseScale(value, line12) {
16050
- const toks = value.split(/\s+/).filter(Boolean);
16051
- const min = Number(toks[0]);
16052
- const max = Number(toks[1]);
16053
- if (!Number.isFinite(min) || !Number.isFinite(max)) {
16054
- pushError(line12, `scale requires numeric <min> <max> (got "${value}").`);
16055
- return null;
16056
- }
16057
- const scale = { min, max };
16058
- if (toks[2] === "center") {
16059
- const c = Number(toks[3]);
16060
- if (Number.isFinite(c)) scale.center = c;
16061
- else
16062
- pushError(
16063
- line12,
16064
- `scale center requires a number (got "${toks[3] ?? ""}").`
16065
- );
16066
- }
16067
- return scale;
16068
- }
16069
16435
  function handleTag(trimmed, line12) {
16070
16436
  const m = matchTagBlockHeading(trimmed);
16071
16437
  if (!m) {
@@ -16265,13 +16631,15 @@ function parseMap(content) {
16265
16631
  pushError(line12, `Edge has an empty endpoint: "${trimmed}".`);
16266
16632
  continue;
16267
16633
  }
16268
- const meta = k === links.length - 1 ? lastSplit.meta : {};
16634
+ const isLast = k === links.length - 1;
16635
+ const meta = isLast ? lastSplit.meta : {};
16636
+ const style = links[k].style === "arc" ? "arc" : "straight";
16269
16637
  edges.push({
16270
16638
  from,
16271
16639
  to,
16272
16640
  ...links[k].label !== void 0 && { label: links[k].label },
16273
16641
  directed: links[k].directed,
16274
- style: links[k].style,
16642
+ style,
16275
16643
  meta,
16276
16644
  lineNumber: line12
16277
16645
  });
@@ -16357,22 +16725,19 @@ var init_parser12 = __esm({
16357
16725
  LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
16358
16726
  AT_RE = /(^|[\s,])at\s*:/i;
16359
16727
  DIRECTIVE_SET = /* @__PURE__ */ new Set([
16360
- "region",
16361
- "projection",
16362
16728
  "region-metric",
16363
16729
  "poi-metric",
16364
16730
  "flow-metric",
16365
- "scale",
16366
- "region-labels",
16367
- "poi-labels",
16368
- "default-country",
16369
- "default-state",
16731
+ "locale",
16370
16732
  "active-tag",
16733
+ "caption",
16371
16734
  "no-legend",
16372
- "no-insets",
16373
- "relief",
16374
- "subtitle",
16375
- "caption"
16735
+ "no-coastline",
16736
+ "no-relief",
16737
+ "no-context-labels",
16738
+ "no-region-labels",
16739
+ "no-poi-labels",
16740
+ "no-colorize"
16376
16741
  ]);
16377
16742
  }
16378
16743
  });
@@ -24294,8 +24659,8 @@ function renderKanban(container, parsed, palette, isDark, options) {
24294
24659
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24295
24660
  for (const meta of tagMeta) {
24296
24661
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(`${meta.label}: `);
24297
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24298
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24662
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24663
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24299
24664
  metaY += sCardMetaLineHeight;
24300
24665
  }
24301
24666
  for (const detail of card.details) {
@@ -24639,8 +25004,8 @@ function renderSwimlaneCard(parent, cardLayout, tagGroups, activeTagGroup, palet
24639
25004
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24640
25005
  for (const meta of tagMeta) {
24641
25006
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", palette.textMuted).text(`${meta.label}: `);
24642
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24643
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
25007
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
25008
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24644
25009
  metaY += sCardMetaLineHeight;
24645
25010
  }
24646
25011
  for (const detail of card.details) {
@@ -25474,8 +25839,8 @@ function classifyEREntities(tables, relationships) {
25474
25839
  }
25475
25840
  }
25476
25841
  const mmParticipants = /* @__PURE__ */ new Set();
25477
- for (const [id, neighbors] of tableStarNeighbors) {
25478
- if (neighbors.size >= 2) mmParticipants.add(id);
25842
+ for (const [id, neighbors2] of tableStarNeighbors) {
25843
+ if (neighbors2.size >= 2) mmParticipants.add(id);
25479
25844
  }
25480
25845
  const indegreeValues = Object.values(indegreeMap);
25481
25846
  const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
@@ -26148,7 +26513,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26148
26513
  controlsExpanded,
26149
26514
  onToggleDescriptions,
26150
26515
  onToggleControlsExpand,
26151
- exportMode = false
26516
+ exportMode = false,
26517
+ controlsHost
26152
26518
  } = options ?? {};
26153
26519
  d3Selection6.select(container).selectAll(":not([data-d3-tooltip])").remove();
26154
26520
  const width = exportDims?.width ?? container.clientWidth;
@@ -26166,7 +26532,11 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26166
26532
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26167
26533
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26168
26534
  const sTitleY = sctx.structural(TITLE_Y);
26169
- const sLegendHeight = sctx.structural(
26535
+ const reserveHasDescriptions = parsed.nodes.some(
26536
+ (n) => n.description && n.description.length > 0
26537
+ );
26538
+ const willRenderLegend = parsed.tagGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26539
+ const sLegendHeight = willRenderLegend ? sctx.structural(
26170
26540
  getMaxLegendReservedHeight(
26171
26541
  {
26172
26542
  groups: parsed.tagGroups,
@@ -26175,7 +26545,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26175
26545
  },
26176
26546
  width
26177
26547
  )
26178
- );
26548
+ ) : 0;
26179
26549
  const activeGroup = resolveActiveTagGroup(
26180
26550
  parsed.tagGroups,
26181
26551
  parsed.options["active-tag"],
@@ -26490,10 +26860,10 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26490
26860
  const hasDescriptions = parsed.nodes.some(
26491
26861
  (n) => n.description && n.description.length > 0
26492
26862
  );
26493
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions;
26863
+ const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26494
26864
  if (hasLegend) {
26495
26865
  let controlsGroup;
26496
- if (hasDescriptions && onToggleDescriptions) {
26866
+ if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
26497
26867
  controlsGroup = {
26498
26868
  toggles: [
26499
26869
  {
@@ -26511,7 +26881,14 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26511
26881
  groups: parsed.tagGroups,
26512
26882
  position: { placement: "top-center", titleRelation: "below-title" },
26513
26883
  mode: exportMode ? "export" : "preview",
26514
- ...controlsGroup !== void 0 && { controlsGroup }
26884
+ // Keep inactive sibling tag groups visible as collapsed pills so the user
26885
+ // can click one to flip the active colouring dimension (preview only —
26886
+ // export shows just the active group). Without this, declaring a second
26887
+ // tag group (e.g. Team) leaves it invisible whenever another group is
26888
+ // active. The app's BoxesAndLinesPreview already wires pill clicks.
26889
+ showInactivePills: true,
26890
+ ...controlsGroup !== void 0 && { controlsGroup },
26891
+ ...controlsHost !== void 0 && { controlsHost }
26515
26892
  };
26516
26893
  const legendState = {
26517
26894
  activeGroup,
@@ -27758,8 +28135,9 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27758
28135
  const containerHeight = exportDims?.height ?? (container.getBoundingClientRect().height || 600);
27759
28136
  d3Selection7.select(container).selectAll("*").remove();
27760
28137
  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);
28138
+ const appHosted = options?.controlsHost === "app";
27761
28139
  const hasControls = !!options?.onToggleColorByDepth || !!options?.onToggleDescriptions;
27762
- const hasLegend = parsed.tagGroups.length > 0 || hasControls;
28140
+ const hasLegend = parsed.tagGroups.length > 0 || hasControls && !appHosted;
27763
28141
  const fixedLegend = !isExport && hasLegend;
27764
28142
  const legendReserve = fixedLegend ? getMaxLegendReservedHeight(
27765
28143
  {
@@ -27853,7 +28231,10 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27853
28231
  }),
27854
28232
  position: { placement: "top-center", titleRelation: "below-title" },
27855
28233
  mode: options?.exportMode ? "export" : "preview",
27856
- ...controlsToggles !== void 0 && { controlsGroup: controlsToggles }
28234
+ ...controlsToggles !== void 0 && { controlsGroup: controlsToggles },
28235
+ ...options?.controlsHost !== void 0 && {
28236
+ controlsHost: options.controlsHost
28237
+ }
27857
28238
  };
27858
28239
  const legendState = {
27859
28240
  activeGroup: options?.colorByDepth ? null : activeTagGroup !== void 0 ? activeTagGroup : parsed.options["active-tag"] ?? null,
@@ -28296,8 +28677,8 @@ function computeFieldAlignX(children) {
28296
28677
  for (const child of children) {
28297
28678
  if (child.metadata["_labelField"] === "true" && child.children.length >= 2) {
28298
28679
  const labelEl = child.children[0];
28299
- const labelWidth = labelEl.label.length * CHAR_WIDTH5;
28300
- maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
28680
+ const labelWidth2 = labelEl.label.length * CHAR_WIDTH5;
28681
+ maxLabelWidth = Math.max(maxLabelWidth, labelWidth2);
28301
28682
  labelFieldCount++;
28302
28683
  }
28303
28684
  }
@@ -33262,7 +33643,7 @@ function hasRoles(node) {
33262
33643
  function computeNodeWidth2(node, expanded, options) {
33263
33644
  const badgeVal = node.computedConcurrentInvocations === 0 && node.computedInstances > 1 ? node.computedInstances : 0;
33264
33645
  const badgeLen = badgeVal > 0 ? `${badgeVal}x`.length + 2 : 0;
33265
- const labelWidth = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33646
+ const labelWidth2 = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33266
33647
  const allKeys = [];
33267
33648
  if (node.computedRps > 0) allKeys.push("RPS");
33268
33649
  if (expanded) {
@@ -33306,7 +33687,7 @@ function computeNodeWidth2(node, expanded, options) {
33306
33687
  allKeys.push("overflow");
33307
33688
  }
33308
33689
  }
33309
- if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth);
33690
+ if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth2);
33310
33691
  const maxKeyLen = Math.max(...allKeys.map((k) => k.length));
33311
33692
  let maxRowWidth = 0;
33312
33693
  if (node.computedRps > 0) {
@@ -33394,7 +33775,7 @@ function computeNodeWidth2(node, expanded, options) {
33394
33775
  truncated.length * META_CHAR_WIDTH3 + PADDING_X3
33395
33776
  );
33396
33777
  }
33397
- return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20, descWidth);
33778
+ return Math.max(MIN_NODE_WIDTH2, labelWidth2, maxRowWidth + 20, descWidth);
33398
33779
  }
33399
33780
  function computeNodeHeight2(node, expanded, options) {
33400
33781
  const propCount = countDisplayProps(node, expanded, options);
@@ -34944,8 +35325,9 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
34944
35325
  }
34945
35326
  return groups;
34946
35327
  }
34947
- function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false) {
35328
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false, controlsHost) {
34948
35329
  if (legendGroups.length === 0 && !playback) return;
35330
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34949
35331
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
34950
35332
  if (activeGroup) {
34951
35333
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
@@ -34954,14 +35336,29 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34954
35336
  name: g.name,
34955
35337
  entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
34956
35338
  }));
34957
- if (playback) {
35339
+ if (playback && !appHostedPlayback) {
34958
35340
  allGroups.push({ name: "Playback", entries: [] });
34959
35341
  }
34960
35342
  const legendConfig = {
34961
35343
  groups: allGroups,
34962
35344
  position: { placement: "top-center", titleRelation: "below-title" },
34963
35345
  mode: exportMode ? "export" : "preview",
34964
- showEmptyGroups: true
35346
+ showEmptyGroups: true,
35347
+ ...appHostedPlayback && {
35348
+ controlsHost: "app",
35349
+ controlsGroup: {
35350
+ toggles: [
35351
+ {
35352
+ id: "playback",
35353
+ type: "toggle",
35354
+ label: "Playback",
35355
+ active: true,
35356
+ onToggle: () => {
35357
+ }
35358
+ }
35359
+ ]
35360
+ }
35361
+ }
34965
35362
  };
34966
35363
  const legendState = { activeGroup };
34967
35364
  renderLegendD3(
@@ -35012,8 +35409,9 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
35012
35409
  }
35013
35410
  }
35014
35411
  }
35015
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
35412
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes, controlsHost) {
35016
35413
  d3Selection11.select(container).selectAll(":not([data-d3-tooltip])").remove();
35414
+ const appHostedPlayback = controlsHost === "app" && !!playback;
35017
35415
  const ctx = ScaleContext.identity();
35018
35416
  const sc = buildScaledConstants(ctx);
35019
35417
  const legendGroups = computeInfraLegendGroups(
@@ -35022,7 +35420,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35022
35420
  palette,
35023
35421
  layout.edges
35024
35422
  );
35025
- const hasLegend = legendGroups.length > 0 || !!playback;
35423
+ const hasLegend = legendGroups.length > 0 || !!playback && !appHostedPlayback;
35026
35424
  const fixedLegend = !exportMode && hasLegend;
35027
35425
  const legendDynamicH = hasLegend ? getMaxLegendReservedHeight(
35028
35426
  {
@@ -35166,7 +35564,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35166
35564
  isDark,
35167
35565
  activeGroup ?? null,
35168
35566
  playback ?? void 0,
35169
- exportMode
35567
+ exportMode,
35568
+ controlsHost
35170
35569
  );
35171
35570
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
35172
35571
  } else {
@@ -35179,7 +35578,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35179
35578
  isDark,
35180
35579
  activeGroup ?? null,
35181
35580
  playback ?? void 0,
35182
- exportMode
35581
+ exportMode,
35582
+ controlsHost
35183
35583
  );
35184
35584
  }
35185
35585
  }
@@ -42813,6 +43213,9 @@ function renderTechRadar(container, parsed, palette, isDark, onClickItem, export
42813
43213
  onToggle: (active) => options.onToggleListing(active)
42814
43214
  }
42815
43215
  ]
43216
+ },
43217
+ ...options.controlsHost !== void 0 && {
43218
+ controlsHost: options.controlsHost
42816
43219
  }
42817
43220
  };
42818
43221
  const legendState = {
@@ -44635,7 +45038,7 @@ function computeCycleLayout(parsed, options) {
44635
45038
  const circleNodes = parsed.options["circle-nodes"] === "true";
44636
45039
  const nodeDims = parsed.nodes.map((node) => {
44637
45040
  const hasDesc = !hideDescriptions && node.description.length > 0;
44638
- const labelWidth = Math.max(
45041
+ const labelWidth2 = Math.max(
44639
45042
  MIN_NODE_WIDTH4,
44640
45043
  node.label.length * LABEL_CHAR_W + NODE_PAD_X * 2
44641
45044
  );
@@ -44644,12 +45047,12 @@ function computeCycleLayout(parsed, options) {
44644
45047
  }
44645
45048
  if (!hasDesc) {
44646
45049
  return {
44647
- width: Math.min(MAX_NODE_WIDTH3, labelWidth),
45050
+ width: Math.min(MAX_NODE_WIDTH3, labelWidth2),
44648
45051
  height: PLAIN_NODE_HEIGHT,
44649
45052
  wrappedDesc: []
44650
45053
  };
44651
45054
  }
44652
- return chooseDescribedRectDims(node.description, labelWidth);
45055
+ return chooseDescribedRectDims(node.description, labelWidth2);
44653
45056
  });
44654
45057
  if (circleNodes) {
44655
45058
  const maxDiam = Math.max(...nodeDims.map((d) => d.width));
@@ -44845,10 +45248,10 @@ function computeCycleLayout(parsed, options) {
44845
45248
  scale
44846
45249
  };
44847
45250
  }
44848
- function chooseDescribedRectDims(description, labelWidth) {
45251
+ function chooseDescribedRectDims(description, labelWidth2) {
44849
45252
  const minW = Math.min(
44850
45253
  MAX_NODE_WIDTH3,
44851
- Math.max(MIN_NODE_WIDTH4, labelWidth, DESC_MIN_WIDTH)
45254
+ Math.max(MIN_NODE_WIDTH4, labelWidth2, DESC_MIN_WIDTH)
44852
45255
  );
44853
45256
  let best = null;
44854
45257
  let bestScore = Infinity;
@@ -45277,7 +45680,8 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45277
45680
  const hideDescriptions = (renderOptions?.hideDescriptions ?? false) || parsed.options["no-descriptions"] === "true" || viewState?.hd === true;
45278
45681
  const showDescriptions = !hideDescriptions;
45279
45682
  const hasDescriptions = parsed.nodes.some((n) => n.description.length > 0) || parsed.edges.some((e) => e.description.length > 0);
45280
- const hasLegend = hasDescriptions && !!renderOptions?.onToggleDescriptions;
45683
+ const appHostedControls = renderOptions?.controlsHost === "app";
45684
+ const hasLegend = !appHostedControls && hasDescriptions && !!renderOptions?.onToggleDescriptions;
45281
45685
  const showTitle = !!parsed.title && parsed.options["no-title"] !== "on";
45282
45686
  const legendOffset = hasLegend ? sLegendHeight : 0;
45283
45687
  const layoutHeight = height - (showTitle ? sTitleAreaHeight : 0) - legendOffset;
@@ -45314,7 +45718,10 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45314
45718
  groups: [],
45315
45719
  position: { placement: "top-center", titleRelation: "below-title" },
45316
45720
  mode: renderOptions?.exportMode ? "export" : "preview",
45317
- controlsGroup
45721
+ controlsGroup,
45722
+ ...renderOptions?.controlsHost !== void 0 && {
45723
+ controlsHost: renderOptions.controlsHost
45724
+ }
45318
45725
  };
45319
45726
  const legendState = {
45320
45727
  activeGroup: null,
@@ -45568,7 +45975,7 @@ var init_renderer15 = __esm({
45568
45975
  });
45569
45976
 
45570
45977
  // src/map/geo.ts
45571
- import { feature } from "topojson-client";
45978
+ import { feature, neighbors } from "topojson-client";
45572
45979
  import { geoBounds, geoArea } from "d3-geo";
45573
45980
  function geomObject(topo) {
45574
45981
  const key = Object.keys(topo.objects)[0];
@@ -45586,6 +45993,107 @@ function featureIndex(topo) {
45586
45993
  }
45587
45994
  return idx;
45588
45995
  }
45996
+ function buildAdjacency(topo) {
45997
+ const cached = adjacencyCache.get(topo);
45998
+ if (cached) return cached;
45999
+ const geometries = geomObject(topo).geometries;
46000
+ const nb = neighbors(geometries);
46001
+ const sets = /* @__PURE__ */ new Map();
46002
+ geometries.forEach((g, i) => {
46003
+ if (!g.type || g.type === "null") return;
46004
+ let set = sets.get(g.id);
46005
+ if (!set) {
46006
+ set = /* @__PURE__ */ new Set();
46007
+ sets.set(g.id, set);
46008
+ }
46009
+ for (const j of nb[i] ?? []) {
46010
+ const nid = geometries[j]?.id;
46011
+ if (nid && nid !== g.id) set.add(nid);
46012
+ }
46013
+ });
46014
+ const out = /* @__PURE__ */ new Map();
46015
+ for (const [iso, set] of sets) out.set(iso, [...set].sort());
46016
+ adjacencyCache.set(topo, out);
46017
+ return out;
46018
+ }
46019
+ function decodeFeatures(topo) {
46020
+ return geomObject(topo).geometries.map((g) => {
46021
+ const f = feature(topo, g);
46022
+ return {
46023
+ type: "Feature",
46024
+ id: g.id,
46025
+ properties: g.properties,
46026
+ geometry: f.geometry
46027
+ };
46028
+ });
46029
+ }
46030
+ function pointInRing(lon, lat, ring) {
46031
+ let inside = false;
46032
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46033
+ const xi = ring[i][0];
46034
+ const yi = ring[i][1];
46035
+ const xj = ring[j][0];
46036
+ const yj = ring[j][1];
46037
+ const intersect = yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi;
46038
+ if (intersect) inside = !inside;
46039
+ }
46040
+ return inside;
46041
+ }
46042
+ function pointOnRingEdge(lon, lat, ring) {
46043
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46044
+ const xi = ring[i][0];
46045
+ const yi = ring[i][1];
46046
+ const xj = ring[j][0];
46047
+ const yj = ring[j][1];
46048
+ if (lon < Math.min(xi, xj) - EDGE_EPS || lon > Math.max(xi, xj) + EDGE_EPS)
46049
+ continue;
46050
+ if (lat < Math.min(yi, yj) - EDGE_EPS || lat > Math.max(yi, yj) + EDGE_EPS)
46051
+ continue;
46052
+ const cross = (xj - xi) * (lat - yi) - (yj - yi) * (lon - xi);
46053
+ if (Math.abs(cross) <= EDGE_EPS) return true;
46054
+ }
46055
+ return false;
46056
+ }
46057
+ function pointInGeometry(geometry, lon, lat) {
46058
+ const g = geometry;
46059
+ if (!g) return false;
46060
+ const polys = g.type === "Polygon" ? [g.coordinates] : g.type === "MultiPolygon" ? g.coordinates : [];
46061
+ for (const rings of polys) {
46062
+ if (!rings.length) continue;
46063
+ if (pointOnRingEdge(lon, lat, rings[0])) return true;
46064
+ if (!pointInRing(lon, lat, rings[0])) continue;
46065
+ let inHole = false;
46066
+ for (let h = 1; h < rings.length; h++) {
46067
+ if (pointInRing(lon, lat, rings[h]) && !pointOnRingEdge(lon, lat, rings[h])) {
46068
+ inHole = true;
46069
+ break;
46070
+ }
46071
+ }
46072
+ if (!inHole) return true;
46073
+ }
46074
+ return false;
46075
+ }
46076
+ function regionAt(lonLat, countries, states) {
46077
+ const lon = lonLat[0];
46078
+ const lat = lonLat[1];
46079
+ let country = null;
46080
+ for (const f of countries) {
46081
+ if (pointInGeometry(f.geometry, lon, lat)) {
46082
+ country = { iso: f.id, name: f.properties.name };
46083
+ break;
46084
+ }
46085
+ }
46086
+ let state = null;
46087
+ if (country?.iso === "US" && states) {
46088
+ for (const f of states) {
46089
+ if (pointInGeometry(f.geometry, lon, lat)) {
46090
+ state = { iso: f.id, name: f.properties.name };
46091
+ break;
46092
+ }
46093
+ }
46094
+ }
46095
+ return { country, state };
46096
+ }
45589
46097
  function featureBbox(topo, geomId) {
45590
46098
  const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45591
46099
  if (!geom) return null;
@@ -45703,11 +46211,13 @@ function unionLongitudes(lons) {
45703
46211
  }
45704
46212
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45705
46213
  }
45706
- var fold, DETACH_GAP_DEG, DETACH_AREA_FRAC;
46214
+ var fold, adjacencyCache, EDGE_EPS, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45707
46215
  var init_geo = __esm({
45708
46216
  "src/map/geo.ts"() {
45709
46217
  "use strict";
45710
46218
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
46219
+ adjacencyCache = /* @__PURE__ */ new WeakMap();
46220
+ EDGE_EPS = 1e-9;
45711
46221
  DETACH_GAP_DEG = 10;
45712
46222
  DETACH_AREA_FRAC = 0.25;
45713
46223
  }
@@ -45727,6 +46237,12 @@ function looksUS(lat, lon) {
45727
46237
  if (lat < 15 || lat > 72) return false;
45728
46238
  return lon >= -180 && lon <= -64 || lon >= 172;
45729
46239
  }
46240
+ function looksNorthAmericaNeighbor(lat, lon) {
46241
+ return lat >= 14 && lat <= 72 && lon >= -141 && lon <= -52;
46242
+ }
46243
+ function isWholeSphere(bb) {
46244
+ return bb[0][0] <= -179 && bb[1][0] >= 179 && bb[0][1] <= -89 && bb[1][1] >= 89;
46245
+ }
45730
46246
  function resolveMap(parsed, data) {
45731
46247
  const diagnostics = [...parsed.diagnostics];
45732
46248
  const err = (line12, message, code) => {
@@ -45737,9 +46253,6 @@ function resolveMap(parsed, data) {
45737
46253
  };
45738
46254
  const result = {
45739
46255
  title: parsed.title,
45740
- ...parsed.directives.subtitle !== void 0 && {
45741
- subtitle: parsed.directives.subtitle
45742
- },
45743
46256
  ...parsed.directives.caption !== void 0 && {
45744
46257
  caption: parsed.directives.caption
45745
46258
  },
@@ -45749,7 +46262,7 @@ function resolveMap(parsed, data) {
45749
46262
  // renderer's job (step 4) — the resolver only carries `tags` + `tagGroups`
45750
46263
  // through; it never resolves a tag value to a palette color (#10).
45751
46264
  directives: { ...parsed.directives },
45752
- basemaps: { world: "coarse", subdivisions: [] },
46265
+ basemaps: { world: "detail", subdivisions: [] },
45753
46266
  regions: [],
45754
46267
  pois: [],
45755
46268
  edges: [],
@@ -45758,7 +46271,8 @@ function resolveMap(parsed, data) {
45758
46271
  [-180, -85],
45759
46272
  [180, 85]
45760
46273
  ],
45761
- projection: "natural-earth",
46274
+ projection: "equirectangular",
46275
+ poiFrameContainers: [],
45762
46276
  diagnostics,
45763
46277
  error: parsed.error
45764
46278
  };
@@ -45768,7 +46282,10 @@ function resolveMap(parsed, data) {
45768
46282
  ...[...countryIndex.values()].map((v) => v.name),
45769
46283
  ...[...usStateIndex.values()].map((v) => v.name)
45770
46284
  ];
45771
- const usScoped = parsed.directives.region === "us-states" || parsed.directives.defaultCountry?.toUpperCase() === "US" || parsed.regions.some((r) => {
46285
+ const localeRaw = parsed.directives.locale?.toUpperCase();
46286
+ const localeCountry = localeRaw ? localeRaw.split("-")[0] : void 0;
46287
+ const localeSubdivision = localeRaw && /^[A-Z]{2}-/.test(localeRaw) ? localeRaw : void 0;
46288
+ const usScoped = localeCountry === "US" || parsed.regions.some((r) => {
45772
46289
  const f = fold(r.name);
45773
46290
  return usStateIndex.has(f) && !countryIndex.has(f);
45774
46291
  }) || parsed.regions.some(
@@ -45919,7 +46436,7 @@ function resolveMap(parsed, data) {
45919
46436
  if (!scope)
45920
46437
  warn(
45921
46438
  line12,
45922
- `"${name}" is ambiguous \u2014 resolved to the most-populous match.`,
46439
+ `"${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.`,
45923
46440
  "W_MAP_AMBIGUOUS_NAME"
45924
46441
  );
45925
46442
  }
@@ -45932,17 +46449,21 @@ function resolveMap(parsed, data) {
45932
46449
  return fold(pos.name);
45933
46450
  };
45934
46451
  const poiCountries = [];
45935
- let anyNonUsPoi = false;
46452
+ let anyUsPoi = false;
46453
+ let anyNonNaPoi = false;
45936
46454
  const noteCountry = (iso) => {
45937
46455
  if (iso) {
45938
46456
  poiCountries.push(iso);
45939
- if (iso !== "US") anyNonUsPoi = true;
46457
+ if (iso === "US") anyUsPoi = true;
46458
+ if (iso !== "US" && iso !== "CA" && iso !== "MX") anyNonNaPoi = true;
45940
46459
  }
45941
46460
  };
45942
46461
  const deferred = [];
45943
46462
  for (const p of parsed.pois) {
45944
46463
  if (p.pos.kind === "coords") {
45945
- if (!looksUS(p.pos.lat, p.pos.lon)) anyNonUsPoi = true;
46464
+ if (looksUS(p.pos.lat, p.pos.lon)) anyUsPoi = true;
46465
+ else if (!looksNorthAmericaNeighbor(p.pos.lat, p.pos.lon))
46466
+ anyNonNaPoi = true;
45946
46467
  addResolvedPoi(p.pos.lat, p.pos.lon, p);
45947
46468
  continue;
45948
46469
  }
@@ -45960,14 +46481,15 @@ function resolveMap(parsed, data) {
45960
46481
  deferred.push(p);
45961
46482
  }
45962
46483
  }
45963
- const inferredCountry = parsed.directives.defaultCountry?.toUpperCase() ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46484
+ const inferredCountry = localeCountry ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46485
+ const inferredScope = localeSubdivision ?? inferredCountry;
45964
46486
  for (const p of deferred) {
45965
46487
  if (p.pos.kind !== "name") continue;
45966
46488
  const got = lookupName(
45967
46489
  p.pos.name,
45968
46490
  p.pos.scope,
45969
46491
  p.lineNumber,
45970
- inferredCountry,
46492
+ inferredScope,
45971
46493
  true
45972
46494
  );
45973
46495
  if (got.kind === "ok") {
@@ -46037,7 +46559,8 @@ function resolveMap(parsed, data) {
46037
46559
  const meta = sizeValue !== void 0 ? { value: sizeValue } : {};
46038
46560
  if (pos.kind === "coords") {
46039
46561
  const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
46040
- if (!looksUS(pos.lat, pos.lon)) anyNonUsPoi = true;
46562
+ if (looksUS(pos.lat, pos.lon)) anyUsPoi = true;
46563
+ else if (!looksNorthAmericaNeighbor(pos.lat, pos.lon)) anyNonNaPoi = true;
46041
46564
  if (!registry.has(id)) {
46042
46565
  registerPoi(
46043
46566
  id,
@@ -46060,7 +46583,7 @@ function resolveMap(parsed, data) {
46060
46583
  if (registry.has(f)) return f;
46061
46584
  const aliased = declaredByName.get(f);
46062
46585
  if (aliased) return aliased;
46063
- const got = lookupName(pos.name, pos.scope, line12, inferredCountry, true);
46586
+ const got = lookupName(pos.name, pos.scope, line12, inferredScope, true);
46064
46587
  if (got.kind !== "ok") return null;
46065
46588
  noteCountry(got.iso);
46066
46589
  registerPoi(
@@ -46117,9 +46640,12 @@ function resolveMap(parsed, data) {
46117
46640
  }
46118
46641
  routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
46119
46642
  }
46643
+ const hasUsContent = usSubdivisionReferenced || anyUsPoi || localeCountry === "US";
46644
+ const usOriented = !anyNonNaPoi && !regions.some(
46645
+ (r) => r.layer === "country" && !["US", "CA", "MX"].includes(r.iso)
46646
+ ) && hasUsContent;
46120
46647
  const subdivisions = [];
46121
- if (usSubdivisionReferenced || parsed.directives.region === "us-states")
46122
- subdivisions.push("us-states");
46648
+ if (usSubdivisionReferenced || usOriented) subdivisions.push("us-states");
46123
46649
  const regionBoxes = [];
46124
46650
  for (const ref of referencedRegionIds) {
46125
46651
  const bb = featureBbox(data.usStates, ref.id);
@@ -46137,17 +46663,51 @@ function resolveMap(parsed, data) {
46137
46663
  [-180, -85],
46138
46664
  [180, 85]
46139
46665
  ];
46140
- let extent2 = unioned ? pad(unioned, PAD_FRACTION) : DEFAULT_EXTENT;
46666
+ const basePad = regions.length > 0 ? REGION_PAD_FRACTION : PAD_FRACTION;
46667
+ let extent2 = unioned ? pad(unioned, basePad) : DEFAULT_EXTENT;
46668
+ const isPoiOnly = pois.length > 0 && regions.length === 0;
46669
+ const containerRegionIds = [];
46670
+ if (isPoiOnly) {
46671
+ const countries = decodeFeatures(data.worldDetail);
46672
+ const states = decodeFeatures(data.usStates);
46673
+ const seen = /* @__PURE__ */ new Set();
46674
+ const containerBoxes = [];
46675
+ for (const p of pois) {
46676
+ const { country, state } = regionAt([p.lon, p.lat], countries, states);
46677
+ const id = state?.iso ?? country?.iso;
46678
+ if (!id || seen.has(id)) continue;
46679
+ seen.add(id);
46680
+ containerRegionIds.push(id);
46681
+ const bb = state ? featureBbox(data.usStates, id) : featureBboxPrimary(data.worldCoarse, id);
46682
+ if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46683
+ }
46684
+ const containerUnion = unionExtent(containerBoxes, points);
46685
+ if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46686
+ }
46687
+ if (isPoiOnly) {
46688
+ const cx = (extent2[0][0] + extent2[1][0]) / 2;
46689
+ const cy = (extent2[0][1] + extent2[1][1]) / 2;
46690
+ const lon = extent2[1][0] - extent2[0][0];
46691
+ const lat = extent2[1][1] - extent2[0][1];
46692
+ const longer = Math.max(lon, lat);
46693
+ if (longer > 0 && longer < POI_ZOOM_FLOOR_DEG) {
46694
+ const k = POI_ZOOM_FLOOR_DEG / longer;
46695
+ const halfLon = lon * k / 2;
46696
+ const halfLat = lat * k / 2;
46697
+ extent2 = [
46698
+ [cx - halfLon, cy - halfLat],
46699
+ [cx + halfLon, cy + halfLat]
46700
+ ];
46701
+ }
46702
+ }
46141
46703
  const lonSpan = extent2[1][0] - extent2[0][0];
46142
46704
  const latSpan = extent2[1][1] - extent2[0][1];
46143
46705
  const span = Math.max(lonSpan, latSpan);
46144
46706
  const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
46145
- const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46146
46707
  let projection;
46147
- const override = parsed.directives.projection;
46148
- if (override === "equirectangular" || override === "natural-earth" || override === "albers-usa" || override === "mercator") {
46149
- projection = override;
46150
- } else if (usDominant) {
46708
+ if (isPoiOnly && usOriented && lonSpan < US_NATIONAL_LON_SPAN) {
46709
+ projection = "mercator";
46710
+ } else if (usOriented) {
46151
46711
  projection = "albers-usa";
46152
46712
  } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
46153
46713
  projection = "equirectangular";
@@ -46165,11 +46725,20 @@ function resolveMap(parsed, data) {
46165
46725
  result.edges = edges;
46166
46726
  result.routes = routes;
46167
46727
  result.basemaps = {
46168
- world: span > WORLD_SPAN ? "coarse" : "detail",
46728
+ // Tier is intentionally pinned to detail (50m) at ALL scales. Diagrammo maps
46729
+ // are presentational (palette tints, relief hachures, POI hubs), not
46730
+ // survey-grade — recognizability > generalization: 110m coarse drops the
46731
+ // Italian boot to a stump at world scale. `WORLD_SPAN` lives on only for the
46732
+ // projection decision (the `usOriented`/`span > WORLD_SPAN` chain above); it
46733
+ // no longer gates basemap resolution.
46734
+ // `worldCoarse` is still loaded — it's the authoritative name/bbox index
46735
+ // (featureIndex, featureBboxPrimary), not dead code.
46736
+ world: "detail",
46169
46737
  subdivisions
46170
46738
  };
46171
46739
  result.extent = extent2;
46172
46740
  result.projection = projection;
46741
+ result.poiFrameContainers = containerRegionIds;
46173
46742
  result.error = parsed.error ?? firstError(diagnostics);
46174
46743
  return result;
46175
46744
  }
@@ -46206,7 +46775,7 @@ function firstError(diags) {
46206
46775
  const e = diags.find((d) => d.severity === "error");
46207
46776
  return e ? formatDgmoError(e) : null;
46208
46777
  }
46209
- var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46778
+ 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;
46210
46779
  var init_resolver2 = __esm({
46211
46780
  "src/map/resolver.ts"() {
46212
46781
  "use strict";
@@ -46215,8 +46784,11 @@ var init_resolver2 = __esm({
46215
46784
  WORLD_SPAN = 90;
46216
46785
  MERCATOR_MAX_LAT = 80;
46217
46786
  PAD_FRACTION = 0.05;
46787
+ REGION_PAD_FRACTION = 0.12;
46218
46788
  WORLD_LAT_SOUTH = -58;
46219
46789
  WORLD_LAT_NORTH = 78;
46790
+ POI_ZOOM_FLOOR_DEG = 7;
46791
+ US_NATIONAL_LON_SPAN = 48;
46220
46792
  REGION_ALIASES = {
46221
46793
  // Common everyday names → the Natural-Earth display name actually shipped.
46222
46794
  "united states": "united states of america",
@@ -46294,10 +46866,277 @@ var init_resolver2 = __esm({
46294
46866
  }
46295
46867
  });
46296
46868
 
46869
+ // src/map/colorize.ts
46870
+ function assignColors(isos, adjacency) {
46871
+ const sorted = [...isos].sort();
46872
+ const byIso = /* @__PURE__ */ new Map();
46873
+ let maxIndex = -1;
46874
+ for (const iso of sorted) {
46875
+ const taken = /* @__PURE__ */ new Set();
46876
+ for (const n of adjacency.get(iso) ?? []) {
46877
+ const c = byIso.get(n);
46878
+ if (c !== void 0) taken.add(c);
46879
+ }
46880
+ let h = 0;
46881
+ while (taken.has(h)) h++;
46882
+ byIso.set(iso, h);
46883
+ if (h > maxIndex) maxIndex = h;
46884
+ }
46885
+ return { byIso, huesNeeded: maxIndex + 1 };
46886
+ }
46887
+ var init_colorize = __esm({
46888
+ "src/map/colorize.ts"() {
46889
+ "use strict";
46890
+ }
46891
+ });
46892
+
46893
+ // src/map/context-labels.ts
46894
+ function tierBand(maxSpanDeg) {
46895
+ if (maxSpanDeg >= 90) return "world";
46896
+ if (maxSpanDeg >= 20) return "continental";
46897
+ if (maxSpanDeg >= 5) return "regional";
46898
+ return "local";
46899
+ }
46900
+ function labelBudget(width, height, band) {
46901
+ const bandCap = {
46902
+ world: 6,
46903
+ continental: 5,
46904
+ regional: 4,
46905
+ local: 3
46906
+ };
46907
+ const area2 = Math.floor(Math.sqrt(Math.max(0, width * height)) / 150);
46908
+ return Math.max(0, Math.min(area2, bandCap[band]));
46909
+ }
46910
+ function waterEligible(tier, kind, band) {
46911
+ switch (band) {
46912
+ case "world":
46913
+ return tier <= 1 && (kind === "ocean" || kind === "sea");
46914
+ case "continental":
46915
+ return tier <= 2;
46916
+ case "regional":
46917
+ return tier <= 3;
46918
+ case "local":
46919
+ return tier <= 4;
46920
+ }
46921
+ }
46922
+ function insideViewport(p, width, height) {
46923
+ return !!p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && p[0] >= 0 && p[0] <= width && p[1] >= 0 && p[1] <= height;
46924
+ }
46925
+ function labelWidth(text, letterSpacing) {
46926
+ const spacing = letterSpacing > 0 ? Math.max(0, text.length - 1) * letterSpacing : 0;
46927
+ return measureLegendText(text, FONT) + spacing + 2 * PADX;
46928
+ }
46929
+ function wrapLabel2(text, letterSpacing) {
46930
+ const words = text.split(/\s+/).filter(Boolean);
46931
+ if (words.length <= 1) return [text];
46932
+ const maxLines = words.length >= 4 ? 3 : 2;
46933
+ const n = words.length;
46934
+ let best = null;
46935
+ for (let mask = 0; mask < 1 << n - 1; mask++) {
46936
+ const lines = [];
46937
+ let cur = [words[0]];
46938
+ for (let i = 1; i < n; i++) {
46939
+ if (mask & 1 << i - 1) {
46940
+ lines.push(cur.join(" "));
46941
+ cur = [words[i]];
46942
+ } else cur.push(words[i]);
46943
+ }
46944
+ lines.push(cur.join(" "));
46945
+ if (lines.length > maxLines) continue;
46946
+ const cost = Math.round(
46947
+ Math.max(...lines.map((l) => labelWidth(l, letterSpacing)))
46948
+ );
46949
+ const head = labelWidth(lines[0], letterSpacing);
46950
+ if (!best || cost < best.cost || cost === best.cost && lines.length < best.lines.length || cost === best.cost && lines.length === best.lines.length && head > best.head)
46951
+ best = { lines, cost, head };
46952
+ }
46953
+ return best?.lines ?? [text];
46954
+ }
46955
+ function rectAround(cx, cy, lines, letterSpacing) {
46956
+ const w = Math.max(...lines.map((l) => labelWidth(l, letterSpacing)));
46957
+ const h = (lines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY;
46958
+ return { x: cx - w / 2, y: cy - h / 2, w, h };
46959
+ }
46960
+ function rectFits(r, width, height) {
46961
+ return r.x >= 0 && r.y >= 0 && r.x + r.w <= width && r.y + r.h <= height;
46962
+ }
46963
+ function overlapsPadded(a, b, pad2) {
46964
+ 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;
46965
+ }
46966
+ function placeContextLabels(args) {
46967
+ const {
46968
+ projection,
46969
+ dLonSpan,
46970
+ dLatSpan,
46971
+ width,
46972
+ height,
46973
+ waterBodies,
46974
+ countries,
46975
+ palette,
46976
+ project,
46977
+ collides,
46978
+ overLand
46979
+ } = args;
46980
+ void projection;
46981
+ const band = tierBand(Math.max(dLonSpan, dLatSpan));
46982
+ const budget = labelBudget(width, height, band);
46983
+ if (budget <= 0) return [];
46984
+ const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
46985
+ const countryColor = palette.textMuted;
46986
+ const haloColor = palette.bg;
46987
+ const candidates = [];
46988
+ const center = [width / 2, height / 2];
46989
+ for (const e of waterBodies?.entries ?? []) {
46990
+ const [lat, lon, name, tier, kind, alt] = e;
46991
+ if (!waterEligible(tier, kind, band)) continue;
46992
+ const wlines = wrapLabel2(name, WATER_LETTER_SPACING);
46993
+ const anchorsLngLat = [[lon, lat]];
46994
+ for (const a of alt ?? []) anchorsLngLat.push([a[1], a[0]]);
46995
+ let best = null;
46996
+ let bestD = Infinity;
46997
+ let nearestProj = null;
46998
+ let nearestProjD = Infinity;
46999
+ for (const [aLon, aLat] of anchorsLngLat) {
47000
+ const p = project(aLon, aLat);
47001
+ if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
47002
+ const d = (p[0] - center[0]) ** 2 + (p[1] - center[1]) ** 2;
47003
+ if (d < nearestProjD) {
47004
+ nearestProjD = d;
47005
+ nearestProj = p;
47006
+ }
47007
+ if (!insideViewport(p, width, height)) continue;
47008
+ if (d < bestD) {
47009
+ bestD = d;
47010
+ best = p;
47011
+ }
47012
+ }
47013
+ if (!best && tier === 0 && nearestProj) {
47014
+ const overX = Math.max(0, -nearestProj[0], nearestProj[0] - width);
47015
+ const overY = Math.max(0, -nearestProj[1], nearestProj[1] - height);
47016
+ if (overX <= width * EDGE_CLAMP_OVERSHOOT && overY <= height * EDGE_CLAMP_OVERSHOOT) {
47017
+ const halfW = Math.max(...wlines.map((l) => labelWidth(l, WATER_LETTER_SPACING))) / 2;
47018
+ const halfH = ((wlines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY) / 2;
47019
+ const m = EDGE_CLAMP_MARGIN;
47020
+ best = [
47021
+ Math.min(Math.max(nearestProj[0], halfW + m), width - halfW - m),
47022
+ Math.min(Math.max(nearestProj[1], halfH + m), height - halfH - m)
47023
+ ];
47024
+ }
47025
+ }
47026
+ if (!best) continue;
47027
+ candidates.push({
47028
+ text: name,
47029
+ lines: wlines,
47030
+ cx: best[0],
47031
+ cy: best[1],
47032
+ italic: true,
47033
+ letterSpacing: WATER_LETTER_SPACING,
47034
+ color: waterColor,
47035
+ // Water before any country (×1000), then by tier, then kind, then name.
47036
+ sort: tier * 10 + KIND_ORDER[kind]
47037
+ });
47038
+ }
47039
+ const ranked = countries.map((c) => {
47040
+ const [x0, y0, x1, y1] = c.bbox;
47041
+ const w = x1 - x0;
47042
+ const h = y1 - y0;
47043
+ return { c, w, h, area: w * h };
47044
+ }).filter((r) => Number.isFinite(r.area) && r.area > 0).sort((a, b) => b.area - a.area);
47045
+ let ci = 0;
47046
+ for (const r of ranked) {
47047
+ const { c, w, h } = r;
47048
+ if (w > width * 0.66 || h > height * 0.66) continue;
47049
+ if (!insideViewport(c.anchor, width, height)) continue;
47050
+ const text = c.name;
47051
+ const tw = labelWidth(text, 0);
47052
+ if (tw > w || FONT + 2 * PADY > h) continue;
47053
+ candidates.push({
47054
+ text,
47055
+ lines: [text],
47056
+ cx: c.anchor[0],
47057
+ cy: c.anchor[1],
47058
+ italic: false,
47059
+ letterSpacing: 0,
47060
+ color: countryColor,
47061
+ // Always after every water body (+1e6); larger area = earlier.
47062
+ sort: 1e6 + ci++
47063
+ });
47064
+ }
47065
+ candidates.sort((a, b) => a.sort - b.sort);
47066
+ const placed = [];
47067
+ const placedRects = [];
47068
+ for (const cand of candidates) {
47069
+ if (placed.length >= budget) break;
47070
+ const rect = rectAround(cand.cx, cand.cy, cand.lines, cand.letterSpacing);
47071
+ if (!rectFits(rect, width, height)) continue;
47072
+ if (cand.italic && overLand) {
47073
+ const inset = 2;
47074
+ const top = cand.cy - (cand.lines.length - 1) / 2 * LINE_HEIGHT;
47075
+ const touchesLand = cand.lines.some((line12, li) => {
47076
+ const lw = labelWidth(line12, cand.letterSpacing);
47077
+ const x0 = cand.cx - lw / 2 + inset;
47078
+ const x1 = cand.cx + lw / 2 - inset;
47079
+ const xs = [x0, (x0 + cand.cx) / 2, cand.cx, (cand.cx + x1) / 2, x1];
47080
+ const base = top + li * LINE_HEIGHT;
47081
+ return [base, base - FONT * 0.4, base - FONT * 0.8].some(
47082
+ (y) => xs.some((x) => overLand(x, y))
47083
+ );
47084
+ });
47085
+ if (touchesLand) continue;
47086
+ }
47087
+ if (collides(rect)) continue;
47088
+ if (placedRects.some((r) => overlapsPadded(rect, r, CONTEXT_PAD))) continue;
47089
+ placedRects.push(rect);
47090
+ placed.push({
47091
+ x: cand.cx,
47092
+ y: cand.cy,
47093
+ text: cand.text,
47094
+ anchor: "middle",
47095
+ color: cand.color,
47096
+ // No halo: the bg-coloured outline reads as a ghost box behind the text
47097
+ // over the tinted water/land. Context labels are muted enough to sit
47098
+ // cleanly on the basemap without one.
47099
+ halo: false,
47100
+ haloColor,
47101
+ italic: cand.italic,
47102
+ letterSpacing: cand.letterSpacing,
47103
+ ...cand.lines.length > 1 ? { lines: cand.lines } : {},
47104
+ lineNumber: 0
47105
+ });
47106
+ }
47107
+ return placed;
47108
+ }
47109
+ var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, KIND_ORDER;
47110
+ var init_context_labels = __esm({
47111
+ "src/map/context-labels.ts"() {
47112
+ "use strict";
47113
+ init_color_utils();
47114
+ init_legend_constants();
47115
+ FONT = 11;
47116
+ LINE_HEIGHT = FONT + 2;
47117
+ PADX = 4;
47118
+ PADY = 3;
47119
+ WATER_LETTER_SPACING = 1.5;
47120
+ CONTEXT_PAD = 4;
47121
+ EDGE_CLAMP_MARGIN = 8;
47122
+ EDGE_CLAMP_OVERSHOOT = 0.35;
47123
+ KIND_ORDER = {
47124
+ ocean: 0,
47125
+ sea: 1,
47126
+ gulf: 2,
47127
+ bay: 3,
47128
+ strait: 4,
47129
+ channel: 5,
47130
+ sound: 6
47131
+ };
47132
+ }
47133
+ });
47134
+
46297
47135
  // src/map/layout.ts
46298
47136
  import {
46299
47137
  geoPath,
46300
47138
  geoNaturalEarth1,
47139
+ geoEqualEarth,
46301
47140
  geoEquirectangular,
46302
47141
  geoConicEqualArea,
46303
47142
  geoMercator,
@@ -46309,12 +47148,34 @@ function geomObject2(topo) {
46309
47148
  const key = Object.keys(topo.objects)[0];
46310
47149
  return topo.objects[key];
46311
47150
  }
47151
+ function mergeFeatures(a, b) {
47152
+ const polysOf = (f) => {
47153
+ const g = f.geometry;
47154
+ if (!g) return null;
47155
+ if (g.type === "Polygon") return [g.coordinates];
47156
+ if (g.type === "MultiPolygon") return g.coordinates;
47157
+ return null;
47158
+ };
47159
+ const pa = polysOf(a);
47160
+ const pb = polysOf(b);
47161
+ if (!pa || !pb) return a;
47162
+ return {
47163
+ ...a,
47164
+ geometry: { type: "MultiPolygon", coordinates: [...pa, ...pb] }
47165
+ };
47166
+ }
46312
47167
  function decodeLayer(topo) {
47168
+ const cached = decodeCache.get(topo);
47169
+ if (cached) return cached;
46313
47170
  const out = /* @__PURE__ */ new Map();
46314
47171
  for (const g of geomObject2(topo).geometries) {
46315
47172
  const f = feature2(topo, g);
46316
- out.set(g.id, { ...f, id: g.id });
47173
+ if (!f.geometry) continue;
47174
+ const tagged = { ...f, id: g.id };
47175
+ const existing = out.get(g.id);
47176
+ out.set(g.id, existing ? mergeFeatures(existing, tagged) : tagged);
46317
47177
  }
47178
+ decodeCache.set(topo, out);
46318
47179
  return out;
46319
47180
  }
46320
47181
  function projectionFor(family) {
@@ -46323,9 +47184,12 @@ function projectionFor(family) {
46323
47184
  return usConusProjection();
46324
47185
  case "mercator":
46325
47186
  return geoMercator();
47187
+ case "equal-earth":
47188
+ return geoEqualEarth();
47189
+ case "equirectangular":
47190
+ return geoEquirectangular();
46326
47191
  case "natural-earth":
46327
47192
  return geoNaturalEarth1();
46328
- case "equirectangular":
46329
47193
  default:
46330
47194
  return geoEquirectangular();
46331
47195
  }
@@ -46344,13 +47208,11 @@ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46344
47208
  isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT
46345
47209
  );
46346
47210
  }
46347
- function layoutMap(resolved, data, size, opts) {
46348
- const { palette, isDark } = opts;
46349
- const { width, height } = size;
47211
+ function buildMapProjection(resolved, data) {
46350
47212
  const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
46351
- const usCrisp = resolved.projection === "albers-usa" && wantsUsStates && !!data.naLand;
47213
+ const usCrisp = (resolved.projection === "albers-usa" || resolved.projection === "mercator") && wantsUsStates && !!data.naLand;
46352
47214
  const worldTopo = usCrisp ? data.worldDetail : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46353
- const worldLayer = decodeLayer(worldTopo);
47215
+ const worldLayer = new Map(decodeLayer(worldTopo));
46354
47216
  if (usCrisp && data.naLand) {
46355
47217
  const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
46356
47218
  const crisp = decodeLayer(data.naLand);
@@ -46359,16 +47221,109 @@ function layoutMap(resolved, data, size, opts) {
46359
47221
  if (!base) continue;
46360
47222
  const [[bw, bs], [be, bn]] = geoBounds2(base);
46361
47223
  if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
46362
- worldLayer.set(iso, cf);
47224
+ worldLayer.set(iso, { ...cf, properties: base.properties });
46363
47225
  }
46364
47226
  }
46365
47227
  const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
47228
+ const extentOutline = () => {
47229
+ const [[w, s], [e, n]] = resolved.extent;
47230
+ const N = 16;
47231
+ const coords = [];
47232
+ for (let i = 0; i <= N; i++) {
47233
+ const t = i / N;
47234
+ const lon = w + (e - w) * t;
47235
+ const lat = s + (n - s) * t;
47236
+ coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
47237
+ }
47238
+ return {
47239
+ type: "Feature",
47240
+ properties: {},
47241
+ geometry: { type: "MultiPoint", coordinates: coords }
47242
+ };
47243
+ };
47244
+ let fitFeatures;
47245
+ if (resolved.projection === "albers-usa" && usLayer) {
47246
+ fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
47247
+ const neighborPoints = resolved.pois.filter((p) => !inAlaska(p.lon, p.lat) && !inHawaii(p.lon, p.lat)).map((p) => [p.lon, p.lat]);
47248
+ if (neighborPoints.length > 0) {
47249
+ fitFeatures.push({
47250
+ type: "Feature",
47251
+ properties: {},
47252
+ geometry: { type: "MultiPoint", coordinates: neighborPoints }
47253
+ });
47254
+ }
47255
+ for (const r of resolved.regions) {
47256
+ if (r.layer === "country" && (r.iso === "CA" || r.iso === "MX")) {
47257
+ const cf = worldLayer.get(r.iso);
47258
+ if (cf) fitFeatures.push(cf);
47259
+ }
47260
+ }
47261
+ } else {
47262
+ fitFeatures = [extentOutline()];
47263
+ }
47264
+ const fitTarget = { type: "FeatureCollection", features: fitFeatures };
47265
+ const projection = projectionFor(resolved.projection);
47266
+ if (resolved.projection !== "albers-usa") {
47267
+ let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
47268
+ if (centerLon > 180) centerLon -= 360;
47269
+ projection.rotate([-centerLon, 0]);
47270
+ }
47271
+ const fitGB = geoBounds2(fitTarget);
47272
+ const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
47273
+ return {
47274
+ projection,
47275
+ fitTarget,
47276
+ fitIsGlobal,
47277
+ worldLayer,
47278
+ usLayer,
47279
+ usCrisp,
47280
+ wantsUsStates,
47281
+ worldTopo
47282
+ };
47283
+ }
47284
+ function parsePathRings(d) {
47285
+ const rings = [];
47286
+ let cur = [];
47287
+ const re = /([MLZ])([^MLZ]*)/g;
47288
+ let m;
47289
+ while (m = re.exec(d)) {
47290
+ if (m[1] === "Z") {
47291
+ if (cur.length) rings.push(cur);
47292
+ cur = [];
47293
+ continue;
47294
+ }
47295
+ if (m[1] === "M" && cur.length) {
47296
+ rings.push(cur);
47297
+ cur = [];
47298
+ }
47299
+ const nums = m[2].split(/[ ,]+/).map(Number);
47300
+ for (let i = 0; i + 1 < nums.length; i += 2) {
47301
+ const x = nums[i];
47302
+ const y = nums[i + 1];
47303
+ if (Number.isFinite(x) && Number.isFinite(y)) cur.push([x, y]);
47304
+ }
47305
+ }
47306
+ if (cur.length) rings.push(cur);
47307
+ return rings;
47308
+ }
47309
+ function layoutMap(resolved, data, size, opts) {
47310
+ const { palette, isDark } = opts;
47311
+ const { width, height } = size;
47312
+ const {
47313
+ projection,
47314
+ fitTarget,
47315
+ fitIsGlobal,
47316
+ worldLayer,
47317
+ usLayer,
47318
+ usCrisp,
47319
+ worldTopo
47320
+ } = buildMapProjection(resolved, data);
46366
47321
  const usContext = usLayer !== null;
46367
47322
  const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46368
47323
  const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
46369
- const scaleOverride = resolved.directives.scale;
46370
- const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46371
- const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
47324
+ const allNonNegative = values.length > 0 && values.every((v) => v >= 0);
47325
+ const rampMin = allNonNegative ? 0 : Math.min(...values);
47326
+ const rampMax = Math.max(...values);
46372
47327
  const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46373
47328
  const hasRamp = values.length > 0;
46374
47329
  const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
@@ -46389,7 +47344,7 @@ function layoutMap(resolved, data, size, opts) {
46389
47344
  activeGroup = VALUE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46390
47345
  }
46391
47346
  const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
46392
- const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
47347
+ const mutedBasemap = activeGroup !== null;
46393
47348
  const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46394
47349
  const water = mapBackgroundColor(palette, isDark, mutedBasemap);
46395
47350
  const lakeStroke = mix(regionStroke, water, 45);
@@ -46398,6 +47353,39 @@ function layoutMap(resolved, data, size, opts) {
46398
47353
  palette.bg,
46399
47354
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46400
47355
  );
47356
+ const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && resolved.tagGroups.length === 0;
47357
+ const colorByIso = /* @__PURE__ */ new Map();
47358
+ if (colorizeActive) {
47359
+ const adjacency = /* @__PURE__ */ new Map();
47360
+ const addEdges = (src) => {
47361
+ for (const [iso, ns] of src) {
47362
+ const cur = adjacency.get(iso);
47363
+ if (cur) cur.push(...ns);
47364
+ else adjacency.set(iso, [...ns]);
47365
+ }
47366
+ };
47367
+ addEdges(buildAdjacency(worldTopo));
47368
+ if (usLayer) {
47369
+ addEdges(buildAdjacency(data.usStates));
47370
+ for (const [country, states] of Object.entries(FOREIGN_BORDER)) {
47371
+ const cn = adjacency.get(country);
47372
+ if (!cn) continue;
47373
+ for (const st of states) {
47374
+ const sn = adjacency.get(st);
47375
+ if (!sn) continue;
47376
+ cn.push(st);
47377
+ sn.push(country);
47378
+ }
47379
+ }
47380
+ }
47381
+ const { byIso, huesNeeded } = assignColors(
47382
+ [...adjacency.keys()],
47383
+ adjacency
47384
+ );
47385
+ const tints = politicalTints(palette, huesNeeded, isDark);
47386
+ for (const [iso, idx] of byIso) colorByIso.set(iso, tints[idx]);
47387
+ }
47388
+ const colorizeStroke = (fill2) => mix(fill2, palette.text, 35);
46401
47389
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46402
47390
  const fillForValue = (s) => {
46403
47391
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
@@ -46433,43 +47421,15 @@ function layoutMap(resolved, data, size, opts) {
46433
47421
  if (activeIsScore) {
46434
47422
  return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46435
47423
  }
47424
+ if (colorizeActive) return (r.iso && colorByIso.get(r.iso)) ?? neutralFill;
46436
47425
  return tagFill(r.tags, activeGroup) ?? neutralFill;
46437
47426
  };
46438
47427
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
46439
- const extentOutline = () => {
46440
- const [[w, s], [e, n]] = resolved.extent;
46441
- const N = 16;
46442
- const coords = [];
46443
- for (let i = 0; i <= N; i++) {
46444
- const t = i / N;
46445
- const lon = w + (e - w) * t;
46446
- const lat = s + (n - s) * t;
46447
- coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
46448
- }
46449
- return {
46450
- type: "Feature",
46451
- properties: {},
46452
- geometry: { type: "MultiPoint", coordinates: coords }
46453
- };
46454
- };
46455
- let fitFeatures;
46456
- if (resolved.projection === "albers-usa" && usLayer) {
46457
- fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
46458
- } else {
46459
- fitFeatures = [extentOutline()];
46460
- }
46461
- const fitTarget = { type: "FeatureCollection", features: fitFeatures };
46462
- const projection = projectionFor(resolved.projection);
46463
- if (resolved.projection !== "albers-usa") {
46464
- let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
46465
- if (centerLon > 180) centerLon -= 360;
46466
- projection.rotate([-centerLon, 0]);
46467
- }
46468
- const TITLE_GAP = 16;
47428
+ const TITLE_GAP2 = 16;
46469
47429
  let topPad = FIT_PAD;
46470
47430
  if (resolved.title && resolved.pois.length > 0) {
46471
47431
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
46472
- topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
47432
+ topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
46473
47433
  }
46474
47434
  const fitBox = [
46475
47435
  [FIT_PAD, topPad],
@@ -46479,12 +47439,10 @@ function layoutMap(resolved, data, size, opts) {
46479
47439
  ]
46480
47440
  ];
46481
47441
  projection.fitExtent(fitBox, fitTarget);
46482
- const fitGB = geoBounds2(fitTarget);
46483
- const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46484
47442
  let path;
46485
47443
  let project;
46486
47444
  let stretchParams = null;
46487
- if (fitIsGlobal) {
47445
+ if (fitIsGlobal && !opts.preferContain) {
46488
47446
  const cb = geoPath(projection).bounds(fitTarget);
46489
47447
  const bx0 = cb[0][0];
46490
47448
  const by0 = cb[0][1];
@@ -46526,7 +47484,9 @@ function layoutMap(resolved, data, size, opts) {
46526
47484
  const insets = [];
46527
47485
  const insetRegions = [];
46528
47486
  const insetLabelSeeds = [];
46529
- if (resolved.projection === "albers-usa" && usLayer && !resolved.directives.noInsets) {
47487
+ const akRef = resolved.regions.some((r) => r.iso === "US-AK") || resolved.pois.some((p) => inAlaska(p.lon, p.lat));
47488
+ const hiRef = resolved.regions.some((r) => r.iso === "US-HI") || resolved.pois.some((p) => inHawaii(p.lon, p.lat));
47489
+ if (resolved.projection === "albers-usa" && usLayer && (akRef || hiRef)) {
46530
47490
  const PAD = 8;
46531
47491
  const GAP = 12;
46532
47492
  const yB = height - FIT_PAD;
@@ -46591,8 +47551,18 @@ function layoutMap(resolved, data, size, opts) {
46591
47551
  );
46592
47552
  const d = geoPath(proj)(f) ?? "";
46593
47553
  if (!d) return xr;
47554
+ let contextLand;
47555
+ if (iso === "US-AK") {
47556
+ const can = worldLayer.get("CA");
47557
+ const cd = can ? geoPath(proj)(can) ?? "" : "";
47558
+ if (cd)
47559
+ contextLand = {
47560
+ d: cd,
47561
+ fill: colorizeActive ? colorByIso.get("CA") ?? foreignFill : foreignFill
47562
+ };
47563
+ }
46594
47564
  const r = regionById.get(iso);
46595
- let fill2 = neutralFill;
47565
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? neutralFill : neutralFill;
46596
47566
  let lineNumber = -1;
46597
47567
  if (r?.layer === "us-state") {
46598
47568
  fill2 = regionFill(r);
@@ -46611,13 +47581,14 @@ function layoutMap(resolved, data, size, opts) {
46611
47581
  ],
46612
47582
  // The FITTED inset projection (just fit to this box) — captured so the
46613
47583
  // geo-query can invert pixels inside the frame back to AK/HI coords.
46614
- projection: proj
47584
+ projection: proj,
47585
+ ...contextLand && { contextLand }
46615
47586
  });
46616
47587
  insetRegions.push({
46617
47588
  id: iso,
46618
47589
  d,
46619
47590
  fill: fill2,
46620
- stroke: regionStroke,
47591
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46621
47592
  lineNumber,
46622
47593
  layer: "us-state",
46623
47594
  ...r?.value !== void 0 && { value: r.value },
@@ -46630,13 +47601,16 @@ function layoutMap(resolved, data, size, opts) {
46630
47601
  }
46631
47602
  return xr;
46632
47603
  };
46633
- const akRight = placeInset(
46634
- "US-AK",
46635
- alaskaProjection(),
46636
- FIT_PAD,
46637
- width * 0.15
46638
- );
46639
- placeInset("US-HI", hawaiiProjection(), akRight + 24, width * 0.1);
47604
+ let akRight = FIT_PAD;
47605
+ if (akRef)
47606
+ akRight = placeInset("US-AK", alaskaProjection(), FIT_PAD, width * 0.15);
47607
+ if (hiRef)
47608
+ placeInset(
47609
+ "US-HI",
47610
+ hawaiiProjection(),
47611
+ akRef ? akRight + 24 : FIT_PAD,
47612
+ width * 0.1
47613
+ );
46640
47614
  }
46641
47615
  const conusFit = resolved.projection === "albers-usa" && !!usLayer;
46642
47616
  const classifyExtent = conusFit ? geoBounds2(fitTarget) : resolved.extent;
@@ -46652,15 +47626,24 @@ function layoutMap(resolved, data, size, opts) {
46652
47626
  };
46653
47627
  const ringOverlapsView = (ring) => {
46654
47628
  let loMin = Infinity, loMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
47629
+ const lons = [];
46655
47630
  for (const [rawLon] of ring) {
46656
47631
  const lon = normLon(rawLon);
47632
+ lons.push(lon);
46657
47633
  if (lon < loMin) loMin = lon;
46658
47634
  if (lon > loMax) loMax = lon;
46659
47635
  if (rawLon < rawMin) rawMin = rawLon;
46660
47636
  if (rawLon > rawMax) rawMax = rawLon;
46661
47637
  }
46662
- if (loMax - loMin > 270) return false;
46663
- if (rawMax - rawMin > 180 && loMax - loMin < 90) return false;
47638
+ lons.sort((a, b) => a - b);
47639
+ let maxGap = 0;
47640
+ for (let i = 1; i < lons.length; i++)
47641
+ maxGap = Math.max(maxGap, lons[i] - lons[i - 1]);
47642
+ if (lons.length > 1)
47643
+ maxGap = Math.max(maxGap, lons[0] + 360 - lons[lons.length - 1]);
47644
+ const occupiedArc = 360 - maxGap;
47645
+ if (occupiedArc > 270) return false;
47646
+ if (rawMax - rawMin > 180 && occupiedArc < 90) return false;
46664
47647
  let px0 = Infinity, py0 = Infinity, px1 = -Infinity, py1 = -Infinity, anyFinite = false;
46665
47648
  for (const [lon, lat] of ring) {
46666
47649
  const p = project(lon, lat);
@@ -46733,7 +47716,7 @@ function layoutMap(resolved, data, size, opts) {
46733
47716
  const regions = [];
46734
47717
  const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
46735
47718
  for (const [iso, f] of layerFeatures) {
46736
- if (layerKind === "us-state" && usContext && INSET_STATES.has(iso))
47719
+ if (layerKind === "us-state" && usContext && resolved.projection === "albers-usa" && INSET_STATES.has(iso))
46737
47720
  continue;
46738
47721
  if (layerKind === "country" && usContext && iso === "US") continue;
46739
47722
  if (layerKind === "country" && iso === "AQ" && !regionById.has("AQ"))
@@ -46745,7 +47728,8 @@ function layoutMap(resolved, data, size, opts) {
46745
47728
  if (!d) continue;
46746
47729
  const isThisLayer = r?.layer === layerKind;
46747
47730
  const isForeign = layerKind === "country" && usContext && iso !== "US";
46748
- let fill2 = isForeign ? foreignFill : neutralFill;
47731
+ const baseFill = isForeign ? foreignFill : neutralFill;
47732
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? baseFill : baseFill;
46749
47733
  let label;
46750
47734
  let lineNumber = -1;
46751
47735
  let layer = "base";
@@ -46754,12 +47738,14 @@ function layoutMap(resolved, data, size, opts) {
46754
47738
  lineNumber = r.lineNumber;
46755
47739
  layer = layerKind;
46756
47740
  label = r.name;
47741
+ } else {
47742
+ label = f.properties?.name;
46757
47743
  }
46758
47744
  regions.push({
46759
47745
  id: iso,
46760
47746
  d,
46761
47747
  fill: fill2,
46762
- stroke: regionStroke,
47748
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46763
47749
  lineNumber,
46764
47750
  layer,
46765
47751
  ...label !== void 0 && { label },
@@ -46787,9 +47773,41 @@ function layoutMap(resolved, data, size, opts) {
46787
47773
  });
46788
47774
  }
46789
47775
  }
47776
+ const pointInRings = (px, py, rings) => {
47777
+ let inside = false;
47778
+ for (const ring of rings) {
47779
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
47780
+ const [xi, yi] = ring[i];
47781
+ const [xj, yj] = ring[j];
47782
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
47783
+ inside = !inside;
47784
+ }
47785
+ }
47786
+ return inside;
47787
+ };
47788
+ const fillHitTargets = [...regions, ...insetRegions].map((r) => ({
47789
+ fill: r.fill,
47790
+ rings: parsePathRings(r.d)
47791
+ }));
47792
+ const fillAt = (x, y) => {
47793
+ let hit = water;
47794
+ for (const t of fillHitTargets)
47795
+ if (pointInRings(x, y, t.rings)) hit = t.fill;
47796
+ return hit;
47797
+ };
47798
+ const labelOnFill = (fill2) => {
47799
+ const color = contrastRatio(fill2, palette.textOnFillDark) >= contrastRatio(fill2, palette.textOnFillLight) ? palette.textOnFillDark : palette.textOnFillLight;
47800
+ const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47801
+ return {
47802
+ color,
47803
+ halo: contrastRatio(fill2, color) < REGION_LABEL_HALO_RATIO,
47804
+ haloColor
47805
+ };
47806
+ };
47807
+ const reliefAllowed = resolved.directives.noRelief !== true;
46790
47808
  const relief = [];
46791
47809
  let reliefHatch = null;
46792
- if (resolved.directives.relief === true && data.mountainRanges) {
47810
+ if (reliefAllowed && data.mountainRanges) {
46793
47811
  for (const [, f] of decodeLayer(data.mountainRanges)) {
46794
47812
  const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
46795
47813
  if (!viewF) continue;
@@ -46805,16 +47823,32 @@ function layoutMap(resolved, data, size, opts) {
46805
47823
  if (relief.length) {
46806
47824
  const darkTone = isDark ? palette.bg : palette.text;
46807
47825
  const lightTone = isDark ? palette.text : palette.bg;
46808
- const landLum = relativeLuminance(neutralFill);
47826
+ const reliefLandRef = colorizeActive ? isDark ? palette.surface : palette.bg : neutralFill;
47827
+ const landLum = relativeLuminance(reliefLandRef);
46809
47828
  const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
46810
47829
  reliefHatch = {
46811
- color: mix(tone, neutralFill, RELIEF_HATCH_STRENGTH),
47830
+ color: mix(tone, reliefLandRef, RELIEF_HATCH_STRENGTH),
46812
47831
  spacing: RELIEF_HATCH_SPACING,
46813
47832
  width: RELIEF_HATCH_WIDTH
46814
47833
  };
46815
47834
  }
46816
47835
  }
46817
- const riverColor = mix(water, regionStroke, 16);
47836
+ let coastlineStyle = null;
47837
+ if (resolved.directives.noCoastline !== true) {
47838
+ const minDim = Math.min(width, height);
47839
+ coastlineStyle = {
47840
+ color: mix(regionStroke, water, COASTLINE_STROKE_MIX),
47841
+ // N equal-width rings: distance steps outward by COASTLINE_STEP; opacity
47842
+ // fades linearly from NEAR (innermost) to FAR (outermost).
47843
+ lines: Array.from({ length: COASTLINE_RING_COUNT }, (_, k) => ({
47844
+ d: (COASTLINE_D0 + k * COASTLINE_STEP) * minDim,
47845
+ thickness: COASTLINE_THICKNESS * minDim,
47846
+ opacity: COASTLINE_OPACITY_NEAR + (COASTLINE_OPACITY_FAR - COASTLINE_OPACITY_NEAR) * k / (COASTLINE_RING_COUNT - 1)
47847
+ })),
47848
+ minExtent: (isGlobalView ? COASTLINE_MIN_EXTENT_GLOBAL : COASTLINE_MIN_EXTENT) * minDim
47849
+ };
47850
+ }
47851
+ const riverColor = mix(palette.colors.blue, water, 32);
46818
47852
  const rivers = [];
46819
47853
  if (data.rivers) {
46820
47854
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -46870,38 +47904,108 @@ function layoutMap(resolved, data, size, opts) {
46870
47904
  const xy = project(p.lon, p.lat);
46871
47905
  if (xy) projected.push({ p, xy });
46872
47906
  }
46873
- const coloGroups = /* @__PURE__ */ new Map();
47907
+ const placePoi = (e, cx, cy, clusterId) => {
47908
+ const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
47909
+ poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
47910
+ const num = routeNumberById.get(e.p.id);
47911
+ pois.push({
47912
+ id: e.p.id,
47913
+ cx,
47914
+ cy,
47915
+ r: radiusFor(e.p),
47916
+ fill: fill2,
47917
+ stroke: stroke2,
47918
+ lineNumber: e.p.lineNumber,
47919
+ implicit: !!e.p.implicit,
47920
+ isOrigin: originIds.has(e.p.id),
47921
+ ...num !== void 0 && { routeNumber: num },
47922
+ ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags },
47923
+ ...clusterId !== void 0 && { clusterId }
47924
+ });
47925
+ };
47926
+ const clusters = [];
47927
+ const connected = /* @__PURE__ */ new Set();
47928
+ for (const e of resolved.edges) {
47929
+ connected.add(e.fromId);
47930
+ connected.add(e.toId);
47931
+ }
47932
+ for (const rt of resolved.routes) {
47933
+ rt.stopIds.forEach((id) => connected.add(id));
47934
+ }
47935
+ const radiusOf = (e) => radiusFor(e.p);
46874
47936
  for (const e of projected) {
46875
- const key = `${Math.round(e.xy[0] / COLO_EPS)},${Math.round(e.xy[1] / COLO_EPS)}`;
46876
- const arr = coloGroups.get(key);
46877
- if (arr) arr.push(e);
46878
- else coloGroups.set(key, [e]);
46879
- }
46880
- for (const group of coloGroups.values()) {
46881
- group.forEach((e, i) => {
46882
- let cx = e.xy[0];
46883
- let cy = e.xy[1];
46884
- if (group.length > 1) {
46885
- const ang = i * GOLDEN_ANGLE;
46886
- cx += Math.cos(ang) * COLO_R;
46887
- cy += Math.sin(ang) * COLO_R;
46888
- }
46889
- const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
46890
- poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
46891
- const num = routeNumberById.get(e.p.id);
46892
- pois.push({
46893
- id: e.p.id,
46894
- cx,
46895
- cy,
46896
- r: radiusFor(e.p),
46897
- fill: fill2,
46898
- stroke: stroke2,
46899
- lineNumber: e.p.lineNumber,
46900
- implicit: !!e.p.implicit,
46901
- isOrigin: originIds.has(e.p.id),
46902
- ...num !== void 0 && { routeNumber: num },
46903
- ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags }
46904
- });
47937
+ if (connected.has(e.p.id)) placePoi(e, e.xy[0], e.xy[1]);
47938
+ }
47939
+ const groups = [];
47940
+ for (const e of projected) {
47941
+ if (connected.has(e.p.id)) continue;
47942
+ const r = radiusOf(e);
47943
+ const near = groups.find(
47944
+ (g) => g.some(
47945
+ (q) => Math.hypot(q.xy[0] - e.xy[0], q.xy[1] - e.xy[1]) < (r + radiusOf(q)) * STACK_OVERLAP
47946
+ )
47947
+ );
47948
+ if (near) near.push(e);
47949
+ else groups.push([e]);
47950
+ }
47951
+ for (const g of groups) {
47952
+ if (g.length === 1) {
47953
+ placePoi(g[0], g[0].xy[0], g[0].xy[1]);
47954
+ continue;
47955
+ }
47956
+ const clusterId = g[0].p.id;
47957
+ const cx0 = g.reduce((s, e) => s + e.xy[0], 0) / g.length;
47958
+ const cy0 = g.reduce((s, e) => s + e.xy[1], 0) / g.length;
47959
+ const maxR = Math.max(...g.map(radiusOf));
47960
+ const sep = 2 * maxR + STACK_RING_GAP;
47961
+ const ringR = Math.max(
47962
+ COLO_R,
47963
+ sep / (2 * Math.sin(Math.PI / Math.max(g.length, 2)))
47964
+ );
47965
+ const positions = g.map((e, i) => {
47966
+ if (g.length <= STACK_RING_MAX) {
47967
+ const ang2 = -Math.PI / 2 + i * 2 * Math.PI / g.length;
47968
+ return {
47969
+ e,
47970
+ mx: cx0 + Math.cos(ang2) * ringR,
47971
+ my: cy0 + Math.sin(ang2) * ringR
47972
+ };
47973
+ }
47974
+ const ang = i * GOLDEN_ANGLE;
47975
+ const rr = ringR * Math.sqrt((i + 1) / g.length);
47976
+ return { e, mx: cx0 + Math.cos(ang) * rr, my: cy0 + Math.sin(ang) * rr };
47977
+ });
47978
+ let minX = cx0 - maxR;
47979
+ let maxX = cx0 + maxR;
47980
+ let minY = cy0 - maxR;
47981
+ let maxY = cy0 + maxR;
47982
+ for (const { mx, my, e } of positions) {
47983
+ const r = radiusOf(e);
47984
+ minX = Math.min(minX, mx - r);
47985
+ maxX = Math.max(maxX, mx + r);
47986
+ minY = Math.min(minY, my - r);
47987
+ maxY = Math.max(maxY, my + r);
47988
+ }
47989
+ let dx = 0;
47990
+ let dy = 0;
47991
+ if (minX + dx < 2) dx = 2 - minX;
47992
+ if (maxX + dx > width - 2) dx = width - 2 - maxX;
47993
+ if (minY + dy < 2) dy = 2 - minY;
47994
+ if (maxY + dy > height - 2) dy = height - 2 - maxY;
47995
+ const legsOut = [];
47996
+ for (const { e, mx, my } of positions) {
47997
+ const fx = mx + dx;
47998
+ const fy = my + dy;
47999
+ placePoi(e, fx, fy, clusterId);
48000
+ legsOut.push({ x2: fx, y2: fy, color: poiFill(e.p).fill });
48001
+ }
48002
+ clusters.push({
48003
+ id: clusterId,
48004
+ cx: cx0 + dx,
48005
+ cy: cy0 + dy,
48006
+ count: g.length,
48007
+ hitR: ringR + maxR + 6,
48008
+ legs: legsOut
46905
48009
  });
46906
48010
  }
46907
48011
  const legs = [];
@@ -46951,16 +48055,26 @@ function layoutMap(resolved, data, size, opts) {
46951
48055
  if (!a || !b) continue;
46952
48056
  const mx = (a.cx + b.cx) / 2;
46953
48057
  const my = (a.cy + b.cy) / 2;
48058
+ const bow = {
48059
+ curved: leg.style === "arc",
48060
+ offset: 0,
48061
+ labelX: mx,
48062
+ labelY: my - 4
48063
+ };
48064
+ const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46954
48065
  legs.push({
46955
- d: legPath(a, b, leg.style === "arc", 0),
48066
+ d: legPath(a, b, bow.curved, bow.offset),
46956
48067
  width: routeWidthFor(Number(leg.value)),
46957
48068
  color: mix(palette.text, palette.bg, 72),
46958
48069
  arrow: true,
46959
48070
  lineNumber: leg.lineNumber,
46960
48071
  ...leg.label !== void 0 && {
46961
48072
  label: leg.label,
46962
- labelX: mx,
46963
- labelY: my - 4
48073
+ labelX: bow.labelX,
48074
+ labelY: bow.labelY,
48075
+ labelColor: routeLabelStyle.color,
48076
+ labelHalo: routeLabelStyle.halo,
48077
+ labelHaloColor: routeLabelStyle.haloColor
46964
48078
  }
46965
48079
  });
46966
48080
  }
@@ -46988,20 +48102,29 @@ function layoutMap(resolved, data, size, opts) {
46988
48102
  const a = poiScreen.get(e.fromId);
46989
48103
  const b = poiScreen.get(e.toId);
46990
48104
  if (!a || !b) return;
46991
- const curved = e.style === "arc" || n > 1;
46992
- const offset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
48105
+ const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
46993
48106
  const mx = (a.cx + b.cx) / 2;
46994
48107
  const my = (a.cy + b.cy) / 2;
48108
+ const bow = {
48109
+ curved: e.style === "arc" || n > 1,
48110
+ offset: fanOffset,
48111
+ labelX: mx,
48112
+ labelY: my - 4
48113
+ };
48114
+ const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46995
48115
  legs.push({
46996
- d: legPath(a, b, curved, offset),
48116
+ d: legPath(a, b, bow.curved, bow.offset),
46997
48117
  width: widthFor(e),
46998
48118
  color: mix(palette.text, palette.bg, 66),
46999
48119
  arrow: e.directed,
47000
48120
  lineNumber: e.lineNumber,
47001
48121
  ...e.label !== void 0 && {
47002
48122
  label: e.label,
47003
- labelX: mx,
47004
- labelY: my - 4
48123
+ labelX: bow.labelX,
48124
+ labelY: bow.labelY,
48125
+ labelColor: edgeLabelStyle.color,
48126
+ labelHalo: edgeLabelStyle.halo,
48127
+ labelHaloColor: edgeLabelStyle.haloColor
47005
48128
  }
47006
48129
  });
47007
48130
  });
@@ -47043,25 +48166,25 @@ function layoutMap(resolved, data, size, opts) {
47043
48166
  }
47044
48167
  }
47045
48168
  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));
47046
- const regionLabelMode = resolved.directives.regionLabels ?? "off";
48169
+ const showRegionLabels = resolved.directives.noRegionLabels !== true;
48170
+ const isCompact = width < COMPACT_WIDTH_PX;
47047
48171
  const LABEL_PADX = 6;
47048
48172
  const LABEL_PADY = 3;
47049
- const labelW = (text) => measureLegendText(text, FONT) + 2 * LABEL_PADX;
47050
- const labelH = FONT + 2 * LABEL_PADY;
48173
+ const labelW = (text) => measureLegendText(text, FONT2) + 2 * LABEL_PADX;
48174
+ const labelH = FONT2 + 2 * LABEL_PADY;
47051
48175
  const pushRegionLabel = (x, y, text, fill2, lineNumber) => {
47052
- const color = contrastText(
47053
- fill2,
47054
- palette.textOnFillLight,
47055
- palette.textOnFillDark
48176
+ const { color, haloColor } = labelOnFill(fill2);
48177
+ const halfW = measureLegendText(text, FONT2) / 2;
48178
+ const overflows = [y - FONT2 * 0.55, y - FONT2 * 0.1].some(
48179
+ (sy) => fillAt(x - halfW, sy) !== fill2 || fillAt(x + halfW, sy) !== fill2
47056
48180
  );
47057
- const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47058
48181
  labels.push({
47059
48182
  x,
47060
48183
  y,
47061
48184
  text,
47062
48185
  anchor: "middle",
47063
48186
  color,
47064
- halo: true,
48187
+ halo: overflows,
47065
48188
  haloColor,
47066
48189
  lineNumber
47067
48190
  });
@@ -47070,21 +48193,50 @@ function layoutMap(resolved, data, size, opts) {
47070
48193
  US: [-98.5, 39.5]
47071
48194
  // CONUS geographic centre (near Lebanon, Kansas)
47072
48195
  };
47073
- if (regionLabelMode === "full" || regionLabelMode === "abbrev") {
47074
- for (const r of regions) {
47075
- if (r.layer === "base" || r.label === void 0) continue;
47076
- const f = r.layer === "us-state" ? usLayer?.get(r.id) : worldLayer.get(r.id);
47077
- if (!f) continue;
48196
+ const REGION_LABEL_GAP = 2;
48197
+ const regionLabelRect = (cx, cy, text) => {
48198
+ const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
48199
+ return { x: cx - w / 2, y: cy - FONT2 / 2, w, h: FONT2 };
48200
+ };
48201
+ if (showRegionLabels) {
48202
+ const frameContainers = new Set(resolved.poiFrameContainers);
48203
+ const entries = regions.map((r) => {
48204
+ const isContainer = frameContainers.has(r.id);
48205
+ if (r.layer === "base" && !isContainer || r.label === void 0)
48206
+ return null;
48207
+ const isUsState = r.layer === "us-state" || r.id.startsWith("US-");
48208
+ const f = isUsState ? usLayer?.get(r.id) : worldLayer.get(r.id);
48209
+ if (!f) return null;
47078
48210
  const [[x0, y0], [x1, y1]] = path.bounds(f);
47079
- const text = regionLabelMode === "abbrev" ? r.id.replace(/^US-/, "") : r.label;
47080
- if (labelW(text) > x1 - x0 || labelH > y1 - y0) continue;
47081
- const anchor = r.layer !== "us-state" ? WORLD_LABEL_ANCHORS[r.id] : void 0;
48211
+ const boxW = x1 - x0;
48212
+ const boxH = y1 - y0;
48213
+ const abbrev = isUsState ? r.id.replace(/^US-/, "") : void 0;
48214
+ const candidates = abbrev !== void 0 ? isCompact ? [abbrev, r.label] : [r.label, abbrev] : [r.label];
48215
+ const anchor = !isUsState ? WORLD_LABEL_ANCHORS[r.id] : void 0;
47082
48216
  const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
47083
- if (!c || !Number.isFinite(c[0])) continue;
48217
+ if (!c || !Number.isFinite(c[0])) return null;
48218
+ return { r, c, boxW, boxH, area: boxW * boxH, candidates };
48219
+ }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
48220
+ const placedRegionRects = [];
48221
+ const POI_LABEL_PAD = 14;
48222
+ const poiObstacles = pois.map((p) => ({
48223
+ x: p.cx - p.r - POI_LABEL_PAD,
48224
+ y: p.cy - p.r - POI_LABEL_PAD,
48225
+ w: 2 * (p.r + POI_LABEL_PAD),
48226
+ h: 2 * (p.r + POI_LABEL_PAD)
48227
+ }));
48228
+ for (const { r, c, boxW, boxH, candidates } of entries) {
48229
+ const text = candidates.find((t) => {
48230
+ if (labelW(t) > boxW || labelH > boxH) return false;
48231
+ const rect = regionLabelRect(c[0], c[1], t);
48232
+ return !placedRegionRects.some((p) => rectsOverlap(rect, p)) && !poiObstacles.some((o) => rectsOverlap(rect, o));
48233
+ });
48234
+ if (text === void 0) continue;
48235
+ placedRegionRects.push(regionLabelRect(c[0], c[1], text));
47084
48236
  pushRegionLabel(c[0], c[1], text, r.fill, r.lineNumber);
47085
48237
  }
47086
48238
  for (const seed of insetLabelSeeds) {
47087
- const text = regionLabelMode === "abbrev" ? seed.iso.replace(/^US-/, "") : seed.name;
48239
+ const text = isCompact ? seed.iso.replace(/^US-/, "") : seed.name;
47088
48240
  const src = regionById.get(seed.iso);
47089
48241
  pushRegionLabel(
47090
48242
  seed.x,
@@ -47095,22 +48247,26 @@ function layoutMap(resolved, data, size, opts) {
47095
48247
  );
47096
48248
  }
47097
48249
  }
47098
- const poiLabelMode = resolved.directives.poiLabels ?? "auto";
47099
- if (poiLabelMode !== "off") {
47100
- const ordered = [...pois].sort(
47101
- (a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1)
47102
- );
48250
+ if (resolved.directives.noPoiLabels !== true) {
48251
+ const ordered = [...pois].filter((p) => p.clusterId === void 0).sort((a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1));
47103
48252
  const poiById = new Map(resolved.pois.map((q) => [q.id, q]));
47104
48253
  const labelText = (p) => {
47105
48254
  const src = poiById.get(p.id);
47106
48255
  return src?.label ?? src?.name ?? p.id;
47107
48256
  };
47108
- const poiLabH = FONT * 1.25;
48257
+ const poiLabH = FONT2 * 1.25;
47109
48258
  const labelInfo = (p) => {
47110
48259
  const text = labelText(p);
47111
- return { text, w: measureLegendText(text, FONT) };
48260
+ return { text, w: measureLegendText(text, FONT2) };
47112
48261
  };
47113
48262
  const GAP = 3;
48263
+ const clusterMembersById = /* @__PURE__ */ new Map();
48264
+ for (const p of pois) {
48265
+ if (p.clusterId === void 0) continue;
48266
+ const arr = clusterMembersById.get(p.clusterId);
48267
+ if (arr) arr.push(p);
48268
+ else clusterMembersById.set(p.clusterId, [p]);
48269
+ }
47114
48270
  const inlineRect = (p, w, side) => {
47115
48271
  switch (side) {
47116
48272
  case "right":
@@ -47140,11 +48296,11 @@ function layoutMap(resolved, data, size, opts) {
47140
48296
  const x = side === "right" ? rect.x : side === "left" ? rect.x + w : p.cx;
47141
48297
  labels.push({
47142
48298
  x,
47143
- y: rect.y + poiLabH / 2 + FONT / 3,
48299
+ y: rect.y + poiLabH / 2 + FONT2 / 3,
47144
48300
  text,
47145
48301
  anchor,
47146
48302
  color: palette.text,
47147
- halo: true,
48303
+ halo: false,
47148
48304
  haloColor: palette.bg,
47149
48305
  poiId: p.id,
47150
48306
  lineNumber: p.lineNumber
@@ -47155,43 +48311,60 @@ function layoutMap(resolved, data, size, opts) {
47155
48311
  return rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect);
47156
48312
  };
47157
48313
  const GROUP_R = 30;
47158
- const groups = [];
48314
+ const groups2 = [];
47159
48315
  for (const p of ordered) {
47160
- const near = groups.find(
48316
+ const near = groups2.find(
47161
48317
  (g) => g.some((q) => Math.hypot(q.cx - p.cx, q.cy - p.cy) < GROUP_R)
47162
48318
  );
47163
48319
  if (near) near.push(p);
47164
- else groups.push([p]);
48320
+ else groups2.push([p]);
47165
48321
  }
47166
48322
  const ROW_GAP2 = 3;
47167
48323
  const step = poiLabH + ROW_GAP2;
47168
48324
  const COL_GAP = 16;
47169
- const placeColumn = (group) => {
47170
- const items = group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48325
+ const makeItems = (group) => group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48326
+ const columnRows = (items, side) => {
47171
48327
  const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
47172
48328
  const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
47173
- const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
47174
48329
  const maxW = Math.max(...items.map((o) => o.w));
47175
- const side = right + COL_GAP + maxW <= width - 2 ? "right" : "left";
47176
- const colX = side === "right" ? right + COL_GAP : left - COL_GAP;
48330
+ const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
48331
+ const colX = side === "right" ? Math.min(right + COL_GAP, width - 2 - maxW) : Math.max(left - COL_GAP, 2 + maxW);
47177
48332
  const totalH = items.length * step;
47178
48333
  let startY = cyMid - totalH / 2;
47179
48334
  startY = Math.max(2, Math.min(startY, height - totalH - 2));
47180
- items.forEach((o, i) => {
48335
+ return items.map((o, i) => {
47181
48336
  const rowCy = startY + i * step + step / 2;
47182
- obstacles.push({
47183
- x: side === "right" ? colX : colX - o.w,
47184
- y: rowCy - poiLabH / 2,
47185
- w: o.w,
47186
- h: poiLabH
47187
- });
48337
+ return {
48338
+ o,
48339
+ colX,
48340
+ rowCy,
48341
+ rect: {
48342
+ x: side === "right" ? colX : colX - o.w,
48343
+ y: rowCy - poiLabH / 2,
48344
+ w: o.w,
48345
+ h: poiLabH
48346
+ }
48347
+ };
48348
+ });
48349
+ };
48350
+ const wouldColumnBeClean = (items, side) => columnRows(items, side).every(
48351
+ ({ rect }) => rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect)
48352
+ );
48353
+ const defaultColumnSide = (items) => {
48354
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48355
+ const maxW = Math.max(...items.map((o) => o.w));
48356
+ return right + COL_GAP + maxW <= width - 2 ? "right" : "left";
48357
+ };
48358
+ const commitColumn = (items, side, clusterId) => {
48359
+ for (const { o, colX, rowCy, rect } of columnRows(items, side)) {
48360
+ obstacles.push(rect);
47188
48361
  labels.push({
47189
48362
  x: colX,
47190
- y: rowCy + FONT / 3,
48363
+ y: rowCy + FONT2 / 3,
47191
48364
  text: o.text,
47192
48365
  anchor: side === "right" ? "start" : "end",
47193
48366
  color: palette.text,
47194
- halo: true,
48367
+ halo: false,
47195
48368
  haloColor: palette.bg,
47196
48369
  leader: {
47197
48370
  x1: o.p.cx,
@@ -47201,24 +48374,141 @@ function layoutMap(resolved, data, size, opts) {
47201
48374
  },
47202
48375
  leaderColor: o.p.fill,
47203
48376
  poiId: o.p.id,
47204
- lineNumber: o.p.lineNumber
48377
+ lineNumber: o.p.lineNumber,
48378
+ ...clusterId !== void 0 && { clusterMember: clusterId }
47205
48379
  });
48380
+ }
48381
+ };
48382
+ const pushHidden = (p) => {
48383
+ const { text, w } = labelInfo(p);
48384
+ let x = p.cx + p.r + GAP;
48385
+ let anchor = "start";
48386
+ if (x + w > width) {
48387
+ x = p.cx - p.r - GAP - w;
48388
+ anchor = "end";
48389
+ }
48390
+ const y = Math.max(0, Math.min(p.cy - poiLabH / 2, height - poiLabH));
48391
+ labels.push({
48392
+ x: anchor === "start" ? x : x + w,
48393
+ y: y + poiLabH / 2 + FONT2 / 3,
48394
+ text,
48395
+ anchor,
48396
+ color: palette.text,
48397
+ halo: false,
48398
+ haloColor: palette.bg,
48399
+ poiId: p.id,
48400
+ hidden: true,
48401
+ lineNumber: p.lineNumber
47206
48402
  });
47207
48403
  };
47208
- for (const g of groups) {
48404
+ for (const [clusterId, members] of clusterMembersById) {
48405
+ if (members.length === 0) continue;
48406
+ const items = makeItems(members);
48407
+ const side = wouldColumnBeClean(items, "right") ? "right" : wouldColumnBeClean(items, "left") ? "left" : defaultColumnSide(items);
48408
+ commitColumn(items, side, clusterId);
48409
+ }
48410
+ const maxExtent = MAX_CLUSTER_EXTENT_FACTOR * Math.min(width, height);
48411
+ const clusterPending = [];
48412
+ for (const g of groups2) {
48413
+ const items = makeItems(g);
47209
48414
  if (g.length === 1) {
47210
- const p = g[0];
47211
- const { text, w } = labelInfo(p);
48415
+ const { p, text, w } = items[0];
47212
48416
  const side = ["right", "left", "above", "below"].find(
47213
48417
  (s) => inlineFits(p, w, s)
47214
48418
  );
47215
- if (side) {
47216
- pushInline(p, text, w, side);
47217
- continue;
48419
+ if (side) pushInline(p, text, w, side);
48420
+ else commitColumn(items, defaultColumnSide(items));
48421
+ continue;
48422
+ }
48423
+ const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
48424
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48425
+ const minCy = Math.min(...items.map((o) => o.p.cy));
48426
+ const maxCy = Math.max(...items.map((o) => o.p.cy));
48427
+ const diag = Math.hypot(right - left, maxCy - minCy);
48428
+ if (diag > maxExtent || items.length > MAX_COLUMN_ROWS) {
48429
+ items.forEach((o) => pushHidden(o.p));
48430
+ } else {
48431
+ clusterPending.push(items);
48432
+ }
48433
+ }
48434
+ for (const items of clusterPending) {
48435
+ const side = ["right", "left"].find(
48436
+ (s) => wouldColumnBeClean(items, s)
48437
+ );
48438
+ if (side) commitColumn(items, side);
48439
+ else items.forEach((o) => pushHidden(o.p));
48440
+ }
48441
+ }
48442
+ if (resolved.directives.noContextLabels !== true) {
48443
+ for (const l of labels) {
48444
+ if (l.hidden) continue;
48445
+ const w = labelW(l.text);
48446
+ const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
48447
+ obstacles.push({ x, y: l.y - labelH / 2, w, h: labelH });
48448
+ }
48449
+ for (const box of insets)
48450
+ obstacles.push({ x: box.x, y: box.y, w: box.w, h: box.h });
48451
+ const countryCandidates = [];
48452
+ for (const f of worldLayer.values()) {
48453
+ const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
48454
+ if (!iso || regionById.has(iso)) continue;
48455
+ let hasReferencedSub = false;
48456
+ for (const k of regionById.keys())
48457
+ if (k.startsWith(iso + "-")) {
48458
+ hasReferencedSub = true;
48459
+ break;
47218
48460
  }
48461
+ if (hasReferencedSub) continue;
48462
+ const b = path.bounds(f);
48463
+ const [x0, y0] = b[0];
48464
+ const [x1, y1] = b[1];
48465
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48466
+ const anchorLngLat = WORLD_LABEL_ANCHORS[iso];
48467
+ const a = anchorLngLat ? project(anchorLngLat[0], anchorLngLat[1]) : path.centroid(f);
48468
+ countryCandidates.push({
48469
+ name: f.properties?.name ?? iso,
48470
+ bbox: [x0, y0, x1, y1],
48471
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48472
+ });
48473
+ }
48474
+ const framedStateContainers = (resolved.poiFrameContainers ?? []).some(
48475
+ (id) => id.startsWith("US-")
48476
+ );
48477
+ if (usLayer && framedStateContainers) {
48478
+ const containerSet = new Set(resolved.poiFrameContainers);
48479
+ for (const [iso, f] of usLayer) {
48480
+ if (containerSet.has(iso) || regionById.has(iso)) continue;
48481
+ const viewF = cullFeatureToView(f);
48482
+ if (!viewF) continue;
48483
+ const b = path.bounds(viewF);
48484
+ const [x0, y0] = b[0];
48485
+ const [x1, y1] = b[1];
48486
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48487
+ const a = path.centroid(viewF);
48488
+ countryCandidates.push({
48489
+ name: f.properties?.name ?? iso,
48490
+ bbox: [x0, y0, x1, y1],
48491
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48492
+ });
47219
48493
  }
47220
- placeColumn(g);
47221
48494
  }
48495
+ const contextLabels = placeContextLabels({
48496
+ projection: resolved.projection,
48497
+ dLonSpan,
48498
+ dLatSpan,
48499
+ width,
48500
+ height,
48501
+ waterBodies: data.waterBodies,
48502
+ countries: countryCandidates,
48503
+ palette,
48504
+ project,
48505
+ collides,
48506
+ // Water labels must stay over open water — `fillAt` returns the ocean
48507
+ // backdrop colour off-land and a region fill on-land (lakes/states count
48508
+ // as land here, which is the safe side for an ocean name).
48509
+ overLand: (x, y) => fillAt(x, y) !== water
48510
+ });
48511
+ labels.push(...contextLabels);
47222
48512
  }
47223
48513
  let legend = null;
47224
48514
  if (!resolved.directives.noLegend) {
@@ -47255,25 +48545,31 @@ function layoutMap(resolved, data, size, opts) {
47255
48545
  rivers,
47256
48546
  relief,
47257
48547
  reliefHatch,
48548
+ coastlineStyle,
47258
48549
  legs,
47259
48550
  pois,
48551
+ clusters,
47260
48552
  labels,
47261
48553
  legend,
47262
48554
  insets,
47263
48555
  insetRegions,
47264
48556
  projection,
47265
- stretch: stretchParams
48557
+ stretch: stretchParams,
48558
+ diagnostics: []
47266
48559
  };
47267
48560
  }
47268
- var FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, COLO_EPS, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, FAN_STEP, ARC_CURVE_FRAC, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, US_NON_CONUS;
48561
+ var FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT2, MAX_CLUSTER_EXTENT_FACTOR, MAX_COLUMN_ROWS, REGION_LABEL_HALO_RATIO, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, COMPACT_WIDTH_PX, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, COASTLINE_RING_COUNT, COASTLINE_D0, COASTLINE_STEP, COASTLINE_THICKNESS, COASTLINE_OPACITY_NEAR, COASTLINE_OPACITY_FAR, COASTLINE_MIN_EXTENT, COASTLINE_MIN_EXTENT_GLOBAL, COASTLINE_STROKE_MIX, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, STACK_OVERLAP, STACK_RING_MAX, STACK_RING_GAP, FAN_STEP, ARC_CURVE_FRAC, decodeCache, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, inAlaska, inHawaii, FOREIGN_BORDER, US_NON_CONUS;
47269
48562
  var init_layout15 = __esm({
47270
48563
  "src/map/layout.ts"() {
47271
48564
  "use strict";
47272
48565
  init_color_utils();
48566
+ init_geo();
48567
+ init_colorize();
47273
48568
  init_colors();
47274
48569
  init_label_layout();
47275
48570
  init_legend_constants();
47276
48571
  init_title_constants();
48572
+ init_context_labels();
47277
48573
  FIT_PAD = 24;
47278
48574
  RAMP_FLOOR = 15;
47279
48575
  R_DEFAULT = 6;
@@ -47281,32 +48577,66 @@ var init_layout15 = __esm({
47281
48577
  R_MAX = 22;
47282
48578
  W_MIN = 1.25;
47283
48579
  W_MAX = 8;
47284
- FONT = 11;
47285
- COLO_EPS = 1.5;
48580
+ FONT2 = 11;
48581
+ MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48582
+ MAX_COLUMN_ROWS = 7;
48583
+ REGION_LABEL_HALO_RATIO = 4.5;
47286
48584
  LAND_TINT_LIGHT = 12;
47287
48585
  LAND_TINT_DARK = 24;
47288
48586
  TAG_TINT_LIGHT = 60;
47289
48587
  TAG_TINT_DARK = 68;
47290
- WATER_TINT_LIGHT = 13;
47291
- WATER_TINT_DARK = 14;
48588
+ WATER_TINT_LIGHT = 24;
48589
+ WATER_TINT_DARK = 24;
47292
48590
  RIVER_WIDTH = 1.3;
48591
+ COMPACT_WIDTH_PX = 480;
47293
48592
  RELIEF_MIN_AREA = 12;
47294
48593
  RELIEF_MIN_DIM = 2;
47295
- RELIEF_HATCH_SPACING = 3;
47296
- RELIEF_HATCH_WIDTH = 0.25;
48594
+ RELIEF_HATCH_SPACING = 2;
48595
+ RELIEF_HATCH_WIDTH = 0.15;
47297
48596
  RELIEF_HATCH_STRENGTH = 32;
48597
+ COASTLINE_RING_COUNT = 5;
48598
+ COASTLINE_D0 = 16e-4;
48599
+ COASTLINE_STEP = 28e-4;
48600
+ COASTLINE_THICKNESS = 14e-4;
48601
+ COASTLINE_OPACITY_NEAR = 0.5;
48602
+ COASTLINE_OPACITY_FAR = 0.1;
48603
+ COASTLINE_MIN_EXTENT = 6e-4;
48604
+ COASTLINE_MIN_EXTENT_GLOBAL = 6e-4;
48605
+ COASTLINE_STROKE_MIX = 32;
47298
48606
  FOREIGN_TINT_LIGHT = 30;
47299
48607
  FOREIGN_TINT_DARK = 62;
47300
48608
  MUTED_FOREIGN_LIGHT = 28;
47301
48609
  MUTED_FOREIGN_DARK = 16;
47302
48610
  COLO_R = 9;
47303
48611
  GOLDEN_ANGLE = 2.399963229728653;
48612
+ STACK_OVERLAP = 1;
48613
+ STACK_RING_MAX = 8;
48614
+ STACK_RING_GAP = 4;
47304
48615
  FAN_STEP = 16;
47305
48616
  ARC_CURVE_FRAC = 0.18;
48617
+ decodeCache = /* @__PURE__ */ new WeakMap();
47306
48618
  usConusProjection = () => geoConicEqualArea().parallels([29.5, 45.5]).rotate([96, 0]);
47307
48619
  alaskaProjection = () => geoConicEqualArea().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
47308
48620
  hawaiiProjection = () => geoMercator();
47309
48621
  INSET_STATES = /* @__PURE__ */ new Set(["US-AK", "US-HI"]);
48622
+ inAlaska = (lon, lat) => lat >= 51 && (lon <= -129 || lon >= 172);
48623
+ inHawaii = (lon, lat) => lat >= 18 && lat <= 23 && lon >= -161 && lon <= -154;
48624
+ FOREIGN_BORDER = {
48625
+ CA: [
48626
+ "US-AK",
48627
+ "US-WA",
48628
+ "US-ID",
48629
+ "US-MT",
48630
+ "US-ND",
48631
+ "US-MN",
48632
+ "US-MI",
48633
+ "US-NY",
48634
+ "US-VT",
48635
+ "US-NH",
48636
+ "US-ME"
48637
+ ],
48638
+ MX: ["US-CA", "US-AZ", "US-NM", "US-TX"]
48639
+ };
47310
48640
  US_NON_CONUS = /* @__PURE__ */ new Set([
47311
48641
  "US-AK",
47312
48642
  "US-HI",
@@ -47326,6 +48656,58 @@ __export(renderer_exports16, {
47326
48656
  renderMapForExport: () => renderMapForExport
47327
48657
  });
47328
48658
  import * as d3Selection18 from "d3-selection";
48659
+ function pointInRing2(px, py, ring) {
48660
+ let inside = false;
48661
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
48662
+ const [xi, yi] = ring[i];
48663
+ const [xj, yj] = ring[j];
48664
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
48665
+ inside = !inside;
48666
+ }
48667
+ return inside;
48668
+ }
48669
+ function ringToPath(ring) {
48670
+ let d = "";
48671
+ for (let i = 0; i < ring.length; i++)
48672
+ d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48673
+ return d + "Z";
48674
+ }
48675
+ function coastlineOuterRings(regions, minExtent) {
48676
+ const paths = [];
48677
+ for (const r of regions) {
48678
+ const rings = parsePathRings(r.d);
48679
+ for (let i = 0; i < rings.length; i++) {
48680
+ const ring = rings[i];
48681
+ if (ring.length < 3) continue;
48682
+ let minX = Infinity;
48683
+ let minY = Infinity;
48684
+ let maxX = -Infinity;
48685
+ let maxY = -Infinity;
48686
+ for (const [x, y] of ring) {
48687
+ if (x < minX) minX = x;
48688
+ if (x > maxX) maxX = x;
48689
+ if (y < minY) minY = y;
48690
+ if (y > maxY) maxY = y;
48691
+ }
48692
+ if (Math.max(maxX - minX, maxY - minY) < minExtent) continue;
48693
+ const [fx, fy] = ring[0];
48694
+ let depth = 0;
48695
+ for (let j = 0; j < rings.length; j++)
48696
+ if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48697
+ if (depth % 2 === 1) continue;
48698
+ paths.push(ringToPath(ring));
48699
+ }
48700
+ }
48701
+ return paths;
48702
+ }
48703
+ function appendWaterLines(g, outerRings, style, flatWater) {
48704
+ const d = outerRings.join(" ");
48705
+ const linesOuterFirst = [...style.lines].sort((a, b) => b.d - a.d);
48706
+ for (const line12 of linesOuterFirst) {
48707
+ 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");
48708
+ g.append("path").attr("d", d).attr("stroke", flatWater).attr("stroke-width", 2 * line12.d).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
48709
+ }
48710
+ }
47329
48711
  function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims, activeGroupOverride) {
47330
48712
  d3Selection18.select(container).selectAll(":not([data-d3-tooltip])").remove();
47331
48713
  const width = exportDims?.width ?? container.clientWidth;
@@ -47338,6 +48720,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47338
48720
  {
47339
48721
  palette,
47340
48722
  isDark,
48723
+ // Export-only: forward the contain-fit request from mapExportDimensions so a
48724
+ // clamped/floored (off-aspect) export canvas letterboxes instead of
48725
+ // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48726
+ // keeps the global stretch-fill.
48727
+ preferContain: exportDims?.preferContain ?? false,
47341
48728
  ...activeGroupOverride !== void 0 && {
47342
48729
  activeGroup: activeGroupOverride
47343
48730
  }
@@ -47351,6 +48738,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47351
48738
  const gRegions = svg.append("g").attr("class", "dgmo-map-regions");
47352
48739
  const drawRegion = (g, r, strokeWidth) => {
47353
48740
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
48741
+ if (r.label) p.attr("data-region-name", r.label);
47354
48742
  if (r.layer !== "base") {
47355
48743
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
47356
48744
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -47385,6 +48773,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47385
48773
  gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
47386
48774
  }
47387
48775
  }
48776
+ if (layout.coastlineStyle) {
48777
+ const cs = layout.coastlineStyle;
48778
+ const maskId = "dgmo-map-water-mask";
48779
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48780
+ mask.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "white");
48781
+ const landD = layout.regions.filter((r) => r.id !== "lake").map((r) => r.d).join(" ");
48782
+ const lakeD = layout.regions.filter((r) => r.id === "lake").map((r) => r.d).join(" ");
48783
+ if (landD) mask.append("path").attr("d", landD).attr("fill", "black");
48784
+ if (lakeD) mask.append("path").attr("d", lakeD).attr("fill", "white");
48785
+ if (layout.insets.length) {
48786
+ const reach = Math.max(0, ...cs.lines.map((l) => l.d + l.thickness));
48787
+ for (const box of layout.insets) {
48788
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48789
+ mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
48790
+ }
48791
+ }
48792
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
48793
+ appendWaterLines(
48794
+ gWater,
48795
+ coastlineOuterRings(layout.regions, cs.minExtent),
48796
+ cs,
48797
+ layout.background
48798
+ );
48799
+ const byStroke = /* @__PURE__ */ new Map();
48800
+ for (const r of layout.regions) {
48801
+ const arr = byStroke.get(r.stroke);
48802
+ if (arr) arr.push(r.d);
48803
+ else byStroke.set(r.stroke, [r.d]);
48804
+ }
48805
+ for (const [stroke2, ds] of byStroke)
48806
+ gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48807
+ }
47388
48808
  if (layout.rivers.length) {
47389
48809
  const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47390
48810
  for (const r of layout.rivers) {
@@ -47393,15 +48813,61 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47393
48813
  }
47394
48814
  if (layout.insets.length) {
47395
48815
  const insetG = svg.append("g").attr("class", "dgmo-map-insets");
47396
- for (const box of layout.insets) {
48816
+ layout.insets.forEach((box, bi) => {
47397
48817
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
47398
48818
  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");
47399
- }
48819
+ if (box.contextLand) {
48820
+ const clipId = `dgmo-map-inset-clip-${bi}`;
48821
+ defs.append("clipPath").attr("id", clipId).append("path").attr("d", d);
48822
+ insetG.append("path").attr("d", box.contextLand.d).attr("fill", box.contextLand.fill).attr("clip-path", `url(#${clipId})`);
48823
+ }
48824
+ });
47400
48825
  for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
47401
- }
48826
+ if (layout.coastlineStyle) {
48827
+ const cs = layout.coastlineStyle;
48828
+ const maskId = "dgmo-map-inset-water-mask";
48829
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48830
+ for (const box of layout.insets) {
48831
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48832
+ mask.append("path").attr("d", d).attr("fill", "white");
48833
+ }
48834
+ layout.insets.forEach((box, bi) => {
48835
+ if (box.contextLand)
48836
+ mask.append("path").attr("d", box.contextLand.d).attr("fill", "black").attr("clip-path", `url(#dgmo-map-inset-clip-${bi})`);
48837
+ });
48838
+ for (const r of layout.insetRegions)
48839
+ if (r.id !== "lake")
48840
+ mask.append("path").attr("d", r.d).attr("fill", "black");
48841
+ for (const r of layout.insetRegions)
48842
+ if (r.id === "lake")
48843
+ mask.append("path").attr("d", r.d).attr("fill", "white");
48844
+ const clipId = "dgmo-map-inset-water-clip";
48845
+ const clip = defs.append("clipPath").attr("id", clipId);
48846
+ for (const box of layout.insets) {
48847
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48848
+ clip.append("path").attr("d", d);
48849
+ }
48850
+ 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})`);
48851
+ appendWaterLines(
48852
+ gInsetWater,
48853
+ coastlineOuterRings(layout.insetRegions, cs.minExtent),
48854
+ cs,
48855
+ layout.background
48856
+ );
48857
+ for (const r of layout.insetRegions)
48858
+ gInsetWater.append("path").attr("d", r.d).attr("stroke", r.stroke).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48859
+ }
48860
+ }
48861
+ const wireSync = (sel, lineNumber) => {
48862
+ if (lineNumber < 1) return;
48863
+ sel.attr("data-line-number", lineNumber);
48864
+ if (onClickItem)
48865
+ sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
48866
+ };
47402
48867
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
47403
48868
  layout.legs.forEach((leg, i) => {
47404
48869
  const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round");
48870
+ wireSync(p, leg.lineNumber);
47405
48871
  if (leg.arrow) {
47406
48872
  const id = `dgmo-map-arrow-${i}`;
47407
48873
  const s = arrowSize(leg.width);
@@ -47409,25 +48875,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47409
48875
  p.attr("marker-end", `url(#${id})`);
47410
48876
  }
47411
48877
  if (leg.label !== void 0 && leg.labelX !== void 0) {
47412
- emitText(
48878
+ const lt = emitText(
47413
48879
  gLegs,
47414
48880
  leg.labelX,
47415
48881
  leg.labelY ?? 0,
47416
48882
  leg.label,
47417
48883
  "middle",
47418
- palette.textMuted,
47419
- haloColor,
47420
- true,
48884
+ leg.labelColor ?? palette.textMuted,
48885
+ leg.labelHaloColor ?? haloColor,
48886
+ leg.labelHalo ?? true,
47421
48887
  LABEL_FONT - 1
47422
48888
  );
48889
+ wireSync(lt, leg.lineNumber);
47423
48890
  }
47424
48891
  });
48892
+ const gSpider = svg.append("g").attr("class", "dgmo-map-spider");
48893
+ for (const cl of layout.clusters) {
48894
+ if (!exportDims) {
48895
+ 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");
48896
+ }
48897
+ for (const leg of cl.legs) {
48898
+ 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");
48899
+ }
48900
+ 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");
48901
+ }
47425
48902
  const gPois = svg.append("g").attr("class", "dgmo-map-pois");
47426
48903
  for (const poi of layout.pois) {
47427
48904
  if (poi.isOrigin) {
47428
48905
  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);
47429
48906
  }
47430
48907
  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);
48908
+ if (poi.clusterId !== void 0)
48909
+ c.attr("data-cluster-member", poi.clusterId);
47431
48910
  if (poi.tags) {
47432
48911
  for (const [group, value] of Object.entries(poi.tags)) {
47433
48912
  c.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
@@ -47455,12 +48934,32 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47455
48934
  }
47456
48935
  const gLabels = svg.append("g").attr("class", "dgmo-map-labels");
47457
48936
  for (const lab of layout.labels) {
48937
+ if (lab.hidden) {
48938
+ if (exportDims) continue;
48939
+ emitText(
48940
+ gLabels,
48941
+ lab.x,
48942
+ lab.y,
48943
+ lab.text,
48944
+ lab.anchor,
48945
+ lab.color,
48946
+ lab.haloColor,
48947
+ lab.halo,
48948
+ LABEL_FONT,
48949
+ lab.italic,
48950
+ lab.letterSpacing
48951
+ ).attr("data-poi", lab.poiId ?? null).attr("data-poi-hidden", "").style("opacity", 0).style("pointer-events", "none");
48952
+ continue;
48953
+ }
47458
48954
  if (lab.leader) {
47459
48955
  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(
47460
48956
  "stroke",
47461
48957
  lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
47462
48958
  ).attr("stroke-width", lab.leaderColor ? 1 : 0.75);
47463
48959
  if (lab.poiId !== void 0) line12.attr("data-poi", lab.poiId);
48960
+ if (lab.clusterMember !== void 0)
48961
+ line12.attr("data-cluster-member", lab.clusterMember);
48962
+ wireSync(line12, lab.lineNumber);
47464
48963
  }
47465
48964
  const t = emitText(
47466
48965
  gLabels,
@@ -47471,11 +48970,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47471
48970
  lab.color,
47472
48971
  lab.haloColor,
47473
48972
  lab.halo,
47474
- LABEL_FONT
48973
+ LABEL_FONT,
48974
+ lab.italic,
48975
+ lab.letterSpacing,
48976
+ lab.lines
47475
48977
  );
47476
48978
  if (lab.poiId !== void 0) {
47477
48979
  t.attr("data-poi", lab.poiId).style("cursor", "default");
47478
48980
  }
48981
+ if (lab.clusterMember !== void 0) {
48982
+ t.attr("data-cluster-member", lab.clusterMember);
48983
+ }
48984
+ wireSync(t, lab.lineNumber);
48985
+ }
48986
+ if (!exportDims && layout.clusters.length) {
48987
+ const gBadge = svg.append("g").attr("class", "dgmo-map-cluster-badges");
48988
+ for (const cl of layout.clusters) {
48989
+ const g = gBadge.append("g").attr("data-cluster", cl.id).style("opacity", 0).style("pointer-events", "none");
48990
+ const R = 9;
48991
+ 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);
48992
+ 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);
48993
+ emitText(
48994
+ g,
48995
+ cl.cx,
48996
+ cl.cy + 3,
48997
+ String(cl.count),
48998
+ "middle",
48999
+ palette.text,
49000
+ palette.bg,
49001
+ false,
49002
+ LABEL_FONT
49003
+ );
49004
+ }
47479
49005
  }
47480
49006
  if (layout.legend) {
47481
49007
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
@@ -47512,7 +49038,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47512
49038
  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);
47513
49039
  }
47514
49040
  if (layout.subtitle) {
47515
- 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);
49041
+ 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);
47516
49042
  }
47517
49043
  if (layout.caption) {
47518
49044
  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);
@@ -47521,10 +49047,21 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47521
49047
  function renderMapForExport(container, resolved, data, palette, isDark, exportDims) {
47522
49048
  renderMap(container, resolved, data, palette, isDark, void 0, exportDims);
47523
49049
  }
47524
- function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize) {
47525
- const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color).text(text);
49050
+ function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize, italic, letterSpacing, lines) {
49051
+ const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color);
49052
+ if (lines && lines.length > 1) {
49053
+ const lineHeight = fontSize + 2;
49054
+ const startDy = -((lines.length - 1) / 2) * lineHeight;
49055
+ lines.forEach((ln, i) => {
49056
+ t.append("tspan").attr("x", x).attr("dy", i === 0 ? startDy : lineHeight).text(ln);
49057
+ });
49058
+ } else {
49059
+ t.text(text);
49060
+ }
49061
+ if (italic) t.attr("font-style", "italic");
49062
+ if (letterSpacing) t.attr("letter-spacing", letterSpacing);
47526
49063
  if (withHalo) {
47527
- t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7);
49064
+ 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);
47528
49065
  }
47529
49066
  return t;
47530
49067
  }
@@ -47541,6 +49078,56 @@ var init_renderer16 = __esm({
47541
49078
  }
47542
49079
  });
47543
49080
 
49081
+ // src/map/dimensions.ts
49082
+ var dimensions_exports = {};
49083
+ __export(dimensions_exports, {
49084
+ mapContentAspect: () => mapContentAspect,
49085
+ mapExportDimensions: () => mapExportDimensions
49086
+ });
49087
+ import { geoPath as geoPath2 } from "d3-geo";
49088
+ function mapContentAspect(resolved, data, ref = REF) {
49089
+ const { projection, fitTarget } = buildMapProjection(resolved, data);
49090
+ projection.fitSize([ref, ref], fitTarget);
49091
+ const b = geoPath2(projection).bounds(fitTarget);
49092
+ const w = b[1][0] - b[0][0];
49093
+ const h = b[1][1] - b[0][1];
49094
+ const aspect = w / h;
49095
+ return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49096
+ }
49097
+ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49098
+ const raw = mapContentAspect(resolved, data);
49099
+ const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49100
+ const width = baseWidth;
49101
+ let height = Math.round(width / clamped);
49102
+ let chromeReserve = 0;
49103
+ if (resolved.title && resolved.pois.length > 0) {
49104
+ const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
49105
+ chromeReserve += Math.max(FIT_PAD2, bannerBottom + TITLE_GAP) - FIT_PAD2;
49106
+ }
49107
+ let floored = false;
49108
+ if (height - chromeReserve < MIN_MAP_BAND) {
49109
+ height = Math.round(chromeReserve + MIN_MAP_BAND);
49110
+ floored = true;
49111
+ }
49112
+ const preferContain = clamped !== raw || floored;
49113
+ return { width, height, preferContain };
49114
+ }
49115
+ var FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
49116
+ var init_dimensions = __esm({
49117
+ "src/map/dimensions.ts"() {
49118
+ "use strict";
49119
+ init_title_constants();
49120
+ init_layout15();
49121
+ FIT_PAD2 = 24;
49122
+ TITLE_GAP = 16;
49123
+ ASPECT_MAX = 3;
49124
+ ASPECT_MIN = 0.9;
49125
+ MIN_MAP_BAND = 200;
49126
+ FALLBACK_ASPECT = 1.5;
49127
+ REF = 1e3;
49128
+ }
49129
+ });
49130
+
47544
49131
  // src/map/load-data.ts
47545
49132
  var load_data_exports = {};
47546
49133
  __export(load_data_exports, {
@@ -47599,12 +49186,17 @@ function loadMapData() {
47599
49186
  mountainRanges,
47600
49187
  naLand,
47601
49188
  naLakes,
49189
+ waterBodies,
47602
49190
  gazetteer
47603
49191
  ] = await Promise.all([
49192
+ // worldCoarse (110m) is LOAD-BEARING but NOT a render source: the world
49193
+ // basemap renders from worldDetail (50m) at all scales (resolver pins
49194
+ // basemaps.world = 'detail'). Coarse stays as the authoritative region
49195
+ // name index + dominant-landmass bbox source in resolver.ts. Do not drop it.
47604
49196
  readJson(nb, dir, FILES.worldCoarse),
47605
49197
  readJson(nb, dir, FILES.worldDetail),
47606
49198
  readJson(nb, dir, FILES.usStates),
47607
- // Lakes/rivers/mountain/NA assets are optional — older bundles may predate them.
49199
+ // Lakes/rivers/mountain/NA/water assets are optional — older bundles may predate them.
47608
49200
  readJson(nb, dir, FILES.lakes).catch(() => void 0),
47609
49201
  readJson(nb, dir, FILES.rivers).catch(() => void 0),
47610
49202
  readJson(nb, dir, FILES.mountainRanges).catch(
@@ -47612,6 +49204,7 @@ function loadMapData() {
47612
49204
  ),
47613
49205
  readJson(nb, dir, FILES.naLand).catch(() => void 0),
47614
49206
  readJson(nb, dir, FILES.naLakes).catch(() => void 0),
49207
+ readJson(nb, dir, FILES.waterBodies).catch(() => void 0),
47615
49208
  readJson(nb, dir, FILES.gazetteer)
47616
49209
  ]);
47617
49210
  return validate({
@@ -47623,7 +49216,8 @@ function loadMapData() {
47623
49216
  ...rivers && { rivers },
47624
49217
  ...mountainRanges && { mountainRanges },
47625
49218
  ...naLand && { naLand },
47626
- ...naLakes && { naLakes }
49219
+ ...naLakes && { naLakes },
49220
+ ...waterBodies && { waterBodies }
47627
49221
  });
47628
49222
  })().catch((e) => {
47629
49223
  cache = void 0;
@@ -47644,6 +49238,7 @@ var init_load_data = __esm({
47644
49238
  mountainRanges: "mountain-ranges.json",
47645
49239
  naLand: "na-land.json",
47646
49240
  naLakes: "na-lakes.json",
49241
+ waterBodies: "water-bodies.json",
47647
49242
  gazetteer: "gazetteer.json"
47648
49243
  };
47649
49244
  CANDIDATE_DIRS = [
@@ -49657,8 +51252,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
49657
51252
  const lines = splitParticipantLabel(p.label, LABEL_MAX_CHARS);
49658
51253
  if (lines.length === 0) continue;
49659
51254
  const widest = Math.max(...lines.map((l) => l.length));
49660
- const labelWidth = widest * LABEL_CHAR_WIDTH + 10;
49661
- uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth);
51255
+ const labelWidth2 = widest * LABEL_CHAR_WIDTH + 10;
51256
+ uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth2);
49662
51257
  }
49663
51258
  uniformBoxWidth = Math.min(MAX_BOX_WIDTH, uniformBoxWidth);
49664
51259
  const effectiveGap = Math.max(PARTICIPANT_GAP, uniformBoxWidth + 30);
@@ -52357,15 +53952,15 @@ function renderArcDiagram(container, parsed, palette, _isDark, onClickItem, expo
52357
53952
  textColor,
52358
53953
  onClickItem
52359
53954
  );
52360
- const neighbors = /* @__PURE__ */ new Map();
52361
- for (const node of nodes) neighbors.set(node, /* @__PURE__ */ new Set());
53955
+ const neighbors2 = /* @__PURE__ */ new Map();
53956
+ for (const node of nodes) neighbors2.set(node, /* @__PURE__ */ new Set());
52362
53957
  for (const link of links) {
52363
- neighbors.get(link.source).add(link.target);
52364
- neighbors.get(link.target).add(link.source);
53958
+ neighbors2.get(link.source).add(link.target);
53959
+ neighbors2.get(link.target).add(link.source);
52365
53960
  }
52366
53961
  const FADE_OPACITY3 = 0.1;
52367
53962
  function handleMouseEnter(hovered) {
52368
- const connected = neighbors.get(hovered);
53963
+ const connected = neighbors2.get(hovered);
52369
53964
  g.selectAll(".arc-link").each(function() {
52370
53965
  const el = d3Selection23.select(this);
52371
53966
  const src = el.attr("data-source");
@@ -54300,7 +55895,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54300
55895
  8,
54301
55896
  Math.floor(OVERLAP_WRAP_TARGET_W / OVERLAP_CH_W)
54302
55897
  );
54303
- function wrapLabel2(text, maxChars) {
55898
+ function wrapLabel3(text, maxChars) {
54304
55899
  const words = text.split(/\s+/).filter(Boolean);
54305
55900
  const lines = [];
54306
55901
  let cur = "";
@@ -54346,7 +55941,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54346
55941
  if (!ov.label) continue;
54347
55942
  const idxs = ov.sets.map((s) => vennSets.findIndex((vs) => vs.name === s));
54348
55943
  if (idxs.some((idx) => idx < 0)) continue;
54349
- const lines = wrapLabel2(ov.label, MAX_WRAP_CHARS);
55944
+ const lines = wrapLabel3(ov.label, MAX_WRAP_CHARS);
54350
55945
  wrappedOverlapLabels.set(ov, lines);
54351
55946
  const dir = predictOverlapDirRaw(idxs);
54352
55947
  const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
@@ -55784,6 +57379,7 @@ async function renderForExport(content, theme, palette, viewState, options) {
55784
57379
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55785
57380
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55786
57381
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
57382
+ const { mapExportDimensions: mapExportDimensions2 } = await Promise.resolve().then(() => (init_dimensions(), dimensions_exports));
55787
57383
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55788
57384
  const mapParsed = parseMap2(content);
55789
57385
  let mapData = options?.mapData;
@@ -55796,14 +57392,15 @@ async function renderForExport(content, theme, palette, viewState, options) {
55796
57392
  }
55797
57393
  }
55798
57394
  const mapResolved = resolveMap2(mapParsed, mapData);
55799
- const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
57395
+ const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57396
+ const container2 = createExportContainer(dims2.width, dims2.height);
55800
57397
  renderMapForExport2(
55801
57398
  container2,
55802
57399
  mapResolved,
55803
57400
  mapData,
55804
57401
  effectivePalette2,
55805
57402
  theme === "dark",
55806
- { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
57403
+ dims2
55807
57404
  );
55808
57405
  return finalizeSvgExport(container2, theme, effectivePalette2);
55809
57406
  }
@@ -56645,7 +58242,8 @@ async function render(content, options) {
56645
58242
  ...options?.c4Container !== void 0 && {
56646
58243
  c4Container: options.c4Container
56647
58244
  },
56648
- ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup }
58245
+ ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup },
58246
+ ...options?.mapData !== void 0 && { mapData: options.mapData }
56649
58247
  });
56650
58248
  if (chartType === "map") {
56651
58249
  try {
@@ -56656,7 +58254,7 @@ async function render(content, options) {
56656
58254
  Promise.resolve().then(() => (init_load_data(), load_data_exports))
56657
58255
  ]
56658
58256
  );
56659
- const data = await loadMapData2();
58257
+ const data = options?.mapData ?? await loadMapData2();
56660
58258
  diagnostics = [...resolveMap2(parseMap2(content), data).diagnostics];
56661
58259
  } catch {
56662
58260
  }