@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.js CHANGED
@@ -91,18 +91,18 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
91
91
  const results = [];
92
92
  for (let i = 0; i < points.length; i++) {
93
93
  const pt = points[i];
94
- const labelWidth = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
94
+ const labelWidth2 = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
95
95
  let best = null;
96
96
  const directions = [
97
97
  {
98
98
  // Above
99
99
  gen: (offset) => {
100
- const lx = pt.cx - labelWidth / 2;
100
+ const lx = pt.cx - labelWidth2 / 2;
101
101
  const ly = pt.cy - offset - labelHeight;
102
- if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
102
+ if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
103
103
  return null;
104
104
  return {
105
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
105
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
106
106
  textX: pt.cx,
107
107
  textY: ly + labelHeight / 2,
108
108
  anchor: "middle"
@@ -112,12 +112,12 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
112
112
  {
113
113
  // Below
114
114
  gen: (offset) => {
115
- const lx = pt.cx - labelWidth / 2;
115
+ const lx = pt.cx - labelWidth2 / 2;
116
116
  const ly = pt.cy + offset;
117
- if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
117
+ if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
118
118
  return null;
119
119
  return {
120
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
120
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
121
121
  textX: pt.cx,
122
122
  textY: ly + labelHeight / 2,
123
123
  anchor: "middle"
@@ -129,10 +129,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
129
129
  gen: (offset) => {
130
130
  const lx = pt.cx + offset;
131
131
  const ly = pt.cy - labelHeight / 2;
132
- if (lx + labelWidth > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
132
+ if (lx + labelWidth2 > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
133
133
  return null;
134
134
  return {
135
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
135
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
136
136
  textX: lx,
137
137
  textY: pt.cy,
138
138
  anchor: "start"
@@ -142,13 +142,13 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
142
142
  {
143
143
  // Left
144
144
  gen: (offset) => {
145
- const lx = pt.cx - offset - labelWidth;
145
+ const lx = pt.cx - offset - labelWidth2;
146
146
  const ly = pt.cy - labelHeight / 2;
147
147
  if (lx < chartBounds.left || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
148
148
  return null;
149
149
  return {
150
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
151
- textX: lx + labelWidth,
150
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
151
+ textX: lx + labelWidth2,
152
152
  textY: pt.cy,
153
153
  anchor: "end"
154
154
  };
@@ -198,10 +198,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
198
198
  }
199
199
  }
200
200
  if (!best) {
201
- const lx = pt.cx - labelWidth / 2;
201
+ const lx = pt.cx - labelWidth2 / 2;
202
202
  const ly = pt.cy - minGap - labelHeight;
203
203
  best = {
204
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
204
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
205
205
  textX: pt.cx,
206
206
  textY: ly + labelHeight / 2,
207
207
  anchor: "middle",
@@ -836,6 +836,9 @@ var init_reserved_key_registry = __esm({
836
836
  "value",
837
837
  "label",
838
838
  "style"
839
+ // `surface:` was removed in the 2026-06-02 defaults-on review — it is no longer
840
+ // a recognized metadata key (the route/edge surface feature was cut; §24B.7).
841
+ // A stray `surface: water` is no longer captured as a reserved key.
839
842
  ]);
840
843
  ORG_REGISTRY = staticRegistry([
841
844
  "color",
@@ -1898,77 +1901,266 @@ function getSegmentColors(palette, count) {
1898
1901
  (_, i) => hslToHex(Math.round((startHue + i * step) % 360), avgS, avgL)
1899
1902
  );
1900
1903
  }
1904
+ function politicalTints(palette, count, isDark) {
1905
+ if (count <= 0) return [];
1906
+ const base = isDark ? palette.surface : palette.bg;
1907
+ const c = palette.colors;
1908
+ const swatches = [
1909
+ .../* @__PURE__ */ new Set([
1910
+ c.green,
1911
+ c.yellow,
1912
+ c.orange,
1913
+ c.purple,
1914
+ c.red,
1915
+ c.teal,
1916
+ c.cyan,
1917
+ c.blue
1918
+ ])
1919
+ ];
1920
+ const bands = isDark ? POLITICAL_TINT_BANDS.dark : POLITICAL_TINT_BANDS.light;
1921
+ const out = [];
1922
+ for (const pct of bands) {
1923
+ if (out.length >= count) break;
1924
+ for (const s of swatches) out.push(mix(s, base, pct));
1925
+ }
1926
+ return out.slice(0, count);
1927
+ }
1928
+ var POLITICAL_TINT_BANDS;
1901
1929
  var init_color_utils = __esm({
1902
1930
  "src/palettes/color-utils.ts"() {
1903
1931
  "use strict";
1932
+ POLITICAL_TINT_BANDS = {
1933
+ light: [32, 48, 64, 80],
1934
+ dark: [44, 58, 72, 86]
1935
+ };
1904
1936
  }
1905
1937
  });
1906
1938
 
1907
- // src/palettes/bold.ts
1908
- var boldPalette;
1909
- var init_bold = __esm({
1910
- "src/palettes/bold.ts"() {
1939
+ // src/palettes/atlas.ts
1940
+ var atlasPalette;
1941
+ var init_atlas = __esm({
1942
+ "src/palettes/atlas.ts"() {
1911
1943
  "use strict";
1912
1944
  init_registry();
1913
- boldPalette = {
1914
- id: "bold",
1915
- name: "Bold",
1945
+ atlasPalette = {
1946
+ id: "atlas",
1947
+ name: "Atlas",
1916
1948
  light: {
1917
- bg: "#ffffff",
1918
- surface: "#f0f0f0",
1919
- overlay: "#f0f0f0",
1920
- border: "#cccccc",
1921
- text: "#000000",
1922
- textMuted: "#666666",
1923
- textOnFillLight: "#ffffff",
1924
- textOnFillDark: "#000000",
1925
- primary: "#0000ff",
1926
- secondary: "#ff00ff",
1927
- accent: "#00cccc",
1928
- destructive: "#ff0000",
1949
+ bg: "#f3ead3",
1950
+ // warm manila / parchment
1951
+ surface: "#ece0c0",
1952
+ // deeper paper (cards, panels)
1953
+ overlay: "#e8dab8",
1954
+ // popovers, dropdowns
1955
+ border: "#bcaa86",
1956
+ // muted sepia rule line
1957
+ text: "#463a26",
1958
+ // aged sepia-brown ink
1959
+ textMuted: "#7a6a4f",
1960
+ // faded annotation ink
1961
+ textOnFillLight: "#f7f1de",
1962
+ // parchment (light text on dark fills)
1963
+ textOnFillDark: "#3a2e1c",
1964
+ // deep ink (dark text on light fills)
1965
+ primary: "#5b7a99",
1966
+ // pull-down map ocean (steel-blue)
1967
+ secondary: "#7e9a6f",
1968
+ // lowland sage / celadon
1969
+ accent: "#b07f7c",
1970
+ // dusty rose
1971
+ destructive: "#b25a45",
1972
+ // brick / terracotta
1929
1973
  colors: {
1930
- red: "#ff0000",
1931
- orange: "#ff8000",
1932
- yellow: "#ffcc00",
1933
- green: "#00cc00",
1934
- blue: "#0000ff",
1935
- purple: "#cc00cc",
1936
- teal: "#008080",
1937
- cyan: "#00cccc",
1938
- gray: "#808080",
1939
- black: "#000000",
1940
- white: "#f0f0f0"
1974
+ red: "#bf6a52",
1975
+ // terracotta brick
1976
+ orange: "#cf9a5c",
1977
+ // map tan / ochre
1978
+ yellow: "#cdb35e",
1979
+ // straw / muted lemon
1980
+ green: "#7e9a6f",
1981
+ // sage / celadon lowland
1982
+ blue: "#5b7a99",
1983
+ // steel-blue ocean
1984
+ purple: "#9a7fa6",
1985
+ // dusty lilac / mauve
1986
+ teal: "#6fa094",
1987
+ // muted seafoam
1988
+ cyan: "#79a7b5",
1989
+ // shallow-water blue
1990
+ gray: "#8a7d68",
1991
+ // warm taupe
1992
+ black: "#463a26",
1993
+ // ink
1994
+ white: "#ece0c0"
1995
+ // paper
1941
1996
  }
1942
1997
  },
1943
1998
  dark: {
1944
- bg: "#000000",
1945
- surface: "#111111",
1946
- overlay: "#1a1a1a",
1947
- border: "#333333",
1948
- text: "#ffffff",
1949
- textMuted: "#aaaaaa",
1950
- textOnFillLight: "#ffffff",
1951
- textOnFillDark: "#000000",
1952
- primary: "#00ccff",
1953
- secondary: "#ff00ff",
1954
- accent: "#ffff00",
1955
- destructive: "#ff0000",
1999
+ bg: "#1e2a33",
2000
+ // deep map ocean (night globe)
2001
+ surface: "#27353f",
2002
+ // raised ocean
2003
+ overlay: "#2e3d48",
2004
+ // popovers, dropdowns
2005
+ border: "#3d4f5c",
2006
+ // depth-contour line
2007
+ text: "#e8dcc0",
2008
+ // parchment ink, inverted
2009
+ textMuted: "#a89a7d",
2010
+ // faded label
2011
+ textOnFillLight: "#f7f1de",
2012
+ // parchment
2013
+ textOnFillDark: "#1a242c",
2014
+ // deep ocean ink
2015
+ primary: "#7ba0bf",
2016
+ // brighter ocean
2017
+ secondary: "#9bb588",
2018
+ // sage, lifted
2019
+ accent: "#cf9a96",
2020
+ // dusty rose, lifted
2021
+ destructive: "#c9745c",
2022
+ // brick, lifted
1956
2023
  colors: {
1957
- red: "#ff0000",
1958
- orange: "#ff8000",
1959
- yellow: "#ffff00",
1960
- green: "#00ff00",
1961
- blue: "#0066ff",
1962
- purple: "#ff00ff",
1963
- teal: "#00cccc",
1964
- cyan: "#00ffff",
1965
- gray: "#808080",
1966
- black: "#111111",
1967
- white: "#ffffff"
2024
+ red: "#cf7a60",
2025
+ // terracotta
2026
+ orange: "#d9a96a",
2027
+ // tan / ochre
2028
+ yellow: "#d8c074",
2029
+ // straw
2030
+ green: "#9bb588",
2031
+ // sage lowland
2032
+ blue: "#7ba0bf",
2033
+ // ocean
2034
+ purple: "#b59ac0",
2035
+ // lilac / mauve
2036
+ teal: "#85b3a6",
2037
+ // seafoam
2038
+ cyan: "#92bccb",
2039
+ // shallow-water blue
2040
+ gray: "#9a8d76",
2041
+ // warm taupe
2042
+ black: "#27353f",
2043
+ // raised ocean
2044
+ white: "#e8dcc0"
2045
+ // parchment
1968
2046
  }
1969
2047
  }
1970
2048
  };
1971
- registerPalette(boldPalette);
2049
+ registerPalette(atlasPalette);
2050
+ }
2051
+ });
2052
+
2053
+ // src/palettes/blueprint.ts
2054
+ var blueprintPalette;
2055
+ var init_blueprint = __esm({
2056
+ "src/palettes/blueprint.ts"() {
2057
+ "use strict";
2058
+ init_registry();
2059
+ blueprintPalette = {
2060
+ id: "blueprint",
2061
+ name: "Blueprint",
2062
+ light: {
2063
+ bg: "#f4f8fb",
2064
+ // pale drafting white (faint cyan)
2065
+ surface: "#e6eef4",
2066
+ // drafting panel
2067
+ overlay: "#dde9f1",
2068
+ // popovers, dropdowns
2069
+ border: "#aac3d6",
2070
+ // pale blue grid line
2071
+ text: "#123a5e",
2072
+ // blueprint navy ink
2073
+ textMuted: "#4f7390",
2074
+ // faint draft note
2075
+ textOnFillLight: "#f4f8fb",
2076
+ // drafting white
2077
+ textOnFillDark: "#0c2f4d",
2078
+ // deep blueprint navy
2079
+ primary: "#1f5e8c",
2080
+ // blueprint blue
2081
+ secondary: "#5b7d96",
2082
+ // steel
2083
+ accent: "#b08a3e",
2084
+ // draftsman's ochre highlight
2085
+ destructive: "#c0504d",
2086
+ // correction red
2087
+ colors: {
2088
+ red: "#c25a4e",
2089
+ // correction red
2090
+ orange: "#c2823e",
2091
+ // ochre
2092
+ yellow: "#c2a843",
2093
+ // pencil gold
2094
+ green: "#4f8a6b",
2095
+ // drafting green
2096
+ blue: "#1f5e8c",
2097
+ // blueprint blue
2098
+ purple: "#6f5e96",
2099
+ // indigo pencil
2100
+ teal: "#3a8a8a",
2101
+ // teal
2102
+ cyan: "#3f8fb5",
2103
+ // cyan
2104
+ gray: "#7e8e98",
2105
+ // graphite
2106
+ black: "#123a5e",
2107
+ // navy ink
2108
+ white: "#e6eef4"
2109
+ // panel
2110
+ }
2111
+ },
2112
+ dark: {
2113
+ bg: "#103a5e",
2114
+ // deep blueprint blue (cyanotype ground)
2115
+ surface: "#16466e",
2116
+ // raised sheet
2117
+ overlay: "#1c5180",
2118
+ // popovers, dropdowns
2119
+ border: "#3a6f96",
2120
+ // grid line
2121
+ text: "#eaf2f8",
2122
+ // chalk white
2123
+ textMuted: "#9fc0d6",
2124
+ // faint chalk note
2125
+ textOnFillLight: "#eaf2f8",
2126
+ // chalk white
2127
+ textOnFillDark: "#0c2f4d",
2128
+ // deep blueprint navy
2129
+ primary: "#7fb8d8",
2130
+ // chalk cyan
2131
+ secondary: "#9fb8c8",
2132
+ // pale steel
2133
+ accent: "#d8c27a",
2134
+ // chalk amber
2135
+ destructive: "#e08a7a",
2136
+ // chalk correction red
2137
+ colors: {
2138
+ red: "#e0907e",
2139
+ // chalk red
2140
+ orange: "#e0ab78",
2141
+ // chalk amber
2142
+ yellow: "#e3d089",
2143
+ // chalk gold
2144
+ green: "#93c79e",
2145
+ // chalk green
2146
+ blue: "#8ec3e0",
2147
+ // chalk cyan-blue
2148
+ purple: "#b6a6d8",
2149
+ // chalk indigo
2150
+ teal: "#84c7c2",
2151
+ // chalk teal
2152
+ cyan: "#9fd6e0",
2153
+ // chalk cyan
2154
+ gray: "#aebecb",
2155
+ // chalk graphite
2156
+ black: "#16466e",
2157
+ // raised sheet
2158
+ white: "#eaf2f8"
2159
+ // chalk white
2160
+ }
2161
+ }
2162
+ };
2163
+ registerPalette(blueprintPalette);
1972
2164
  }
1973
2165
  });
1974
2166
 
@@ -2465,6 +2657,120 @@ var init_rose_pine = __esm({
2465
2657
  }
2466
2658
  });
2467
2659
 
2660
+ // src/palettes/slate.ts
2661
+ var slatePalette;
2662
+ var init_slate = __esm({
2663
+ "src/palettes/slate.ts"() {
2664
+ "use strict";
2665
+ init_registry();
2666
+ slatePalette = {
2667
+ id: "slate",
2668
+ name: "Slate",
2669
+ light: {
2670
+ bg: "#ffffff",
2671
+ // clean slide white
2672
+ surface: "#f3f5f8",
2673
+ // light cool-gray panel
2674
+ overlay: "#eaeef3",
2675
+ // popovers, dropdowns
2676
+ border: "#d4dae1",
2677
+ // hairline rule
2678
+ text: "#1f2933",
2679
+ // near-black slate (softer than pure black)
2680
+ textMuted: "#5b6672",
2681
+ // secondary label
2682
+ textOnFillLight: "#ffffff",
2683
+ // light text on dark fills
2684
+ textOnFillDark: "#1f2933",
2685
+ // dark text on light fills
2686
+ primary: "#3b6ea5",
2687
+ // confident corporate blue
2688
+ secondary: "#5b6672",
2689
+ // slate gray
2690
+ accent: "#3a9188",
2691
+ // muted teal accent
2692
+ destructive: "#c0504d",
2693
+ // brick red
2694
+ colors: {
2695
+ red: "#c0504d",
2696
+ // brick
2697
+ orange: "#cc7a33",
2698
+ // muted amber
2699
+ yellow: "#c9a227",
2700
+ // gold (not neon)
2701
+ green: "#5b9357",
2702
+ // forest / sage
2703
+ blue: "#3b6ea5",
2704
+ // corporate blue
2705
+ purple: "#7d5ba6",
2706
+ // muted violet
2707
+ teal: "#3a9188",
2708
+ // teal
2709
+ cyan: "#4f96c4",
2710
+ // steel cyan
2711
+ gray: "#7e8a97",
2712
+ // cool gray
2713
+ black: "#1f2933",
2714
+ // slate ink
2715
+ white: "#f3f5f8"
2716
+ // panel
2717
+ }
2718
+ },
2719
+ dark: {
2720
+ bg: "#161b22",
2721
+ // deep slate (keynote dark)
2722
+ surface: "#202833",
2723
+ // raised panel
2724
+ overlay: "#29323e",
2725
+ // popovers, dropdowns
2726
+ border: "#38424f",
2727
+ // divider
2728
+ text: "#e6eaef",
2729
+ // off-white
2730
+ textMuted: "#9aa5b1",
2731
+ // secondary label
2732
+ textOnFillLight: "#ffffff",
2733
+ // light text on dark fills
2734
+ textOnFillDark: "#161b22",
2735
+ // dark text on light fills
2736
+ primary: "#5b9bd5",
2737
+ // lifted corporate blue
2738
+ secondary: "#8593a3",
2739
+ // slate gray, lifted
2740
+ accent: "#45b3a3",
2741
+ // teal, lifted
2742
+ destructive: "#e07b6e",
2743
+ // brick, lifted
2744
+ colors: {
2745
+ red: "#e07b6e",
2746
+ // brick
2747
+ orange: "#e0975a",
2748
+ // amber
2749
+ yellow: "#d9bd5a",
2750
+ // gold
2751
+ green: "#74b56e",
2752
+ // forest / sage
2753
+ blue: "#5b9bd5",
2754
+ // corporate blue
2755
+ purple: "#a585c9",
2756
+ // violet
2757
+ teal: "#45b3a3",
2758
+ // teal
2759
+ cyan: "#62b0d9",
2760
+ // steel cyan
2761
+ gray: "#95a1ae",
2762
+ // cool gray
2763
+ black: "#202833",
2764
+ // raised panel
2765
+ white: "#e6eaef"
2766
+ // off-white
2767
+ }
2768
+ }
2769
+ };
2770
+ registerPalette(slatePalette);
2771
+ }
2772
+ });
2773
+
2468
2774
  // src/palettes/solarized.ts
2469
2775
  var solarizedPalette;
2470
2776
  var init_solarized = __esm({
@@ -2560,6 +2866,120 @@ var init_solarized = __esm({
2560
2866
  }
2561
2867
  });
2562
2868
 
2869
+ // src/palettes/tidewater.ts
2870
+ var tidewaterPalette;
2871
+ var init_tidewater = __esm({
2872
+ "src/palettes/tidewater.ts"() {
2873
+ "use strict";
2874
+ init_registry();
2875
+ tidewaterPalette = {
2876
+ id: "tidewater",
2877
+ name: "Tidewater",
2878
+ light: {
2879
+ bg: "#eceff0",
2880
+ // weathered sea-mist paper
2881
+ surface: "#e0e4e3",
2882
+ // worn deck panel
2883
+ overlay: "#dadfdf",
2884
+ // popovers, dropdowns
2885
+ border: "#a9b2b3",
2886
+ // muted slate rule
2887
+ text: "#18313f",
2888
+ // ship's-log navy ink
2889
+ textMuted: "#51636b",
2890
+ // faded log entry
2891
+ textOnFillLight: "#f3f5f3",
2892
+ // weathered white
2893
+ textOnFillDark: "#162c38",
2894
+ // deep navy
2895
+ primary: "#1f4e6b",
2896
+ // deep-sea navy
2897
+ secondary: "#b08a4f",
2898
+ // rope / manila tan
2899
+ accent: "#c69a3e",
2900
+ // brass
2901
+ destructive: "#c1433a",
2902
+ // signal-flag red
2903
+ colors: {
2904
+ red: "#c1433a",
2905
+ // signal-flag red
2906
+ orange: "#cc7a38",
2907
+ // weathered amber
2908
+ yellow: "#d6bf5a",
2909
+ // brass gold
2910
+ green: "#4f8a6b",
2911
+ // sea-glass green
2912
+ blue: "#1f4e6b",
2913
+ // deep-sea navy
2914
+ purple: "#6a5a8c",
2915
+ // twilight harbor
2916
+ teal: "#3d8c8c",
2917
+ // sea-glass teal
2918
+ cyan: "#4f9bb5",
2919
+ // shallow water
2920
+ gray: "#8a8d86",
2921
+ // driftwood gray
2922
+ black: "#18313f",
2923
+ // navy ink
2924
+ white: "#e0e4e3"
2925
+ // deck panel
2926
+ }
2927
+ },
2928
+ dark: {
2929
+ bg: "#0f2230",
2930
+ // night-harbor deep sea
2931
+ surface: "#16303f",
2932
+ // raised hull
2933
+ overlay: "#1d3a4a",
2934
+ // popovers, dropdowns
2935
+ border: "#2c4856",
2936
+ // rigging line
2937
+ text: "#e6ebe8",
2938
+ // weathered white
2939
+ textMuted: "#9aaab0",
2940
+ // faded label
2941
+ textOnFillLight: "#f3f5f3",
2942
+ // weathered white
2943
+ textOnFillDark: "#0f2230",
2944
+ // deep sea
2945
+ primary: "#4f9bc4",
2946
+ // lifted sea blue
2947
+ secondary: "#c9a46a",
2948
+ // rope tan, lifted
2949
+ accent: "#d9b25a",
2950
+ // brass, lifted
2951
+ destructive: "#e06a5e",
2952
+ // signal red, lifted
2953
+ colors: {
2954
+ red: "#e06a5e",
2955
+ // signal-flag red
2956
+ orange: "#df9a52",
2957
+ // amber
2958
+ yellow: "#e0c662",
2959
+ // brass gold
2960
+ green: "#6fb58c",
2961
+ // sea-glass green
2962
+ blue: "#4f9bc4",
2963
+ // sea blue
2964
+ purple: "#9486bf",
2965
+ // twilight harbor
2966
+ teal: "#5cb0ac",
2967
+ // sea-glass teal
2968
+ cyan: "#62b4cf",
2969
+ // shallow water
2970
+ gray: "#9aa39c",
2971
+ // driftwood gray
2972
+ black: "#16303f",
2973
+ // raised hull
2974
+ white: "#e6ebe8"
2975
+ // weathered white
2976
+ }
2977
+ }
2978
+ };
2979
+ registerPalette(tidewaterPalette);
2980
+ }
2981
+ });
2982
+
2563
2983
  // src/palettes/tokyo-night.ts
2564
2984
  var tokyoNightPalette;
2565
2985
  var init_tokyo_night = __esm({
@@ -2835,7 +3255,8 @@ var init_monokai = __esm({
2835
3255
  // src/palettes/index.ts
2836
3256
  var palettes_exports = {};
2837
3257
  __export(palettes_exports, {
2838
- boldPalette: () => boldPalette,
3258
+ atlasPalette: () => atlasPalette,
3259
+ blueprintPalette: () => blueprintPalette,
2839
3260
  catppuccinPalette: () => catppuccinPalette,
2840
3261
  contrastText: () => contrastText,
2841
3262
  draculaPalette: () => draculaPalette,
@@ -2856,7 +3277,9 @@ __export(palettes_exports, {
2856
3277
  rosePinePalette: () => rosePinePalette,
2857
3278
  shade: () => shade,
2858
3279
  shapeFill: () => shapeFill,
3280
+ slatePalette: () => slatePalette,
2859
3281
  solarizedPalette: () => solarizedPalette,
3282
+ tidewaterPalette: () => tidewaterPalette,
2860
3283
  tint: () => tint,
2861
3284
  tokyoNightPalette: () => tokyoNightPalette
2862
3285
  });
@@ -2866,17 +3289,21 @@ var init_palettes = __esm({
2866
3289
  "use strict";
2867
3290
  init_registry();
2868
3291
  init_color_utils();
2869
- init_bold();
3292
+ init_atlas();
3293
+ init_blueprint();
2870
3294
  init_catppuccin();
2871
3295
  init_gruvbox();
2872
3296
  init_nord();
2873
3297
  init_one_dark();
2874
3298
  init_rose_pine();
3299
+ init_slate();
2875
3300
  init_solarized();
3301
+ init_tidewater();
2876
3302
  init_tokyo_night();
2877
3303
  init_dracula();
2878
3304
  init_monokai();
2879
- init_bold();
3305
+ init_atlas();
3306
+ init_blueprint();
2880
3307
  init_catppuccin();
2881
3308
  init_dracula();
2882
3309
  init_gruvbox();
@@ -2884,9 +3311,15 @@ var init_palettes = __esm({
2884
3311
  init_nord();
2885
3312
  init_one_dark();
2886
3313
  init_rose_pine();
3314
+ init_slate();
2887
3315
  init_solarized();
3316
+ init_tidewater();
2888
3317
  init_tokyo_night();
2889
3318
  palettes = {
3319
+ atlas: atlasPalette,
3320
+ blueprint: blueprintPalette,
3321
+ slate: slatePalette,
3322
+ tidewater: tidewaterPalette,
2890
3323
  nord: nordPalette,
2891
3324
  catppuccin: catppuccinPalette,
2892
3325
  solarized: solarizedPalette,
@@ -2895,8 +3328,7 @@ var init_palettes = __esm({
2895
3328
  oneDark: oneDarkPalette,
2896
3329
  rosePine: rosePinePalette,
2897
3330
  dracula: draculaPalette,
2898
- monokai: monokaiPalette,
2899
- bold: boldPalette
3331
+ monokai: monokaiPalette
2900
3332
  };
2901
3333
  }
2902
3334
  });
@@ -3406,6 +3838,9 @@ function controlsGroupCapsuleWidth(toggles) {
3406
3838
  }
3407
3839
  return w;
3408
3840
  }
3841
+ function isAppHostedControls(config, isExport) {
3842
+ return !isExport && config.controlsHost === "app" && !!config.controlsGroup && config.controlsGroup.toggles.length > 0;
3843
+ }
3409
3844
  function buildControlsGroupLayout(config, state) {
3410
3845
  const cg = config.controlsGroup;
3411
3846
  if (!cg || cg.toggles.length === 0) return void 0;
@@ -3459,6 +3894,7 @@ function buildControlsGroupLayout(config, state) {
3459
3894
  function computeLegendLayout(config, state, containerWidth) {
3460
3895
  const { groups, controls: configControls, mode } = config;
3461
3896
  const isExport = mode === "export";
3897
+ const gated = isAppHostedControls(config, isExport);
3462
3898
  const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
3463
3899
  if (isExport && !activeGroupName) {
3464
3900
  return {
@@ -3469,7 +3905,7 @@ function computeLegendLayout(config, state, containerWidth) {
3469
3905
  pills: []
3470
3906
  };
3471
3907
  }
3472
- const controlsGroupLayout = isExport ? void 0 : buildControlsGroupLayout(config, state);
3908
+ const controlsGroupLayout = isExport || gated ? void 0 : buildControlsGroupLayout(config, state);
3473
3909
  const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0 || !!g.gradient);
3474
3910
  if (visibleGroups.length === 0 && (!configControls || configControls.length === 0) && !controlsGroupLayout) {
3475
3911
  return {
@@ -8371,8 +8807,8 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8371
8807
  const pt = points[i];
8372
8808
  const ptSize = pt.size ?? symbolSize;
8373
8809
  const minGap = ptSize / 2 + 4;
8374
- const labelWidth = pt.name.length * fontSize * 0.6 + 8;
8375
- const labelX = pt.px - labelWidth / 2;
8810
+ const labelWidth2 = pt.name.length * fontSize * 0.6 + 8;
8811
+ const labelX = pt.px - labelWidth2 / 2;
8376
8812
  let bestLabelY = 0;
8377
8813
  let bestOffset = Infinity;
8378
8814
  let placed = false;
@@ -8384,7 +8820,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8384
8820
  const candidate = {
8385
8821
  x: labelX,
8386
8822
  y: labelY,
8387
- w: labelWidth,
8823
+ w: labelWidth2,
8388
8824
  h: labelHeight
8389
8825
  };
8390
8826
  let collision = false;
@@ -8426,7 +8862,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8426
8862
  const labelRect = {
8427
8863
  x: labelX,
8428
8864
  y: bestLabelY,
8429
- w: labelWidth,
8865
+ w: labelWidth2,
8430
8866
  h: labelHeight
8431
8867
  };
8432
8868
  placedLabels.push(labelRect);
@@ -8462,7 +8898,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8462
8898
  shape: {
8463
8899
  x: labelX - bgPad,
8464
8900
  y: bestLabelY - bgPad,
8465
- width: labelWidth + bgPad * 2,
8901
+ width: labelWidth2 + bgPad * 2,
8466
8902
  height: labelHeight + bgPad * 2
8467
8903
  },
8468
8904
  style: { fill: bg },
@@ -15898,10 +16334,6 @@ function parseMap(content) {
15898
16334
  handleTag(trimmed, lineNumber);
15899
16335
  continue;
15900
16336
  }
15901
- if ((firstWord === "muted" || firstWord === "natural") && trimmed === firstWord) {
15902
- handleDirective(firstWord, "", lineNumber);
15903
- continue;
15904
- }
15905
16337
  if (DIRECTIVE_SET.has(firstWord) && !trimmed.slice(firstWord.length).trimStart().startsWith(":")) {
15906
16338
  handleDirective(
15907
16339
  firstWord,
@@ -15948,28 +16380,13 @@ function parseMap(content) {
15948
16380
  pushWarning(line12, `Duplicate directive "${key}" \u2014 last value wins.`);
15949
16381
  };
15950
16382
  switch (key) {
15951
- case "region":
15952
- dup(d.region);
15953
- d.region = value;
15954
- break;
15955
- case "projection":
15956
- dup(d.projection);
15957
- if (value && ![
15958
- "equirectangular",
15959
- "natural-earth",
15960
- "albers-usa",
15961
- "mercator"
15962
- ].includes(value))
15963
- pushWarning(
15964
- line12,
15965
- `Unknown projection "${value}" (expected equirectangular | natural-earth | albers-usa | mercator).`
15966
- );
15967
- d.projection = value;
15968
- break;
15969
- case "region-metric":
16383
+ case "region-metric": {
15970
16384
  dup(d.regionMetric);
15971
- d.regionMetric = value;
16385
+ const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
16386
+ d.regionMetric = rmLabel;
16387
+ if (rmColor) d.regionMetricColor = rmColor;
15972
16388
  break;
16389
+ }
15973
16390
  case "poi-metric":
15974
16391
  dup(d.poiMetric);
15975
16392
  d.poiMetric = value;
@@ -15978,85 +16395,43 @@ function parseMap(content) {
15978
16395
  dup(d.flowMetric);
15979
16396
  d.flowMetric = value;
15980
16397
  break;
15981
- case "scale":
15982
- dup(d.scale);
15983
- {
15984
- const s = parseScale(value, line12);
15985
- if (s) d.scale = s;
15986
- }
15987
- break;
15988
- case "region-labels":
15989
- dup(d.regionLabels);
15990
- if (value && !["full", "abbrev", "off"].includes(value))
15991
- pushWarning(
15992
- line12,
15993
- `Unknown region-labels "${value}" (expected full | abbrev | off).`
15994
- );
15995
- d.regionLabels = value;
15996
- break;
15997
- case "poi-labels":
15998
- dup(d.poiLabels);
15999
- if (value && !["off", "auto", "all"].includes(value))
16000
- pushWarning(
16001
- line12,
16002
- `Unknown poi-labels "${value}" (expected off | auto | all).`
16003
- );
16004
- d.poiLabels = value;
16005
- break;
16006
- case "default-country":
16007
- dup(d.defaultCountry);
16008
- d.defaultCountry = value;
16009
- break;
16010
- case "default-state":
16011
- dup(d.defaultState);
16012
- d.defaultState = value;
16398
+ case "locale":
16399
+ dup(d.locale);
16400
+ d.locale = value;
16013
16401
  break;
16014
16402
  case "active-tag":
16015
16403
  dup(d.activeTag);
16016
16404
  d.activeTag = value;
16017
16405
  break;
16406
+ case "caption":
16407
+ dup(d.caption);
16408
+ d.caption = value;
16409
+ break;
16410
+ // ── Cosmetic `no-*` opt-outs: bare flags, idempotent (mirror `no-legend`,
16411
+ // no dup warning); each defaults the feature ON when absent. ──
16018
16412
  case "no-legend":
16019
16413
  d.noLegend = true;
16020
16414
  break;
16021
- case "muted":
16022
- case "natural":
16023
- if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
16024
- pushWarning(
16025
- line12,
16026
- `Conflicting basemap dress \u2014 "${d.basemapStyle}" then "${key}"; last wins.`
16027
- );
16028
- d.basemapStyle = key;
16415
+ case "no-coastline":
16416
+ d.noCoastline = true;
16029
16417
  break;
16030
- case "subtitle":
16031
- dup(d.subtitle);
16032
- d.subtitle = value;
16418
+ case "no-relief":
16419
+ d.noRelief = true;
16033
16420
  break;
16034
- case "caption":
16035
- dup(d.caption);
16036
- d.caption = value;
16421
+ case "no-context-labels":
16422
+ d.noContextLabels = true;
16423
+ break;
16424
+ case "no-region-labels":
16425
+ d.noRegionLabels = true;
16426
+ break;
16427
+ case "no-poi-labels":
16428
+ d.noPoiLabels = true;
16429
+ break;
16430
+ case "no-colorize":
16431
+ d.noColorize = true;
16037
16432
  break;
16038
16433
  }
16039
16434
  }
16040
- function parseScale(value, line12) {
16041
- const toks = value.split(/\s+/).filter(Boolean);
16042
- const min = Number(toks[0]);
16043
- const max = Number(toks[1]);
16044
- if (!Number.isFinite(min) || !Number.isFinite(max)) {
16045
- pushError(line12, `scale requires numeric <min> <max> (got "${value}").`);
16046
- return null;
16047
- }
16048
- const scale = { min, max };
16049
- if (toks[2] === "center") {
16050
- const c = Number(toks[3]);
16051
- if (Number.isFinite(c)) scale.center = c;
16052
- else
16053
- pushError(
16054
- line12,
16055
- `scale center requires a number (got "${toks[3] ?? ""}").`
16056
- );
16057
- }
16058
- return scale;
16059
- }
16060
16435
  function handleTag(trimmed, line12) {
16061
16436
  const m = matchTagBlockHeading(trimmed);
16062
16437
  if (!m) {
@@ -16130,6 +16505,7 @@ function parseMap(content) {
16130
16505
  };
16131
16506
  if (regionScope !== void 0) region.scope = regionScope;
16132
16507
  if (valueNum !== void 0) region.value = valueNum;
16508
+ if (split.color) region.color = split.color;
16133
16509
  regions.push(region);
16134
16510
  }
16135
16511
  function handlePoi(rest, line12, indent) {
@@ -16154,6 +16530,7 @@ function parseMap(content) {
16154
16530
  const poi = { pos, tags, meta, lineNumber: line12 };
16155
16531
  if (split.alias) poi.alias = split.alias;
16156
16532
  if (label !== void 0) poi.label = label;
16533
+ if (split.color) poi.color = split.color;
16157
16534
  pois.push(poi);
16158
16535
  open.poi = { poi, indent };
16159
16536
  }
@@ -16254,13 +16631,15 @@ function parseMap(content) {
16254
16631
  pushError(line12, `Edge has an empty endpoint: "${trimmed}".`);
16255
16632
  continue;
16256
16633
  }
16257
- const meta = k === links.length - 1 ? lastSplit.meta : {};
16634
+ const isLast = k === links.length - 1;
16635
+ const meta = isLast ? lastSplit.meta : {};
16636
+ const style = links[k].style === "arc" ? "arc" : "straight";
16258
16637
  edges.push({
16259
16638
  from,
16260
16639
  to,
16261
16640
  ...links[k].label !== void 0 && { label: links[k].label },
16262
16641
  directed: links[k].directed,
16263
- style: links[k].style,
16642
+ style,
16264
16643
  meta,
16265
16644
  lineNumber: line12
16266
16645
  });
@@ -16346,20 +16725,19 @@ var init_parser12 = __esm({
16346
16725
  LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
16347
16726
  AT_RE = /(^|[\s,])at\s*:/i;
16348
16727
  DIRECTIVE_SET = /* @__PURE__ */ new Set([
16349
- "region",
16350
- "projection",
16351
16728
  "region-metric",
16352
16729
  "poi-metric",
16353
16730
  "flow-metric",
16354
- "scale",
16355
- "region-labels",
16356
- "poi-labels",
16357
- "default-country",
16358
- "default-state",
16731
+ "locale",
16359
16732
  "active-tag",
16733
+ "caption",
16360
16734
  "no-legend",
16361
- "subtitle",
16362
- "caption"
16735
+ "no-coastline",
16736
+ "no-relief",
16737
+ "no-context-labels",
16738
+ "no-region-labels",
16739
+ "no-poi-labels",
16740
+ "no-colorize"
16363
16741
  ]);
16364
16742
  }
16365
16743
  });
@@ -24281,8 +24659,8 @@ function renderKanban(container, parsed, palette, isDark, options) {
24281
24659
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24282
24660
  for (const meta of tagMeta) {
24283
24661
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(`${meta.label}: `);
24284
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24285
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24662
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24663
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24286
24664
  metaY += sCardMetaLineHeight;
24287
24665
  }
24288
24666
  for (const detail of card.details) {
@@ -24626,8 +25004,8 @@ function renderSwimlaneCard(parent, cardLayout, tagGroups, activeTagGroup, palet
24626
25004
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24627
25005
  for (const meta of tagMeta) {
24628
25006
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", palette.textMuted).text(`${meta.label}: `);
24629
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24630
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
25007
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
25008
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24631
25009
  metaY += sCardMetaLineHeight;
24632
25010
  }
24633
25011
  for (const detail of card.details) {
@@ -25461,8 +25839,8 @@ function classifyEREntities(tables, relationships) {
25461
25839
  }
25462
25840
  }
25463
25841
  const mmParticipants = /* @__PURE__ */ new Set();
25464
- for (const [id, neighbors] of tableStarNeighbors) {
25465
- if (neighbors.size >= 2) mmParticipants.add(id);
25842
+ for (const [id, neighbors2] of tableStarNeighbors) {
25843
+ if (neighbors2.size >= 2) mmParticipants.add(id);
25466
25844
  }
25467
25845
  const indegreeValues = Object.values(indegreeMap);
25468
25846
  const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
@@ -26135,7 +26513,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26135
26513
  controlsExpanded,
26136
26514
  onToggleDescriptions,
26137
26515
  onToggleControlsExpand,
26138
- exportMode = false
26516
+ exportMode = false,
26517
+ controlsHost
26139
26518
  } = options ?? {};
26140
26519
  d3Selection6.select(container).selectAll(":not([data-d3-tooltip])").remove();
26141
26520
  const width = exportDims?.width ?? container.clientWidth;
@@ -26153,7 +26532,11 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26153
26532
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26154
26533
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26155
26534
  const sTitleY = sctx.structural(TITLE_Y);
26156
- const sLegendHeight = sctx.structural(
26535
+ const reserveHasDescriptions = parsed.nodes.some(
26536
+ (n) => n.description && n.description.length > 0
26537
+ );
26538
+ const willRenderLegend = parsed.tagGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26539
+ const sLegendHeight = willRenderLegend ? sctx.structural(
26157
26540
  getMaxLegendReservedHeight(
26158
26541
  {
26159
26542
  groups: parsed.tagGroups,
@@ -26162,7 +26545,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26162
26545
  },
26163
26546
  width
26164
26547
  )
26165
- );
26548
+ ) : 0;
26166
26549
  const activeGroup = resolveActiveTagGroup(
26167
26550
  parsed.tagGroups,
26168
26551
  parsed.options["active-tag"],
@@ -26477,10 +26860,10 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26477
26860
  const hasDescriptions = parsed.nodes.some(
26478
26861
  (n) => n.description && n.description.length > 0
26479
26862
  );
26480
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions;
26863
+ const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26481
26864
  if (hasLegend) {
26482
26865
  let controlsGroup;
26483
- if (hasDescriptions && onToggleDescriptions) {
26866
+ if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
26484
26867
  controlsGroup = {
26485
26868
  toggles: [
26486
26869
  {
@@ -26498,7 +26881,14 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26498
26881
  groups: parsed.tagGroups,
26499
26882
  position: { placement: "top-center", titleRelation: "below-title" },
26500
26883
  mode: exportMode ? "export" : "preview",
26501
- ...controlsGroup !== void 0 && { controlsGroup }
26884
+ // Keep inactive sibling tag groups visible as collapsed pills so the user
26885
+ // can click one to flip the active colouring dimension (preview only —
26886
+ // export shows just the active group). Without this, declaring a second
26887
+ // tag group (e.g. Team) leaves it invisible whenever another group is
26888
+ // active. The app's BoxesAndLinesPreview already wires pill clicks.
26889
+ showInactivePills: true,
26890
+ ...controlsGroup !== void 0 && { controlsGroup },
26891
+ ...controlsHost !== void 0 && { controlsHost }
26502
26892
  };
26503
26893
  const legendState = {
26504
26894
  activeGroup,
@@ -27745,8 +28135,9 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27745
28135
  const containerHeight = exportDims?.height ?? (container.getBoundingClientRect().height || 600);
27746
28136
  d3Selection7.select(container).selectAll("*").remove();
27747
28137
  const svg = d3Selection7.select(container).append("svg").attr("width", containerWidth).attr("height", containerHeight).attr("viewBox", `0 0 ${containerWidth} ${containerHeight}`).attr("preserveAspectRatio", "xMidYMin meet").style("font-family", FONT_FAMILY);
28138
+ const appHosted = options?.controlsHost === "app";
27748
28139
  const hasControls = !!options?.onToggleColorByDepth || !!options?.onToggleDescriptions;
27749
- const hasLegend = parsed.tagGroups.length > 0 || hasControls;
28140
+ const hasLegend = parsed.tagGroups.length > 0 || hasControls && !appHosted;
27750
28141
  const fixedLegend = !isExport && hasLegend;
27751
28142
  const legendReserve = fixedLegend ? getMaxLegendReservedHeight(
27752
28143
  {
@@ -27840,7 +28231,10 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27840
28231
  }),
27841
28232
  position: { placement: "top-center", titleRelation: "below-title" },
27842
28233
  mode: options?.exportMode ? "export" : "preview",
27843
- ...controlsToggles !== void 0 && { controlsGroup: controlsToggles }
28234
+ ...controlsToggles !== void 0 && { controlsGroup: controlsToggles },
28235
+ ...options?.controlsHost !== void 0 && {
28236
+ controlsHost: options.controlsHost
28237
+ }
27844
28238
  };
27845
28239
  const legendState = {
27846
28240
  activeGroup: options?.colorByDepth ? null : activeTagGroup !== void 0 ? activeTagGroup : parsed.options["active-tag"] ?? null,
@@ -28283,8 +28677,8 @@ function computeFieldAlignX(children) {
28283
28677
  for (const child of children) {
28284
28678
  if (child.metadata["_labelField"] === "true" && child.children.length >= 2) {
28285
28679
  const labelEl = child.children[0];
28286
- const labelWidth = labelEl.label.length * CHAR_WIDTH5;
28287
- maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
28680
+ const labelWidth2 = labelEl.label.length * CHAR_WIDTH5;
28681
+ maxLabelWidth = Math.max(maxLabelWidth, labelWidth2);
28288
28682
  labelFieldCount++;
28289
28683
  }
28290
28684
  }
@@ -33249,7 +33643,7 @@ function hasRoles(node) {
33249
33643
  function computeNodeWidth2(node, expanded, options) {
33250
33644
  const badgeVal = node.computedConcurrentInvocations === 0 && node.computedInstances > 1 ? node.computedInstances : 0;
33251
33645
  const badgeLen = badgeVal > 0 ? `${badgeVal}x`.length + 2 : 0;
33252
- const labelWidth = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33646
+ const labelWidth2 = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33253
33647
  const allKeys = [];
33254
33648
  if (node.computedRps > 0) allKeys.push("RPS");
33255
33649
  if (expanded) {
@@ -33293,7 +33687,7 @@ function computeNodeWidth2(node, expanded, options) {
33293
33687
  allKeys.push("overflow");
33294
33688
  }
33295
33689
  }
33296
- if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth);
33690
+ if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth2);
33297
33691
  const maxKeyLen = Math.max(...allKeys.map((k) => k.length));
33298
33692
  let maxRowWidth = 0;
33299
33693
  if (node.computedRps > 0) {
@@ -33381,7 +33775,7 @@ function computeNodeWidth2(node, expanded, options) {
33381
33775
  truncated.length * META_CHAR_WIDTH3 + PADDING_X3
33382
33776
  );
33383
33777
  }
33384
- return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20, descWidth);
33778
+ return Math.max(MIN_NODE_WIDTH2, labelWidth2, maxRowWidth + 20, descWidth);
33385
33779
  }
33386
33780
  function computeNodeHeight2(node, expanded, options) {
33387
33781
  const propCount = countDisplayProps(node, expanded, options);
@@ -34931,8 +35325,9 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
34931
35325
  }
34932
35326
  return groups;
34933
35327
  }
34934
- function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false) {
35328
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false, controlsHost) {
34935
35329
  if (legendGroups.length === 0 && !playback) return;
35330
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34936
35331
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
34937
35332
  if (activeGroup) {
34938
35333
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
@@ -34941,14 +35336,29 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34941
35336
  name: g.name,
34942
35337
  entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
34943
35338
  }));
34944
- if (playback) {
35339
+ if (playback && !appHostedPlayback) {
34945
35340
  allGroups.push({ name: "Playback", entries: [] });
34946
35341
  }
34947
35342
  const legendConfig = {
34948
35343
  groups: allGroups,
34949
35344
  position: { placement: "top-center", titleRelation: "below-title" },
34950
35345
  mode: exportMode ? "export" : "preview",
34951
- showEmptyGroups: true
35346
+ showEmptyGroups: true,
35347
+ ...appHostedPlayback && {
35348
+ controlsHost: "app",
35349
+ controlsGroup: {
35350
+ toggles: [
35351
+ {
35352
+ id: "playback",
35353
+ type: "toggle",
35354
+ label: "Playback",
35355
+ active: true,
35356
+ onToggle: () => {
35357
+ }
35358
+ }
35359
+ ]
35360
+ }
35361
+ }
34952
35362
  };
34953
35363
  const legendState = { activeGroup };
34954
35364
  renderLegendD3(
@@ -34999,8 +35409,9 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34999
35409
  }
35000
35410
  }
35001
35411
  }
35002
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
35412
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes, controlsHost) {
35003
35413
  d3Selection11.select(container).selectAll(":not([data-d3-tooltip])").remove();
35414
+ const appHostedPlayback = controlsHost === "app" && !!playback;
35004
35415
  const ctx = ScaleContext.identity();
35005
35416
  const sc = buildScaledConstants(ctx);
35006
35417
  const legendGroups = computeInfraLegendGroups(
@@ -35009,7 +35420,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35009
35420
  palette,
35010
35421
  layout.edges
35011
35422
  );
35012
- const hasLegend = legendGroups.length > 0 || !!playback;
35423
+ const hasLegend = legendGroups.length > 0 || !!playback && !appHostedPlayback;
35013
35424
  const fixedLegend = !exportMode && hasLegend;
35014
35425
  const legendDynamicH = hasLegend ? getMaxLegendReservedHeight(
35015
35426
  {
@@ -35153,7 +35564,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35153
35564
  isDark,
35154
35565
  activeGroup ?? null,
35155
35566
  playback ?? void 0,
35156
- exportMode
35567
+ exportMode,
35568
+ controlsHost
35157
35569
  );
35158
35570
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
35159
35571
  } else {
@@ -35166,7 +35578,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35166
35578
  isDark,
35167
35579
  activeGroup ?? null,
35168
35580
  playback ?? void 0,
35169
- exportMode
35581
+ exportMode,
35582
+ controlsHost
35170
35583
  );
35171
35584
  }
35172
35585
  }
@@ -42800,6 +43213,9 @@ function renderTechRadar(container, parsed, palette, isDark, onClickItem, export
42800
43213
  onToggle: (active) => options.onToggleListing(active)
42801
43214
  }
42802
43215
  ]
43216
+ },
43217
+ ...options.controlsHost !== void 0 && {
43218
+ controlsHost: options.controlsHost
42803
43219
  }
42804
43220
  };
42805
43221
  const legendState = {
@@ -44622,7 +45038,7 @@ function computeCycleLayout(parsed, options) {
44622
45038
  const circleNodes = parsed.options["circle-nodes"] === "true";
44623
45039
  const nodeDims = parsed.nodes.map((node) => {
44624
45040
  const hasDesc = !hideDescriptions && node.description.length > 0;
44625
- const labelWidth = Math.max(
45041
+ const labelWidth2 = Math.max(
44626
45042
  MIN_NODE_WIDTH4,
44627
45043
  node.label.length * LABEL_CHAR_W + NODE_PAD_X * 2
44628
45044
  );
@@ -44631,12 +45047,12 @@ function computeCycleLayout(parsed, options) {
44631
45047
  }
44632
45048
  if (!hasDesc) {
44633
45049
  return {
44634
- width: Math.min(MAX_NODE_WIDTH3, labelWidth),
45050
+ width: Math.min(MAX_NODE_WIDTH3, labelWidth2),
44635
45051
  height: PLAIN_NODE_HEIGHT,
44636
45052
  wrappedDesc: []
44637
45053
  };
44638
45054
  }
44639
- return chooseDescribedRectDims(node.description, labelWidth);
45055
+ return chooseDescribedRectDims(node.description, labelWidth2);
44640
45056
  });
44641
45057
  if (circleNodes) {
44642
45058
  const maxDiam = Math.max(...nodeDims.map((d) => d.width));
@@ -44832,10 +45248,10 @@ function computeCycleLayout(parsed, options) {
44832
45248
  scale
44833
45249
  };
44834
45250
  }
44835
- function chooseDescribedRectDims(description, labelWidth) {
45251
+ function chooseDescribedRectDims(description, labelWidth2) {
44836
45252
  const minW = Math.min(
44837
45253
  MAX_NODE_WIDTH3,
44838
- Math.max(MIN_NODE_WIDTH4, labelWidth, DESC_MIN_WIDTH)
45254
+ Math.max(MIN_NODE_WIDTH4, labelWidth2, DESC_MIN_WIDTH)
44839
45255
  );
44840
45256
  let best = null;
44841
45257
  let bestScore = Infinity;
@@ -45264,7 +45680,8 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45264
45680
  const hideDescriptions = (renderOptions?.hideDescriptions ?? false) || parsed.options["no-descriptions"] === "true" || viewState?.hd === true;
45265
45681
  const showDescriptions = !hideDescriptions;
45266
45682
  const hasDescriptions = parsed.nodes.some((n) => n.description.length > 0) || parsed.edges.some((e) => e.description.length > 0);
45267
- const hasLegend = hasDescriptions && !!renderOptions?.onToggleDescriptions;
45683
+ const appHostedControls = renderOptions?.controlsHost === "app";
45684
+ const hasLegend = !appHostedControls && hasDescriptions && !!renderOptions?.onToggleDescriptions;
45268
45685
  const showTitle = !!parsed.title && parsed.options["no-title"] !== "on";
45269
45686
  const legendOffset = hasLegend ? sLegendHeight : 0;
45270
45687
  const layoutHeight = height - (showTitle ? sTitleAreaHeight : 0) - legendOffset;
@@ -45301,7 +45718,10 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45301
45718
  groups: [],
45302
45719
  position: { placement: "top-center", titleRelation: "below-title" },
45303
45720
  mode: renderOptions?.exportMode ? "export" : "preview",
45304
- controlsGroup
45721
+ controlsGroup,
45722
+ ...renderOptions?.controlsHost !== void 0 && {
45723
+ controlsHost: renderOptions.controlsHost
45724
+ }
45305
45725
  };
45306
45726
  const legendState = {
45307
45727
  activeGroup: null,
@@ -45555,8 +45975,8 @@ var init_renderer15 = __esm({
45555
45975
  });
45556
45976
 
45557
45977
  // src/map/geo.ts
45558
- import { feature } from "topojson-client";
45559
- import { geoBounds } from "d3-geo";
45978
+ import { feature, neighbors } from "topojson-client";
45979
+ import { geoBounds, geoArea } from "d3-geo";
45560
45980
  function geomObject(topo) {
45561
45981
  const key = Object.keys(topo.objects)[0];
45562
45982
  return topo.objects[key];
@@ -45573,6 +45993,107 @@ function featureIndex(topo) {
45573
45993
  }
45574
45994
  return idx;
45575
45995
  }
45996
+ function buildAdjacency(topo) {
45997
+ const cached = adjacencyCache.get(topo);
45998
+ if (cached) return cached;
45999
+ const geometries = geomObject(topo).geometries;
46000
+ const nb = neighbors(geometries);
46001
+ const sets = /* @__PURE__ */ new Map();
46002
+ geometries.forEach((g, i) => {
46003
+ if (!g.type || g.type === "null") return;
46004
+ let set = sets.get(g.id);
46005
+ if (!set) {
46006
+ set = /* @__PURE__ */ new Set();
46007
+ sets.set(g.id, set);
46008
+ }
46009
+ for (const j of nb[i] ?? []) {
46010
+ const nid = geometries[j]?.id;
46011
+ if (nid && nid !== g.id) set.add(nid);
46012
+ }
46013
+ });
46014
+ const out = /* @__PURE__ */ new Map();
46015
+ for (const [iso, set] of sets) out.set(iso, [...set].sort());
46016
+ adjacencyCache.set(topo, out);
46017
+ return out;
46018
+ }
46019
+ function decodeFeatures(topo) {
46020
+ return geomObject(topo).geometries.map((g) => {
46021
+ const f = feature(topo, g);
46022
+ return {
46023
+ type: "Feature",
46024
+ id: g.id,
46025
+ properties: g.properties,
46026
+ geometry: f.geometry
46027
+ };
46028
+ });
46029
+ }
46030
+ function pointInRing(lon, lat, ring) {
46031
+ let inside = false;
46032
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46033
+ const xi = ring[i][0];
46034
+ const yi = ring[i][1];
46035
+ const xj = ring[j][0];
46036
+ const yj = ring[j][1];
46037
+ const intersect = yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi;
46038
+ if (intersect) inside = !inside;
46039
+ }
46040
+ return inside;
46041
+ }
46042
+ function pointOnRingEdge(lon, lat, ring) {
46043
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46044
+ const xi = ring[i][0];
46045
+ const yi = ring[i][1];
46046
+ const xj = ring[j][0];
46047
+ const yj = ring[j][1];
46048
+ if (lon < Math.min(xi, xj) - EDGE_EPS || lon > Math.max(xi, xj) + EDGE_EPS)
46049
+ continue;
46050
+ if (lat < Math.min(yi, yj) - EDGE_EPS || lat > Math.max(yi, yj) + EDGE_EPS)
46051
+ continue;
46052
+ const cross = (xj - xi) * (lat - yi) - (yj - yi) * (lon - xi);
46053
+ if (Math.abs(cross) <= EDGE_EPS) return true;
46054
+ }
46055
+ return false;
46056
+ }
46057
+ function pointInGeometry(geometry, lon, lat) {
46058
+ const g = geometry;
46059
+ if (!g) return false;
46060
+ const polys = g.type === "Polygon" ? [g.coordinates] : g.type === "MultiPolygon" ? g.coordinates : [];
46061
+ for (const rings of polys) {
46062
+ if (!rings.length) continue;
46063
+ if (pointOnRingEdge(lon, lat, rings[0])) return true;
46064
+ if (!pointInRing(lon, lat, rings[0])) continue;
46065
+ let inHole = false;
46066
+ for (let h = 1; h < rings.length; h++) {
46067
+ if (pointInRing(lon, lat, rings[h]) && !pointOnRingEdge(lon, lat, rings[h])) {
46068
+ inHole = true;
46069
+ break;
46070
+ }
46071
+ }
46072
+ if (!inHole) return true;
46073
+ }
46074
+ return false;
46075
+ }
46076
+ function regionAt(lonLat, countries, states) {
46077
+ const lon = lonLat[0];
46078
+ const lat = lonLat[1];
46079
+ let country = null;
46080
+ for (const f of countries) {
46081
+ if (pointInGeometry(f.geometry, lon, lat)) {
46082
+ country = { iso: f.id, name: f.properties.name };
46083
+ break;
46084
+ }
46085
+ }
46086
+ let state = null;
46087
+ if (country?.iso === "US" && states) {
46088
+ for (const f of states) {
46089
+ if (pointInGeometry(f.geometry, lon, lat)) {
46090
+ state = { iso: f.id, name: f.properties.name };
46091
+ break;
46092
+ }
46093
+ }
46094
+ }
46095
+ return { country, state };
46096
+ }
45576
46097
  function featureBbox(topo, geomId) {
45577
46098
  const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45578
46099
  if (!geom) return null;
@@ -45584,6 +46105,74 @@ function featureBbox(topo, geomId) {
45584
46105
  [b[1][0], b[1][1]]
45585
46106
  ];
45586
46107
  }
46108
+ function explodePolygons(gj) {
46109
+ const g = gj.geometry ?? gj;
46110
+ const t = g.type;
46111
+ const coords = g.coordinates;
46112
+ if (t === "Polygon") {
46113
+ return [
46114
+ { type: "Feature", geometry: { type: "Polygon", coordinates: coords } }
46115
+ ];
46116
+ }
46117
+ if (t === "MultiPolygon") {
46118
+ return coords.map((rings) => ({
46119
+ type: "Feature",
46120
+ geometry: { type: "Polygon", coordinates: rings }
46121
+ }));
46122
+ }
46123
+ return [];
46124
+ }
46125
+ function bboxGap(a, b) {
46126
+ const lonGap = Math.max(0, a[0][0] - b[1][0], b[0][0] - a[1][0]);
46127
+ const latGap = Math.max(0, a[0][1] - b[1][1], b[0][1] - a[1][1]);
46128
+ return Math.max(lonGap, latGap);
46129
+ }
46130
+ function featureBboxPrimary(topo, geomId) {
46131
+ const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
46132
+ if (!geom) return null;
46133
+ const gj = feature(topo, geom);
46134
+ const parts = explodePolygons(gj);
46135
+ if (parts.length <= 1) return featureBbox(topo, geomId);
46136
+ const polys = parts.map((p) => {
46137
+ const b = geoBounds(p);
46138
+ if (!b || !Number.isFinite(b[0][0])) return null;
46139
+ const wraps = b[1][0] < b[0][0];
46140
+ const bbox = [
46141
+ [b[0][0], b[0][1]],
46142
+ [b[1][0], b[1][1]]
46143
+ ];
46144
+ return { bbox, area: geoArea(p), wraps };
46145
+ }).filter(
46146
+ (p) => p !== null
46147
+ );
46148
+ if (polys.length <= 1 || polys.some((p) => p.wraps))
46149
+ return featureBbox(topo, geomId);
46150
+ const maxArea = Math.max(...polys.map((p) => p.area));
46151
+ const anchor = polys.find((p) => p.area === maxArea);
46152
+ const cluster = [
46153
+ [anchor.bbox[0][0], anchor.bbox[0][1]],
46154
+ [anchor.bbox[1][0], anchor.bbox[1][1]]
46155
+ ];
46156
+ const remaining = polys.filter((p) => p !== anchor);
46157
+ let added = true;
46158
+ while (added) {
46159
+ added = false;
46160
+ for (let i = remaining.length - 1; i >= 0; i--) {
46161
+ const p = remaining[i];
46162
+ const near = bboxGap(p.bbox, cluster) <= DETACH_GAP_DEG;
46163
+ const large = p.area >= DETACH_AREA_FRAC * maxArea;
46164
+ if (near || large) {
46165
+ cluster[0][0] = Math.min(cluster[0][0], p.bbox[0][0]);
46166
+ cluster[0][1] = Math.min(cluster[0][1], p.bbox[0][1]);
46167
+ cluster[1][0] = Math.max(cluster[1][0], p.bbox[1][0]);
46168
+ cluster[1][1] = Math.max(cluster[1][1], p.bbox[1][1]);
46169
+ remaining.splice(i, 1);
46170
+ added = true;
46171
+ }
46172
+ }
46173
+ }
46174
+ return cluster;
46175
+ }
45587
46176
  function unionExtent(boxes, points) {
45588
46177
  const lats = [];
45589
46178
  const lons = [];
@@ -45622,11 +46211,15 @@ function unionLongitudes(lons) {
45622
46211
  }
45623
46212
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45624
46213
  }
45625
- var fold;
46214
+ var fold, adjacencyCache, EDGE_EPS, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45626
46215
  var init_geo = __esm({
45627
46216
  "src/map/geo.ts"() {
45628
46217
  "use strict";
45629
46218
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
46219
+ adjacencyCache = /* @__PURE__ */ new WeakMap();
46220
+ EDGE_EPS = 1e-9;
46221
+ DETACH_GAP_DEG = 10;
46222
+ DETACH_AREA_FRAC = 0.25;
45630
46223
  }
45631
46224
  });
45632
46225
 
@@ -45644,6 +46237,12 @@ function looksUS(lat, lon) {
45644
46237
  if (lat < 15 || lat > 72) return false;
45645
46238
  return lon >= -180 && lon <= -64 || lon >= 172;
45646
46239
  }
46240
+ function looksNorthAmericaNeighbor(lat, lon) {
46241
+ return lat >= 14 && lat <= 72 && lon >= -141 && lon <= -52;
46242
+ }
46243
+ function isWholeSphere(bb) {
46244
+ return bb[0][0] <= -179 && bb[1][0] >= 179 && bb[0][1] <= -89 && bb[1][1] >= 89;
46245
+ }
45647
46246
  function resolveMap(parsed, data) {
45648
46247
  const diagnostics = [...parsed.diagnostics];
45649
46248
  const err = (line12, message, code) => {
@@ -45654,9 +46253,6 @@ function resolveMap(parsed, data) {
45654
46253
  };
45655
46254
  const result = {
45656
46255
  title: parsed.title,
45657
- ...parsed.directives.subtitle !== void 0 && {
45658
- subtitle: parsed.directives.subtitle
45659
- },
45660
46256
  ...parsed.directives.caption !== void 0 && {
45661
46257
  caption: parsed.directives.caption
45662
46258
  },
@@ -45666,7 +46262,7 @@ function resolveMap(parsed, data) {
45666
46262
  // renderer's job (step 4) — the resolver only carries `tags` + `tagGroups`
45667
46263
  // through; it never resolves a tag value to a palette color (#10).
45668
46264
  directives: { ...parsed.directives },
45669
- basemaps: { world: "coarse", subdivisions: [] },
46265
+ basemaps: { world: "detail", subdivisions: [] },
45670
46266
  regions: [],
45671
46267
  pois: [],
45672
46268
  edges: [],
@@ -45675,7 +46271,8 @@ function resolveMap(parsed, data) {
45675
46271
  [-180, -85],
45676
46272
  [180, 85]
45677
46273
  ],
45678
- projection: "natural-earth",
46274
+ projection: "equirectangular",
46275
+ poiFrameContainers: [],
45679
46276
  diagnostics,
45680
46277
  error: parsed.error
45681
46278
  };
@@ -45685,7 +46282,10 @@ function resolveMap(parsed, data) {
45685
46282
  ...[...countryIndex.values()].map((v) => v.name),
45686
46283
  ...[...usStateIndex.values()].map((v) => v.name)
45687
46284
  ];
45688
- const usScoped = parsed.directives.region === "us-states" || parsed.directives.defaultCountry?.toUpperCase() === "US" || parsed.regions.some((r) => {
46285
+ const localeRaw = parsed.directives.locale?.toUpperCase();
46286
+ const localeCountry = localeRaw ? localeRaw.split("-")[0] : void 0;
46287
+ const localeSubdivision = localeRaw && /^[A-Z]{2}-/.test(localeRaw) ? localeRaw : void 0;
46288
+ const usScoped = localeCountry === "US" || parsed.regions.some((r) => {
45689
46289
  const f = fold(r.name);
45690
46290
  return usStateIndex.has(f) && !countryIndex.has(f);
45691
46291
  }) || parsed.regions.some(
@@ -45730,12 +46330,12 @@ function resolveMap(parsed, data) {
45730
46330
  chosen = { ...inState, layer: "us-state" };
45731
46331
  } else {
45732
46332
  chosen = { ...inCountry, layer: "country" };
46333
+ warn(
46334
+ r.lineNumber,
46335
+ `"${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}").`,
46336
+ "W_MAP_REGION_AMBIGUOUS"
46337
+ );
45733
46338
  }
45734
- warn(
45735
- r.lineNumber,
45736
- `"${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}").`,
45737
- "W_MAP_REGION_AMBIGUOUS"
45738
- );
45739
46339
  } else if (inState) {
45740
46340
  chosen = { ...inState, layer: "us-state" };
45741
46341
  } else if (inCountry) {
@@ -45759,6 +46359,7 @@ function resolveMap(parsed, data) {
45759
46359
  name: chosen.name,
45760
46360
  layer: chosen.layer,
45761
46361
  ...r.value !== void 0 && { value: r.value },
46362
+ ...r.color !== void 0 && { color: r.color },
45762
46363
  tags: r.tags,
45763
46364
  meta: r.meta,
45764
46365
  lineNumber: r.lineNumber
@@ -45835,7 +46436,7 @@ function resolveMap(parsed, data) {
45835
46436
  if (!scope)
45836
46437
  warn(
45837
46438
  line12,
45838
- `"${name}" is ambiguous \u2014 resolved to the most-populous match.`,
46439
+ `"${name}" is ambiguous \u2014 resolved to the most-populous match. Set a default with \`locale <ISO>\` (e.g. \`locale US\` / \`locale US-GA\`) to steer it.`,
45839
46440
  "W_MAP_AMBIGUOUS_NAME"
45840
46441
  );
45841
46442
  }
@@ -45848,17 +46449,21 @@ function resolveMap(parsed, data) {
45848
46449
  return fold(pos.name);
45849
46450
  };
45850
46451
  const poiCountries = [];
45851
- let anyNonUsPoi = false;
46452
+ let anyUsPoi = false;
46453
+ let anyNonNaPoi = false;
45852
46454
  const noteCountry = (iso) => {
45853
46455
  if (iso) {
45854
46456
  poiCountries.push(iso);
45855
- if (iso !== "US") anyNonUsPoi = true;
46457
+ if (iso === "US") anyUsPoi = true;
46458
+ if (iso !== "US" && iso !== "CA" && iso !== "MX") anyNonNaPoi = true;
45856
46459
  }
45857
46460
  };
45858
46461
  const deferred = [];
45859
46462
  for (const p of parsed.pois) {
45860
46463
  if (p.pos.kind === "coords") {
45861
- if (!looksUS(p.pos.lat, p.pos.lon)) anyNonUsPoi = true;
46464
+ if (looksUS(p.pos.lat, p.pos.lon)) anyUsPoi = true;
46465
+ else if (!looksNorthAmericaNeighbor(p.pos.lat, p.pos.lon))
46466
+ anyNonNaPoi = true;
45862
46467
  addResolvedPoi(p.pos.lat, p.pos.lon, p);
45863
46468
  continue;
45864
46469
  }
@@ -45876,14 +46481,15 @@ function resolveMap(parsed, data) {
45876
46481
  deferred.push(p);
45877
46482
  }
45878
46483
  }
45879
- const inferredCountry = parsed.directives.defaultCountry?.toUpperCase() ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46484
+ const inferredCountry = localeCountry ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46485
+ const inferredScope = localeSubdivision ?? inferredCountry;
45880
46486
  for (const p of deferred) {
45881
46487
  if (p.pos.kind !== "name") continue;
45882
46488
  const got = lookupName(
45883
46489
  p.pos.name,
45884
46490
  p.pos.scope,
45885
46491
  p.lineNumber,
45886
- inferredCountry,
46492
+ inferredScope,
45887
46493
  true
45888
46494
  );
45889
46495
  if (got.kind === "ok") {
@@ -45900,6 +46506,7 @@ function resolveMap(parsed, data) {
45900
46506
  lat,
45901
46507
  lon,
45902
46508
  ...p.label !== void 0 && { label: p.label },
46509
+ ...p.color !== void 0 && { color: p.color },
45903
46510
  tags: p.tags,
45904
46511
  meta: p.meta,
45905
46512
  lineNumber: p.lineNumber
@@ -45952,7 +46559,8 @@ function resolveMap(parsed, data) {
45952
46559
  const meta = sizeValue !== void 0 ? { value: sizeValue } : {};
45953
46560
  if (pos.kind === "coords") {
45954
46561
  const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
45955
- if (!looksUS(pos.lat, pos.lon)) anyNonUsPoi = true;
46562
+ if (looksUS(pos.lat, pos.lon)) anyUsPoi = true;
46563
+ else if (!looksNorthAmericaNeighbor(pos.lat, pos.lon)) anyNonNaPoi = true;
45956
46564
  if (!registry.has(id)) {
45957
46565
  registerPoi(
45958
46566
  id,
@@ -45975,7 +46583,7 @@ function resolveMap(parsed, data) {
45975
46583
  if (registry.has(f)) return f;
45976
46584
  const aliased = declaredByName.get(f);
45977
46585
  if (aliased) return aliased;
45978
- const got = lookupName(pos.name, pos.scope, line12, inferredCountry, true);
46586
+ const got = lookupName(pos.name, pos.scope, line12, inferredScope, true);
45979
46587
  if (got.kind !== "ok") return null;
45980
46588
  noteCountry(got.iso);
45981
46589
  registerPoi(
@@ -46032,9 +46640,12 @@ function resolveMap(parsed, data) {
46032
46640
  }
46033
46641
  routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
46034
46642
  }
46643
+ const hasUsContent = usSubdivisionReferenced || anyUsPoi || localeCountry === "US";
46644
+ const usOriented = !anyNonNaPoi && !regions.some(
46645
+ (r) => r.layer === "country" && !["US", "CA", "MX"].includes(r.iso)
46646
+ ) && hasUsContent;
46035
46647
  const subdivisions = [];
46036
- if (usSubdivisionReferenced || parsed.directives.region === "us-states")
46037
- subdivisions.push("us-states");
46648
+ if (usSubdivisionReferenced || usOriented) subdivisions.push("us-states");
46038
46649
  const regionBoxes = [];
46039
46650
  for (const ref of referencedRegionIds) {
46040
46651
  const bb = featureBbox(data.usStates, ref.id);
@@ -46042,7 +46653,7 @@ function resolveMap(parsed, data) {
46042
46653
  }
46043
46654
  for (const r of regions) {
46044
46655
  if (r.layer === "country") {
46045
- const bb = featureBbox(data.worldCoarse, r.iso);
46656
+ const bb = featureBboxPrimary(data.worldCoarse, r.iso);
46046
46657
  if (bb) regionBoxes.push(bb);
46047
46658
  }
46048
46659
  }
@@ -46052,23 +46663,56 @@ function resolveMap(parsed, data) {
46052
46663
  [-180, -85],
46053
46664
  [180, 85]
46054
46665
  ];
46055
- let extent2 = unioned ? pad(unioned, PAD_FRACTION) : DEFAULT_EXTENT;
46666
+ const basePad = regions.length > 0 ? REGION_PAD_FRACTION : PAD_FRACTION;
46667
+ let extent2 = unioned ? pad(unioned, basePad) : DEFAULT_EXTENT;
46668
+ const isPoiOnly = pois.length > 0 && regions.length === 0;
46669
+ const containerRegionIds = [];
46670
+ if (isPoiOnly) {
46671
+ const countries = decodeFeatures(data.worldDetail);
46672
+ const states = decodeFeatures(data.usStates);
46673
+ const seen = /* @__PURE__ */ new Set();
46674
+ const containerBoxes = [];
46675
+ for (const p of pois) {
46676
+ const { country, state } = regionAt([p.lon, p.lat], countries, states);
46677
+ const id = state?.iso ?? country?.iso;
46678
+ if (!id || seen.has(id)) continue;
46679
+ seen.add(id);
46680
+ containerRegionIds.push(id);
46681
+ const bb = state ? featureBbox(data.usStates, id) : featureBboxPrimary(data.worldCoarse, id);
46682
+ if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46683
+ }
46684
+ const containerUnion = unionExtent(containerBoxes, points);
46685
+ if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46686
+ }
46687
+ if (isPoiOnly) {
46688
+ const cx = (extent2[0][0] + extent2[1][0]) / 2;
46689
+ const cy = (extent2[0][1] + extent2[1][1]) / 2;
46690
+ const lon = extent2[1][0] - extent2[0][0];
46691
+ const lat = extent2[1][1] - extent2[0][1];
46692
+ const longer = Math.max(lon, lat);
46693
+ if (longer > 0 && longer < POI_ZOOM_FLOOR_DEG) {
46694
+ const k = POI_ZOOM_FLOOR_DEG / longer;
46695
+ const halfLon = lon * k / 2;
46696
+ const halfLat = lat * k / 2;
46697
+ extent2 = [
46698
+ [cx - halfLon, cy - halfLat],
46699
+ [cx + halfLon, cy + halfLat]
46700
+ ];
46701
+ }
46702
+ }
46056
46703
  const lonSpan = extent2[1][0] - extent2[0][0];
46057
46704
  const latSpan = extent2[1][1] - extent2[0][1];
46058
46705
  const span = Math.max(lonSpan, latSpan);
46059
- const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46706
+ const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
46060
46707
  let projection;
46061
- const override = parsed.directives.projection;
46062
- if (override === "equirectangular" || override === "natural-earth" || override === "albers-usa" || override === "mercator") {
46063
- projection = override;
46064
- } else if (usDominant) {
46708
+ if (isPoiOnly && usOriented && lonSpan < US_NATIONAL_LON_SPAN) {
46709
+ projection = "mercator";
46710
+ } else if (usOriented) {
46065
46711
  projection = "albers-usa";
46066
- } else if (span > WORLD_SPAN) {
46712
+ } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
46067
46713
  projection = "equirectangular";
46068
- } else if (span < MERCATOR_MAX_SPAN) {
46069
- projection = "mercator";
46070
46714
  } else {
46071
- projection = "equirectangular";
46715
+ projection = "mercator";
46072
46716
  }
46073
46717
  if (lonSpan >= 180) {
46074
46718
  extent2 = [
@@ -46081,11 +46725,20 @@ function resolveMap(parsed, data) {
46081
46725
  result.edges = edges;
46082
46726
  result.routes = routes;
46083
46727
  result.basemaps = {
46084
- world: span > WORLD_SPAN ? "coarse" : "detail",
46728
+ // Tier is intentionally pinned to detail (50m) at ALL scales. Diagrammo maps
46729
+ // are presentational (palette tints, relief hachures, POI hubs), not
46730
+ // survey-grade — recognizability > generalization: 110m coarse drops the
46731
+ // Italian boot to a stump at world scale. `WORLD_SPAN` lives on only for the
46732
+ // projection decision (the `usOriented`/`span > WORLD_SPAN` chain above); it
46733
+ // no longer gates basemap resolution.
46734
+ // `worldCoarse` is still loaded — it's the authoritative name/bbox index
46735
+ // (featureIndex, featureBboxPrimary), not dead code.
46736
+ world: "detail",
46085
46737
  subdivisions
46086
46738
  };
46087
46739
  result.extent = extent2;
46088
46740
  result.projection = projection;
46741
+ result.poiFrameContainers = containerRegionIds;
46089
46742
  result.error = parsed.error ?? firstError(diagnostics);
46090
46743
  return result;
46091
46744
  }
@@ -46122,17 +46775,20 @@ function firstError(diags) {
46122
46775
  const e = diags.find((d) => d.severity === "error");
46123
46776
  return e ? formatDgmoError(e) : null;
46124
46777
  }
46125
- var WORLD_SPAN, MERCATOR_MAX_SPAN, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46778
+ var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, REGION_PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, POI_ZOOM_FLOOR_DEG, US_NATIONAL_LON_SPAN, REGION_ALIASES, US_STATE_POSTAL;
46126
46779
  var init_resolver2 = __esm({
46127
46780
  "src/map/resolver.ts"() {
46128
46781
  "use strict";
46129
46782
  init_diagnostics();
46130
46783
  init_geo();
46131
46784
  WORLD_SPAN = 90;
46132
- MERCATOR_MAX_SPAN = 25;
46785
+ MERCATOR_MAX_LAT = 80;
46133
46786
  PAD_FRACTION = 0.05;
46787
+ REGION_PAD_FRACTION = 0.12;
46134
46788
  WORLD_LAT_SOUTH = -58;
46135
46789
  WORLD_LAT_NORTH = 78;
46790
+ POI_ZOOM_FLOOR_DEG = 7;
46791
+ US_NATIONAL_LON_SPAN = 48;
46136
46792
  REGION_ALIASES = {
46137
46793
  // Common everyday names → the Natural-Earth display name actually shipped.
46138
46794
  "united states": "united states of america",
@@ -46210,111 +46866,269 @@ var init_resolver2 = __esm({
46210
46866
  }
46211
46867
  });
46212
46868
 
46213
- // src/map/load-data.ts
46214
- var load_data_exports = {};
46215
- __export(load_data_exports, {
46216
- loadMapData: () => loadMapData
46869
+ // src/map/colorize.ts
46870
+ function assignColors(isos, adjacency) {
46871
+ const sorted = [...isos].sort();
46872
+ const byIso = /* @__PURE__ */ new Map();
46873
+ let maxIndex = -1;
46874
+ for (const iso of sorted) {
46875
+ const taken = /* @__PURE__ */ new Set();
46876
+ for (const n of adjacency.get(iso) ?? []) {
46877
+ const c = byIso.get(n);
46878
+ if (c !== void 0) taken.add(c);
46879
+ }
46880
+ let h = 0;
46881
+ while (taken.has(h)) h++;
46882
+ byIso.set(iso, h);
46883
+ if (h > maxIndex) maxIndex = h;
46884
+ }
46885
+ return { byIso, huesNeeded: maxIndex + 1 };
46886
+ }
46887
+ var init_colorize = __esm({
46888
+ "src/map/colorize.ts"() {
46889
+ "use strict";
46890
+ }
46217
46891
  });
46218
- async function loadNodeBuiltins() {
46219
- const [{ readFile }, { fileURLToPath }, { dirname, resolve }] = await Promise.all([
46220
- import("fs/promises"),
46221
- import("url"),
46222
- import("path")
46223
- ]);
46224
- return { readFile, fileURLToPath, dirname, resolve };
46225
- }
46226
- async function readJson(nb, dir, name) {
46227
- return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
46228
- }
46229
- async function firstExistingDir(nb, baseDir) {
46230
- for (const rel of CANDIDATE_DIRS) {
46231
- const dir = nb.resolve(baseDir, rel);
46232
- try {
46233
- await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
46234
- return dir;
46235
- } catch {
46892
+
46893
+ // src/map/context-labels.ts
46894
+ function tierBand(maxSpanDeg) {
46895
+ if (maxSpanDeg >= 90) return "world";
46896
+ if (maxSpanDeg >= 20) return "continental";
46897
+ if (maxSpanDeg >= 5) return "regional";
46898
+ return "local";
46899
+ }
46900
+ function labelBudget(width, height, band) {
46901
+ const bandCap = {
46902
+ world: 6,
46903
+ continental: 5,
46904
+ regional: 4,
46905
+ local: 3
46906
+ };
46907
+ const area2 = Math.floor(Math.sqrt(Math.max(0, width * height)) / 150);
46908
+ return Math.max(0, Math.min(area2, bandCap[band]));
46909
+ }
46910
+ function waterEligible(tier, kind, band) {
46911
+ switch (band) {
46912
+ case "world":
46913
+ return tier <= 1 && (kind === "ocean" || kind === "sea");
46914
+ case "continental":
46915
+ return tier <= 2;
46916
+ case "regional":
46917
+ return tier <= 3;
46918
+ case "local":
46919
+ return tier <= 4;
46920
+ }
46921
+ }
46922
+ function insideViewport(p, width, height) {
46923
+ return !!p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && p[0] >= 0 && p[0] <= width && p[1] >= 0 && p[1] <= height;
46924
+ }
46925
+ function labelWidth(text, letterSpacing) {
46926
+ const spacing = letterSpacing > 0 ? Math.max(0, text.length - 1) * letterSpacing : 0;
46927
+ return measureLegendText(text, FONT) + spacing + 2 * PADX;
46928
+ }
46929
+ function wrapLabel2(text, letterSpacing) {
46930
+ const words = text.split(/\s+/).filter(Boolean);
46931
+ if (words.length <= 1) return [text];
46932
+ const maxLines = words.length >= 4 ? 3 : 2;
46933
+ const n = words.length;
46934
+ let best = null;
46935
+ for (let mask = 0; mask < 1 << n - 1; mask++) {
46936
+ const lines = [];
46937
+ let cur = [words[0]];
46938
+ for (let i = 1; i < n; i++) {
46939
+ if (mask & 1 << i - 1) {
46940
+ lines.push(cur.join(" "));
46941
+ cur = [words[i]];
46942
+ } else cur.push(words[i]);
46236
46943
  }
46944
+ lines.push(cur.join(" "));
46945
+ if (lines.length > maxLines) continue;
46946
+ const cost = Math.round(
46947
+ Math.max(...lines.map((l) => labelWidth(l, letterSpacing)))
46948
+ );
46949
+ const head = labelWidth(lines[0], letterSpacing);
46950
+ if (!best || cost < best.cost || cost === best.cost && lines.length < best.lines.length || cost === best.cost && lines.length === best.lines.length && head > best.head)
46951
+ best = { lines, cost, head };
46237
46952
  }
46238
- throw new Error(
46239
- `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
46240
- );
46953
+ return best?.lines ?? [text];
46241
46954
  }
46242
- function validate(data) {
46243
- const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
46244
- if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
46245
- throw new Error("map data assets are malformed (failed shape validation)");
46246
- }
46247
- return data;
46955
+ function rectAround(cx, cy, lines, letterSpacing) {
46956
+ const w = Math.max(...lines.map((l) => labelWidth(l, letterSpacing)));
46957
+ const h = (lines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY;
46958
+ return { x: cx - w / 2, y: cy - h / 2, w, h };
46248
46959
  }
46249
- function moduleBaseDir(nb) {
46250
- try {
46251
- const url = import.meta.url;
46252
- if (url) return nb.dirname(nb.fileURLToPath(url));
46253
- } catch {
46254
- }
46255
- if (typeof __dirname !== "undefined") return __dirname;
46256
- return process.cwd();
46960
+ function rectFits(r, width, height) {
46961
+ return r.x >= 0 && r.y >= 0 && r.x + r.w <= width && r.y + r.h <= height;
46257
46962
  }
46258
- function loadMapData() {
46259
- cache ??= (async () => {
46260
- const nb = await loadNodeBuiltins();
46261
- const dir = await firstExistingDir(nb, moduleBaseDir(nb));
46262
- const [
46263
- worldCoarse,
46264
- worldDetail,
46265
- usStates,
46266
- lakes,
46267
- rivers,
46268
- naLand,
46269
- naLakes,
46270
- gazetteer
46271
- ] = await Promise.all([
46272
- readJson(nb, dir, FILES.worldCoarse),
46273
- readJson(nb, dir, FILES.worldDetail),
46274
- readJson(nb, dir, FILES.usStates),
46275
- // Lakes/rivers/NA assets are optional — older bundles may predate them.
46276
- readJson(nb, dir, FILES.lakes).catch(() => void 0),
46277
- readJson(nb, dir, FILES.rivers).catch(() => void 0),
46278
- readJson(nb, dir, FILES.naLand).catch(() => void 0),
46279
- readJson(nb, dir, FILES.naLakes).catch(() => void 0),
46280
- readJson(nb, dir, FILES.gazetteer)
46281
- ]);
46282
- return validate({
46283
- worldCoarse,
46284
- worldDetail,
46285
- usStates,
46286
- gazetteer,
46287
- ...lakes && { lakes },
46288
- ...rivers && { rivers },
46289
- ...naLand && { naLand },
46290
- ...naLakes && { naLakes }
46963
+ function overlapsPadded(a, b, pad2) {
46964
+ return a.x - pad2 < b.x + b.w && a.x + a.w + pad2 > b.x && a.y - pad2 < b.y + b.h && a.y + a.h + pad2 > b.y;
46965
+ }
46966
+ function placeContextLabels(args) {
46967
+ const {
46968
+ projection,
46969
+ dLonSpan,
46970
+ dLatSpan,
46971
+ width,
46972
+ height,
46973
+ waterBodies,
46974
+ countries,
46975
+ palette,
46976
+ project,
46977
+ collides,
46978
+ overLand
46979
+ } = args;
46980
+ void projection;
46981
+ const band = tierBand(Math.max(dLonSpan, dLatSpan));
46982
+ const budget = labelBudget(width, height, band);
46983
+ if (budget <= 0) return [];
46984
+ const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
46985
+ const countryColor = palette.textMuted;
46986
+ const haloColor = palette.bg;
46987
+ const candidates = [];
46988
+ const center = [width / 2, height / 2];
46989
+ for (const e of waterBodies?.entries ?? []) {
46990
+ const [lat, lon, name, tier, kind, alt] = e;
46991
+ if (!waterEligible(tier, kind, band)) continue;
46992
+ const wlines = wrapLabel2(name, WATER_LETTER_SPACING);
46993
+ const anchorsLngLat = [[lon, lat]];
46994
+ for (const a of alt ?? []) anchorsLngLat.push([a[1], a[0]]);
46995
+ let best = null;
46996
+ let bestD = Infinity;
46997
+ let nearestProj = null;
46998
+ let nearestProjD = Infinity;
46999
+ for (const [aLon, aLat] of anchorsLngLat) {
47000
+ const p = project(aLon, aLat);
47001
+ if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
47002
+ const d = (p[0] - center[0]) ** 2 + (p[1] - center[1]) ** 2;
47003
+ if (d < nearestProjD) {
47004
+ nearestProjD = d;
47005
+ nearestProj = p;
47006
+ }
47007
+ if (!insideViewport(p, width, height)) continue;
47008
+ if (d < bestD) {
47009
+ bestD = d;
47010
+ best = p;
47011
+ }
47012
+ }
47013
+ if (!best && tier === 0 && nearestProj) {
47014
+ const overX = Math.max(0, -nearestProj[0], nearestProj[0] - width);
47015
+ const overY = Math.max(0, -nearestProj[1], nearestProj[1] - height);
47016
+ if (overX <= width * EDGE_CLAMP_OVERSHOOT && overY <= height * EDGE_CLAMP_OVERSHOOT) {
47017
+ const halfW = Math.max(...wlines.map((l) => labelWidth(l, WATER_LETTER_SPACING))) / 2;
47018
+ const halfH = ((wlines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY) / 2;
47019
+ const m = EDGE_CLAMP_MARGIN;
47020
+ best = [
47021
+ Math.min(Math.max(nearestProj[0], halfW + m), width - halfW - m),
47022
+ Math.min(Math.max(nearestProj[1], halfH + m), height - halfH - m)
47023
+ ];
47024
+ }
47025
+ }
47026
+ if (!best) continue;
47027
+ candidates.push({
47028
+ text: name,
47029
+ lines: wlines,
47030
+ cx: best[0],
47031
+ cy: best[1],
47032
+ italic: true,
47033
+ letterSpacing: WATER_LETTER_SPACING,
47034
+ color: waterColor,
47035
+ // Water before any country (×1000), then by tier, then kind, then name.
47036
+ sort: tier * 10 + KIND_ORDER[kind]
46291
47037
  });
46292
- })().catch((e) => {
46293
- cache = void 0;
46294
- throw e;
46295
- });
46296
- return cache;
47038
+ }
47039
+ const ranked = countries.map((c) => {
47040
+ const [x0, y0, x1, y1] = c.bbox;
47041
+ const w = x1 - x0;
47042
+ const h = y1 - y0;
47043
+ return { c, w, h, area: w * h };
47044
+ }).filter((r) => Number.isFinite(r.area) && r.area > 0).sort((a, b) => b.area - a.area);
47045
+ let ci = 0;
47046
+ for (const r of ranked) {
47047
+ const { c, w, h } = r;
47048
+ if (w > width * 0.66 || h > height * 0.66) continue;
47049
+ if (!insideViewport(c.anchor, width, height)) continue;
47050
+ const text = c.name;
47051
+ const tw = labelWidth(text, 0);
47052
+ if (tw > w || FONT + 2 * PADY > h) continue;
47053
+ candidates.push({
47054
+ text,
47055
+ lines: [text],
47056
+ cx: c.anchor[0],
47057
+ cy: c.anchor[1],
47058
+ italic: false,
47059
+ letterSpacing: 0,
47060
+ color: countryColor,
47061
+ // Always after every water body (+1e6); larger area = earlier.
47062
+ sort: 1e6 + ci++
47063
+ });
47064
+ }
47065
+ candidates.sort((a, b) => a.sort - b.sort);
47066
+ const placed = [];
47067
+ const placedRects = [];
47068
+ for (const cand of candidates) {
47069
+ if (placed.length >= budget) break;
47070
+ const rect = rectAround(cand.cx, cand.cy, cand.lines, cand.letterSpacing);
47071
+ if (!rectFits(rect, width, height)) continue;
47072
+ if (cand.italic && overLand) {
47073
+ const inset = 2;
47074
+ const top = cand.cy - (cand.lines.length - 1) / 2 * LINE_HEIGHT;
47075
+ const touchesLand = cand.lines.some((line12, li) => {
47076
+ const lw = labelWidth(line12, cand.letterSpacing);
47077
+ const x0 = cand.cx - lw / 2 + inset;
47078
+ const x1 = cand.cx + lw / 2 - inset;
47079
+ const xs = [x0, (x0 + cand.cx) / 2, cand.cx, (cand.cx + x1) / 2, x1];
47080
+ const base = top + li * LINE_HEIGHT;
47081
+ return [base, base - FONT * 0.4, base - FONT * 0.8].some(
47082
+ (y) => xs.some((x) => overLand(x, y))
47083
+ );
47084
+ });
47085
+ if (touchesLand) continue;
47086
+ }
47087
+ if (collides(rect)) continue;
47088
+ if (placedRects.some((r) => overlapsPadded(rect, r, CONTEXT_PAD))) continue;
47089
+ placedRects.push(rect);
47090
+ placed.push({
47091
+ x: cand.cx,
47092
+ y: cand.cy,
47093
+ text: cand.text,
47094
+ anchor: "middle",
47095
+ color: cand.color,
47096
+ // No halo: the bg-coloured outline reads as a ghost box behind the text
47097
+ // over the tinted water/land. Context labels are muted enough to sit
47098
+ // cleanly on the basemap without one.
47099
+ halo: false,
47100
+ haloColor,
47101
+ italic: cand.italic,
47102
+ letterSpacing: cand.letterSpacing,
47103
+ ...cand.lines.length > 1 ? { lines: cand.lines } : {},
47104
+ lineNumber: 0
47105
+ });
47106
+ }
47107
+ return placed;
46297
47108
  }
46298
- var FILES, CANDIDATE_DIRS, cache;
46299
- var init_load_data = __esm({
46300
- "src/map/load-data.ts"() {
47109
+ var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, KIND_ORDER;
47110
+ var init_context_labels = __esm({
47111
+ "src/map/context-labels.ts"() {
46301
47112
  "use strict";
46302
- FILES = {
46303
- worldCoarse: "world-coarse.json",
46304
- worldDetail: "world-detail.json",
46305
- usStates: "us-states.json",
46306
- lakes: "lakes.json",
46307
- rivers: "rivers.json",
46308
- naLand: "na-land.json",
46309
- naLakes: "na-lakes.json",
46310
- gazetteer: "gazetteer.json"
47113
+ init_color_utils();
47114
+ init_legend_constants();
47115
+ FONT = 11;
47116
+ LINE_HEIGHT = FONT + 2;
47117
+ PADX = 4;
47118
+ PADY = 3;
47119
+ WATER_LETTER_SPACING = 1.5;
47120
+ CONTEXT_PAD = 4;
47121
+ EDGE_CLAMP_MARGIN = 8;
47122
+ EDGE_CLAMP_OVERSHOOT = 0.35;
47123
+ KIND_ORDER = {
47124
+ ocean: 0,
47125
+ sea: 1,
47126
+ gulf: 2,
47127
+ bay: 3,
47128
+ strait: 4,
47129
+ channel: 5,
47130
+ sound: 6
46311
47131
  };
46312
- CANDIDATE_DIRS = [
46313
- "./data",
46314
- "./map-data",
46315
- "../map-data",
46316
- "../src/map/data"
46317
- ];
46318
47132
  }
46319
47133
  });
46320
47134
 
@@ -46322,6 +47136,7 @@ var init_load_data = __esm({
46322
47136
  import {
46323
47137
  geoPath,
46324
47138
  geoNaturalEarth1,
47139
+ geoEqualEarth,
46325
47140
  geoEquirectangular,
46326
47141
  geoConicEqualArea,
46327
47142
  geoMercator,
@@ -46333,12 +47148,34 @@ function geomObject2(topo) {
46333
47148
  const key = Object.keys(topo.objects)[0];
46334
47149
  return topo.objects[key];
46335
47150
  }
47151
+ function mergeFeatures(a, b) {
47152
+ const polysOf = (f) => {
47153
+ const g = f.geometry;
47154
+ if (!g) return null;
47155
+ if (g.type === "Polygon") return [g.coordinates];
47156
+ if (g.type === "MultiPolygon") return g.coordinates;
47157
+ return null;
47158
+ };
47159
+ const pa = polysOf(a);
47160
+ const pb = polysOf(b);
47161
+ if (!pa || !pb) return a;
47162
+ return {
47163
+ ...a,
47164
+ geometry: { type: "MultiPolygon", coordinates: [...pa, ...pb] }
47165
+ };
47166
+ }
46336
47167
  function decodeLayer(topo) {
47168
+ const cached = decodeCache.get(topo);
47169
+ if (cached) return cached;
46337
47170
  const out = /* @__PURE__ */ new Map();
46338
47171
  for (const g of geomObject2(topo).geometries) {
46339
47172
  const f = feature2(topo, g);
46340
- out.set(g.id, { ...f, id: g.id });
47173
+ if (!f.geometry) continue;
47174
+ const tagged = { ...f, id: g.id };
47175
+ const existing = out.get(g.id);
47176
+ out.set(g.id, existing ? mergeFeatures(existing, tagged) : tagged);
46341
47177
  }
47178
+ decodeCache.set(topo, out);
46342
47179
  return out;
46343
47180
  }
46344
47181
  function projectionFor(family) {
@@ -46347,38 +47184,35 @@ function projectionFor(family) {
46347
47184
  return usConusProjection();
46348
47185
  case "mercator":
46349
47186
  return geoMercator();
47187
+ case "equal-earth":
47188
+ return geoEqualEarth();
47189
+ case "equirectangular":
47190
+ return geoEquirectangular();
46350
47191
  case "natural-earth":
46351
47192
  return geoNaturalEarth1();
46352
- case "equirectangular":
46353
47193
  default:
46354
47194
  return geoEquirectangular();
46355
47195
  }
46356
47196
  }
46357
- function mapBackgroundColor(palette, isDark = false, dataActive = false) {
46358
- if (dataActive)
46359
- return mix(
46360
- palette.colors.gray,
46361
- palette.bg,
46362
- isDark ? MUTED_WATER_DARK : MUTED_WATER_LIGHT
46363
- );
46364
- return mix(palette.colors.blue, palette.bg, WATER_TINT);
47197
+ function mapBackgroundColor(palette, isDark = false, _dataActive = false) {
47198
+ return mix(
47199
+ palette.colors.blue,
47200
+ palette.bg,
47201
+ isDark ? WATER_TINT_DARK : WATER_TINT_LIGHT
47202
+ );
46365
47203
  }
46366
- function mapNeutralLandColor(palette, isDark, dataActive = false) {
46367
- if (dataActive)
46368
- return isDark ? mix(palette.colors.gray, palette.bg, MUTED_LAND_DARK) : palette.bg;
47204
+ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46369
47205
  return mix(
46370
47206
  palette.colors.green,
46371
47207
  palette.bg,
46372
47208
  isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT
46373
47209
  );
46374
47210
  }
46375
- function layoutMap(resolved, data, size, opts) {
46376
- const { palette, isDark } = opts;
46377
- const { width, height } = size;
47211
+ function buildMapProjection(resolved, data) {
46378
47212
  const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
46379
- const usCrisp = resolved.projection === "albers-usa" && wantsUsStates && !!data.naLand;
47213
+ const usCrisp = (resolved.projection === "albers-usa" || resolved.projection === "mercator") && wantsUsStates && !!data.naLand;
46380
47214
  const worldTopo = usCrisp ? data.worldDetail : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46381
- const worldLayer = decodeLayer(worldTopo);
47215
+ const worldLayer = new Map(decodeLayer(worldTopo));
46382
47216
  if (usCrisp && data.naLand) {
46383
47217
  const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
46384
47218
  const crisp = decodeLayer(data.naLand);
@@ -46387,17 +47221,110 @@ function layoutMap(resolved, data, size, opts) {
46387
47221
  if (!base) continue;
46388
47222
  const [[bw, bs], [be, bn]] = geoBounds2(base);
46389
47223
  if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
46390
- worldLayer.set(iso, cf);
47224
+ worldLayer.set(iso, { ...cf, properties: base.properties });
46391
47225
  }
46392
47226
  }
46393
47227
  const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
47228
+ const extentOutline = () => {
47229
+ const [[w, s], [e, n]] = resolved.extent;
47230
+ const N = 16;
47231
+ const coords = [];
47232
+ for (let i = 0; i <= N; i++) {
47233
+ const t = i / N;
47234
+ const lon = w + (e - w) * t;
47235
+ const lat = s + (n - s) * t;
47236
+ coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
47237
+ }
47238
+ return {
47239
+ type: "Feature",
47240
+ properties: {},
47241
+ geometry: { type: "MultiPoint", coordinates: coords }
47242
+ };
47243
+ };
47244
+ let fitFeatures;
47245
+ if (resolved.projection === "albers-usa" && usLayer) {
47246
+ fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
47247
+ const neighborPoints = resolved.pois.filter((p) => !inAlaska(p.lon, p.lat) && !inHawaii(p.lon, p.lat)).map((p) => [p.lon, p.lat]);
47248
+ if (neighborPoints.length > 0) {
47249
+ fitFeatures.push({
47250
+ type: "Feature",
47251
+ properties: {},
47252
+ geometry: { type: "MultiPoint", coordinates: neighborPoints }
47253
+ });
47254
+ }
47255
+ for (const r of resolved.regions) {
47256
+ if (r.layer === "country" && (r.iso === "CA" || r.iso === "MX")) {
47257
+ const cf = worldLayer.get(r.iso);
47258
+ if (cf) fitFeatures.push(cf);
47259
+ }
47260
+ }
47261
+ } else {
47262
+ fitFeatures = [extentOutline()];
47263
+ }
47264
+ const fitTarget = { type: "FeatureCollection", features: fitFeatures };
47265
+ const projection = projectionFor(resolved.projection);
47266
+ if (resolved.projection !== "albers-usa") {
47267
+ let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
47268
+ if (centerLon > 180) centerLon -= 360;
47269
+ projection.rotate([-centerLon, 0]);
47270
+ }
47271
+ const fitGB = geoBounds2(fitTarget);
47272
+ const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
47273
+ return {
47274
+ projection,
47275
+ fitTarget,
47276
+ fitIsGlobal,
47277
+ worldLayer,
47278
+ usLayer,
47279
+ usCrisp,
47280
+ wantsUsStates,
47281
+ worldTopo
47282
+ };
47283
+ }
47284
+ function parsePathRings(d) {
47285
+ const rings = [];
47286
+ let cur = [];
47287
+ const re = /([MLZ])([^MLZ]*)/g;
47288
+ let m;
47289
+ while (m = re.exec(d)) {
47290
+ if (m[1] === "Z") {
47291
+ if (cur.length) rings.push(cur);
47292
+ cur = [];
47293
+ continue;
47294
+ }
47295
+ if (m[1] === "M" && cur.length) {
47296
+ rings.push(cur);
47297
+ cur = [];
47298
+ }
47299
+ const nums = m[2].split(/[ ,]+/).map(Number);
47300
+ for (let i = 0; i + 1 < nums.length; i += 2) {
47301
+ const x = nums[i];
47302
+ const y = nums[i + 1];
47303
+ if (Number.isFinite(x) && Number.isFinite(y)) cur.push([x, y]);
47304
+ }
47305
+ }
47306
+ if (cur.length) rings.push(cur);
47307
+ return rings;
47308
+ }
47309
+ function layoutMap(resolved, data, size, opts) {
47310
+ const { palette, isDark } = opts;
47311
+ const { width, height } = size;
47312
+ const {
47313
+ projection,
47314
+ fitTarget,
47315
+ fitIsGlobal,
47316
+ worldLayer,
47317
+ usLayer,
47318
+ usCrisp,
47319
+ worldTopo
47320
+ } = buildMapProjection(resolved, data);
46394
47321
  const usContext = usLayer !== null;
46395
47322
  const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46396
47323
  const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
46397
- const scaleOverride = resolved.directives.scale;
46398
- const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46399
- const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
46400
- const rampHue = palette.colors.red;
47324
+ const allNonNegative = values.length > 0 && values.every((v) => v >= 0);
47325
+ const rampMin = allNonNegative ? 0 : Math.min(...values);
47326
+ const rampMax = Math.max(...values);
47327
+ const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46401
47328
  const hasRamp = values.length > 0;
46402
47329
  const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
46403
47330
  const matchColorGroup = (v) => {
@@ -46417,14 +47344,48 @@ function layoutMap(resolved, data, size, opts) {
46417
47344
  activeGroup = VALUE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46418
47345
  }
46419
47346
  const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
46420
- const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
47347
+ const mutedBasemap = activeGroup !== null;
46421
47348
  const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46422
47349
  const water = mapBackgroundColor(palette, isDark, mutedBasemap);
47350
+ const lakeStroke = mix(regionStroke, water, 45);
46423
47351
  const foreignFill = mix(
46424
47352
  palette.colors.gray,
46425
47353
  palette.bg,
46426
47354
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46427
47355
  );
47356
+ const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && resolved.tagGroups.length === 0;
47357
+ const colorByIso = /* @__PURE__ */ new Map();
47358
+ if (colorizeActive) {
47359
+ const adjacency = /* @__PURE__ */ new Map();
47360
+ const addEdges = (src) => {
47361
+ for (const [iso, ns] of src) {
47362
+ const cur = adjacency.get(iso);
47363
+ if (cur) cur.push(...ns);
47364
+ else adjacency.set(iso, [...ns]);
47365
+ }
47366
+ };
47367
+ addEdges(buildAdjacency(worldTopo));
47368
+ if (usLayer) {
47369
+ addEdges(buildAdjacency(data.usStates));
47370
+ for (const [country, states] of Object.entries(FOREIGN_BORDER)) {
47371
+ const cn = adjacency.get(country);
47372
+ if (!cn) continue;
47373
+ for (const st of states) {
47374
+ const sn = adjacency.get(st);
47375
+ if (!sn) continue;
47376
+ cn.push(st);
47377
+ sn.push(country);
47378
+ }
47379
+ }
47380
+ }
47381
+ const { byIso, huesNeeded } = assignColors(
47382
+ [...adjacency.keys()],
47383
+ adjacency
47384
+ );
47385
+ const tints = politicalTints(palette, huesNeeded, isDark);
47386
+ for (const [iso, idx] of byIso) colorByIso.set(iso, tints[idx]);
47387
+ }
47388
+ const colorizeStroke = (fill2) => mix(fill2, palette.text, 35);
46428
47389
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46429
47390
  const fillForValue = (s) => {
46430
47391
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
@@ -46449,47 +47410,26 @@ function layoutMap(resolved, data, size, opts) {
46449
47410
  isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
46450
47411
  );
46451
47412
  };
47413
+ const directFill = (name) => {
47414
+ const hex = name ? resolveColor(name, palette) : null;
47415
+ if (!hex) return null;
47416
+ return mix(hex, palette.bg, isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT);
47417
+ };
46452
47418
  const regionFill = (r) => {
47419
+ const direct = directFill(r.color);
47420
+ if (direct) return direct;
46453
47421
  if (activeIsScore) {
46454
47422
  return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46455
47423
  }
47424
+ if (colorizeActive) return (r.iso && colorByIso.get(r.iso)) ?? neutralFill;
46456
47425
  return tagFill(r.tags, activeGroup) ?? neutralFill;
46457
47426
  };
46458
47427
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
46459
- const extentOutline = () => {
46460
- const [[w, s], [e, n]] = resolved.extent;
46461
- const N = 16;
46462
- const coords = [];
46463
- for (let i = 0; i <= N; i++) {
46464
- const t = i / N;
46465
- const lon = w + (e - w) * t;
46466
- const lat = s + (n - s) * t;
46467
- coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
46468
- }
46469
- return {
46470
- type: "Feature",
46471
- properties: {},
46472
- geometry: { type: "MultiPoint", coordinates: coords }
46473
- };
46474
- };
46475
- let fitFeatures;
46476
- if (resolved.projection === "albers-usa" && usLayer) {
46477
- fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
46478
- } else {
46479
- fitFeatures = [extentOutline()];
46480
- }
46481
- const fitTarget = { type: "FeatureCollection", features: fitFeatures };
46482
- const projection = projectionFor(resolved.projection);
46483
- if (resolved.projection !== "albers-usa") {
46484
- let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
46485
- if (centerLon > 180) centerLon -= 360;
46486
- projection.rotate([-centerLon, 0]);
46487
- }
46488
- const TITLE_GAP = 16;
47428
+ const TITLE_GAP2 = 16;
46489
47429
  let topPad = FIT_PAD;
46490
47430
  if (resolved.title && resolved.pois.length > 0) {
46491
47431
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
46492
- topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
47432
+ topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
46493
47433
  }
46494
47434
  const fitBox = [
46495
47435
  [FIT_PAD, topPad],
@@ -46499,11 +47439,10 @@ function layoutMap(resolved, data, size, opts) {
46499
47439
  ]
46500
47440
  ];
46501
47441
  projection.fitExtent(fitBox, fitTarget);
46502
- const fitGB = geoBounds2(fitTarget);
46503
- const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46504
47442
  let path;
46505
47443
  let project;
46506
- if (fitIsGlobal) {
47444
+ let stretchParams = null;
47445
+ if (fitIsGlobal && !opts.preferContain) {
46507
47446
  const cb = geoPath(projection).bounds(fitTarget);
46508
47447
  const bx0 = cb[0][0];
46509
47448
  const by0 = cb[0][1];
@@ -46513,6 +47452,7 @@ function layoutMap(resolved, data, size, opts) {
46513
47452
  const oy = fitBox[0][1];
46514
47453
  const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46515
47454
  const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47455
+ stretchParams = { sx, sy, ox, oy, bx0, by0 };
46516
47456
  const stretch = (x, y) => [
46517
47457
  ox + (x - bx0) * sx,
46518
47458
  oy + (y - by0) * sy
@@ -46544,7 +47484,9 @@ function layoutMap(resolved, data, size, opts) {
46544
47484
  const insets = [];
46545
47485
  const insetRegions = [];
46546
47486
  const insetLabelSeeds = [];
46547
- if (resolved.projection === "albers-usa" && usLayer) {
47487
+ const akRef = resolved.regions.some((r) => r.iso === "US-AK") || resolved.pois.some((p) => inAlaska(p.lon, p.lat));
47488
+ const hiRef = resolved.regions.some((r) => r.iso === "US-HI") || resolved.pois.some((p) => inHawaii(p.lon, p.lat));
47489
+ if (resolved.projection === "albers-usa" && usLayer && (akRef || hiRef)) {
46548
47490
  const PAD = 8;
46549
47491
  const GAP = 12;
46550
47492
  const yB = height - FIT_PAD;
@@ -46575,38 +47517,14 @@ function layoutMap(resolved, data, size, opts) {
46575
47517
  }
46576
47518
  return y;
46577
47519
  };
46578
- const coastTop = (x0, xr) => {
47520
+ const coastFloor = (x0, xr) => {
46579
47521
  const n = 24;
46580
- const pts = [];
46581
47522
  let maxY = -Infinity;
46582
47523
  for (let i = 0; i <= n; i++) {
46583
- const x = x0 + (xr - x0) * i / n;
46584
- const y = at(x);
46585
- if (y > -Infinity) {
46586
- pts.push([x, y]);
46587
- if (y > maxY) maxY = y;
46588
- }
46589
- }
46590
- if (pts.length === 0) return () => yB - height * 0.42;
46591
- let m = 0;
46592
- if (pts.length >= 2) {
46593
- let sx = 0, sy = 0, sxx = 0, sxy = 0;
46594
- for (const [x, y] of pts) {
46595
- sx += x;
46596
- sy += y;
46597
- sxx += x * x;
46598
- sxy += x * y;
46599
- }
46600
- const den = pts.length * sxx - sx * sx;
46601
- if (den !== 0) m = (pts.length * sxy - sx * sy) / den;
46602
- }
46603
- m = Math.max(-0.35, Math.min(0.35, m));
46604
- let c = -Infinity;
46605
- for (const [x, y] of pts) {
46606
- const need = y - m * x + GAP;
46607
- if (need > c) c = need;
46608
- }
46609
- return (x) => m * x + c;
47524
+ const y = at(x0 + (xr - x0) * i / n);
47525
+ if (y > maxY) maxY = y;
47526
+ }
47527
+ return maxY;
46610
47528
  };
46611
47529
  const placeInset = (iso, proj, boxX, iwReq) => {
46612
47530
  const f = usLayer.get(iso);
@@ -46615,19 +47533,15 @@ function layoutMap(resolved, data, size, opts) {
46615
47533
  const iw = Math.min(iwReq, width - FIT_PAD - x0 - 2 * PAD);
46616
47534
  if (iw < 24) return boxX;
46617
47535
  const xr = x0 + iw + 2 * PAD;
46618
- const top = coastTop(x0, xr);
46619
- const yL = top(x0);
46620
- const yR = top(xr);
47536
+ const floor = coastFloor(x0, xr);
47537
+ const topGuess = floor > -Infinity ? floor + GAP : yB - height * 0.42;
46621
47538
  proj.fitWidth(iw, f);
46622
47539
  const bb = geoPath(proj).bounds(f);
46623
47540
  const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
46624
47541
  const needH = sh + 2 * PAD;
46625
- let topFit = Math.max(yL, yR);
47542
+ let topFit = topGuess;
46626
47543
  const bottom = Math.min(topFit + needH, yB);
46627
47544
  if (bottom - topFit < needH) topFit = bottom - needH;
46628
- const lift = topFit - Math.max(yL, yR);
46629
- const topL = yL + lift;
46630
- const topR = yR + lift;
46631
47545
  proj.fitExtent(
46632
47546
  [
46633
47547
  [x0 + PAD, topFit + PAD],
@@ -46637,8 +47551,18 @@ function layoutMap(resolved, data, size, opts) {
46637
47551
  );
46638
47552
  const d = geoPath(proj)(f) ?? "";
46639
47553
  if (!d) return xr;
47554
+ let contextLand;
47555
+ if (iso === "US-AK") {
47556
+ const can = worldLayer.get("CA");
47557
+ const cd = can ? geoPath(proj)(can) ?? "" : "";
47558
+ if (cd)
47559
+ contextLand = {
47560
+ d: cd,
47561
+ fill: colorizeActive ? colorByIso.get("CA") ?? foreignFill : foreignFill
47562
+ };
47563
+ }
46640
47564
  const r = regionById.get(iso);
46641
- let fill2 = neutralFill;
47565
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? neutralFill : neutralFill;
46642
47566
  let lineNumber = -1;
46643
47567
  if (r?.layer === "us-state") {
46644
47568
  fill2 = regionFill(r);
@@ -46646,21 +47570,25 @@ function layoutMap(resolved, data, size, opts) {
46646
47570
  }
46647
47571
  insets.push({
46648
47572
  x: x0,
46649
- y: Math.min(topL, topR),
47573
+ y: topFit,
46650
47574
  w: xr - x0,
46651
- h: bottom - Math.min(topL, topR),
47575
+ h: bottom - topFit,
46652
47576
  points: [
46653
- [x0, topL],
46654
- [xr, topR],
47577
+ [x0, topFit],
47578
+ [xr, topFit],
46655
47579
  [xr, bottom],
46656
47580
  [x0, bottom]
46657
- ]
47581
+ ],
47582
+ // The FITTED inset projection (just fit to this box) — captured so the
47583
+ // geo-query can invert pixels inside the frame back to AK/HI coords.
47584
+ projection: proj,
47585
+ ...contextLand && { contextLand }
46658
47586
  });
46659
47587
  insetRegions.push({
46660
47588
  id: iso,
46661
47589
  d,
46662
47590
  fill: fill2,
46663
- stroke: regionStroke,
47591
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46664
47592
  lineNumber,
46665
47593
  layer: "us-state",
46666
47594
  ...r?.value !== void 0 && { value: r.value },
@@ -46673,13 +47601,16 @@ function layoutMap(resolved, data, size, opts) {
46673
47601
  }
46674
47602
  return xr;
46675
47603
  };
46676
- const akRight = placeInset(
46677
- "US-AK",
46678
- alaskaProjection(),
46679
- FIT_PAD,
46680
- width * 0.15
46681
- );
46682
- placeInset("US-HI", hawaiiProjection(), akRight + 24, width * 0.1);
47604
+ let akRight = FIT_PAD;
47605
+ if (akRef)
47606
+ akRight = placeInset("US-AK", alaskaProjection(), FIT_PAD, width * 0.15);
47607
+ if (hiRef)
47608
+ placeInset(
47609
+ "US-HI",
47610
+ hawaiiProjection(),
47611
+ akRef ? akRight + 24 : FIT_PAD,
47612
+ width * 0.1
47613
+ );
46683
47614
  }
46684
47615
  const conusFit = resolved.projection === "albers-usa" && !!usLayer;
46685
47616
  const classifyExtent = conusFit ? geoBounds2(fitTarget) : resolved.extent;
@@ -46695,15 +47626,24 @@ function layoutMap(resolved, data, size, opts) {
46695
47626
  };
46696
47627
  const ringOverlapsView = (ring) => {
46697
47628
  let loMin = Infinity, loMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
47629
+ const lons = [];
46698
47630
  for (const [rawLon] of ring) {
46699
47631
  const lon = normLon(rawLon);
47632
+ lons.push(lon);
46700
47633
  if (lon < loMin) loMin = lon;
46701
47634
  if (lon > loMax) loMax = lon;
46702
47635
  if (rawLon < rawMin) rawMin = rawLon;
46703
47636
  if (rawLon > rawMax) rawMax = rawLon;
46704
47637
  }
46705
- if (loMax - loMin > 270) return false;
46706
- if (rawMax - rawMin > 180 && loMax - loMin < 90) return false;
47638
+ lons.sort((a, b) => a - b);
47639
+ let maxGap = 0;
47640
+ for (let i = 1; i < lons.length; i++)
47641
+ maxGap = Math.max(maxGap, lons[i] - lons[i - 1]);
47642
+ if (lons.length > 1)
47643
+ maxGap = Math.max(maxGap, lons[0] + 360 - lons[lons.length - 1]);
47644
+ const occupiedArc = 360 - maxGap;
47645
+ if (occupiedArc > 270) return false;
47646
+ if (rawMax - rawMin > 180 && occupiedArc < 90) return false;
46707
47647
  let px0 = Infinity, py0 = Infinity, px1 = -Infinity, py1 = -Infinity, anyFinite = false;
46708
47648
  for (const [lon, lat] of ring) {
46709
47649
  const p = project(lon, lat);
@@ -46776,7 +47716,7 @@ function layoutMap(resolved, data, size, opts) {
46776
47716
  const regions = [];
46777
47717
  const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
46778
47718
  for (const [iso, f] of layerFeatures) {
46779
- if (layerKind === "us-state" && usContext && INSET_STATES.has(iso))
47719
+ if (layerKind === "us-state" && usContext && resolved.projection === "albers-usa" && INSET_STATES.has(iso))
46780
47720
  continue;
46781
47721
  if (layerKind === "country" && usContext && iso === "US") continue;
46782
47722
  if (layerKind === "country" && iso === "AQ" && !regionById.has("AQ"))
@@ -46788,7 +47728,8 @@ function layoutMap(resolved, data, size, opts) {
46788
47728
  if (!d) continue;
46789
47729
  const isThisLayer = r?.layer === layerKind;
46790
47730
  const isForeign = layerKind === "country" && usContext && iso !== "US";
46791
- let fill2 = isForeign ? foreignFill : neutralFill;
47731
+ const baseFill = isForeign ? foreignFill : neutralFill;
47732
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? baseFill : baseFill;
46792
47733
  let label;
46793
47734
  let lineNumber = -1;
46794
47735
  let layer = "base";
@@ -46797,12 +47738,14 @@ function layoutMap(resolved, data, size, opts) {
46797
47738
  lineNumber = r.lineNumber;
46798
47739
  layer = layerKind;
46799
47740
  label = r.name;
47741
+ } else {
47742
+ label = f.properties?.name;
46800
47743
  }
46801
47744
  regions.push({
46802
47745
  id: iso,
46803
47746
  d,
46804
47747
  fill: fill2,
46805
- stroke: regionStroke,
47748
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46806
47749
  lineNumber,
46807
47750
  layer,
46808
47751
  ...label !== void 0 && { label },
@@ -46824,13 +47767,88 @@ function layoutMap(resolved, data, size, opts) {
46824
47767
  id: "lake",
46825
47768
  d,
46826
47769
  fill: water,
46827
- stroke: "none",
47770
+ stroke: lakeStroke,
46828
47771
  lineNumber: -1,
46829
47772
  layer: "base"
46830
47773
  });
46831
47774
  }
46832
47775
  }
46833
- const riverColor = water;
47776
+ const pointInRings = (px, py, rings) => {
47777
+ let inside = false;
47778
+ for (const ring of rings) {
47779
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
47780
+ const [xi, yi] = ring[i];
47781
+ const [xj, yj] = ring[j];
47782
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
47783
+ inside = !inside;
47784
+ }
47785
+ }
47786
+ return inside;
47787
+ };
47788
+ const fillHitTargets = [...regions, ...insetRegions].map((r) => ({
47789
+ fill: r.fill,
47790
+ rings: parsePathRings(r.d)
47791
+ }));
47792
+ const fillAt = (x, y) => {
47793
+ let hit = water;
47794
+ for (const t of fillHitTargets)
47795
+ if (pointInRings(x, y, t.rings)) hit = t.fill;
47796
+ return hit;
47797
+ };
47798
+ const labelOnFill = (fill2) => {
47799
+ const color = contrastRatio(fill2, palette.textOnFillDark) >= contrastRatio(fill2, palette.textOnFillLight) ? palette.textOnFillDark : palette.textOnFillLight;
47800
+ const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47801
+ return {
47802
+ color,
47803
+ halo: contrastRatio(fill2, color) < REGION_LABEL_HALO_RATIO,
47804
+ haloColor
47805
+ };
47806
+ };
47807
+ const reliefAllowed = resolved.directives.noRelief !== true;
47808
+ const relief = [];
47809
+ let reliefHatch = null;
47810
+ if (reliefAllowed && data.mountainRanges) {
47811
+ for (const [, f] of decodeLayer(data.mountainRanges)) {
47812
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
47813
+ if (!viewF) continue;
47814
+ const area2 = path.area(viewF);
47815
+ if (!Number.isFinite(area2) || area2 < RELIEF_MIN_AREA) continue;
47816
+ const box = path.bounds(viewF);
47817
+ if (box[1][0] - box[0][0] < RELIEF_MIN_DIM || box[1][1] - box[0][1] < RELIEF_MIN_DIM)
47818
+ continue;
47819
+ const d = path(viewF) ?? "";
47820
+ if (!d) continue;
47821
+ relief.push({ d });
47822
+ }
47823
+ if (relief.length) {
47824
+ const darkTone = isDark ? palette.bg : palette.text;
47825
+ const lightTone = isDark ? palette.text : palette.bg;
47826
+ const reliefLandRef = colorizeActive ? isDark ? palette.surface : palette.bg : neutralFill;
47827
+ const landLum = relativeLuminance(reliefLandRef);
47828
+ const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
47829
+ reliefHatch = {
47830
+ color: mix(tone, reliefLandRef, RELIEF_HATCH_STRENGTH),
47831
+ spacing: RELIEF_HATCH_SPACING,
47832
+ width: RELIEF_HATCH_WIDTH
47833
+ };
47834
+ }
47835
+ }
47836
+ let coastlineStyle = null;
47837
+ if (resolved.directives.noCoastline !== true) {
47838
+ const minDim = Math.min(width, height);
47839
+ coastlineStyle = {
47840
+ color: mix(regionStroke, water, COASTLINE_STROKE_MIX),
47841
+ // N equal-width rings: distance steps outward by COASTLINE_STEP; opacity
47842
+ // fades linearly from NEAR (innermost) to FAR (outermost).
47843
+ lines: Array.from({ length: COASTLINE_RING_COUNT }, (_, k) => ({
47844
+ d: (COASTLINE_D0 + k * COASTLINE_STEP) * minDim,
47845
+ thickness: COASTLINE_THICKNESS * minDim,
47846
+ opacity: COASTLINE_OPACITY_NEAR + (COASTLINE_OPACITY_FAR - COASTLINE_OPACITY_NEAR) * k / (COASTLINE_RING_COUNT - 1)
47847
+ })),
47848
+ minExtent: (isGlobalView ? COASTLINE_MIN_EXTENT_GLOBAL : COASTLINE_MIN_EXTENT) * minDim
47849
+ };
47850
+ }
47851
+ const riverColor = mix(palette.colors.blue, water, 32);
46834
47852
  const rivers = [];
46835
47853
  if (data.rivers) {
46836
47854
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -46851,6 +47869,9 @@ function layoutMap(resolved, data, size, opts) {
46851
47869
  return R_MIN + Math.max(0, Math.min(1, t)) * (R_MAX - R_MIN);
46852
47870
  };
46853
47871
  const poiFill = (p) => {
47872
+ const directHex = p.color ? resolveColor(p.color, palette) : null;
47873
+ if (directHex)
47874
+ return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
46854
47875
  for (const group of resolved.tagGroups) {
46855
47876
  const val = p.tags[group.name.toLowerCase()];
46856
47877
  if (!val) continue;
@@ -46883,38 +47904,108 @@ function layoutMap(resolved, data, size, opts) {
46883
47904
  const xy = project(p.lon, p.lat);
46884
47905
  if (xy) projected.push({ p, xy });
46885
47906
  }
46886
- const coloGroups = /* @__PURE__ */ new Map();
47907
+ const placePoi = (e, cx, cy, clusterId) => {
47908
+ const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
47909
+ poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
47910
+ const num = routeNumberById.get(e.p.id);
47911
+ pois.push({
47912
+ id: e.p.id,
47913
+ cx,
47914
+ cy,
47915
+ r: radiusFor(e.p),
47916
+ fill: fill2,
47917
+ stroke: stroke2,
47918
+ lineNumber: e.p.lineNumber,
47919
+ implicit: !!e.p.implicit,
47920
+ isOrigin: originIds.has(e.p.id),
47921
+ ...num !== void 0 && { routeNumber: num },
47922
+ ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags },
47923
+ ...clusterId !== void 0 && { clusterId }
47924
+ });
47925
+ };
47926
+ const clusters = [];
47927
+ const connected = /* @__PURE__ */ new Set();
47928
+ for (const e of resolved.edges) {
47929
+ connected.add(e.fromId);
47930
+ connected.add(e.toId);
47931
+ }
47932
+ for (const rt of resolved.routes) {
47933
+ rt.stopIds.forEach((id) => connected.add(id));
47934
+ }
47935
+ const radiusOf = (e) => radiusFor(e.p);
46887
47936
  for (const e of projected) {
46888
- const key = `${Math.round(e.xy[0] / COLO_EPS)},${Math.round(e.xy[1] / COLO_EPS)}`;
46889
- const arr = coloGroups.get(key);
46890
- if (arr) arr.push(e);
46891
- else coloGroups.set(key, [e]);
46892
- }
46893
- for (const group of coloGroups.values()) {
46894
- group.forEach((e, i) => {
46895
- let cx = e.xy[0];
46896
- let cy = e.xy[1];
46897
- if (group.length > 1) {
46898
- const ang = i * GOLDEN_ANGLE;
46899
- cx += Math.cos(ang) * COLO_R;
46900
- cy += Math.sin(ang) * COLO_R;
46901
- }
46902
- const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
46903
- poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
46904
- const num = routeNumberById.get(e.p.id);
46905
- pois.push({
46906
- id: e.p.id,
46907
- cx,
46908
- cy,
46909
- r: radiusFor(e.p),
46910
- fill: fill2,
46911
- stroke: stroke2,
46912
- lineNumber: e.p.lineNumber,
46913
- implicit: !!e.p.implicit,
46914
- isOrigin: originIds.has(e.p.id),
46915
- ...num !== void 0 && { routeNumber: num },
46916
- ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags }
46917
- });
47937
+ if (connected.has(e.p.id)) placePoi(e, e.xy[0], e.xy[1]);
47938
+ }
47939
+ const groups = [];
47940
+ for (const e of projected) {
47941
+ if (connected.has(e.p.id)) continue;
47942
+ const r = radiusOf(e);
47943
+ const near = groups.find(
47944
+ (g) => g.some(
47945
+ (q) => Math.hypot(q.xy[0] - e.xy[0], q.xy[1] - e.xy[1]) < (r + radiusOf(q)) * STACK_OVERLAP
47946
+ )
47947
+ );
47948
+ if (near) near.push(e);
47949
+ else groups.push([e]);
47950
+ }
47951
+ for (const g of groups) {
47952
+ if (g.length === 1) {
47953
+ placePoi(g[0], g[0].xy[0], g[0].xy[1]);
47954
+ continue;
47955
+ }
47956
+ const clusterId = g[0].p.id;
47957
+ const cx0 = g.reduce((s, e) => s + e.xy[0], 0) / g.length;
47958
+ const cy0 = g.reduce((s, e) => s + e.xy[1], 0) / g.length;
47959
+ const maxR = Math.max(...g.map(radiusOf));
47960
+ const sep = 2 * maxR + STACK_RING_GAP;
47961
+ const ringR = Math.max(
47962
+ COLO_R,
47963
+ sep / (2 * Math.sin(Math.PI / Math.max(g.length, 2)))
47964
+ );
47965
+ const positions = g.map((e, i) => {
47966
+ if (g.length <= STACK_RING_MAX) {
47967
+ const ang2 = -Math.PI / 2 + i * 2 * Math.PI / g.length;
47968
+ return {
47969
+ e,
47970
+ mx: cx0 + Math.cos(ang2) * ringR,
47971
+ my: cy0 + Math.sin(ang2) * ringR
47972
+ };
47973
+ }
47974
+ const ang = i * GOLDEN_ANGLE;
47975
+ const rr = ringR * Math.sqrt((i + 1) / g.length);
47976
+ return { e, mx: cx0 + Math.cos(ang) * rr, my: cy0 + Math.sin(ang) * rr };
47977
+ });
47978
+ let minX = cx0 - maxR;
47979
+ let maxX = cx0 + maxR;
47980
+ let minY = cy0 - maxR;
47981
+ let maxY = cy0 + maxR;
47982
+ for (const { mx, my, e } of positions) {
47983
+ const r = radiusOf(e);
47984
+ minX = Math.min(minX, mx - r);
47985
+ maxX = Math.max(maxX, mx + r);
47986
+ minY = Math.min(minY, my - r);
47987
+ maxY = Math.max(maxY, my + r);
47988
+ }
47989
+ let dx = 0;
47990
+ let dy = 0;
47991
+ if (minX + dx < 2) dx = 2 - minX;
47992
+ if (maxX + dx > width - 2) dx = width - 2 - maxX;
47993
+ if (minY + dy < 2) dy = 2 - minY;
47994
+ if (maxY + dy > height - 2) dy = height - 2 - maxY;
47995
+ const legsOut = [];
47996
+ for (const { e, mx, my } of positions) {
47997
+ const fx = mx + dx;
47998
+ const fy = my + dy;
47999
+ placePoi(e, fx, fy, clusterId);
48000
+ legsOut.push({ x2: fx, y2: fy, color: poiFill(e.p).fill });
48001
+ }
48002
+ clusters.push({
48003
+ id: clusterId,
48004
+ cx: cx0 + dx,
48005
+ cy: cy0 + dy,
48006
+ count: g.length,
48007
+ hitR: ringR + maxR + 6,
48008
+ legs: legsOut
46918
48009
  });
46919
48010
  }
46920
48011
  const legs = [];
@@ -46964,16 +48055,26 @@ function layoutMap(resolved, data, size, opts) {
46964
48055
  if (!a || !b) continue;
46965
48056
  const mx = (a.cx + b.cx) / 2;
46966
48057
  const my = (a.cy + b.cy) / 2;
48058
+ const bow = {
48059
+ curved: leg.style === "arc",
48060
+ offset: 0,
48061
+ labelX: mx,
48062
+ labelY: my - 4
48063
+ };
48064
+ const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46967
48065
  legs.push({
46968
- d: legPath(a, b, leg.style === "arc", 0),
48066
+ d: legPath(a, b, bow.curved, bow.offset),
46969
48067
  width: routeWidthFor(Number(leg.value)),
46970
48068
  color: mix(palette.text, palette.bg, 72),
46971
48069
  arrow: true,
46972
48070
  lineNumber: leg.lineNumber,
46973
48071
  ...leg.label !== void 0 && {
46974
48072
  label: leg.label,
46975
- labelX: mx,
46976
- labelY: my - 4
48073
+ labelX: bow.labelX,
48074
+ labelY: bow.labelY,
48075
+ labelColor: routeLabelStyle.color,
48076
+ labelHalo: routeLabelStyle.halo,
48077
+ labelHaloColor: routeLabelStyle.haloColor
46977
48078
  }
46978
48079
  });
46979
48080
  }
@@ -47001,20 +48102,29 @@ function layoutMap(resolved, data, size, opts) {
47001
48102
  const a = poiScreen.get(e.fromId);
47002
48103
  const b = poiScreen.get(e.toId);
47003
48104
  if (!a || !b) return;
47004
- const curved = e.style === "arc" || n > 1;
47005
- const offset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
48105
+ const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
47006
48106
  const mx = (a.cx + b.cx) / 2;
47007
48107
  const my = (a.cy + b.cy) / 2;
48108
+ const bow = {
48109
+ curved: e.style === "arc" || n > 1,
48110
+ offset: fanOffset,
48111
+ labelX: mx,
48112
+ labelY: my - 4
48113
+ };
48114
+ const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
47008
48115
  legs.push({
47009
- d: legPath(a, b, curved, offset),
48116
+ d: legPath(a, b, bow.curved, bow.offset),
47010
48117
  width: widthFor(e),
47011
48118
  color: mix(palette.text, palette.bg, 66),
47012
48119
  arrow: e.directed,
47013
48120
  lineNumber: e.lineNumber,
47014
48121
  ...e.label !== void 0 && {
47015
48122
  label: e.label,
47016
- labelX: mx,
47017
- labelY: my - 4
48123
+ labelX: bow.labelX,
48124
+ labelY: bow.labelY,
48125
+ labelColor: edgeLabelStyle.color,
48126
+ labelHalo: edgeLabelStyle.halo,
48127
+ labelHaloColor: edgeLabelStyle.haloColor
47018
48128
  }
47019
48129
  });
47020
48130
  });
@@ -47056,25 +48166,25 @@ function layoutMap(resolved, data, size, opts) {
47056
48166
  }
47057
48167
  }
47058
48168
  const collides = (rect) => markers.some((m) => rectCircleOverlap(rect, m)) || obstacles.some((o) => rectsOverlap(rect, o)) || legSegments.some((s) => segmentRectOverlap(s[0], s[1], s[2], s[3], rect));
47059
- const regionLabelMode = resolved.directives.regionLabels ?? "off";
48169
+ const showRegionLabels = resolved.directives.noRegionLabels !== true;
48170
+ const isCompact = width < COMPACT_WIDTH_PX;
47060
48171
  const LABEL_PADX = 6;
47061
48172
  const LABEL_PADY = 3;
47062
- const labelW = (text) => measureLegendText(text, FONT) + 2 * LABEL_PADX;
47063
- const labelH = FONT + 2 * LABEL_PADY;
48173
+ const labelW = (text) => measureLegendText(text, FONT2) + 2 * LABEL_PADX;
48174
+ const labelH = FONT2 + 2 * LABEL_PADY;
47064
48175
  const pushRegionLabel = (x, y, text, fill2, lineNumber) => {
47065
- const color = contrastText(
47066
- fill2,
47067
- palette.textOnFillLight,
47068
- palette.textOnFillDark
48176
+ const { color, haloColor } = labelOnFill(fill2);
48177
+ const halfW = measureLegendText(text, FONT2) / 2;
48178
+ const overflows = [y - FONT2 * 0.55, y - FONT2 * 0.1].some(
48179
+ (sy) => fillAt(x - halfW, sy) !== fill2 || fillAt(x + halfW, sy) !== fill2
47069
48180
  );
47070
- const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47071
48181
  labels.push({
47072
48182
  x,
47073
48183
  y,
47074
48184
  text,
47075
48185
  anchor: "middle",
47076
48186
  color,
47077
- halo: true,
48187
+ halo: overflows,
47078
48188
  haloColor,
47079
48189
  lineNumber
47080
48190
  });
@@ -47083,21 +48193,50 @@ function layoutMap(resolved, data, size, opts) {
47083
48193
  US: [-98.5, 39.5]
47084
48194
  // CONUS geographic centre (near Lebanon, Kansas)
47085
48195
  };
47086
- if (regionLabelMode === "full" || regionLabelMode === "abbrev") {
47087
- for (const r of regions) {
47088
- if (r.layer === "base" || r.label === void 0) continue;
47089
- const f = r.layer === "us-state" ? usLayer?.get(r.id) : worldLayer.get(r.id);
47090
- if (!f) continue;
48196
+ const REGION_LABEL_GAP = 2;
48197
+ const regionLabelRect = (cx, cy, text) => {
48198
+ const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
48199
+ return { x: cx - w / 2, y: cy - FONT2 / 2, w, h: FONT2 };
48200
+ };
48201
+ if (showRegionLabels) {
48202
+ const frameContainers = new Set(resolved.poiFrameContainers);
48203
+ const entries = regions.map((r) => {
48204
+ const isContainer = frameContainers.has(r.id);
48205
+ if (r.layer === "base" && !isContainer || r.label === void 0)
48206
+ return null;
48207
+ const isUsState = r.layer === "us-state" || r.id.startsWith("US-");
48208
+ const f = isUsState ? usLayer?.get(r.id) : worldLayer.get(r.id);
48209
+ if (!f) return null;
47091
48210
  const [[x0, y0], [x1, y1]] = path.bounds(f);
47092
- const text = regionLabelMode === "abbrev" ? r.id.replace(/^US-/, "") : r.label;
47093
- if (labelW(text) > x1 - x0 || labelH > y1 - y0) continue;
47094
- const anchor = r.layer !== "us-state" ? WORLD_LABEL_ANCHORS[r.id] : void 0;
48211
+ const boxW = x1 - x0;
48212
+ const boxH = y1 - y0;
48213
+ const abbrev = isUsState ? r.id.replace(/^US-/, "") : void 0;
48214
+ const candidates = abbrev !== void 0 ? isCompact ? [abbrev, r.label] : [r.label, abbrev] : [r.label];
48215
+ const anchor = !isUsState ? WORLD_LABEL_ANCHORS[r.id] : void 0;
47095
48216
  const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
47096
- if (!c || !Number.isFinite(c[0])) continue;
48217
+ if (!c || !Number.isFinite(c[0])) return null;
48218
+ return { r, c, boxW, boxH, area: boxW * boxH, candidates };
48219
+ }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
48220
+ const placedRegionRects = [];
48221
+ const POI_LABEL_PAD = 14;
48222
+ const poiObstacles = pois.map((p) => ({
48223
+ x: p.cx - p.r - POI_LABEL_PAD,
48224
+ y: p.cy - p.r - POI_LABEL_PAD,
48225
+ w: 2 * (p.r + POI_LABEL_PAD),
48226
+ h: 2 * (p.r + POI_LABEL_PAD)
48227
+ }));
48228
+ for (const { r, c, boxW, boxH, candidates } of entries) {
48229
+ const text = candidates.find((t) => {
48230
+ if (labelW(t) > boxW || labelH > boxH) return false;
48231
+ const rect = regionLabelRect(c[0], c[1], t);
48232
+ return !placedRegionRects.some((p) => rectsOverlap(rect, p)) && !poiObstacles.some((o) => rectsOverlap(rect, o));
48233
+ });
48234
+ if (text === void 0) continue;
48235
+ placedRegionRects.push(regionLabelRect(c[0], c[1], text));
47097
48236
  pushRegionLabel(c[0], c[1], text, r.fill, r.lineNumber);
47098
48237
  }
47099
48238
  for (const seed of insetLabelSeeds) {
47100
- const text = regionLabelMode === "abbrev" ? seed.iso.replace(/^US-/, "") : seed.name;
48239
+ const text = isCompact ? seed.iso.replace(/^US-/, "") : seed.name;
47101
48240
  const src = regionById.get(seed.iso);
47102
48241
  pushRegionLabel(
47103
48242
  seed.x,
@@ -47108,22 +48247,26 @@ function layoutMap(resolved, data, size, opts) {
47108
48247
  );
47109
48248
  }
47110
48249
  }
47111
- const poiLabelMode = resolved.directives.poiLabels ?? "auto";
47112
- if (poiLabelMode !== "off") {
47113
- const ordered = [...pois].sort(
47114
- (a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1)
47115
- );
48250
+ if (resolved.directives.noPoiLabels !== true) {
48251
+ const ordered = [...pois].filter((p) => p.clusterId === void 0).sort((a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1));
47116
48252
  const poiById = new Map(resolved.pois.map((q) => [q.id, q]));
47117
48253
  const labelText = (p) => {
47118
48254
  const src = poiById.get(p.id);
47119
48255
  return src?.label ?? src?.name ?? p.id;
47120
48256
  };
47121
- const poiLabH = FONT * 1.25;
48257
+ const poiLabH = FONT2 * 1.25;
47122
48258
  const labelInfo = (p) => {
47123
48259
  const text = labelText(p);
47124
- return { text, w: measureLegendText(text, FONT) };
48260
+ return { text, w: measureLegendText(text, FONT2) };
47125
48261
  };
47126
48262
  const GAP = 3;
48263
+ const clusterMembersById = /* @__PURE__ */ new Map();
48264
+ for (const p of pois) {
48265
+ if (p.clusterId === void 0) continue;
48266
+ const arr = clusterMembersById.get(p.clusterId);
48267
+ if (arr) arr.push(p);
48268
+ else clusterMembersById.set(p.clusterId, [p]);
48269
+ }
47127
48270
  const inlineRect = (p, w, side) => {
47128
48271
  switch (side) {
47129
48272
  case "right":
@@ -47153,11 +48296,11 @@ function layoutMap(resolved, data, size, opts) {
47153
48296
  const x = side === "right" ? rect.x : side === "left" ? rect.x + w : p.cx;
47154
48297
  labels.push({
47155
48298
  x,
47156
- y: rect.y + poiLabH / 2 + FONT / 3,
48299
+ y: rect.y + poiLabH / 2 + FONT2 / 3,
47157
48300
  text,
47158
48301
  anchor,
47159
48302
  color: palette.text,
47160
- halo: true,
48303
+ halo: false,
47161
48304
  haloColor: palette.bg,
47162
48305
  poiId: p.id,
47163
48306
  lineNumber: p.lineNumber
@@ -47168,43 +48311,60 @@ function layoutMap(resolved, data, size, opts) {
47168
48311
  return rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect);
47169
48312
  };
47170
48313
  const GROUP_R = 30;
47171
- const groups = [];
48314
+ const groups2 = [];
47172
48315
  for (const p of ordered) {
47173
- const near = groups.find(
48316
+ const near = groups2.find(
47174
48317
  (g) => g.some((q) => Math.hypot(q.cx - p.cx, q.cy - p.cy) < GROUP_R)
47175
48318
  );
47176
48319
  if (near) near.push(p);
47177
- else groups.push([p]);
48320
+ else groups2.push([p]);
47178
48321
  }
47179
48322
  const ROW_GAP2 = 3;
47180
48323
  const step = poiLabH + ROW_GAP2;
47181
48324
  const COL_GAP = 16;
47182
- const placeColumn = (group) => {
47183
- const items = group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48325
+ const makeItems = (group) => group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48326
+ const columnRows = (items, side) => {
47184
48327
  const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
47185
48328
  const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
47186
- const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
47187
48329
  const maxW = Math.max(...items.map((o) => o.w));
47188
- const side = right + COL_GAP + maxW <= width - 2 ? "right" : "left";
47189
- const colX = side === "right" ? right + COL_GAP : left - COL_GAP;
48330
+ const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
48331
+ const colX = side === "right" ? Math.min(right + COL_GAP, width - 2 - maxW) : Math.max(left - COL_GAP, 2 + maxW);
47190
48332
  const totalH = items.length * step;
47191
48333
  let startY = cyMid - totalH / 2;
47192
48334
  startY = Math.max(2, Math.min(startY, height - totalH - 2));
47193
- items.forEach((o, i) => {
48335
+ return items.map((o, i) => {
47194
48336
  const rowCy = startY + i * step + step / 2;
47195
- obstacles.push({
47196
- x: side === "right" ? colX : colX - o.w,
47197
- y: rowCy - poiLabH / 2,
47198
- w: o.w,
47199
- h: poiLabH
47200
- });
48337
+ return {
48338
+ o,
48339
+ colX,
48340
+ rowCy,
48341
+ rect: {
48342
+ x: side === "right" ? colX : colX - o.w,
48343
+ y: rowCy - poiLabH / 2,
48344
+ w: o.w,
48345
+ h: poiLabH
48346
+ }
48347
+ };
48348
+ });
48349
+ };
48350
+ const wouldColumnBeClean = (items, side) => columnRows(items, side).every(
48351
+ ({ rect }) => rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect)
48352
+ );
48353
+ const defaultColumnSide = (items) => {
48354
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48355
+ const maxW = Math.max(...items.map((o) => o.w));
48356
+ return right + COL_GAP + maxW <= width - 2 ? "right" : "left";
48357
+ };
48358
+ const commitColumn = (items, side, clusterId) => {
48359
+ for (const { o, colX, rowCy, rect } of columnRows(items, side)) {
48360
+ obstacles.push(rect);
47201
48361
  labels.push({
47202
48362
  x: colX,
47203
- y: rowCy + FONT / 3,
48363
+ y: rowCy + FONT2 / 3,
47204
48364
  text: o.text,
47205
48365
  anchor: side === "right" ? "start" : "end",
47206
48366
  color: palette.text,
47207
- halo: true,
48367
+ halo: false,
47208
48368
  haloColor: palette.bg,
47209
48369
  leader: {
47210
48370
  x1: o.p.cx,
@@ -47214,24 +48374,141 @@ function layoutMap(resolved, data, size, opts) {
47214
48374
  },
47215
48375
  leaderColor: o.p.fill,
47216
48376
  poiId: o.p.id,
47217
- lineNumber: o.p.lineNumber
48377
+ lineNumber: o.p.lineNumber,
48378
+ ...clusterId !== void 0 && { clusterMember: clusterId }
47218
48379
  });
48380
+ }
48381
+ };
48382
+ const pushHidden = (p) => {
48383
+ const { text, w } = labelInfo(p);
48384
+ let x = p.cx + p.r + GAP;
48385
+ let anchor = "start";
48386
+ if (x + w > width) {
48387
+ x = p.cx - p.r - GAP - w;
48388
+ anchor = "end";
48389
+ }
48390
+ const y = Math.max(0, Math.min(p.cy - poiLabH / 2, height - poiLabH));
48391
+ labels.push({
48392
+ x: anchor === "start" ? x : x + w,
48393
+ y: y + poiLabH / 2 + FONT2 / 3,
48394
+ text,
48395
+ anchor,
48396
+ color: palette.text,
48397
+ halo: false,
48398
+ haloColor: palette.bg,
48399
+ poiId: p.id,
48400
+ hidden: true,
48401
+ lineNumber: p.lineNumber
47219
48402
  });
47220
48403
  };
47221
- for (const g of groups) {
48404
+ for (const [clusterId, members] of clusterMembersById) {
48405
+ if (members.length === 0) continue;
48406
+ const items = makeItems(members);
48407
+ const side = wouldColumnBeClean(items, "right") ? "right" : wouldColumnBeClean(items, "left") ? "left" : defaultColumnSide(items);
48408
+ commitColumn(items, side, clusterId);
48409
+ }
48410
+ const maxExtent = MAX_CLUSTER_EXTENT_FACTOR * Math.min(width, height);
48411
+ const clusterPending = [];
48412
+ for (const g of groups2) {
48413
+ const items = makeItems(g);
47222
48414
  if (g.length === 1) {
47223
- const p = g[0];
47224
- const { text, w } = labelInfo(p);
48415
+ const { p, text, w } = items[0];
47225
48416
  const side = ["right", "left", "above", "below"].find(
47226
48417
  (s) => inlineFits(p, w, s)
47227
48418
  );
47228
- if (side) {
47229
- pushInline(p, text, w, side);
47230
- continue;
48419
+ if (side) pushInline(p, text, w, side);
48420
+ else commitColumn(items, defaultColumnSide(items));
48421
+ continue;
48422
+ }
48423
+ const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
48424
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48425
+ const minCy = Math.min(...items.map((o) => o.p.cy));
48426
+ const maxCy = Math.max(...items.map((o) => o.p.cy));
48427
+ const diag = Math.hypot(right - left, maxCy - minCy);
48428
+ if (diag > maxExtent || items.length > MAX_COLUMN_ROWS) {
48429
+ items.forEach((o) => pushHidden(o.p));
48430
+ } else {
48431
+ clusterPending.push(items);
48432
+ }
48433
+ }
48434
+ for (const items of clusterPending) {
48435
+ const side = ["right", "left"].find(
48436
+ (s) => wouldColumnBeClean(items, s)
48437
+ );
48438
+ if (side) commitColumn(items, side);
48439
+ else items.forEach((o) => pushHidden(o.p));
48440
+ }
48441
+ }
48442
+ if (resolved.directives.noContextLabels !== true) {
48443
+ for (const l of labels) {
48444
+ if (l.hidden) continue;
48445
+ const w = labelW(l.text);
48446
+ const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
48447
+ obstacles.push({ x, y: l.y - labelH / 2, w, h: labelH });
48448
+ }
48449
+ for (const box of insets)
48450
+ obstacles.push({ x: box.x, y: box.y, w: box.w, h: box.h });
48451
+ const countryCandidates = [];
48452
+ for (const f of worldLayer.values()) {
48453
+ const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
48454
+ if (!iso || regionById.has(iso)) continue;
48455
+ let hasReferencedSub = false;
48456
+ for (const k of regionById.keys())
48457
+ if (k.startsWith(iso + "-")) {
48458
+ hasReferencedSub = true;
48459
+ break;
47231
48460
  }
48461
+ if (hasReferencedSub) continue;
48462
+ const b = path.bounds(f);
48463
+ const [x0, y0] = b[0];
48464
+ const [x1, y1] = b[1];
48465
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48466
+ const anchorLngLat = WORLD_LABEL_ANCHORS[iso];
48467
+ const a = anchorLngLat ? project(anchorLngLat[0], anchorLngLat[1]) : path.centroid(f);
48468
+ countryCandidates.push({
48469
+ name: f.properties?.name ?? iso,
48470
+ bbox: [x0, y0, x1, y1],
48471
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48472
+ });
48473
+ }
48474
+ const framedStateContainers = (resolved.poiFrameContainers ?? []).some(
48475
+ (id) => id.startsWith("US-")
48476
+ );
48477
+ if (usLayer && framedStateContainers) {
48478
+ const containerSet = new Set(resolved.poiFrameContainers);
48479
+ for (const [iso, f] of usLayer) {
48480
+ if (containerSet.has(iso) || regionById.has(iso)) continue;
48481
+ const viewF = cullFeatureToView(f);
48482
+ if (!viewF) continue;
48483
+ const b = path.bounds(viewF);
48484
+ const [x0, y0] = b[0];
48485
+ const [x1, y1] = b[1];
48486
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48487
+ const a = path.centroid(viewF);
48488
+ countryCandidates.push({
48489
+ name: f.properties?.name ?? iso,
48490
+ bbox: [x0, y0, x1, y1],
48491
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48492
+ });
47232
48493
  }
47233
- placeColumn(g);
47234
48494
  }
48495
+ const contextLabels = placeContextLabels({
48496
+ projection: resolved.projection,
48497
+ dLonSpan,
48498
+ dLatSpan,
48499
+ width,
48500
+ height,
48501
+ waterBodies: data.waterBodies,
48502
+ countries: countryCandidates,
48503
+ palette,
48504
+ project,
48505
+ collides,
48506
+ // Water labels must stay over open water — `fillAt` returns the ocean
48507
+ // backdrop colour off-land and a region fill on-land (lakes/states count
48508
+ // as land here, which is the safe side for an ocean name).
48509
+ overLand: (x, y) => fillAt(x, y) !== water
48510
+ });
48511
+ labels.push(...contextLabels);
47235
48512
  }
47236
48513
  let legend = null;
47237
48514
  if (!resolved.directives.noLegend) {
@@ -47266,22 +48543,33 @@ function layoutMap(resolved, data, size, opts) {
47266
48543
  ...resolved.caption !== void 0 && { caption: resolved.caption },
47267
48544
  regions,
47268
48545
  rivers,
48546
+ relief,
48547
+ reliefHatch,
48548
+ coastlineStyle,
47269
48549
  legs,
47270
48550
  pois,
48551
+ clusters,
47271
48552
  labels,
47272
48553
  legend,
47273
48554
  insets,
47274
- insetRegions
48555
+ insetRegions,
48556
+ projection,
48557
+ stretch: stretchParams,
48558
+ diagnostics: []
47275
48559
  };
47276
48560
  }
47277
- var FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, COLO_EPS, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT, RIVER_WIDTH, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_WATER_LIGHT, MUTED_WATER_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, MUTED_LAND_DARK, COLO_R, GOLDEN_ANGLE, FAN_STEP, ARC_CURVE_FRAC, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, US_NON_CONUS;
48561
+ var FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT2, MAX_CLUSTER_EXTENT_FACTOR, MAX_COLUMN_ROWS, REGION_LABEL_HALO_RATIO, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, COMPACT_WIDTH_PX, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, COASTLINE_RING_COUNT, COASTLINE_D0, COASTLINE_STEP, COASTLINE_THICKNESS, COASTLINE_OPACITY_NEAR, COASTLINE_OPACITY_FAR, COASTLINE_MIN_EXTENT, COASTLINE_MIN_EXTENT_GLOBAL, COASTLINE_STROKE_MIX, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, STACK_OVERLAP, STACK_RING_MAX, STACK_RING_GAP, FAN_STEP, ARC_CURVE_FRAC, decodeCache, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, inAlaska, inHawaii, FOREIGN_BORDER, US_NON_CONUS;
47278
48562
  var init_layout15 = __esm({
47279
48563
  "src/map/layout.ts"() {
47280
48564
  "use strict";
47281
48565
  init_color_utils();
48566
+ init_geo();
48567
+ init_colorize();
48568
+ init_colors();
47282
48569
  init_label_layout();
47283
48570
  init_legend_constants();
47284
48571
  init_title_constants();
48572
+ init_context_labels();
47285
48573
  FIT_PAD = 24;
47286
48574
  RAMP_FLOOR = 15;
47287
48575
  R_DEFAULT = 6;
@@ -47289,29 +48577,66 @@ var init_layout15 = __esm({
47289
48577
  R_MAX = 22;
47290
48578
  W_MIN = 1.25;
47291
48579
  W_MAX = 8;
47292
- FONT = 11;
47293
- COLO_EPS = 1.5;
47294
- LAND_TINT_LIGHT = 58;
47295
- LAND_TINT_DARK = 75;
48580
+ FONT2 = 11;
48581
+ MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48582
+ MAX_COLUMN_ROWS = 7;
48583
+ REGION_LABEL_HALO_RATIO = 4.5;
48584
+ LAND_TINT_LIGHT = 12;
48585
+ LAND_TINT_DARK = 24;
47296
48586
  TAG_TINT_LIGHT = 60;
47297
48587
  TAG_TINT_DARK = 68;
47298
- WATER_TINT = 55;
48588
+ WATER_TINT_LIGHT = 24;
48589
+ WATER_TINT_DARK = 24;
47299
48590
  RIVER_WIDTH = 1.3;
48591
+ COMPACT_WIDTH_PX = 480;
48592
+ RELIEF_MIN_AREA = 12;
48593
+ RELIEF_MIN_DIM = 2;
48594
+ RELIEF_HATCH_SPACING = 2;
48595
+ RELIEF_HATCH_WIDTH = 0.15;
48596
+ RELIEF_HATCH_STRENGTH = 32;
48597
+ COASTLINE_RING_COUNT = 5;
48598
+ COASTLINE_D0 = 16e-4;
48599
+ COASTLINE_STEP = 28e-4;
48600
+ COASTLINE_THICKNESS = 14e-4;
48601
+ COASTLINE_OPACITY_NEAR = 0.5;
48602
+ COASTLINE_OPACITY_FAR = 0.1;
48603
+ COASTLINE_MIN_EXTENT = 6e-4;
48604
+ COASTLINE_MIN_EXTENT_GLOBAL = 6e-4;
48605
+ COASTLINE_STROKE_MIX = 32;
47300
48606
  FOREIGN_TINT_LIGHT = 30;
47301
48607
  FOREIGN_TINT_DARK = 62;
47302
- MUTED_WATER_LIGHT = 14;
47303
- MUTED_WATER_DARK = 10;
47304
48608
  MUTED_FOREIGN_LIGHT = 28;
47305
48609
  MUTED_FOREIGN_DARK = 16;
47306
- MUTED_LAND_DARK = 24;
47307
48610
  COLO_R = 9;
47308
48611
  GOLDEN_ANGLE = 2.399963229728653;
48612
+ STACK_OVERLAP = 1;
48613
+ STACK_RING_MAX = 8;
48614
+ STACK_RING_GAP = 4;
47309
48615
  FAN_STEP = 16;
47310
48616
  ARC_CURVE_FRAC = 0.18;
48617
+ decodeCache = /* @__PURE__ */ new WeakMap();
47311
48618
  usConusProjection = () => geoConicEqualArea().parallels([29.5, 45.5]).rotate([96, 0]);
47312
48619
  alaskaProjection = () => geoConicEqualArea().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
47313
48620
  hawaiiProjection = () => geoMercator();
47314
48621
  INSET_STATES = /* @__PURE__ */ new Set(["US-AK", "US-HI"]);
48622
+ inAlaska = (lon, lat) => lat >= 51 && (lon <= -129 || lon >= 172);
48623
+ inHawaii = (lon, lat) => lat >= 18 && lat <= 23 && lon >= -161 && lon <= -154;
48624
+ FOREIGN_BORDER = {
48625
+ CA: [
48626
+ "US-AK",
48627
+ "US-WA",
48628
+ "US-ID",
48629
+ "US-MT",
48630
+ "US-ND",
48631
+ "US-MN",
48632
+ "US-MI",
48633
+ "US-NY",
48634
+ "US-VT",
48635
+ "US-NH",
48636
+ "US-ME"
48637
+ ],
48638
+ MX: ["US-CA", "US-AZ", "US-NM", "US-TX"]
48639
+ };
47315
48640
  US_NON_CONUS = /* @__PURE__ */ new Set([
47316
48641
  "US-AK",
47317
48642
  "US-HI",
@@ -47331,6 +48656,58 @@ __export(renderer_exports16, {
47331
48656
  renderMapForExport: () => renderMapForExport
47332
48657
  });
47333
48658
  import * as d3Selection18 from "d3-selection";
48659
+ function pointInRing2(px, py, ring) {
48660
+ let inside = false;
48661
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
48662
+ const [xi, yi] = ring[i];
48663
+ const [xj, yj] = ring[j];
48664
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
48665
+ inside = !inside;
48666
+ }
48667
+ return inside;
48668
+ }
48669
+ function ringToPath(ring) {
48670
+ let d = "";
48671
+ for (let i = 0; i < ring.length; i++)
48672
+ d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48673
+ return d + "Z";
48674
+ }
48675
+ function coastlineOuterRings(regions, minExtent) {
48676
+ const paths = [];
48677
+ for (const r of regions) {
48678
+ const rings = parsePathRings(r.d);
48679
+ for (let i = 0; i < rings.length; i++) {
48680
+ const ring = rings[i];
48681
+ if (ring.length < 3) continue;
48682
+ let minX = Infinity;
48683
+ let minY = Infinity;
48684
+ let maxX = -Infinity;
48685
+ let maxY = -Infinity;
48686
+ for (const [x, y] of ring) {
48687
+ if (x < minX) minX = x;
48688
+ if (x > maxX) maxX = x;
48689
+ if (y < minY) minY = y;
48690
+ if (y > maxY) maxY = y;
48691
+ }
48692
+ if (Math.max(maxX - minX, maxY - minY) < minExtent) continue;
48693
+ const [fx, fy] = ring[0];
48694
+ let depth = 0;
48695
+ for (let j = 0; j < rings.length; j++)
48696
+ if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48697
+ if (depth % 2 === 1) continue;
48698
+ paths.push(ringToPath(ring));
48699
+ }
48700
+ }
48701
+ return paths;
48702
+ }
48703
+ function appendWaterLines(g, outerRings, style, flatWater) {
48704
+ const d = outerRings.join(" ");
48705
+ const linesOuterFirst = [...style.lines].sort((a, b) => b.d - a.d);
48706
+ for (const line12 of linesOuterFirst) {
48707
+ g.append("path").attr("d", d).attr("stroke", style.color).attr("stroke-width", 2 * (line12.d + line12.thickness)).attr("stroke-opacity", line12.opacity).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
48708
+ g.append("path").attr("d", d).attr("stroke", flatWater).attr("stroke-width", 2 * line12.d).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
48709
+ }
48710
+ }
47334
48711
  function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims, activeGroupOverride) {
47335
48712
  d3Selection18.select(container).selectAll(":not([data-d3-tooltip])").remove();
47336
48713
  const width = exportDims?.width ?? container.clientWidth;
@@ -47343,6 +48720,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47343
48720
  {
47344
48721
  palette,
47345
48722
  isDark,
48723
+ // Export-only: forward the contain-fit request from mapExportDimensions so a
48724
+ // clamped/floored (off-aspect) export canvas letterboxes instead of
48725
+ // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48726
+ // keeps the global stretch-fill.
48727
+ preferContain: exportDims?.preferContain ?? false,
47346
48728
  ...activeGroupOverride !== void 0 && {
47347
48729
  activeGroup: activeGroupOverride
47348
48730
  }
@@ -47356,6 +48738,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47356
48738
  const gRegions = svg.append("g").attr("class", "dgmo-map-regions");
47357
48739
  const drawRegion = (g, r, strokeWidth) => {
47358
48740
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
48741
+ if (r.label) p.attr("data-region-name", r.label);
47359
48742
  if (r.layer !== "base") {
47360
48743
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
47361
48744
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -47376,6 +48759,52 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47376
48759
  }
47377
48760
  };
47378
48761
  for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
48762
+ if (layout.relief.length && layout.reliefHatch) {
48763
+ const h = layout.reliefHatch;
48764
+ const rangeClipId = "dgmo-relief-clip";
48765
+ const landClipId = "dgmo-relief-land";
48766
+ const rangeClip = defs.append("clipPath").attr("id", rangeClipId);
48767
+ for (const s of layout.relief) rangeClip.append("path").attr("d", s.d);
48768
+ const landClip = defs.append("clipPath").attr("id", landClipId);
48769
+ for (const r of layout.regions)
48770
+ if (r.id !== "lake") landClip.append("path").attr("d", r.d);
48771
+ 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");
48772
+ for (let y = h.spacing; y < height; y += h.spacing) {
48773
+ gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
48774
+ }
48775
+ }
48776
+ if (layout.coastlineStyle) {
48777
+ const cs = layout.coastlineStyle;
48778
+ const maskId = "dgmo-map-water-mask";
48779
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48780
+ mask.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "white");
48781
+ const landD = layout.regions.filter((r) => r.id !== "lake").map((r) => r.d).join(" ");
48782
+ const lakeD = layout.regions.filter((r) => r.id === "lake").map((r) => r.d).join(" ");
48783
+ if (landD) mask.append("path").attr("d", landD).attr("fill", "black");
48784
+ if (lakeD) mask.append("path").attr("d", lakeD).attr("fill", "white");
48785
+ if (layout.insets.length) {
48786
+ const reach = Math.max(0, ...cs.lines.map((l) => l.d + l.thickness));
48787
+ for (const box of layout.insets) {
48788
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48789
+ mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
48790
+ }
48791
+ }
48792
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
48793
+ appendWaterLines(
48794
+ gWater,
48795
+ coastlineOuterRings(layout.regions, cs.minExtent),
48796
+ cs,
48797
+ layout.background
48798
+ );
48799
+ const byStroke = /* @__PURE__ */ new Map();
48800
+ for (const r of layout.regions) {
48801
+ const arr = byStroke.get(r.stroke);
48802
+ if (arr) arr.push(r.d);
48803
+ else byStroke.set(r.stroke, [r.d]);
48804
+ }
48805
+ for (const [stroke2, ds] of byStroke)
48806
+ gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48807
+ }
47379
48808
  if (layout.rivers.length) {
47380
48809
  const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47381
48810
  for (const r of layout.rivers) {
@@ -47384,15 +48813,61 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47384
48813
  }
47385
48814
  if (layout.insets.length) {
47386
48815
  const insetG = svg.append("g").attr("class", "dgmo-map-insets");
47387
- for (const box of layout.insets) {
48816
+ layout.insets.forEach((box, bi) => {
47388
48817
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
47389
48818
  insetG.append("path").attr("d", d).attr("fill", layout.background).attr("stroke", mix(palette.text, palette.bg, 55)).attr("stroke-width", 1).attr("stroke-linejoin", "round");
47390
- }
48819
+ if (box.contextLand) {
48820
+ const clipId = `dgmo-map-inset-clip-${bi}`;
48821
+ defs.append("clipPath").attr("id", clipId).append("path").attr("d", d);
48822
+ insetG.append("path").attr("d", box.contextLand.d).attr("fill", box.contextLand.fill).attr("clip-path", `url(#${clipId})`);
48823
+ }
48824
+ });
47391
48825
  for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
47392
- }
48826
+ if (layout.coastlineStyle) {
48827
+ const cs = layout.coastlineStyle;
48828
+ const maskId = "dgmo-map-inset-water-mask";
48829
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48830
+ for (const box of layout.insets) {
48831
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48832
+ mask.append("path").attr("d", d).attr("fill", "white");
48833
+ }
48834
+ layout.insets.forEach((box, bi) => {
48835
+ if (box.contextLand)
48836
+ mask.append("path").attr("d", box.contextLand.d).attr("fill", "black").attr("clip-path", `url(#dgmo-map-inset-clip-${bi})`);
48837
+ });
48838
+ for (const r of layout.insetRegions)
48839
+ if (r.id !== "lake")
48840
+ mask.append("path").attr("d", r.d).attr("fill", "black");
48841
+ for (const r of layout.insetRegions)
48842
+ if (r.id === "lake")
48843
+ mask.append("path").attr("d", r.d).attr("fill", "white");
48844
+ const clipId = "dgmo-map-inset-water-clip";
48845
+ const clip = defs.append("clipPath").attr("id", clipId);
48846
+ for (const box of layout.insets) {
48847
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48848
+ clip.append("path").attr("d", d);
48849
+ }
48850
+ const gInsetWater = insetG.append("g").attr("clip-path", `url(#${clipId})`).append("g").attr("class", "dgmo-map-inset-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
48851
+ appendWaterLines(
48852
+ gInsetWater,
48853
+ coastlineOuterRings(layout.insetRegions, cs.minExtent),
48854
+ cs,
48855
+ layout.background
48856
+ );
48857
+ for (const r of layout.insetRegions)
48858
+ gInsetWater.append("path").attr("d", r.d).attr("stroke", r.stroke).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48859
+ }
48860
+ }
48861
+ const wireSync = (sel, lineNumber) => {
48862
+ if (lineNumber < 1) return;
48863
+ sel.attr("data-line-number", lineNumber);
48864
+ if (onClickItem)
48865
+ sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
48866
+ };
47393
48867
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
47394
48868
  layout.legs.forEach((leg, i) => {
47395
48869
  const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round");
48870
+ wireSync(p, leg.lineNumber);
47396
48871
  if (leg.arrow) {
47397
48872
  const id = `dgmo-map-arrow-${i}`;
47398
48873
  const s = arrowSize(leg.width);
@@ -47400,25 +48875,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47400
48875
  p.attr("marker-end", `url(#${id})`);
47401
48876
  }
47402
48877
  if (leg.label !== void 0 && leg.labelX !== void 0) {
47403
- emitText(
48878
+ const lt = emitText(
47404
48879
  gLegs,
47405
48880
  leg.labelX,
47406
48881
  leg.labelY ?? 0,
47407
48882
  leg.label,
47408
48883
  "middle",
47409
- palette.textMuted,
47410
- haloColor,
47411
- true,
48884
+ leg.labelColor ?? palette.textMuted,
48885
+ leg.labelHaloColor ?? haloColor,
48886
+ leg.labelHalo ?? true,
47412
48887
  LABEL_FONT - 1
47413
48888
  );
48889
+ wireSync(lt, leg.lineNumber);
47414
48890
  }
47415
48891
  });
48892
+ const gSpider = svg.append("g").attr("class", "dgmo-map-spider");
48893
+ for (const cl of layout.clusters) {
48894
+ if (!exportDims) {
48895
+ gSpider.append("circle").attr("cx", cl.cx).attr("cy", cl.cy).attr("r", cl.hitR).attr("fill", "transparent").attr("data-cluster-hit", cl.id).style("cursor", "pointer");
48896
+ }
48897
+ for (const leg of cl.legs) {
48898
+ gSpider.append("line").attr("x1", cl.cx).attr("y1", cl.cy).attr("x2", leg.x2).attr("y2", leg.y2).attr("stroke", leg.color).attr("stroke-width", 1).attr("data-cluster-deco", cl.id).style("pointer-events", "none");
48899
+ }
48900
+ gSpider.append("circle").attr("cx", cl.cx).attr("cy", cl.cy).attr("r", 2).attr("fill", mix(palette.textMuted, palette.bg, 40)).attr("data-cluster-deco", cl.id).style("pointer-events", "none");
48901
+ }
47416
48902
  const gPois = svg.append("g").attr("class", "dgmo-map-pois");
47417
48903
  for (const poi of layout.pois) {
47418
48904
  if (poi.isOrigin) {
47419
48905
  gPois.append("circle").attr("cx", poi.cx).attr("cy", poi.cy).attr("r", poi.r + 3).attr("fill", "none").attr("stroke", poi.stroke).attr("stroke-width", 1.5);
47420
48906
  }
47421
48907
  const c = gPois.append("circle").attr("cx", poi.cx).attr("cy", poi.cy).attr("r", poi.r).attr("fill", poi.fill).attr("stroke", poi.stroke).attr("stroke-width", 1).attr("data-line-number", poi.lineNumber).attr("data-poi", poi.id);
48908
+ if (poi.clusterId !== void 0)
48909
+ c.attr("data-cluster-member", poi.clusterId);
47422
48910
  if (poi.tags) {
47423
48911
  for (const [group, value] of Object.entries(poi.tags)) {
47424
48912
  c.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
@@ -47446,12 +48934,32 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47446
48934
  }
47447
48935
  const gLabels = svg.append("g").attr("class", "dgmo-map-labels");
47448
48936
  for (const lab of layout.labels) {
48937
+ if (lab.hidden) {
48938
+ if (exportDims) continue;
48939
+ emitText(
48940
+ gLabels,
48941
+ lab.x,
48942
+ lab.y,
48943
+ lab.text,
48944
+ lab.anchor,
48945
+ lab.color,
48946
+ lab.haloColor,
48947
+ lab.halo,
48948
+ LABEL_FONT,
48949
+ lab.italic,
48950
+ lab.letterSpacing
48951
+ ).attr("data-poi", lab.poiId ?? null).attr("data-poi-hidden", "").style("opacity", 0).style("pointer-events", "none");
48952
+ continue;
48953
+ }
47449
48954
  if (lab.leader) {
47450
48955
  const line12 = gLabels.append("line").attr("x1", lab.leader.x1).attr("y1", lab.leader.y1).attr("x2", lab.leader.x2).attr("y2", lab.leader.y2).attr(
47451
48956
  "stroke",
47452
48957
  lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
47453
48958
  ).attr("stroke-width", lab.leaderColor ? 1 : 0.75);
47454
48959
  if (lab.poiId !== void 0) line12.attr("data-poi", lab.poiId);
48960
+ if (lab.clusterMember !== void 0)
48961
+ line12.attr("data-cluster-member", lab.clusterMember);
48962
+ wireSync(line12, lab.lineNumber);
47455
48963
  }
47456
48964
  const t = emitText(
47457
48965
  gLabels,
@@ -47462,11 +48970,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47462
48970
  lab.color,
47463
48971
  lab.haloColor,
47464
48972
  lab.halo,
47465
- LABEL_FONT
48973
+ LABEL_FONT,
48974
+ lab.italic,
48975
+ lab.letterSpacing,
48976
+ lab.lines
47466
48977
  );
47467
48978
  if (lab.poiId !== void 0) {
47468
48979
  t.attr("data-poi", lab.poiId).style("cursor", "default");
47469
48980
  }
48981
+ if (lab.clusterMember !== void 0) {
48982
+ t.attr("data-cluster-member", lab.clusterMember);
48983
+ }
48984
+ wireSync(t, lab.lineNumber);
48985
+ }
48986
+ if (!exportDims && layout.clusters.length) {
48987
+ const gBadge = svg.append("g").attr("class", "dgmo-map-cluster-badges");
48988
+ for (const cl of layout.clusters) {
48989
+ const g = gBadge.append("g").attr("data-cluster", cl.id).style("opacity", 0).style("pointer-events", "none");
48990
+ const R = 9;
48991
+ g.append("circle").attr("cx", cl.cx).attr("cy", cl.cy).attr("r", R).attr("fill", mix(palette.textMuted, palette.bg, 35)).attr("stroke", palette.textMuted).attr("stroke-width", 1);
48992
+ g.append("circle").attr("cx", cl.cx).attr("cy", cl.cy).attr("r", R + 2.5).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1);
48993
+ emitText(
48994
+ g,
48995
+ cl.cx,
48996
+ cl.cy + 3,
48997
+ String(cl.count),
48998
+ "middle",
48999
+ palette.text,
49000
+ palette.bg,
49001
+ false,
49002
+ LABEL_FONT
49003
+ );
49004
+ }
47470
49005
  }
47471
49006
  if (layout.legend) {
47472
49007
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
@@ -47500,10 +49035,10 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47500
49035
  }
47501
49036
  }
47502
49037
  if (layout.title) {
47503
- 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);
49038
+ svg.append("text").attr("class", "dgmo-map-title").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).attr("fill", palette.text).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 4).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.title);
47504
49039
  }
47505
49040
  if (layout.subtitle) {
47506
- svg.append("text").attr("x", width / 2).attr("y", TITLE_Y + TITLE_FONT_SIZE).attr("text-anchor", "middle").attr("font-size", LABEL_FONT + 1).attr("fill", palette.textMuted).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.subtitle);
49041
+ svg.append("text").attr("class", "dgmo-map-subtitle").attr("x", width / 2).attr("y", TITLE_Y + TITLE_FONT_SIZE).attr("text-anchor", "middle").attr("font-size", LABEL_FONT + 1).attr("fill", palette.textMuted).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.subtitle);
47507
49042
  }
47508
49043
  if (layout.caption) {
47509
49044
  svg.append("text").attr("x", width / 2).attr("y", height - 8).attr("text-anchor", "middle").attr("font-size", LABEL_FONT).attr("fill", palette.textMuted).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.caption);
@@ -47512,10 +49047,21 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47512
49047
  function renderMapForExport(container, resolved, data, palette, isDark, exportDims) {
47513
49048
  renderMap(container, resolved, data, palette, isDark, void 0, exportDims);
47514
49049
  }
47515
- function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize) {
47516
- const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color).text(text);
49050
+ function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize, italic, letterSpacing, lines) {
49051
+ const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color);
49052
+ if (lines && lines.length > 1) {
49053
+ const lineHeight = fontSize + 2;
49054
+ const startDy = -((lines.length - 1) / 2) * lineHeight;
49055
+ lines.forEach((ln, i) => {
49056
+ t.append("tspan").attr("x", x).attr("dy", i === 0 ? startDy : lineHeight).text(ln);
49057
+ });
49058
+ } else {
49059
+ t.text(text);
49060
+ }
49061
+ if (italic) t.attr("font-style", "italic");
49062
+ if (letterSpacing) t.attr("letter-spacing", letterSpacing);
47517
49063
  if (withHalo) {
47518
- t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7);
49064
+ t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 2).attr("stroke-linejoin", "round").attr("stroke-linecap", "round").attr("stroke-opacity", 0.55);
47519
49065
  }
47520
49066
  return t;
47521
49067
  }
@@ -47532,6 +49078,178 @@ var init_renderer16 = __esm({
47532
49078
  }
47533
49079
  });
47534
49080
 
49081
+ // src/map/dimensions.ts
49082
+ var dimensions_exports = {};
49083
+ __export(dimensions_exports, {
49084
+ mapContentAspect: () => mapContentAspect,
49085
+ mapExportDimensions: () => mapExportDimensions
49086
+ });
49087
+ import { geoPath as geoPath2 } from "d3-geo";
49088
+ function mapContentAspect(resolved, data, ref = REF) {
49089
+ const { projection, fitTarget } = buildMapProjection(resolved, data);
49090
+ projection.fitSize([ref, ref], fitTarget);
49091
+ const b = geoPath2(projection).bounds(fitTarget);
49092
+ const w = b[1][0] - b[0][0];
49093
+ const h = b[1][1] - b[0][1];
49094
+ const aspect = w / h;
49095
+ return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49096
+ }
49097
+ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49098
+ const raw = mapContentAspect(resolved, data);
49099
+ const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49100
+ const width = baseWidth;
49101
+ let height = Math.round(width / clamped);
49102
+ let chromeReserve = 0;
49103
+ if (resolved.title && resolved.pois.length > 0) {
49104
+ const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
49105
+ chromeReserve += Math.max(FIT_PAD2, bannerBottom + TITLE_GAP) - FIT_PAD2;
49106
+ }
49107
+ let floored = false;
49108
+ if (height - chromeReserve < MIN_MAP_BAND) {
49109
+ height = Math.round(chromeReserve + MIN_MAP_BAND);
49110
+ floored = true;
49111
+ }
49112
+ const preferContain = clamped !== raw || floored;
49113
+ return { width, height, preferContain };
49114
+ }
49115
+ var FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
49116
+ var init_dimensions = __esm({
49117
+ "src/map/dimensions.ts"() {
49118
+ "use strict";
49119
+ init_title_constants();
49120
+ init_layout15();
49121
+ FIT_PAD2 = 24;
49122
+ TITLE_GAP = 16;
49123
+ ASPECT_MAX = 3;
49124
+ ASPECT_MIN = 0.9;
49125
+ MIN_MAP_BAND = 200;
49126
+ FALLBACK_ASPECT = 1.5;
49127
+ REF = 1e3;
49128
+ }
49129
+ });
49130
+
49131
+ // src/map/load-data.ts
49132
+ var load_data_exports = {};
49133
+ __export(load_data_exports, {
49134
+ loadMapData: () => loadMapData
49135
+ });
49136
+ async function loadNodeBuiltins() {
49137
+ const [{ readFile }, { fileURLToPath }, { dirname, resolve }] = await Promise.all([
49138
+ import("fs/promises"),
49139
+ import("url"),
49140
+ import("path")
49141
+ ]);
49142
+ return { readFile, fileURLToPath, dirname, resolve };
49143
+ }
49144
+ async function readJson(nb, dir, name) {
49145
+ return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
49146
+ }
49147
+ async function firstExistingDir(nb, baseDir) {
49148
+ for (const rel of CANDIDATE_DIRS) {
49149
+ const dir = nb.resolve(baseDir, rel);
49150
+ try {
49151
+ await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
49152
+ return dir;
49153
+ } catch {
49154
+ }
49155
+ }
49156
+ throw new Error(
49157
+ `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
49158
+ );
49159
+ }
49160
+ function validate(data) {
49161
+ const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
49162
+ if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
49163
+ throw new Error("map data assets are malformed (failed shape validation)");
49164
+ }
49165
+ return data;
49166
+ }
49167
+ function moduleBaseDir(nb) {
49168
+ try {
49169
+ const url = import.meta.url;
49170
+ if (url) return nb.dirname(nb.fileURLToPath(url));
49171
+ } catch {
49172
+ }
49173
+ if (typeof __dirname !== "undefined") return __dirname;
49174
+ return process.cwd();
49175
+ }
49176
+ function loadMapData() {
49177
+ cache ??= (async () => {
49178
+ const nb = await loadNodeBuiltins();
49179
+ const dir = await firstExistingDir(nb, moduleBaseDir(nb));
49180
+ const [
49181
+ worldCoarse,
49182
+ worldDetail,
49183
+ usStates,
49184
+ lakes,
49185
+ rivers,
49186
+ mountainRanges,
49187
+ naLand,
49188
+ naLakes,
49189
+ waterBodies,
49190
+ gazetteer
49191
+ ] = await Promise.all([
49192
+ // worldCoarse (110m) is LOAD-BEARING but NOT a render source: the world
49193
+ // basemap renders from worldDetail (50m) at all scales (resolver pins
49194
+ // basemaps.world = 'detail'). Coarse stays as the authoritative region
49195
+ // name index + dominant-landmass bbox source in resolver.ts. Do not drop it.
49196
+ readJson(nb, dir, FILES.worldCoarse),
49197
+ readJson(nb, dir, FILES.worldDetail),
49198
+ readJson(nb, dir, FILES.usStates),
49199
+ // Lakes/rivers/mountain/NA/water assets are optional — older bundles may predate them.
49200
+ readJson(nb, dir, FILES.lakes).catch(() => void 0),
49201
+ readJson(nb, dir, FILES.rivers).catch(() => void 0),
49202
+ readJson(nb, dir, FILES.mountainRanges).catch(
49203
+ () => void 0
49204
+ ),
49205
+ readJson(nb, dir, FILES.naLand).catch(() => void 0),
49206
+ readJson(nb, dir, FILES.naLakes).catch(() => void 0),
49207
+ readJson(nb, dir, FILES.waterBodies).catch(() => void 0),
49208
+ readJson(nb, dir, FILES.gazetteer)
49209
+ ]);
49210
+ return validate({
49211
+ worldCoarse,
49212
+ worldDetail,
49213
+ usStates,
49214
+ gazetteer,
49215
+ ...lakes && { lakes },
49216
+ ...rivers && { rivers },
49217
+ ...mountainRanges && { mountainRanges },
49218
+ ...naLand && { naLand },
49219
+ ...naLakes && { naLakes },
49220
+ ...waterBodies && { waterBodies }
49221
+ });
49222
+ })().catch((e) => {
49223
+ cache = void 0;
49224
+ throw e;
49225
+ });
49226
+ return cache;
49227
+ }
49228
+ var FILES, CANDIDATE_DIRS, cache;
49229
+ var init_load_data = __esm({
49230
+ "src/map/load-data.ts"() {
49231
+ "use strict";
49232
+ FILES = {
49233
+ worldCoarse: "world-coarse.json",
49234
+ worldDetail: "world-detail.json",
49235
+ usStates: "us-states.json",
49236
+ lakes: "lakes.json",
49237
+ rivers: "rivers.json",
49238
+ mountainRanges: "mountain-ranges.json",
49239
+ naLand: "na-land.json",
49240
+ naLakes: "na-lakes.json",
49241
+ waterBodies: "water-bodies.json",
49242
+ gazetteer: "gazetteer.json"
49243
+ };
49244
+ CANDIDATE_DIRS = [
49245
+ "./data",
49246
+ "./map-data",
49247
+ "../map-data",
49248
+ "../src/map/data"
49249
+ ];
49250
+ }
49251
+ });
49252
+
47535
49253
  // src/pyramid/renderer.ts
47536
49254
  var renderer_exports17 = {};
47537
49255
  __export(renderer_exports17, {
@@ -49534,8 +51252,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
49534
51252
  const lines = splitParticipantLabel(p.label, LABEL_MAX_CHARS);
49535
51253
  if (lines.length === 0) continue;
49536
51254
  const widest = Math.max(...lines.map((l) => l.length));
49537
- const labelWidth = widest * LABEL_CHAR_WIDTH + 10;
49538
- uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth);
51255
+ const labelWidth2 = widest * LABEL_CHAR_WIDTH + 10;
51256
+ uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth2);
49539
51257
  }
49540
51258
  uniformBoxWidth = Math.min(MAX_BOX_WIDTH, uniformBoxWidth);
49541
51259
  const effectiveGap = Math.max(PARTICIPANT_GAP, uniformBoxWidth + 30);
@@ -52234,15 +53952,15 @@ function renderArcDiagram(container, parsed, palette, _isDark, onClickItem, expo
52234
53952
  textColor,
52235
53953
  onClickItem
52236
53954
  );
52237
- const neighbors = /* @__PURE__ */ new Map();
52238
- for (const node of nodes) neighbors.set(node, /* @__PURE__ */ new Set());
53955
+ const neighbors2 = /* @__PURE__ */ new Map();
53956
+ for (const node of nodes) neighbors2.set(node, /* @__PURE__ */ new Set());
52239
53957
  for (const link of links) {
52240
- neighbors.get(link.source).add(link.target);
52241
- neighbors.get(link.target).add(link.source);
53958
+ neighbors2.get(link.source).add(link.target);
53959
+ neighbors2.get(link.target).add(link.source);
52242
53960
  }
52243
53961
  const FADE_OPACITY3 = 0.1;
52244
53962
  function handleMouseEnter(hovered) {
52245
- const connected = neighbors.get(hovered);
53963
+ const connected = neighbors2.get(hovered);
52246
53964
  g.selectAll(".arc-link").each(function() {
52247
53965
  const el = d3Selection23.select(this);
52248
53966
  const src = el.attr("data-source");
@@ -54177,7 +55895,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54177
55895
  8,
54178
55896
  Math.floor(OVERLAP_WRAP_TARGET_W / OVERLAP_CH_W)
54179
55897
  );
54180
- function wrapLabel2(text, maxChars) {
55898
+ function wrapLabel3(text, maxChars) {
54181
55899
  const words = text.split(/\s+/).filter(Boolean);
54182
55900
  const lines = [];
54183
55901
  let cur = "";
@@ -54223,7 +55941,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54223
55941
  if (!ov.label) continue;
54224
55942
  const idxs = ov.sets.map((s) => vennSets.findIndex((vs) => vs.name === s));
54225
55943
  if (idxs.some((idx) => idx < 0)) continue;
54226
- const lines = wrapLabel2(ov.label, MAX_WRAP_CHARS);
55944
+ const lines = wrapLabel3(ov.label, MAX_WRAP_CHARS);
54227
55945
  wrappedOverlapLabels.set(ov, lines);
54228
55946
  const dir = predictOverlapDirRaw(idxs);
54229
55947
  const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
@@ -55660,25 +57378,29 @@ async function renderForExport(content, theme, palette, viewState, options) {
55660
57378
  if (detectedType === "map") {
55661
57379
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55662
57380
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55663
- const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
55664
57381
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
57382
+ const { mapExportDimensions: mapExportDimensions2 } = await Promise.resolve().then(() => (init_dimensions(), dimensions_exports));
55665
57383
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55666
57384
  const mapParsed = parseMap2(content);
55667
- let mapData;
55668
- try {
55669
- mapData = await loadMapData2();
55670
- } catch {
55671
- return "";
57385
+ let mapData = options?.mapData;
57386
+ if (!mapData) {
57387
+ const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
57388
+ try {
57389
+ mapData = await loadMapData2();
57390
+ } catch {
57391
+ return "";
57392
+ }
55672
57393
  }
55673
57394
  const mapResolved = resolveMap2(mapParsed, mapData);
55674
- const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
57395
+ const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57396
+ const container2 = createExportContainer(dims2.width, dims2.height);
55675
57397
  renderMapForExport2(
55676
57398
  container2,
55677
57399
  mapResolved,
55678
57400
  mapData,
55679
57401
  effectivePalette2,
55680
57402
  theme === "dark",
55681
- { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
57403
+ dims2
55682
57404
  );
55683
57405
  return finalizeSvgExport(container2, theme, effectivePalette2);
55684
57406
  }
@@ -56520,7 +58242,8 @@ async function render(content, options) {
56520
58242
  ...options?.c4Container !== void 0 && {
56521
58243
  c4Container: options.c4Container
56522
58244
  },
56523
- ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup }
58245
+ ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup },
58246
+ ...options?.mapData !== void 0 && { mapData: options.mapData }
56524
58247
  });
56525
58248
  if (chartType === "map") {
56526
58249
  try {
@@ -56531,7 +58254,7 @@ async function render(content, options) {
56531
58254
  Promise.resolve().then(() => (init_load_data(), load_data_exports))
56532
58255
  ]
56533
58256
  );
56534
- const data = await loadMapData2();
58257
+ const data = options?.mapData ?? await loadMapData2();
56535
58258
  diagnostics = [...resolveMap2(parseMap2(content), data).diagnostics];
56536
58259
  } catch {
56537
58260
  }