@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.cjs CHANGED
@@ -93,18 +93,18 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
93
93
  const results = [];
94
94
  for (let i = 0; i < points.length; i++) {
95
95
  const pt = points[i];
96
- const labelWidth = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
96
+ const labelWidth2 = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
97
97
  let best = null;
98
98
  const directions = [
99
99
  {
100
100
  // Above
101
101
  gen: (offset) => {
102
- const lx = pt.cx - labelWidth / 2;
102
+ const lx = pt.cx - labelWidth2 / 2;
103
103
  const ly = pt.cy - offset - labelHeight;
104
- if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
104
+ if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
105
105
  return null;
106
106
  return {
107
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
107
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
108
108
  textX: pt.cx,
109
109
  textY: ly + labelHeight / 2,
110
110
  anchor: "middle"
@@ -114,12 +114,12 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
114
114
  {
115
115
  // Below
116
116
  gen: (offset) => {
117
- const lx = pt.cx - labelWidth / 2;
117
+ const lx = pt.cx - labelWidth2 / 2;
118
118
  const ly = pt.cy + offset;
119
- if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
119
+ if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
120
120
  return null;
121
121
  return {
122
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
122
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
123
123
  textX: pt.cx,
124
124
  textY: ly + labelHeight / 2,
125
125
  anchor: "middle"
@@ -131,10 +131,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
131
131
  gen: (offset) => {
132
132
  const lx = pt.cx + offset;
133
133
  const ly = pt.cy - labelHeight / 2;
134
- if (lx + labelWidth > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
134
+ if (lx + labelWidth2 > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
135
135
  return null;
136
136
  return {
137
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
137
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
138
138
  textX: lx,
139
139
  textY: pt.cy,
140
140
  anchor: "start"
@@ -144,13 +144,13 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
144
144
  {
145
145
  // Left
146
146
  gen: (offset) => {
147
- const lx = pt.cx - offset - labelWidth;
147
+ const lx = pt.cx - offset - labelWidth2;
148
148
  const ly = pt.cy - labelHeight / 2;
149
149
  if (lx < chartBounds.left || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
150
150
  return null;
151
151
  return {
152
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
153
- textX: lx + labelWidth,
152
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
153
+ textX: lx + labelWidth2,
154
154
  textY: pt.cy,
155
155
  anchor: "end"
156
156
  };
@@ -200,10 +200,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
200
200
  }
201
201
  }
202
202
  if (!best) {
203
- const lx = pt.cx - labelWidth / 2;
203
+ const lx = pt.cx - labelWidth2 / 2;
204
204
  const ly = pt.cy - minGap - labelHeight;
205
205
  best = {
206
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
206
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
207
207
  textX: pt.cx,
208
208
  textY: ly + labelHeight / 2,
209
209
  anchor: "middle",
@@ -838,6 +838,9 @@ var init_reserved_key_registry = __esm({
838
838
  "value",
839
839
  "label",
840
840
  "style"
841
+ // `surface:` was removed in the 2026-06-02 defaults-on review — it is no longer
842
+ // a recognized metadata key (the route/edge surface feature was cut; §24B.7).
843
+ // A stray `surface: water` is no longer captured as a reserved key.
841
844
  ]);
842
845
  ORG_REGISTRY = staticRegistry([
843
846
  "color",
@@ -1900,77 +1903,266 @@ function getSegmentColors(palette, count) {
1900
1903
  (_, i) => hslToHex(Math.round((startHue + i * step) % 360), avgS, avgL)
1901
1904
  );
1902
1905
  }
1906
+ function politicalTints(palette, count, isDark) {
1907
+ if (count <= 0) return [];
1908
+ const base = isDark ? palette.surface : palette.bg;
1909
+ const c = palette.colors;
1910
+ const swatches = [
1911
+ .../* @__PURE__ */ new Set([
1912
+ c.green,
1913
+ c.yellow,
1914
+ c.orange,
1915
+ c.purple,
1916
+ c.red,
1917
+ c.teal,
1918
+ c.cyan,
1919
+ c.blue
1920
+ ])
1921
+ ];
1922
+ const bands = isDark ? POLITICAL_TINT_BANDS.dark : POLITICAL_TINT_BANDS.light;
1923
+ const out = [];
1924
+ for (const pct of bands) {
1925
+ if (out.length >= count) break;
1926
+ for (const s of swatches) out.push(mix(s, base, pct));
1927
+ }
1928
+ return out.slice(0, count);
1929
+ }
1930
+ var POLITICAL_TINT_BANDS;
1903
1931
  var init_color_utils = __esm({
1904
1932
  "src/palettes/color-utils.ts"() {
1905
1933
  "use strict";
1934
+ POLITICAL_TINT_BANDS = {
1935
+ light: [32, 48, 64, 80],
1936
+ dark: [44, 58, 72, 86]
1937
+ };
1906
1938
  }
1907
1939
  });
1908
1940
 
1909
- // src/palettes/bold.ts
1910
- var boldPalette;
1911
- var init_bold = __esm({
1912
- "src/palettes/bold.ts"() {
1941
+ // src/palettes/atlas.ts
1942
+ var atlasPalette;
1943
+ var init_atlas = __esm({
1944
+ "src/palettes/atlas.ts"() {
1913
1945
  "use strict";
1914
1946
  init_registry();
1915
- boldPalette = {
1916
- id: "bold",
1917
- name: "Bold",
1947
+ atlasPalette = {
1948
+ id: "atlas",
1949
+ name: "Atlas",
1918
1950
  light: {
1919
- bg: "#ffffff",
1920
- surface: "#f0f0f0",
1921
- overlay: "#f0f0f0",
1922
- border: "#cccccc",
1923
- text: "#000000",
1924
- textMuted: "#666666",
1925
- textOnFillLight: "#ffffff",
1926
- textOnFillDark: "#000000",
1927
- primary: "#0000ff",
1928
- secondary: "#ff00ff",
1929
- accent: "#00cccc",
1930
- destructive: "#ff0000",
1951
+ bg: "#f3ead3",
1952
+ // warm manila / parchment
1953
+ surface: "#ece0c0",
1954
+ // deeper paper (cards, panels)
1955
+ overlay: "#e8dab8",
1956
+ // popovers, dropdowns
1957
+ border: "#bcaa86",
1958
+ // muted sepia rule line
1959
+ text: "#463a26",
1960
+ // aged sepia-brown ink
1961
+ textMuted: "#7a6a4f",
1962
+ // faded annotation ink
1963
+ textOnFillLight: "#f7f1de",
1964
+ // parchment (light text on dark fills)
1965
+ textOnFillDark: "#3a2e1c",
1966
+ // deep ink (dark text on light fills)
1967
+ primary: "#5b7a99",
1968
+ // pull-down map ocean (steel-blue)
1969
+ secondary: "#7e9a6f",
1970
+ // lowland sage / celadon
1971
+ accent: "#b07f7c",
1972
+ // dusty rose
1973
+ destructive: "#b25a45",
1974
+ // brick / terracotta
1931
1975
  colors: {
1932
- red: "#ff0000",
1933
- orange: "#ff8000",
1934
- yellow: "#ffcc00",
1935
- green: "#00cc00",
1936
- blue: "#0000ff",
1937
- purple: "#cc00cc",
1938
- teal: "#008080",
1939
- cyan: "#00cccc",
1940
- gray: "#808080",
1941
- black: "#000000",
1942
- white: "#f0f0f0"
1976
+ red: "#bf6a52",
1977
+ // terracotta brick
1978
+ orange: "#cf9a5c",
1979
+ // map tan / ochre
1980
+ yellow: "#cdb35e",
1981
+ // straw / muted lemon
1982
+ green: "#7e9a6f",
1983
+ // sage / celadon lowland
1984
+ blue: "#5b7a99",
1985
+ // steel-blue ocean
1986
+ purple: "#9a7fa6",
1987
+ // dusty lilac / mauve
1988
+ teal: "#6fa094",
1989
+ // muted seafoam
1990
+ cyan: "#79a7b5",
1991
+ // shallow-water blue
1992
+ gray: "#8a7d68",
1993
+ // warm taupe
1994
+ black: "#463a26",
1995
+ // ink
1996
+ white: "#ece0c0"
1997
+ // paper
1943
1998
  }
1944
1999
  },
1945
2000
  dark: {
1946
- bg: "#000000",
1947
- surface: "#111111",
1948
- overlay: "#1a1a1a",
1949
- border: "#333333",
1950
- text: "#ffffff",
1951
- textMuted: "#aaaaaa",
1952
- textOnFillLight: "#ffffff",
1953
- textOnFillDark: "#000000",
1954
- primary: "#00ccff",
1955
- secondary: "#ff00ff",
1956
- accent: "#ffff00",
1957
- destructive: "#ff0000",
2001
+ bg: "#1e2a33",
2002
+ // deep map ocean (night globe)
2003
+ surface: "#27353f",
2004
+ // raised ocean
2005
+ overlay: "#2e3d48",
2006
+ // popovers, dropdowns
2007
+ border: "#3d4f5c",
2008
+ // depth-contour line
2009
+ text: "#e8dcc0",
2010
+ // parchment ink, inverted
2011
+ textMuted: "#a89a7d",
2012
+ // faded label
2013
+ textOnFillLight: "#f7f1de",
2014
+ // parchment
2015
+ textOnFillDark: "#1a242c",
2016
+ // deep ocean ink
2017
+ primary: "#7ba0bf",
2018
+ // brighter ocean
2019
+ secondary: "#9bb588",
2020
+ // sage, lifted
2021
+ accent: "#cf9a96",
2022
+ // dusty rose, lifted
2023
+ destructive: "#c9745c",
2024
+ // brick, lifted
1958
2025
  colors: {
1959
- red: "#ff0000",
1960
- orange: "#ff8000",
1961
- yellow: "#ffff00",
1962
- green: "#00ff00",
1963
- blue: "#0066ff",
1964
- purple: "#ff00ff",
1965
- teal: "#00cccc",
1966
- cyan: "#00ffff",
1967
- gray: "#808080",
1968
- black: "#111111",
1969
- white: "#ffffff"
2026
+ red: "#cf7a60",
2027
+ // terracotta
2028
+ orange: "#d9a96a",
2029
+ // tan / ochre
2030
+ yellow: "#d8c074",
2031
+ // straw
2032
+ green: "#9bb588",
2033
+ // sage lowland
2034
+ blue: "#7ba0bf",
2035
+ // ocean
2036
+ purple: "#b59ac0",
2037
+ // lilac / mauve
2038
+ teal: "#85b3a6",
2039
+ // seafoam
2040
+ cyan: "#92bccb",
2041
+ // shallow-water blue
2042
+ gray: "#9a8d76",
2043
+ // warm taupe
2044
+ black: "#27353f",
2045
+ // raised ocean
2046
+ white: "#e8dcc0"
2047
+ // parchment
1970
2048
  }
1971
2049
  }
1972
2050
  };
1973
- registerPalette(boldPalette);
2051
+ registerPalette(atlasPalette);
2052
+ }
2053
+ });
2054
+
2055
+ // src/palettes/blueprint.ts
2056
+ var blueprintPalette;
2057
+ var init_blueprint = __esm({
2058
+ "src/palettes/blueprint.ts"() {
2059
+ "use strict";
2060
+ init_registry();
2061
+ blueprintPalette = {
2062
+ id: "blueprint",
2063
+ name: "Blueprint",
2064
+ light: {
2065
+ bg: "#f4f8fb",
2066
+ // pale drafting white (faint cyan)
2067
+ surface: "#e6eef4",
2068
+ // drafting panel
2069
+ overlay: "#dde9f1",
2070
+ // popovers, dropdowns
2071
+ border: "#aac3d6",
2072
+ // pale blue grid line
2073
+ text: "#123a5e",
2074
+ // blueprint navy ink
2075
+ textMuted: "#4f7390",
2076
+ // faint draft note
2077
+ textOnFillLight: "#f4f8fb",
2078
+ // drafting white
2079
+ textOnFillDark: "#0c2f4d",
2080
+ // deep blueprint navy
2081
+ primary: "#1f5e8c",
2082
+ // blueprint blue
2083
+ secondary: "#5b7d96",
2084
+ // steel
2085
+ accent: "#b08a3e",
2086
+ // draftsman's ochre highlight
2087
+ destructive: "#c0504d",
2088
+ // correction red
2089
+ colors: {
2090
+ red: "#c25a4e",
2091
+ // correction red
2092
+ orange: "#c2823e",
2093
+ // ochre
2094
+ yellow: "#c2a843",
2095
+ // pencil gold
2096
+ green: "#4f8a6b",
2097
+ // drafting green
2098
+ blue: "#1f5e8c",
2099
+ // blueprint blue
2100
+ purple: "#6f5e96",
2101
+ // indigo pencil
2102
+ teal: "#3a8a8a",
2103
+ // teal
2104
+ cyan: "#3f8fb5",
2105
+ // cyan
2106
+ gray: "#7e8e98",
2107
+ // graphite
2108
+ black: "#123a5e",
2109
+ // navy ink
2110
+ white: "#e6eef4"
2111
+ // panel
2112
+ }
2113
+ },
2114
+ dark: {
2115
+ bg: "#103a5e",
2116
+ // deep blueprint blue (cyanotype ground)
2117
+ surface: "#16466e",
2118
+ // raised sheet
2119
+ overlay: "#1c5180",
2120
+ // popovers, dropdowns
2121
+ border: "#3a6f96",
2122
+ // grid line
2123
+ text: "#eaf2f8",
2124
+ // chalk white
2125
+ textMuted: "#9fc0d6",
2126
+ // faint chalk note
2127
+ textOnFillLight: "#eaf2f8",
2128
+ // chalk white
2129
+ textOnFillDark: "#0c2f4d",
2130
+ // deep blueprint navy
2131
+ primary: "#7fb8d8",
2132
+ // chalk cyan
2133
+ secondary: "#9fb8c8",
2134
+ // pale steel
2135
+ accent: "#d8c27a",
2136
+ // chalk amber
2137
+ destructive: "#e08a7a",
2138
+ // chalk correction red
2139
+ colors: {
2140
+ red: "#e0907e",
2141
+ // chalk red
2142
+ orange: "#e0ab78",
2143
+ // chalk amber
2144
+ yellow: "#e3d089",
2145
+ // chalk gold
2146
+ green: "#93c79e",
2147
+ // chalk green
2148
+ blue: "#8ec3e0",
2149
+ // chalk cyan-blue
2150
+ purple: "#b6a6d8",
2151
+ // chalk indigo
2152
+ teal: "#84c7c2",
2153
+ // chalk teal
2154
+ cyan: "#9fd6e0",
2155
+ // chalk cyan
2156
+ gray: "#aebecb",
2157
+ // chalk graphite
2158
+ black: "#16466e",
2159
+ // raised sheet
2160
+ white: "#eaf2f8"
2161
+ // chalk white
2162
+ }
2163
+ }
2164
+ };
2165
+ registerPalette(blueprintPalette);
1974
2166
  }
1975
2167
  });
1976
2168
 
@@ -2467,6 +2659,120 @@ var init_rose_pine = __esm({
2467
2659
  }
2468
2660
  });
2469
2661
 
2662
+ // src/palettes/slate.ts
2663
+ var slatePalette;
2664
+ var init_slate = __esm({
2665
+ "src/palettes/slate.ts"() {
2666
+ "use strict";
2667
+ init_registry();
2668
+ slatePalette = {
2669
+ id: "slate",
2670
+ name: "Slate",
2671
+ light: {
2672
+ bg: "#ffffff",
2673
+ // clean slide white
2674
+ surface: "#f3f5f8",
2675
+ // light cool-gray panel
2676
+ overlay: "#eaeef3",
2677
+ // popovers, dropdowns
2678
+ border: "#d4dae1",
2679
+ // hairline rule
2680
+ text: "#1f2933",
2681
+ // near-black slate (softer than pure black)
2682
+ textMuted: "#5b6672",
2683
+ // secondary label
2684
+ textOnFillLight: "#ffffff",
2685
+ // light text on dark fills
2686
+ textOnFillDark: "#1f2933",
2687
+ // dark text on light fills
2688
+ primary: "#3b6ea5",
2689
+ // confident corporate blue
2690
+ secondary: "#5b6672",
2691
+ // slate gray
2692
+ accent: "#3a9188",
2693
+ // muted teal accent
2694
+ destructive: "#c0504d",
2695
+ // brick red
2696
+ colors: {
2697
+ red: "#c0504d",
2698
+ // brick
2699
+ orange: "#cc7a33",
2700
+ // muted amber
2701
+ yellow: "#c9a227",
2702
+ // gold (not neon)
2703
+ green: "#5b9357",
2704
+ // forest / sage
2705
+ blue: "#3b6ea5",
2706
+ // corporate blue
2707
+ purple: "#7d5ba6",
2708
+ // muted violet
2709
+ teal: "#3a9188",
2710
+ // teal
2711
+ cyan: "#4f96c4",
2712
+ // steel cyan
2713
+ gray: "#7e8a97",
2714
+ // cool gray
2715
+ black: "#1f2933",
2716
+ // slate ink
2717
+ white: "#f3f5f8"
2718
+ // panel
2719
+ }
2720
+ },
2721
+ dark: {
2722
+ bg: "#161b22",
2723
+ // deep slate (keynote dark)
2724
+ surface: "#202833",
2725
+ // raised panel
2726
+ overlay: "#29323e",
2727
+ // popovers, dropdowns
2728
+ border: "#38424f",
2729
+ // divider
2730
+ text: "#e6eaef",
2731
+ // off-white
2732
+ textMuted: "#9aa5b1",
2733
+ // secondary label
2734
+ textOnFillLight: "#ffffff",
2735
+ // light text on dark fills
2736
+ textOnFillDark: "#161b22",
2737
+ // dark text on light fills
2738
+ primary: "#5b9bd5",
2739
+ // lifted corporate blue
2740
+ secondary: "#8593a3",
2741
+ // slate gray, lifted
2742
+ accent: "#45b3a3",
2743
+ // teal, lifted
2744
+ destructive: "#e07b6e",
2745
+ // brick, lifted
2746
+ colors: {
2747
+ red: "#e07b6e",
2748
+ // brick
2749
+ orange: "#e0975a",
2750
+ // amber
2751
+ yellow: "#d9bd5a",
2752
+ // gold
2753
+ green: "#74b56e",
2754
+ // forest / sage
2755
+ blue: "#5b9bd5",
2756
+ // corporate blue
2757
+ purple: "#a585c9",
2758
+ // violet
2759
+ teal: "#45b3a3",
2760
+ // teal
2761
+ cyan: "#62b0d9",
2762
+ // steel cyan
2763
+ gray: "#95a1ae",
2764
+ // cool gray
2765
+ black: "#202833",
2766
+ // raised panel
2767
+ white: "#e6eaef"
2768
+ // off-white
2769
+ }
2770
+ }
2771
+ };
2772
+ registerPalette(slatePalette);
2773
+ }
2774
+ });
2775
+
2470
2776
  // src/palettes/solarized.ts
2471
2777
  var solarizedPalette;
2472
2778
  var init_solarized = __esm({
@@ -2562,6 +2868,120 @@ var init_solarized = __esm({
2562
2868
  }
2563
2869
  });
2564
2870
 
2871
+ // src/palettes/tidewater.ts
2872
+ var tidewaterPalette;
2873
+ var init_tidewater = __esm({
2874
+ "src/palettes/tidewater.ts"() {
2875
+ "use strict";
2876
+ init_registry();
2877
+ tidewaterPalette = {
2878
+ id: "tidewater",
2879
+ name: "Tidewater",
2880
+ light: {
2881
+ bg: "#eceff0",
2882
+ // weathered sea-mist paper
2883
+ surface: "#e0e4e3",
2884
+ // worn deck panel
2885
+ overlay: "#dadfdf",
2886
+ // popovers, dropdowns
2887
+ border: "#a9b2b3",
2888
+ // muted slate rule
2889
+ text: "#18313f",
2890
+ // ship's-log navy ink
2891
+ textMuted: "#51636b",
2892
+ // faded log entry
2893
+ textOnFillLight: "#f3f5f3",
2894
+ // weathered white
2895
+ textOnFillDark: "#162c38",
2896
+ // deep navy
2897
+ primary: "#1f4e6b",
2898
+ // deep-sea navy
2899
+ secondary: "#b08a4f",
2900
+ // rope / manila tan
2901
+ accent: "#c69a3e",
2902
+ // brass
2903
+ destructive: "#c1433a",
2904
+ // signal-flag red
2905
+ colors: {
2906
+ red: "#c1433a",
2907
+ // signal-flag red
2908
+ orange: "#cc7a38",
2909
+ // weathered amber
2910
+ yellow: "#d6bf5a",
2911
+ // brass gold
2912
+ green: "#4f8a6b",
2913
+ // sea-glass green
2914
+ blue: "#1f4e6b",
2915
+ // deep-sea navy
2916
+ purple: "#6a5a8c",
2917
+ // twilight harbor
2918
+ teal: "#3d8c8c",
2919
+ // sea-glass teal
2920
+ cyan: "#4f9bb5",
2921
+ // shallow water
2922
+ gray: "#8a8d86",
2923
+ // driftwood gray
2924
+ black: "#18313f",
2925
+ // navy ink
2926
+ white: "#e0e4e3"
2927
+ // deck panel
2928
+ }
2929
+ },
2930
+ dark: {
2931
+ bg: "#0f2230",
2932
+ // night-harbor deep sea
2933
+ surface: "#16303f",
2934
+ // raised hull
2935
+ overlay: "#1d3a4a",
2936
+ // popovers, dropdowns
2937
+ border: "#2c4856",
2938
+ // rigging line
2939
+ text: "#e6ebe8",
2940
+ // weathered white
2941
+ textMuted: "#9aaab0",
2942
+ // faded label
2943
+ textOnFillLight: "#f3f5f3",
2944
+ // weathered white
2945
+ textOnFillDark: "#0f2230",
2946
+ // deep sea
2947
+ primary: "#4f9bc4",
2948
+ // lifted sea blue
2949
+ secondary: "#c9a46a",
2950
+ // rope tan, lifted
2951
+ accent: "#d9b25a",
2952
+ // brass, lifted
2953
+ destructive: "#e06a5e",
2954
+ // signal red, lifted
2955
+ colors: {
2956
+ red: "#e06a5e",
2957
+ // signal-flag red
2958
+ orange: "#df9a52",
2959
+ // amber
2960
+ yellow: "#e0c662",
2961
+ // brass gold
2962
+ green: "#6fb58c",
2963
+ // sea-glass green
2964
+ blue: "#4f9bc4",
2965
+ // sea blue
2966
+ purple: "#9486bf",
2967
+ // twilight harbor
2968
+ teal: "#5cb0ac",
2969
+ // sea-glass teal
2970
+ cyan: "#62b4cf",
2971
+ // shallow water
2972
+ gray: "#9aa39c",
2973
+ // driftwood gray
2974
+ black: "#16303f",
2975
+ // raised hull
2976
+ white: "#e6ebe8"
2977
+ // weathered white
2978
+ }
2979
+ }
2980
+ };
2981
+ registerPalette(tidewaterPalette);
2982
+ }
2983
+ });
2984
+
2565
2985
  // src/palettes/tokyo-night.ts
2566
2986
  var tokyoNightPalette;
2567
2987
  var init_tokyo_night = __esm({
@@ -2837,7 +3257,8 @@ var init_monokai = __esm({
2837
3257
  // src/palettes/index.ts
2838
3258
  var palettes_exports = {};
2839
3259
  __export(palettes_exports, {
2840
- boldPalette: () => boldPalette,
3260
+ atlasPalette: () => atlasPalette,
3261
+ blueprintPalette: () => blueprintPalette,
2841
3262
  catppuccinPalette: () => catppuccinPalette,
2842
3263
  contrastText: () => contrastText,
2843
3264
  draculaPalette: () => draculaPalette,
@@ -2858,7 +3279,9 @@ __export(palettes_exports, {
2858
3279
  rosePinePalette: () => rosePinePalette,
2859
3280
  shade: () => shade,
2860
3281
  shapeFill: () => shapeFill,
3282
+ slatePalette: () => slatePalette,
2861
3283
  solarizedPalette: () => solarizedPalette,
3284
+ tidewaterPalette: () => tidewaterPalette,
2862
3285
  tint: () => tint,
2863
3286
  tokyoNightPalette: () => tokyoNightPalette
2864
3287
  });
@@ -2868,17 +3291,21 @@ var init_palettes = __esm({
2868
3291
  "use strict";
2869
3292
  init_registry();
2870
3293
  init_color_utils();
2871
- init_bold();
3294
+ init_atlas();
3295
+ init_blueprint();
2872
3296
  init_catppuccin();
2873
3297
  init_gruvbox();
2874
3298
  init_nord();
2875
3299
  init_one_dark();
2876
3300
  init_rose_pine();
3301
+ init_slate();
2877
3302
  init_solarized();
3303
+ init_tidewater();
2878
3304
  init_tokyo_night();
2879
3305
  init_dracula();
2880
3306
  init_monokai();
2881
- init_bold();
3307
+ init_atlas();
3308
+ init_blueprint();
2882
3309
  init_catppuccin();
2883
3310
  init_dracula();
2884
3311
  init_gruvbox();
@@ -2886,9 +3313,15 @@ var init_palettes = __esm({
2886
3313
  init_nord();
2887
3314
  init_one_dark();
2888
3315
  init_rose_pine();
3316
+ init_slate();
2889
3317
  init_solarized();
3318
+ init_tidewater();
2890
3319
  init_tokyo_night();
2891
3320
  palettes = {
3321
+ atlas: atlasPalette,
3322
+ blueprint: blueprintPalette,
3323
+ slate: slatePalette,
3324
+ tidewater: tidewaterPalette,
2892
3325
  nord: nordPalette,
2893
3326
  catppuccin: catppuccinPalette,
2894
3327
  solarized: solarizedPalette,
@@ -2897,8 +3330,7 @@ var init_palettes = __esm({
2897
3330
  oneDark: oneDarkPalette,
2898
3331
  rosePine: rosePinePalette,
2899
3332
  dracula: draculaPalette,
2900
- monokai: monokaiPalette,
2901
- bold: boldPalette
3333
+ monokai: monokaiPalette
2902
3334
  };
2903
3335
  }
2904
3336
  });
@@ -3408,6 +3840,9 @@ function controlsGroupCapsuleWidth(toggles) {
3408
3840
  }
3409
3841
  return w;
3410
3842
  }
3843
+ function isAppHostedControls(config, isExport) {
3844
+ return !isExport && config.controlsHost === "app" && !!config.controlsGroup && config.controlsGroup.toggles.length > 0;
3845
+ }
3411
3846
  function buildControlsGroupLayout(config, state) {
3412
3847
  const cg = config.controlsGroup;
3413
3848
  if (!cg || cg.toggles.length === 0) return void 0;
@@ -3461,6 +3896,7 @@ function buildControlsGroupLayout(config, state) {
3461
3896
  function computeLegendLayout(config, state, containerWidth) {
3462
3897
  const { groups, controls: configControls, mode } = config;
3463
3898
  const isExport = mode === "export";
3899
+ const gated = isAppHostedControls(config, isExport);
3464
3900
  const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
3465
3901
  if (isExport && !activeGroupName) {
3466
3902
  return {
@@ -3471,7 +3907,7 @@ function computeLegendLayout(config, state, containerWidth) {
3471
3907
  pills: []
3472
3908
  };
3473
3909
  }
3474
- const controlsGroupLayout = isExport ? void 0 : buildControlsGroupLayout(config, state);
3910
+ const controlsGroupLayout = isExport || gated ? void 0 : buildControlsGroupLayout(config, state);
3475
3911
  const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0 || !!g.gradient);
3476
3912
  if (visibleGroups.length === 0 && (!configControls || configControls.length === 0) && !controlsGroupLayout) {
3477
3913
  return {
@@ -8351,8 +8787,8 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8351
8787
  const pt = points[i];
8352
8788
  const ptSize = pt.size ?? symbolSize;
8353
8789
  const minGap = ptSize / 2 + 4;
8354
- const labelWidth = pt.name.length * fontSize * 0.6 + 8;
8355
- const labelX = pt.px - labelWidth / 2;
8790
+ const labelWidth2 = pt.name.length * fontSize * 0.6 + 8;
8791
+ const labelX = pt.px - labelWidth2 / 2;
8356
8792
  let bestLabelY = 0;
8357
8793
  let bestOffset = Infinity;
8358
8794
  let placed = false;
@@ -8364,7 +8800,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8364
8800
  const candidate = {
8365
8801
  x: labelX,
8366
8802
  y: labelY,
8367
- w: labelWidth,
8803
+ w: labelWidth2,
8368
8804
  h: labelHeight
8369
8805
  };
8370
8806
  let collision = false;
@@ -8406,7 +8842,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8406
8842
  const labelRect = {
8407
8843
  x: labelX,
8408
8844
  y: bestLabelY,
8409
- w: labelWidth,
8845
+ w: labelWidth2,
8410
8846
  h: labelHeight
8411
8847
  };
8412
8848
  placedLabels.push(labelRect);
@@ -8442,7 +8878,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8442
8878
  shape: {
8443
8879
  x: labelX - bgPad,
8444
8880
  y: bestLabelY - bgPad,
8445
- width: labelWidth + bgPad * 2,
8881
+ width: labelWidth2 + bgPad * 2,
8446
8882
  height: labelHeight + bgPad * 2
8447
8883
  },
8448
8884
  style: { fill: bg },
@@ -15882,10 +16318,6 @@ function parseMap(content) {
15882
16318
  handleTag(trimmed, lineNumber);
15883
16319
  continue;
15884
16320
  }
15885
- if ((firstWord === "muted" || firstWord === "natural") && trimmed === firstWord) {
15886
- handleDirective(firstWord, "", lineNumber);
15887
- continue;
15888
- }
15889
16321
  if (DIRECTIVE_SET.has(firstWord) && !trimmed.slice(firstWord.length).trimStart().startsWith(":")) {
15890
16322
  handleDirective(
15891
16323
  firstWord,
@@ -15932,24 +16364,6 @@ function parseMap(content) {
15932
16364
  pushWarning(line12, `Duplicate directive "${key}" \u2014 last value wins.`);
15933
16365
  };
15934
16366
  switch (key) {
15935
- case "region":
15936
- dup(d.region);
15937
- d.region = value;
15938
- break;
15939
- case "projection":
15940
- dup(d.projection);
15941
- if (value && ![
15942
- "equirectangular",
15943
- "natural-earth",
15944
- "albers-usa",
15945
- "mercator"
15946
- ].includes(value))
15947
- pushWarning(
15948
- line12,
15949
- `Unknown projection "${value}" (expected equirectangular | natural-earth | albers-usa | mercator).`
15950
- );
15951
- d.projection = value;
15952
- break;
15953
16367
  case "region-metric": {
15954
16368
  dup(d.regionMetric);
15955
16369
  const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
@@ -15965,91 +16379,43 @@ function parseMap(content) {
15965
16379
  dup(d.flowMetric);
15966
16380
  d.flowMetric = value;
15967
16381
  break;
15968
- case "scale":
15969
- dup(d.scale);
15970
- {
15971
- const s = parseScale(value, line12);
15972
- if (s) d.scale = s;
15973
- }
15974
- break;
15975
- case "region-labels":
15976
- dup(d.regionLabels);
15977
- if (value && !["full", "abbrev", "off"].includes(value))
15978
- pushWarning(
15979
- line12,
15980
- `Unknown region-labels "${value}" (expected full | abbrev | off).`
15981
- );
15982
- d.regionLabels = value;
15983
- break;
15984
- case "poi-labels":
15985
- dup(d.poiLabels);
15986
- if (value && !["off", "auto", "all"].includes(value))
15987
- pushWarning(
15988
- line12,
15989
- `Unknown poi-labels "${value}" (expected off | auto | all).`
15990
- );
15991
- d.poiLabels = value;
15992
- break;
15993
- case "default-country":
15994
- dup(d.defaultCountry);
15995
- d.defaultCountry = value;
15996
- break;
15997
- case "default-state":
15998
- dup(d.defaultState);
15999
- d.defaultState = value;
16382
+ case "locale":
16383
+ dup(d.locale);
16384
+ d.locale = value;
16000
16385
  break;
16001
16386
  case "active-tag":
16002
16387
  dup(d.activeTag);
16003
16388
  d.activeTag = value;
16004
16389
  break;
16390
+ case "caption":
16391
+ dup(d.caption);
16392
+ d.caption = value;
16393
+ break;
16394
+ // ── Cosmetic `no-*` opt-outs: bare flags, idempotent (mirror `no-legend`,
16395
+ // no dup warning); each defaults the feature ON when absent. ──
16005
16396
  case "no-legend":
16006
16397
  d.noLegend = true;
16007
16398
  break;
16008
- case "no-insets":
16009
- d.noInsets = true;
16399
+ case "no-coastline":
16400
+ d.noCoastline = true;
16010
16401
  break;
16011
- case "relief":
16012
- d.relief = true;
16402
+ case "no-relief":
16403
+ d.noRelief = true;
16013
16404
  break;
16014
- case "muted":
16015
- case "natural":
16016
- if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
16017
- pushWarning(
16018
- line12,
16019
- `Conflicting basemap dress \u2014 "${d.basemapStyle}" then "${key}"; last wins.`
16020
- );
16021
- d.basemapStyle = key;
16405
+ case "no-context-labels":
16406
+ d.noContextLabels = true;
16022
16407
  break;
16023
- case "subtitle":
16024
- dup(d.subtitle);
16025
- d.subtitle = value;
16408
+ case "no-region-labels":
16409
+ d.noRegionLabels = true;
16026
16410
  break;
16027
- case "caption":
16028
- dup(d.caption);
16029
- d.caption = value;
16411
+ case "no-poi-labels":
16412
+ d.noPoiLabels = true;
16413
+ break;
16414
+ case "no-colorize":
16415
+ d.noColorize = true;
16030
16416
  break;
16031
16417
  }
16032
16418
  }
16033
- function parseScale(value, line12) {
16034
- const toks = value.split(/\s+/).filter(Boolean);
16035
- const min = Number(toks[0]);
16036
- const max = Number(toks[1]);
16037
- if (!Number.isFinite(min) || !Number.isFinite(max)) {
16038
- pushError(line12, `scale requires numeric <min> <max> (got "${value}").`);
16039
- return null;
16040
- }
16041
- const scale = { min, max };
16042
- if (toks[2] === "center") {
16043
- const c = Number(toks[3]);
16044
- if (Number.isFinite(c)) scale.center = c;
16045
- else
16046
- pushError(
16047
- line12,
16048
- `scale center requires a number (got "${toks[3] ?? ""}").`
16049
- );
16050
- }
16051
- return scale;
16052
- }
16053
16419
  function handleTag(trimmed, line12) {
16054
16420
  const m = matchTagBlockHeading(trimmed);
16055
16421
  if (!m) {
@@ -16249,13 +16615,15 @@ function parseMap(content) {
16249
16615
  pushError(line12, `Edge has an empty endpoint: "${trimmed}".`);
16250
16616
  continue;
16251
16617
  }
16252
- const meta = k === links.length - 1 ? lastSplit.meta : {};
16618
+ const isLast = k === links.length - 1;
16619
+ const meta = isLast ? lastSplit.meta : {};
16620
+ const style = links[k].style === "arc" ? "arc" : "straight";
16253
16621
  edges.push({
16254
16622
  from,
16255
16623
  to,
16256
16624
  ...links[k].label !== void 0 && { label: links[k].label },
16257
16625
  directed: links[k].directed,
16258
- style: links[k].style,
16626
+ style,
16259
16627
  meta,
16260
16628
  lineNumber: line12
16261
16629
  });
@@ -16341,22 +16709,19 @@ var init_parser12 = __esm({
16341
16709
  LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
16342
16710
  AT_RE = /(^|[\s,])at\s*:/i;
16343
16711
  DIRECTIVE_SET = /* @__PURE__ */ new Set([
16344
- "region",
16345
- "projection",
16346
16712
  "region-metric",
16347
16713
  "poi-metric",
16348
16714
  "flow-metric",
16349
- "scale",
16350
- "region-labels",
16351
- "poi-labels",
16352
- "default-country",
16353
- "default-state",
16715
+ "locale",
16354
16716
  "active-tag",
16717
+ "caption",
16355
16718
  "no-legend",
16356
- "no-insets",
16357
- "relief",
16358
- "subtitle",
16359
- "caption"
16719
+ "no-coastline",
16720
+ "no-relief",
16721
+ "no-context-labels",
16722
+ "no-region-labels",
16723
+ "no-poi-labels",
16724
+ "no-colorize"
16360
16725
  ]);
16361
16726
  }
16362
16727
  });
@@ -24277,8 +24642,8 @@ function renderKanban(container, parsed, palette, isDark, options) {
24277
24642
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24278
24643
  for (const meta of tagMeta) {
24279
24644
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(`${meta.label}: `);
24280
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24281
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24645
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24646
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24282
24647
  metaY += sCardMetaLineHeight;
24283
24648
  }
24284
24649
  for (const detail of card.details) {
@@ -24622,8 +24987,8 @@ function renderSwimlaneCard(parent, cardLayout, tagGroups, activeTagGroup, palet
24622
24987
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24623
24988
  for (const meta of tagMeta) {
24624
24989
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", palette.textMuted).text(`${meta.label}: `);
24625
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24626
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24990
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24991
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24627
24992
  metaY += sCardMetaLineHeight;
24628
24993
  }
24629
24994
  for (const detail of card.details) {
@@ -25458,8 +25823,8 @@ function classifyEREntities(tables, relationships) {
25458
25823
  }
25459
25824
  }
25460
25825
  const mmParticipants = /* @__PURE__ */ new Set();
25461
- for (const [id, neighbors] of tableStarNeighbors) {
25462
- if (neighbors.size >= 2) mmParticipants.add(id);
25826
+ for (const [id, neighbors2] of tableStarNeighbors) {
25827
+ if (neighbors2.size >= 2) mmParticipants.add(id);
25463
25828
  }
25464
25829
  const indegreeValues = Object.values(indegreeMap);
25465
25830
  const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
@@ -26130,7 +26495,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26130
26495
  controlsExpanded,
26131
26496
  onToggleDescriptions,
26132
26497
  onToggleControlsExpand,
26133
- exportMode = false
26498
+ exportMode = false,
26499
+ controlsHost
26134
26500
  } = options ?? {};
26135
26501
  d3Selection6.select(container).selectAll(":not([data-d3-tooltip])").remove();
26136
26502
  const width = exportDims?.width ?? container.clientWidth;
@@ -26148,7 +26514,11 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26148
26514
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26149
26515
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26150
26516
  const sTitleY = sctx.structural(TITLE_Y);
26151
- const sLegendHeight = sctx.structural(
26517
+ const reserveHasDescriptions = parsed.nodes.some(
26518
+ (n) => n.description && n.description.length > 0
26519
+ );
26520
+ const willRenderLegend = parsed.tagGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26521
+ const sLegendHeight = willRenderLegend ? sctx.structural(
26152
26522
  getMaxLegendReservedHeight(
26153
26523
  {
26154
26524
  groups: parsed.tagGroups,
@@ -26157,7 +26527,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26157
26527
  },
26158
26528
  width
26159
26529
  )
26160
- );
26530
+ ) : 0;
26161
26531
  const activeGroup = resolveActiveTagGroup(
26162
26532
  parsed.tagGroups,
26163
26533
  parsed.options["active-tag"],
@@ -26472,10 +26842,10 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26472
26842
  const hasDescriptions = parsed.nodes.some(
26473
26843
  (n) => n.description && n.description.length > 0
26474
26844
  );
26475
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions;
26845
+ const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26476
26846
  if (hasLegend) {
26477
26847
  let controlsGroup;
26478
- if (hasDescriptions && onToggleDescriptions) {
26848
+ if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
26479
26849
  controlsGroup = {
26480
26850
  toggles: [
26481
26851
  {
@@ -26493,7 +26863,14 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26493
26863
  groups: parsed.tagGroups,
26494
26864
  position: { placement: "top-center", titleRelation: "below-title" },
26495
26865
  mode: exportMode ? "export" : "preview",
26496
- ...controlsGroup !== void 0 && { controlsGroup }
26866
+ // Keep inactive sibling tag groups visible as collapsed pills so the user
26867
+ // can click one to flip the active colouring dimension (preview only —
26868
+ // export shows just the active group). Without this, declaring a second
26869
+ // tag group (e.g. Team) leaves it invisible whenever another group is
26870
+ // active. The app's BoxesAndLinesPreview already wires pill clicks.
26871
+ showInactivePills: true,
26872
+ ...controlsGroup !== void 0 && { controlsGroup },
26873
+ ...controlsHost !== void 0 && { controlsHost }
26497
26874
  };
26498
26875
  const legendState = {
26499
26876
  activeGroup,
@@ -27741,8 +28118,9 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27741
28118
  const containerHeight = exportDims?.height ?? (container.getBoundingClientRect().height || 600);
27742
28119
  d3Selection7.select(container).selectAll("*").remove();
27743
28120
  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);
28121
+ const appHosted = options?.controlsHost === "app";
27744
28122
  const hasControls = !!options?.onToggleColorByDepth || !!options?.onToggleDescriptions;
27745
- const hasLegend = parsed.tagGroups.length > 0 || hasControls;
28123
+ const hasLegend = parsed.tagGroups.length > 0 || hasControls && !appHosted;
27746
28124
  const fixedLegend = !isExport && hasLegend;
27747
28125
  const legendReserve = fixedLegend ? getMaxLegendReservedHeight(
27748
28126
  {
@@ -27836,7 +28214,10 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27836
28214
  }),
27837
28215
  position: { placement: "top-center", titleRelation: "below-title" },
27838
28216
  mode: options?.exportMode ? "export" : "preview",
27839
- ...controlsToggles !== void 0 && { controlsGroup: controlsToggles }
28217
+ ...controlsToggles !== void 0 && { controlsGroup: controlsToggles },
28218
+ ...options?.controlsHost !== void 0 && {
28219
+ controlsHost: options.controlsHost
28220
+ }
27840
28221
  };
27841
28222
  const legendState = {
27842
28223
  activeGroup: options?.colorByDepth ? null : activeTagGroup !== void 0 ? activeTagGroup : parsed.options["active-tag"] ?? null,
@@ -28280,8 +28661,8 @@ function computeFieldAlignX(children) {
28280
28661
  for (const child of children) {
28281
28662
  if (child.metadata["_labelField"] === "true" && child.children.length >= 2) {
28282
28663
  const labelEl = child.children[0];
28283
- const labelWidth = labelEl.label.length * CHAR_WIDTH5;
28284
- maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
28664
+ const labelWidth2 = labelEl.label.length * CHAR_WIDTH5;
28665
+ maxLabelWidth = Math.max(maxLabelWidth, labelWidth2);
28285
28666
  labelFieldCount++;
28286
28667
  }
28287
28668
  }
@@ -33245,7 +33626,7 @@ function hasRoles(node) {
33245
33626
  function computeNodeWidth2(node, expanded, options) {
33246
33627
  const badgeVal = node.computedConcurrentInvocations === 0 && node.computedInstances > 1 ? node.computedInstances : 0;
33247
33628
  const badgeLen = badgeVal > 0 ? `${badgeVal}x`.length + 2 : 0;
33248
- const labelWidth = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33629
+ const labelWidth2 = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33249
33630
  const allKeys = [];
33250
33631
  if (node.computedRps > 0) allKeys.push("RPS");
33251
33632
  if (expanded) {
@@ -33289,7 +33670,7 @@ function computeNodeWidth2(node, expanded, options) {
33289
33670
  allKeys.push("overflow");
33290
33671
  }
33291
33672
  }
33292
- if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth);
33673
+ if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth2);
33293
33674
  const maxKeyLen = Math.max(...allKeys.map((k) => k.length));
33294
33675
  let maxRowWidth = 0;
33295
33676
  if (node.computedRps > 0) {
@@ -33377,7 +33758,7 @@ function computeNodeWidth2(node, expanded, options) {
33377
33758
  truncated.length * META_CHAR_WIDTH3 + PADDING_X3
33378
33759
  );
33379
33760
  }
33380
- return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20, descWidth);
33761
+ return Math.max(MIN_NODE_WIDTH2, labelWidth2, maxRowWidth + 20, descWidth);
33381
33762
  }
33382
33763
  function computeNodeHeight2(node, expanded, options) {
33383
33764
  const propCount = countDisplayProps(node, expanded, options);
@@ -34926,8 +35307,9 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
34926
35307
  }
34927
35308
  return groups;
34928
35309
  }
34929
- function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false) {
35310
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false, controlsHost) {
34930
35311
  if (legendGroups.length === 0 && !playback) return;
35312
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34931
35313
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
34932
35314
  if (activeGroup) {
34933
35315
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
@@ -34936,14 +35318,29 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34936
35318
  name: g.name,
34937
35319
  entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
34938
35320
  }));
34939
- if (playback) {
35321
+ if (playback && !appHostedPlayback) {
34940
35322
  allGroups.push({ name: "Playback", entries: [] });
34941
35323
  }
34942
35324
  const legendConfig = {
34943
35325
  groups: allGroups,
34944
35326
  position: { placement: "top-center", titleRelation: "below-title" },
34945
35327
  mode: exportMode ? "export" : "preview",
34946
- showEmptyGroups: true
35328
+ showEmptyGroups: true,
35329
+ ...appHostedPlayback && {
35330
+ controlsHost: "app",
35331
+ controlsGroup: {
35332
+ toggles: [
35333
+ {
35334
+ id: "playback",
35335
+ type: "toggle",
35336
+ label: "Playback",
35337
+ active: true,
35338
+ onToggle: () => {
35339
+ }
35340
+ }
35341
+ ]
35342
+ }
35343
+ }
34947
35344
  };
34948
35345
  const legendState = { activeGroup };
34949
35346
  renderLegendD3(
@@ -34994,8 +35391,9 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34994
35391
  }
34995
35392
  }
34996
35393
  }
34997
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
35394
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes, controlsHost) {
34998
35395
  d3Selection11.select(container).selectAll(":not([data-d3-tooltip])").remove();
35396
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34999
35397
  const ctx = ScaleContext.identity();
35000
35398
  const sc = buildScaledConstants(ctx);
35001
35399
  const legendGroups = computeInfraLegendGroups(
@@ -35004,7 +35402,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35004
35402
  palette,
35005
35403
  layout.edges
35006
35404
  );
35007
- const hasLegend = legendGroups.length > 0 || !!playback;
35405
+ const hasLegend = legendGroups.length > 0 || !!playback && !appHostedPlayback;
35008
35406
  const fixedLegend = !exportMode && hasLegend;
35009
35407
  const legendDynamicH = hasLegend ? getMaxLegendReservedHeight(
35010
35408
  {
@@ -35148,7 +35546,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35148
35546
  isDark,
35149
35547
  activeGroup ?? null,
35150
35548
  playback ?? void 0,
35151
- exportMode
35549
+ exportMode,
35550
+ controlsHost
35152
35551
  );
35153
35552
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
35154
35553
  } else {
@@ -35161,7 +35560,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35161
35560
  isDark,
35162
35561
  activeGroup ?? null,
35163
35562
  playback ?? void 0,
35164
- exportMode
35563
+ exportMode,
35564
+ controlsHost
35165
35565
  );
35166
35566
  }
35167
35567
  }
@@ -42796,6 +43196,9 @@ function renderTechRadar(container, parsed, palette, isDark, onClickItem, export
42796
43196
  onToggle: (active) => options.onToggleListing(active)
42797
43197
  }
42798
43198
  ]
43199
+ },
43200
+ ...options.controlsHost !== void 0 && {
43201
+ controlsHost: options.controlsHost
42799
43202
  }
42800
43203
  };
42801
43204
  const legendState = {
@@ -44619,7 +45022,7 @@ function computeCycleLayout(parsed, options) {
44619
45022
  const circleNodes = parsed.options["circle-nodes"] === "true";
44620
45023
  const nodeDims = parsed.nodes.map((node) => {
44621
45024
  const hasDesc = !hideDescriptions && node.description.length > 0;
44622
- const labelWidth = Math.max(
45025
+ const labelWidth2 = Math.max(
44623
45026
  MIN_NODE_WIDTH4,
44624
45027
  node.label.length * LABEL_CHAR_W + NODE_PAD_X * 2
44625
45028
  );
@@ -44628,12 +45031,12 @@ function computeCycleLayout(parsed, options) {
44628
45031
  }
44629
45032
  if (!hasDesc) {
44630
45033
  return {
44631
- width: Math.min(MAX_NODE_WIDTH3, labelWidth),
45034
+ width: Math.min(MAX_NODE_WIDTH3, labelWidth2),
44632
45035
  height: PLAIN_NODE_HEIGHT,
44633
45036
  wrappedDesc: []
44634
45037
  };
44635
45038
  }
44636
- return chooseDescribedRectDims(node.description, labelWidth);
45039
+ return chooseDescribedRectDims(node.description, labelWidth2);
44637
45040
  });
44638
45041
  if (circleNodes) {
44639
45042
  const maxDiam = Math.max(...nodeDims.map((d) => d.width));
@@ -44829,10 +45232,10 @@ function computeCycleLayout(parsed, options) {
44829
45232
  scale
44830
45233
  };
44831
45234
  }
44832
- function chooseDescribedRectDims(description, labelWidth) {
45235
+ function chooseDescribedRectDims(description, labelWidth2) {
44833
45236
  const minW = Math.min(
44834
45237
  MAX_NODE_WIDTH3,
44835
- Math.max(MIN_NODE_WIDTH4, labelWidth, DESC_MIN_WIDTH)
45238
+ Math.max(MIN_NODE_WIDTH4, labelWidth2, DESC_MIN_WIDTH)
44836
45239
  );
44837
45240
  let best = null;
44838
45241
  let bestScore = Infinity;
@@ -45260,7 +45663,8 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45260
45663
  const hideDescriptions = (renderOptions?.hideDescriptions ?? false) || parsed.options["no-descriptions"] === "true" || viewState?.hd === true;
45261
45664
  const showDescriptions = !hideDescriptions;
45262
45665
  const hasDescriptions = parsed.nodes.some((n) => n.description.length > 0) || parsed.edges.some((e) => e.description.length > 0);
45263
- const hasLegend = hasDescriptions && !!renderOptions?.onToggleDescriptions;
45666
+ const appHostedControls = renderOptions?.controlsHost === "app";
45667
+ const hasLegend = !appHostedControls && hasDescriptions && !!renderOptions?.onToggleDescriptions;
45264
45668
  const showTitle = !!parsed.title && parsed.options["no-title"] !== "on";
45265
45669
  const legendOffset = hasLegend ? sLegendHeight : 0;
45266
45670
  const layoutHeight = height - (showTitle ? sTitleAreaHeight : 0) - legendOffset;
@@ -45297,7 +45701,10 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45297
45701
  groups: [],
45298
45702
  position: { placement: "top-center", titleRelation: "below-title" },
45299
45703
  mode: renderOptions?.exportMode ? "export" : "preview",
45300
- controlsGroup
45704
+ controlsGroup,
45705
+ ...renderOptions?.controlsHost !== void 0 && {
45706
+ controlsHost: renderOptions.controlsHost
45707
+ }
45301
45708
  };
45302
45709
  const legendState = {
45303
45710
  activeGroup: null,
@@ -45568,6 +45975,107 @@ function featureIndex(topo) {
45568
45975
  }
45569
45976
  return idx;
45570
45977
  }
45978
+ function buildAdjacency(topo) {
45979
+ const cached = adjacencyCache.get(topo);
45980
+ if (cached) return cached;
45981
+ const geometries = geomObject(topo).geometries;
45982
+ const nb = (0, import_topojson_client.neighbors)(geometries);
45983
+ const sets = /* @__PURE__ */ new Map();
45984
+ geometries.forEach((g, i) => {
45985
+ if (!g.type || g.type === "null") return;
45986
+ let set = sets.get(g.id);
45987
+ if (!set) {
45988
+ set = /* @__PURE__ */ new Set();
45989
+ sets.set(g.id, set);
45990
+ }
45991
+ for (const j of nb[i] ?? []) {
45992
+ const nid = geometries[j]?.id;
45993
+ if (nid && nid !== g.id) set.add(nid);
45994
+ }
45995
+ });
45996
+ const out = /* @__PURE__ */ new Map();
45997
+ for (const [iso, set] of sets) out.set(iso, [...set].sort());
45998
+ adjacencyCache.set(topo, out);
45999
+ return out;
46000
+ }
46001
+ function decodeFeatures(topo) {
46002
+ return geomObject(topo).geometries.map((g) => {
46003
+ const f = (0, import_topojson_client.feature)(topo, g);
46004
+ return {
46005
+ type: "Feature",
46006
+ id: g.id,
46007
+ properties: g.properties,
46008
+ geometry: f.geometry
46009
+ };
46010
+ });
46011
+ }
46012
+ function pointInRing(lon, lat, ring) {
46013
+ let inside = false;
46014
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46015
+ const xi = ring[i][0];
46016
+ const yi = ring[i][1];
46017
+ const xj = ring[j][0];
46018
+ const yj = ring[j][1];
46019
+ const intersect = yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi;
46020
+ if (intersect) inside = !inside;
46021
+ }
46022
+ return inside;
46023
+ }
46024
+ function pointOnRingEdge(lon, lat, ring) {
46025
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46026
+ const xi = ring[i][0];
46027
+ const yi = ring[i][1];
46028
+ const xj = ring[j][0];
46029
+ const yj = ring[j][1];
46030
+ if (lon < Math.min(xi, xj) - EDGE_EPS || lon > Math.max(xi, xj) + EDGE_EPS)
46031
+ continue;
46032
+ if (lat < Math.min(yi, yj) - EDGE_EPS || lat > Math.max(yi, yj) + EDGE_EPS)
46033
+ continue;
46034
+ const cross = (xj - xi) * (lat - yi) - (yj - yi) * (lon - xi);
46035
+ if (Math.abs(cross) <= EDGE_EPS) return true;
46036
+ }
46037
+ return false;
46038
+ }
46039
+ function pointInGeometry(geometry, lon, lat) {
46040
+ const g = geometry;
46041
+ if (!g) return false;
46042
+ const polys = g.type === "Polygon" ? [g.coordinates] : g.type === "MultiPolygon" ? g.coordinates : [];
46043
+ for (const rings of polys) {
46044
+ if (!rings.length) continue;
46045
+ if (pointOnRingEdge(lon, lat, rings[0])) return true;
46046
+ if (!pointInRing(lon, lat, rings[0])) continue;
46047
+ let inHole = false;
46048
+ for (let h = 1; h < rings.length; h++) {
46049
+ if (pointInRing(lon, lat, rings[h]) && !pointOnRingEdge(lon, lat, rings[h])) {
46050
+ inHole = true;
46051
+ break;
46052
+ }
46053
+ }
46054
+ if (!inHole) return true;
46055
+ }
46056
+ return false;
46057
+ }
46058
+ function regionAt(lonLat, countries, states) {
46059
+ const lon = lonLat[0];
46060
+ const lat = lonLat[1];
46061
+ let country = null;
46062
+ for (const f of countries) {
46063
+ if (pointInGeometry(f.geometry, lon, lat)) {
46064
+ country = { iso: f.id, name: f.properties.name };
46065
+ break;
46066
+ }
46067
+ }
46068
+ let state = null;
46069
+ if (country?.iso === "US" && states) {
46070
+ for (const f of states) {
46071
+ if (pointInGeometry(f.geometry, lon, lat)) {
46072
+ state = { iso: f.id, name: f.properties.name };
46073
+ break;
46074
+ }
46075
+ }
46076
+ }
46077
+ return { country, state };
46078
+ }
45571
46079
  function featureBbox(topo, geomId) {
45572
46080
  const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45573
46081
  if (!geom) return null;
@@ -45685,13 +46193,15 @@ function unionLongitudes(lons) {
45685
46193
  }
45686
46194
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45687
46195
  }
45688
- var import_topojson_client, import_d3_geo, fold, DETACH_GAP_DEG, DETACH_AREA_FRAC;
46196
+ var import_topojson_client, import_d3_geo, fold, adjacencyCache, EDGE_EPS, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45689
46197
  var init_geo = __esm({
45690
46198
  "src/map/geo.ts"() {
45691
46199
  "use strict";
45692
46200
  import_topojson_client = require("topojson-client");
45693
46201
  import_d3_geo = require("d3-geo");
45694
46202
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
46203
+ adjacencyCache = /* @__PURE__ */ new WeakMap();
46204
+ EDGE_EPS = 1e-9;
45695
46205
  DETACH_GAP_DEG = 10;
45696
46206
  DETACH_AREA_FRAC = 0.25;
45697
46207
  }
@@ -45711,6 +46221,12 @@ function looksUS(lat, lon) {
45711
46221
  if (lat < 15 || lat > 72) return false;
45712
46222
  return lon >= -180 && lon <= -64 || lon >= 172;
45713
46223
  }
46224
+ function looksNorthAmericaNeighbor(lat, lon) {
46225
+ return lat >= 14 && lat <= 72 && lon >= -141 && lon <= -52;
46226
+ }
46227
+ function isWholeSphere(bb) {
46228
+ return bb[0][0] <= -179 && bb[1][0] >= 179 && bb[0][1] <= -89 && bb[1][1] >= 89;
46229
+ }
45714
46230
  function resolveMap(parsed, data) {
45715
46231
  const diagnostics = [...parsed.diagnostics];
45716
46232
  const err = (line12, message, code) => {
@@ -45721,9 +46237,6 @@ function resolveMap(parsed, data) {
45721
46237
  };
45722
46238
  const result = {
45723
46239
  title: parsed.title,
45724
- ...parsed.directives.subtitle !== void 0 && {
45725
- subtitle: parsed.directives.subtitle
45726
- },
45727
46240
  ...parsed.directives.caption !== void 0 && {
45728
46241
  caption: parsed.directives.caption
45729
46242
  },
@@ -45733,7 +46246,7 @@ function resolveMap(parsed, data) {
45733
46246
  // renderer's job (step 4) — the resolver only carries `tags` + `tagGroups`
45734
46247
  // through; it never resolves a tag value to a palette color (#10).
45735
46248
  directives: { ...parsed.directives },
45736
- basemaps: { world: "coarse", subdivisions: [] },
46249
+ basemaps: { world: "detail", subdivisions: [] },
45737
46250
  regions: [],
45738
46251
  pois: [],
45739
46252
  edges: [],
@@ -45742,7 +46255,8 @@ function resolveMap(parsed, data) {
45742
46255
  [-180, -85],
45743
46256
  [180, 85]
45744
46257
  ],
45745
- projection: "natural-earth",
46258
+ projection: "equirectangular",
46259
+ poiFrameContainers: [],
45746
46260
  diagnostics,
45747
46261
  error: parsed.error
45748
46262
  };
@@ -45752,7 +46266,10 @@ function resolveMap(parsed, data) {
45752
46266
  ...[...countryIndex.values()].map((v) => v.name),
45753
46267
  ...[...usStateIndex.values()].map((v) => v.name)
45754
46268
  ];
45755
- const usScoped = parsed.directives.region === "us-states" || parsed.directives.defaultCountry?.toUpperCase() === "US" || parsed.regions.some((r) => {
46269
+ const localeRaw = parsed.directives.locale?.toUpperCase();
46270
+ const localeCountry = localeRaw ? localeRaw.split("-")[0] : void 0;
46271
+ const localeSubdivision = localeRaw && /^[A-Z]{2}-/.test(localeRaw) ? localeRaw : void 0;
46272
+ const usScoped = localeCountry === "US" || parsed.regions.some((r) => {
45756
46273
  const f = fold(r.name);
45757
46274
  return usStateIndex.has(f) && !countryIndex.has(f);
45758
46275
  }) || parsed.regions.some(
@@ -45903,7 +46420,7 @@ function resolveMap(parsed, data) {
45903
46420
  if (!scope)
45904
46421
  warn(
45905
46422
  line12,
45906
- `"${name}" is ambiguous \u2014 resolved to the most-populous match.`,
46423
+ `"${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.`,
45907
46424
  "W_MAP_AMBIGUOUS_NAME"
45908
46425
  );
45909
46426
  }
@@ -45916,17 +46433,21 @@ function resolveMap(parsed, data) {
45916
46433
  return fold(pos.name);
45917
46434
  };
45918
46435
  const poiCountries = [];
45919
- let anyNonUsPoi = false;
46436
+ let anyUsPoi = false;
46437
+ let anyNonNaPoi = false;
45920
46438
  const noteCountry = (iso) => {
45921
46439
  if (iso) {
45922
46440
  poiCountries.push(iso);
45923
- if (iso !== "US") anyNonUsPoi = true;
46441
+ if (iso === "US") anyUsPoi = true;
46442
+ if (iso !== "US" && iso !== "CA" && iso !== "MX") anyNonNaPoi = true;
45924
46443
  }
45925
46444
  };
45926
46445
  const deferred = [];
45927
46446
  for (const p of parsed.pois) {
45928
46447
  if (p.pos.kind === "coords") {
45929
- if (!looksUS(p.pos.lat, p.pos.lon)) anyNonUsPoi = true;
46448
+ if (looksUS(p.pos.lat, p.pos.lon)) anyUsPoi = true;
46449
+ else if (!looksNorthAmericaNeighbor(p.pos.lat, p.pos.lon))
46450
+ anyNonNaPoi = true;
45930
46451
  addResolvedPoi(p.pos.lat, p.pos.lon, p);
45931
46452
  continue;
45932
46453
  }
@@ -45944,14 +46465,15 @@ function resolveMap(parsed, data) {
45944
46465
  deferred.push(p);
45945
46466
  }
45946
46467
  }
45947
- const inferredCountry = parsed.directives.defaultCountry?.toUpperCase() ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46468
+ const inferredCountry = localeCountry ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46469
+ const inferredScope = localeSubdivision ?? inferredCountry;
45948
46470
  for (const p of deferred) {
45949
46471
  if (p.pos.kind !== "name") continue;
45950
46472
  const got = lookupName(
45951
46473
  p.pos.name,
45952
46474
  p.pos.scope,
45953
46475
  p.lineNumber,
45954
- inferredCountry,
46476
+ inferredScope,
45955
46477
  true
45956
46478
  );
45957
46479
  if (got.kind === "ok") {
@@ -46021,7 +46543,8 @@ function resolveMap(parsed, data) {
46021
46543
  const meta = sizeValue !== void 0 ? { value: sizeValue } : {};
46022
46544
  if (pos.kind === "coords") {
46023
46545
  const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
46024
- if (!looksUS(pos.lat, pos.lon)) anyNonUsPoi = true;
46546
+ if (looksUS(pos.lat, pos.lon)) anyUsPoi = true;
46547
+ else if (!looksNorthAmericaNeighbor(pos.lat, pos.lon)) anyNonNaPoi = true;
46025
46548
  if (!registry.has(id)) {
46026
46549
  registerPoi(
46027
46550
  id,
@@ -46044,7 +46567,7 @@ function resolveMap(parsed, data) {
46044
46567
  if (registry.has(f)) return f;
46045
46568
  const aliased = declaredByName.get(f);
46046
46569
  if (aliased) return aliased;
46047
- const got = lookupName(pos.name, pos.scope, line12, inferredCountry, true);
46570
+ const got = lookupName(pos.name, pos.scope, line12, inferredScope, true);
46048
46571
  if (got.kind !== "ok") return null;
46049
46572
  noteCountry(got.iso);
46050
46573
  registerPoi(
@@ -46101,9 +46624,12 @@ function resolveMap(parsed, data) {
46101
46624
  }
46102
46625
  routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
46103
46626
  }
46627
+ const hasUsContent = usSubdivisionReferenced || anyUsPoi || localeCountry === "US";
46628
+ const usOriented = !anyNonNaPoi && !regions.some(
46629
+ (r) => r.layer === "country" && !["US", "CA", "MX"].includes(r.iso)
46630
+ ) && hasUsContent;
46104
46631
  const subdivisions = [];
46105
- if (usSubdivisionReferenced || parsed.directives.region === "us-states")
46106
- subdivisions.push("us-states");
46632
+ if (usSubdivisionReferenced || usOriented) subdivisions.push("us-states");
46107
46633
  const regionBoxes = [];
46108
46634
  for (const ref of referencedRegionIds) {
46109
46635
  const bb = featureBbox(data.usStates, ref.id);
@@ -46121,17 +46647,51 @@ function resolveMap(parsed, data) {
46121
46647
  [-180, -85],
46122
46648
  [180, 85]
46123
46649
  ];
46124
- let extent2 = unioned ? pad(unioned, PAD_FRACTION) : DEFAULT_EXTENT;
46650
+ const basePad = regions.length > 0 ? REGION_PAD_FRACTION : PAD_FRACTION;
46651
+ let extent2 = unioned ? pad(unioned, basePad) : DEFAULT_EXTENT;
46652
+ const isPoiOnly = pois.length > 0 && regions.length === 0;
46653
+ const containerRegionIds = [];
46654
+ if (isPoiOnly) {
46655
+ const countries = decodeFeatures(data.worldDetail);
46656
+ const states = decodeFeatures(data.usStates);
46657
+ const seen = /* @__PURE__ */ new Set();
46658
+ const containerBoxes = [];
46659
+ for (const p of pois) {
46660
+ const { country, state } = regionAt([p.lon, p.lat], countries, states);
46661
+ const id = state?.iso ?? country?.iso;
46662
+ if (!id || seen.has(id)) continue;
46663
+ seen.add(id);
46664
+ containerRegionIds.push(id);
46665
+ const bb = state ? featureBbox(data.usStates, id) : featureBboxPrimary(data.worldCoarse, id);
46666
+ if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46667
+ }
46668
+ const containerUnion = unionExtent(containerBoxes, points);
46669
+ if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46670
+ }
46671
+ if (isPoiOnly) {
46672
+ const cx = (extent2[0][0] + extent2[1][0]) / 2;
46673
+ const cy = (extent2[0][1] + extent2[1][1]) / 2;
46674
+ const lon = extent2[1][0] - extent2[0][0];
46675
+ const lat = extent2[1][1] - extent2[0][1];
46676
+ const longer = Math.max(lon, lat);
46677
+ if (longer > 0 && longer < POI_ZOOM_FLOOR_DEG) {
46678
+ const k = POI_ZOOM_FLOOR_DEG / longer;
46679
+ const halfLon = lon * k / 2;
46680
+ const halfLat = lat * k / 2;
46681
+ extent2 = [
46682
+ [cx - halfLon, cy - halfLat],
46683
+ [cx + halfLon, cy + halfLat]
46684
+ ];
46685
+ }
46686
+ }
46125
46687
  const lonSpan = extent2[1][0] - extent2[0][0];
46126
46688
  const latSpan = extent2[1][1] - extent2[0][1];
46127
46689
  const span = Math.max(lonSpan, latSpan);
46128
46690
  const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
46129
- const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46130
46691
  let projection;
46131
- const override = parsed.directives.projection;
46132
- if (override === "equirectangular" || override === "natural-earth" || override === "albers-usa" || override === "mercator") {
46133
- projection = override;
46134
- } else if (usDominant) {
46692
+ if (isPoiOnly && usOriented && lonSpan < US_NATIONAL_LON_SPAN) {
46693
+ projection = "mercator";
46694
+ } else if (usOriented) {
46135
46695
  projection = "albers-usa";
46136
46696
  } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
46137
46697
  projection = "equirectangular";
@@ -46149,11 +46709,20 @@ function resolveMap(parsed, data) {
46149
46709
  result.edges = edges;
46150
46710
  result.routes = routes;
46151
46711
  result.basemaps = {
46152
- world: span > WORLD_SPAN ? "coarse" : "detail",
46712
+ // Tier is intentionally pinned to detail (50m) at ALL scales. Diagrammo maps
46713
+ // are presentational (palette tints, relief hachures, POI hubs), not
46714
+ // survey-grade — recognizability > generalization: 110m coarse drops the
46715
+ // Italian boot to a stump at world scale. `WORLD_SPAN` lives on only for the
46716
+ // projection decision (the `usOriented`/`span > WORLD_SPAN` chain above); it
46717
+ // no longer gates basemap resolution.
46718
+ // `worldCoarse` is still loaded — it's the authoritative name/bbox index
46719
+ // (featureIndex, featureBboxPrimary), not dead code.
46720
+ world: "detail",
46153
46721
  subdivisions
46154
46722
  };
46155
46723
  result.extent = extent2;
46156
46724
  result.projection = projection;
46725
+ result.poiFrameContainers = containerRegionIds;
46157
46726
  result.error = parsed.error ?? firstError(diagnostics);
46158
46727
  return result;
46159
46728
  }
@@ -46190,7 +46759,7 @@ function firstError(diags) {
46190
46759
  const e = diags.find((d) => d.severity === "error");
46191
46760
  return e ? formatDgmoError(e) : null;
46192
46761
  }
46193
- var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46762
+ 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;
46194
46763
  var init_resolver2 = __esm({
46195
46764
  "src/map/resolver.ts"() {
46196
46765
  "use strict";
@@ -46199,8 +46768,11 @@ var init_resolver2 = __esm({
46199
46768
  WORLD_SPAN = 90;
46200
46769
  MERCATOR_MAX_LAT = 80;
46201
46770
  PAD_FRACTION = 0.05;
46771
+ REGION_PAD_FRACTION = 0.12;
46202
46772
  WORLD_LAT_SOUTH = -58;
46203
46773
  WORLD_LAT_NORTH = 78;
46774
+ POI_ZOOM_FLOOR_DEG = 7;
46775
+ US_NATIONAL_LON_SPAN = 48;
46204
46776
  REGION_ALIASES = {
46205
46777
  // Common everyday names → the Natural-Earth display name actually shipped.
46206
46778
  "united states": "united states of america",
@@ -46278,17 +46850,305 @@ var init_resolver2 = __esm({
46278
46850
  }
46279
46851
  });
46280
46852
 
46853
+ // src/map/colorize.ts
46854
+ function assignColors(isos, adjacency) {
46855
+ const sorted = [...isos].sort();
46856
+ const byIso = /* @__PURE__ */ new Map();
46857
+ let maxIndex = -1;
46858
+ for (const iso of sorted) {
46859
+ const taken = /* @__PURE__ */ new Set();
46860
+ for (const n of adjacency.get(iso) ?? []) {
46861
+ const c = byIso.get(n);
46862
+ if (c !== void 0) taken.add(c);
46863
+ }
46864
+ let h = 0;
46865
+ while (taken.has(h)) h++;
46866
+ byIso.set(iso, h);
46867
+ if (h > maxIndex) maxIndex = h;
46868
+ }
46869
+ return { byIso, huesNeeded: maxIndex + 1 };
46870
+ }
46871
+ var init_colorize = __esm({
46872
+ "src/map/colorize.ts"() {
46873
+ "use strict";
46874
+ }
46875
+ });
46876
+
46877
+ // src/map/context-labels.ts
46878
+ function tierBand(maxSpanDeg) {
46879
+ if (maxSpanDeg >= 90) return "world";
46880
+ if (maxSpanDeg >= 20) return "continental";
46881
+ if (maxSpanDeg >= 5) return "regional";
46882
+ return "local";
46883
+ }
46884
+ function labelBudget(width, height, band) {
46885
+ const bandCap = {
46886
+ world: 6,
46887
+ continental: 5,
46888
+ regional: 4,
46889
+ local: 3
46890
+ };
46891
+ const area2 = Math.floor(Math.sqrt(Math.max(0, width * height)) / 150);
46892
+ return Math.max(0, Math.min(area2, bandCap[band]));
46893
+ }
46894
+ function waterEligible(tier, kind, band) {
46895
+ switch (band) {
46896
+ case "world":
46897
+ return tier <= 1 && (kind === "ocean" || kind === "sea");
46898
+ case "continental":
46899
+ return tier <= 2;
46900
+ case "regional":
46901
+ return tier <= 3;
46902
+ case "local":
46903
+ return tier <= 4;
46904
+ }
46905
+ }
46906
+ function insideViewport(p, width, height) {
46907
+ return !!p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && p[0] >= 0 && p[0] <= width && p[1] >= 0 && p[1] <= height;
46908
+ }
46909
+ function labelWidth(text, letterSpacing) {
46910
+ const spacing = letterSpacing > 0 ? Math.max(0, text.length - 1) * letterSpacing : 0;
46911
+ return measureLegendText(text, FONT) + spacing + 2 * PADX;
46912
+ }
46913
+ function wrapLabel2(text, letterSpacing) {
46914
+ const words = text.split(/\s+/).filter(Boolean);
46915
+ if (words.length <= 1) return [text];
46916
+ const maxLines = words.length >= 4 ? 3 : 2;
46917
+ const n = words.length;
46918
+ let best = null;
46919
+ for (let mask = 0; mask < 1 << n - 1; mask++) {
46920
+ const lines = [];
46921
+ let cur = [words[0]];
46922
+ for (let i = 1; i < n; i++) {
46923
+ if (mask & 1 << i - 1) {
46924
+ lines.push(cur.join(" "));
46925
+ cur = [words[i]];
46926
+ } else cur.push(words[i]);
46927
+ }
46928
+ lines.push(cur.join(" "));
46929
+ if (lines.length > maxLines) continue;
46930
+ const cost = Math.round(
46931
+ Math.max(...lines.map((l) => labelWidth(l, letterSpacing)))
46932
+ );
46933
+ const head = labelWidth(lines[0], letterSpacing);
46934
+ if (!best || cost < best.cost || cost === best.cost && lines.length < best.lines.length || cost === best.cost && lines.length === best.lines.length && head > best.head)
46935
+ best = { lines, cost, head };
46936
+ }
46937
+ return best?.lines ?? [text];
46938
+ }
46939
+ function rectAround(cx, cy, lines, letterSpacing) {
46940
+ const w = Math.max(...lines.map((l) => labelWidth(l, letterSpacing)));
46941
+ const h = (lines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY;
46942
+ return { x: cx - w / 2, y: cy - h / 2, w, h };
46943
+ }
46944
+ function rectFits(r, width, height) {
46945
+ return r.x >= 0 && r.y >= 0 && r.x + r.w <= width && r.y + r.h <= height;
46946
+ }
46947
+ function overlapsPadded(a, b, pad2) {
46948
+ 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;
46949
+ }
46950
+ function placeContextLabels(args) {
46951
+ const {
46952
+ projection,
46953
+ dLonSpan,
46954
+ dLatSpan,
46955
+ width,
46956
+ height,
46957
+ waterBodies,
46958
+ countries,
46959
+ palette,
46960
+ project,
46961
+ collides,
46962
+ overLand
46963
+ } = args;
46964
+ void projection;
46965
+ const band = tierBand(Math.max(dLonSpan, dLatSpan));
46966
+ const budget = labelBudget(width, height, band);
46967
+ if (budget <= 0) return [];
46968
+ const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
46969
+ const countryColor = palette.textMuted;
46970
+ const haloColor = palette.bg;
46971
+ const candidates = [];
46972
+ const center = [width / 2, height / 2];
46973
+ for (const e of waterBodies?.entries ?? []) {
46974
+ const [lat, lon, name, tier, kind, alt] = e;
46975
+ if (!waterEligible(tier, kind, band)) continue;
46976
+ const wlines = wrapLabel2(name, WATER_LETTER_SPACING);
46977
+ const anchorsLngLat = [[lon, lat]];
46978
+ for (const a of alt ?? []) anchorsLngLat.push([a[1], a[0]]);
46979
+ let best = null;
46980
+ let bestD = Infinity;
46981
+ let nearestProj = null;
46982
+ let nearestProjD = Infinity;
46983
+ for (const [aLon, aLat] of anchorsLngLat) {
46984
+ const p = project(aLon, aLat);
46985
+ if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
46986
+ const d = (p[0] - center[0]) ** 2 + (p[1] - center[1]) ** 2;
46987
+ if (d < nearestProjD) {
46988
+ nearestProjD = d;
46989
+ nearestProj = p;
46990
+ }
46991
+ if (!insideViewport(p, width, height)) continue;
46992
+ if (d < bestD) {
46993
+ bestD = d;
46994
+ best = p;
46995
+ }
46996
+ }
46997
+ if (!best && tier === 0 && nearestProj) {
46998
+ const overX = Math.max(0, -nearestProj[0], nearestProj[0] - width);
46999
+ const overY = Math.max(0, -nearestProj[1], nearestProj[1] - height);
47000
+ if (overX <= width * EDGE_CLAMP_OVERSHOOT && overY <= height * EDGE_CLAMP_OVERSHOOT) {
47001
+ const halfW = Math.max(...wlines.map((l) => labelWidth(l, WATER_LETTER_SPACING))) / 2;
47002
+ const halfH = ((wlines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY) / 2;
47003
+ const m = EDGE_CLAMP_MARGIN;
47004
+ best = [
47005
+ Math.min(Math.max(nearestProj[0], halfW + m), width - halfW - m),
47006
+ Math.min(Math.max(nearestProj[1], halfH + m), height - halfH - m)
47007
+ ];
47008
+ }
47009
+ }
47010
+ if (!best) continue;
47011
+ candidates.push({
47012
+ text: name,
47013
+ lines: wlines,
47014
+ cx: best[0],
47015
+ cy: best[1],
47016
+ italic: true,
47017
+ letterSpacing: WATER_LETTER_SPACING,
47018
+ color: waterColor,
47019
+ // Water before any country (×1000), then by tier, then kind, then name.
47020
+ sort: tier * 10 + KIND_ORDER[kind]
47021
+ });
47022
+ }
47023
+ const ranked = countries.map((c) => {
47024
+ const [x0, y0, x1, y1] = c.bbox;
47025
+ const w = x1 - x0;
47026
+ const h = y1 - y0;
47027
+ return { c, w, h, area: w * h };
47028
+ }).filter((r) => Number.isFinite(r.area) && r.area > 0).sort((a, b) => b.area - a.area);
47029
+ let ci = 0;
47030
+ for (const r of ranked) {
47031
+ const { c, w, h } = r;
47032
+ if (w > width * 0.66 || h > height * 0.66) continue;
47033
+ if (!insideViewport(c.anchor, width, height)) continue;
47034
+ const text = c.name;
47035
+ const tw = labelWidth(text, 0);
47036
+ if (tw > w || FONT + 2 * PADY > h) continue;
47037
+ candidates.push({
47038
+ text,
47039
+ lines: [text],
47040
+ cx: c.anchor[0],
47041
+ cy: c.anchor[1],
47042
+ italic: false,
47043
+ letterSpacing: 0,
47044
+ color: countryColor,
47045
+ // Always after every water body (+1e6); larger area = earlier.
47046
+ sort: 1e6 + ci++
47047
+ });
47048
+ }
47049
+ candidates.sort((a, b) => a.sort - b.sort);
47050
+ const placed = [];
47051
+ const placedRects = [];
47052
+ for (const cand of candidates) {
47053
+ if (placed.length >= budget) break;
47054
+ const rect = rectAround(cand.cx, cand.cy, cand.lines, cand.letterSpacing);
47055
+ if (!rectFits(rect, width, height)) continue;
47056
+ if (cand.italic && overLand) {
47057
+ const inset = 2;
47058
+ const top = cand.cy - (cand.lines.length - 1) / 2 * LINE_HEIGHT;
47059
+ const touchesLand = cand.lines.some((line12, li) => {
47060
+ const lw = labelWidth(line12, cand.letterSpacing);
47061
+ const x0 = cand.cx - lw / 2 + inset;
47062
+ const x1 = cand.cx + lw / 2 - inset;
47063
+ const xs = [x0, (x0 + cand.cx) / 2, cand.cx, (cand.cx + x1) / 2, x1];
47064
+ const base = top + li * LINE_HEIGHT;
47065
+ return [base, base - FONT * 0.4, base - FONT * 0.8].some(
47066
+ (y) => xs.some((x) => overLand(x, y))
47067
+ );
47068
+ });
47069
+ if (touchesLand) continue;
47070
+ }
47071
+ if (collides(rect)) continue;
47072
+ if (placedRects.some((r) => overlapsPadded(rect, r, CONTEXT_PAD))) continue;
47073
+ placedRects.push(rect);
47074
+ placed.push({
47075
+ x: cand.cx,
47076
+ y: cand.cy,
47077
+ text: cand.text,
47078
+ anchor: "middle",
47079
+ color: cand.color,
47080
+ // No halo: the bg-coloured outline reads as a ghost box behind the text
47081
+ // over the tinted water/land. Context labels are muted enough to sit
47082
+ // cleanly on the basemap without one.
47083
+ halo: false,
47084
+ haloColor,
47085
+ italic: cand.italic,
47086
+ letterSpacing: cand.letterSpacing,
47087
+ ...cand.lines.length > 1 ? { lines: cand.lines } : {},
47088
+ lineNumber: 0
47089
+ });
47090
+ }
47091
+ return placed;
47092
+ }
47093
+ var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, KIND_ORDER;
47094
+ var init_context_labels = __esm({
47095
+ "src/map/context-labels.ts"() {
47096
+ "use strict";
47097
+ init_color_utils();
47098
+ init_legend_constants();
47099
+ FONT = 11;
47100
+ LINE_HEIGHT = FONT + 2;
47101
+ PADX = 4;
47102
+ PADY = 3;
47103
+ WATER_LETTER_SPACING = 1.5;
47104
+ CONTEXT_PAD = 4;
47105
+ EDGE_CLAMP_MARGIN = 8;
47106
+ EDGE_CLAMP_OVERSHOOT = 0.35;
47107
+ KIND_ORDER = {
47108
+ ocean: 0,
47109
+ sea: 1,
47110
+ gulf: 2,
47111
+ bay: 3,
47112
+ strait: 4,
47113
+ channel: 5,
47114
+ sound: 6
47115
+ };
47116
+ }
47117
+ });
47118
+
46281
47119
  // src/map/layout.ts
46282
47120
  function geomObject2(topo) {
46283
47121
  const key = Object.keys(topo.objects)[0];
46284
47122
  return topo.objects[key];
46285
47123
  }
47124
+ function mergeFeatures(a, b) {
47125
+ const polysOf = (f) => {
47126
+ const g = f.geometry;
47127
+ if (!g) return null;
47128
+ if (g.type === "Polygon") return [g.coordinates];
47129
+ if (g.type === "MultiPolygon") return g.coordinates;
47130
+ return null;
47131
+ };
47132
+ const pa = polysOf(a);
47133
+ const pb = polysOf(b);
47134
+ if (!pa || !pb) return a;
47135
+ return {
47136
+ ...a,
47137
+ geometry: { type: "MultiPolygon", coordinates: [...pa, ...pb] }
47138
+ };
47139
+ }
46286
47140
  function decodeLayer(topo) {
47141
+ const cached = decodeCache.get(topo);
47142
+ if (cached) return cached;
46287
47143
  const out = /* @__PURE__ */ new Map();
46288
47144
  for (const g of geomObject2(topo).geometries) {
46289
47145
  const f = (0, import_topojson_client2.feature)(topo, g);
46290
- out.set(g.id, { ...f, id: g.id });
47146
+ if (!f.geometry) continue;
47147
+ const tagged = { ...f, id: g.id };
47148
+ const existing = out.get(g.id);
47149
+ out.set(g.id, existing ? mergeFeatures(existing, tagged) : tagged);
46291
47150
  }
47151
+ decodeCache.set(topo, out);
46292
47152
  return out;
46293
47153
  }
46294
47154
  function projectionFor(family) {
@@ -46297,9 +47157,12 @@ function projectionFor(family) {
46297
47157
  return usConusProjection();
46298
47158
  case "mercator":
46299
47159
  return (0, import_d3_geo2.geoMercator)();
47160
+ case "equal-earth":
47161
+ return (0, import_d3_geo2.geoEqualEarth)();
47162
+ case "equirectangular":
47163
+ return (0, import_d3_geo2.geoEquirectangular)();
46300
47164
  case "natural-earth":
46301
47165
  return (0, import_d3_geo2.geoNaturalEarth1)();
46302
- case "equirectangular":
46303
47166
  default:
46304
47167
  return (0, import_d3_geo2.geoEquirectangular)();
46305
47168
  }
@@ -46318,13 +47181,11 @@ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46318
47181
  isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT
46319
47182
  );
46320
47183
  }
46321
- function layoutMap(resolved, data, size, opts) {
46322
- const { palette, isDark } = opts;
46323
- const { width, height } = size;
47184
+ function buildMapProjection(resolved, data) {
46324
47185
  const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
46325
- const usCrisp = resolved.projection === "albers-usa" && wantsUsStates && !!data.naLand;
47186
+ const usCrisp = (resolved.projection === "albers-usa" || resolved.projection === "mercator") && wantsUsStates && !!data.naLand;
46326
47187
  const worldTopo = usCrisp ? data.worldDetail : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46327
- const worldLayer = decodeLayer(worldTopo);
47188
+ const worldLayer = new Map(decodeLayer(worldTopo));
46328
47189
  if (usCrisp && data.naLand) {
46329
47190
  const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
46330
47191
  const crisp = decodeLayer(data.naLand);
@@ -46333,16 +47194,109 @@ function layoutMap(resolved, data, size, opts) {
46333
47194
  if (!base) continue;
46334
47195
  const [[bw, bs], [be, bn]] = (0, import_d3_geo2.geoBounds)(base);
46335
47196
  if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
46336
- worldLayer.set(iso, cf);
47197
+ worldLayer.set(iso, { ...cf, properties: base.properties });
46337
47198
  }
46338
47199
  }
46339
47200
  const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
47201
+ const extentOutline = () => {
47202
+ const [[w, s], [e, n]] = resolved.extent;
47203
+ const N = 16;
47204
+ const coords = [];
47205
+ for (let i = 0; i <= N; i++) {
47206
+ const t = i / N;
47207
+ const lon = w + (e - w) * t;
47208
+ const lat = s + (n - s) * t;
47209
+ coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
47210
+ }
47211
+ return {
47212
+ type: "Feature",
47213
+ properties: {},
47214
+ geometry: { type: "MultiPoint", coordinates: coords }
47215
+ };
47216
+ };
47217
+ let fitFeatures;
47218
+ if (resolved.projection === "albers-usa" && usLayer) {
47219
+ fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
47220
+ const neighborPoints = resolved.pois.filter((p) => !inAlaska(p.lon, p.lat) && !inHawaii(p.lon, p.lat)).map((p) => [p.lon, p.lat]);
47221
+ if (neighborPoints.length > 0) {
47222
+ fitFeatures.push({
47223
+ type: "Feature",
47224
+ properties: {},
47225
+ geometry: { type: "MultiPoint", coordinates: neighborPoints }
47226
+ });
47227
+ }
47228
+ for (const r of resolved.regions) {
47229
+ if (r.layer === "country" && (r.iso === "CA" || r.iso === "MX")) {
47230
+ const cf = worldLayer.get(r.iso);
47231
+ if (cf) fitFeatures.push(cf);
47232
+ }
47233
+ }
47234
+ } else {
47235
+ fitFeatures = [extentOutline()];
47236
+ }
47237
+ const fitTarget = { type: "FeatureCollection", features: fitFeatures };
47238
+ const projection = projectionFor(resolved.projection);
47239
+ if (resolved.projection !== "albers-usa") {
47240
+ let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
47241
+ if (centerLon > 180) centerLon -= 360;
47242
+ projection.rotate([-centerLon, 0]);
47243
+ }
47244
+ const fitGB = (0, import_d3_geo2.geoBounds)(fitTarget);
47245
+ const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
47246
+ return {
47247
+ projection,
47248
+ fitTarget,
47249
+ fitIsGlobal,
47250
+ worldLayer,
47251
+ usLayer,
47252
+ usCrisp,
47253
+ wantsUsStates,
47254
+ worldTopo
47255
+ };
47256
+ }
47257
+ function parsePathRings(d) {
47258
+ const rings = [];
47259
+ let cur = [];
47260
+ const re = /([MLZ])([^MLZ]*)/g;
47261
+ let m;
47262
+ while (m = re.exec(d)) {
47263
+ if (m[1] === "Z") {
47264
+ if (cur.length) rings.push(cur);
47265
+ cur = [];
47266
+ continue;
47267
+ }
47268
+ if (m[1] === "M" && cur.length) {
47269
+ rings.push(cur);
47270
+ cur = [];
47271
+ }
47272
+ const nums = m[2].split(/[ ,]+/).map(Number);
47273
+ for (let i = 0; i + 1 < nums.length; i += 2) {
47274
+ const x = nums[i];
47275
+ const y = nums[i + 1];
47276
+ if (Number.isFinite(x) && Number.isFinite(y)) cur.push([x, y]);
47277
+ }
47278
+ }
47279
+ if (cur.length) rings.push(cur);
47280
+ return rings;
47281
+ }
47282
+ function layoutMap(resolved, data, size, opts) {
47283
+ const { palette, isDark } = opts;
47284
+ const { width, height } = size;
47285
+ const {
47286
+ projection,
47287
+ fitTarget,
47288
+ fitIsGlobal,
47289
+ worldLayer,
47290
+ usLayer,
47291
+ usCrisp,
47292
+ worldTopo
47293
+ } = buildMapProjection(resolved, data);
46340
47294
  const usContext = usLayer !== null;
46341
47295
  const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46342
47296
  const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
46343
- const scaleOverride = resolved.directives.scale;
46344
- const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46345
- const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
47297
+ const allNonNegative = values.length > 0 && values.every((v) => v >= 0);
47298
+ const rampMin = allNonNegative ? 0 : Math.min(...values);
47299
+ const rampMax = Math.max(...values);
46346
47300
  const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46347
47301
  const hasRamp = values.length > 0;
46348
47302
  const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
@@ -46363,7 +47317,7 @@ function layoutMap(resolved, data, size, opts) {
46363
47317
  activeGroup = VALUE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46364
47318
  }
46365
47319
  const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
46366
- const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
47320
+ const mutedBasemap = activeGroup !== null;
46367
47321
  const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46368
47322
  const water = mapBackgroundColor(palette, isDark, mutedBasemap);
46369
47323
  const lakeStroke = mix(regionStroke, water, 45);
@@ -46372,6 +47326,39 @@ function layoutMap(resolved, data, size, opts) {
46372
47326
  palette.bg,
46373
47327
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46374
47328
  );
47329
+ const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && resolved.tagGroups.length === 0;
47330
+ const colorByIso = /* @__PURE__ */ new Map();
47331
+ if (colorizeActive) {
47332
+ const adjacency = /* @__PURE__ */ new Map();
47333
+ const addEdges = (src) => {
47334
+ for (const [iso, ns] of src) {
47335
+ const cur = adjacency.get(iso);
47336
+ if (cur) cur.push(...ns);
47337
+ else adjacency.set(iso, [...ns]);
47338
+ }
47339
+ };
47340
+ addEdges(buildAdjacency(worldTopo));
47341
+ if (usLayer) {
47342
+ addEdges(buildAdjacency(data.usStates));
47343
+ for (const [country, states] of Object.entries(FOREIGN_BORDER)) {
47344
+ const cn = adjacency.get(country);
47345
+ if (!cn) continue;
47346
+ for (const st of states) {
47347
+ const sn = adjacency.get(st);
47348
+ if (!sn) continue;
47349
+ cn.push(st);
47350
+ sn.push(country);
47351
+ }
47352
+ }
47353
+ }
47354
+ const { byIso, huesNeeded } = assignColors(
47355
+ [...adjacency.keys()],
47356
+ adjacency
47357
+ );
47358
+ const tints = politicalTints(palette, huesNeeded, isDark);
47359
+ for (const [iso, idx] of byIso) colorByIso.set(iso, tints[idx]);
47360
+ }
47361
+ const colorizeStroke = (fill2) => mix(fill2, palette.text, 35);
46375
47362
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46376
47363
  const fillForValue = (s) => {
46377
47364
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
@@ -46407,43 +47394,15 @@ function layoutMap(resolved, data, size, opts) {
46407
47394
  if (activeIsScore) {
46408
47395
  return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46409
47396
  }
47397
+ if (colorizeActive) return (r.iso && colorByIso.get(r.iso)) ?? neutralFill;
46410
47398
  return tagFill(r.tags, activeGroup) ?? neutralFill;
46411
47399
  };
46412
47400
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
46413
- const extentOutline = () => {
46414
- const [[w, s], [e, n]] = resolved.extent;
46415
- const N = 16;
46416
- const coords = [];
46417
- for (let i = 0; i <= N; i++) {
46418
- const t = i / N;
46419
- const lon = w + (e - w) * t;
46420
- const lat = s + (n - s) * t;
46421
- coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
46422
- }
46423
- return {
46424
- type: "Feature",
46425
- properties: {},
46426
- geometry: { type: "MultiPoint", coordinates: coords }
46427
- };
46428
- };
46429
- let fitFeatures;
46430
- if (resolved.projection === "albers-usa" && usLayer) {
46431
- fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
46432
- } else {
46433
- fitFeatures = [extentOutline()];
46434
- }
46435
- const fitTarget = { type: "FeatureCollection", features: fitFeatures };
46436
- const projection = projectionFor(resolved.projection);
46437
- if (resolved.projection !== "albers-usa") {
46438
- let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
46439
- if (centerLon > 180) centerLon -= 360;
46440
- projection.rotate([-centerLon, 0]);
46441
- }
46442
- const TITLE_GAP = 16;
47401
+ const TITLE_GAP2 = 16;
46443
47402
  let topPad = FIT_PAD;
46444
47403
  if (resolved.title && resolved.pois.length > 0) {
46445
47404
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
46446
- topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
47405
+ topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
46447
47406
  }
46448
47407
  const fitBox = [
46449
47408
  [FIT_PAD, topPad],
@@ -46453,12 +47412,10 @@ function layoutMap(resolved, data, size, opts) {
46453
47412
  ]
46454
47413
  ];
46455
47414
  projection.fitExtent(fitBox, fitTarget);
46456
- const fitGB = (0, import_d3_geo2.geoBounds)(fitTarget);
46457
- const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46458
47415
  let path;
46459
47416
  let project;
46460
47417
  let stretchParams = null;
46461
- if (fitIsGlobal) {
47418
+ if (fitIsGlobal && !opts.preferContain) {
46462
47419
  const cb = (0, import_d3_geo2.geoPath)(projection).bounds(fitTarget);
46463
47420
  const bx0 = cb[0][0];
46464
47421
  const by0 = cb[0][1];
@@ -46500,7 +47457,9 @@ function layoutMap(resolved, data, size, opts) {
46500
47457
  const insets = [];
46501
47458
  const insetRegions = [];
46502
47459
  const insetLabelSeeds = [];
46503
- if (resolved.projection === "albers-usa" && usLayer && !resolved.directives.noInsets) {
47460
+ const akRef = resolved.regions.some((r) => r.iso === "US-AK") || resolved.pois.some((p) => inAlaska(p.lon, p.lat));
47461
+ const hiRef = resolved.regions.some((r) => r.iso === "US-HI") || resolved.pois.some((p) => inHawaii(p.lon, p.lat));
47462
+ if (resolved.projection === "albers-usa" && usLayer && (akRef || hiRef)) {
46504
47463
  const PAD = 8;
46505
47464
  const GAP = 12;
46506
47465
  const yB = height - FIT_PAD;
@@ -46565,8 +47524,18 @@ function layoutMap(resolved, data, size, opts) {
46565
47524
  );
46566
47525
  const d = (0, import_d3_geo2.geoPath)(proj)(f) ?? "";
46567
47526
  if (!d) return xr;
47527
+ let contextLand;
47528
+ if (iso === "US-AK") {
47529
+ const can = worldLayer.get("CA");
47530
+ const cd = can ? (0, import_d3_geo2.geoPath)(proj)(can) ?? "" : "";
47531
+ if (cd)
47532
+ contextLand = {
47533
+ d: cd,
47534
+ fill: colorizeActive ? colorByIso.get("CA") ?? foreignFill : foreignFill
47535
+ };
47536
+ }
46568
47537
  const r = regionById.get(iso);
46569
- let fill2 = neutralFill;
47538
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? neutralFill : neutralFill;
46570
47539
  let lineNumber = -1;
46571
47540
  if (r?.layer === "us-state") {
46572
47541
  fill2 = regionFill(r);
@@ -46585,13 +47554,14 @@ function layoutMap(resolved, data, size, opts) {
46585
47554
  ],
46586
47555
  // The FITTED inset projection (just fit to this box) — captured so the
46587
47556
  // geo-query can invert pixels inside the frame back to AK/HI coords.
46588
- projection: proj
47557
+ projection: proj,
47558
+ ...contextLand && { contextLand }
46589
47559
  });
46590
47560
  insetRegions.push({
46591
47561
  id: iso,
46592
47562
  d,
46593
47563
  fill: fill2,
46594
- stroke: regionStroke,
47564
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46595
47565
  lineNumber,
46596
47566
  layer: "us-state",
46597
47567
  ...r?.value !== void 0 && { value: r.value },
@@ -46604,13 +47574,16 @@ function layoutMap(resolved, data, size, opts) {
46604
47574
  }
46605
47575
  return xr;
46606
47576
  };
46607
- const akRight = placeInset(
46608
- "US-AK",
46609
- alaskaProjection(),
46610
- FIT_PAD,
46611
- width * 0.15
46612
- );
46613
- placeInset("US-HI", hawaiiProjection(), akRight + 24, width * 0.1);
47577
+ let akRight = FIT_PAD;
47578
+ if (akRef)
47579
+ akRight = placeInset("US-AK", alaskaProjection(), FIT_PAD, width * 0.15);
47580
+ if (hiRef)
47581
+ placeInset(
47582
+ "US-HI",
47583
+ hawaiiProjection(),
47584
+ akRef ? akRight + 24 : FIT_PAD,
47585
+ width * 0.1
47586
+ );
46614
47587
  }
46615
47588
  const conusFit = resolved.projection === "albers-usa" && !!usLayer;
46616
47589
  const classifyExtent = conusFit ? (0, import_d3_geo2.geoBounds)(fitTarget) : resolved.extent;
@@ -46626,15 +47599,24 @@ function layoutMap(resolved, data, size, opts) {
46626
47599
  };
46627
47600
  const ringOverlapsView = (ring) => {
46628
47601
  let loMin = Infinity, loMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
47602
+ const lons = [];
46629
47603
  for (const [rawLon] of ring) {
46630
47604
  const lon = normLon(rawLon);
47605
+ lons.push(lon);
46631
47606
  if (lon < loMin) loMin = lon;
46632
47607
  if (lon > loMax) loMax = lon;
46633
47608
  if (rawLon < rawMin) rawMin = rawLon;
46634
47609
  if (rawLon > rawMax) rawMax = rawLon;
46635
47610
  }
46636
- if (loMax - loMin > 270) return false;
46637
- if (rawMax - rawMin > 180 && loMax - loMin < 90) return false;
47611
+ lons.sort((a, b) => a - b);
47612
+ let maxGap = 0;
47613
+ for (let i = 1; i < lons.length; i++)
47614
+ maxGap = Math.max(maxGap, lons[i] - lons[i - 1]);
47615
+ if (lons.length > 1)
47616
+ maxGap = Math.max(maxGap, lons[0] + 360 - lons[lons.length - 1]);
47617
+ const occupiedArc = 360 - maxGap;
47618
+ if (occupiedArc > 270) return false;
47619
+ if (rawMax - rawMin > 180 && occupiedArc < 90) return false;
46638
47620
  let px0 = Infinity, py0 = Infinity, px1 = -Infinity, py1 = -Infinity, anyFinite = false;
46639
47621
  for (const [lon, lat] of ring) {
46640
47622
  const p = project(lon, lat);
@@ -46707,7 +47689,7 @@ function layoutMap(resolved, data, size, opts) {
46707
47689
  const regions = [];
46708
47690
  const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
46709
47691
  for (const [iso, f] of layerFeatures) {
46710
- if (layerKind === "us-state" && usContext && INSET_STATES.has(iso))
47692
+ if (layerKind === "us-state" && usContext && resolved.projection === "albers-usa" && INSET_STATES.has(iso))
46711
47693
  continue;
46712
47694
  if (layerKind === "country" && usContext && iso === "US") continue;
46713
47695
  if (layerKind === "country" && iso === "AQ" && !regionById.has("AQ"))
@@ -46719,7 +47701,8 @@ function layoutMap(resolved, data, size, opts) {
46719
47701
  if (!d) continue;
46720
47702
  const isThisLayer = r?.layer === layerKind;
46721
47703
  const isForeign = layerKind === "country" && usContext && iso !== "US";
46722
- let fill2 = isForeign ? foreignFill : neutralFill;
47704
+ const baseFill = isForeign ? foreignFill : neutralFill;
47705
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? baseFill : baseFill;
46723
47706
  let label;
46724
47707
  let lineNumber = -1;
46725
47708
  let layer = "base";
@@ -46728,12 +47711,14 @@ function layoutMap(resolved, data, size, opts) {
46728
47711
  lineNumber = r.lineNumber;
46729
47712
  layer = layerKind;
46730
47713
  label = r.name;
47714
+ } else {
47715
+ label = f.properties?.name;
46731
47716
  }
46732
47717
  regions.push({
46733
47718
  id: iso,
46734
47719
  d,
46735
47720
  fill: fill2,
46736
- stroke: regionStroke,
47721
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46737
47722
  lineNumber,
46738
47723
  layer,
46739
47724
  ...label !== void 0 && { label },
@@ -46761,9 +47746,41 @@ function layoutMap(resolved, data, size, opts) {
46761
47746
  });
46762
47747
  }
46763
47748
  }
47749
+ const pointInRings = (px, py, rings) => {
47750
+ let inside = false;
47751
+ for (const ring of rings) {
47752
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
47753
+ const [xi, yi] = ring[i];
47754
+ const [xj, yj] = ring[j];
47755
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
47756
+ inside = !inside;
47757
+ }
47758
+ }
47759
+ return inside;
47760
+ };
47761
+ const fillHitTargets = [...regions, ...insetRegions].map((r) => ({
47762
+ fill: r.fill,
47763
+ rings: parsePathRings(r.d)
47764
+ }));
47765
+ const fillAt = (x, y) => {
47766
+ let hit = water;
47767
+ for (const t of fillHitTargets)
47768
+ if (pointInRings(x, y, t.rings)) hit = t.fill;
47769
+ return hit;
47770
+ };
47771
+ const labelOnFill = (fill2) => {
47772
+ const color = contrastRatio(fill2, palette.textOnFillDark) >= contrastRatio(fill2, palette.textOnFillLight) ? palette.textOnFillDark : palette.textOnFillLight;
47773
+ const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47774
+ return {
47775
+ color,
47776
+ halo: contrastRatio(fill2, color) < REGION_LABEL_HALO_RATIO,
47777
+ haloColor
47778
+ };
47779
+ };
47780
+ const reliefAllowed = resolved.directives.noRelief !== true;
46764
47781
  const relief = [];
46765
47782
  let reliefHatch = null;
46766
- if (resolved.directives.relief === true && data.mountainRanges) {
47783
+ if (reliefAllowed && data.mountainRanges) {
46767
47784
  for (const [, f] of decodeLayer(data.mountainRanges)) {
46768
47785
  const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
46769
47786
  if (!viewF) continue;
@@ -46779,16 +47796,32 @@ function layoutMap(resolved, data, size, opts) {
46779
47796
  if (relief.length) {
46780
47797
  const darkTone = isDark ? palette.bg : palette.text;
46781
47798
  const lightTone = isDark ? palette.text : palette.bg;
46782
- const landLum = relativeLuminance(neutralFill);
47799
+ const reliefLandRef = colorizeActive ? isDark ? palette.surface : palette.bg : neutralFill;
47800
+ const landLum = relativeLuminance(reliefLandRef);
46783
47801
  const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
46784
47802
  reliefHatch = {
46785
- color: mix(tone, neutralFill, RELIEF_HATCH_STRENGTH),
47803
+ color: mix(tone, reliefLandRef, RELIEF_HATCH_STRENGTH),
46786
47804
  spacing: RELIEF_HATCH_SPACING,
46787
47805
  width: RELIEF_HATCH_WIDTH
46788
47806
  };
46789
47807
  }
46790
47808
  }
46791
- const riverColor = mix(water, regionStroke, 16);
47809
+ let coastlineStyle = null;
47810
+ if (resolved.directives.noCoastline !== true) {
47811
+ const minDim = Math.min(width, height);
47812
+ coastlineStyle = {
47813
+ color: mix(regionStroke, water, COASTLINE_STROKE_MIX),
47814
+ // N equal-width rings: distance steps outward by COASTLINE_STEP; opacity
47815
+ // fades linearly from NEAR (innermost) to FAR (outermost).
47816
+ lines: Array.from({ length: COASTLINE_RING_COUNT }, (_, k) => ({
47817
+ d: (COASTLINE_D0 + k * COASTLINE_STEP) * minDim,
47818
+ thickness: COASTLINE_THICKNESS * minDim,
47819
+ opacity: COASTLINE_OPACITY_NEAR + (COASTLINE_OPACITY_FAR - COASTLINE_OPACITY_NEAR) * k / (COASTLINE_RING_COUNT - 1)
47820
+ })),
47821
+ minExtent: (isGlobalView ? COASTLINE_MIN_EXTENT_GLOBAL : COASTLINE_MIN_EXTENT) * minDim
47822
+ };
47823
+ }
47824
+ const riverColor = mix(palette.colors.blue, water, 32);
46792
47825
  const rivers = [];
46793
47826
  if (data.rivers) {
46794
47827
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -46844,38 +47877,108 @@ function layoutMap(resolved, data, size, opts) {
46844
47877
  const xy = project(p.lon, p.lat);
46845
47878
  if (xy) projected.push({ p, xy });
46846
47879
  }
46847
- const coloGroups = /* @__PURE__ */ new Map();
47880
+ const placePoi = (e, cx, cy, clusterId) => {
47881
+ const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
47882
+ poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
47883
+ const num = routeNumberById.get(e.p.id);
47884
+ pois.push({
47885
+ id: e.p.id,
47886
+ cx,
47887
+ cy,
47888
+ r: radiusFor(e.p),
47889
+ fill: fill2,
47890
+ stroke: stroke2,
47891
+ lineNumber: e.p.lineNumber,
47892
+ implicit: !!e.p.implicit,
47893
+ isOrigin: originIds.has(e.p.id),
47894
+ ...num !== void 0 && { routeNumber: num },
47895
+ ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags },
47896
+ ...clusterId !== void 0 && { clusterId }
47897
+ });
47898
+ };
47899
+ const clusters = [];
47900
+ const connected = /* @__PURE__ */ new Set();
47901
+ for (const e of resolved.edges) {
47902
+ connected.add(e.fromId);
47903
+ connected.add(e.toId);
47904
+ }
47905
+ for (const rt of resolved.routes) {
47906
+ rt.stopIds.forEach((id) => connected.add(id));
47907
+ }
47908
+ const radiusOf = (e) => radiusFor(e.p);
46848
47909
  for (const e of projected) {
46849
- const key = `${Math.round(e.xy[0] / COLO_EPS)},${Math.round(e.xy[1] / COLO_EPS)}`;
46850
- const arr = coloGroups.get(key);
46851
- if (arr) arr.push(e);
46852
- else coloGroups.set(key, [e]);
46853
- }
46854
- for (const group of coloGroups.values()) {
46855
- group.forEach((e, i) => {
46856
- let cx = e.xy[0];
46857
- let cy = e.xy[1];
46858
- if (group.length > 1) {
46859
- const ang = i * GOLDEN_ANGLE;
46860
- cx += Math.cos(ang) * COLO_R;
46861
- cy += Math.sin(ang) * COLO_R;
46862
- }
46863
- const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
46864
- poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
46865
- const num = routeNumberById.get(e.p.id);
46866
- pois.push({
46867
- id: e.p.id,
46868
- cx,
46869
- cy,
46870
- r: radiusFor(e.p),
46871
- fill: fill2,
46872
- stroke: stroke2,
46873
- lineNumber: e.p.lineNumber,
46874
- implicit: !!e.p.implicit,
46875
- isOrigin: originIds.has(e.p.id),
46876
- ...num !== void 0 && { routeNumber: num },
46877
- ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags }
46878
- });
47910
+ if (connected.has(e.p.id)) placePoi(e, e.xy[0], e.xy[1]);
47911
+ }
47912
+ const groups = [];
47913
+ for (const e of projected) {
47914
+ if (connected.has(e.p.id)) continue;
47915
+ const r = radiusOf(e);
47916
+ const near = groups.find(
47917
+ (g) => g.some(
47918
+ (q) => Math.hypot(q.xy[0] - e.xy[0], q.xy[1] - e.xy[1]) < (r + radiusOf(q)) * STACK_OVERLAP
47919
+ )
47920
+ );
47921
+ if (near) near.push(e);
47922
+ else groups.push([e]);
47923
+ }
47924
+ for (const g of groups) {
47925
+ if (g.length === 1) {
47926
+ placePoi(g[0], g[0].xy[0], g[0].xy[1]);
47927
+ continue;
47928
+ }
47929
+ const clusterId = g[0].p.id;
47930
+ const cx0 = g.reduce((s, e) => s + e.xy[0], 0) / g.length;
47931
+ const cy0 = g.reduce((s, e) => s + e.xy[1], 0) / g.length;
47932
+ const maxR = Math.max(...g.map(radiusOf));
47933
+ const sep = 2 * maxR + STACK_RING_GAP;
47934
+ const ringR = Math.max(
47935
+ COLO_R,
47936
+ sep / (2 * Math.sin(Math.PI / Math.max(g.length, 2)))
47937
+ );
47938
+ const positions = g.map((e, i) => {
47939
+ if (g.length <= STACK_RING_MAX) {
47940
+ const ang2 = -Math.PI / 2 + i * 2 * Math.PI / g.length;
47941
+ return {
47942
+ e,
47943
+ mx: cx0 + Math.cos(ang2) * ringR,
47944
+ my: cy0 + Math.sin(ang2) * ringR
47945
+ };
47946
+ }
47947
+ const ang = i * GOLDEN_ANGLE;
47948
+ const rr = ringR * Math.sqrt((i + 1) / g.length);
47949
+ return { e, mx: cx0 + Math.cos(ang) * rr, my: cy0 + Math.sin(ang) * rr };
47950
+ });
47951
+ let minX = cx0 - maxR;
47952
+ let maxX = cx0 + maxR;
47953
+ let minY = cy0 - maxR;
47954
+ let maxY = cy0 + maxR;
47955
+ for (const { mx, my, e } of positions) {
47956
+ const r = radiusOf(e);
47957
+ minX = Math.min(minX, mx - r);
47958
+ maxX = Math.max(maxX, mx + r);
47959
+ minY = Math.min(minY, my - r);
47960
+ maxY = Math.max(maxY, my + r);
47961
+ }
47962
+ let dx = 0;
47963
+ let dy = 0;
47964
+ if (minX + dx < 2) dx = 2 - minX;
47965
+ if (maxX + dx > width - 2) dx = width - 2 - maxX;
47966
+ if (minY + dy < 2) dy = 2 - minY;
47967
+ if (maxY + dy > height - 2) dy = height - 2 - maxY;
47968
+ const legsOut = [];
47969
+ for (const { e, mx, my } of positions) {
47970
+ const fx = mx + dx;
47971
+ const fy = my + dy;
47972
+ placePoi(e, fx, fy, clusterId);
47973
+ legsOut.push({ x2: fx, y2: fy, color: poiFill(e.p).fill });
47974
+ }
47975
+ clusters.push({
47976
+ id: clusterId,
47977
+ cx: cx0 + dx,
47978
+ cy: cy0 + dy,
47979
+ count: g.length,
47980
+ hitR: ringR + maxR + 6,
47981
+ legs: legsOut
46879
47982
  });
46880
47983
  }
46881
47984
  const legs = [];
@@ -46925,16 +48028,26 @@ function layoutMap(resolved, data, size, opts) {
46925
48028
  if (!a || !b) continue;
46926
48029
  const mx = (a.cx + b.cx) / 2;
46927
48030
  const my = (a.cy + b.cy) / 2;
48031
+ const bow = {
48032
+ curved: leg.style === "arc",
48033
+ offset: 0,
48034
+ labelX: mx,
48035
+ labelY: my - 4
48036
+ };
48037
+ const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46928
48038
  legs.push({
46929
- d: legPath(a, b, leg.style === "arc", 0),
48039
+ d: legPath(a, b, bow.curved, bow.offset),
46930
48040
  width: routeWidthFor(Number(leg.value)),
46931
48041
  color: mix(palette.text, palette.bg, 72),
46932
48042
  arrow: true,
46933
48043
  lineNumber: leg.lineNumber,
46934
48044
  ...leg.label !== void 0 && {
46935
48045
  label: leg.label,
46936
- labelX: mx,
46937
- labelY: my - 4
48046
+ labelX: bow.labelX,
48047
+ labelY: bow.labelY,
48048
+ labelColor: routeLabelStyle.color,
48049
+ labelHalo: routeLabelStyle.halo,
48050
+ labelHaloColor: routeLabelStyle.haloColor
46938
48051
  }
46939
48052
  });
46940
48053
  }
@@ -46962,20 +48075,29 @@ function layoutMap(resolved, data, size, opts) {
46962
48075
  const a = poiScreen.get(e.fromId);
46963
48076
  const b = poiScreen.get(e.toId);
46964
48077
  if (!a || !b) return;
46965
- const curved = e.style === "arc" || n > 1;
46966
- const offset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
48078
+ const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
46967
48079
  const mx = (a.cx + b.cx) / 2;
46968
48080
  const my = (a.cy + b.cy) / 2;
48081
+ const bow = {
48082
+ curved: e.style === "arc" || n > 1,
48083
+ offset: fanOffset,
48084
+ labelX: mx,
48085
+ labelY: my - 4
48086
+ };
48087
+ const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46969
48088
  legs.push({
46970
- d: legPath(a, b, curved, offset),
48089
+ d: legPath(a, b, bow.curved, bow.offset),
46971
48090
  width: widthFor(e),
46972
48091
  color: mix(palette.text, palette.bg, 66),
46973
48092
  arrow: e.directed,
46974
48093
  lineNumber: e.lineNumber,
46975
48094
  ...e.label !== void 0 && {
46976
48095
  label: e.label,
46977
- labelX: mx,
46978
- labelY: my - 4
48096
+ labelX: bow.labelX,
48097
+ labelY: bow.labelY,
48098
+ labelColor: edgeLabelStyle.color,
48099
+ labelHalo: edgeLabelStyle.halo,
48100
+ labelHaloColor: edgeLabelStyle.haloColor
46979
48101
  }
46980
48102
  });
46981
48103
  });
@@ -47017,25 +48139,25 @@ function layoutMap(resolved, data, size, opts) {
47017
48139
  }
47018
48140
  }
47019
48141
  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));
47020
- const regionLabelMode = resolved.directives.regionLabels ?? "off";
48142
+ const showRegionLabels = resolved.directives.noRegionLabels !== true;
48143
+ const isCompact = width < COMPACT_WIDTH_PX;
47021
48144
  const LABEL_PADX = 6;
47022
48145
  const LABEL_PADY = 3;
47023
- const labelW = (text) => measureLegendText(text, FONT) + 2 * LABEL_PADX;
47024
- const labelH = FONT + 2 * LABEL_PADY;
48146
+ const labelW = (text) => measureLegendText(text, FONT2) + 2 * LABEL_PADX;
48147
+ const labelH = FONT2 + 2 * LABEL_PADY;
47025
48148
  const pushRegionLabel = (x, y, text, fill2, lineNumber) => {
47026
- const color = contrastText(
47027
- fill2,
47028
- palette.textOnFillLight,
47029
- palette.textOnFillDark
48149
+ const { color, haloColor } = labelOnFill(fill2);
48150
+ const halfW = measureLegendText(text, FONT2) / 2;
48151
+ const overflows = [y - FONT2 * 0.55, y - FONT2 * 0.1].some(
48152
+ (sy) => fillAt(x - halfW, sy) !== fill2 || fillAt(x + halfW, sy) !== fill2
47030
48153
  );
47031
- const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47032
48154
  labels.push({
47033
48155
  x,
47034
48156
  y,
47035
48157
  text,
47036
48158
  anchor: "middle",
47037
48159
  color,
47038
- halo: true,
48160
+ halo: overflows,
47039
48161
  haloColor,
47040
48162
  lineNumber
47041
48163
  });
@@ -47044,21 +48166,50 @@ function layoutMap(resolved, data, size, opts) {
47044
48166
  US: [-98.5, 39.5]
47045
48167
  // CONUS geographic centre (near Lebanon, Kansas)
47046
48168
  };
47047
- if (regionLabelMode === "full" || regionLabelMode === "abbrev") {
47048
- for (const r of regions) {
47049
- if (r.layer === "base" || r.label === void 0) continue;
47050
- const f = r.layer === "us-state" ? usLayer?.get(r.id) : worldLayer.get(r.id);
47051
- if (!f) continue;
48169
+ const REGION_LABEL_GAP = 2;
48170
+ const regionLabelRect = (cx, cy, text) => {
48171
+ const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
48172
+ return { x: cx - w / 2, y: cy - FONT2 / 2, w, h: FONT2 };
48173
+ };
48174
+ if (showRegionLabels) {
48175
+ const frameContainers = new Set(resolved.poiFrameContainers);
48176
+ const entries = regions.map((r) => {
48177
+ const isContainer = frameContainers.has(r.id);
48178
+ if (r.layer === "base" && !isContainer || r.label === void 0)
48179
+ return null;
48180
+ const isUsState = r.layer === "us-state" || r.id.startsWith("US-");
48181
+ const f = isUsState ? usLayer?.get(r.id) : worldLayer.get(r.id);
48182
+ if (!f) return null;
47052
48183
  const [[x0, y0], [x1, y1]] = path.bounds(f);
47053
- const text = regionLabelMode === "abbrev" ? r.id.replace(/^US-/, "") : r.label;
47054
- if (labelW(text) > x1 - x0 || labelH > y1 - y0) continue;
47055
- const anchor = r.layer !== "us-state" ? WORLD_LABEL_ANCHORS[r.id] : void 0;
48184
+ const boxW = x1 - x0;
48185
+ const boxH = y1 - y0;
48186
+ const abbrev = isUsState ? r.id.replace(/^US-/, "") : void 0;
48187
+ const candidates = abbrev !== void 0 ? isCompact ? [abbrev, r.label] : [r.label, abbrev] : [r.label];
48188
+ const anchor = !isUsState ? WORLD_LABEL_ANCHORS[r.id] : void 0;
47056
48189
  const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
47057
- if (!c || !Number.isFinite(c[0])) continue;
48190
+ if (!c || !Number.isFinite(c[0])) return null;
48191
+ return { r, c, boxW, boxH, area: boxW * boxH, candidates };
48192
+ }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
48193
+ const placedRegionRects = [];
48194
+ const POI_LABEL_PAD = 14;
48195
+ const poiObstacles = pois.map((p) => ({
48196
+ x: p.cx - p.r - POI_LABEL_PAD,
48197
+ y: p.cy - p.r - POI_LABEL_PAD,
48198
+ w: 2 * (p.r + POI_LABEL_PAD),
48199
+ h: 2 * (p.r + POI_LABEL_PAD)
48200
+ }));
48201
+ for (const { r, c, boxW, boxH, candidates } of entries) {
48202
+ const text = candidates.find((t) => {
48203
+ if (labelW(t) > boxW || labelH > boxH) return false;
48204
+ const rect = regionLabelRect(c[0], c[1], t);
48205
+ return !placedRegionRects.some((p) => rectsOverlap(rect, p)) && !poiObstacles.some((o) => rectsOverlap(rect, o));
48206
+ });
48207
+ if (text === void 0) continue;
48208
+ placedRegionRects.push(regionLabelRect(c[0], c[1], text));
47058
48209
  pushRegionLabel(c[0], c[1], text, r.fill, r.lineNumber);
47059
48210
  }
47060
48211
  for (const seed of insetLabelSeeds) {
47061
- const text = regionLabelMode === "abbrev" ? seed.iso.replace(/^US-/, "") : seed.name;
48212
+ const text = isCompact ? seed.iso.replace(/^US-/, "") : seed.name;
47062
48213
  const src = regionById.get(seed.iso);
47063
48214
  pushRegionLabel(
47064
48215
  seed.x,
@@ -47069,22 +48220,26 @@ function layoutMap(resolved, data, size, opts) {
47069
48220
  );
47070
48221
  }
47071
48222
  }
47072
- const poiLabelMode = resolved.directives.poiLabels ?? "auto";
47073
- if (poiLabelMode !== "off") {
47074
- const ordered = [...pois].sort(
47075
- (a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1)
47076
- );
48223
+ if (resolved.directives.noPoiLabels !== true) {
48224
+ const ordered = [...pois].filter((p) => p.clusterId === void 0).sort((a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1));
47077
48225
  const poiById = new Map(resolved.pois.map((q) => [q.id, q]));
47078
48226
  const labelText = (p) => {
47079
48227
  const src = poiById.get(p.id);
47080
48228
  return src?.label ?? src?.name ?? p.id;
47081
48229
  };
47082
- const poiLabH = FONT * 1.25;
48230
+ const poiLabH = FONT2 * 1.25;
47083
48231
  const labelInfo = (p) => {
47084
48232
  const text = labelText(p);
47085
- return { text, w: measureLegendText(text, FONT) };
48233
+ return { text, w: measureLegendText(text, FONT2) };
47086
48234
  };
47087
48235
  const GAP = 3;
48236
+ const clusterMembersById = /* @__PURE__ */ new Map();
48237
+ for (const p of pois) {
48238
+ if (p.clusterId === void 0) continue;
48239
+ const arr = clusterMembersById.get(p.clusterId);
48240
+ if (arr) arr.push(p);
48241
+ else clusterMembersById.set(p.clusterId, [p]);
48242
+ }
47088
48243
  const inlineRect = (p, w, side) => {
47089
48244
  switch (side) {
47090
48245
  case "right":
@@ -47114,11 +48269,11 @@ function layoutMap(resolved, data, size, opts) {
47114
48269
  const x = side === "right" ? rect.x : side === "left" ? rect.x + w : p.cx;
47115
48270
  labels.push({
47116
48271
  x,
47117
- y: rect.y + poiLabH / 2 + FONT / 3,
48272
+ y: rect.y + poiLabH / 2 + FONT2 / 3,
47118
48273
  text,
47119
48274
  anchor,
47120
48275
  color: palette.text,
47121
- halo: true,
48276
+ halo: false,
47122
48277
  haloColor: palette.bg,
47123
48278
  poiId: p.id,
47124
48279
  lineNumber: p.lineNumber
@@ -47129,43 +48284,60 @@ function layoutMap(resolved, data, size, opts) {
47129
48284
  return rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect);
47130
48285
  };
47131
48286
  const GROUP_R = 30;
47132
- const groups = [];
48287
+ const groups2 = [];
47133
48288
  for (const p of ordered) {
47134
- const near = groups.find(
48289
+ const near = groups2.find(
47135
48290
  (g) => g.some((q) => Math.hypot(q.cx - p.cx, q.cy - p.cy) < GROUP_R)
47136
48291
  );
47137
48292
  if (near) near.push(p);
47138
- else groups.push([p]);
48293
+ else groups2.push([p]);
47139
48294
  }
47140
48295
  const ROW_GAP2 = 3;
47141
48296
  const step = poiLabH + ROW_GAP2;
47142
48297
  const COL_GAP = 16;
47143
- const placeColumn = (group) => {
47144
- const items = group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48298
+ const makeItems = (group) => group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48299
+ const columnRows = (items, side) => {
47145
48300
  const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
47146
48301
  const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
47147
- const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
47148
48302
  const maxW = Math.max(...items.map((o) => o.w));
47149
- const side = right + COL_GAP + maxW <= width - 2 ? "right" : "left";
47150
- const colX = side === "right" ? right + COL_GAP : left - COL_GAP;
48303
+ const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
48304
+ const colX = side === "right" ? Math.min(right + COL_GAP, width - 2 - maxW) : Math.max(left - COL_GAP, 2 + maxW);
47151
48305
  const totalH = items.length * step;
47152
48306
  let startY = cyMid - totalH / 2;
47153
48307
  startY = Math.max(2, Math.min(startY, height - totalH - 2));
47154
- items.forEach((o, i) => {
48308
+ return items.map((o, i) => {
47155
48309
  const rowCy = startY + i * step + step / 2;
47156
- obstacles.push({
47157
- x: side === "right" ? colX : colX - o.w,
47158
- y: rowCy - poiLabH / 2,
47159
- w: o.w,
47160
- h: poiLabH
47161
- });
48310
+ return {
48311
+ o,
48312
+ colX,
48313
+ rowCy,
48314
+ rect: {
48315
+ x: side === "right" ? colX : colX - o.w,
48316
+ y: rowCy - poiLabH / 2,
48317
+ w: o.w,
48318
+ h: poiLabH
48319
+ }
48320
+ };
48321
+ });
48322
+ };
48323
+ const wouldColumnBeClean = (items, side) => columnRows(items, side).every(
48324
+ ({ rect }) => rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect)
48325
+ );
48326
+ const defaultColumnSide = (items) => {
48327
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48328
+ const maxW = Math.max(...items.map((o) => o.w));
48329
+ return right + COL_GAP + maxW <= width - 2 ? "right" : "left";
48330
+ };
48331
+ const commitColumn = (items, side, clusterId) => {
48332
+ for (const { o, colX, rowCy, rect } of columnRows(items, side)) {
48333
+ obstacles.push(rect);
47162
48334
  labels.push({
47163
48335
  x: colX,
47164
- y: rowCy + FONT / 3,
48336
+ y: rowCy + FONT2 / 3,
47165
48337
  text: o.text,
47166
48338
  anchor: side === "right" ? "start" : "end",
47167
48339
  color: palette.text,
47168
- halo: true,
48340
+ halo: false,
47169
48341
  haloColor: palette.bg,
47170
48342
  leader: {
47171
48343
  x1: o.p.cx,
@@ -47175,24 +48347,141 @@ function layoutMap(resolved, data, size, opts) {
47175
48347
  },
47176
48348
  leaderColor: o.p.fill,
47177
48349
  poiId: o.p.id,
47178
- lineNumber: o.p.lineNumber
48350
+ lineNumber: o.p.lineNumber,
48351
+ ...clusterId !== void 0 && { clusterMember: clusterId }
47179
48352
  });
48353
+ }
48354
+ };
48355
+ const pushHidden = (p) => {
48356
+ const { text, w } = labelInfo(p);
48357
+ let x = p.cx + p.r + GAP;
48358
+ let anchor = "start";
48359
+ if (x + w > width) {
48360
+ x = p.cx - p.r - GAP - w;
48361
+ anchor = "end";
48362
+ }
48363
+ const y = Math.max(0, Math.min(p.cy - poiLabH / 2, height - poiLabH));
48364
+ labels.push({
48365
+ x: anchor === "start" ? x : x + w,
48366
+ y: y + poiLabH / 2 + FONT2 / 3,
48367
+ text,
48368
+ anchor,
48369
+ color: palette.text,
48370
+ halo: false,
48371
+ haloColor: palette.bg,
48372
+ poiId: p.id,
48373
+ hidden: true,
48374
+ lineNumber: p.lineNumber
47180
48375
  });
47181
48376
  };
47182
- for (const g of groups) {
48377
+ for (const [clusterId, members] of clusterMembersById) {
48378
+ if (members.length === 0) continue;
48379
+ const items = makeItems(members);
48380
+ const side = wouldColumnBeClean(items, "right") ? "right" : wouldColumnBeClean(items, "left") ? "left" : defaultColumnSide(items);
48381
+ commitColumn(items, side, clusterId);
48382
+ }
48383
+ const maxExtent = MAX_CLUSTER_EXTENT_FACTOR * Math.min(width, height);
48384
+ const clusterPending = [];
48385
+ for (const g of groups2) {
48386
+ const items = makeItems(g);
47183
48387
  if (g.length === 1) {
47184
- const p = g[0];
47185
- const { text, w } = labelInfo(p);
48388
+ const { p, text, w } = items[0];
47186
48389
  const side = ["right", "left", "above", "below"].find(
47187
48390
  (s) => inlineFits(p, w, s)
47188
48391
  );
47189
- if (side) {
47190
- pushInline(p, text, w, side);
47191
- continue;
48392
+ if (side) pushInline(p, text, w, side);
48393
+ else commitColumn(items, defaultColumnSide(items));
48394
+ continue;
48395
+ }
48396
+ const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
48397
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48398
+ const minCy = Math.min(...items.map((o) => o.p.cy));
48399
+ const maxCy = Math.max(...items.map((o) => o.p.cy));
48400
+ const diag = Math.hypot(right - left, maxCy - minCy);
48401
+ if (diag > maxExtent || items.length > MAX_COLUMN_ROWS) {
48402
+ items.forEach((o) => pushHidden(o.p));
48403
+ } else {
48404
+ clusterPending.push(items);
48405
+ }
48406
+ }
48407
+ for (const items of clusterPending) {
48408
+ const side = ["right", "left"].find(
48409
+ (s) => wouldColumnBeClean(items, s)
48410
+ );
48411
+ if (side) commitColumn(items, side);
48412
+ else items.forEach((o) => pushHidden(o.p));
48413
+ }
48414
+ }
48415
+ if (resolved.directives.noContextLabels !== true) {
48416
+ for (const l of labels) {
48417
+ if (l.hidden) continue;
48418
+ const w = labelW(l.text);
48419
+ const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
48420
+ obstacles.push({ x, y: l.y - labelH / 2, w, h: labelH });
48421
+ }
48422
+ for (const box of insets)
48423
+ obstacles.push({ x: box.x, y: box.y, w: box.w, h: box.h });
48424
+ const countryCandidates = [];
48425
+ for (const f of worldLayer.values()) {
48426
+ const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
48427
+ if (!iso || regionById.has(iso)) continue;
48428
+ let hasReferencedSub = false;
48429
+ for (const k of regionById.keys())
48430
+ if (k.startsWith(iso + "-")) {
48431
+ hasReferencedSub = true;
48432
+ break;
47192
48433
  }
48434
+ if (hasReferencedSub) continue;
48435
+ const b = path.bounds(f);
48436
+ const [x0, y0] = b[0];
48437
+ const [x1, y1] = b[1];
48438
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48439
+ const anchorLngLat = WORLD_LABEL_ANCHORS[iso];
48440
+ const a = anchorLngLat ? project(anchorLngLat[0], anchorLngLat[1]) : path.centroid(f);
48441
+ countryCandidates.push({
48442
+ name: f.properties?.name ?? iso,
48443
+ bbox: [x0, y0, x1, y1],
48444
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48445
+ });
48446
+ }
48447
+ const framedStateContainers = (resolved.poiFrameContainers ?? []).some(
48448
+ (id) => id.startsWith("US-")
48449
+ );
48450
+ if (usLayer && framedStateContainers) {
48451
+ const containerSet = new Set(resolved.poiFrameContainers);
48452
+ for (const [iso, f] of usLayer) {
48453
+ if (containerSet.has(iso) || regionById.has(iso)) continue;
48454
+ const viewF = cullFeatureToView(f);
48455
+ if (!viewF) continue;
48456
+ const b = path.bounds(viewF);
48457
+ const [x0, y0] = b[0];
48458
+ const [x1, y1] = b[1];
48459
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48460
+ const a = path.centroid(viewF);
48461
+ countryCandidates.push({
48462
+ name: f.properties?.name ?? iso,
48463
+ bbox: [x0, y0, x1, y1],
48464
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48465
+ });
47193
48466
  }
47194
- placeColumn(g);
47195
48467
  }
48468
+ const contextLabels = placeContextLabels({
48469
+ projection: resolved.projection,
48470
+ dLonSpan,
48471
+ dLatSpan,
48472
+ width,
48473
+ height,
48474
+ waterBodies: data.waterBodies,
48475
+ countries: countryCandidates,
48476
+ palette,
48477
+ project,
48478
+ collides,
48479
+ // Water labels must stay over open water — `fillAt` returns the ocean
48480
+ // backdrop colour off-land and a region fill on-land (lakes/states count
48481
+ // as land here, which is the safe side for an ocean name).
48482
+ overLand: (x, y) => fillAt(x, y) !== water
48483
+ });
48484
+ labels.push(...contextLabels);
47196
48485
  }
47197
48486
  let legend = null;
47198
48487
  if (!resolved.directives.noLegend) {
@@ -47229,27 +48518,33 @@ function layoutMap(resolved, data, size, opts) {
47229
48518
  rivers,
47230
48519
  relief,
47231
48520
  reliefHatch,
48521
+ coastlineStyle,
47232
48522
  legs,
47233
48523
  pois,
48524
+ clusters,
47234
48525
  labels,
47235
48526
  legend,
47236
48527
  insets,
47237
48528
  insetRegions,
47238
48529
  projection,
47239
- stretch: stretchParams
48530
+ stretch: stretchParams,
48531
+ diagnostics: []
47240
48532
  };
47241
48533
  }
47242
- var import_d3_geo2, import_topojson_client2, FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, COLO_EPS, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_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;
48534
+ var import_d3_geo2, import_topojson_client2, FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT2, MAX_CLUSTER_EXTENT_FACTOR, MAX_COLUMN_ROWS, REGION_LABEL_HALO_RATIO, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, COMPACT_WIDTH_PX, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, COASTLINE_RING_COUNT, COASTLINE_D0, COASTLINE_STEP, COASTLINE_THICKNESS, COASTLINE_OPACITY_NEAR, COASTLINE_OPACITY_FAR, COASTLINE_MIN_EXTENT, COASTLINE_MIN_EXTENT_GLOBAL, COASTLINE_STROKE_MIX, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, STACK_OVERLAP, STACK_RING_MAX, STACK_RING_GAP, FAN_STEP, ARC_CURVE_FRAC, decodeCache, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, inAlaska, inHawaii, FOREIGN_BORDER, US_NON_CONUS;
47243
48535
  var init_layout15 = __esm({
47244
48536
  "src/map/layout.ts"() {
47245
48537
  "use strict";
47246
48538
  import_d3_geo2 = require("d3-geo");
47247
48539
  import_topojson_client2 = require("topojson-client");
47248
48540
  init_color_utils();
48541
+ init_geo();
48542
+ init_colorize();
47249
48543
  init_colors();
47250
48544
  init_label_layout();
47251
48545
  init_legend_constants();
47252
48546
  init_title_constants();
48547
+ init_context_labels();
47253
48548
  FIT_PAD = 24;
47254
48549
  RAMP_FLOOR = 15;
47255
48550
  R_DEFAULT = 6;
@@ -47257,32 +48552,66 @@ var init_layout15 = __esm({
47257
48552
  R_MAX = 22;
47258
48553
  W_MIN = 1.25;
47259
48554
  W_MAX = 8;
47260
- FONT = 11;
47261
- COLO_EPS = 1.5;
48555
+ FONT2 = 11;
48556
+ MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48557
+ MAX_COLUMN_ROWS = 7;
48558
+ REGION_LABEL_HALO_RATIO = 4.5;
47262
48559
  LAND_TINT_LIGHT = 12;
47263
48560
  LAND_TINT_DARK = 24;
47264
48561
  TAG_TINT_LIGHT = 60;
47265
48562
  TAG_TINT_DARK = 68;
47266
- WATER_TINT_LIGHT = 13;
47267
- WATER_TINT_DARK = 14;
48563
+ WATER_TINT_LIGHT = 24;
48564
+ WATER_TINT_DARK = 24;
47268
48565
  RIVER_WIDTH = 1.3;
48566
+ COMPACT_WIDTH_PX = 480;
47269
48567
  RELIEF_MIN_AREA = 12;
47270
48568
  RELIEF_MIN_DIM = 2;
47271
- RELIEF_HATCH_SPACING = 3;
47272
- RELIEF_HATCH_WIDTH = 0.25;
48569
+ RELIEF_HATCH_SPACING = 2;
48570
+ RELIEF_HATCH_WIDTH = 0.15;
47273
48571
  RELIEF_HATCH_STRENGTH = 32;
48572
+ COASTLINE_RING_COUNT = 5;
48573
+ COASTLINE_D0 = 16e-4;
48574
+ COASTLINE_STEP = 28e-4;
48575
+ COASTLINE_THICKNESS = 14e-4;
48576
+ COASTLINE_OPACITY_NEAR = 0.5;
48577
+ COASTLINE_OPACITY_FAR = 0.1;
48578
+ COASTLINE_MIN_EXTENT = 6e-4;
48579
+ COASTLINE_MIN_EXTENT_GLOBAL = 6e-4;
48580
+ COASTLINE_STROKE_MIX = 32;
47274
48581
  FOREIGN_TINT_LIGHT = 30;
47275
48582
  FOREIGN_TINT_DARK = 62;
47276
48583
  MUTED_FOREIGN_LIGHT = 28;
47277
48584
  MUTED_FOREIGN_DARK = 16;
47278
48585
  COLO_R = 9;
47279
48586
  GOLDEN_ANGLE = 2.399963229728653;
48587
+ STACK_OVERLAP = 1;
48588
+ STACK_RING_MAX = 8;
48589
+ STACK_RING_GAP = 4;
47280
48590
  FAN_STEP = 16;
47281
48591
  ARC_CURVE_FRAC = 0.18;
48592
+ decodeCache = /* @__PURE__ */ new WeakMap();
47282
48593
  usConusProjection = () => (0, import_d3_geo2.geoConicEqualArea)().parallels([29.5, 45.5]).rotate([96, 0]);
47283
48594
  alaskaProjection = () => (0, import_d3_geo2.geoConicEqualArea)().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
47284
48595
  hawaiiProjection = () => (0, import_d3_geo2.geoMercator)();
47285
48596
  INSET_STATES = /* @__PURE__ */ new Set(["US-AK", "US-HI"]);
48597
+ inAlaska = (lon, lat) => lat >= 51 && (lon <= -129 || lon >= 172);
48598
+ inHawaii = (lon, lat) => lat >= 18 && lat <= 23 && lon >= -161 && lon <= -154;
48599
+ FOREIGN_BORDER = {
48600
+ CA: [
48601
+ "US-AK",
48602
+ "US-WA",
48603
+ "US-ID",
48604
+ "US-MT",
48605
+ "US-ND",
48606
+ "US-MN",
48607
+ "US-MI",
48608
+ "US-NY",
48609
+ "US-VT",
48610
+ "US-NH",
48611
+ "US-ME"
48612
+ ],
48613
+ MX: ["US-CA", "US-AZ", "US-NM", "US-TX"]
48614
+ };
47286
48615
  US_NON_CONUS = /* @__PURE__ */ new Set([
47287
48616
  "US-AK",
47288
48617
  "US-HI",
@@ -47301,6 +48630,58 @@ __export(renderer_exports16, {
47301
48630
  renderMap: () => renderMap,
47302
48631
  renderMapForExport: () => renderMapForExport
47303
48632
  });
48633
+ function pointInRing2(px, py, ring) {
48634
+ let inside = false;
48635
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
48636
+ const [xi, yi] = ring[i];
48637
+ const [xj, yj] = ring[j];
48638
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
48639
+ inside = !inside;
48640
+ }
48641
+ return inside;
48642
+ }
48643
+ function ringToPath(ring) {
48644
+ let d = "";
48645
+ for (let i = 0; i < ring.length; i++)
48646
+ d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48647
+ return d + "Z";
48648
+ }
48649
+ function coastlineOuterRings(regions, minExtent) {
48650
+ const paths = [];
48651
+ for (const r of regions) {
48652
+ const rings = parsePathRings(r.d);
48653
+ for (let i = 0; i < rings.length; i++) {
48654
+ const ring = rings[i];
48655
+ if (ring.length < 3) continue;
48656
+ let minX = Infinity;
48657
+ let minY = Infinity;
48658
+ let maxX = -Infinity;
48659
+ let maxY = -Infinity;
48660
+ for (const [x, y] of ring) {
48661
+ if (x < minX) minX = x;
48662
+ if (x > maxX) maxX = x;
48663
+ if (y < minY) minY = y;
48664
+ if (y > maxY) maxY = y;
48665
+ }
48666
+ if (Math.max(maxX - minX, maxY - minY) < minExtent) continue;
48667
+ const [fx, fy] = ring[0];
48668
+ let depth = 0;
48669
+ for (let j = 0; j < rings.length; j++)
48670
+ if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48671
+ if (depth % 2 === 1) continue;
48672
+ paths.push(ringToPath(ring));
48673
+ }
48674
+ }
48675
+ return paths;
48676
+ }
48677
+ function appendWaterLines(g, outerRings, style, flatWater) {
48678
+ const d = outerRings.join(" ");
48679
+ const linesOuterFirst = [...style.lines].sort((a, b) => b.d - a.d);
48680
+ for (const line12 of linesOuterFirst) {
48681
+ 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");
48682
+ g.append("path").attr("d", d).attr("stroke", flatWater).attr("stroke-width", 2 * line12.d).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
48683
+ }
48684
+ }
47304
48685
  function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims, activeGroupOverride) {
47305
48686
  d3Selection18.select(container).selectAll(":not([data-d3-tooltip])").remove();
47306
48687
  const width = exportDims?.width ?? container.clientWidth;
@@ -47313,6 +48694,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47313
48694
  {
47314
48695
  palette,
47315
48696
  isDark,
48697
+ // Export-only: forward the contain-fit request from mapExportDimensions so a
48698
+ // clamped/floored (off-aspect) export canvas letterboxes instead of
48699
+ // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48700
+ // keeps the global stretch-fill.
48701
+ preferContain: exportDims?.preferContain ?? false,
47316
48702
  ...activeGroupOverride !== void 0 && {
47317
48703
  activeGroup: activeGroupOverride
47318
48704
  }
@@ -47326,6 +48712,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47326
48712
  const gRegions = svg.append("g").attr("class", "dgmo-map-regions");
47327
48713
  const drawRegion = (g, r, strokeWidth) => {
47328
48714
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
48715
+ if (r.label) p.attr("data-region-name", r.label);
47329
48716
  if (r.layer !== "base") {
47330
48717
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
47331
48718
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -47360,6 +48747,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47360
48747
  gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
47361
48748
  }
47362
48749
  }
48750
+ if (layout.coastlineStyle) {
48751
+ const cs = layout.coastlineStyle;
48752
+ const maskId = "dgmo-map-water-mask";
48753
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48754
+ mask.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "white");
48755
+ const landD = layout.regions.filter((r) => r.id !== "lake").map((r) => r.d).join(" ");
48756
+ const lakeD = layout.regions.filter((r) => r.id === "lake").map((r) => r.d).join(" ");
48757
+ if (landD) mask.append("path").attr("d", landD).attr("fill", "black");
48758
+ if (lakeD) mask.append("path").attr("d", lakeD).attr("fill", "white");
48759
+ if (layout.insets.length) {
48760
+ const reach = Math.max(0, ...cs.lines.map((l) => l.d + l.thickness));
48761
+ for (const box of layout.insets) {
48762
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48763
+ mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
48764
+ }
48765
+ }
48766
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
48767
+ appendWaterLines(
48768
+ gWater,
48769
+ coastlineOuterRings(layout.regions, cs.minExtent),
48770
+ cs,
48771
+ layout.background
48772
+ );
48773
+ const byStroke = /* @__PURE__ */ new Map();
48774
+ for (const r of layout.regions) {
48775
+ const arr = byStroke.get(r.stroke);
48776
+ if (arr) arr.push(r.d);
48777
+ else byStroke.set(r.stroke, [r.d]);
48778
+ }
48779
+ for (const [stroke2, ds] of byStroke)
48780
+ gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48781
+ }
47363
48782
  if (layout.rivers.length) {
47364
48783
  const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47365
48784
  for (const r of layout.rivers) {
@@ -47368,15 +48787,61 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47368
48787
  }
47369
48788
  if (layout.insets.length) {
47370
48789
  const insetG = svg.append("g").attr("class", "dgmo-map-insets");
47371
- for (const box of layout.insets) {
48790
+ layout.insets.forEach((box, bi) => {
47372
48791
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
47373
48792
  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");
47374
- }
48793
+ if (box.contextLand) {
48794
+ const clipId = `dgmo-map-inset-clip-${bi}`;
48795
+ defs.append("clipPath").attr("id", clipId).append("path").attr("d", d);
48796
+ insetG.append("path").attr("d", box.contextLand.d).attr("fill", box.contextLand.fill).attr("clip-path", `url(#${clipId})`);
48797
+ }
48798
+ });
47375
48799
  for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
47376
- }
48800
+ if (layout.coastlineStyle) {
48801
+ const cs = layout.coastlineStyle;
48802
+ const maskId = "dgmo-map-inset-water-mask";
48803
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48804
+ for (const box of layout.insets) {
48805
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48806
+ mask.append("path").attr("d", d).attr("fill", "white");
48807
+ }
48808
+ layout.insets.forEach((box, bi) => {
48809
+ if (box.contextLand)
48810
+ mask.append("path").attr("d", box.contextLand.d).attr("fill", "black").attr("clip-path", `url(#dgmo-map-inset-clip-${bi})`);
48811
+ });
48812
+ for (const r of layout.insetRegions)
48813
+ if (r.id !== "lake")
48814
+ mask.append("path").attr("d", r.d).attr("fill", "black");
48815
+ for (const r of layout.insetRegions)
48816
+ if (r.id === "lake")
48817
+ mask.append("path").attr("d", r.d).attr("fill", "white");
48818
+ const clipId = "dgmo-map-inset-water-clip";
48819
+ const clip = defs.append("clipPath").attr("id", clipId);
48820
+ for (const box of layout.insets) {
48821
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48822
+ clip.append("path").attr("d", d);
48823
+ }
48824
+ 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})`);
48825
+ appendWaterLines(
48826
+ gInsetWater,
48827
+ coastlineOuterRings(layout.insetRegions, cs.minExtent),
48828
+ cs,
48829
+ layout.background
48830
+ );
48831
+ for (const r of layout.insetRegions)
48832
+ gInsetWater.append("path").attr("d", r.d).attr("stroke", r.stroke).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48833
+ }
48834
+ }
48835
+ const wireSync = (sel, lineNumber) => {
48836
+ if (lineNumber < 1) return;
48837
+ sel.attr("data-line-number", lineNumber);
48838
+ if (onClickItem)
48839
+ sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
48840
+ };
47377
48841
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
47378
48842
  layout.legs.forEach((leg, i) => {
47379
48843
  const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round");
48844
+ wireSync(p, leg.lineNumber);
47380
48845
  if (leg.arrow) {
47381
48846
  const id = `dgmo-map-arrow-${i}`;
47382
48847
  const s = arrowSize(leg.width);
@@ -47384,25 +48849,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47384
48849
  p.attr("marker-end", `url(#${id})`);
47385
48850
  }
47386
48851
  if (leg.label !== void 0 && leg.labelX !== void 0) {
47387
- emitText(
48852
+ const lt = emitText(
47388
48853
  gLegs,
47389
48854
  leg.labelX,
47390
48855
  leg.labelY ?? 0,
47391
48856
  leg.label,
47392
48857
  "middle",
47393
- palette.textMuted,
47394
- haloColor,
47395
- true,
48858
+ leg.labelColor ?? palette.textMuted,
48859
+ leg.labelHaloColor ?? haloColor,
48860
+ leg.labelHalo ?? true,
47396
48861
  LABEL_FONT - 1
47397
48862
  );
48863
+ wireSync(lt, leg.lineNumber);
47398
48864
  }
47399
48865
  });
48866
+ const gSpider = svg.append("g").attr("class", "dgmo-map-spider");
48867
+ for (const cl of layout.clusters) {
48868
+ if (!exportDims) {
48869
+ 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");
48870
+ }
48871
+ for (const leg of cl.legs) {
48872
+ 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");
48873
+ }
48874
+ 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");
48875
+ }
47400
48876
  const gPois = svg.append("g").attr("class", "dgmo-map-pois");
47401
48877
  for (const poi of layout.pois) {
47402
48878
  if (poi.isOrigin) {
47403
48879
  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);
47404
48880
  }
47405
48881
  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);
48882
+ if (poi.clusterId !== void 0)
48883
+ c.attr("data-cluster-member", poi.clusterId);
47406
48884
  if (poi.tags) {
47407
48885
  for (const [group, value] of Object.entries(poi.tags)) {
47408
48886
  c.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
@@ -47430,12 +48908,32 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47430
48908
  }
47431
48909
  const gLabels = svg.append("g").attr("class", "dgmo-map-labels");
47432
48910
  for (const lab of layout.labels) {
48911
+ if (lab.hidden) {
48912
+ if (exportDims) continue;
48913
+ emitText(
48914
+ gLabels,
48915
+ lab.x,
48916
+ lab.y,
48917
+ lab.text,
48918
+ lab.anchor,
48919
+ lab.color,
48920
+ lab.haloColor,
48921
+ lab.halo,
48922
+ LABEL_FONT,
48923
+ lab.italic,
48924
+ lab.letterSpacing
48925
+ ).attr("data-poi", lab.poiId ?? null).attr("data-poi-hidden", "").style("opacity", 0).style("pointer-events", "none");
48926
+ continue;
48927
+ }
47433
48928
  if (lab.leader) {
47434
48929
  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(
47435
48930
  "stroke",
47436
48931
  lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
47437
48932
  ).attr("stroke-width", lab.leaderColor ? 1 : 0.75);
47438
48933
  if (lab.poiId !== void 0) line12.attr("data-poi", lab.poiId);
48934
+ if (lab.clusterMember !== void 0)
48935
+ line12.attr("data-cluster-member", lab.clusterMember);
48936
+ wireSync(line12, lab.lineNumber);
47439
48937
  }
47440
48938
  const t = emitText(
47441
48939
  gLabels,
@@ -47446,11 +48944,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47446
48944
  lab.color,
47447
48945
  lab.haloColor,
47448
48946
  lab.halo,
47449
- LABEL_FONT
48947
+ LABEL_FONT,
48948
+ lab.italic,
48949
+ lab.letterSpacing,
48950
+ lab.lines
47450
48951
  );
47451
48952
  if (lab.poiId !== void 0) {
47452
48953
  t.attr("data-poi", lab.poiId).style("cursor", "default");
47453
48954
  }
48955
+ if (lab.clusterMember !== void 0) {
48956
+ t.attr("data-cluster-member", lab.clusterMember);
48957
+ }
48958
+ wireSync(t, lab.lineNumber);
48959
+ }
48960
+ if (!exportDims && layout.clusters.length) {
48961
+ const gBadge = svg.append("g").attr("class", "dgmo-map-cluster-badges");
48962
+ for (const cl of layout.clusters) {
48963
+ const g = gBadge.append("g").attr("data-cluster", cl.id).style("opacity", 0).style("pointer-events", "none");
48964
+ const R = 9;
48965
+ 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);
48966
+ 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);
48967
+ emitText(
48968
+ g,
48969
+ cl.cx,
48970
+ cl.cy + 3,
48971
+ String(cl.count),
48972
+ "middle",
48973
+ palette.text,
48974
+ palette.bg,
48975
+ false,
48976
+ LABEL_FONT
48977
+ );
48978
+ }
47454
48979
  }
47455
48980
  if (layout.legend) {
47456
48981
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
@@ -47487,7 +49012,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47487
49012
  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);
47488
49013
  }
47489
49014
  if (layout.subtitle) {
47490
- 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);
49015
+ 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);
47491
49016
  }
47492
49017
  if (layout.caption) {
47493
49018
  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);
@@ -47496,10 +49021,21 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47496
49021
  function renderMapForExport(container, resolved, data, palette, isDark, exportDims) {
47497
49022
  renderMap(container, resolved, data, palette, isDark, void 0, exportDims);
47498
49023
  }
47499
- function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize) {
47500
- const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color).text(text);
49024
+ function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize, italic, letterSpacing, lines) {
49025
+ const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color);
49026
+ if (lines && lines.length > 1) {
49027
+ const lineHeight = fontSize + 2;
49028
+ const startDy = -((lines.length - 1) / 2) * lineHeight;
49029
+ lines.forEach((ln, i) => {
49030
+ t.append("tspan").attr("x", x).attr("dy", i === 0 ? startDy : lineHeight).text(ln);
49031
+ });
49032
+ } else {
49033
+ t.text(text);
49034
+ }
49035
+ if (italic) t.attr("font-style", "italic");
49036
+ if (letterSpacing) t.attr("letter-spacing", letterSpacing);
47501
49037
  if (withHalo) {
47502
- t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7);
49038
+ 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);
47503
49039
  }
47504
49040
  return t;
47505
49041
  }
@@ -47517,6 +49053,56 @@ var init_renderer16 = __esm({
47517
49053
  }
47518
49054
  });
47519
49055
 
49056
+ // src/map/dimensions.ts
49057
+ var dimensions_exports = {};
49058
+ __export(dimensions_exports, {
49059
+ mapContentAspect: () => mapContentAspect,
49060
+ mapExportDimensions: () => mapExportDimensions
49061
+ });
49062
+ function mapContentAspect(resolved, data, ref = REF) {
49063
+ const { projection, fitTarget } = buildMapProjection(resolved, data);
49064
+ projection.fitSize([ref, ref], fitTarget);
49065
+ const b = (0, import_d3_geo3.geoPath)(projection).bounds(fitTarget);
49066
+ const w = b[1][0] - b[0][0];
49067
+ const h = b[1][1] - b[0][1];
49068
+ const aspect = w / h;
49069
+ return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49070
+ }
49071
+ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49072
+ const raw = mapContentAspect(resolved, data);
49073
+ const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49074
+ const width = baseWidth;
49075
+ let height = Math.round(width / clamped);
49076
+ let chromeReserve = 0;
49077
+ if (resolved.title && resolved.pois.length > 0) {
49078
+ const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
49079
+ chromeReserve += Math.max(FIT_PAD2, bannerBottom + TITLE_GAP) - FIT_PAD2;
49080
+ }
49081
+ let floored = false;
49082
+ if (height - chromeReserve < MIN_MAP_BAND) {
49083
+ height = Math.round(chromeReserve + MIN_MAP_BAND);
49084
+ floored = true;
49085
+ }
49086
+ const preferContain = clamped !== raw || floored;
49087
+ return { width, height, preferContain };
49088
+ }
49089
+ var import_d3_geo3, FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
49090
+ var init_dimensions = __esm({
49091
+ "src/map/dimensions.ts"() {
49092
+ "use strict";
49093
+ import_d3_geo3 = require("d3-geo");
49094
+ init_title_constants();
49095
+ init_layout15();
49096
+ FIT_PAD2 = 24;
49097
+ TITLE_GAP = 16;
49098
+ ASPECT_MAX = 3;
49099
+ ASPECT_MIN = 0.9;
49100
+ MIN_MAP_BAND = 200;
49101
+ FALLBACK_ASPECT = 1.5;
49102
+ REF = 1e3;
49103
+ }
49104
+ });
49105
+
47520
49106
  // src/map/load-data.ts
47521
49107
  var load_data_exports = {};
47522
49108
  __export(load_data_exports, {
@@ -47575,12 +49161,17 @@ function loadMapData() {
47575
49161
  mountainRanges,
47576
49162
  naLand,
47577
49163
  naLakes,
49164
+ waterBodies,
47578
49165
  gazetteer
47579
49166
  ] = await Promise.all([
49167
+ // worldCoarse (110m) is LOAD-BEARING but NOT a render source: the world
49168
+ // basemap renders from worldDetail (50m) at all scales (resolver pins
49169
+ // basemaps.world = 'detail'). Coarse stays as the authoritative region
49170
+ // name index + dominant-landmass bbox source in resolver.ts. Do not drop it.
47580
49171
  readJson(nb, dir, FILES.worldCoarse),
47581
49172
  readJson(nb, dir, FILES.worldDetail),
47582
49173
  readJson(nb, dir, FILES.usStates),
47583
- // Lakes/rivers/mountain/NA assets are optional — older bundles may predate them.
49174
+ // Lakes/rivers/mountain/NA/water assets are optional — older bundles may predate them.
47584
49175
  readJson(nb, dir, FILES.lakes).catch(() => void 0),
47585
49176
  readJson(nb, dir, FILES.rivers).catch(() => void 0),
47586
49177
  readJson(nb, dir, FILES.mountainRanges).catch(
@@ -47588,6 +49179,7 @@ function loadMapData() {
47588
49179
  ),
47589
49180
  readJson(nb, dir, FILES.naLand).catch(() => void 0),
47590
49181
  readJson(nb, dir, FILES.naLakes).catch(() => void 0),
49182
+ readJson(nb, dir, FILES.waterBodies).catch(() => void 0),
47591
49183
  readJson(nb, dir, FILES.gazetteer)
47592
49184
  ]);
47593
49185
  return validate({
@@ -47599,7 +49191,8 @@ function loadMapData() {
47599
49191
  ...rivers && { rivers },
47600
49192
  ...mountainRanges && { mountainRanges },
47601
49193
  ...naLand && { naLand },
47602
- ...naLakes && { naLakes }
49194
+ ...naLakes && { naLakes },
49195
+ ...waterBodies && { waterBodies }
47603
49196
  });
47604
49197
  })().catch((e) => {
47605
49198
  cache = void 0;
@@ -47621,6 +49214,7 @@ var init_load_data = __esm({
47621
49214
  mountainRanges: "mountain-ranges.json",
47622
49215
  naLand: "na-land.json",
47623
49216
  naLakes: "na-lakes.json",
49217
+ waterBodies: "water-bodies.json",
47624
49218
  gazetteer: "gazetteer.json"
47625
49219
  };
47626
49220
  CANDIDATE_DIRS = [
@@ -49633,8 +51227,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
49633
51227
  const lines = splitParticipantLabel(p.label, LABEL_MAX_CHARS);
49634
51228
  if (lines.length === 0) continue;
49635
51229
  const widest = Math.max(...lines.map((l) => l.length));
49636
- const labelWidth = widest * LABEL_CHAR_WIDTH + 10;
49637
- uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth);
51230
+ const labelWidth2 = widest * LABEL_CHAR_WIDTH + 10;
51231
+ uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth2);
49638
51232
  }
49639
51233
  uniformBoxWidth = Math.min(MAX_BOX_WIDTH, uniformBoxWidth);
49640
51234
  const effectiveGap = Math.max(PARTICIPANT_GAP, uniformBoxWidth + 30);
@@ -52329,15 +53923,15 @@ function renderArcDiagram(container, parsed, palette, _isDark, onClickItem, expo
52329
53923
  textColor,
52330
53924
  onClickItem
52331
53925
  );
52332
- const neighbors = /* @__PURE__ */ new Map();
52333
- for (const node of nodes) neighbors.set(node, /* @__PURE__ */ new Set());
53926
+ const neighbors2 = /* @__PURE__ */ new Map();
53927
+ for (const node of nodes) neighbors2.set(node, /* @__PURE__ */ new Set());
52334
53928
  for (const link of links) {
52335
- neighbors.get(link.source).add(link.target);
52336
- neighbors.get(link.target).add(link.source);
53929
+ neighbors2.get(link.source).add(link.target);
53930
+ neighbors2.get(link.target).add(link.source);
52337
53931
  }
52338
53932
  const FADE_OPACITY3 = 0.1;
52339
53933
  function handleMouseEnter(hovered) {
52340
- const connected = neighbors.get(hovered);
53934
+ const connected = neighbors2.get(hovered);
52341
53935
  g.selectAll(".arc-link").each(function() {
52342
53936
  const el = d3Selection23.select(this);
52343
53937
  const src = el.attr("data-source");
@@ -54272,7 +55866,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54272
55866
  8,
54273
55867
  Math.floor(OVERLAP_WRAP_TARGET_W / OVERLAP_CH_W)
54274
55868
  );
54275
- function wrapLabel2(text, maxChars) {
55869
+ function wrapLabel3(text, maxChars) {
54276
55870
  const words = text.split(/\s+/).filter(Boolean);
54277
55871
  const lines = [];
54278
55872
  let cur = "";
@@ -54318,7 +55912,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54318
55912
  if (!ov.label) continue;
54319
55913
  const idxs = ov.sets.map((s) => vennSets.findIndex((vs) => vs.name === s));
54320
55914
  if (idxs.some((idx) => idx < 0)) continue;
54321
- const lines = wrapLabel2(ov.label, MAX_WRAP_CHARS);
55915
+ const lines = wrapLabel3(ov.label, MAX_WRAP_CHARS);
54322
55916
  wrappedOverlapLabels.set(ov, lines);
54323
55917
  const dir = predictOverlapDirRaw(idxs);
54324
55918
  const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
@@ -55756,6 +57350,7 @@ async function renderForExport(content, theme, palette, viewState, options) {
55756
57350
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55757
57351
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55758
57352
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
57353
+ const { mapExportDimensions: mapExportDimensions2 } = await Promise.resolve().then(() => (init_dimensions(), dimensions_exports));
55759
57354
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55760
57355
  const mapParsed = parseMap2(content);
55761
57356
  let mapData = options?.mapData;
@@ -55768,14 +57363,15 @@ async function renderForExport(content, theme, palette, viewState, options) {
55768
57363
  }
55769
57364
  }
55770
57365
  const mapResolved = resolveMap2(mapParsed, mapData);
55771
- const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
57366
+ const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57367
+ const container2 = createExportContainer(dims2.width, dims2.height);
55772
57368
  renderMapForExport2(
55773
57369
  container2,
55774
57370
  mapResolved,
55775
57371
  mapData,
55776
57372
  effectivePalette2,
55777
57373
  theme === "dark",
55778
- { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
57374
+ dims2
55779
57375
  );
55780
57376
  return finalizeSvgExport(container2, theme, effectivePalette2);
55781
57377
  }
@@ -56639,7 +58235,8 @@ async function render(content, options) {
56639
58235
  ...options?.c4Container !== void 0 && {
56640
58236
  c4Container: options.c4Container
56641
58237
  },
56642
- ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup }
58238
+ ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup },
58239
+ ...options?.mapData !== void 0 && { mapData: options.mapData }
56643
58240
  });
56644
58241
  if (chartType === "map") {
56645
58242
  try {
@@ -56650,7 +58247,7 @@ async function render(content, options) {
56650
58247
  Promise.resolve().then(() => (init_load_data(), load_data_exports))
56651
58248
  ]
56652
58249
  );
56653
- const data = await loadMapData2();
58250
+ const data = options?.mapData ?? await loadMapData2();
56654
58251
  diagnostics = [...resolveMap2(parseMap2(content), data).diagnostics];
56655
58252
  } catch {
56656
58253
  }