@diagrammo/dgmo 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +16 -6
  2. package/dist/advanced.cjs +2521 -623
  3. package/dist/advanced.d.cts +917 -534
  4. package/dist/advanced.d.ts +917 -534
  5. package/dist/advanced.js +2516 -623
  6. package/dist/auto.cjs +2333 -608
  7. package/dist/auto.js +119 -119
  8. package/dist/auto.mjs +2335 -609
  9. package/dist/cli.cjs +168 -168
  10. package/dist/editor.cjs +13 -15
  11. package/dist/editor.js +13 -15
  12. package/dist/highlight.cjs +15 -12
  13. package/dist/highlight.js +15 -12
  14. package/dist/index.cjs +2317 -595
  15. package/dist/index.d.cts +4 -1
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.js +2319 -596
  18. package/dist/internal.cjs +2521 -623
  19. package/dist/internal.d.cts +917 -534
  20. package/dist/internal.d.ts +917 -534
  21. package/dist/internal.js +2516 -623
  22. package/dist/map-data/PROVENANCE.json +1 -1
  23. package/dist/map-data/mountain-ranges.json +1 -0
  24. package/dist/map-data/water-bodies.json +1 -0
  25. package/docs/language-reference.md +44 -31
  26. package/gallery/fixtures/map-categorical-world.dgmo +16 -0
  27. package/gallery/fixtures/map-categorical.dgmo +0 -1
  28. package/gallery/fixtures/map-choropleth.dgmo +0 -1
  29. package/gallery/fixtures/map-coastline.dgmo +7 -0
  30. package/gallery/fixtures/map-colorize.dgmo +11 -0
  31. package/gallery/fixtures/map-direct-color.dgmo +9 -0
  32. package/gallery/fixtures/map-reference-world.dgmo +11 -0
  33. package/gallery/fixtures/map-region-scope.dgmo +0 -3
  34. package/gallery/fixtures/map-route.dgmo +0 -1
  35. package/package.json +1 -1
  36. package/src/advanced.ts +26 -1
  37. package/src/boxes-and-lines/renderer.ts +39 -12
  38. package/src/cli.ts +1 -1
  39. package/src/completion.ts +32 -24
  40. package/src/cycle/renderer.ts +14 -1
  41. package/src/d3.ts +23 -11
  42. package/src/editor/highlight-api.ts +4 -0
  43. package/src/editor/keywords.ts +13 -15
  44. package/src/infra/renderer.ts +35 -7
  45. package/src/map/colorize.ts +54 -0
  46. package/src/map/context-labels.ts +429 -0
  47. package/src/map/data/PROVENANCE.json +1 -1
  48. package/src/map/data/mountain-ranges.json +1 -0
  49. package/src/map/data/types.ts +34 -0
  50. package/src/map/data/water-bodies.json +1 -0
  51. package/src/map/dimensions.ts +117 -0
  52. package/src/map/geo-query.ts +295 -0
  53. package/src/map/geo.ts +305 -2
  54. package/src/map/invert.ts +111 -0
  55. package/src/map/layout.ts +1504 -335
  56. package/src/map/load-data.ts +16 -2
  57. package/src/map/parser.ts +57 -111
  58. package/src/map/renderer.ts +556 -13
  59. package/src/map/resolved-types.ts +24 -2
  60. package/src/map/resolver.ts +237 -67
  61. package/src/map/types.ts +39 -23
  62. package/src/mindmap/renderer.ts +10 -1
  63. package/src/palettes/atlas.ts +77 -0
  64. package/src/palettes/blueprint.ts +73 -0
  65. package/src/palettes/color-utils.ts +58 -1
  66. package/src/palettes/index.ts +12 -3
  67. package/src/palettes/slate.ts +73 -0
  68. package/src/palettes/tidewater.ts +73 -0
  69. package/src/render.ts +8 -1
  70. package/src/tech-radar/renderer.ts +3 -0
  71. package/src/tech-radar/types.ts +3 -0
  72. package/src/utils/d3-types.ts +5 -0
  73. package/src/utils/legend-layout.ts +21 -4
  74. package/src/utils/legend-types.ts +7 -0
  75. package/src/utils/reserved-key-registry.ts +3 -0
  76. package/src/palettes/bold.ts +0 -67
package/dist/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,28 +16364,13 @@ 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
- case "region-metric":
16367
+ case "region-metric": {
15954
16368
  dup(d.regionMetric);
15955
- d.regionMetric = value;
16369
+ const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
16370
+ d.regionMetric = rmLabel;
16371
+ if (rmColor) d.regionMetricColor = rmColor;
15956
16372
  break;
16373
+ }
15957
16374
  case "poi-metric":
15958
16375
  dup(d.poiMetric);
15959
16376
  d.poiMetric = value;
@@ -15962,85 +16379,43 @@ function parseMap(content) {
15962
16379
  dup(d.flowMetric);
15963
16380
  d.flowMetric = value;
15964
16381
  break;
15965
- case "scale":
15966
- dup(d.scale);
15967
- {
15968
- const s = parseScale(value, line12);
15969
- if (s) d.scale = s;
15970
- }
15971
- break;
15972
- case "region-labels":
15973
- dup(d.regionLabels);
15974
- if (value && !["full", "abbrev", "off"].includes(value))
15975
- pushWarning(
15976
- line12,
15977
- `Unknown region-labels "${value}" (expected full | abbrev | off).`
15978
- );
15979
- d.regionLabels = value;
15980
- break;
15981
- case "poi-labels":
15982
- dup(d.poiLabels);
15983
- if (value && !["off", "auto", "all"].includes(value))
15984
- pushWarning(
15985
- line12,
15986
- `Unknown poi-labels "${value}" (expected off | auto | all).`
15987
- );
15988
- d.poiLabels = value;
15989
- break;
15990
- case "default-country":
15991
- dup(d.defaultCountry);
15992
- d.defaultCountry = value;
15993
- break;
15994
- case "default-state":
15995
- dup(d.defaultState);
15996
- d.defaultState = value;
16382
+ case "locale":
16383
+ dup(d.locale);
16384
+ d.locale = value;
15997
16385
  break;
15998
16386
  case "active-tag":
15999
16387
  dup(d.activeTag);
16000
16388
  d.activeTag = value;
16001
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. ──
16002
16396
  case "no-legend":
16003
16397
  d.noLegend = true;
16004
16398
  break;
16005
- case "muted":
16006
- case "natural":
16007
- if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
16008
- pushWarning(
16009
- line12,
16010
- `Conflicting basemap dress \u2014 "${d.basemapStyle}" then "${key}"; last wins.`
16011
- );
16012
- d.basemapStyle = key;
16399
+ case "no-coastline":
16400
+ d.noCoastline = true;
16013
16401
  break;
16014
- case "subtitle":
16015
- dup(d.subtitle);
16016
- d.subtitle = value;
16402
+ case "no-relief":
16403
+ d.noRelief = true;
16017
16404
  break;
16018
- case "caption":
16019
- dup(d.caption);
16020
- d.caption = value;
16405
+ case "no-context-labels":
16406
+ d.noContextLabels = true;
16407
+ break;
16408
+ case "no-region-labels":
16409
+ d.noRegionLabels = true;
16410
+ break;
16411
+ case "no-poi-labels":
16412
+ d.noPoiLabels = true;
16413
+ break;
16414
+ case "no-colorize":
16415
+ d.noColorize = true;
16021
16416
  break;
16022
16417
  }
16023
16418
  }
16024
- function parseScale(value, line12) {
16025
- const toks = value.split(/\s+/).filter(Boolean);
16026
- const min = Number(toks[0]);
16027
- const max = Number(toks[1]);
16028
- if (!Number.isFinite(min) || !Number.isFinite(max)) {
16029
- pushError(line12, `scale requires numeric <min> <max> (got "${value}").`);
16030
- return null;
16031
- }
16032
- const scale = { min, max };
16033
- if (toks[2] === "center") {
16034
- const c = Number(toks[3]);
16035
- if (Number.isFinite(c)) scale.center = c;
16036
- else
16037
- pushError(
16038
- line12,
16039
- `scale center requires a number (got "${toks[3] ?? ""}").`
16040
- );
16041
- }
16042
- return scale;
16043
- }
16044
16419
  function handleTag(trimmed, line12) {
16045
16420
  const m = matchTagBlockHeading(trimmed);
16046
16421
  if (!m) {
@@ -16114,6 +16489,7 @@ function parseMap(content) {
16114
16489
  };
16115
16490
  if (regionScope !== void 0) region.scope = regionScope;
16116
16491
  if (valueNum !== void 0) region.value = valueNum;
16492
+ if (split.color) region.color = split.color;
16117
16493
  regions.push(region);
16118
16494
  }
16119
16495
  function handlePoi(rest, line12, indent) {
@@ -16138,6 +16514,7 @@ function parseMap(content) {
16138
16514
  const poi = { pos, tags, meta, lineNumber: line12 };
16139
16515
  if (split.alias) poi.alias = split.alias;
16140
16516
  if (label !== void 0) poi.label = label;
16517
+ if (split.color) poi.color = split.color;
16141
16518
  pois.push(poi);
16142
16519
  open.poi = { poi, indent };
16143
16520
  }
@@ -16238,13 +16615,15 @@ function parseMap(content) {
16238
16615
  pushError(line12, `Edge has an empty endpoint: "${trimmed}".`);
16239
16616
  continue;
16240
16617
  }
16241
- 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";
16242
16621
  edges.push({
16243
16622
  from,
16244
16623
  to,
16245
16624
  ...links[k].label !== void 0 && { label: links[k].label },
16246
16625
  directed: links[k].directed,
16247
- style: links[k].style,
16626
+ style,
16248
16627
  meta,
16249
16628
  lineNumber: line12
16250
16629
  });
@@ -16330,20 +16709,19 @@ var init_parser12 = __esm({
16330
16709
  LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
16331
16710
  AT_RE = /(^|[\s,])at\s*:/i;
16332
16711
  DIRECTIVE_SET = /* @__PURE__ */ new Set([
16333
- "region",
16334
- "projection",
16335
16712
  "region-metric",
16336
16713
  "poi-metric",
16337
16714
  "flow-metric",
16338
- "scale",
16339
- "region-labels",
16340
- "poi-labels",
16341
- "default-country",
16342
- "default-state",
16715
+ "locale",
16343
16716
  "active-tag",
16717
+ "caption",
16344
16718
  "no-legend",
16345
- "subtitle",
16346
- "caption"
16719
+ "no-coastline",
16720
+ "no-relief",
16721
+ "no-context-labels",
16722
+ "no-region-labels",
16723
+ "no-poi-labels",
16724
+ "no-colorize"
16347
16725
  ]);
16348
16726
  }
16349
16727
  });
@@ -24264,8 +24642,8 @@ function renderKanban(container, parsed, palette, isDark, options) {
24264
24642
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24265
24643
  for (const meta of tagMeta) {
24266
24644
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(`${meta.label}: `);
24267
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24268
- 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);
24269
24647
  metaY += sCardMetaLineHeight;
24270
24648
  }
24271
24649
  for (const detail of card.details) {
@@ -24609,8 +24987,8 @@ function renderSwimlaneCard(parent, cardLayout, tagGroups, activeTagGroup, palet
24609
24987
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24610
24988
  for (const meta of tagMeta) {
24611
24989
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", palette.textMuted).text(`${meta.label}: `);
24612
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24613
- 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);
24614
24992
  metaY += sCardMetaLineHeight;
24615
24993
  }
24616
24994
  for (const detail of card.details) {
@@ -25445,8 +25823,8 @@ function classifyEREntities(tables, relationships) {
25445
25823
  }
25446
25824
  }
25447
25825
  const mmParticipants = /* @__PURE__ */ new Set();
25448
- for (const [id, neighbors] of tableStarNeighbors) {
25449
- if (neighbors.size >= 2) mmParticipants.add(id);
25826
+ for (const [id, neighbors2] of tableStarNeighbors) {
25827
+ if (neighbors2.size >= 2) mmParticipants.add(id);
25450
25828
  }
25451
25829
  const indegreeValues = Object.values(indegreeMap);
25452
25830
  const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
@@ -26117,7 +26495,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26117
26495
  controlsExpanded,
26118
26496
  onToggleDescriptions,
26119
26497
  onToggleControlsExpand,
26120
- exportMode = false
26498
+ exportMode = false,
26499
+ controlsHost
26121
26500
  } = options ?? {};
26122
26501
  d3Selection6.select(container).selectAll(":not([data-d3-tooltip])").remove();
26123
26502
  const width = exportDims?.width ?? container.clientWidth;
@@ -26135,7 +26514,11 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26135
26514
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26136
26515
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26137
26516
  const sTitleY = sctx.structural(TITLE_Y);
26138
- 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(
26139
26522
  getMaxLegendReservedHeight(
26140
26523
  {
26141
26524
  groups: parsed.tagGroups,
@@ -26144,7 +26527,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26144
26527
  },
26145
26528
  width
26146
26529
  )
26147
- );
26530
+ ) : 0;
26148
26531
  const activeGroup = resolveActiveTagGroup(
26149
26532
  parsed.tagGroups,
26150
26533
  parsed.options["active-tag"],
@@ -26459,10 +26842,10 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26459
26842
  const hasDescriptions = parsed.nodes.some(
26460
26843
  (n) => n.description && n.description.length > 0
26461
26844
  );
26462
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions;
26845
+ const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26463
26846
  if (hasLegend) {
26464
26847
  let controlsGroup;
26465
- if (hasDescriptions && onToggleDescriptions) {
26848
+ if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
26466
26849
  controlsGroup = {
26467
26850
  toggles: [
26468
26851
  {
@@ -26480,7 +26863,14 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26480
26863
  groups: parsed.tagGroups,
26481
26864
  position: { placement: "top-center", titleRelation: "below-title" },
26482
26865
  mode: exportMode ? "export" : "preview",
26483
- ...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 }
26484
26874
  };
26485
26875
  const legendState = {
26486
26876
  activeGroup,
@@ -27728,8 +28118,9 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27728
28118
  const containerHeight = exportDims?.height ?? (container.getBoundingClientRect().height || 600);
27729
28119
  d3Selection7.select(container).selectAll("*").remove();
27730
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";
27731
28122
  const hasControls = !!options?.onToggleColorByDepth || !!options?.onToggleDescriptions;
27732
- const hasLegend = parsed.tagGroups.length > 0 || hasControls;
28123
+ const hasLegend = parsed.tagGroups.length > 0 || hasControls && !appHosted;
27733
28124
  const fixedLegend = !isExport && hasLegend;
27734
28125
  const legendReserve = fixedLegend ? getMaxLegendReservedHeight(
27735
28126
  {
@@ -27823,7 +28214,10 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27823
28214
  }),
27824
28215
  position: { placement: "top-center", titleRelation: "below-title" },
27825
28216
  mode: options?.exportMode ? "export" : "preview",
27826
- ...controlsToggles !== void 0 && { controlsGroup: controlsToggles }
28217
+ ...controlsToggles !== void 0 && { controlsGroup: controlsToggles },
28218
+ ...options?.controlsHost !== void 0 && {
28219
+ controlsHost: options.controlsHost
28220
+ }
27827
28221
  };
27828
28222
  const legendState = {
27829
28223
  activeGroup: options?.colorByDepth ? null : activeTagGroup !== void 0 ? activeTagGroup : parsed.options["active-tag"] ?? null,
@@ -28267,8 +28661,8 @@ function computeFieldAlignX(children) {
28267
28661
  for (const child of children) {
28268
28662
  if (child.metadata["_labelField"] === "true" && child.children.length >= 2) {
28269
28663
  const labelEl = child.children[0];
28270
- const labelWidth = labelEl.label.length * CHAR_WIDTH5;
28271
- maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
28664
+ const labelWidth2 = labelEl.label.length * CHAR_WIDTH5;
28665
+ maxLabelWidth = Math.max(maxLabelWidth, labelWidth2);
28272
28666
  labelFieldCount++;
28273
28667
  }
28274
28668
  }
@@ -33232,7 +33626,7 @@ function hasRoles(node) {
33232
33626
  function computeNodeWidth2(node, expanded, options) {
33233
33627
  const badgeVal = node.computedConcurrentInvocations === 0 && node.computedInstances > 1 ? node.computedInstances : 0;
33234
33628
  const badgeLen = badgeVal > 0 ? `${badgeVal}x`.length + 2 : 0;
33235
- const labelWidth = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33629
+ const labelWidth2 = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33236
33630
  const allKeys = [];
33237
33631
  if (node.computedRps > 0) allKeys.push("RPS");
33238
33632
  if (expanded) {
@@ -33276,7 +33670,7 @@ function computeNodeWidth2(node, expanded, options) {
33276
33670
  allKeys.push("overflow");
33277
33671
  }
33278
33672
  }
33279
- if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth);
33673
+ if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth2);
33280
33674
  const maxKeyLen = Math.max(...allKeys.map((k) => k.length));
33281
33675
  let maxRowWidth = 0;
33282
33676
  if (node.computedRps > 0) {
@@ -33364,7 +33758,7 @@ function computeNodeWidth2(node, expanded, options) {
33364
33758
  truncated.length * META_CHAR_WIDTH3 + PADDING_X3
33365
33759
  );
33366
33760
  }
33367
- return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20, descWidth);
33761
+ return Math.max(MIN_NODE_WIDTH2, labelWidth2, maxRowWidth + 20, descWidth);
33368
33762
  }
33369
33763
  function computeNodeHeight2(node, expanded, options) {
33370
33764
  const propCount = countDisplayProps(node, expanded, options);
@@ -34913,8 +35307,9 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
34913
35307
  }
34914
35308
  return groups;
34915
35309
  }
34916
- 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) {
34917
35311
  if (legendGroups.length === 0 && !playback) return;
35312
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34918
35313
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
34919
35314
  if (activeGroup) {
34920
35315
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
@@ -34923,14 +35318,29 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34923
35318
  name: g.name,
34924
35319
  entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
34925
35320
  }));
34926
- if (playback) {
35321
+ if (playback && !appHostedPlayback) {
34927
35322
  allGroups.push({ name: "Playback", entries: [] });
34928
35323
  }
34929
35324
  const legendConfig = {
34930
35325
  groups: allGroups,
34931
35326
  position: { placement: "top-center", titleRelation: "below-title" },
34932
35327
  mode: exportMode ? "export" : "preview",
34933
- 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
+ }
34934
35344
  };
34935
35345
  const legendState = { activeGroup };
34936
35346
  renderLegendD3(
@@ -34981,8 +35391,9 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34981
35391
  }
34982
35392
  }
34983
35393
  }
34984
- 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) {
34985
35395
  d3Selection11.select(container).selectAll(":not([data-d3-tooltip])").remove();
35396
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34986
35397
  const ctx = ScaleContext.identity();
34987
35398
  const sc = buildScaledConstants(ctx);
34988
35399
  const legendGroups = computeInfraLegendGroups(
@@ -34991,7 +35402,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
34991
35402
  palette,
34992
35403
  layout.edges
34993
35404
  );
34994
- const hasLegend = legendGroups.length > 0 || !!playback;
35405
+ const hasLegend = legendGroups.length > 0 || !!playback && !appHostedPlayback;
34995
35406
  const fixedLegend = !exportMode && hasLegend;
34996
35407
  const legendDynamicH = hasLegend ? getMaxLegendReservedHeight(
34997
35408
  {
@@ -35135,7 +35546,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35135
35546
  isDark,
35136
35547
  activeGroup ?? null,
35137
35548
  playback ?? void 0,
35138
- exportMode
35549
+ exportMode,
35550
+ controlsHost
35139
35551
  );
35140
35552
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
35141
35553
  } else {
@@ -35148,7 +35560,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35148
35560
  isDark,
35149
35561
  activeGroup ?? null,
35150
35562
  playback ?? void 0,
35151
- exportMode
35563
+ exportMode,
35564
+ controlsHost
35152
35565
  );
35153
35566
  }
35154
35567
  }
@@ -42783,6 +43196,9 @@ function renderTechRadar(container, parsed, palette, isDark, onClickItem, export
42783
43196
  onToggle: (active) => options.onToggleListing(active)
42784
43197
  }
42785
43198
  ]
43199
+ },
43200
+ ...options.controlsHost !== void 0 && {
43201
+ controlsHost: options.controlsHost
42786
43202
  }
42787
43203
  };
42788
43204
  const legendState = {
@@ -44606,7 +45022,7 @@ function computeCycleLayout(parsed, options) {
44606
45022
  const circleNodes = parsed.options["circle-nodes"] === "true";
44607
45023
  const nodeDims = parsed.nodes.map((node) => {
44608
45024
  const hasDesc = !hideDescriptions && node.description.length > 0;
44609
- const labelWidth = Math.max(
45025
+ const labelWidth2 = Math.max(
44610
45026
  MIN_NODE_WIDTH4,
44611
45027
  node.label.length * LABEL_CHAR_W + NODE_PAD_X * 2
44612
45028
  );
@@ -44615,12 +45031,12 @@ function computeCycleLayout(parsed, options) {
44615
45031
  }
44616
45032
  if (!hasDesc) {
44617
45033
  return {
44618
- width: Math.min(MAX_NODE_WIDTH3, labelWidth),
45034
+ width: Math.min(MAX_NODE_WIDTH3, labelWidth2),
44619
45035
  height: PLAIN_NODE_HEIGHT,
44620
45036
  wrappedDesc: []
44621
45037
  };
44622
45038
  }
44623
- return chooseDescribedRectDims(node.description, labelWidth);
45039
+ return chooseDescribedRectDims(node.description, labelWidth2);
44624
45040
  });
44625
45041
  if (circleNodes) {
44626
45042
  const maxDiam = Math.max(...nodeDims.map((d) => d.width));
@@ -44816,10 +45232,10 @@ function computeCycleLayout(parsed, options) {
44816
45232
  scale
44817
45233
  };
44818
45234
  }
44819
- function chooseDescribedRectDims(description, labelWidth) {
45235
+ function chooseDescribedRectDims(description, labelWidth2) {
44820
45236
  const minW = Math.min(
44821
45237
  MAX_NODE_WIDTH3,
44822
- Math.max(MIN_NODE_WIDTH4, labelWidth, DESC_MIN_WIDTH)
45238
+ Math.max(MIN_NODE_WIDTH4, labelWidth2, DESC_MIN_WIDTH)
44823
45239
  );
44824
45240
  let best = null;
44825
45241
  let bestScore = Infinity;
@@ -45247,7 +45663,8 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45247
45663
  const hideDescriptions = (renderOptions?.hideDescriptions ?? false) || parsed.options["no-descriptions"] === "true" || viewState?.hd === true;
45248
45664
  const showDescriptions = !hideDescriptions;
45249
45665
  const hasDescriptions = parsed.nodes.some((n) => n.description.length > 0) || parsed.edges.some((e) => e.description.length > 0);
45250
- const hasLegend = hasDescriptions && !!renderOptions?.onToggleDescriptions;
45666
+ const appHostedControls = renderOptions?.controlsHost === "app";
45667
+ const hasLegend = !appHostedControls && hasDescriptions && !!renderOptions?.onToggleDescriptions;
45251
45668
  const showTitle = !!parsed.title && parsed.options["no-title"] !== "on";
45252
45669
  const legendOffset = hasLegend ? sLegendHeight : 0;
45253
45670
  const layoutHeight = height - (showTitle ? sTitleAreaHeight : 0) - legendOffset;
@@ -45284,7 +45701,10 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45284
45701
  groups: [],
45285
45702
  position: { placement: "top-center", titleRelation: "below-title" },
45286
45703
  mode: renderOptions?.exportMode ? "export" : "preview",
45287
- controlsGroup
45704
+ controlsGroup,
45705
+ ...renderOptions?.controlsHost !== void 0 && {
45706
+ controlsHost: renderOptions.controlsHost
45707
+ }
45288
45708
  };
45289
45709
  const legendState = {
45290
45710
  activeGroup: null,
@@ -45555,6 +45975,107 @@ function featureIndex(topo) {
45555
45975
  }
45556
45976
  return idx;
45557
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
+ }
45558
46079
  function featureBbox(topo, geomId) {
45559
46080
  const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45560
46081
  if (!geom) return null;
@@ -45566,6 +46087,74 @@ function featureBbox(topo, geomId) {
45566
46087
  [b[1][0], b[1][1]]
45567
46088
  ];
45568
46089
  }
46090
+ function explodePolygons(gj) {
46091
+ const g = gj.geometry ?? gj;
46092
+ const t = g.type;
46093
+ const coords = g.coordinates;
46094
+ if (t === "Polygon") {
46095
+ return [
46096
+ { type: "Feature", geometry: { type: "Polygon", coordinates: coords } }
46097
+ ];
46098
+ }
46099
+ if (t === "MultiPolygon") {
46100
+ return coords.map((rings) => ({
46101
+ type: "Feature",
46102
+ geometry: { type: "Polygon", coordinates: rings }
46103
+ }));
46104
+ }
46105
+ return [];
46106
+ }
46107
+ function bboxGap(a, b) {
46108
+ const lonGap = Math.max(0, a[0][0] - b[1][0], b[0][0] - a[1][0]);
46109
+ const latGap = Math.max(0, a[0][1] - b[1][1], b[0][1] - a[1][1]);
46110
+ return Math.max(lonGap, latGap);
46111
+ }
46112
+ function featureBboxPrimary(topo, geomId) {
46113
+ const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
46114
+ if (!geom) return null;
46115
+ const gj = (0, import_topojson_client.feature)(topo, geom);
46116
+ const parts = explodePolygons(gj);
46117
+ if (parts.length <= 1) return featureBbox(topo, geomId);
46118
+ const polys = parts.map((p) => {
46119
+ const b = (0, import_d3_geo.geoBounds)(p);
46120
+ if (!b || !Number.isFinite(b[0][0])) return null;
46121
+ const wraps = b[1][0] < b[0][0];
46122
+ const bbox = [
46123
+ [b[0][0], b[0][1]],
46124
+ [b[1][0], b[1][1]]
46125
+ ];
46126
+ return { bbox, area: (0, import_d3_geo.geoArea)(p), wraps };
46127
+ }).filter(
46128
+ (p) => p !== null
46129
+ );
46130
+ if (polys.length <= 1 || polys.some((p) => p.wraps))
46131
+ return featureBbox(topo, geomId);
46132
+ const maxArea = Math.max(...polys.map((p) => p.area));
46133
+ const anchor = polys.find((p) => p.area === maxArea);
46134
+ const cluster = [
46135
+ [anchor.bbox[0][0], anchor.bbox[0][1]],
46136
+ [anchor.bbox[1][0], anchor.bbox[1][1]]
46137
+ ];
46138
+ const remaining = polys.filter((p) => p !== anchor);
46139
+ let added = true;
46140
+ while (added) {
46141
+ added = false;
46142
+ for (let i = remaining.length - 1; i >= 0; i--) {
46143
+ const p = remaining[i];
46144
+ const near = bboxGap(p.bbox, cluster) <= DETACH_GAP_DEG;
46145
+ const large = p.area >= DETACH_AREA_FRAC * maxArea;
46146
+ if (near || large) {
46147
+ cluster[0][0] = Math.min(cluster[0][0], p.bbox[0][0]);
46148
+ cluster[0][1] = Math.min(cluster[0][1], p.bbox[0][1]);
46149
+ cluster[1][0] = Math.max(cluster[1][0], p.bbox[1][0]);
46150
+ cluster[1][1] = Math.max(cluster[1][1], p.bbox[1][1]);
46151
+ remaining.splice(i, 1);
46152
+ added = true;
46153
+ }
46154
+ }
46155
+ }
46156
+ return cluster;
46157
+ }
45569
46158
  function unionExtent(boxes, points) {
45570
46159
  const lats = [];
45571
46160
  const lons = [];
@@ -45604,13 +46193,17 @@ function unionLongitudes(lons) {
45604
46193
  }
45605
46194
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45606
46195
  }
45607
- var import_topojson_client, import_d3_geo, fold;
46196
+ var import_topojson_client, import_d3_geo, fold, adjacencyCache, EDGE_EPS, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45608
46197
  var init_geo = __esm({
45609
46198
  "src/map/geo.ts"() {
45610
46199
  "use strict";
45611
46200
  import_topojson_client = require("topojson-client");
45612
46201
  import_d3_geo = require("d3-geo");
45613
46202
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
46203
+ adjacencyCache = /* @__PURE__ */ new WeakMap();
46204
+ EDGE_EPS = 1e-9;
46205
+ DETACH_GAP_DEG = 10;
46206
+ DETACH_AREA_FRAC = 0.25;
45614
46207
  }
45615
46208
  });
45616
46209
 
@@ -45628,6 +46221,12 @@ function looksUS(lat, lon) {
45628
46221
  if (lat < 15 || lat > 72) return false;
45629
46222
  return lon >= -180 && lon <= -64 || lon >= 172;
45630
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
+ }
45631
46230
  function resolveMap(parsed, data) {
45632
46231
  const diagnostics = [...parsed.diagnostics];
45633
46232
  const err = (line12, message, code) => {
@@ -45638,9 +46237,6 @@ function resolveMap(parsed, data) {
45638
46237
  };
45639
46238
  const result = {
45640
46239
  title: parsed.title,
45641
- ...parsed.directives.subtitle !== void 0 && {
45642
- subtitle: parsed.directives.subtitle
45643
- },
45644
46240
  ...parsed.directives.caption !== void 0 && {
45645
46241
  caption: parsed.directives.caption
45646
46242
  },
@@ -45650,7 +46246,7 @@ function resolveMap(parsed, data) {
45650
46246
  // renderer's job (step 4) — the resolver only carries `tags` + `tagGroups`
45651
46247
  // through; it never resolves a tag value to a palette color (#10).
45652
46248
  directives: { ...parsed.directives },
45653
- basemaps: { world: "coarse", subdivisions: [] },
46249
+ basemaps: { world: "detail", subdivisions: [] },
45654
46250
  regions: [],
45655
46251
  pois: [],
45656
46252
  edges: [],
@@ -45659,7 +46255,8 @@ function resolveMap(parsed, data) {
45659
46255
  [-180, -85],
45660
46256
  [180, 85]
45661
46257
  ],
45662
- projection: "natural-earth",
46258
+ projection: "equirectangular",
46259
+ poiFrameContainers: [],
45663
46260
  diagnostics,
45664
46261
  error: parsed.error
45665
46262
  };
@@ -45669,7 +46266,10 @@ function resolveMap(parsed, data) {
45669
46266
  ...[...countryIndex.values()].map((v) => v.name),
45670
46267
  ...[...usStateIndex.values()].map((v) => v.name)
45671
46268
  ];
45672
- 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) => {
45673
46273
  const f = fold(r.name);
45674
46274
  return usStateIndex.has(f) && !countryIndex.has(f);
45675
46275
  }) || parsed.regions.some(
@@ -45714,12 +46314,12 @@ function resolveMap(parsed, data) {
45714
46314
  chosen = { ...inState, layer: "us-state" };
45715
46315
  } else {
45716
46316
  chosen = { ...inCountry, layer: "country" };
46317
+ warn(
46318
+ r.lineNumber,
46319
+ `"${r.name}" is both a country and a US state \u2014 resolved as ${chosen.layer} (${chosen.id}). Pin it with an ISO code (${inState.id} / ${inCountry.id}) or name + scope ("${r.name} US" / "${r.name} ${inCountry.id}").`,
46320
+ "W_MAP_REGION_AMBIGUOUS"
46321
+ );
45717
46322
  }
45718
- warn(
45719
- r.lineNumber,
45720
- `"${r.name}" is both a country and a US state \u2014 resolved as ${chosen.layer} (${chosen.id}). Pin it with an ISO code (${inState.id} / ${inCountry.id}) or name + scope ("${r.name} US" / "${r.name} ${inCountry.id}").`,
45721
- "W_MAP_REGION_AMBIGUOUS"
45722
- );
45723
46323
  } else if (inState) {
45724
46324
  chosen = { ...inState, layer: "us-state" };
45725
46325
  } else if (inCountry) {
@@ -45743,6 +46343,7 @@ function resolveMap(parsed, data) {
45743
46343
  name: chosen.name,
45744
46344
  layer: chosen.layer,
45745
46345
  ...r.value !== void 0 && { value: r.value },
46346
+ ...r.color !== void 0 && { color: r.color },
45746
46347
  tags: r.tags,
45747
46348
  meta: r.meta,
45748
46349
  lineNumber: r.lineNumber
@@ -45819,7 +46420,7 @@ function resolveMap(parsed, data) {
45819
46420
  if (!scope)
45820
46421
  warn(
45821
46422
  line12,
45822
- `"${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.`,
45823
46424
  "W_MAP_AMBIGUOUS_NAME"
45824
46425
  );
45825
46426
  }
@@ -45832,17 +46433,21 @@ function resolveMap(parsed, data) {
45832
46433
  return fold(pos.name);
45833
46434
  };
45834
46435
  const poiCountries = [];
45835
- let anyNonUsPoi = false;
46436
+ let anyUsPoi = false;
46437
+ let anyNonNaPoi = false;
45836
46438
  const noteCountry = (iso) => {
45837
46439
  if (iso) {
45838
46440
  poiCountries.push(iso);
45839
- if (iso !== "US") anyNonUsPoi = true;
46441
+ if (iso === "US") anyUsPoi = true;
46442
+ if (iso !== "US" && iso !== "CA" && iso !== "MX") anyNonNaPoi = true;
45840
46443
  }
45841
46444
  };
45842
46445
  const deferred = [];
45843
46446
  for (const p of parsed.pois) {
45844
46447
  if (p.pos.kind === "coords") {
45845
- 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;
45846
46451
  addResolvedPoi(p.pos.lat, p.pos.lon, p);
45847
46452
  continue;
45848
46453
  }
@@ -45860,14 +46465,15 @@ function resolveMap(parsed, data) {
45860
46465
  deferred.push(p);
45861
46466
  }
45862
46467
  }
45863
- 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;
45864
46470
  for (const p of deferred) {
45865
46471
  if (p.pos.kind !== "name") continue;
45866
46472
  const got = lookupName(
45867
46473
  p.pos.name,
45868
46474
  p.pos.scope,
45869
46475
  p.lineNumber,
45870
- inferredCountry,
46476
+ inferredScope,
45871
46477
  true
45872
46478
  );
45873
46479
  if (got.kind === "ok") {
@@ -45884,6 +46490,7 @@ function resolveMap(parsed, data) {
45884
46490
  lat,
45885
46491
  lon,
45886
46492
  ...p.label !== void 0 && { label: p.label },
46493
+ ...p.color !== void 0 && { color: p.color },
45887
46494
  tags: p.tags,
45888
46495
  meta: p.meta,
45889
46496
  lineNumber: p.lineNumber
@@ -45936,7 +46543,8 @@ function resolveMap(parsed, data) {
45936
46543
  const meta = sizeValue !== void 0 ? { value: sizeValue } : {};
45937
46544
  if (pos.kind === "coords") {
45938
46545
  const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
45939
- 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;
45940
46548
  if (!registry.has(id)) {
45941
46549
  registerPoi(
45942
46550
  id,
@@ -45959,7 +46567,7 @@ function resolveMap(parsed, data) {
45959
46567
  if (registry.has(f)) return f;
45960
46568
  const aliased = declaredByName.get(f);
45961
46569
  if (aliased) return aliased;
45962
- const got = lookupName(pos.name, pos.scope, line12, inferredCountry, true);
46570
+ const got = lookupName(pos.name, pos.scope, line12, inferredScope, true);
45963
46571
  if (got.kind !== "ok") return null;
45964
46572
  noteCountry(got.iso);
45965
46573
  registerPoi(
@@ -46016,9 +46624,12 @@ function resolveMap(parsed, data) {
46016
46624
  }
46017
46625
  routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
46018
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;
46019
46631
  const subdivisions = [];
46020
- if (usSubdivisionReferenced || parsed.directives.region === "us-states")
46021
- subdivisions.push("us-states");
46632
+ if (usSubdivisionReferenced || usOriented) subdivisions.push("us-states");
46022
46633
  const regionBoxes = [];
46023
46634
  for (const ref of referencedRegionIds) {
46024
46635
  const bb = featureBbox(data.usStates, ref.id);
@@ -46026,7 +46637,7 @@ function resolveMap(parsed, data) {
46026
46637
  }
46027
46638
  for (const r of regions) {
46028
46639
  if (r.layer === "country") {
46029
- const bb = featureBbox(data.worldCoarse, r.iso);
46640
+ const bb = featureBboxPrimary(data.worldCoarse, r.iso);
46030
46641
  if (bb) regionBoxes.push(bb);
46031
46642
  }
46032
46643
  }
@@ -46036,23 +46647,56 @@ function resolveMap(parsed, data) {
46036
46647
  [-180, -85],
46037
46648
  [180, 85]
46038
46649
  ];
46039
- 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
+ }
46040
46687
  const lonSpan = extent2[1][0] - extent2[0][0];
46041
46688
  const latSpan = extent2[1][1] - extent2[0][1];
46042
46689
  const span = Math.max(lonSpan, latSpan);
46043
- const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46690
+ const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
46044
46691
  let projection;
46045
- const override = parsed.directives.projection;
46046
- if (override === "equirectangular" || override === "natural-earth" || override === "albers-usa" || override === "mercator") {
46047
- projection = override;
46048
- } else if (usDominant) {
46692
+ if (isPoiOnly && usOriented && lonSpan < US_NATIONAL_LON_SPAN) {
46693
+ projection = "mercator";
46694
+ } else if (usOriented) {
46049
46695
  projection = "albers-usa";
46050
- } else if (span > WORLD_SPAN) {
46696
+ } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
46051
46697
  projection = "equirectangular";
46052
- } else if (span < MERCATOR_MAX_SPAN) {
46053
- projection = "mercator";
46054
46698
  } else {
46055
- projection = "equirectangular";
46699
+ projection = "mercator";
46056
46700
  }
46057
46701
  if (lonSpan >= 180) {
46058
46702
  extent2 = [
@@ -46065,11 +46709,20 @@ function resolveMap(parsed, data) {
46065
46709
  result.edges = edges;
46066
46710
  result.routes = routes;
46067
46711
  result.basemaps = {
46068
- 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",
46069
46721
  subdivisions
46070
46722
  };
46071
46723
  result.extent = extent2;
46072
46724
  result.projection = projection;
46725
+ result.poiFrameContainers = containerRegionIds;
46073
46726
  result.error = parsed.error ?? firstError(diagnostics);
46074
46727
  return result;
46075
46728
  }
@@ -46106,17 +46759,20 @@ function firstError(diags) {
46106
46759
  const e = diags.find((d) => d.severity === "error");
46107
46760
  return e ? formatDgmoError(e) : null;
46108
46761
  }
46109
- var WORLD_SPAN, MERCATOR_MAX_SPAN, 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;
46110
46763
  var init_resolver2 = __esm({
46111
46764
  "src/map/resolver.ts"() {
46112
46765
  "use strict";
46113
46766
  init_diagnostics();
46114
46767
  init_geo();
46115
46768
  WORLD_SPAN = 90;
46116
- MERCATOR_MAX_SPAN = 25;
46769
+ MERCATOR_MAX_LAT = 80;
46117
46770
  PAD_FRACTION = 0.05;
46771
+ REGION_PAD_FRACTION = 0.12;
46118
46772
  WORLD_LAT_SOUTH = -58;
46119
46773
  WORLD_LAT_NORTH = 78;
46774
+ POI_ZOOM_FLOOR_DEG = 7;
46775
+ US_NATIONAL_LON_SPAN = 48;
46120
46776
  REGION_ALIASES = {
46121
46777
  // Common everyday names → the Natural-Earth display name actually shipped.
46122
46778
  "united states": "united states of america",
@@ -46194,112 +46850,269 @@ var init_resolver2 = __esm({
46194
46850
  }
46195
46851
  });
46196
46852
 
46197
- // src/map/load-data.ts
46198
- var load_data_exports = {};
46199
- __export(load_data_exports, {
46200
- loadMapData: () => loadMapData
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
+ }
46201
46875
  });
46202
- async function loadNodeBuiltins() {
46203
- const [{ readFile }, { fileURLToPath }, { dirname, resolve }] = await Promise.all([
46204
- import("fs/promises"),
46205
- import("url"),
46206
- import("path")
46207
- ]);
46208
- return { readFile, fileURLToPath, dirname, resolve };
46209
- }
46210
- async function readJson(nb, dir, name) {
46211
- return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
46212
- }
46213
- async function firstExistingDir(nb, baseDir) {
46214
- for (const rel of CANDIDATE_DIRS) {
46215
- const dir = nb.resolve(baseDir, rel);
46216
- try {
46217
- await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
46218
- return dir;
46219
- } catch {
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]);
46220
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 };
46221
46936
  }
46222
- throw new Error(
46223
- `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
46224
- );
46937
+ return best?.lines ?? [text];
46225
46938
  }
46226
- function validate(data) {
46227
- const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
46228
- if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
46229
- throw new Error("map data assets are malformed (failed shape validation)");
46230
- }
46231
- return data;
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 };
46232
46943
  }
46233
- function moduleBaseDir(nb) {
46234
- try {
46235
- const url = import_meta.url;
46236
- if (url) return nb.dirname(nb.fileURLToPath(url));
46237
- } catch {
46238
- }
46239
- if (typeof __dirname !== "undefined") return __dirname;
46240
- return process.cwd();
46944
+ function rectFits(r, width, height) {
46945
+ return r.x >= 0 && r.y >= 0 && r.x + r.w <= width && r.y + r.h <= height;
46241
46946
  }
46242
- function loadMapData() {
46243
- cache ??= (async () => {
46244
- const nb = await loadNodeBuiltins();
46245
- const dir = await firstExistingDir(nb, moduleBaseDir(nb));
46246
- const [
46247
- worldCoarse,
46248
- worldDetail,
46249
- usStates,
46250
- lakes,
46251
- rivers,
46252
- naLand,
46253
- naLakes,
46254
- gazetteer
46255
- ] = await Promise.all([
46256
- readJson(nb, dir, FILES.worldCoarse),
46257
- readJson(nb, dir, FILES.worldDetail),
46258
- readJson(nb, dir, FILES.usStates),
46259
- // Lakes/rivers/NA assets are optional — older bundles may predate them.
46260
- readJson(nb, dir, FILES.lakes).catch(() => void 0),
46261
- readJson(nb, dir, FILES.rivers).catch(() => void 0),
46262
- readJson(nb, dir, FILES.naLand).catch(() => void 0),
46263
- readJson(nb, dir, FILES.naLakes).catch(() => void 0),
46264
- readJson(nb, dir, FILES.gazetteer)
46265
- ]);
46266
- return validate({
46267
- worldCoarse,
46268
- worldDetail,
46269
- usStates,
46270
- gazetteer,
46271
- ...lakes && { lakes },
46272
- ...rivers && { rivers },
46273
- ...naLand && { naLand },
46274
- ...naLakes && { naLakes }
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]
46275
47021
  });
46276
- })().catch((e) => {
46277
- cache = void 0;
46278
- throw e;
46279
- });
46280
- return cache;
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;
46281
47092
  }
46282
- var import_meta, FILES, CANDIDATE_DIRS, cache;
46283
- var init_load_data = __esm({
46284
- "src/map/load-data.ts"() {
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"() {
46285
47096
  "use strict";
46286
- import_meta = {};
46287
- FILES = {
46288
- worldCoarse: "world-coarse.json",
46289
- worldDetail: "world-detail.json",
46290
- usStates: "us-states.json",
46291
- lakes: "lakes.json",
46292
- rivers: "rivers.json",
46293
- naLand: "na-land.json",
46294
- naLakes: "na-lakes.json",
46295
- gazetteer: "gazetteer.json"
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
46296
47115
  };
46297
- CANDIDATE_DIRS = [
46298
- "./data",
46299
- "./map-data",
46300
- "../map-data",
46301
- "../src/map/data"
46302
- ];
46303
47116
  }
46304
47117
  });
46305
47118
 
@@ -46308,12 +47121,34 @@ function geomObject2(topo) {
46308
47121
  const key = Object.keys(topo.objects)[0];
46309
47122
  return topo.objects[key];
46310
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
+ }
46311
47140
  function decodeLayer(topo) {
47141
+ const cached = decodeCache.get(topo);
47142
+ if (cached) return cached;
46312
47143
  const out = /* @__PURE__ */ new Map();
46313
47144
  for (const g of geomObject2(topo).geometries) {
46314
47145
  const f = (0, import_topojson_client2.feature)(topo, g);
46315
- 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);
46316
47150
  }
47151
+ decodeCache.set(topo, out);
46317
47152
  return out;
46318
47153
  }
46319
47154
  function projectionFor(family) {
@@ -46322,38 +47157,35 @@ function projectionFor(family) {
46322
47157
  return usConusProjection();
46323
47158
  case "mercator":
46324
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)();
46325
47164
  case "natural-earth":
46326
47165
  return (0, import_d3_geo2.geoNaturalEarth1)();
46327
- case "equirectangular":
46328
47166
  default:
46329
47167
  return (0, import_d3_geo2.geoEquirectangular)();
46330
47168
  }
46331
47169
  }
46332
- function mapBackgroundColor(palette, isDark = false, dataActive = false) {
46333
- if (dataActive)
46334
- return mix(
46335
- palette.colors.gray,
46336
- palette.bg,
46337
- isDark ? MUTED_WATER_DARK : MUTED_WATER_LIGHT
46338
- );
46339
- return mix(palette.colors.blue, palette.bg, WATER_TINT);
47170
+ function mapBackgroundColor(palette, isDark = false, _dataActive = false) {
47171
+ return mix(
47172
+ palette.colors.blue,
47173
+ palette.bg,
47174
+ isDark ? WATER_TINT_DARK : WATER_TINT_LIGHT
47175
+ );
46340
47176
  }
46341
- function mapNeutralLandColor(palette, isDark, dataActive = false) {
46342
- if (dataActive)
46343
- return isDark ? mix(palette.colors.gray, palette.bg, MUTED_LAND_DARK) : palette.bg;
47177
+ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46344
47178
  return mix(
46345
47179
  palette.colors.green,
46346
47180
  palette.bg,
46347
47181
  isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT
46348
47182
  );
46349
47183
  }
46350
- function layoutMap(resolved, data, size, opts) {
46351
- const { palette, isDark } = opts;
46352
- const { width, height } = size;
47184
+ function buildMapProjection(resolved, data) {
46353
47185
  const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
46354
- const usCrisp = resolved.projection === "albers-usa" && wantsUsStates && !!data.naLand;
47186
+ const usCrisp = (resolved.projection === "albers-usa" || resolved.projection === "mercator") && wantsUsStates && !!data.naLand;
46355
47187
  const worldTopo = usCrisp ? data.worldDetail : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46356
- const worldLayer = decodeLayer(worldTopo);
47188
+ const worldLayer = new Map(decodeLayer(worldTopo));
46357
47189
  if (usCrisp && data.naLand) {
46358
47190
  const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
46359
47191
  const crisp = decodeLayer(data.naLand);
@@ -46362,17 +47194,110 @@ function layoutMap(resolved, data, size, opts) {
46362
47194
  if (!base) continue;
46363
47195
  const [[bw, bs], [be, bn]] = (0, import_d3_geo2.geoBounds)(base);
46364
47196
  if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
46365
- worldLayer.set(iso, cf);
47197
+ worldLayer.set(iso, { ...cf, properties: base.properties });
46366
47198
  }
46367
47199
  }
46368
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);
46369
47294
  const usContext = usLayer !== null;
46370
47295
  const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46371
47296
  const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
46372
- const scaleOverride = resolved.directives.scale;
46373
- const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46374
- const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
46375
- const rampHue = palette.colors.red;
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);
47300
+ const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46376
47301
  const hasRamp = values.length > 0;
46377
47302
  const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
46378
47303
  const matchColorGroup = (v) => {
@@ -46392,14 +47317,48 @@ function layoutMap(resolved, data, size, opts) {
46392
47317
  activeGroup = VALUE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46393
47318
  }
46394
47319
  const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
46395
- const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
47320
+ const mutedBasemap = activeGroup !== null;
46396
47321
  const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46397
47322
  const water = mapBackgroundColor(palette, isDark, mutedBasemap);
47323
+ const lakeStroke = mix(regionStroke, water, 45);
46398
47324
  const foreignFill = mix(
46399
47325
  palette.colors.gray,
46400
47326
  palette.bg,
46401
47327
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46402
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);
46403
47362
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46404
47363
  const fillForValue = (s) => {
46405
47364
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
@@ -46424,47 +47383,26 @@ function layoutMap(resolved, data, size, opts) {
46424
47383
  isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
46425
47384
  );
46426
47385
  };
47386
+ const directFill = (name) => {
47387
+ const hex = name ? resolveColor(name, palette) : null;
47388
+ if (!hex) return null;
47389
+ return mix(hex, palette.bg, isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT);
47390
+ };
46427
47391
  const regionFill = (r) => {
47392
+ const direct = directFill(r.color);
47393
+ if (direct) return direct;
46428
47394
  if (activeIsScore) {
46429
47395
  return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46430
47396
  }
47397
+ if (colorizeActive) return (r.iso && colorByIso.get(r.iso)) ?? neutralFill;
46431
47398
  return tagFill(r.tags, activeGroup) ?? neutralFill;
46432
47399
  };
46433
47400
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
46434
- const extentOutline = () => {
46435
- const [[w, s], [e, n]] = resolved.extent;
46436
- const N = 16;
46437
- const coords = [];
46438
- for (let i = 0; i <= N; i++) {
46439
- const t = i / N;
46440
- const lon = w + (e - w) * t;
46441
- const lat = s + (n - s) * t;
46442
- coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
46443
- }
46444
- return {
46445
- type: "Feature",
46446
- properties: {},
46447
- geometry: { type: "MultiPoint", coordinates: coords }
46448
- };
46449
- };
46450
- let fitFeatures;
46451
- if (resolved.projection === "albers-usa" && usLayer) {
46452
- fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
46453
- } else {
46454
- fitFeatures = [extentOutline()];
46455
- }
46456
- const fitTarget = { type: "FeatureCollection", features: fitFeatures };
46457
- const projection = projectionFor(resolved.projection);
46458
- if (resolved.projection !== "albers-usa") {
46459
- let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
46460
- if (centerLon > 180) centerLon -= 360;
46461
- projection.rotate([-centerLon, 0]);
46462
- }
46463
- const TITLE_GAP = 16;
47401
+ const TITLE_GAP2 = 16;
46464
47402
  let topPad = FIT_PAD;
46465
47403
  if (resolved.title && resolved.pois.length > 0) {
46466
47404
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
46467
- topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
47405
+ topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
46468
47406
  }
46469
47407
  const fitBox = [
46470
47408
  [FIT_PAD, topPad],
@@ -46474,11 +47412,10 @@ function layoutMap(resolved, data, size, opts) {
46474
47412
  ]
46475
47413
  ];
46476
47414
  projection.fitExtent(fitBox, fitTarget);
46477
- const fitGB = (0, import_d3_geo2.geoBounds)(fitTarget);
46478
- const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46479
47415
  let path;
46480
47416
  let project;
46481
- if (fitIsGlobal) {
47417
+ let stretchParams = null;
47418
+ if (fitIsGlobal && !opts.preferContain) {
46482
47419
  const cb = (0, import_d3_geo2.geoPath)(projection).bounds(fitTarget);
46483
47420
  const bx0 = cb[0][0];
46484
47421
  const by0 = cb[0][1];
@@ -46488,6 +47425,7 @@ function layoutMap(resolved, data, size, opts) {
46488
47425
  const oy = fitBox[0][1];
46489
47426
  const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46490
47427
  const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47428
+ stretchParams = { sx, sy, ox, oy, bx0, by0 };
46491
47429
  const stretch = (x, y) => [
46492
47430
  ox + (x - bx0) * sx,
46493
47431
  oy + (y - by0) * sy
@@ -46519,7 +47457,9 @@ function layoutMap(resolved, data, size, opts) {
46519
47457
  const insets = [];
46520
47458
  const insetRegions = [];
46521
47459
  const insetLabelSeeds = [];
46522
- if (resolved.projection === "albers-usa" && usLayer) {
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)) {
46523
47463
  const PAD = 8;
46524
47464
  const GAP = 12;
46525
47465
  const yB = height - FIT_PAD;
@@ -46550,38 +47490,14 @@ function layoutMap(resolved, data, size, opts) {
46550
47490
  }
46551
47491
  return y;
46552
47492
  };
46553
- const coastTop = (x0, xr) => {
47493
+ const coastFloor = (x0, xr) => {
46554
47494
  const n = 24;
46555
- const pts = [];
46556
47495
  let maxY = -Infinity;
46557
47496
  for (let i = 0; i <= n; i++) {
46558
- const x = x0 + (xr - x0) * i / n;
46559
- const y = at(x);
46560
- if (y > -Infinity) {
46561
- pts.push([x, y]);
46562
- if (y > maxY) maxY = y;
46563
- }
46564
- }
46565
- if (pts.length === 0) return () => yB - height * 0.42;
46566
- let m = 0;
46567
- if (pts.length >= 2) {
46568
- let sx = 0, sy = 0, sxx = 0, sxy = 0;
46569
- for (const [x, y] of pts) {
46570
- sx += x;
46571
- sy += y;
46572
- sxx += x * x;
46573
- sxy += x * y;
46574
- }
46575
- const den = pts.length * sxx - sx * sx;
46576
- if (den !== 0) m = (pts.length * sxy - sx * sy) / den;
46577
- }
46578
- m = Math.max(-0.35, Math.min(0.35, m));
46579
- let c = -Infinity;
46580
- for (const [x, y] of pts) {
46581
- const need = y - m * x + GAP;
46582
- if (need > c) c = need;
46583
- }
46584
- return (x) => m * x + c;
47497
+ const y = at(x0 + (xr - x0) * i / n);
47498
+ if (y > maxY) maxY = y;
47499
+ }
47500
+ return maxY;
46585
47501
  };
46586
47502
  const placeInset = (iso, proj, boxX, iwReq) => {
46587
47503
  const f = usLayer.get(iso);
@@ -46590,19 +47506,15 @@ function layoutMap(resolved, data, size, opts) {
46590
47506
  const iw = Math.min(iwReq, width - FIT_PAD - x0 - 2 * PAD);
46591
47507
  if (iw < 24) return boxX;
46592
47508
  const xr = x0 + iw + 2 * PAD;
46593
- const top = coastTop(x0, xr);
46594
- const yL = top(x0);
46595
- const yR = top(xr);
47509
+ const floor = coastFloor(x0, xr);
47510
+ const topGuess = floor > -Infinity ? floor + GAP : yB - height * 0.42;
46596
47511
  proj.fitWidth(iw, f);
46597
47512
  const bb = (0, import_d3_geo2.geoPath)(proj).bounds(f);
46598
47513
  const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
46599
47514
  const needH = sh + 2 * PAD;
46600
- let topFit = Math.max(yL, yR);
47515
+ let topFit = topGuess;
46601
47516
  const bottom = Math.min(topFit + needH, yB);
46602
47517
  if (bottom - topFit < needH) topFit = bottom - needH;
46603
- const lift = topFit - Math.max(yL, yR);
46604
- const topL = yL + lift;
46605
- const topR = yR + lift;
46606
47518
  proj.fitExtent(
46607
47519
  [
46608
47520
  [x0 + PAD, topFit + PAD],
@@ -46612,8 +47524,18 @@ function layoutMap(resolved, data, size, opts) {
46612
47524
  );
46613
47525
  const d = (0, import_d3_geo2.geoPath)(proj)(f) ?? "";
46614
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
+ }
46615
47537
  const r = regionById.get(iso);
46616
- let fill2 = neutralFill;
47538
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? neutralFill : neutralFill;
46617
47539
  let lineNumber = -1;
46618
47540
  if (r?.layer === "us-state") {
46619
47541
  fill2 = regionFill(r);
@@ -46621,21 +47543,25 @@ function layoutMap(resolved, data, size, opts) {
46621
47543
  }
46622
47544
  insets.push({
46623
47545
  x: x0,
46624
- y: Math.min(topL, topR),
47546
+ y: topFit,
46625
47547
  w: xr - x0,
46626
- h: bottom - Math.min(topL, topR),
47548
+ h: bottom - topFit,
46627
47549
  points: [
46628
- [x0, topL],
46629
- [xr, topR],
47550
+ [x0, topFit],
47551
+ [xr, topFit],
46630
47552
  [xr, bottom],
46631
47553
  [x0, bottom]
46632
- ]
47554
+ ],
47555
+ // The FITTED inset projection (just fit to this box) — captured so the
47556
+ // geo-query can invert pixels inside the frame back to AK/HI coords.
47557
+ projection: proj,
47558
+ ...contextLand && { contextLand }
46633
47559
  });
46634
47560
  insetRegions.push({
46635
47561
  id: iso,
46636
47562
  d,
46637
47563
  fill: fill2,
46638
- stroke: regionStroke,
47564
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46639
47565
  lineNumber,
46640
47566
  layer: "us-state",
46641
47567
  ...r?.value !== void 0 && { value: r.value },
@@ -46648,13 +47574,16 @@ function layoutMap(resolved, data, size, opts) {
46648
47574
  }
46649
47575
  return xr;
46650
47576
  };
46651
- const akRight = placeInset(
46652
- "US-AK",
46653
- alaskaProjection(),
46654
- FIT_PAD,
46655
- width * 0.15
46656
- );
46657
- 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
+ );
46658
47587
  }
46659
47588
  const conusFit = resolved.projection === "albers-usa" && !!usLayer;
46660
47589
  const classifyExtent = conusFit ? (0, import_d3_geo2.geoBounds)(fitTarget) : resolved.extent;
@@ -46670,15 +47599,24 @@ function layoutMap(resolved, data, size, opts) {
46670
47599
  };
46671
47600
  const ringOverlapsView = (ring) => {
46672
47601
  let loMin = Infinity, loMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
47602
+ const lons = [];
46673
47603
  for (const [rawLon] of ring) {
46674
47604
  const lon = normLon(rawLon);
47605
+ lons.push(lon);
46675
47606
  if (lon < loMin) loMin = lon;
46676
47607
  if (lon > loMax) loMax = lon;
46677
47608
  if (rawLon < rawMin) rawMin = rawLon;
46678
47609
  if (rawLon > rawMax) rawMax = rawLon;
46679
47610
  }
46680
- if (loMax - loMin > 270) return false;
46681
- 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;
46682
47620
  let px0 = Infinity, py0 = Infinity, px1 = -Infinity, py1 = -Infinity, anyFinite = false;
46683
47621
  for (const [lon, lat] of ring) {
46684
47622
  const p = project(lon, lat);
@@ -46751,7 +47689,7 @@ function layoutMap(resolved, data, size, opts) {
46751
47689
  const regions = [];
46752
47690
  const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
46753
47691
  for (const [iso, f] of layerFeatures) {
46754
- if (layerKind === "us-state" && usContext && INSET_STATES.has(iso))
47692
+ if (layerKind === "us-state" && usContext && resolved.projection === "albers-usa" && INSET_STATES.has(iso))
46755
47693
  continue;
46756
47694
  if (layerKind === "country" && usContext && iso === "US") continue;
46757
47695
  if (layerKind === "country" && iso === "AQ" && !regionById.has("AQ"))
@@ -46763,7 +47701,8 @@ function layoutMap(resolved, data, size, opts) {
46763
47701
  if (!d) continue;
46764
47702
  const isThisLayer = r?.layer === layerKind;
46765
47703
  const isForeign = layerKind === "country" && usContext && iso !== "US";
46766
- let fill2 = isForeign ? foreignFill : neutralFill;
47704
+ const baseFill = isForeign ? foreignFill : neutralFill;
47705
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? baseFill : baseFill;
46767
47706
  let label;
46768
47707
  let lineNumber = -1;
46769
47708
  let layer = "base";
@@ -46772,12 +47711,14 @@ function layoutMap(resolved, data, size, opts) {
46772
47711
  lineNumber = r.lineNumber;
46773
47712
  layer = layerKind;
46774
47713
  label = r.name;
47714
+ } else {
47715
+ label = f.properties?.name;
46775
47716
  }
46776
47717
  regions.push({
46777
47718
  id: iso,
46778
47719
  d,
46779
47720
  fill: fill2,
46780
- stroke: regionStroke,
47721
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46781
47722
  lineNumber,
46782
47723
  layer,
46783
47724
  ...label !== void 0 && { label },
@@ -46799,13 +47740,88 @@ function layoutMap(resolved, data, size, opts) {
46799
47740
  id: "lake",
46800
47741
  d,
46801
47742
  fill: water,
46802
- stroke: "none",
47743
+ stroke: lakeStroke,
46803
47744
  lineNumber: -1,
46804
47745
  layer: "base"
46805
47746
  });
46806
47747
  }
46807
47748
  }
46808
- const riverColor = water;
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;
47781
+ const relief = [];
47782
+ let reliefHatch = null;
47783
+ if (reliefAllowed && data.mountainRanges) {
47784
+ for (const [, f] of decodeLayer(data.mountainRanges)) {
47785
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
47786
+ if (!viewF) continue;
47787
+ const area2 = path.area(viewF);
47788
+ if (!Number.isFinite(area2) || area2 < RELIEF_MIN_AREA) continue;
47789
+ const box = path.bounds(viewF);
47790
+ if (box[1][0] - box[0][0] < RELIEF_MIN_DIM || box[1][1] - box[0][1] < RELIEF_MIN_DIM)
47791
+ continue;
47792
+ const d = path(viewF) ?? "";
47793
+ if (!d) continue;
47794
+ relief.push({ d });
47795
+ }
47796
+ if (relief.length) {
47797
+ const darkTone = isDark ? palette.bg : palette.text;
47798
+ const lightTone = isDark ? palette.text : palette.bg;
47799
+ const reliefLandRef = colorizeActive ? isDark ? palette.surface : palette.bg : neutralFill;
47800
+ const landLum = relativeLuminance(reliefLandRef);
47801
+ const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
47802
+ reliefHatch = {
47803
+ color: mix(tone, reliefLandRef, RELIEF_HATCH_STRENGTH),
47804
+ spacing: RELIEF_HATCH_SPACING,
47805
+ width: RELIEF_HATCH_WIDTH
47806
+ };
47807
+ }
47808
+ }
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);
46809
47825
  const rivers = [];
46810
47826
  if (data.rivers) {
46811
47827
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -46826,6 +47842,9 @@ function layoutMap(resolved, data, size, opts) {
46826
47842
  return R_MIN + Math.max(0, Math.min(1, t)) * (R_MAX - R_MIN);
46827
47843
  };
46828
47844
  const poiFill = (p) => {
47845
+ const directHex = p.color ? resolveColor(p.color, palette) : null;
47846
+ if (directHex)
47847
+ return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
46829
47848
  for (const group of resolved.tagGroups) {
46830
47849
  const val = p.tags[group.name.toLowerCase()];
46831
47850
  if (!val) continue;
@@ -46858,38 +47877,108 @@ function layoutMap(resolved, data, size, opts) {
46858
47877
  const xy = project(p.lon, p.lat);
46859
47878
  if (xy) projected.push({ p, xy });
46860
47879
  }
46861
- 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);
46862
47909
  for (const e of projected) {
46863
- const key = `${Math.round(e.xy[0] / COLO_EPS)},${Math.round(e.xy[1] / COLO_EPS)}`;
46864
- const arr = coloGroups.get(key);
46865
- if (arr) arr.push(e);
46866
- else coloGroups.set(key, [e]);
46867
- }
46868
- for (const group of coloGroups.values()) {
46869
- group.forEach((e, i) => {
46870
- let cx = e.xy[0];
46871
- let cy = e.xy[1];
46872
- if (group.length > 1) {
46873
- const ang = i * GOLDEN_ANGLE;
46874
- cx += Math.cos(ang) * COLO_R;
46875
- cy += Math.sin(ang) * COLO_R;
46876
- }
46877
- const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
46878
- poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
46879
- const num = routeNumberById.get(e.p.id);
46880
- pois.push({
46881
- id: e.p.id,
46882
- cx,
46883
- cy,
46884
- r: radiusFor(e.p),
46885
- fill: fill2,
46886
- stroke: stroke2,
46887
- lineNumber: e.p.lineNumber,
46888
- implicit: !!e.p.implicit,
46889
- isOrigin: originIds.has(e.p.id),
46890
- ...num !== void 0 && { routeNumber: num },
46891
- ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags }
46892
- });
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
46893
47982
  });
46894
47983
  }
46895
47984
  const legs = [];
@@ -46939,16 +48028,26 @@ function layoutMap(resolved, data, size, opts) {
46939
48028
  if (!a || !b) continue;
46940
48029
  const mx = (a.cx + b.cx) / 2;
46941
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;
46942
48038
  legs.push({
46943
- d: legPath(a, b, leg.style === "arc", 0),
48039
+ d: legPath(a, b, bow.curved, bow.offset),
46944
48040
  width: routeWidthFor(Number(leg.value)),
46945
48041
  color: mix(palette.text, palette.bg, 72),
46946
48042
  arrow: true,
46947
48043
  lineNumber: leg.lineNumber,
46948
48044
  ...leg.label !== void 0 && {
46949
48045
  label: leg.label,
46950
- labelX: mx,
46951
- labelY: my - 4
48046
+ labelX: bow.labelX,
48047
+ labelY: bow.labelY,
48048
+ labelColor: routeLabelStyle.color,
48049
+ labelHalo: routeLabelStyle.halo,
48050
+ labelHaloColor: routeLabelStyle.haloColor
46952
48051
  }
46953
48052
  });
46954
48053
  }
@@ -46976,20 +48075,29 @@ function layoutMap(resolved, data, size, opts) {
46976
48075
  const a = poiScreen.get(e.fromId);
46977
48076
  const b = poiScreen.get(e.toId);
46978
48077
  if (!a || !b) return;
46979
- const curved = e.style === "arc" || n > 1;
46980
- const offset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
48078
+ const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
46981
48079
  const mx = (a.cx + b.cx) / 2;
46982
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;
46983
48088
  legs.push({
46984
- d: legPath(a, b, curved, offset),
48089
+ d: legPath(a, b, bow.curved, bow.offset),
46985
48090
  width: widthFor(e),
46986
48091
  color: mix(palette.text, palette.bg, 66),
46987
48092
  arrow: e.directed,
46988
48093
  lineNumber: e.lineNumber,
46989
48094
  ...e.label !== void 0 && {
46990
48095
  label: e.label,
46991
- labelX: mx,
46992
- labelY: my - 4
48096
+ labelX: bow.labelX,
48097
+ labelY: bow.labelY,
48098
+ labelColor: edgeLabelStyle.color,
48099
+ labelHalo: edgeLabelStyle.halo,
48100
+ labelHaloColor: edgeLabelStyle.haloColor
46993
48101
  }
46994
48102
  });
46995
48103
  });
@@ -47031,25 +48139,25 @@ function layoutMap(resolved, data, size, opts) {
47031
48139
  }
47032
48140
  }
47033
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));
47034
- const regionLabelMode = resolved.directives.regionLabels ?? "off";
48142
+ const showRegionLabels = resolved.directives.noRegionLabels !== true;
48143
+ const isCompact = width < COMPACT_WIDTH_PX;
47035
48144
  const LABEL_PADX = 6;
47036
48145
  const LABEL_PADY = 3;
47037
- const labelW = (text) => measureLegendText(text, FONT) + 2 * LABEL_PADX;
47038
- const labelH = FONT + 2 * LABEL_PADY;
48146
+ const labelW = (text) => measureLegendText(text, FONT2) + 2 * LABEL_PADX;
48147
+ const labelH = FONT2 + 2 * LABEL_PADY;
47039
48148
  const pushRegionLabel = (x, y, text, fill2, lineNumber) => {
47040
- const color = contrastText(
47041
- fill2,
47042
- palette.textOnFillLight,
47043
- 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
47044
48153
  );
47045
- const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47046
48154
  labels.push({
47047
48155
  x,
47048
48156
  y,
47049
48157
  text,
47050
48158
  anchor: "middle",
47051
48159
  color,
47052
- halo: true,
48160
+ halo: overflows,
47053
48161
  haloColor,
47054
48162
  lineNumber
47055
48163
  });
@@ -47058,21 +48166,50 @@ function layoutMap(resolved, data, size, opts) {
47058
48166
  US: [-98.5, 39.5]
47059
48167
  // CONUS geographic centre (near Lebanon, Kansas)
47060
48168
  };
47061
- if (regionLabelMode === "full" || regionLabelMode === "abbrev") {
47062
- for (const r of regions) {
47063
- if (r.layer === "base" || r.label === void 0) continue;
47064
- const f = r.layer === "us-state" ? usLayer?.get(r.id) : worldLayer.get(r.id);
47065
- 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;
47066
48183
  const [[x0, y0], [x1, y1]] = path.bounds(f);
47067
- const text = regionLabelMode === "abbrev" ? r.id.replace(/^US-/, "") : r.label;
47068
- if (labelW(text) > x1 - x0 || labelH > y1 - y0) continue;
47069
- 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;
47070
48189
  const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
47071
- 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));
47072
48209
  pushRegionLabel(c[0], c[1], text, r.fill, r.lineNumber);
47073
48210
  }
47074
48211
  for (const seed of insetLabelSeeds) {
47075
- const text = regionLabelMode === "abbrev" ? seed.iso.replace(/^US-/, "") : seed.name;
48212
+ const text = isCompact ? seed.iso.replace(/^US-/, "") : seed.name;
47076
48213
  const src = regionById.get(seed.iso);
47077
48214
  pushRegionLabel(
47078
48215
  seed.x,
@@ -47083,22 +48220,26 @@ function layoutMap(resolved, data, size, opts) {
47083
48220
  );
47084
48221
  }
47085
48222
  }
47086
- const poiLabelMode = resolved.directives.poiLabels ?? "auto";
47087
- if (poiLabelMode !== "off") {
47088
- const ordered = [...pois].sort(
47089
- (a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1)
47090
- );
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));
47091
48225
  const poiById = new Map(resolved.pois.map((q) => [q.id, q]));
47092
48226
  const labelText = (p) => {
47093
48227
  const src = poiById.get(p.id);
47094
48228
  return src?.label ?? src?.name ?? p.id;
47095
48229
  };
47096
- const poiLabH = FONT * 1.25;
48230
+ const poiLabH = FONT2 * 1.25;
47097
48231
  const labelInfo = (p) => {
47098
48232
  const text = labelText(p);
47099
- return { text, w: measureLegendText(text, FONT) };
48233
+ return { text, w: measureLegendText(text, FONT2) };
47100
48234
  };
47101
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
+ }
47102
48243
  const inlineRect = (p, w, side) => {
47103
48244
  switch (side) {
47104
48245
  case "right":
@@ -47128,11 +48269,11 @@ function layoutMap(resolved, data, size, opts) {
47128
48269
  const x = side === "right" ? rect.x : side === "left" ? rect.x + w : p.cx;
47129
48270
  labels.push({
47130
48271
  x,
47131
- y: rect.y + poiLabH / 2 + FONT / 3,
48272
+ y: rect.y + poiLabH / 2 + FONT2 / 3,
47132
48273
  text,
47133
48274
  anchor,
47134
48275
  color: palette.text,
47135
- halo: true,
48276
+ halo: false,
47136
48277
  haloColor: palette.bg,
47137
48278
  poiId: p.id,
47138
48279
  lineNumber: p.lineNumber
@@ -47143,43 +48284,60 @@ function layoutMap(resolved, data, size, opts) {
47143
48284
  return rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect);
47144
48285
  };
47145
48286
  const GROUP_R = 30;
47146
- const groups = [];
48287
+ const groups2 = [];
47147
48288
  for (const p of ordered) {
47148
- const near = groups.find(
48289
+ const near = groups2.find(
47149
48290
  (g) => g.some((q) => Math.hypot(q.cx - p.cx, q.cy - p.cy) < GROUP_R)
47150
48291
  );
47151
48292
  if (near) near.push(p);
47152
- else groups.push([p]);
48293
+ else groups2.push([p]);
47153
48294
  }
47154
48295
  const ROW_GAP2 = 3;
47155
48296
  const step = poiLabH + ROW_GAP2;
47156
48297
  const COL_GAP = 16;
47157
- const placeColumn = (group) => {
47158
- 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) => {
47159
48300
  const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
47160
48301
  const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
47161
- const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
47162
48302
  const maxW = Math.max(...items.map((o) => o.w));
47163
- const side = right + COL_GAP + maxW <= width - 2 ? "right" : "left";
47164
- 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);
47165
48305
  const totalH = items.length * step;
47166
48306
  let startY = cyMid - totalH / 2;
47167
48307
  startY = Math.max(2, Math.min(startY, height - totalH - 2));
47168
- items.forEach((o, i) => {
48308
+ return items.map((o, i) => {
47169
48309
  const rowCy = startY + i * step + step / 2;
47170
- obstacles.push({
47171
- x: side === "right" ? colX : colX - o.w,
47172
- y: rowCy - poiLabH / 2,
47173
- w: o.w,
47174
- h: poiLabH
47175
- });
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);
47176
48334
  labels.push({
47177
48335
  x: colX,
47178
- y: rowCy + FONT / 3,
48336
+ y: rowCy + FONT2 / 3,
47179
48337
  text: o.text,
47180
48338
  anchor: side === "right" ? "start" : "end",
47181
48339
  color: palette.text,
47182
- halo: true,
48340
+ halo: false,
47183
48341
  haloColor: palette.bg,
47184
48342
  leader: {
47185
48343
  x1: o.p.cx,
@@ -47189,24 +48347,141 @@ function layoutMap(resolved, data, size, opts) {
47189
48347
  },
47190
48348
  leaderColor: o.p.fill,
47191
48349
  poiId: o.p.id,
47192
- lineNumber: o.p.lineNumber
48350
+ lineNumber: o.p.lineNumber,
48351
+ ...clusterId !== void 0 && { clusterMember: clusterId }
47193
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
47194
48375
  });
47195
48376
  };
47196
- 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);
47197
48387
  if (g.length === 1) {
47198
- const p = g[0];
47199
- const { text, w } = labelInfo(p);
48388
+ const { p, text, w } = items[0];
47200
48389
  const side = ["right", "left", "above", "below"].find(
47201
48390
  (s) => inlineFits(p, w, s)
47202
48391
  );
47203
- if (side) {
47204
- pushInline(p, text, w, side);
47205
- 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;
47206
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
+ });
47207
48466
  }
47208
- placeColumn(g);
47209
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);
47210
48485
  }
47211
48486
  let legend = null;
47212
48487
  if (!resolved.directives.noLegend) {
@@ -47241,24 +48516,35 @@ function layoutMap(resolved, data, size, opts) {
47241
48516
  ...resolved.caption !== void 0 && { caption: resolved.caption },
47242
48517
  regions,
47243
48518
  rivers,
48519
+ relief,
48520
+ reliefHatch,
48521
+ coastlineStyle,
47244
48522
  legs,
47245
48523
  pois,
48524
+ clusters,
47246
48525
  labels,
47247
48526
  legend,
47248
48527
  insets,
47249
- insetRegions
48528
+ insetRegions,
48529
+ projection,
48530
+ stretch: stretchParams,
48531
+ diagnostics: []
47250
48532
  };
47251
48533
  }
47252
- var import_d3_geo2, import_topojson_client2, FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, COLO_EPS, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT, RIVER_WIDTH, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_WATER_LIGHT, MUTED_WATER_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, MUTED_LAND_DARK, COLO_R, GOLDEN_ANGLE, FAN_STEP, ARC_CURVE_FRAC, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, US_NON_CONUS;
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;
47253
48535
  var init_layout15 = __esm({
47254
48536
  "src/map/layout.ts"() {
47255
48537
  "use strict";
47256
48538
  import_d3_geo2 = require("d3-geo");
47257
48539
  import_topojson_client2 = require("topojson-client");
47258
48540
  init_color_utils();
48541
+ init_geo();
48542
+ init_colorize();
48543
+ init_colors();
47259
48544
  init_label_layout();
47260
48545
  init_legend_constants();
47261
48546
  init_title_constants();
48547
+ init_context_labels();
47262
48548
  FIT_PAD = 24;
47263
48549
  RAMP_FLOOR = 15;
47264
48550
  R_DEFAULT = 6;
@@ -47266,29 +48552,66 @@ var init_layout15 = __esm({
47266
48552
  R_MAX = 22;
47267
48553
  W_MIN = 1.25;
47268
48554
  W_MAX = 8;
47269
- FONT = 11;
47270
- COLO_EPS = 1.5;
47271
- LAND_TINT_LIGHT = 58;
47272
- LAND_TINT_DARK = 75;
48555
+ FONT2 = 11;
48556
+ MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48557
+ MAX_COLUMN_ROWS = 7;
48558
+ REGION_LABEL_HALO_RATIO = 4.5;
48559
+ LAND_TINT_LIGHT = 12;
48560
+ LAND_TINT_DARK = 24;
47273
48561
  TAG_TINT_LIGHT = 60;
47274
48562
  TAG_TINT_DARK = 68;
47275
- WATER_TINT = 55;
48563
+ WATER_TINT_LIGHT = 24;
48564
+ WATER_TINT_DARK = 24;
47276
48565
  RIVER_WIDTH = 1.3;
48566
+ COMPACT_WIDTH_PX = 480;
48567
+ RELIEF_MIN_AREA = 12;
48568
+ RELIEF_MIN_DIM = 2;
48569
+ RELIEF_HATCH_SPACING = 2;
48570
+ RELIEF_HATCH_WIDTH = 0.15;
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;
47277
48581
  FOREIGN_TINT_LIGHT = 30;
47278
48582
  FOREIGN_TINT_DARK = 62;
47279
- MUTED_WATER_LIGHT = 14;
47280
- MUTED_WATER_DARK = 10;
47281
48583
  MUTED_FOREIGN_LIGHT = 28;
47282
48584
  MUTED_FOREIGN_DARK = 16;
47283
- MUTED_LAND_DARK = 24;
47284
48585
  COLO_R = 9;
47285
48586
  GOLDEN_ANGLE = 2.399963229728653;
48587
+ STACK_OVERLAP = 1;
48588
+ STACK_RING_MAX = 8;
48589
+ STACK_RING_GAP = 4;
47286
48590
  FAN_STEP = 16;
47287
48591
  ARC_CURVE_FRAC = 0.18;
48592
+ decodeCache = /* @__PURE__ */ new WeakMap();
47288
48593
  usConusProjection = () => (0, import_d3_geo2.geoConicEqualArea)().parallels([29.5, 45.5]).rotate([96, 0]);
47289
48594
  alaskaProjection = () => (0, import_d3_geo2.geoConicEqualArea)().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
47290
48595
  hawaiiProjection = () => (0, import_d3_geo2.geoMercator)();
47291
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
+ };
47292
48615
  US_NON_CONUS = /* @__PURE__ */ new Set([
47293
48616
  "US-AK",
47294
48617
  "US-HI",
@@ -47307,6 +48630,58 @@ __export(renderer_exports16, {
47307
48630
  renderMap: () => renderMap,
47308
48631
  renderMapForExport: () => renderMapForExport
47309
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
+ }
47310
48685
  function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims, activeGroupOverride) {
47311
48686
  d3Selection18.select(container).selectAll(":not([data-d3-tooltip])").remove();
47312
48687
  const width = exportDims?.width ?? container.clientWidth;
@@ -47319,6 +48694,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47319
48694
  {
47320
48695
  palette,
47321
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,
47322
48702
  ...activeGroupOverride !== void 0 && {
47323
48703
  activeGroup: activeGroupOverride
47324
48704
  }
@@ -47332,6 +48712,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47332
48712
  const gRegions = svg.append("g").attr("class", "dgmo-map-regions");
47333
48713
  const drawRegion = (g, r, strokeWidth) => {
47334
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);
47335
48716
  if (r.layer !== "base") {
47336
48717
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
47337
48718
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -47352,6 +48733,52 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47352
48733
  }
47353
48734
  };
47354
48735
  for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
48736
+ if (layout.relief.length && layout.reliefHatch) {
48737
+ const h = layout.reliefHatch;
48738
+ const rangeClipId = "dgmo-relief-clip";
48739
+ const landClipId = "dgmo-relief-land";
48740
+ const rangeClip = defs.append("clipPath").attr("id", rangeClipId);
48741
+ for (const s of layout.relief) rangeClip.append("path").attr("d", s.d);
48742
+ const landClip = defs.append("clipPath").attr("id", landClipId);
48743
+ for (const r of layout.regions)
48744
+ if (r.id !== "lake") landClip.append("path").attr("d", r.d);
48745
+ const gRelief = svg.append("g").attr("clip-path", `url(#${landClipId})`).append("g").attr("class", "dgmo-map-relief").attr("clip-path", `url(#${rangeClipId})`).attr("stroke", h.color).attr("stroke-width", h.width).attr("vector-effect", "non-scaling-stroke");
48746
+ for (let y = h.spacing; y < height; y += h.spacing) {
48747
+ gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
48748
+ }
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
+ }
47355
48782
  if (layout.rivers.length) {
47356
48783
  const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47357
48784
  for (const r of layout.rivers) {
@@ -47360,15 +48787,61 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47360
48787
  }
47361
48788
  if (layout.insets.length) {
47362
48789
  const insetG = svg.append("g").attr("class", "dgmo-map-insets");
47363
- for (const box of layout.insets) {
48790
+ layout.insets.forEach((box, bi) => {
47364
48791
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
47365
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");
47366
- }
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
+ });
47367
48799
  for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
47368
- }
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
+ };
47369
48841
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
47370
48842
  layout.legs.forEach((leg, i) => {
47371
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);
47372
48845
  if (leg.arrow) {
47373
48846
  const id = `dgmo-map-arrow-${i}`;
47374
48847
  const s = arrowSize(leg.width);
@@ -47376,25 +48849,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47376
48849
  p.attr("marker-end", `url(#${id})`);
47377
48850
  }
47378
48851
  if (leg.label !== void 0 && leg.labelX !== void 0) {
47379
- emitText(
48852
+ const lt = emitText(
47380
48853
  gLegs,
47381
48854
  leg.labelX,
47382
48855
  leg.labelY ?? 0,
47383
48856
  leg.label,
47384
48857
  "middle",
47385
- palette.textMuted,
47386
- haloColor,
47387
- true,
48858
+ leg.labelColor ?? palette.textMuted,
48859
+ leg.labelHaloColor ?? haloColor,
48860
+ leg.labelHalo ?? true,
47388
48861
  LABEL_FONT - 1
47389
48862
  );
48863
+ wireSync(lt, leg.lineNumber);
47390
48864
  }
47391
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
+ }
47392
48876
  const gPois = svg.append("g").attr("class", "dgmo-map-pois");
47393
48877
  for (const poi of layout.pois) {
47394
48878
  if (poi.isOrigin) {
47395
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);
47396
48880
  }
47397
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);
47398
48884
  if (poi.tags) {
47399
48885
  for (const [group, value] of Object.entries(poi.tags)) {
47400
48886
  c.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
@@ -47422,12 +48908,32 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47422
48908
  }
47423
48909
  const gLabels = svg.append("g").attr("class", "dgmo-map-labels");
47424
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
+ }
47425
48928
  if (lab.leader) {
47426
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(
47427
48930
  "stroke",
47428
48931
  lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
47429
48932
  ).attr("stroke-width", lab.leaderColor ? 1 : 0.75);
47430
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);
47431
48937
  }
47432
48938
  const t = emitText(
47433
48939
  gLabels,
@@ -47438,11 +48944,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47438
48944
  lab.color,
47439
48945
  lab.haloColor,
47440
48946
  lab.halo,
47441
- LABEL_FONT
48947
+ LABEL_FONT,
48948
+ lab.italic,
48949
+ lab.letterSpacing,
48950
+ lab.lines
47442
48951
  );
47443
48952
  if (lab.poiId !== void 0) {
47444
48953
  t.attr("data-poi", lab.poiId).style("cursor", "default");
47445
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
+ }
47446
48979
  }
47447
48980
  if (layout.legend) {
47448
48981
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
@@ -47476,10 +49009,10 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47476
49009
  }
47477
49010
  }
47478
49011
  if (layout.title) {
47479
- svg.append("text").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).attr("fill", palette.text).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 4).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.title);
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);
47480
49013
  }
47481
49014
  if (layout.subtitle) {
47482
- 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);
47483
49016
  }
47484
49017
  if (layout.caption) {
47485
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);
@@ -47488,10 +49021,21 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47488
49021
  function renderMapForExport(container, resolved, data, palette, isDark, exportDims) {
47489
49022
  renderMap(container, resolved, data, palette, isDark, void 0, exportDims);
47490
49023
  }
47491
- function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize) {
47492
- 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);
47493
49037
  if (withHalo) {
47494
- 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);
47495
49039
  }
47496
49040
  return t;
47497
49041
  }
@@ -47509,6 +49053,179 @@ var init_renderer16 = __esm({
47509
49053
  }
47510
49054
  });
47511
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
+
49106
+ // src/map/load-data.ts
49107
+ var load_data_exports = {};
49108
+ __export(load_data_exports, {
49109
+ loadMapData: () => loadMapData
49110
+ });
49111
+ async function loadNodeBuiltins() {
49112
+ const [{ readFile }, { fileURLToPath }, { dirname, resolve }] = await Promise.all([
49113
+ import("fs/promises"),
49114
+ import("url"),
49115
+ import("path")
49116
+ ]);
49117
+ return { readFile, fileURLToPath, dirname, resolve };
49118
+ }
49119
+ async function readJson(nb, dir, name) {
49120
+ return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
49121
+ }
49122
+ async function firstExistingDir(nb, baseDir) {
49123
+ for (const rel of CANDIDATE_DIRS) {
49124
+ const dir = nb.resolve(baseDir, rel);
49125
+ try {
49126
+ await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
49127
+ return dir;
49128
+ } catch {
49129
+ }
49130
+ }
49131
+ throw new Error(
49132
+ `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
49133
+ );
49134
+ }
49135
+ function validate(data) {
49136
+ const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
49137
+ if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
49138
+ throw new Error("map data assets are malformed (failed shape validation)");
49139
+ }
49140
+ return data;
49141
+ }
49142
+ function moduleBaseDir(nb) {
49143
+ try {
49144
+ const url = import_meta.url;
49145
+ if (url) return nb.dirname(nb.fileURLToPath(url));
49146
+ } catch {
49147
+ }
49148
+ if (typeof __dirname !== "undefined") return __dirname;
49149
+ return process.cwd();
49150
+ }
49151
+ function loadMapData() {
49152
+ cache ??= (async () => {
49153
+ const nb = await loadNodeBuiltins();
49154
+ const dir = await firstExistingDir(nb, moduleBaseDir(nb));
49155
+ const [
49156
+ worldCoarse,
49157
+ worldDetail,
49158
+ usStates,
49159
+ lakes,
49160
+ rivers,
49161
+ mountainRanges,
49162
+ naLand,
49163
+ naLakes,
49164
+ waterBodies,
49165
+ gazetteer
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.
49171
+ readJson(nb, dir, FILES.worldCoarse),
49172
+ readJson(nb, dir, FILES.worldDetail),
49173
+ readJson(nb, dir, FILES.usStates),
49174
+ // Lakes/rivers/mountain/NA/water assets are optional — older bundles may predate them.
49175
+ readJson(nb, dir, FILES.lakes).catch(() => void 0),
49176
+ readJson(nb, dir, FILES.rivers).catch(() => void 0),
49177
+ readJson(nb, dir, FILES.mountainRanges).catch(
49178
+ () => void 0
49179
+ ),
49180
+ readJson(nb, dir, FILES.naLand).catch(() => void 0),
49181
+ readJson(nb, dir, FILES.naLakes).catch(() => void 0),
49182
+ readJson(nb, dir, FILES.waterBodies).catch(() => void 0),
49183
+ readJson(nb, dir, FILES.gazetteer)
49184
+ ]);
49185
+ return validate({
49186
+ worldCoarse,
49187
+ worldDetail,
49188
+ usStates,
49189
+ gazetteer,
49190
+ ...lakes && { lakes },
49191
+ ...rivers && { rivers },
49192
+ ...mountainRanges && { mountainRanges },
49193
+ ...naLand && { naLand },
49194
+ ...naLakes && { naLakes },
49195
+ ...waterBodies && { waterBodies }
49196
+ });
49197
+ })().catch((e) => {
49198
+ cache = void 0;
49199
+ throw e;
49200
+ });
49201
+ return cache;
49202
+ }
49203
+ var import_meta, FILES, CANDIDATE_DIRS, cache;
49204
+ var init_load_data = __esm({
49205
+ "src/map/load-data.ts"() {
49206
+ "use strict";
49207
+ import_meta = {};
49208
+ FILES = {
49209
+ worldCoarse: "world-coarse.json",
49210
+ worldDetail: "world-detail.json",
49211
+ usStates: "us-states.json",
49212
+ lakes: "lakes.json",
49213
+ rivers: "rivers.json",
49214
+ mountainRanges: "mountain-ranges.json",
49215
+ naLand: "na-land.json",
49216
+ naLakes: "na-lakes.json",
49217
+ waterBodies: "water-bodies.json",
49218
+ gazetteer: "gazetteer.json"
49219
+ };
49220
+ CANDIDATE_DIRS = [
49221
+ "./data",
49222
+ "./map-data",
49223
+ "../map-data",
49224
+ "../src/map/data"
49225
+ ];
49226
+ }
49227
+ });
49228
+
47512
49229
  // src/pyramid/renderer.ts
47513
49230
  var renderer_exports17 = {};
47514
49231
  __export(renderer_exports17, {
@@ -49510,8 +51227,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
49510
51227
  const lines = splitParticipantLabel(p.label, LABEL_MAX_CHARS);
49511
51228
  if (lines.length === 0) continue;
49512
51229
  const widest = Math.max(...lines.map((l) => l.length));
49513
- const labelWidth = widest * LABEL_CHAR_WIDTH + 10;
49514
- uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth);
51230
+ const labelWidth2 = widest * LABEL_CHAR_WIDTH + 10;
51231
+ uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth2);
49515
51232
  }
49516
51233
  uniformBoxWidth = Math.min(MAX_BOX_WIDTH, uniformBoxWidth);
49517
51234
  const effectiveGap = Math.max(PARTICIPANT_GAP, uniformBoxWidth + 30);
@@ -52206,15 +53923,15 @@ function renderArcDiagram(container, parsed, palette, _isDark, onClickItem, expo
52206
53923
  textColor,
52207
53924
  onClickItem
52208
53925
  );
52209
- const neighbors = /* @__PURE__ */ new Map();
52210
- 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());
52211
53928
  for (const link of links) {
52212
- neighbors.get(link.source).add(link.target);
52213
- neighbors.get(link.target).add(link.source);
53929
+ neighbors2.get(link.source).add(link.target);
53930
+ neighbors2.get(link.target).add(link.source);
52214
53931
  }
52215
53932
  const FADE_OPACITY3 = 0.1;
52216
53933
  function handleMouseEnter(hovered) {
52217
- const connected = neighbors.get(hovered);
53934
+ const connected = neighbors2.get(hovered);
52218
53935
  g.selectAll(".arc-link").each(function() {
52219
53936
  const el = d3Selection23.select(this);
52220
53937
  const src = el.attr("data-source");
@@ -54149,7 +55866,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54149
55866
  8,
54150
55867
  Math.floor(OVERLAP_WRAP_TARGET_W / OVERLAP_CH_W)
54151
55868
  );
54152
- function wrapLabel2(text, maxChars) {
55869
+ function wrapLabel3(text, maxChars) {
54153
55870
  const words = text.split(/\s+/).filter(Boolean);
54154
55871
  const lines = [];
54155
55872
  let cur = "";
@@ -54195,7 +55912,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54195
55912
  if (!ov.label) continue;
54196
55913
  const idxs = ov.sets.map((s) => vennSets.findIndex((vs) => vs.name === s));
54197
55914
  if (idxs.some((idx) => idx < 0)) continue;
54198
- const lines = wrapLabel2(ov.label, MAX_WRAP_CHARS);
55915
+ const lines = wrapLabel3(ov.label, MAX_WRAP_CHARS);
54199
55916
  wrappedOverlapLabels.set(ov, lines);
54200
55917
  const dir = predictOverlapDirRaw(idxs);
54201
55918
  const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
@@ -55632,25 +57349,29 @@ async function renderForExport(content, theme, palette, viewState, options) {
55632
57349
  if (detectedType === "map") {
55633
57350
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55634
57351
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55635
- const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
55636
57352
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
57353
+ const { mapExportDimensions: mapExportDimensions2 } = await Promise.resolve().then(() => (init_dimensions(), dimensions_exports));
55637
57354
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55638
57355
  const mapParsed = parseMap2(content);
55639
- let mapData;
55640
- try {
55641
- mapData = await loadMapData2();
55642
- } catch {
55643
- return "";
57356
+ let mapData = options?.mapData;
57357
+ if (!mapData) {
57358
+ const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
57359
+ try {
57360
+ mapData = await loadMapData2();
57361
+ } catch {
57362
+ return "";
57363
+ }
55644
57364
  }
55645
57365
  const mapResolved = resolveMap2(mapParsed, mapData);
55646
- const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
57366
+ const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57367
+ const container2 = createExportContainer(dims2.width, dims2.height);
55647
57368
  renderMapForExport2(
55648
57369
  container2,
55649
57370
  mapResolved,
55650
57371
  mapData,
55651
57372
  effectivePalette2,
55652
57373
  theme === "dark",
55653
- { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
57374
+ dims2
55654
57375
  );
55655
57376
  return finalizeSvgExport(container2, theme, effectivePalette2);
55656
57377
  }
@@ -56514,7 +58235,8 @@ async function render(content, options) {
56514
58235
  ...options?.c4Container !== void 0 && {
56515
58236
  c4Container: options.c4Container
56516
58237
  },
56517
- ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup }
58238
+ ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup },
58239
+ ...options?.mapData !== void 0 && { mapData: options.mapData }
56518
58240
  });
56519
58241
  if (chartType === "map") {
56520
58242
  try {
@@ -56525,7 +58247,7 @@ async function render(content, options) {
56525
58247
  Promise.resolve().then(() => (init_load_data(), load_data_exports))
56526
58248
  ]
56527
58249
  );
56528
- const data = await loadMapData2();
58250
+ const data = options?.mapData ?? await loadMapData2();
56529
58251
  diagnostics = [...resolveMap2(parseMap2(content), data).diagnostics];
56530
58252
  } catch {
56531
58253
  }