@diagrammo/dgmo 0.21.1 → 0.23.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 (87) hide show
  1. package/README.md +16 -6
  2. package/dist/advanced.cjs +2230 -503
  3. package/dist/advanced.d.cts +5731 -0
  4. package/dist/advanced.d.ts +5731 -0
  5. package/dist/advanced.js +2226 -503
  6. package/dist/auto.cjs +2272 -479
  7. package/dist/auto.d.cts +39 -0
  8. package/dist/auto.d.ts +39 -0
  9. package/dist/auto.js +124 -124
  10. package/dist/auto.mjs +2274 -480
  11. package/dist/cli.cjs +170 -170
  12. package/dist/editor.cjs +16 -16
  13. package/dist/editor.js +16 -16
  14. package/dist/highlight.cjs +18 -13
  15. package/dist/highlight.js +18 -13
  16. package/dist/index.cjs +2253 -465
  17. package/dist/index.d.cts +339 -0
  18. package/dist/index.d.ts +339 -0
  19. package/dist/index.js +2255 -466
  20. package/dist/internal.cjs +2230 -503
  21. package/dist/internal.d.cts +5731 -0
  22. package/dist/internal.d.ts +5731 -0
  23. package/dist/internal.js +2226 -503
  24. package/dist/map-data/PROVENANCE.json +1 -1
  25. package/dist/map-data/gazetteer.json +1 -1
  26. package/dist/map-data/mountain-ranges.json +1 -1
  27. package/dist/map-data/water-bodies.json +1 -0
  28. package/dist/map-data/world-coarse.json +1 -1
  29. package/dist/map-data/world-detail.json +1 -1
  30. package/docs/language-reference.md +55 -9
  31. package/gallery/fixtures/boxes-and-lines.dgmo +6 -4
  32. package/gallery/fixtures/map-categorical-world.dgmo +16 -0
  33. package/gallery/fixtures/map-categorical.dgmo +0 -1
  34. package/gallery/fixtures/map-choropleth.dgmo +0 -1
  35. package/gallery/fixtures/map-coastline.dgmo +7 -0
  36. package/gallery/fixtures/map-colorize.dgmo +11 -0
  37. package/gallery/fixtures/map-direct-color.dgmo +0 -1
  38. package/gallery/fixtures/map-reference-world.dgmo +11 -0
  39. package/gallery/fixtures/map-region-scope.dgmo +0 -3
  40. package/gallery/fixtures/map-route.dgmo +0 -1
  41. package/package.json +1 -1
  42. package/src/advanced.ts +12 -1
  43. package/src/boxes-and-lines/parser.ts +39 -0
  44. package/src/boxes-and-lines/renderer.ts +205 -20
  45. package/src/boxes-and-lines/types.ts +9 -0
  46. package/src/cli.ts +1 -1
  47. package/src/completion.ts +36 -30
  48. package/src/cycle/renderer.ts +14 -1
  49. package/src/d3.ts +20 -6
  50. package/src/editor/highlight-api.ts +4 -0
  51. package/src/editor/keywords.ts +16 -16
  52. package/src/infra/renderer.ts +35 -7
  53. package/src/map/colorize.ts +54 -0
  54. package/src/map/context-labels.ts +429 -0
  55. package/src/map/data/PROVENANCE.json +1 -1
  56. package/src/map/data/README.md +6 -0
  57. package/src/map/data/gazetteer.json +1 -1
  58. package/src/map/data/mountain-ranges.json +1 -1
  59. package/src/map/data/types.ts +34 -0
  60. package/src/map/data/water-bodies.json +1 -0
  61. package/src/map/data/world-coarse.json +1 -1
  62. package/src/map/data/world-detail.json +1 -1
  63. package/src/map/dimensions.ts +117 -0
  64. package/src/map/geo-query.ts +21 -3
  65. package/src/map/geo.ts +47 -1
  66. package/src/map/layout.ts +1408 -266
  67. package/src/map/load-data.ts +10 -2
  68. package/src/map/parser.ts +42 -116
  69. package/src/map/renderer.ts +604 -14
  70. package/src/map/resolved-types.ts +16 -2
  71. package/src/map/resolver.ts +208 -59
  72. package/src/map/types.ts +30 -32
  73. package/src/mindmap/renderer.ts +10 -1
  74. package/src/palettes/atlas.ts +77 -0
  75. package/src/palettes/blueprint.ts +73 -0
  76. package/src/palettes/color-utils.ts +58 -1
  77. package/src/palettes/index.ts +12 -3
  78. package/src/palettes/slate.ts +73 -0
  79. package/src/palettes/tidewater.ts +73 -0
  80. package/src/render.ts +8 -1
  81. package/src/tech-radar/renderer.ts +3 -0
  82. package/src/tech-radar/types.ts +3 -0
  83. package/src/utils/d3-types.ts +5 -0
  84. package/src/utils/legend-layout.ts +21 -4
  85. package/src/utils/legend-types.ts +7 -0
  86. package/src/utils/reserved-key-registry.ts +8 -3
  87. 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",
@@ -890,9 +893,7 @@ var init_reserved_key_registry = __esm({
890
893
  BOXES_AND_LINES_REGISTRY = staticRegistry([
891
894
  "color",
892
895
  "description",
893
- "width",
894
- "split",
895
- "fanout"
896
+ "value"
896
897
  ]);
897
898
  TIMELINE_REGISTRY = staticRegistry([
898
899
  "color",
@@ -1898,77 +1899,266 @@ function getSegmentColors(palette, count) {
1898
1899
  (_, i) => hslToHex(Math.round((startHue + i * step) % 360), avgS, avgL)
1899
1900
  );
1900
1901
  }
1902
+ function politicalTints(palette, count, isDark) {
1903
+ if (count <= 0) return [];
1904
+ const base = isDark ? palette.surface : palette.bg;
1905
+ const c = palette.colors;
1906
+ const swatches = [
1907
+ .../* @__PURE__ */ new Set([
1908
+ c.green,
1909
+ c.yellow,
1910
+ c.orange,
1911
+ c.purple,
1912
+ c.red,
1913
+ c.teal,
1914
+ c.cyan,
1915
+ c.blue
1916
+ ])
1917
+ ];
1918
+ const bands = isDark ? POLITICAL_TINT_BANDS.dark : POLITICAL_TINT_BANDS.light;
1919
+ const out = [];
1920
+ for (const pct of bands) {
1921
+ if (out.length >= count) break;
1922
+ for (const s of swatches) out.push(mix(s, base, pct));
1923
+ }
1924
+ return out.slice(0, count);
1925
+ }
1926
+ var POLITICAL_TINT_BANDS;
1901
1927
  var init_color_utils = __esm({
1902
1928
  "src/palettes/color-utils.ts"() {
1903
1929
  "use strict";
1930
+ POLITICAL_TINT_BANDS = {
1931
+ light: [32, 48, 64, 80],
1932
+ dark: [44, 58, 72, 86]
1933
+ };
1904
1934
  }
1905
1935
  });
1906
1936
 
1907
- // src/palettes/bold.ts
1908
- var boldPalette;
1909
- var init_bold = __esm({
1910
- "src/palettes/bold.ts"() {
1937
+ // src/palettes/atlas.ts
1938
+ var atlasPalette;
1939
+ var init_atlas = __esm({
1940
+ "src/palettes/atlas.ts"() {
1911
1941
  "use strict";
1912
1942
  init_registry();
1913
- boldPalette = {
1914
- id: "bold",
1915
- name: "Bold",
1943
+ atlasPalette = {
1944
+ id: "atlas",
1945
+ name: "Atlas",
1916
1946
  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",
1947
+ bg: "#f3ead3",
1948
+ // warm manila / parchment
1949
+ surface: "#ece0c0",
1950
+ // deeper paper (cards, panels)
1951
+ overlay: "#e8dab8",
1952
+ // popovers, dropdowns
1953
+ border: "#bcaa86",
1954
+ // muted sepia rule line
1955
+ text: "#463a26",
1956
+ // aged sepia-brown ink
1957
+ textMuted: "#7a6a4f",
1958
+ // faded annotation ink
1959
+ textOnFillLight: "#f7f1de",
1960
+ // parchment (light text on dark fills)
1961
+ textOnFillDark: "#3a2e1c",
1962
+ // deep ink (dark text on light fills)
1963
+ primary: "#5b7a99",
1964
+ // pull-down map ocean (steel-blue)
1965
+ secondary: "#7e9a6f",
1966
+ // lowland sage / celadon
1967
+ accent: "#b07f7c",
1968
+ // dusty rose
1969
+ destructive: "#b25a45",
1970
+ // brick / terracotta
1929
1971
  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"
1972
+ red: "#bf6a52",
1973
+ // terracotta brick
1974
+ orange: "#cf9a5c",
1975
+ // map tan / ochre
1976
+ yellow: "#cdb35e",
1977
+ // straw / muted lemon
1978
+ green: "#7e9a6f",
1979
+ // sage / celadon lowland
1980
+ blue: "#5b7a99",
1981
+ // steel-blue ocean
1982
+ purple: "#9a7fa6",
1983
+ // dusty lilac / mauve
1984
+ teal: "#6fa094",
1985
+ // muted seafoam
1986
+ cyan: "#79a7b5",
1987
+ // shallow-water blue
1988
+ gray: "#8a7d68",
1989
+ // warm taupe
1990
+ black: "#463a26",
1991
+ // ink
1992
+ white: "#ece0c0"
1993
+ // paper
1941
1994
  }
1942
1995
  },
1943
1996
  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",
1997
+ bg: "#1e2a33",
1998
+ // deep map ocean (night globe)
1999
+ surface: "#27353f",
2000
+ // raised ocean
2001
+ overlay: "#2e3d48",
2002
+ // popovers, dropdowns
2003
+ border: "#3d4f5c",
2004
+ // depth-contour line
2005
+ text: "#e8dcc0",
2006
+ // parchment ink, inverted
2007
+ textMuted: "#a89a7d",
2008
+ // faded label
2009
+ textOnFillLight: "#f7f1de",
2010
+ // parchment
2011
+ textOnFillDark: "#1a242c",
2012
+ // deep ocean ink
2013
+ primary: "#7ba0bf",
2014
+ // brighter ocean
2015
+ secondary: "#9bb588",
2016
+ // sage, lifted
2017
+ accent: "#cf9a96",
2018
+ // dusty rose, lifted
2019
+ destructive: "#c9745c",
2020
+ // brick, lifted
1956
2021
  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"
2022
+ red: "#cf7a60",
2023
+ // terracotta
2024
+ orange: "#d9a96a",
2025
+ // tan / ochre
2026
+ yellow: "#d8c074",
2027
+ // straw
2028
+ green: "#9bb588",
2029
+ // sage lowland
2030
+ blue: "#7ba0bf",
2031
+ // ocean
2032
+ purple: "#b59ac0",
2033
+ // lilac / mauve
2034
+ teal: "#85b3a6",
2035
+ // seafoam
2036
+ cyan: "#92bccb",
2037
+ // shallow-water blue
2038
+ gray: "#9a8d76",
2039
+ // warm taupe
2040
+ black: "#27353f",
2041
+ // raised ocean
2042
+ white: "#e8dcc0"
2043
+ // parchment
1968
2044
  }
1969
2045
  }
1970
2046
  };
1971
- registerPalette(boldPalette);
2047
+ registerPalette(atlasPalette);
2048
+ }
2049
+ });
2050
+
2051
+ // src/palettes/blueprint.ts
2052
+ var blueprintPalette;
2053
+ var init_blueprint = __esm({
2054
+ "src/palettes/blueprint.ts"() {
2055
+ "use strict";
2056
+ init_registry();
2057
+ blueprintPalette = {
2058
+ id: "blueprint",
2059
+ name: "Blueprint",
2060
+ light: {
2061
+ bg: "#f4f8fb",
2062
+ // pale drafting white (faint cyan)
2063
+ surface: "#e6eef4",
2064
+ // drafting panel
2065
+ overlay: "#dde9f1",
2066
+ // popovers, dropdowns
2067
+ border: "#aac3d6",
2068
+ // pale blue grid line
2069
+ text: "#123a5e",
2070
+ // blueprint navy ink
2071
+ textMuted: "#4f7390",
2072
+ // faint draft note
2073
+ textOnFillLight: "#f4f8fb",
2074
+ // drafting white
2075
+ textOnFillDark: "#0c2f4d",
2076
+ // deep blueprint navy
2077
+ primary: "#1f5e8c",
2078
+ // blueprint blue
2079
+ secondary: "#5b7d96",
2080
+ // steel
2081
+ accent: "#b08a3e",
2082
+ // draftsman's ochre highlight
2083
+ destructive: "#c0504d",
2084
+ // correction red
2085
+ colors: {
2086
+ red: "#c25a4e",
2087
+ // correction red
2088
+ orange: "#c2823e",
2089
+ // ochre
2090
+ yellow: "#c2a843",
2091
+ // pencil gold
2092
+ green: "#4f8a6b",
2093
+ // drafting green
2094
+ blue: "#1f5e8c",
2095
+ // blueprint blue
2096
+ purple: "#6f5e96",
2097
+ // indigo pencil
2098
+ teal: "#3a8a8a",
2099
+ // teal
2100
+ cyan: "#3f8fb5",
2101
+ // cyan
2102
+ gray: "#7e8e98",
2103
+ // graphite
2104
+ black: "#123a5e",
2105
+ // navy ink
2106
+ white: "#e6eef4"
2107
+ // panel
2108
+ }
2109
+ },
2110
+ dark: {
2111
+ bg: "#103a5e",
2112
+ // deep blueprint blue (cyanotype ground)
2113
+ surface: "#16466e",
2114
+ // raised sheet
2115
+ overlay: "#1c5180",
2116
+ // popovers, dropdowns
2117
+ border: "#3a6f96",
2118
+ // grid line
2119
+ text: "#eaf2f8",
2120
+ // chalk white
2121
+ textMuted: "#9fc0d6",
2122
+ // faint chalk note
2123
+ textOnFillLight: "#eaf2f8",
2124
+ // chalk white
2125
+ textOnFillDark: "#0c2f4d",
2126
+ // deep blueprint navy
2127
+ primary: "#7fb8d8",
2128
+ // chalk cyan
2129
+ secondary: "#9fb8c8",
2130
+ // pale steel
2131
+ accent: "#d8c27a",
2132
+ // chalk amber
2133
+ destructive: "#e08a7a",
2134
+ // chalk correction red
2135
+ colors: {
2136
+ red: "#e0907e",
2137
+ // chalk red
2138
+ orange: "#e0ab78",
2139
+ // chalk amber
2140
+ yellow: "#e3d089",
2141
+ // chalk gold
2142
+ green: "#93c79e",
2143
+ // chalk green
2144
+ blue: "#8ec3e0",
2145
+ // chalk cyan-blue
2146
+ purple: "#b6a6d8",
2147
+ // chalk indigo
2148
+ teal: "#84c7c2",
2149
+ // chalk teal
2150
+ cyan: "#9fd6e0",
2151
+ // chalk cyan
2152
+ gray: "#aebecb",
2153
+ // chalk graphite
2154
+ black: "#16466e",
2155
+ // raised sheet
2156
+ white: "#eaf2f8"
2157
+ // chalk white
2158
+ }
2159
+ }
2160
+ };
2161
+ registerPalette(blueprintPalette);
1972
2162
  }
1973
2163
  });
1974
2164
 
@@ -2465,6 +2655,120 @@ var init_rose_pine = __esm({
2465
2655
  }
2466
2656
  });
2467
2657
 
2658
+ // src/palettes/slate.ts
2659
+ var slatePalette;
2660
+ var init_slate = __esm({
2661
+ "src/palettes/slate.ts"() {
2662
+ "use strict";
2663
+ init_registry();
2664
+ slatePalette = {
2665
+ id: "slate",
2666
+ name: "Slate",
2667
+ light: {
2668
+ bg: "#ffffff",
2669
+ // clean slide white
2670
+ surface: "#f3f5f8",
2671
+ // light cool-gray panel
2672
+ overlay: "#eaeef3",
2673
+ // popovers, dropdowns
2674
+ border: "#d4dae1",
2675
+ // hairline rule
2676
+ text: "#1f2933",
2677
+ // near-black slate (softer than pure black)
2678
+ textMuted: "#5b6672",
2679
+ // secondary label
2680
+ textOnFillLight: "#ffffff",
2681
+ // light text on dark fills
2682
+ textOnFillDark: "#1f2933",
2683
+ // dark text on light fills
2684
+ primary: "#3b6ea5",
2685
+ // confident corporate blue
2686
+ secondary: "#5b6672",
2687
+ // slate gray
2688
+ accent: "#3a9188",
2689
+ // muted teal accent
2690
+ destructive: "#c0504d",
2691
+ // brick red
2692
+ colors: {
2693
+ red: "#c0504d",
2694
+ // brick
2695
+ orange: "#cc7a33",
2696
+ // muted amber
2697
+ yellow: "#c9a227",
2698
+ // gold (not neon)
2699
+ green: "#5b9357",
2700
+ // forest / sage
2701
+ blue: "#3b6ea5",
2702
+ // corporate blue
2703
+ purple: "#7d5ba6",
2704
+ // muted violet
2705
+ teal: "#3a9188",
2706
+ // teal
2707
+ cyan: "#4f96c4",
2708
+ // steel cyan
2709
+ gray: "#7e8a97",
2710
+ // cool gray
2711
+ black: "#1f2933",
2712
+ // slate ink
2713
+ white: "#f3f5f8"
2714
+ // panel
2715
+ }
2716
+ },
2717
+ dark: {
2718
+ bg: "#161b22",
2719
+ // deep slate (keynote dark)
2720
+ surface: "#202833",
2721
+ // raised panel
2722
+ overlay: "#29323e",
2723
+ // popovers, dropdowns
2724
+ border: "#38424f",
2725
+ // divider
2726
+ text: "#e6eaef",
2727
+ // off-white
2728
+ textMuted: "#9aa5b1",
2729
+ // secondary label
2730
+ textOnFillLight: "#ffffff",
2731
+ // light text on dark fills
2732
+ textOnFillDark: "#161b22",
2733
+ // dark text on light fills
2734
+ primary: "#5b9bd5",
2735
+ // lifted corporate blue
2736
+ secondary: "#8593a3",
2737
+ // slate gray, lifted
2738
+ accent: "#45b3a3",
2739
+ // teal, lifted
2740
+ destructive: "#e07b6e",
2741
+ // brick, lifted
2742
+ colors: {
2743
+ red: "#e07b6e",
2744
+ // brick
2745
+ orange: "#e0975a",
2746
+ // amber
2747
+ yellow: "#d9bd5a",
2748
+ // gold
2749
+ green: "#74b56e",
2750
+ // forest / sage
2751
+ blue: "#5b9bd5",
2752
+ // corporate blue
2753
+ purple: "#a585c9",
2754
+ // violet
2755
+ teal: "#45b3a3",
2756
+ // teal
2757
+ cyan: "#62b0d9",
2758
+ // steel cyan
2759
+ gray: "#95a1ae",
2760
+ // cool gray
2761
+ black: "#202833",
2762
+ // raised panel
2763
+ white: "#e6eaef"
2764
+ // off-white
2765
+ }
2766
+ }
2767
+ };
2768
+ registerPalette(slatePalette);
2769
+ }
2770
+ });
2771
+
2468
2772
  // src/palettes/solarized.ts
2469
2773
  var solarizedPalette;
2470
2774
  var init_solarized = __esm({
@@ -2560,6 +2864,120 @@ var init_solarized = __esm({
2560
2864
  }
2561
2865
  });
2562
2866
 
2867
+ // src/palettes/tidewater.ts
2868
+ var tidewaterPalette;
2869
+ var init_tidewater = __esm({
2870
+ "src/palettes/tidewater.ts"() {
2871
+ "use strict";
2872
+ init_registry();
2873
+ tidewaterPalette = {
2874
+ id: "tidewater",
2875
+ name: "Tidewater",
2876
+ light: {
2877
+ bg: "#eceff0",
2878
+ // weathered sea-mist paper
2879
+ surface: "#e0e4e3",
2880
+ // worn deck panel
2881
+ overlay: "#dadfdf",
2882
+ // popovers, dropdowns
2883
+ border: "#a9b2b3",
2884
+ // muted slate rule
2885
+ text: "#18313f",
2886
+ // ship's-log navy ink
2887
+ textMuted: "#51636b",
2888
+ // faded log entry
2889
+ textOnFillLight: "#f3f5f3",
2890
+ // weathered white
2891
+ textOnFillDark: "#162c38",
2892
+ // deep navy
2893
+ primary: "#1f4e6b",
2894
+ // deep-sea navy
2895
+ secondary: "#b08a4f",
2896
+ // rope / manila tan
2897
+ accent: "#c69a3e",
2898
+ // brass
2899
+ destructive: "#c1433a",
2900
+ // signal-flag red
2901
+ colors: {
2902
+ red: "#c1433a",
2903
+ // signal-flag red
2904
+ orange: "#cc7a38",
2905
+ // weathered amber
2906
+ yellow: "#d6bf5a",
2907
+ // brass gold
2908
+ green: "#4f8a6b",
2909
+ // sea-glass green
2910
+ blue: "#1f4e6b",
2911
+ // deep-sea navy
2912
+ purple: "#6a5a8c",
2913
+ // twilight harbor
2914
+ teal: "#3d8c8c",
2915
+ // sea-glass teal
2916
+ cyan: "#4f9bb5",
2917
+ // shallow water
2918
+ gray: "#8a8d86",
2919
+ // driftwood gray
2920
+ black: "#18313f",
2921
+ // navy ink
2922
+ white: "#e0e4e3"
2923
+ // deck panel
2924
+ }
2925
+ },
2926
+ dark: {
2927
+ bg: "#0f2230",
2928
+ // night-harbor deep sea
2929
+ surface: "#16303f",
2930
+ // raised hull
2931
+ overlay: "#1d3a4a",
2932
+ // popovers, dropdowns
2933
+ border: "#2c4856",
2934
+ // rigging line
2935
+ text: "#e6ebe8",
2936
+ // weathered white
2937
+ textMuted: "#9aaab0",
2938
+ // faded label
2939
+ textOnFillLight: "#f3f5f3",
2940
+ // weathered white
2941
+ textOnFillDark: "#0f2230",
2942
+ // deep sea
2943
+ primary: "#4f9bc4",
2944
+ // lifted sea blue
2945
+ secondary: "#c9a46a",
2946
+ // rope tan, lifted
2947
+ accent: "#d9b25a",
2948
+ // brass, lifted
2949
+ destructive: "#e06a5e",
2950
+ // signal red, lifted
2951
+ colors: {
2952
+ red: "#e06a5e",
2953
+ // signal-flag red
2954
+ orange: "#df9a52",
2955
+ // amber
2956
+ yellow: "#e0c662",
2957
+ // brass gold
2958
+ green: "#6fb58c",
2959
+ // sea-glass green
2960
+ blue: "#4f9bc4",
2961
+ // sea blue
2962
+ purple: "#9486bf",
2963
+ // twilight harbor
2964
+ teal: "#5cb0ac",
2965
+ // sea-glass teal
2966
+ cyan: "#62b4cf",
2967
+ // shallow water
2968
+ gray: "#9aa39c",
2969
+ // driftwood gray
2970
+ black: "#16303f",
2971
+ // raised hull
2972
+ white: "#e6ebe8"
2973
+ // weathered white
2974
+ }
2975
+ }
2976
+ };
2977
+ registerPalette(tidewaterPalette);
2978
+ }
2979
+ });
2980
+
2563
2981
  // src/palettes/tokyo-night.ts
2564
2982
  var tokyoNightPalette;
2565
2983
  var init_tokyo_night = __esm({
@@ -2835,7 +3253,8 @@ var init_monokai = __esm({
2835
3253
  // src/palettes/index.ts
2836
3254
  var palettes_exports = {};
2837
3255
  __export(palettes_exports, {
2838
- boldPalette: () => boldPalette,
3256
+ atlasPalette: () => atlasPalette,
3257
+ blueprintPalette: () => blueprintPalette,
2839
3258
  catppuccinPalette: () => catppuccinPalette,
2840
3259
  contrastText: () => contrastText,
2841
3260
  draculaPalette: () => draculaPalette,
@@ -2856,7 +3275,9 @@ __export(palettes_exports, {
2856
3275
  rosePinePalette: () => rosePinePalette,
2857
3276
  shade: () => shade,
2858
3277
  shapeFill: () => shapeFill,
3278
+ slatePalette: () => slatePalette,
2859
3279
  solarizedPalette: () => solarizedPalette,
3280
+ tidewaterPalette: () => tidewaterPalette,
2860
3281
  tint: () => tint,
2861
3282
  tokyoNightPalette: () => tokyoNightPalette
2862
3283
  });
@@ -2866,17 +3287,21 @@ var init_palettes = __esm({
2866
3287
  "use strict";
2867
3288
  init_registry();
2868
3289
  init_color_utils();
2869
- init_bold();
3290
+ init_atlas();
3291
+ init_blueprint();
2870
3292
  init_catppuccin();
2871
3293
  init_gruvbox();
2872
3294
  init_nord();
2873
3295
  init_one_dark();
2874
3296
  init_rose_pine();
3297
+ init_slate();
2875
3298
  init_solarized();
3299
+ init_tidewater();
2876
3300
  init_tokyo_night();
2877
3301
  init_dracula();
2878
3302
  init_monokai();
2879
- init_bold();
3303
+ init_atlas();
3304
+ init_blueprint();
2880
3305
  init_catppuccin();
2881
3306
  init_dracula();
2882
3307
  init_gruvbox();
@@ -2884,9 +3309,15 @@ var init_palettes = __esm({
2884
3309
  init_nord();
2885
3310
  init_one_dark();
2886
3311
  init_rose_pine();
3312
+ init_slate();
2887
3313
  init_solarized();
3314
+ init_tidewater();
2888
3315
  init_tokyo_night();
2889
3316
  palettes = {
3317
+ atlas: atlasPalette,
3318
+ blueprint: blueprintPalette,
3319
+ slate: slatePalette,
3320
+ tidewater: tidewaterPalette,
2890
3321
  nord: nordPalette,
2891
3322
  catppuccin: catppuccinPalette,
2892
3323
  solarized: solarizedPalette,
@@ -2895,8 +3326,7 @@ var init_palettes = __esm({
2895
3326
  oneDark: oneDarkPalette,
2896
3327
  rosePine: rosePinePalette,
2897
3328
  dracula: draculaPalette,
2898
- monokai: monokaiPalette,
2899
- bold: boldPalette
3329
+ monokai: monokaiPalette
2900
3330
  };
2901
3331
  }
2902
3332
  });
@@ -3406,6 +3836,9 @@ function controlsGroupCapsuleWidth(toggles) {
3406
3836
  }
3407
3837
  return w;
3408
3838
  }
3839
+ function isAppHostedControls(config, isExport) {
3840
+ return !isExport && config.controlsHost === "app" && !!config.controlsGroup && config.controlsGroup.toggles.length > 0;
3841
+ }
3409
3842
  function buildControlsGroupLayout(config, state) {
3410
3843
  const cg = config.controlsGroup;
3411
3844
  if (!cg || cg.toggles.length === 0) return void 0;
@@ -3459,6 +3892,7 @@ function buildControlsGroupLayout(config, state) {
3459
3892
  function computeLegendLayout(config, state, containerWidth) {
3460
3893
  const { groups, controls: configControls, mode } = config;
3461
3894
  const isExport = mode === "export";
3895
+ const gated = isAppHostedControls(config, isExport);
3462
3896
  const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
3463
3897
  if (isExport && !activeGroupName) {
3464
3898
  return {
@@ -3469,7 +3903,7 @@ function computeLegendLayout(config, state, containerWidth) {
3469
3903
  pills: []
3470
3904
  };
3471
3905
  }
3472
- const controlsGroupLayout = isExport ? void 0 : buildControlsGroupLayout(config, state);
3906
+ const controlsGroupLayout = isExport || gated ? void 0 : buildControlsGroupLayout(config, state);
3473
3907
  const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0 || !!g.gradient);
3474
3908
  if (visibleGroups.length === 0 && (!configControls || configControls.length === 0) && !controlsGroupLayout) {
3475
3909
  return {
@@ -8371,8 +8805,8 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8371
8805
  const pt = points[i];
8372
8806
  const ptSize = pt.size ?? symbolSize;
8373
8807
  const minGap = ptSize / 2 + 4;
8374
- const labelWidth = pt.name.length * fontSize * 0.6 + 8;
8375
- const labelX = pt.px - labelWidth / 2;
8808
+ const labelWidth2 = pt.name.length * fontSize * 0.6 + 8;
8809
+ const labelX = pt.px - labelWidth2 / 2;
8376
8810
  let bestLabelY = 0;
8377
8811
  let bestOffset = Infinity;
8378
8812
  let placed = false;
@@ -8384,7 +8818,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8384
8818
  const candidate = {
8385
8819
  x: labelX,
8386
8820
  y: labelY,
8387
- w: labelWidth,
8821
+ w: labelWidth2,
8388
8822
  h: labelHeight
8389
8823
  };
8390
8824
  let collision = false;
@@ -8426,7 +8860,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8426
8860
  const labelRect = {
8427
8861
  x: labelX,
8428
8862
  y: bestLabelY,
8429
- w: labelWidth,
8863
+ w: labelWidth2,
8430
8864
  h: labelHeight
8431
8865
  };
8432
8866
  placedLabels.push(labelRect);
@@ -8462,7 +8896,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8462
8896
  shape: {
8463
8897
  x: labelX - bgPad,
8464
8898
  y: bestLabelY - bgPad,
8465
- width: labelWidth + bgPad * 2,
8899
+ width: labelWidth2 + bgPad * 2,
8466
8900
  height: labelHeight + bgPad * 2
8467
8901
  },
8468
8902
  style: { fill: bg },
@@ -15898,10 +16332,6 @@ function parseMap(content) {
15898
16332
  handleTag(trimmed, lineNumber);
15899
16333
  continue;
15900
16334
  }
15901
- if ((firstWord === "muted" || firstWord === "natural") && trimmed === firstWord) {
15902
- handleDirective(firstWord, "", lineNumber);
15903
- continue;
15904
- }
15905
16335
  if (DIRECTIVE_SET.has(firstWord) && !trimmed.slice(firstWord.length).trimStart().startsWith(":")) {
15906
16336
  handleDirective(
15907
16337
  firstWord,
@@ -15948,24 +16378,6 @@ function parseMap(content) {
15948
16378
  pushWarning(line12, `Duplicate directive "${key}" \u2014 last value wins.`);
15949
16379
  };
15950
16380
  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
16381
  case "region-metric": {
15970
16382
  dup(d.regionMetric);
15971
16383
  const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
@@ -15981,91 +16393,43 @@ function parseMap(content) {
15981
16393
  dup(d.flowMetric);
15982
16394
  d.flowMetric = value;
15983
16395
  break;
15984
- case "scale":
15985
- dup(d.scale);
15986
- {
15987
- const s = parseScale(value, line12);
15988
- if (s) d.scale = s;
15989
- }
15990
- break;
15991
- case "region-labels":
15992
- dup(d.regionLabels);
15993
- if (value && !["full", "abbrev", "off"].includes(value))
15994
- pushWarning(
15995
- line12,
15996
- `Unknown region-labels "${value}" (expected full | abbrev | off).`
15997
- );
15998
- d.regionLabels = value;
15999
- break;
16000
- case "poi-labels":
16001
- dup(d.poiLabels);
16002
- if (value && !["off", "auto", "all"].includes(value))
16003
- pushWarning(
16004
- line12,
16005
- `Unknown poi-labels "${value}" (expected off | auto | all).`
16006
- );
16007
- d.poiLabels = value;
16008
- break;
16009
- case "default-country":
16010
- dup(d.defaultCountry);
16011
- d.defaultCountry = value;
16012
- break;
16013
- case "default-state":
16014
- dup(d.defaultState);
16015
- d.defaultState = value;
16396
+ case "locale":
16397
+ dup(d.locale);
16398
+ d.locale = value;
16016
16399
  break;
16017
16400
  case "active-tag":
16018
16401
  dup(d.activeTag);
16019
16402
  d.activeTag = value;
16020
16403
  break;
16404
+ case "caption":
16405
+ dup(d.caption);
16406
+ d.caption = value;
16407
+ break;
16408
+ // ── Cosmetic `no-*` opt-outs: bare flags, idempotent (mirror `no-legend`,
16409
+ // no dup warning); each defaults the feature ON when absent. ──
16021
16410
  case "no-legend":
16022
16411
  d.noLegend = true;
16023
16412
  break;
16024
- case "no-insets":
16025
- d.noInsets = true;
16413
+ case "no-coastline":
16414
+ d.noCoastline = true;
16026
16415
  break;
16027
- case "relief":
16028
- d.relief = true;
16416
+ case "no-relief":
16417
+ d.noRelief = true;
16029
16418
  break;
16030
- case "muted":
16031
- case "natural":
16032
- if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
16033
- pushWarning(
16034
- line12,
16035
- `Conflicting basemap dress \u2014 "${d.basemapStyle}" then "${key}"; last wins.`
16036
- );
16037
- d.basemapStyle = key;
16419
+ case "no-context-labels":
16420
+ d.noContextLabels = true;
16038
16421
  break;
16039
- case "subtitle":
16040
- dup(d.subtitle);
16041
- d.subtitle = value;
16422
+ case "no-region-labels":
16423
+ d.noRegionLabels = true;
16042
16424
  break;
16043
- case "caption":
16044
- dup(d.caption);
16045
- d.caption = value;
16425
+ case "no-poi-labels":
16426
+ d.noPoiLabels = true;
16427
+ break;
16428
+ case "no-colorize":
16429
+ d.noColorize = true;
16046
16430
  break;
16047
16431
  }
16048
16432
  }
16049
- function parseScale(value, line12) {
16050
- const toks = value.split(/\s+/).filter(Boolean);
16051
- const min = Number(toks[0]);
16052
- const max = Number(toks[1]);
16053
- if (!Number.isFinite(min) || !Number.isFinite(max)) {
16054
- pushError(line12, `scale requires numeric <min> <max> (got "${value}").`);
16055
- return null;
16056
- }
16057
- const scale = { min, max };
16058
- if (toks[2] === "center") {
16059
- const c = Number(toks[3]);
16060
- if (Number.isFinite(c)) scale.center = c;
16061
- else
16062
- pushError(
16063
- line12,
16064
- `scale center requires a number (got "${toks[3] ?? ""}").`
16065
- );
16066
- }
16067
- return scale;
16068
- }
16069
16433
  function handleTag(trimmed, line12) {
16070
16434
  const m = matchTagBlockHeading(trimmed);
16071
16435
  if (!m) {
@@ -16265,13 +16629,15 @@ function parseMap(content) {
16265
16629
  pushError(line12, `Edge has an empty endpoint: "${trimmed}".`);
16266
16630
  continue;
16267
16631
  }
16268
- const meta = k === links.length - 1 ? lastSplit.meta : {};
16632
+ const isLast = k === links.length - 1;
16633
+ const meta = isLast ? lastSplit.meta : {};
16634
+ const style = links[k].style === "arc" ? "arc" : "straight";
16269
16635
  edges.push({
16270
16636
  from,
16271
16637
  to,
16272
16638
  ...links[k].label !== void 0 && { label: links[k].label },
16273
16639
  directed: links[k].directed,
16274
- style: links[k].style,
16640
+ style,
16275
16641
  meta,
16276
16642
  lineNumber: line12
16277
16643
  });
@@ -16357,22 +16723,19 @@ var init_parser12 = __esm({
16357
16723
  LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
16358
16724
  AT_RE = /(^|[\s,])at\s*:/i;
16359
16725
  DIRECTIVE_SET = /* @__PURE__ */ new Set([
16360
- "region",
16361
- "projection",
16362
16726
  "region-metric",
16363
16727
  "poi-metric",
16364
16728
  "flow-metric",
16365
- "scale",
16366
- "region-labels",
16367
- "poi-labels",
16368
- "default-country",
16369
- "default-state",
16729
+ "locale",
16370
16730
  "active-tag",
16731
+ "caption",
16371
16732
  "no-legend",
16372
- "no-insets",
16373
- "relief",
16374
- "subtitle",
16375
- "caption"
16733
+ "no-coastline",
16734
+ "no-relief",
16735
+ "no-context-labels",
16736
+ "no-region-labels",
16737
+ "no-poi-labels",
16738
+ "no-colorize"
16376
16739
  ]);
16377
16740
  }
16378
16741
  });
@@ -16550,6 +16913,21 @@ function parseBoxesAndLines(content) {
16550
16913
  }
16551
16914
  continue;
16552
16915
  }
16916
+ if (!contentStarted) {
16917
+ const metricMatch = trimmed.match(/^box-metric\s+(.+)$/i);
16918
+ if (metricMatch) {
16919
+ const { label, colorName } = peelTrailingColorName(
16920
+ metricMatch[1].trim()
16921
+ );
16922
+ result.boxMetric = label;
16923
+ if (colorName !== void 0) result.boxMetricColor = colorName;
16924
+ continue;
16925
+ }
16926
+ if (/^show-values$/i.test(trimmed)) {
16927
+ result.showValues = true;
16928
+ continue;
16929
+ }
16930
+ }
16553
16931
  if (!contentStarted) {
16554
16932
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
16555
16933
  if (optMatch) {
@@ -16928,6 +17306,19 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
16928
17306
  description = [metadata["description"]];
16929
17307
  delete metadata["description"];
16930
17308
  }
17309
+ let value;
17310
+ if (metadata["value"] !== void 0) {
17311
+ const raw = metadata["value"];
17312
+ const num = Number(raw);
17313
+ if (Number.isFinite(num)) {
17314
+ value = num;
17315
+ } else {
17316
+ diagnostics.push(
17317
+ makeDgmoError(lineNum, `value must be a number (got "${raw}")`, "error")
17318
+ );
17319
+ }
17320
+ delete metadata["value"];
17321
+ }
16931
17322
  if (split.alias) {
16932
17323
  nameAliasMap?.set(normalizeName(split.alias), label);
16933
17324
  }
@@ -16936,7 +17327,8 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
16936
17327
  label,
16937
17328
  lineNumber: lineNum,
16938
17329
  metadata,
16939
- ...description !== void 0 && { description }
17330
+ ...description !== void 0 && { description },
17331
+ ...value !== void 0 && { value }
16940
17332
  };
16941
17333
  }
16942
17334
  function splitTargetAndMeta(rest, metaAliasMap) {
@@ -24294,8 +24686,8 @@ function renderKanban(container, parsed, palette, isDark, options) {
24294
24686
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24295
24687
  for (const meta of tagMeta) {
24296
24688
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(`${meta.label}: `);
24297
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24298
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24689
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24690
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24299
24691
  metaY += sCardMetaLineHeight;
24300
24692
  }
24301
24693
  for (const detail of card.details) {
@@ -24639,8 +25031,8 @@ function renderSwimlaneCard(parent, cardLayout, tagGroups, activeTagGroup, palet
24639
25031
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24640
25032
  for (const meta of tagMeta) {
24641
25033
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", palette.textMuted).text(`${meta.label}: `);
24642
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24643
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
25034
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
25035
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24644
25036
  metaY += sCardMetaLineHeight;
24645
25037
  }
24646
25038
  for (const detail of card.details) {
@@ -25474,8 +25866,8 @@ function classifyEREntities(tables, relationships) {
25474
25866
  }
25475
25867
  }
25476
25868
  const mmParticipants = /* @__PURE__ */ new Set();
25477
- for (const [id, neighbors] of tableStarNeighbors) {
25478
- if (neighbors.size >= 2) mmParticipants.add(id);
25869
+ for (const [id, neighbors2] of tableStarNeighbors) {
25870
+ if (neighbors2.size >= 2) mmParticipants.add(id);
25479
25871
  }
25480
25872
  const indegreeValues = Object.values(indegreeMap);
25481
25873
  const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
@@ -26058,7 +26450,18 @@ function fitLabelToHeader(label, nodeWidth, maxLines) {
26058
26450
  const truncated = label.length > maxChars ? label.slice(0, maxChars - 1) + "\u2026" : label;
26059
26451
  return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
26060
26452
  }
26061
- function nodeColors(node, tagGroups, activeGroupName, palette, isDark, solid) {
26453
+ function nodeColors(node, tagGroups, activeGroupName, palette, isDark, value, solid) {
26454
+ const neutralFill = mix(palette.bg, palette.text, isDark ? 90 : 95);
26455
+ if (value.active) {
26456
+ const fill3 = node.value !== void 0 ? value.fillForValue(node.value) : neutralFill;
26457
+ const stroke3 = value.hue;
26458
+ const text2 = contrastText(
26459
+ fill3,
26460
+ palette.textOnFillLight,
26461
+ palette.textOnFillDark
26462
+ );
26463
+ return { fill: fill3, stroke: stroke3, text: text2 };
26464
+ }
26062
26465
  const tagColor = resolveTagColor(
26063
26466
  node.metadata,
26064
26467
  [...tagGroups],
@@ -26148,7 +26551,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26148
26551
  controlsExpanded,
26149
26552
  onToggleDescriptions,
26150
26553
  onToggleControlsExpand,
26151
- exportMode = false
26554
+ exportMode = false,
26555
+ controlsHost
26152
26556
  } = options ?? {};
26153
26557
  d3Selection6.select(container).selectAll(":not([data-d3-tooltip])").remove();
26154
26558
  const width = exportDims?.width ?? container.clientWidth;
@@ -26166,21 +26570,65 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26166
26570
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26167
26571
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26168
26572
  const sTitleY = sctx.structural(TITLE_Y);
26169
- const sLegendHeight = sctx.structural(
26573
+ const nodeValues = parsed.nodes.filter((n) => n.value !== void 0).map((n) => n.value);
26574
+ const hasRamp = nodeValues.length > 0;
26575
+ const allNonNegative = hasRamp && nodeValues.every((v) => v >= 0);
26576
+ const rampMin = allNonNegative ? 0 : Math.min(...nodeValues);
26577
+ const rampMax = Math.max(...nodeValues);
26578
+ const rampHue = resolveColor(parsed.boxMetricColor ?? "", palette) ?? palette.primary;
26579
+ const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
26580
+ const fillForValue = (v) => {
26581
+ const t = rampMax > rampMin ? (v - rampMin) / (rampMax - rampMin) : 1;
26582
+ const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
26583
+ return mix(rampHue, rampBase, pct);
26584
+ };
26585
+ const VALUE_NAME = hasRamp ? parsed.boxMetric?.trim() || "Value" : null;
26586
+ const matchColorGroup = (v) => {
26587
+ const lv = v.trim().toLowerCase();
26588
+ if (lv === "none") return null;
26589
+ const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
26590
+ if (tg) return tg.name;
26591
+ if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
26592
+ return v;
26593
+ };
26594
+ const override = activeTagGroup;
26595
+ let activeGroup;
26596
+ if (override !== void 0) {
26597
+ activeGroup = override === null ? null : matchColorGroup(override);
26598
+ } else if (parsed.options["active-tag"] !== void 0) {
26599
+ activeGroup = matchColorGroup(parsed.options["active-tag"]);
26600
+ } else {
26601
+ activeGroup = VALUE_NAME ?? (parsed.tagGroups.length > 0 ? parsed.tagGroups[0].name : null);
26602
+ }
26603
+ const activeIsValue = VALUE_NAME !== null && activeGroup === VALUE_NAME;
26604
+ const valueGroup = VALUE_NAME !== null ? {
26605
+ name: VALUE_NAME,
26606
+ entries: [],
26607
+ gradient: {
26608
+ min: rampMin,
26609
+ max: rampMax,
26610
+ hue: rampHue,
26611
+ base: rampBase
26612
+ }
26613
+ } : null;
26614
+ const legendGroups = [
26615
+ ...valueGroup ? [valueGroup] : [],
26616
+ ...parsed.tagGroups
26617
+ ];
26618
+ const reserveHasDescriptions = parsed.nodes.some(
26619
+ (n) => n.description && n.description.length > 0
26620
+ );
26621
+ const willRenderLegend = legendGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26622
+ const sLegendHeight = willRenderLegend ? sctx.structural(
26170
26623
  getMaxLegendReservedHeight(
26171
26624
  {
26172
- groups: parsed.tagGroups,
26625
+ groups: legendGroups,
26173
26626
  position: { placement: "top-center", titleRelation: "below-title" },
26174
26627
  mode: exportMode ? "export" : "preview"
26175
26628
  },
26176
26629
  width
26177
26630
  )
26178
- );
26179
- const activeGroup = resolveActiveTagGroup(
26180
- parsed.tagGroups,
26181
- parsed.options["active-tag"],
26182
- activeTagGroup
26183
- );
26631
+ ) : 0;
26184
26632
  const hidden = hiddenTagValues ?? parsed.initialHiddenTagValues;
26185
26633
  const nodeMap = /* @__PURE__ */ new Map();
26186
26634
  for (const node of parsed.nodes) nodeMap.set(node.label, node);
@@ -26191,7 +26639,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26191
26639
  const hasAnyDescriptions = parsed.nodes.some(
26192
26640
  (n) => n.description && n.description.length > 0
26193
26641
  );
26194
- const needsLegend = parsed.tagGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26642
+ const needsLegend = legendGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26195
26643
  const legendH = needsLegend ? sLegendHeight + 8 : 0;
26196
26644
  const groupLabelsSet = new Set(layout.groups.map((g) => g.label));
26197
26645
  let labelZoneExtension = 0;
@@ -26397,12 +26845,16 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26397
26845
  activeGroup,
26398
26846
  palette,
26399
26847
  isDark,
26848
+ { active: activeIsValue, hue: rampHue, fillForValue },
26400
26849
  parsed.options["solid-fill"] === "on"
26401
26850
  );
26402
26851
  const nodeG = diagramG.append("g").attr("class", "bl-node").attr("transform", `translate(${ln.x},${ln.y})`).attr("data-line-number", node.lineNumber).attr("data-node-id", node.label).style("cursor", onClickItem ? "pointer" : "default").style("--bl-node-stroke", colors.stroke);
26403
26852
  for (const [key, val] of Object.entries(node.metadata)) {
26404
26853
  nodeG.attr(`data-tag-${key.toLowerCase()}`, val.toLowerCase());
26405
26854
  }
26855
+ if (node.value !== void 0) {
26856
+ nodeG.attr("data-value", node.value);
26857
+ }
26406
26858
  if (onClickItem) {
26407
26859
  nodeG.on("click", (event) => {
26408
26860
  const target = event.target;
@@ -26486,14 +26938,30 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26486
26938
  nodeG.append("text").attr("x", 0).attr("y", -totalH / 2 + lineH / 2 + li * lineH).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", fitted.fontSize).attr("font-weight", "600").attr("fill", colors.text).text(fitted.lines[li]);
26487
26939
  }
26488
26940
  }
26941
+ if (parsed.showValues && node.value !== void 0) {
26942
+ const valueText = String(node.value);
26943
+ const descShown = !!(desc && desc.length > 0 && !hideDescriptions);
26944
+ if (descShown) {
26945
+ const padX = 6;
26946
+ const padY = 5;
26947
+ const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
26948
+ const bh = VALUE_FONT_SIZE + 4;
26949
+ const bx = ln.width / 2 - bw - 4;
26950
+ const by = -ln.height / 2 + 4;
26951
+ nodeG.append("rect").attr("x", bx).attr("y", by).attr("width", bw).attr("height", bh).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.85);
26952
+ nodeG.append("text").attr("class", "bl-node-value").attr("x", bx + bw - padX).attr("y", by + padY).attr("text-anchor", "end").attr("dominant-baseline", "central").attr("font-size", VALUE_FONT_SIZE).attr("font-weight", "600").attr("fill", palette.textMuted).text(valueText);
26953
+ } else {
26954
+ nodeG.append("text").attr("class", "bl-node-value").attr("x", 0).attr("y", ln.height / 2 - VALUE_FONT_SIZE).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", VALUE_FONT_SIZE).attr("font-weight", "600").attr("fill", colors.text).attr("opacity", 0.8).text(valueText);
26955
+ }
26956
+ }
26489
26957
  }
26490
26958
  const hasDescriptions = parsed.nodes.some(
26491
26959
  (n) => n.description && n.description.length > 0
26492
26960
  );
26493
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions;
26961
+ const hasLegend = legendGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26494
26962
  if (hasLegend) {
26495
26963
  let controlsGroup;
26496
- if (hasDescriptions && onToggleDescriptions) {
26964
+ if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
26497
26965
  controlsGroup = {
26498
26966
  toggles: [
26499
26967
  {
@@ -26508,10 +26976,17 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26508
26976
  };
26509
26977
  }
26510
26978
  const legendConfig = {
26511
- groups: parsed.tagGroups,
26979
+ groups: legendGroups,
26512
26980
  position: { placement: "top-center", titleRelation: "below-title" },
26513
26981
  mode: exportMode ? "export" : "preview",
26514
- ...controlsGroup !== void 0 && { controlsGroup }
26982
+ // Keep inactive sibling tag groups visible as collapsed pills so the user
26983
+ // can click one to flip the active colouring dimension (preview only —
26984
+ // export shows just the active group). Without this, declaring a second
26985
+ // tag group (e.g. Team) leaves it invisible whenever another group is
26986
+ // active. The app's BoxesAndLinesPreview already wires pill clicks.
26987
+ showInactivePills: true,
26988
+ ...controlsGroup !== void 0 && { controlsGroup },
26989
+ ...controlsHost !== void 0 && { controlsHost }
26515
26990
  };
26516
26991
  const legendState = {
26517
26992
  activeGroup,
@@ -26556,7 +27031,7 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
26556
27031
  }
26557
27032
  });
26558
27033
  }
26559
- var DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, DESC_FONT_SIZE, DESC_LINE_HEIGHT, MAX_DESC_LINES, CHAR_WIDTH_RATIO2, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, GROUP_LABEL_ZONE, lineGeneratorLR, lineGeneratorTB;
27034
+ var DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, DESC_FONT_SIZE, DESC_LINE_HEIGHT, MAX_DESC_LINES, CHAR_WIDTH_RATIO2, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, GROUP_LABEL_ZONE, RAMP_FLOOR, VALUE_FONT_SIZE, lineGeneratorLR, lineGeneratorTB;
26560
27035
  var init_renderer6 = __esm({
26561
27036
  "src/boxes-and-lines/renderer.ts"() {
26562
27037
  "use strict";
@@ -26565,6 +27040,7 @@ var init_renderer6 = __esm({
26565
27040
  init_legend_layout();
26566
27041
  init_title_constants();
26567
27042
  init_color_utils();
27043
+ init_colors();
26568
27044
  init_tag_groups();
26569
27045
  init_inline_markdown();
26570
27046
  init_wrapped_desc();
@@ -26587,6 +27063,8 @@ var init_renderer6 = __esm({
26587
27063
  GROUP_RX = 8;
26588
27064
  GROUP_LABEL_FONT_SIZE = 14;
26589
27065
  GROUP_LABEL_ZONE = 32;
27066
+ RAMP_FLOOR = 15;
27067
+ VALUE_FONT_SIZE = 11;
26590
27068
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
26591
27069
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
26592
27070
  }
@@ -27758,8 +28236,9 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27758
28236
  const containerHeight = exportDims?.height ?? (container.getBoundingClientRect().height || 600);
27759
28237
  d3Selection7.select(container).selectAll("*").remove();
27760
28238
  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);
28239
+ const appHosted = options?.controlsHost === "app";
27761
28240
  const hasControls = !!options?.onToggleColorByDepth || !!options?.onToggleDescriptions;
27762
- const hasLegend = parsed.tagGroups.length > 0 || hasControls;
28241
+ const hasLegend = parsed.tagGroups.length > 0 || hasControls && !appHosted;
27763
28242
  const fixedLegend = !isExport && hasLegend;
27764
28243
  const legendReserve = fixedLegend ? getMaxLegendReservedHeight(
27765
28244
  {
@@ -27853,7 +28332,10 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27853
28332
  }),
27854
28333
  position: { placement: "top-center", titleRelation: "below-title" },
27855
28334
  mode: options?.exportMode ? "export" : "preview",
27856
- ...controlsToggles !== void 0 && { controlsGroup: controlsToggles }
28335
+ ...controlsToggles !== void 0 && { controlsGroup: controlsToggles },
28336
+ ...options?.controlsHost !== void 0 && {
28337
+ controlsHost: options.controlsHost
28338
+ }
27857
28339
  };
27858
28340
  const legendState = {
27859
28341
  activeGroup: options?.colorByDepth ? null : activeTagGroup !== void 0 ? activeTagGroup : parsed.options["active-tag"] ?? null,
@@ -28296,8 +28778,8 @@ function computeFieldAlignX(children) {
28296
28778
  for (const child of children) {
28297
28779
  if (child.metadata["_labelField"] === "true" && child.children.length >= 2) {
28298
28780
  const labelEl = child.children[0];
28299
- const labelWidth = labelEl.label.length * CHAR_WIDTH5;
28300
- maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
28781
+ const labelWidth2 = labelEl.label.length * CHAR_WIDTH5;
28782
+ maxLabelWidth = Math.max(maxLabelWidth, labelWidth2);
28301
28783
  labelFieldCount++;
28302
28784
  }
28303
28785
  }
@@ -33262,7 +33744,7 @@ function hasRoles(node) {
33262
33744
  function computeNodeWidth2(node, expanded, options) {
33263
33745
  const badgeVal = node.computedConcurrentInvocations === 0 && node.computedInstances > 1 ? node.computedInstances : 0;
33264
33746
  const badgeLen = badgeVal > 0 ? `${badgeVal}x`.length + 2 : 0;
33265
- const labelWidth = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33747
+ const labelWidth2 = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33266
33748
  const allKeys = [];
33267
33749
  if (node.computedRps > 0) allKeys.push("RPS");
33268
33750
  if (expanded) {
@@ -33306,7 +33788,7 @@ function computeNodeWidth2(node, expanded, options) {
33306
33788
  allKeys.push("overflow");
33307
33789
  }
33308
33790
  }
33309
- if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth);
33791
+ if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth2);
33310
33792
  const maxKeyLen = Math.max(...allKeys.map((k) => k.length));
33311
33793
  let maxRowWidth = 0;
33312
33794
  if (node.computedRps > 0) {
@@ -33394,7 +33876,7 @@ function computeNodeWidth2(node, expanded, options) {
33394
33876
  truncated.length * META_CHAR_WIDTH3 + PADDING_X3
33395
33877
  );
33396
33878
  }
33397
- return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20, descWidth);
33879
+ return Math.max(MIN_NODE_WIDTH2, labelWidth2, maxRowWidth + 20, descWidth);
33398
33880
  }
33399
33881
  function computeNodeHeight2(node, expanded, options) {
33400
33882
  const propCount = countDisplayProps(node, expanded, options);
@@ -34944,8 +35426,9 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
34944
35426
  }
34945
35427
  return groups;
34946
35428
  }
34947
- function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false) {
35429
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false, controlsHost) {
34948
35430
  if (legendGroups.length === 0 && !playback) return;
35431
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34949
35432
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
34950
35433
  if (activeGroup) {
34951
35434
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
@@ -34954,14 +35437,29 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34954
35437
  name: g.name,
34955
35438
  entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
34956
35439
  }));
34957
- if (playback) {
35440
+ if (playback && !appHostedPlayback) {
34958
35441
  allGroups.push({ name: "Playback", entries: [] });
34959
35442
  }
34960
35443
  const legendConfig = {
34961
35444
  groups: allGroups,
34962
35445
  position: { placement: "top-center", titleRelation: "below-title" },
34963
35446
  mode: exportMode ? "export" : "preview",
34964
- showEmptyGroups: true
35447
+ showEmptyGroups: true,
35448
+ ...appHostedPlayback && {
35449
+ controlsHost: "app",
35450
+ controlsGroup: {
35451
+ toggles: [
35452
+ {
35453
+ id: "playback",
35454
+ type: "toggle",
35455
+ label: "Playback",
35456
+ active: true,
35457
+ onToggle: () => {
35458
+ }
35459
+ }
35460
+ ]
35461
+ }
35462
+ }
34965
35463
  };
34966
35464
  const legendState = { activeGroup };
34967
35465
  renderLegendD3(
@@ -35012,8 +35510,9 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
35012
35510
  }
35013
35511
  }
35014
35512
  }
35015
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
35513
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes, controlsHost) {
35016
35514
  d3Selection11.select(container).selectAll(":not([data-d3-tooltip])").remove();
35515
+ const appHostedPlayback = controlsHost === "app" && !!playback;
35017
35516
  const ctx = ScaleContext.identity();
35018
35517
  const sc = buildScaledConstants(ctx);
35019
35518
  const legendGroups = computeInfraLegendGroups(
@@ -35022,7 +35521,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35022
35521
  palette,
35023
35522
  layout.edges
35024
35523
  );
35025
- const hasLegend = legendGroups.length > 0 || !!playback;
35524
+ const hasLegend = legendGroups.length > 0 || !!playback && !appHostedPlayback;
35026
35525
  const fixedLegend = !exportMode && hasLegend;
35027
35526
  const legendDynamicH = hasLegend ? getMaxLegendReservedHeight(
35028
35527
  {
@@ -35166,7 +35665,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35166
35665
  isDark,
35167
35666
  activeGroup ?? null,
35168
35667
  playback ?? void 0,
35169
- exportMode
35668
+ exportMode,
35669
+ controlsHost
35170
35670
  );
35171
35671
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
35172
35672
  } else {
@@ -35179,7 +35679,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35179
35679
  isDark,
35180
35680
  activeGroup ?? null,
35181
35681
  playback ?? void 0,
35182
- exportMode
35682
+ exportMode,
35683
+ controlsHost
35183
35684
  );
35184
35685
  }
35185
35686
  }
@@ -42813,6 +43314,9 @@ function renderTechRadar(container, parsed, palette, isDark, onClickItem, export
42813
43314
  onToggle: (active) => options.onToggleListing(active)
42814
43315
  }
42815
43316
  ]
43317
+ },
43318
+ ...options.controlsHost !== void 0 && {
43319
+ controlsHost: options.controlsHost
42816
43320
  }
42817
43321
  };
42818
43322
  const legendState = {
@@ -44635,7 +45139,7 @@ function computeCycleLayout(parsed, options) {
44635
45139
  const circleNodes = parsed.options["circle-nodes"] === "true";
44636
45140
  const nodeDims = parsed.nodes.map((node) => {
44637
45141
  const hasDesc = !hideDescriptions && node.description.length > 0;
44638
- const labelWidth = Math.max(
45142
+ const labelWidth2 = Math.max(
44639
45143
  MIN_NODE_WIDTH4,
44640
45144
  node.label.length * LABEL_CHAR_W + NODE_PAD_X * 2
44641
45145
  );
@@ -44644,12 +45148,12 @@ function computeCycleLayout(parsed, options) {
44644
45148
  }
44645
45149
  if (!hasDesc) {
44646
45150
  return {
44647
- width: Math.min(MAX_NODE_WIDTH3, labelWidth),
45151
+ width: Math.min(MAX_NODE_WIDTH3, labelWidth2),
44648
45152
  height: PLAIN_NODE_HEIGHT,
44649
45153
  wrappedDesc: []
44650
45154
  };
44651
45155
  }
44652
- return chooseDescribedRectDims(node.description, labelWidth);
45156
+ return chooseDescribedRectDims(node.description, labelWidth2);
44653
45157
  });
44654
45158
  if (circleNodes) {
44655
45159
  const maxDiam = Math.max(...nodeDims.map((d) => d.width));
@@ -44845,10 +45349,10 @@ function computeCycleLayout(parsed, options) {
44845
45349
  scale
44846
45350
  };
44847
45351
  }
44848
- function chooseDescribedRectDims(description, labelWidth) {
45352
+ function chooseDescribedRectDims(description, labelWidth2) {
44849
45353
  const minW = Math.min(
44850
45354
  MAX_NODE_WIDTH3,
44851
- Math.max(MIN_NODE_WIDTH4, labelWidth, DESC_MIN_WIDTH)
45355
+ Math.max(MIN_NODE_WIDTH4, labelWidth2, DESC_MIN_WIDTH)
44852
45356
  );
44853
45357
  let best = null;
44854
45358
  let bestScore = Infinity;
@@ -45277,7 +45781,8 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45277
45781
  const hideDescriptions = (renderOptions?.hideDescriptions ?? false) || parsed.options["no-descriptions"] === "true" || viewState?.hd === true;
45278
45782
  const showDescriptions = !hideDescriptions;
45279
45783
  const hasDescriptions = parsed.nodes.some((n) => n.description.length > 0) || parsed.edges.some((e) => e.description.length > 0);
45280
- const hasLegend = hasDescriptions && !!renderOptions?.onToggleDescriptions;
45784
+ const appHostedControls = renderOptions?.controlsHost === "app";
45785
+ const hasLegend = !appHostedControls && hasDescriptions && !!renderOptions?.onToggleDescriptions;
45281
45786
  const showTitle = !!parsed.title && parsed.options["no-title"] !== "on";
45282
45787
  const legendOffset = hasLegend ? sLegendHeight : 0;
45283
45788
  const layoutHeight = height - (showTitle ? sTitleAreaHeight : 0) - legendOffset;
@@ -45314,7 +45819,10 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45314
45819
  groups: [],
45315
45820
  position: { placement: "top-center", titleRelation: "below-title" },
45316
45821
  mode: renderOptions?.exportMode ? "export" : "preview",
45317
- controlsGroup
45822
+ controlsGroup,
45823
+ ...renderOptions?.controlsHost !== void 0 && {
45824
+ controlsHost: renderOptions.controlsHost
45825
+ }
45318
45826
  };
45319
45827
  const legendState = {
45320
45828
  activeGroup: null,
@@ -45568,7 +46076,7 @@ var init_renderer15 = __esm({
45568
46076
  });
45569
46077
 
45570
46078
  // src/map/geo.ts
45571
- import { feature } from "topojson-client";
46079
+ import { feature, neighbors } from "topojson-client";
45572
46080
  import { geoBounds, geoArea } from "d3-geo";
45573
46081
  function geomObject(topo) {
45574
46082
  const key = Object.keys(topo.objects)[0];
@@ -45586,6 +46094,107 @@ function featureIndex(topo) {
45586
46094
  }
45587
46095
  return idx;
45588
46096
  }
46097
+ function buildAdjacency(topo) {
46098
+ const cached = adjacencyCache.get(topo);
46099
+ if (cached) return cached;
46100
+ const geometries = geomObject(topo).geometries;
46101
+ const nb = neighbors(geometries);
46102
+ const sets = /* @__PURE__ */ new Map();
46103
+ geometries.forEach((g, i) => {
46104
+ if (!g.type || g.type === "null") return;
46105
+ let set = sets.get(g.id);
46106
+ if (!set) {
46107
+ set = /* @__PURE__ */ new Set();
46108
+ sets.set(g.id, set);
46109
+ }
46110
+ for (const j of nb[i] ?? []) {
46111
+ const nid = geometries[j]?.id;
46112
+ if (nid && nid !== g.id) set.add(nid);
46113
+ }
46114
+ });
46115
+ const out = /* @__PURE__ */ new Map();
46116
+ for (const [iso, set] of sets) out.set(iso, [...set].sort());
46117
+ adjacencyCache.set(topo, out);
46118
+ return out;
46119
+ }
46120
+ function decodeFeatures(topo) {
46121
+ return geomObject(topo).geometries.map((g) => {
46122
+ const f = feature(topo, g);
46123
+ return {
46124
+ type: "Feature",
46125
+ id: g.id,
46126
+ properties: g.properties,
46127
+ geometry: f.geometry
46128
+ };
46129
+ });
46130
+ }
46131
+ function pointInRing(lon, lat, ring) {
46132
+ let inside = false;
46133
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46134
+ const xi = ring[i][0];
46135
+ const yi = ring[i][1];
46136
+ const xj = ring[j][0];
46137
+ const yj = ring[j][1];
46138
+ const intersect = yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi;
46139
+ if (intersect) inside = !inside;
46140
+ }
46141
+ return inside;
46142
+ }
46143
+ function pointOnRingEdge(lon, lat, ring) {
46144
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46145
+ const xi = ring[i][0];
46146
+ const yi = ring[i][1];
46147
+ const xj = ring[j][0];
46148
+ const yj = ring[j][1];
46149
+ if (lon < Math.min(xi, xj) - EDGE_EPS || lon > Math.max(xi, xj) + EDGE_EPS)
46150
+ continue;
46151
+ if (lat < Math.min(yi, yj) - EDGE_EPS || lat > Math.max(yi, yj) + EDGE_EPS)
46152
+ continue;
46153
+ const cross = (xj - xi) * (lat - yi) - (yj - yi) * (lon - xi);
46154
+ if (Math.abs(cross) <= EDGE_EPS) return true;
46155
+ }
46156
+ return false;
46157
+ }
46158
+ function pointInGeometry(geometry, lon, lat) {
46159
+ const g = geometry;
46160
+ if (!g) return false;
46161
+ const polys = g.type === "Polygon" ? [g.coordinates] : g.type === "MultiPolygon" ? g.coordinates : [];
46162
+ for (const rings of polys) {
46163
+ if (!rings.length) continue;
46164
+ if (pointOnRingEdge(lon, lat, rings[0])) return true;
46165
+ if (!pointInRing(lon, lat, rings[0])) continue;
46166
+ let inHole = false;
46167
+ for (let h = 1; h < rings.length; h++) {
46168
+ if (pointInRing(lon, lat, rings[h]) && !pointOnRingEdge(lon, lat, rings[h])) {
46169
+ inHole = true;
46170
+ break;
46171
+ }
46172
+ }
46173
+ if (!inHole) return true;
46174
+ }
46175
+ return false;
46176
+ }
46177
+ function regionAt(lonLat, countries, states) {
46178
+ const lon = lonLat[0];
46179
+ const lat = lonLat[1];
46180
+ let country = null;
46181
+ for (const f of countries) {
46182
+ if (pointInGeometry(f.geometry, lon, lat)) {
46183
+ country = { iso: f.id, name: f.properties.name };
46184
+ break;
46185
+ }
46186
+ }
46187
+ let state = null;
46188
+ if (country?.iso === "US" && states) {
46189
+ for (const f of states) {
46190
+ if (pointInGeometry(f.geometry, lon, lat)) {
46191
+ state = { iso: f.id, name: f.properties.name };
46192
+ break;
46193
+ }
46194
+ }
46195
+ }
46196
+ return { country, state };
46197
+ }
45589
46198
  function featureBbox(topo, geomId) {
45590
46199
  const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45591
46200
  if (!geom) return null;
@@ -45703,11 +46312,13 @@ function unionLongitudes(lons) {
45703
46312
  }
45704
46313
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45705
46314
  }
45706
- var fold, DETACH_GAP_DEG, DETACH_AREA_FRAC;
46315
+ var fold, adjacencyCache, EDGE_EPS, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45707
46316
  var init_geo = __esm({
45708
46317
  "src/map/geo.ts"() {
45709
46318
  "use strict";
45710
46319
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
46320
+ adjacencyCache = /* @__PURE__ */ new WeakMap();
46321
+ EDGE_EPS = 1e-9;
45711
46322
  DETACH_GAP_DEG = 10;
45712
46323
  DETACH_AREA_FRAC = 0.25;
45713
46324
  }
@@ -45727,6 +46338,12 @@ function looksUS(lat, lon) {
45727
46338
  if (lat < 15 || lat > 72) return false;
45728
46339
  return lon >= -180 && lon <= -64 || lon >= 172;
45729
46340
  }
46341
+ function looksNorthAmericaNeighbor(lat, lon) {
46342
+ return lat >= 14 && lat <= 72 && lon >= -141 && lon <= -52;
46343
+ }
46344
+ function isWholeSphere(bb) {
46345
+ return bb[0][0] <= -179 && bb[1][0] >= 179 && bb[0][1] <= -89 && bb[1][1] >= 89;
46346
+ }
45730
46347
  function resolveMap(parsed, data) {
45731
46348
  const diagnostics = [...parsed.diagnostics];
45732
46349
  const err = (line12, message, code) => {
@@ -45737,9 +46354,6 @@ function resolveMap(parsed, data) {
45737
46354
  };
45738
46355
  const result = {
45739
46356
  title: parsed.title,
45740
- ...parsed.directives.subtitle !== void 0 && {
45741
- subtitle: parsed.directives.subtitle
45742
- },
45743
46357
  ...parsed.directives.caption !== void 0 && {
45744
46358
  caption: parsed.directives.caption
45745
46359
  },
@@ -45749,7 +46363,7 @@ function resolveMap(parsed, data) {
45749
46363
  // renderer's job (step 4) — the resolver only carries `tags` + `tagGroups`
45750
46364
  // through; it never resolves a tag value to a palette color (#10).
45751
46365
  directives: { ...parsed.directives },
45752
- basemaps: { world: "coarse", subdivisions: [] },
46366
+ basemaps: { world: "detail", subdivisions: [] },
45753
46367
  regions: [],
45754
46368
  pois: [],
45755
46369
  edges: [],
@@ -45758,7 +46372,8 @@ function resolveMap(parsed, data) {
45758
46372
  [-180, -85],
45759
46373
  [180, 85]
45760
46374
  ],
45761
- projection: "natural-earth",
46375
+ projection: "equirectangular",
46376
+ poiFrameContainers: [],
45762
46377
  diagnostics,
45763
46378
  error: parsed.error
45764
46379
  };
@@ -45768,7 +46383,10 @@ function resolveMap(parsed, data) {
45768
46383
  ...[...countryIndex.values()].map((v) => v.name),
45769
46384
  ...[...usStateIndex.values()].map((v) => v.name)
45770
46385
  ];
45771
- const usScoped = parsed.directives.region === "us-states" || parsed.directives.defaultCountry?.toUpperCase() === "US" || parsed.regions.some((r) => {
46386
+ const localeRaw = parsed.directives.locale?.toUpperCase();
46387
+ const localeCountry = localeRaw ? localeRaw.split("-")[0] : void 0;
46388
+ const localeSubdivision = localeRaw && /^[A-Z]{2}-/.test(localeRaw) ? localeRaw : void 0;
46389
+ const usScoped = localeCountry === "US" || parsed.regions.some((r) => {
45772
46390
  const f = fold(r.name);
45773
46391
  return usStateIndex.has(f) && !countryIndex.has(f);
45774
46392
  }) || parsed.regions.some(
@@ -45919,7 +46537,7 @@ function resolveMap(parsed, data) {
45919
46537
  if (!scope)
45920
46538
  warn(
45921
46539
  line12,
45922
- `"${name}" is ambiguous \u2014 resolved to the most-populous match.`,
46540
+ `"${name}" is ambiguous \u2014 resolved to the most-populous match. Set a default with \`locale <ISO>\` (e.g. \`locale US\` / \`locale US-GA\`) to steer it.`,
45923
46541
  "W_MAP_AMBIGUOUS_NAME"
45924
46542
  );
45925
46543
  }
@@ -45932,17 +46550,21 @@ function resolveMap(parsed, data) {
45932
46550
  return fold(pos.name);
45933
46551
  };
45934
46552
  const poiCountries = [];
45935
- let anyNonUsPoi = false;
46553
+ let anyUsPoi = false;
46554
+ let anyNonNaPoi = false;
45936
46555
  const noteCountry = (iso) => {
45937
46556
  if (iso) {
45938
46557
  poiCountries.push(iso);
45939
- if (iso !== "US") anyNonUsPoi = true;
46558
+ if (iso === "US") anyUsPoi = true;
46559
+ if (iso !== "US" && iso !== "CA" && iso !== "MX") anyNonNaPoi = true;
45940
46560
  }
45941
46561
  };
45942
46562
  const deferred = [];
45943
46563
  for (const p of parsed.pois) {
45944
46564
  if (p.pos.kind === "coords") {
45945
- if (!looksUS(p.pos.lat, p.pos.lon)) anyNonUsPoi = true;
46565
+ if (looksUS(p.pos.lat, p.pos.lon)) anyUsPoi = true;
46566
+ else if (!looksNorthAmericaNeighbor(p.pos.lat, p.pos.lon))
46567
+ anyNonNaPoi = true;
45946
46568
  addResolvedPoi(p.pos.lat, p.pos.lon, p);
45947
46569
  continue;
45948
46570
  }
@@ -45960,14 +46582,15 @@ function resolveMap(parsed, data) {
45960
46582
  deferred.push(p);
45961
46583
  }
45962
46584
  }
45963
- const inferredCountry = parsed.directives.defaultCountry?.toUpperCase() ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46585
+ const inferredCountry = localeCountry ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46586
+ const inferredScope = localeSubdivision ?? inferredCountry;
45964
46587
  for (const p of deferred) {
45965
46588
  if (p.pos.kind !== "name") continue;
45966
46589
  const got = lookupName(
45967
46590
  p.pos.name,
45968
46591
  p.pos.scope,
45969
46592
  p.lineNumber,
45970
- inferredCountry,
46593
+ inferredScope,
45971
46594
  true
45972
46595
  );
45973
46596
  if (got.kind === "ok") {
@@ -46037,7 +46660,8 @@ function resolveMap(parsed, data) {
46037
46660
  const meta = sizeValue !== void 0 ? { value: sizeValue } : {};
46038
46661
  if (pos.kind === "coords") {
46039
46662
  const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
46040
- if (!looksUS(pos.lat, pos.lon)) anyNonUsPoi = true;
46663
+ if (looksUS(pos.lat, pos.lon)) anyUsPoi = true;
46664
+ else if (!looksNorthAmericaNeighbor(pos.lat, pos.lon)) anyNonNaPoi = true;
46041
46665
  if (!registry.has(id)) {
46042
46666
  registerPoi(
46043
46667
  id,
@@ -46060,7 +46684,7 @@ function resolveMap(parsed, data) {
46060
46684
  if (registry.has(f)) return f;
46061
46685
  const aliased = declaredByName.get(f);
46062
46686
  if (aliased) return aliased;
46063
- const got = lookupName(pos.name, pos.scope, line12, inferredCountry, true);
46687
+ const got = lookupName(pos.name, pos.scope, line12, inferredScope, true);
46064
46688
  if (got.kind !== "ok") return null;
46065
46689
  noteCountry(got.iso);
46066
46690
  registerPoi(
@@ -46117,9 +46741,12 @@ function resolveMap(parsed, data) {
46117
46741
  }
46118
46742
  routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
46119
46743
  }
46744
+ const hasUsContent = usSubdivisionReferenced || anyUsPoi || localeCountry === "US";
46745
+ const usOriented = !anyNonNaPoi && !regions.some(
46746
+ (r) => r.layer === "country" && !["US", "CA", "MX"].includes(r.iso)
46747
+ ) && hasUsContent;
46120
46748
  const subdivisions = [];
46121
- if (usSubdivisionReferenced || parsed.directives.region === "us-states")
46122
- subdivisions.push("us-states");
46749
+ if (usSubdivisionReferenced || usOriented) subdivisions.push("us-states");
46123
46750
  const regionBoxes = [];
46124
46751
  for (const ref of referencedRegionIds) {
46125
46752
  const bb = featureBbox(data.usStates, ref.id);
@@ -46137,17 +46764,51 @@ function resolveMap(parsed, data) {
46137
46764
  [-180, -85],
46138
46765
  [180, 85]
46139
46766
  ];
46140
- let extent2 = unioned ? pad(unioned, PAD_FRACTION) : DEFAULT_EXTENT;
46767
+ const basePad = regions.length > 0 ? REGION_PAD_FRACTION : PAD_FRACTION;
46768
+ let extent2 = unioned ? pad(unioned, basePad) : DEFAULT_EXTENT;
46769
+ const isPoiOnly = pois.length > 0 && regions.length === 0;
46770
+ const containerRegionIds = [];
46771
+ if (isPoiOnly) {
46772
+ const countries = decodeFeatures(data.worldDetail);
46773
+ const states = decodeFeatures(data.usStates);
46774
+ const seen = /* @__PURE__ */ new Set();
46775
+ const containerBoxes = [];
46776
+ for (const p of pois) {
46777
+ const { country, state } = regionAt([p.lon, p.lat], countries, states);
46778
+ const id = state?.iso ?? country?.iso;
46779
+ if (!id || seen.has(id)) continue;
46780
+ seen.add(id);
46781
+ containerRegionIds.push(id);
46782
+ const bb = state ? featureBbox(data.usStates, id) : featureBboxPrimary(data.worldCoarse, id);
46783
+ if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46784
+ }
46785
+ const containerUnion = unionExtent(containerBoxes, points);
46786
+ if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46787
+ }
46788
+ if (isPoiOnly) {
46789
+ const cx = (extent2[0][0] + extent2[1][0]) / 2;
46790
+ const cy = (extent2[0][1] + extent2[1][1]) / 2;
46791
+ const lon = extent2[1][0] - extent2[0][0];
46792
+ const lat = extent2[1][1] - extent2[0][1];
46793
+ const longer = Math.max(lon, lat);
46794
+ if (longer > 0 && longer < POI_ZOOM_FLOOR_DEG) {
46795
+ const k = POI_ZOOM_FLOOR_DEG / longer;
46796
+ const halfLon = lon * k / 2;
46797
+ const halfLat = lat * k / 2;
46798
+ extent2 = [
46799
+ [cx - halfLon, cy - halfLat],
46800
+ [cx + halfLon, cy + halfLat]
46801
+ ];
46802
+ }
46803
+ }
46141
46804
  const lonSpan = extent2[1][0] - extent2[0][0];
46142
46805
  const latSpan = extent2[1][1] - extent2[0][1];
46143
46806
  const span = Math.max(lonSpan, latSpan);
46144
46807
  const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
46145
- const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46146
46808
  let projection;
46147
- const override = parsed.directives.projection;
46148
- if (override === "equirectangular" || override === "natural-earth" || override === "albers-usa" || override === "mercator") {
46149
- projection = override;
46150
- } else if (usDominant) {
46809
+ if (isPoiOnly && usOriented && lonSpan < US_NATIONAL_LON_SPAN) {
46810
+ projection = "mercator";
46811
+ } else if (usOriented) {
46151
46812
  projection = "albers-usa";
46152
46813
  } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
46153
46814
  projection = "equirectangular";
@@ -46165,11 +46826,20 @@ function resolveMap(parsed, data) {
46165
46826
  result.edges = edges;
46166
46827
  result.routes = routes;
46167
46828
  result.basemaps = {
46168
- world: span > WORLD_SPAN ? "coarse" : "detail",
46829
+ // Tier is intentionally pinned to detail (50m) at ALL scales. Diagrammo maps
46830
+ // are presentational (palette tints, relief hachures, POI hubs), not
46831
+ // survey-grade — recognizability > generalization: 110m coarse drops the
46832
+ // Italian boot to a stump at world scale. `WORLD_SPAN` lives on only for the
46833
+ // projection decision (the `usOriented`/`span > WORLD_SPAN` chain above); it
46834
+ // no longer gates basemap resolution.
46835
+ // `worldCoarse` is still loaded — it's the authoritative name/bbox index
46836
+ // (featureIndex, featureBboxPrimary), not dead code.
46837
+ world: "detail",
46169
46838
  subdivisions
46170
46839
  };
46171
46840
  result.extent = extent2;
46172
46841
  result.projection = projection;
46842
+ result.poiFrameContainers = containerRegionIds;
46173
46843
  result.error = parsed.error ?? firstError(diagnostics);
46174
46844
  return result;
46175
46845
  }
@@ -46206,7 +46876,7 @@ function firstError(diags) {
46206
46876
  const e = diags.find((d) => d.severity === "error");
46207
46877
  return e ? formatDgmoError(e) : null;
46208
46878
  }
46209
- var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46879
+ var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, REGION_PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, POI_ZOOM_FLOOR_DEG, US_NATIONAL_LON_SPAN, REGION_ALIASES, US_STATE_POSTAL;
46210
46880
  var init_resolver2 = __esm({
46211
46881
  "src/map/resolver.ts"() {
46212
46882
  "use strict";
@@ -46215,8 +46885,11 @@ var init_resolver2 = __esm({
46215
46885
  WORLD_SPAN = 90;
46216
46886
  MERCATOR_MAX_LAT = 80;
46217
46887
  PAD_FRACTION = 0.05;
46888
+ REGION_PAD_FRACTION = 0.12;
46218
46889
  WORLD_LAT_SOUTH = -58;
46219
46890
  WORLD_LAT_NORTH = 78;
46891
+ POI_ZOOM_FLOOR_DEG = 7;
46892
+ US_NATIONAL_LON_SPAN = 48;
46220
46893
  REGION_ALIASES = {
46221
46894
  // Common everyday names → the Natural-Earth display name actually shipped.
46222
46895
  "united states": "united states of america",
@@ -46294,10 +46967,277 @@ var init_resolver2 = __esm({
46294
46967
  }
46295
46968
  });
46296
46969
 
46970
+ // src/map/colorize.ts
46971
+ function assignColors(isos, adjacency) {
46972
+ const sorted = [...isos].sort();
46973
+ const byIso = /* @__PURE__ */ new Map();
46974
+ let maxIndex = -1;
46975
+ for (const iso of sorted) {
46976
+ const taken = /* @__PURE__ */ new Set();
46977
+ for (const n of adjacency.get(iso) ?? []) {
46978
+ const c = byIso.get(n);
46979
+ if (c !== void 0) taken.add(c);
46980
+ }
46981
+ let h = 0;
46982
+ while (taken.has(h)) h++;
46983
+ byIso.set(iso, h);
46984
+ if (h > maxIndex) maxIndex = h;
46985
+ }
46986
+ return { byIso, huesNeeded: maxIndex + 1 };
46987
+ }
46988
+ var init_colorize = __esm({
46989
+ "src/map/colorize.ts"() {
46990
+ "use strict";
46991
+ }
46992
+ });
46993
+
46994
+ // src/map/context-labels.ts
46995
+ function tierBand(maxSpanDeg) {
46996
+ if (maxSpanDeg >= 90) return "world";
46997
+ if (maxSpanDeg >= 20) return "continental";
46998
+ if (maxSpanDeg >= 5) return "regional";
46999
+ return "local";
47000
+ }
47001
+ function labelBudget(width, height, band) {
47002
+ const bandCap = {
47003
+ world: 6,
47004
+ continental: 5,
47005
+ regional: 4,
47006
+ local: 3
47007
+ };
47008
+ const area2 = Math.floor(Math.sqrt(Math.max(0, width * height)) / 150);
47009
+ return Math.max(0, Math.min(area2, bandCap[band]));
47010
+ }
47011
+ function waterEligible(tier, kind, band) {
47012
+ switch (band) {
47013
+ case "world":
47014
+ return tier <= 1 && (kind === "ocean" || kind === "sea");
47015
+ case "continental":
47016
+ return tier <= 2;
47017
+ case "regional":
47018
+ return tier <= 3;
47019
+ case "local":
47020
+ return tier <= 4;
47021
+ }
47022
+ }
47023
+ function insideViewport(p, width, height) {
47024
+ return !!p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && p[0] >= 0 && p[0] <= width && p[1] >= 0 && p[1] <= height;
47025
+ }
47026
+ function labelWidth(text, letterSpacing) {
47027
+ const spacing = letterSpacing > 0 ? Math.max(0, text.length - 1) * letterSpacing : 0;
47028
+ return measureLegendText(text, FONT) + spacing + 2 * PADX;
47029
+ }
47030
+ function wrapLabel2(text, letterSpacing) {
47031
+ const words = text.split(/\s+/).filter(Boolean);
47032
+ if (words.length <= 1) return [text];
47033
+ const maxLines = words.length >= 4 ? 3 : 2;
47034
+ const n = words.length;
47035
+ let best = null;
47036
+ for (let mask = 0; mask < 1 << n - 1; mask++) {
47037
+ const lines = [];
47038
+ let cur = [words[0]];
47039
+ for (let i = 1; i < n; i++) {
47040
+ if (mask & 1 << i - 1) {
47041
+ lines.push(cur.join(" "));
47042
+ cur = [words[i]];
47043
+ } else cur.push(words[i]);
47044
+ }
47045
+ lines.push(cur.join(" "));
47046
+ if (lines.length > maxLines) continue;
47047
+ const cost = Math.round(
47048
+ Math.max(...lines.map((l) => labelWidth(l, letterSpacing)))
47049
+ );
47050
+ const head = labelWidth(lines[0], letterSpacing);
47051
+ if (!best || cost < best.cost || cost === best.cost && lines.length < best.lines.length || cost === best.cost && lines.length === best.lines.length && head > best.head)
47052
+ best = { lines, cost, head };
47053
+ }
47054
+ return best?.lines ?? [text];
47055
+ }
47056
+ function rectAround(cx, cy, lines, letterSpacing) {
47057
+ const w = Math.max(...lines.map((l) => labelWidth(l, letterSpacing)));
47058
+ const h = (lines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY;
47059
+ return { x: cx - w / 2, y: cy - h / 2, w, h };
47060
+ }
47061
+ function rectFits(r, width, height) {
47062
+ return r.x >= 0 && r.y >= 0 && r.x + r.w <= width && r.y + r.h <= height;
47063
+ }
47064
+ function overlapsPadded(a, b, pad2) {
47065
+ 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;
47066
+ }
47067
+ function placeContextLabels(args) {
47068
+ const {
47069
+ projection,
47070
+ dLonSpan,
47071
+ dLatSpan,
47072
+ width,
47073
+ height,
47074
+ waterBodies,
47075
+ countries,
47076
+ palette,
47077
+ project,
47078
+ collides,
47079
+ overLand
47080
+ } = args;
47081
+ void projection;
47082
+ const band = tierBand(Math.max(dLonSpan, dLatSpan));
47083
+ const budget = labelBudget(width, height, band);
47084
+ if (budget <= 0) return [];
47085
+ const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
47086
+ const countryColor = palette.textMuted;
47087
+ const haloColor = palette.bg;
47088
+ const candidates = [];
47089
+ const center = [width / 2, height / 2];
47090
+ for (const e of waterBodies?.entries ?? []) {
47091
+ const [lat, lon, name, tier, kind, alt] = e;
47092
+ if (!waterEligible(tier, kind, band)) continue;
47093
+ const wlines = wrapLabel2(name, WATER_LETTER_SPACING);
47094
+ const anchorsLngLat = [[lon, lat]];
47095
+ for (const a of alt ?? []) anchorsLngLat.push([a[1], a[0]]);
47096
+ let best = null;
47097
+ let bestD = Infinity;
47098
+ let nearestProj = null;
47099
+ let nearestProjD = Infinity;
47100
+ for (const [aLon, aLat] of anchorsLngLat) {
47101
+ const p = project(aLon, aLat);
47102
+ if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
47103
+ const d = (p[0] - center[0]) ** 2 + (p[1] - center[1]) ** 2;
47104
+ if (d < nearestProjD) {
47105
+ nearestProjD = d;
47106
+ nearestProj = p;
47107
+ }
47108
+ if (!insideViewport(p, width, height)) continue;
47109
+ if (d < bestD) {
47110
+ bestD = d;
47111
+ best = p;
47112
+ }
47113
+ }
47114
+ if (!best && tier === 0 && nearestProj) {
47115
+ const overX = Math.max(0, -nearestProj[0], nearestProj[0] - width);
47116
+ const overY = Math.max(0, -nearestProj[1], nearestProj[1] - height);
47117
+ if (overX <= width * EDGE_CLAMP_OVERSHOOT && overY <= height * EDGE_CLAMP_OVERSHOOT) {
47118
+ const halfW = Math.max(...wlines.map((l) => labelWidth(l, WATER_LETTER_SPACING))) / 2;
47119
+ const halfH = ((wlines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY) / 2;
47120
+ const m = EDGE_CLAMP_MARGIN;
47121
+ best = [
47122
+ Math.min(Math.max(nearestProj[0], halfW + m), width - halfW - m),
47123
+ Math.min(Math.max(nearestProj[1], halfH + m), height - halfH - m)
47124
+ ];
47125
+ }
47126
+ }
47127
+ if (!best) continue;
47128
+ candidates.push({
47129
+ text: name,
47130
+ lines: wlines,
47131
+ cx: best[0],
47132
+ cy: best[1],
47133
+ italic: true,
47134
+ letterSpacing: WATER_LETTER_SPACING,
47135
+ color: waterColor,
47136
+ // Water before any country (×1000), then by tier, then kind, then name.
47137
+ sort: tier * 10 + KIND_ORDER[kind]
47138
+ });
47139
+ }
47140
+ const ranked = countries.map((c) => {
47141
+ const [x0, y0, x1, y1] = c.bbox;
47142
+ const w = x1 - x0;
47143
+ const h = y1 - y0;
47144
+ return { c, w, h, area: w * h };
47145
+ }).filter((r) => Number.isFinite(r.area) && r.area > 0).sort((a, b) => b.area - a.area);
47146
+ let ci = 0;
47147
+ for (const r of ranked) {
47148
+ const { c, w, h } = r;
47149
+ if (w > width * 0.66 || h > height * 0.66) continue;
47150
+ if (!insideViewport(c.anchor, width, height)) continue;
47151
+ const text = c.name;
47152
+ const tw = labelWidth(text, 0);
47153
+ if (tw > w || FONT + 2 * PADY > h) continue;
47154
+ candidates.push({
47155
+ text,
47156
+ lines: [text],
47157
+ cx: c.anchor[0],
47158
+ cy: c.anchor[1],
47159
+ italic: false,
47160
+ letterSpacing: 0,
47161
+ color: countryColor,
47162
+ // Always after every water body (+1e6); larger area = earlier.
47163
+ sort: 1e6 + ci++
47164
+ });
47165
+ }
47166
+ candidates.sort((a, b) => a.sort - b.sort);
47167
+ const placed = [];
47168
+ const placedRects = [];
47169
+ for (const cand of candidates) {
47170
+ if (placed.length >= budget) break;
47171
+ const rect = rectAround(cand.cx, cand.cy, cand.lines, cand.letterSpacing);
47172
+ if (!rectFits(rect, width, height)) continue;
47173
+ if (cand.italic && overLand) {
47174
+ const inset = 2;
47175
+ const top = cand.cy - (cand.lines.length - 1) / 2 * LINE_HEIGHT;
47176
+ const touchesLand = cand.lines.some((line12, li) => {
47177
+ const lw = labelWidth(line12, cand.letterSpacing);
47178
+ const x0 = cand.cx - lw / 2 + inset;
47179
+ const x1 = cand.cx + lw / 2 - inset;
47180
+ const xs = [x0, (x0 + cand.cx) / 2, cand.cx, (cand.cx + x1) / 2, x1];
47181
+ const base = top + li * LINE_HEIGHT;
47182
+ return [base, base - FONT * 0.4, base - FONT * 0.8].some(
47183
+ (y) => xs.some((x) => overLand(x, y))
47184
+ );
47185
+ });
47186
+ if (touchesLand) continue;
47187
+ }
47188
+ if (collides(rect)) continue;
47189
+ if (placedRects.some((r) => overlapsPadded(rect, r, CONTEXT_PAD))) continue;
47190
+ placedRects.push(rect);
47191
+ placed.push({
47192
+ x: cand.cx,
47193
+ y: cand.cy,
47194
+ text: cand.text,
47195
+ anchor: "middle",
47196
+ color: cand.color,
47197
+ // No halo: the bg-coloured outline reads as a ghost box behind the text
47198
+ // over the tinted water/land. Context labels are muted enough to sit
47199
+ // cleanly on the basemap without one.
47200
+ halo: false,
47201
+ haloColor,
47202
+ italic: cand.italic,
47203
+ letterSpacing: cand.letterSpacing,
47204
+ ...cand.lines.length > 1 ? { lines: cand.lines } : {},
47205
+ lineNumber: 0
47206
+ });
47207
+ }
47208
+ return placed;
47209
+ }
47210
+ var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, KIND_ORDER;
47211
+ var init_context_labels = __esm({
47212
+ "src/map/context-labels.ts"() {
47213
+ "use strict";
47214
+ init_color_utils();
47215
+ init_legend_constants();
47216
+ FONT = 11;
47217
+ LINE_HEIGHT = FONT + 2;
47218
+ PADX = 4;
47219
+ PADY = 3;
47220
+ WATER_LETTER_SPACING = 1.5;
47221
+ CONTEXT_PAD = 4;
47222
+ EDGE_CLAMP_MARGIN = 8;
47223
+ EDGE_CLAMP_OVERSHOOT = 0.35;
47224
+ KIND_ORDER = {
47225
+ ocean: 0,
47226
+ sea: 1,
47227
+ gulf: 2,
47228
+ bay: 3,
47229
+ strait: 4,
47230
+ channel: 5,
47231
+ sound: 6
47232
+ };
47233
+ }
47234
+ });
47235
+
46297
47236
  // src/map/layout.ts
46298
47237
  import {
46299
47238
  geoPath,
46300
47239
  geoNaturalEarth1,
47240
+ geoEqualEarth,
46301
47241
  geoEquirectangular,
46302
47242
  geoConicEqualArea,
46303
47243
  geoMercator,
@@ -46309,12 +47249,34 @@ function geomObject2(topo) {
46309
47249
  const key = Object.keys(topo.objects)[0];
46310
47250
  return topo.objects[key];
46311
47251
  }
47252
+ function mergeFeatures(a, b) {
47253
+ const polysOf = (f) => {
47254
+ const g = f.geometry;
47255
+ if (!g) return null;
47256
+ if (g.type === "Polygon") return [g.coordinates];
47257
+ if (g.type === "MultiPolygon") return g.coordinates;
47258
+ return null;
47259
+ };
47260
+ const pa = polysOf(a);
47261
+ const pb = polysOf(b);
47262
+ if (!pa || !pb) return a;
47263
+ return {
47264
+ ...a,
47265
+ geometry: { type: "MultiPolygon", coordinates: [...pa, ...pb] }
47266
+ };
47267
+ }
46312
47268
  function decodeLayer(topo) {
47269
+ const cached = decodeCache.get(topo);
47270
+ if (cached) return cached;
46313
47271
  const out = /* @__PURE__ */ new Map();
46314
47272
  for (const g of geomObject2(topo).geometries) {
46315
47273
  const f = feature2(topo, g);
46316
- out.set(g.id, { ...f, id: g.id });
47274
+ if (!f.geometry) continue;
47275
+ const tagged = { ...f, id: g.id };
47276
+ const existing = out.get(g.id);
47277
+ out.set(g.id, existing ? mergeFeatures(existing, tagged) : tagged);
46317
47278
  }
47279
+ decodeCache.set(topo, out);
46318
47280
  return out;
46319
47281
  }
46320
47282
  function projectionFor(family) {
@@ -46323,9 +47285,12 @@ function projectionFor(family) {
46323
47285
  return usConusProjection();
46324
47286
  case "mercator":
46325
47287
  return geoMercator();
47288
+ case "equal-earth":
47289
+ return geoEqualEarth();
47290
+ case "equirectangular":
47291
+ return geoEquirectangular();
46326
47292
  case "natural-earth":
46327
47293
  return geoNaturalEarth1();
46328
- case "equirectangular":
46329
47294
  default:
46330
47295
  return geoEquirectangular();
46331
47296
  }
@@ -46344,13 +47309,11 @@ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46344
47309
  isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT
46345
47310
  );
46346
47311
  }
46347
- function layoutMap(resolved, data, size, opts) {
46348
- const { palette, isDark } = opts;
46349
- const { width, height } = size;
47312
+ function buildMapProjection(resolved, data) {
46350
47313
  const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
46351
- const usCrisp = resolved.projection === "albers-usa" && wantsUsStates && !!data.naLand;
47314
+ const usCrisp = (resolved.projection === "albers-usa" || resolved.projection === "mercator") && wantsUsStates && !!data.naLand;
46352
47315
  const worldTopo = usCrisp ? data.worldDetail : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46353
- const worldLayer = decodeLayer(worldTopo);
47316
+ const worldLayer = new Map(decodeLayer(worldTopo));
46354
47317
  if (usCrisp && data.naLand) {
46355
47318
  const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
46356
47319
  const crisp = decodeLayer(data.naLand);
@@ -46359,16 +47322,141 @@ function layoutMap(resolved, data, size, opts) {
46359
47322
  if (!base) continue;
46360
47323
  const [[bw, bs], [be, bn]] = geoBounds2(base);
46361
47324
  if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
46362
- worldLayer.set(iso, cf);
47325
+ worldLayer.set(iso, { ...cf, properties: base.properties });
46363
47326
  }
46364
47327
  }
46365
47328
  const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
47329
+ const extentOutline = () => {
47330
+ const [[w, s], [e, n]] = resolved.extent;
47331
+ const N = 16;
47332
+ const coords = [];
47333
+ for (let i = 0; i <= N; i++) {
47334
+ const t = i / N;
47335
+ const lon = w + (e - w) * t;
47336
+ const lat = s + (n - s) * t;
47337
+ coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
47338
+ }
47339
+ return {
47340
+ type: "Feature",
47341
+ properties: {},
47342
+ geometry: { type: "MultiPoint", coordinates: coords }
47343
+ };
47344
+ };
47345
+ let fitFeatures;
47346
+ if (resolved.projection === "albers-usa" && usLayer) {
47347
+ fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
47348
+ const neighborPoints = resolved.pois.filter((p) => !inAlaska(p.lon, p.lat) && !inHawaii(p.lon, p.lat)).map((p) => [p.lon, p.lat]);
47349
+ if (neighborPoints.length > 0) {
47350
+ fitFeatures.push({
47351
+ type: "Feature",
47352
+ properties: {},
47353
+ geometry: { type: "MultiPoint", coordinates: neighborPoints }
47354
+ });
47355
+ }
47356
+ for (const r of resolved.regions) {
47357
+ if (r.layer === "country" && (r.iso === "CA" || r.iso === "MX")) {
47358
+ const cf = worldLayer.get(r.iso);
47359
+ if (cf) fitFeatures.push(cf);
47360
+ }
47361
+ }
47362
+ } else {
47363
+ fitFeatures = [extentOutline()];
47364
+ }
47365
+ const fitTarget = { type: "FeatureCollection", features: fitFeatures };
47366
+ const projection = projectionFor(resolved.projection);
47367
+ if (resolved.projection !== "albers-usa") {
47368
+ let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
47369
+ if (centerLon > 180) centerLon -= 360;
47370
+ projection.rotate([-centerLon, 0]);
47371
+ }
47372
+ const fitGB = geoBounds2(fitTarget);
47373
+ const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
47374
+ return {
47375
+ projection,
47376
+ fitTarget,
47377
+ fitIsGlobal,
47378
+ worldLayer,
47379
+ usLayer,
47380
+ usCrisp,
47381
+ wantsUsStates,
47382
+ worldTopo
47383
+ };
47384
+ }
47385
+ function parsePathRings(d) {
47386
+ const rings = [];
47387
+ let cur = [];
47388
+ const re = /([MLZ])([^MLZ]*)/g;
47389
+ let m;
47390
+ while (m = re.exec(d)) {
47391
+ if (m[1] === "Z") {
47392
+ if (cur.length) rings.push(cur);
47393
+ cur = [];
47394
+ continue;
47395
+ }
47396
+ if (m[1] === "M" && cur.length) {
47397
+ rings.push(cur);
47398
+ cur = [];
47399
+ }
47400
+ const nums = m[2].split(/[ ,]+/).map(Number);
47401
+ for (let i = 0; i + 1 < nums.length; i += 2) {
47402
+ const x = nums[i];
47403
+ const y = nums[i + 1];
47404
+ if (Number.isFinite(x) && Number.isFinite(y)) cur.push([x, y]);
47405
+ }
47406
+ }
47407
+ if (cur.length) rings.push(cur);
47408
+ return rings;
47409
+ }
47410
+ function dropAntimeridianWrapSlivers(d, width, height) {
47411
+ const rings = parsePathRings(d);
47412
+ if (rings.length <= 1) return d;
47413
+ const eps = 0.75;
47414
+ const minArea = 3e-3 * width * height;
47415
+ const ringArea = (r) => {
47416
+ let s = 0;
47417
+ for (let i = 0; i < r.length; i++) {
47418
+ const a = r[i];
47419
+ const b = r[(i + 1) % r.length];
47420
+ s += a[0] * b[1] - b[0] * a[1];
47421
+ }
47422
+ return Math.abs(s) / 2;
47423
+ };
47424
+ const areas = rings.map(ringArea);
47425
+ const maxArea = Math.max(...areas);
47426
+ const onVEdge = (a, b) => Math.abs(a[0]) <= eps && Math.abs(b[0]) <= eps || Math.abs(a[0] - width) <= eps && Math.abs(b[0] - width) <= eps;
47427
+ let dropped = false;
47428
+ const kept = rings.filter((r, idx) => {
47429
+ if (areas[idx] >= maxArea || areas[idx] >= minArea) return true;
47430
+ const touches = r.some((p, i) => onVEdge(p, r[(i + 1) % r.length]));
47431
+ if (touches) {
47432
+ dropped = true;
47433
+ return false;
47434
+ }
47435
+ return true;
47436
+ });
47437
+ if (!dropped) return d;
47438
+ return kept.map(
47439
+ (r) => r.map((p, i) => (i ? "L" : "M") + p[0] + "," + p[1]).join("") + "Z"
47440
+ ).join("");
47441
+ }
47442
+ function layoutMap(resolved, data, size, opts) {
47443
+ const { palette, isDark } = opts;
47444
+ const { width, height } = size;
47445
+ const {
47446
+ projection,
47447
+ fitTarget,
47448
+ fitIsGlobal,
47449
+ worldLayer,
47450
+ usLayer,
47451
+ usCrisp,
47452
+ worldTopo
47453
+ } = buildMapProjection(resolved, data);
46366
47454
  const usContext = usLayer !== null;
46367
47455
  const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46368
47456
  const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
46369
- const scaleOverride = resolved.directives.scale;
46370
- const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46371
- const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
47457
+ const allNonNegative = values.length > 0 && values.every((v) => v >= 0);
47458
+ const rampMin = allNonNegative ? 0 : Math.min(...values);
47459
+ const rampMax = Math.max(...values);
46372
47460
  const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46373
47461
  const hasRamp = values.length > 0;
46374
47462
  const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
@@ -46389,7 +47477,7 @@ function layoutMap(resolved, data, size, opts) {
46389
47477
  activeGroup = VALUE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46390
47478
  }
46391
47479
  const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
46392
- const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
47480
+ const mutedBasemap = activeGroup !== null;
46393
47481
  const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46394
47482
  const water = mapBackgroundColor(palette, isDark, mutedBasemap);
46395
47483
  const lakeStroke = mix(regionStroke, water, 45);
@@ -46398,10 +47486,43 @@ function layoutMap(resolved, data, size, opts) {
46398
47486
  palette.bg,
46399
47487
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46400
47488
  );
47489
+ const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && resolved.tagGroups.length === 0;
47490
+ const colorByIso = /* @__PURE__ */ new Map();
47491
+ if (colorizeActive) {
47492
+ const adjacency = /* @__PURE__ */ new Map();
47493
+ const addEdges = (src) => {
47494
+ for (const [iso, ns] of src) {
47495
+ const cur = adjacency.get(iso);
47496
+ if (cur) cur.push(...ns);
47497
+ else adjacency.set(iso, [...ns]);
47498
+ }
47499
+ };
47500
+ addEdges(buildAdjacency(worldTopo));
47501
+ if (usLayer) {
47502
+ addEdges(buildAdjacency(data.usStates));
47503
+ for (const [country, states] of Object.entries(FOREIGN_BORDER)) {
47504
+ const cn = adjacency.get(country);
47505
+ if (!cn) continue;
47506
+ for (const st of states) {
47507
+ const sn = adjacency.get(st);
47508
+ if (!sn) continue;
47509
+ cn.push(st);
47510
+ sn.push(country);
47511
+ }
47512
+ }
47513
+ }
47514
+ const { byIso, huesNeeded } = assignColors(
47515
+ [...adjacency.keys()],
47516
+ adjacency
47517
+ );
47518
+ const tints = politicalTints(palette, huesNeeded, isDark);
47519
+ for (const [iso, idx] of byIso) colorByIso.set(iso, tints[idx]);
47520
+ }
47521
+ const colorizeStroke = (fill2) => mix(fill2, palette.text, 35);
46401
47522
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46402
47523
  const fillForValue = (s) => {
46403
47524
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
46404
- const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
47525
+ const pct = RAMP_FLOOR2 + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR2);
46405
47526
  return mix(rampHue, rampBase, pct);
46406
47527
  };
46407
47528
  const tagFill = (tags, groupName) => {
@@ -46433,43 +47554,15 @@ function layoutMap(resolved, data, size, opts) {
46433
47554
  if (activeIsScore) {
46434
47555
  return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46435
47556
  }
47557
+ if (colorizeActive) return (r.iso && colorByIso.get(r.iso)) ?? neutralFill;
46436
47558
  return tagFill(r.tags, activeGroup) ?? neutralFill;
46437
47559
  };
46438
47560
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
46439
- const extentOutline = () => {
46440
- const [[w, s], [e, n]] = resolved.extent;
46441
- const N = 16;
46442
- const coords = [];
46443
- for (let i = 0; i <= N; i++) {
46444
- const t = i / N;
46445
- const lon = w + (e - w) * t;
46446
- const lat = s + (n - s) * t;
46447
- coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
46448
- }
46449
- return {
46450
- type: "Feature",
46451
- properties: {},
46452
- geometry: { type: "MultiPoint", coordinates: coords }
46453
- };
46454
- };
46455
- let fitFeatures;
46456
- if (resolved.projection === "albers-usa" && usLayer) {
46457
- fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
46458
- } else {
46459
- fitFeatures = [extentOutline()];
46460
- }
46461
- const fitTarget = { type: "FeatureCollection", features: fitFeatures };
46462
- const projection = projectionFor(resolved.projection);
46463
- if (resolved.projection !== "albers-usa") {
46464
- let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
46465
- if (centerLon > 180) centerLon -= 360;
46466
- projection.rotate([-centerLon, 0]);
46467
- }
46468
- const TITLE_GAP = 16;
47561
+ const TITLE_GAP2 = 16;
46469
47562
  let topPad = FIT_PAD;
46470
47563
  if (resolved.title && resolved.pois.length > 0) {
46471
47564
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
46472
- topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
47565
+ topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
46473
47566
  }
46474
47567
  const fitBox = [
46475
47568
  [FIT_PAD, topPad],
@@ -46479,21 +47572,20 @@ function layoutMap(resolved, data, size, opts) {
46479
47572
  ]
46480
47573
  ];
46481
47574
  projection.fitExtent(fitBox, fitTarget);
46482
- const fitGB = geoBounds2(fitTarget);
46483
- const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46484
47575
  let path;
46485
47576
  let project;
46486
47577
  let stretchParams = null;
46487
- if (fitIsGlobal) {
47578
+ if (fitIsGlobal && !opts.preferContain) {
46488
47579
  const cb = geoPath(projection).bounds(fitTarget);
46489
47580
  const bx0 = cb[0][0];
46490
47581
  const by0 = cb[0][1];
46491
47582
  const cw = cb[1][0] - bx0;
46492
47583
  const ch = cb[1][1] - by0;
46493
- const ox = fitBox[0][0];
46494
- const oy = fitBox[0][1];
46495
- const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46496
- const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47584
+ const topReserve = resolved.title && resolved.pois.length > 0 ? topPad : 0;
47585
+ const ox = 0;
47586
+ const oy = topReserve;
47587
+ const sx = cw > 0 ? width / cw : 1;
47588
+ const sy = ch > 0 ? (height - topReserve) / ch : 1;
46497
47589
  stretchParams = { sx, sy, ox, oy, bx0, by0 };
46498
47590
  const stretch = (x, y) => [
46499
47591
  ox + (x - bx0) * sx,
@@ -46526,7 +47618,9 @@ function layoutMap(resolved, data, size, opts) {
46526
47618
  const insets = [];
46527
47619
  const insetRegions = [];
46528
47620
  const insetLabelSeeds = [];
46529
- if (resolved.projection === "albers-usa" && usLayer && !resolved.directives.noInsets) {
47621
+ const akRef = resolved.regions.some((r) => r.iso === "US-AK") || resolved.pois.some((p) => inAlaska(p.lon, p.lat));
47622
+ const hiRef = resolved.regions.some((r) => r.iso === "US-HI") || resolved.pois.some((p) => inHawaii(p.lon, p.lat));
47623
+ if (resolved.projection === "albers-usa" && usLayer && (akRef || hiRef)) {
46530
47624
  const PAD = 8;
46531
47625
  const GAP = 12;
46532
47626
  const yB = height - FIT_PAD;
@@ -46591,8 +47685,18 @@ function layoutMap(resolved, data, size, opts) {
46591
47685
  );
46592
47686
  const d = geoPath(proj)(f) ?? "";
46593
47687
  if (!d) return xr;
47688
+ let contextLand;
47689
+ if (iso === "US-AK") {
47690
+ const can = worldLayer.get("CA");
47691
+ const cd = can ? geoPath(proj)(can) ?? "" : "";
47692
+ if (cd)
47693
+ contextLand = {
47694
+ d: cd,
47695
+ fill: colorizeActive ? colorByIso.get("CA") ?? foreignFill : foreignFill
47696
+ };
47697
+ }
46594
47698
  const r = regionById.get(iso);
46595
- let fill2 = neutralFill;
47699
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? neutralFill : neutralFill;
46596
47700
  let lineNumber = -1;
46597
47701
  if (r?.layer === "us-state") {
46598
47702
  fill2 = regionFill(r);
@@ -46611,13 +47715,14 @@ function layoutMap(resolved, data, size, opts) {
46611
47715
  ],
46612
47716
  // The FITTED inset projection (just fit to this box) — captured so the
46613
47717
  // geo-query can invert pixels inside the frame back to AK/HI coords.
46614
- projection: proj
47718
+ projection: proj,
47719
+ ...contextLand && { contextLand }
46615
47720
  });
46616
47721
  insetRegions.push({
46617
47722
  id: iso,
46618
47723
  d,
46619
47724
  fill: fill2,
46620
- stroke: regionStroke,
47725
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46621
47726
  lineNumber,
46622
47727
  layer: "us-state",
46623
47728
  ...r?.value !== void 0 && { value: r.value },
@@ -46630,13 +47735,16 @@ function layoutMap(resolved, data, size, opts) {
46630
47735
  }
46631
47736
  return xr;
46632
47737
  };
46633
- const akRight = placeInset(
46634
- "US-AK",
46635
- alaskaProjection(),
46636
- FIT_PAD,
46637
- width * 0.15
46638
- );
46639
- placeInset("US-HI", hawaiiProjection(), akRight + 24, width * 0.1);
47738
+ let akRight = FIT_PAD;
47739
+ if (akRef)
47740
+ akRight = placeInset("US-AK", alaskaProjection(), FIT_PAD, width * 0.15);
47741
+ if (hiRef)
47742
+ placeInset(
47743
+ "US-HI",
47744
+ hawaiiProjection(),
47745
+ akRef ? akRight + 24 : FIT_PAD,
47746
+ width * 0.1
47747
+ );
46640
47748
  }
46641
47749
  const conusFit = resolved.projection === "albers-usa" && !!usLayer;
46642
47750
  const classifyExtent = conusFit ? geoBounds2(fitTarget) : resolved.extent;
@@ -46652,15 +47760,24 @@ function layoutMap(resolved, data, size, opts) {
46652
47760
  };
46653
47761
  const ringOverlapsView = (ring) => {
46654
47762
  let loMin = Infinity, loMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
47763
+ const lons = [];
46655
47764
  for (const [rawLon] of ring) {
46656
47765
  const lon = normLon(rawLon);
47766
+ lons.push(lon);
46657
47767
  if (lon < loMin) loMin = lon;
46658
47768
  if (lon > loMax) loMax = lon;
46659
47769
  if (rawLon < rawMin) rawMin = rawLon;
46660
47770
  if (rawLon > rawMax) rawMax = rawLon;
46661
47771
  }
46662
- if (loMax - loMin > 270) return false;
46663
- if (rawMax - rawMin > 180 && loMax - loMin < 90) return false;
47772
+ lons.sort((a, b) => a - b);
47773
+ let maxGap = 0;
47774
+ for (let i = 1; i < lons.length; i++)
47775
+ maxGap = Math.max(maxGap, lons[i] - lons[i - 1]);
47776
+ if (lons.length > 1)
47777
+ maxGap = Math.max(maxGap, lons[0] + 360 - lons[lons.length - 1]);
47778
+ const occupiedArc = 360 - maxGap;
47779
+ if (occupiedArc > 270) return false;
47780
+ if (rawMax - rawMin > 180 && occupiedArc < 90) return false;
46664
47781
  let px0 = Infinity, py0 = Infinity, px1 = -Infinity, py1 = -Infinity, anyFinite = false;
46665
47782
  for (const [lon, lat] of ring) {
46666
47783
  const p = project(lon, lat);
@@ -46733,7 +47850,7 @@ function layoutMap(resolved, data, size, opts) {
46733
47850
  const regions = [];
46734
47851
  const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
46735
47852
  for (const [iso, f] of layerFeatures) {
46736
- if (layerKind === "us-state" && usContext && INSET_STATES.has(iso))
47853
+ if (layerKind === "us-state" && usContext && resolved.projection === "albers-usa" && INSET_STATES.has(iso))
46737
47854
  continue;
46738
47855
  if (layerKind === "country" && usContext && iso === "US") continue;
46739
47856
  if (layerKind === "country" && iso === "AQ" && !regionById.has("AQ"))
@@ -46741,11 +47858,13 @@ function layoutMap(resolved, data, size, opts) {
46741
47858
  const r = regionById.get(iso);
46742
47859
  const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
46743
47860
  if (!viewF) continue;
46744
- const d = path(viewF) ?? "";
47861
+ const raw = path(viewF) ?? "";
47862
+ const d = fitIsGlobal ? dropAntimeridianWrapSlivers(raw, width, height) : raw;
46745
47863
  if (!d) continue;
46746
47864
  const isThisLayer = r?.layer === layerKind;
46747
47865
  const isForeign = layerKind === "country" && usContext && iso !== "US";
46748
- let fill2 = isForeign ? foreignFill : neutralFill;
47866
+ const baseFill = isForeign ? foreignFill : neutralFill;
47867
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? baseFill : baseFill;
46749
47868
  let label;
46750
47869
  let lineNumber = -1;
46751
47870
  let layer = "base";
@@ -46754,15 +47873,21 @@ function layoutMap(resolved, data, size, opts) {
46754
47873
  lineNumber = r.lineNumber;
46755
47874
  layer = layerKind;
46756
47875
  label = r.name;
47876
+ } else {
47877
+ label = f.properties?.name;
46757
47878
  }
47879
+ const labelAnchor = WORLD_LABEL_ANCHORS[iso];
47880
+ const c = labelAnchor ? project(labelAnchor[0], labelAnchor[1]) : path.centroid(viewF);
47881
+ const hasCentroid = c != null && Number.isFinite(c[0]) && Number.isFinite(c[1]);
46758
47882
  regions.push({
46759
47883
  id: iso,
46760
47884
  d,
46761
47885
  fill: fill2,
46762
- stroke: regionStroke,
47886
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46763
47887
  lineNumber,
46764
47888
  layer,
46765
47889
  ...label !== void 0 && { label },
47890
+ ...hasCentroid && { labelX: c[0], labelY: c[1] },
46766
47891
  ...isThisLayer && r.value !== void 0 && { value: r.value },
46767
47892
  ...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
46768
47893
  });
@@ -46787,9 +47912,41 @@ function layoutMap(resolved, data, size, opts) {
46787
47912
  });
46788
47913
  }
46789
47914
  }
47915
+ const pointInRings = (px, py, rings) => {
47916
+ let inside = false;
47917
+ for (const ring of rings) {
47918
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
47919
+ const [xi, yi] = ring[i];
47920
+ const [xj, yj] = ring[j];
47921
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
47922
+ inside = !inside;
47923
+ }
47924
+ }
47925
+ return inside;
47926
+ };
47927
+ const fillHitTargets = [...regions, ...insetRegions].map((r) => ({
47928
+ fill: r.fill,
47929
+ rings: parsePathRings(r.d)
47930
+ }));
47931
+ const fillAt = (x, y) => {
47932
+ let hit = water;
47933
+ for (const t of fillHitTargets)
47934
+ if (pointInRings(x, y, t.rings)) hit = t.fill;
47935
+ return hit;
47936
+ };
47937
+ const labelOnFill = (fill2) => {
47938
+ const color = contrastRatio(fill2, palette.textOnFillDark) >= contrastRatio(fill2, palette.textOnFillLight) ? palette.textOnFillDark : palette.textOnFillLight;
47939
+ const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47940
+ return {
47941
+ color,
47942
+ halo: contrastRatio(fill2, color) < REGION_LABEL_HALO_RATIO,
47943
+ haloColor
47944
+ };
47945
+ };
47946
+ const reliefAllowed = resolved.directives.noRelief !== true;
46790
47947
  const relief = [];
46791
47948
  let reliefHatch = null;
46792
- if (resolved.directives.relief === true && data.mountainRanges) {
47949
+ if (reliefAllowed && data.mountainRanges) {
46793
47950
  for (const [, f] of decodeLayer(data.mountainRanges)) {
46794
47951
  const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
46795
47952
  if (!viewF) continue;
@@ -46805,16 +47962,32 @@ function layoutMap(resolved, data, size, opts) {
46805
47962
  if (relief.length) {
46806
47963
  const darkTone = isDark ? palette.bg : palette.text;
46807
47964
  const lightTone = isDark ? palette.text : palette.bg;
46808
- const landLum = relativeLuminance(neutralFill);
47965
+ const reliefLandRef = colorizeActive ? isDark ? palette.surface : palette.bg : neutralFill;
47966
+ const landLum = relativeLuminance(reliefLandRef);
46809
47967
  const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
46810
47968
  reliefHatch = {
46811
- color: mix(tone, neutralFill, RELIEF_HATCH_STRENGTH),
47969
+ color: mix(tone, reliefLandRef, RELIEF_HATCH_STRENGTH),
46812
47970
  spacing: RELIEF_HATCH_SPACING,
46813
47971
  width: RELIEF_HATCH_WIDTH
46814
47972
  };
46815
47973
  }
46816
47974
  }
46817
- const riverColor = mix(water, regionStroke, 16);
47975
+ let coastlineStyle = null;
47976
+ if (resolved.directives.noCoastline !== true) {
47977
+ const minDim = Math.min(width, height);
47978
+ coastlineStyle = {
47979
+ color: mix(regionStroke, water, COASTLINE_STROKE_MIX),
47980
+ // N equal-width rings: distance steps outward by COASTLINE_STEP; opacity
47981
+ // fades linearly from NEAR (innermost) to FAR (outermost).
47982
+ lines: Array.from({ length: COASTLINE_RING_COUNT }, (_, k) => ({
47983
+ d: (COASTLINE_D0 + k * COASTLINE_STEP) * minDim,
47984
+ thickness: COASTLINE_THICKNESS * minDim,
47985
+ opacity: COASTLINE_OPACITY_NEAR + (COASTLINE_OPACITY_FAR - COASTLINE_OPACITY_NEAR) * k / (COASTLINE_RING_COUNT - 1)
47986
+ })),
47987
+ minExtent: (isGlobalView ? COASTLINE_MIN_EXTENT_GLOBAL : COASTLINE_MIN_EXTENT) * minDim
47988
+ };
47989
+ }
47990
+ const riverColor = mix(palette.colors.blue, water, 32);
46818
47991
  const rivers = [];
46819
47992
  if (data.rivers) {
46820
47993
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -46870,38 +48043,108 @@ function layoutMap(resolved, data, size, opts) {
46870
48043
  const xy = project(p.lon, p.lat);
46871
48044
  if (xy) projected.push({ p, xy });
46872
48045
  }
46873
- const coloGroups = /* @__PURE__ */ new Map();
48046
+ const placePoi = (e, cx, cy, clusterId) => {
48047
+ const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
48048
+ poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
48049
+ const num = routeNumberById.get(e.p.id);
48050
+ pois.push({
48051
+ id: e.p.id,
48052
+ cx,
48053
+ cy,
48054
+ r: radiusFor(e.p),
48055
+ fill: fill2,
48056
+ stroke: stroke2,
48057
+ lineNumber: e.p.lineNumber,
48058
+ implicit: !!e.p.implicit,
48059
+ isOrigin: originIds.has(e.p.id),
48060
+ ...num !== void 0 && { routeNumber: num },
48061
+ ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags },
48062
+ ...clusterId !== void 0 && { clusterId }
48063
+ });
48064
+ };
48065
+ const clusters = [];
48066
+ const connected = /* @__PURE__ */ new Set();
48067
+ for (const e of resolved.edges) {
48068
+ connected.add(e.fromId);
48069
+ connected.add(e.toId);
48070
+ }
48071
+ for (const rt of resolved.routes) {
48072
+ rt.stopIds.forEach((id) => connected.add(id));
48073
+ }
48074
+ const radiusOf = (e) => radiusFor(e.p);
46874
48075
  for (const e of projected) {
46875
- const key = `${Math.round(e.xy[0] / COLO_EPS)},${Math.round(e.xy[1] / COLO_EPS)}`;
46876
- const arr = coloGroups.get(key);
46877
- if (arr) arr.push(e);
46878
- else coloGroups.set(key, [e]);
46879
- }
46880
- for (const group of coloGroups.values()) {
46881
- group.forEach((e, i) => {
46882
- let cx = e.xy[0];
46883
- let cy = e.xy[1];
46884
- if (group.length > 1) {
46885
- const ang = i * GOLDEN_ANGLE;
46886
- cx += Math.cos(ang) * COLO_R;
46887
- cy += Math.sin(ang) * COLO_R;
46888
- }
46889
- const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
46890
- poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
46891
- const num = routeNumberById.get(e.p.id);
46892
- pois.push({
46893
- id: e.p.id,
46894
- cx,
46895
- cy,
46896
- r: radiusFor(e.p),
46897
- fill: fill2,
46898
- stroke: stroke2,
46899
- lineNumber: e.p.lineNumber,
46900
- implicit: !!e.p.implicit,
46901
- isOrigin: originIds.has(e.p.id),
46902
- ...num !== void 0 && { routeNumber: num },
46903
- ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags }
46904
- });
48076
+ if (connected.has(e.p.id)) placePoi(e, e.xy[0], e.xy[1]);
48077
+ }
48078
+ const groups = [];
48079
+ for (const e of projected) {
48080
+ if (connected.has(e.p.id)) continue;
48081
+ const r = radiusOf(e);
48082
+ const near = groups.find(
48083
+ (g) => g.some(
48084
+ (q) => Math.hypot(q.xy[0] - e.xy[0], q.xy[1] - e.xy[1]) < (r + radiusOf(q)) * STACK_OVERLAP
48085
+ )
48086
+ );
48087
+ if (near) near.push(e);
48088
+ else groups.push([e]);
48089
+ }
48090
+ for (const g of groups) {
48091
+ if (g.length === 1) {
48092
+ placePoi(g[0], g[0].xy[0], g[0].xy[1]);
48093
+ continue;
48094
+ }
48095
+ const clusterId = g[0].p.id;
48096
+ const cx0 = g.reduce((s, e) => s + e.xy[0], 0) / g.length;
48097
+ const cy0 = g.reduce((s, e) => s + e.xy[1], 0) / g.length;
48098
+ const maxR = Math.max(...g.map(radiusOf));
48099
+ const sep = 2 * maxR + STACK_RING_GAP;
48100
+ const ringR = Math.max(
48101
+ COLO_R,
48102
+ sep / (2 * Math.sin(Math.PI / Math.max(g.length, 2)))
48103
+ );
48104
+ const positions = g.map((e, i) => {
48105
+ if (g.length <= STACK_RING_MAX) {
48106
+ const ang2 = -Math.PI / 2 + i * 2 * Math.PI / g.length;
48107
+ return {
48108
+ e,
48109
+ mx: cx0 + Math.cos(ang2) * ringR,
48110
+ my: cy0 + Math.sin(ang2) * ringR
48111
+ };
48112
+ }
48113
+ const ang = i * GOLDEN_ANGLE;
48114
+ const rr = ringR * Math.sqrt((i + 1) / g.length);
48115
+ return { e, mx: cx0 + Math.cos(ang) * rr, my: cy0 + Math.sin(ang) * rr };
48116
+ });
48117
+ let minX = cx0 - maxR;
48118
+ let maxX = cx0 + maxR;
48119
+ let minY = cy0 - maxR;
48120
+ let maxY = cy0 + maxR;
48121
+ for (const { mx, my, e } of positions) {
48122
+ const r = radiusOf(e);
48123
+ minX = Math.min(minX, mx - r);
48124
+ maxX = Math.max(maxX, mx + r);
48125
+ minY = Math.min(minY, my - r);
48126
+ maxY = Math.max(maxY, my + r);
48127
+ }
48128
+ let dx = 0;
48129
+ let dy = 0;
48130
+ if (minX + dx < 2) dx = 2 - minX;
48131
+ if (maxX + dx > width - 2) dx = width - 2 - maxX;
48132
+ if (minY + dy < 2) dy = 2 - minY;
48133
+ if (maxY + dy > height - 2) dy = height - 2 - maxY;
48134
+ const legsOut = [];
48135
+ for (const { e, mx, my } of positions) {
48136
+ const fx = mx + dx;
48137
+ const fy = my + dy;
48138
+ placePoi(e, fx, fy, clusterId);
48139
+ legsOut.push({ x2: fx, y2: fy, color: poiFill(e.p).fill });
48140
+ }
48141
+ clusters.push({
48142
+ id: clusterId,
48143
+ cx: cx0 + dx,
48144
+ cy: cy0 + dy,
48145
+ count: g.length,
48146
+ hitR: ringR + maxR + 6,
48147
+ legs: legsOut
46905
48148
  });
46906
48149
  }
46907
48150
  const legs = [];
@@ -46951,16 +48194,26 @@ function layoutMap(resolved, data, size, opts) {
46951
48194
  if (!a || !b) continue;
46952
48195
  const mx = (a.cx + b.cx) / 2;
46953
48196
  const my = (a.cy + b.cy) / 2;
48197
+ const bow = {
48198
+ curved: leg.style === "arc",
48199
+ offset: 0,
48200
+ labelX: mx,
48201
+ labelY: my - 4
48202
+ };
48203
+ const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46954
48204
  legs.push({
46955
- d: legPath(a, b, leg.style === "arc", 0),
48205
+ d: legPath(a, b, bow.curved, bow.offset),
46956
48206
  width: routeWidthFor(Number(leg.value)),
46957
48207
  color: mix(palette.text, palette.bg, 72),
46958
48208
  arrow: true,
46959
48209
  lineNumber: leg.lineNumber,
46960
48210
  ...leg.label !== void 0 && {
46961
48211
  label: leg.label,
46962
- labelX: mx,
46963
- labelY: my - 4
48212
+ labelX: bow.labelX,
48213
+ labelY: bow.labelY,
48214
+ labelColor: routeLabelStyle.color,
48215
+ labelHalo: routeLabelStyle.halo,
48216
+ labelHaloColor: routeLabelStyle.haloColor
46964
48217
  }
46965
48218
  });
46966
48219
  }
@@ -46988,20 +48241,29 @@ function layoutMap(resolved, data, size, opts) {
46988
48241
  const a = poiScreen.get(e.fromId);
46989
48242
  const b = poiScreen.get(e.toId);
46990
48243
  if (!a || !b) return;
46991
- const curved = e.style === "arc" || n > 1;
46992
- const offset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
48244
+ const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
46993
48245
  const mx = (a.cx + b.cx) / 2;
46994
48246
  const my = (a.cy + b.cy) / 2;
48247
+ const bow = {
48248
+ curved: e.style === "arc" || n > 1,
48249
+ offset: fanOffset,
48250
+ labelX: mx,
48251
+ labelY: my - 4
48252
+ };
48253
+ const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46995
48254
  legs.push({
46996
- d: legPath(a, b, curved, offset),
48255
+ d: legPath(a, b, bow.curved, bow.offset),
46997
48256
  width: widthFor(e),
46998
48257
  color: mix(palette.text, palette.bg, 66),
46999
48258
  arrow: e.directed,
47000
48259
  lineNumber: e.lineNumber,
47001
48260
  ...e.label !== void 0 && {
47002
48261
  label: e.label,
47003
- labelX: mx,
47004
- labelY: my - 4
48262
+ labelX: bow.labelX,
48263
+ labelY: bow.labelY,
48264
+ labelColor: edgeLabelStyle.color,
48265
+ labelHalo: edgeLabelStyle.halo,
48266
+ labelHaloColor: edgeLabelStyle.haloColor
47005
48267
  }
47006
48268
  });
47007
48269
  });
@@ -47043,48 +48305,73 @@ function layoutMap(resolved, data, size, opts) {
47043
48305
  }
47044
48306
  }
47045
48307
  const collides = (rect) => markers.some((m) => rectCircleOverlap(rect, m)) || obstacles.some((o) => rectsOverlap(rect, o)) || legSegments.some((s) => segmentRectOverlap(s[0], s[1], s[2], s[3], rect));
47046
- const regionLabelMode = resolved.directives.regionLabels ?? "off";
48308
+ const showRegionLabels = resolved.directives.noRegionLabels !== true;
48309
+ const isCompact = width < COMPACT_WIDTH_PX;
47047
48310
  const LABEL_PADX = 6;
47048
48311
  const LABEL_PADY = 3;
47049
- const labelW = (text) => measureLegendText(text, FONT) + 2 * LABEL_PADX;
47050
- const labelH = FONT + 2 * LABEL_PADY;
48312
+ const labelW = (text) => measureLegendText(text, FONT2) + 2 * LABEL_PADX;
48313
+ const labelH = FONT2 + 2 * LABEL_PADY;
47051
48314
  const pushRegionLabel = (x, y, text, fill2, lineNumber) => {
47052
- const color = contrastText(
47053
- fill2,
47054
- palette.textOnFillLight,
47055
- palette.textOnFillDark
48315
+ const { color, haloColor } = labelOnFill(fill2);
48316
+ const halfW = measureLegendText(text, FONT2) / 2;
48317
+ const overflows = [y - FONT2 * 0.55, y - FONT2 * 0.1].some(
48318
+ (sy) => fillAt(x - halfW, sy) !== fill2 || fillAt(x + halfW, sy) !== fill2
47056
48319
  );
47057
- const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47058
48320
  labels.push({
47059
48321
  x,
47060
48322
  y,
47061
48323
  text,
47062
48324
  anchor: "middle",
47063
48325
  color,
47064
- halo: true,
48326
+ halo: overflows,
47065
48327
  haloColor,
47066
48328
  lineNumber
47067
48329
  });
47068
48330
  };
47069
- const WORLD_LABEL_ANCHORS = {
47070
- US: [-98.5, 39.5]
47071
- // CONUS geographic centre (near Lebanon, Kansas)
48331
+ const REGION_LABEL_GAP = 2;
48332
+ const regionLabelRect = (cx, cy, text) => {
48333
+ const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
48334
+ return { x: cx - w / 2, y: cy - FONT2 / 2, w, h: FONT2 };
47072
48335
  };
47073
- if (regionLabelMode === "full" || regionLabelMode === "abbrev") {
47074
- for (const r of regions) {
47075
- if (r.layer === "base" || r.label === void 0) continue;
47076
- const f = r.layer === "us-state" ? usLayer?.get(r.id) : worldLayer.get(r.id);
47077
- if (!f) continue;
48336
+ if (showRegionLabels) {
48337
+ const frameContainers = new Set(resolved.poiFrameContainers);
48338
+ const entries = regions.map((r) => {
48339
+ const isContainer = frameContainers.has(r.id);
48340
+ if (r.layer === "base" && !isContainer || r.label === void 0)
48341
+ return null;
48342
+ const isUsState = r.layer === "us-state" || r.id.startsWith("US-");
48343
+ const f = isUsState ? usLayer?.get(r.id) : worldLayer.get(r.id);
48344
+ if (!f) return null;
47078
48345
  const [[x0, y0], [x1, y1]] = path.bounds(f);
47079
- const text = regionLabelMode === "abbrev" ? r.id.replace(/^US-/, "") : r.label;
47080
- if (labelW(text) > x1 - x0 || labelH > y1 - y0) continue;
47081
- const anchor = r.layer !== "us-state" ? WORLD_LABEL_ANCHORS[r.id] : void 0;
48346
+ const boxW = x1 - x0;
48347
+ const boxH = y1 - y0;
48348
+ const abbrev = isUsState ? r.id.replace(/^US-/, "") : void 0;
48349
+ const candidates = abbrev !== void 0 ? isCompact ? [abbrev, r.label] : [r.label, abbrev] : [r.label];
48350
+ const anchor = !isUsState ? WORLD_LABEL_ANCHORS[r.id] : void 0;
47082
48351
  const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
47083
- if (!c || !Number.isFinite(c[0])) continue;
48352
+ if (!c || !Number.isFinite(c[0])) return null;
48353
+ return { r, c, boxW, boxH, area: boxW * boxH, candidates };
48354
+ }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
48355
+ const placedRegionRects = [];
48356
+ const POI_LABEL_PAD = 14;
48357
+ const poiObstacles = pois.map((p) => ({
48358
+ x: p.cx - p.r - POI_LABEL_PAD,
48359
+ y: p.cy - p.r - POI_LABEL_PAD,
48360
+ w: 2 * (p.r + POI_LABEL_PAD),
48361
+ h: 2 * (p.r + POI_LABEL_PAD)
48362
+ }));
48363
+ for (const { r, c, boxW, boxH, candidates } of entries) {
48364
+ const text = candidates.find((t) => {
48365
+ if (labelW(t) > boxW || labelH > boxH) return false;
48366
+ const rect = regionLabelRect(c[0], c[1], t);
48367
+ return !placedRegionRects.some((p) => rectsOverlap(rect, p)) && !poiObstacles.some((o) => rectsOverlap(rect, o));
48368
+ });
48369
+ if (text === void 0) continue;
48370
+ placedRegionRects.push(regionLabelRect(c[0], c[1], text));
47084
48371
  pushRegionLabel(c[0], c[1], text, r.fill, r.lineNumber);
47085
48372
  }
47086
48373
  for (const seed of insetLabelSeeds) {
47087
- const text = regionLabelMode === "abbrev" ? seed.iso.replace(/^US-/, "") : seed.name;
48374
+ const text = isCompact ? seed.iso.replace(/^US-/, "") : seed.name;
47088
48375
  const src = regionById.get(seed.iso);
47089
48376
  pushRegionLabel(
47090
48377
  seed.x,
@@ -47095,22 +48382,26 @@ function layoutMap(resolved, data, size, opts) {
47095
48382
  );
47096
48383
  }
47097
48384
  }
47098
- const poiLabelMode = resolved.directives.poiLabels ?? "auto";
47099
- if (poiLabelMode !== "off") {
47100
- const ordered = [...pois].sort(
47101
- (a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1)
47102
- );
48385
+ if (resolved.directives.noPoiLabels !== true) {
48386
+ const ordered = [...pois].filter((p) => p.clusterId === void 0).sort((a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1));
47103
48387
  const poiById = new Map(resolved.pois.map((q) => [q.id, q]));
47104
48388
  const labelText = (p) => {
47105
48389
  const src = poiById.get(p.id);
47106
48390
  return src?.label ?? src?.name ?? p.id;
47107
48391
  };
47108
- const poiLabH = FONT * 1.25;
48392
+ const poiLabH = FONT2 * 1.25;
47109
48393
  const labelInfo = (p) => {
47110
48394
  const text = labelText(p);
47111
- return { text, w: measureLegendText(text, FONT) };
48395
+ return { text, w: measureLegendText(text, FONT2) };
47112
48396
  };
47113
48397
  const GAP = 3;
48398
+ const clusterMembersById = /* @__PURE__ */ new Map();
48399
+ for (const p of pois) {
48400
+ if (p.clusterId === void 0) continue;
48401
+ const arr = clusterMembersById.get(p.clusterId);
48402
+ if (arr) arr.push(p);
48403
+ else clusterMembersById.set(p.clusterId, [p]);
48404
+ }
47114
48405
  const inlineRect = (p, w, side) => {
47115
48406
  switch (side) {
47116
48407
  case "right":
@@ -47140,11 +48431,11 @@ function layoutMap(resolved, data, size, opts) {
47140
48431
  const x = side === "right" ? rect.x : side === "left" ? rect.x + w : p.cx;
47141
48432
  labels.push({
47142
48433
  x,
47143
- y: rect.y + poiLabH / 2 + FONT / 3,
48434
+ y: rect.y + poiLabH / 2 + FONT2 / 3,
47144
48435
  text,
47145
48436
  anchor,
47146
48437
  color: palette.text,
47147
- halo: true,
48438
+ halo: false,
47148
48439
  haloColor: palette.bg,
47149
48440
  poiId: p.id,
47150
48441
  lineNumber: p.lineNumber
@@ -47155,43 +48446,60 @@ function layoutMap(resolved, data, size, opts) {
47155
48446
  return rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect);
47156
48447
  };
47157
48448
  const GROUP_R = 30;
47158
- const groups = [];
48449
+ const groups2 = [];
47159
48450
  for (const p of ordered) {
47160
- const near = groups.find(
48451
+ const near = groups2.find(
47161
48452
  (g) => g.some((q) => Math.hypot(q.cx - p.cx, q.cy - p.cy) < GROUP_R)
47162
48453
  );
47163
48454
  if (near) near.push(p);
47164
- else groups.push([p]);
48455
+ else groups2.push([p]);
47165
48456
  }
47166
48457
  const ROW_GAP2 = 3;
47167
48458
  const step = poiLabH + ROW_GAP2;
47168
48459
  const COL_GAP = 16;
47169
- const placeColumn = (group) => {
47170
- const items = group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48460
+ const makeItems = (group) => group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48461
+ const columnRows = (items, side) => {
47171
48462
  const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
47172
48463
  const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
47173
- const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
47174
48464
  const maxW = Math.max(...items.map((o) => o.w));
47175
- const side = right + COL_GAP + maxW <= width - 2 ? "right" : "left";
47176
- const colX = side === "right" ? right + COL_GAP : left - COL_GAP;
48465
+ const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
48466
+ const colX = side === "right" ? Math.min(right + COL_GAP, width - 2 - maxW) : Math.max(left - COL_GAP, 2 + maxW);
47177
48467
  const totalH = items.length * step;
47178
48468
  let startY = cyMid - totalH / 2;
47179
48469
  startY = Math.max(2, Math.min(startY, height - totalH - 2));
47180
- items.forEach((o, i) => {
48470
+ return items.map((o, i) => {
47181
48471
  const rowCy = startY + i * step + step / 2;
47182
- obstacles.push({
47183
- x: side === "right" ? colX : colX - o.w,
47184
- y: rowCy - poiLabH / 2,
47185
- w: o.w,
47186
- h: poiLabH
47187
- });
48472
+ return {
48473
+ o,
48474
+ colX,
48475
+ rowCy,
48476
+ rect: {
48477
+ x: side === "right" ? colX : colX - o.w,
48478
+ y: rowCy - poiLabH / 2,
48479
+ w: o.w,
48480
+ h: poiLabH
48481
+ }
48482
+ };
48483
+ });
48484
+ };
48485
+ const wouldColumnBeClean = (items, side) => columnRows(items, side).every(
48486
+ ({ rect }) => rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect)
48487
+ );
48488
+ const defaultColumnSide = (items) => {
48489
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48490
+ const maxW = Math.max(...items.map((o) => o.w));
48491
+ return right + COL_GAP + maxW <= width - 2 ? "right" : "left";
48492
+ };
48493
+ const commitColumn = (items, side, clusterId) => {
48494
+ for (const { o, colX, rowCy, rect } of columnRows(items, side)) {
48495
+ obstacles.push(rect);
47188
48496
  labels.push({
47189
48497
  x: colX,
47190
- y: rowCy + FONT / 3,
48498
+ y: rowCy + FONT2 / 3,
47191
48499
  text: o.text,
47192
48500
  anchor: side === "right" ? "start" : "end",
47193
48501
  color: palette.text,
47194
- halo: true,
48502
+ halo: false,
47195
48503
  haloColor: palette.bg,
47196
48504
  leader: {
47197
48505
  x1: o.p.cx,
@@ -47201,24 +48509,141 @@ function layoutMap(resolved, data, size, opts) {
47201
48509
  },
47202
48510
  leaderColor: o.p.fill,
47203
48511
  poiId: o.p.id,
47204
- lineNumber: o.p.lineNumber
48512
+ lineNumber: o.p.lineNumber,
48513
+ ...clusterId !== void 0 && { clusterMember: clusterId }
47205
48514
  });
48515
+ }
48516
+ };
48517
+ const pushHidden = (p) => {
48518
+ const { text, w } = labelInfo(p);
48519
+ let x = p.cx + p.r + GAP;
48520
+ let anchor = "start";
48521
+ if (x + w > width) {
48522
+ x = p.cx - p.r - GAP - w;
48523
+ anchor = "end";
48524
+ }
48525
+ const y = Math.max(0, Math.min(p.cy - poiLabH / 2, height - poiLabH));
48526
+ labels.push({
48527
+ x: anchor === "start" ? x : x + w,
48528
+ y: y + poiLabH / 2 + FONT2 / 3,
48529
+ text,
48530
+ anchor,
48531
+ color: palette.text,
48532
+ halo: false,
48533
+ haloColor: palette.bg,
48534
+ poiId: p.id,
48535
+ hidden: true,
48536
+ lineNumber: p.lineNumber
47206
48537
  });
47207
48538
  };
47208
- for (const g of groups) {
48539
+ for (const [clusterId, members] of clusterMembersById) {
48540
+ if (members.length === 0) continue;
48541
+ const items = makeItems(members);
48542
+ const side = wouldColumnBeClean(items, "right") ? "right" : wouldColumnBeClean(items, "left") ? "left" : defaultColumnSide(items);
48543
+ commitColumn(items, side, clusterId);
48544
+ }
48545
+ const maxExtent = MAX_CLUSTER_EXTENT_FACTOR * Math.min(width, height);
48546
+ const clusterPending = [];
48547
+ for (const g of groups2) {
48548
+ const items = makeItems(g);
47209
48549
  if (g.length === 1) {
47210
- const p = g[0];
47211
- const { text, w } = labelInfo(p);
48550
+ const { p, text, w } = items[0];
47212
48551
  const side = ["right", "left", "above", "below"].find(
47213
48552
  (s) => inlineFits(p, w, s)
47214
48553
  );
47215
- if (side) {
47216
- pushInline(p, text, w, side);
47217
- continue;
48554
+ if (side) pushInline(p, text, w, side);
48555
+ else commitColumn(items, defaultColumnSide(items));
48556
+ continue;
48557
+ }
48558
+ const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
48559
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48560
+ const minCy = Math.min(...items.map((o) => o.p.cy));
48561
+ const maxCy = Math.max(...items.map((o) => o.p.cy));
48562
+ const diag = Math.hypot(right - left, maxCy - minCy);
48563
+ if (diag > maxExtent || items.length > MAX_COLUMN_ROWS) {
48564
+ items.forEach((o) => pushHidden(o.p));
48565
+ } else {
48566
+ clusterPending.push(items);
48567
+ }
48568
+ }
48569
+ for (const items of clusterPending) {
48570
+ const side = ["right", "left"].find(
48571
+ (s) => wouldColumnBeClean(items, s)
48572
+ );
48573
+ if (side) commitColumn(items, side);
48574
+ else items.forEach((o) => pushHidden(o.p));
48575
+ }
48576
+ }
48577
+ if (resolved.directives.noContextLabels !== true) {
48578
+ for (const l of labels) {
48579
+ if (l.hidden) continue;
48580
+ const w = labelW(l.text);
48581
+ const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
48582
+ obstacles.push({ x, y: l.y - labelH / 2, w, h: labelH });
48583
+ }
48584
+ for (const box of insets)
48585
+ obstacles.push({ x: box.x, y: box.y, w: box.w, h: box.h });
48586
+ const countryCandidates = [];
48587
+ for (const f of worldLayer.values()) {
48588
+ const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
48589
+ if (!iso || regionById.has(iso)) continue;
48590
+ let hasReferencedSub = false;
48591
+ for (const k of regionById.keys())
48592
+ if (k.startsWith(iso + "-")) {
48593
+ hasReferencedSub = true;
48594
+ break;
47218
48595
  }
48596
+ if (hasReferencedSub) continue;
48597
+ const b = path.bounds(f);
48598
+ const [x0, y0] = b[0];
48599
+ const [x1, y1] = b[1];
48600
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48601
+ const anchorLngLat = WORLD_LABEL_ANCHORS[iso];
48602
+ const a = anchorLngLat ? project(anchorLngLat[0], anchorLngLat[1]) : path.centroid(f);
48603
+ countryCandidates.push({
48604
+ name: f.properties?.name ?? iso,
48605
+ bbox: [x0, y0, x1, y1],
48606
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48607
+ });
48608
+ }
48609
+ const framedStateContainers = (resolved.poiFrameContainers ?? []).some(
48610
+ (id) => id.startsWith("US-")
48611
+ );
48612
+ if (usLayer && framedStateContainers) {
48613
+ const containerSet = new Set(resolved.poiFrameContainers);
48614
+ for (const [iso, f] of usLayer) {
48615
+ if (containerSet.has(iso) || regionById.has(iso)) continue;
48616
+ const viewF = cullFeatureToView(f);
48617
+ if (!viewF) continue;
48618
+ const b = path.bounds(viewF);
48619
+ const [x0, y0] = b[0];
48620
+ const [x1, y1] = b[1];
48621
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48622
+ const a = path.centroid(viewF);
48623
+ countryCandidates.push({
48624
+ name: f.properties?.name ?? iso,
48625
+ bbox: [x0, y0, x1, y1],
48626
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48627
+ });
47219
48628
  }
47220
- placeColumn(g);
47221
48629
  }
48630
+ const contextLabels = placeContextLabels({
48631
+ projection: resolved.projection,
48632
+ dLonSpan,
48633
+ dLatSpan,
48634
+ width,
48635
+ height,
48636
+ waterBodies: data.waterBodies,
48637
+ countries: countryCandidates,
48638
+ palette,
48639
+ project,
48640
+ collides,
48641
+ // Water labels must stay over open water — `fillAt` returns the ocean
48642
+ // backdrop colour off-land and a region fill on-land (lakes/states count
48643
+ // as land here, which is the safe side for an ocean name).
48644
+ overLand: (x, y) => fillAt(x, y) !== water
48645
+ });
48646
+ labels.push(...contextLabels);
47222
48647
  }
47223
48648
  let legend = null;
47224
48649
  if (!resolved.directives.noLegend) {
@@ -47255,58 +48680,102 @@ function layoutMap(resolved, data, size, opts) {
47255
48680
  rivers,
47256
48681
  relief,
47257
48682
  reliefHatch,
48683
+ coastlineStyle,
47258
48684
  legs,
47259
48685
  pois,
48686
+ clusters,
47260
48687
  labels,
47261
48688
  legend,
47262
48689
  insets,
47263
48690
  insetRegions,
47264
48691
  projection,
47265
- stretch: stretchParams
48692
+ stretch: stretchParams,
48693
+ diagnostics: []
47266
48694
  };
47267
48695
  }
47268
- var FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, COLO_EPS, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, FAN_STEP, ARC_CURVE_FRAC, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, US_NON_CONUS;
48696
+ var FIT_PAD, RAMP_FLOOR2, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT2, WORLD_LABEL_ANCHORS, MAX_CLUSTER_EXTENT_FACTOR, MAX_COLUMN_ROWS, REGION_LABEL_HALO_RATIO, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, COMPACT_WIDTH_PX, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, COASTLINE_RING_COUNT, COASTLINE_D0, COASTLINE_STEP, COASTLINE_THICKNESS, COASTLINE_OPACITY_NEAR, COASTLINE_OPACITY_FAR, COASTLINE_MIN_EXTENT, COASTLINE_MIN_EXTENT_GLOBAL, COASTLINE_STROKE_MIX, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, STACK_OVERLAP, STACK_RING_MAX, STACK_RING_GAP, FAN_STEP, ARC_CURVE_FRAC, decodeCache, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, inAlaska, inHawaii, FOREIGN_BORDER, US_NON_CONUS;
47269
48697
  var init_layout15 = __esm({
47270
48698
  "src/map/layout.ts"() {
47271
48699
  "use strict";
47272
48700
  init_color_utils();
48701
+ init_geo();
48702
+ init_colorize();
47273
48703
  init_colors();
47274
48704
  init_label_layout();
47275
48705
  init_legend_constants();
47276
48706
  init_title_constants();
48707
+ init_context_labels();
47277
48708
  FIT_PAD = 24;
47278
- RAMP_FLOOR = 15;
48709
+ RAMP_FLOOR2 = 15;
47279
48710
  R_DEFAULT = 6;
47280
48711
  R_MIN = 4;
47281
48712
  R_MAX = 22;
47282
48713
  W_MIN = 1.25;
47283
48714
  W_MAX = 8;
47284
- FONT = 11;
47285
- COLO_EPS = 1.5;
48715
+ FONT2 = 11;
48716
+ WORLD_LABEL_ANCHORS = {
48717
+ US: [-98.5, 39.5]
48718
+ // CONUS geographic centre (near Lebanon, Kansas)
48719
+ };
48720
+ MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48721
+ MAX_COLUMN_ROWS = 7;
48722
+ REGION_LABEL_HALO_RATIO = 4.5;
47286
48723
  LAND_TINT_LIGHT = 12;
47287
48724
  LAND_TINT_DARK = 24;
47288
48725
  TAG_TINT_LIGHT = 60;
47289
48726
  TAG_TINT_DARK = 68;
47290
- WATER_TINT_LIGHT = 13;
47291
- WATER_TINT_DARK = 14;
48727
+ WATER_TINT_LIGHT = 24;
48728
+ WATER_TINT_DARK = 24;
47292
48729
  RIVER_WIDTH = 1.3;
48730
+ COMPACT_WIDTH_PX = 480;
47293
48731
  RELIEF_MIN_AREA = 12;
47294
48732
  RELIEF_MIN_DIM = 2;
47295
- RELIEF_HATCH_SPACING = 3;
47296
- RELIEF_HATCH_WIDTH = 0.25;
47297
- RELIEF_HATCH_STRENGTH = 32;
48733
+ RELIEF_HATCH_SPACING = 1.5;
48734
+ RELIEF_HATCH_WIDTH = 0.2;
48735
+ RELIEF_HATCH_STRENGTH = 26;
48736
+ COASTLINE_RING_COUNT = 5;
48737
+ COASTLINE_D0 = 16e-4;
48738
+ COASTLINE_STEP = 28e-4;
48739
+ COASTLINE_THICKNESS = 14e-4;
48740
+ COASTLINE_OPACITY_NEAR = 0.5;
48741
+ COASTLINE_OPACITY_FAR = 0.1;
48742
+ COASTLINE_MIN_EXTENT = 6e-4;
48743
+ COASTLINE_MIN_EXTENT_GLOBAL = 6e-4;
48744
+ COASTLINE_STROKE_MIX = 32;
47298
48745
  FOREIGN_TINT_LIGHT = 30;
47299
48746
  FOREIGN_TINT_DARK = 62;
47300
48747
  MUTED_FOREIGN_LIGHT = 28;
47301
48748
  MUTED_FOREIGN_DARK = 16;
47302
48749
  COLO_R = 9;
47303
48750
  GOLDEN_ANGLE = 2.399963229728653;
48751
+ STACK_OVERLAP = 1;
48752
+ STACK_RING_MAX = 8;
48753
+ STACK_RING_GAP = 4;
47304
48754
  FAN_STEP = 16;
47305
48755
  ARC_CURVE_FRAC = 0.18;
48756
+ decodeCache = /* @__PURE__ */ new WeakMap();
47306
48757
  usConusProjection = () => geoConicEqualArea().parallels([29.5, 45.5]).rotate([96, 0]);
47307
48758
  alaskaProjection = () => geoConicEqualArea().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
47308
48759
  hawaiiProjection = () => geoMercator();
47309
48760
  INSET_STATES = /* @__PURE__ */ new Set(["US-AK", "US-HI"]);
48761
+ inAlaska = (lon, lat) => lat >= 51 && (lon <= -129 || lon >= 172);
48762
+ inHawaii = (lon, lat) => lat >= 18 && lat <= 23 && lon >= -161 && lon <= -154;
48763
+ FOREIGN_BORDER = {
48764
+ CA: [
48765
+ "US-AK",
48766
+ "US-WA",
48767
+ "US-ID",
48768
+ "US-MT",
48769
+ "US-ND",
48770
+ "US-MN",
48771
+ "US-MI",
48772
+ "US-NY",
48773
+ "US-VT",
48774
+ "US-NH",
48775
+ "US-ME"
48776
+ ],
48777
+ MX: ["US-CA", "US-AZ", "US-NM", "US-TX"]
48778
+ };
47310
48779
  US_NON_CONUS = /* @__PURE__ */ new Set([
47311
48780
  "US-AK",
47312
48781
  "US-HI",
@@ -47326,6 +48795,98 @@ __export(renderer_exports16, {
47326
48795
  renderMapForExport: () => renderMapForExport
47327
48796
  });
47328
48797
  import * as d3Selection18 from "d3-selection";
48798
+ function pointInRing2(px, py, ring) {
48799
+ let inside = false;
48800
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
48801
+ const [xi, yi] = ring[i];
48802
+ const [xj, yj] = ring[j];
48803
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
48804
+ inside = !inside;
48805
+ }
48806
+ return inside;
48807
+ }
48808
+ function ringToPath(ring) {
48809
+ let d = "";
48810
+ for (let i = 0; i < ring.length; i++)
48811
+ d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48812
+ return d + "Z";
48813
+ }
48814
+ function polylineToPath(pts) {
48815
+ let d = "";
48816
+ for (let i = 0; i < pts.length; i++)
48817
+ d += (i ? "L" : "M") + pts[i][0] + "," + pts[i][1];
48818
+ return d;
48819
+ }
48820
+ function ringToCoastPaths(ring, frame) {
48821
+ if (!frame) return [ringToPath(ring)];
48822
+ const n = ring.length;
48823
+ const eps = 0.75;
48824
+ const onL = (x) => Math.abs(x) <= eps;
48825
+ const onR = (x) => Math.abs(x - frame.w) <= eps;
48826
+ const onT = (y) => Math.abs(y) <= eps;
48827
+ const onB = (y) => Math.abs(y - frame.h) <= eps;
48828
+ const isFrameEdge = (a, b) => onL(a[0]) && onL(b[0]) || onR(a[0]) && onR(b[0]) || onT(a[1]) && onT(b[1]) || onB(a[1]) && onB(b[1]);
48829
+ let firstBreak = -1;
48830
+ for (let i = 0; i < n; i++)
48831
+ if (isFrameEdge(ring[i], ring[(i + 1) % n])) {
48832
+ firstBreak = i;
48833
+ break;
48834
+ }
48835
+ if (firstBreak === -1) return [ringToPath(ring)];
48836
+ const paths = [];
48837
+ let cur = [];
48838
+ const start = (firstBreak + 1) % n;
48839
+ for (let k = 0; k < n; k++) {
48840
+ const i = (start + k) % n;
48841
+ const a = ring[i];
48842
+ const b = ring[(i + 1) % n];
48843
+ if (isFrameEdge(a, b)) {
48844
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
48845
+ cur = [];
48846
+ continue;
48847
+ }
48848
+ if (cur.length === 0) cur.push(a);
48849
+ cur.push(b);
48850
+ }
48851
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
48852
+ return paths;
48853
+ }
48854
+ function coastlineOuterRings(regions, minExtent, frame) {
48855
+ const paths = [];
48856
+ for (const r of regions) {
48857
+ const rings = parsePathRings(r.d);
48858
+ for (let i = 0; i < rings.length; i++) {
48859
+ const ring = rings[i];
48860
+ if (ring.length < 3) continue;
48861
+ let minX = Infinity;
48862
+ let minY = Infinity;
48863
+ let maxX = -Infinity;
48864
+ let maxY = -Infinity;
48865
+ for (const [x, y] of ring) {
48866
+ if (x < minX) minX = x;
48867
+ if (x > maxX) maxX = x;
48868
+ if (y < minY) minY = y;
48869
+ if (y > maxY) maxY = y;
48870
+ }
48871
+ if (Math.max(maxX - minX, maxY - minY) < minExtent) continue;
48872
+ const [fx, fy] = ring[0];
48873
+ let depth = 0;
48874
+ for (let j = 0; j < rings.length; j++)
48875
+ if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48876
+ if (depth % 2 === 1) continue;
48877
+ paths.push(...ringToCoastPaths(ring, frame));
48878
+ }
48879
+ }
48880
+ return paths;
48881
+ }
48882
+ function appendWaterLines(g, outerRings, style, flatWater) {
48883
+ const d = outerRings.join(" ");
48884
+ const linesOuterFirst = [...style.lines].sort((a, b) => b.d - a.d);
48885
+ for (const line12 of linesOuterFirst) {
48886
+ 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");
48887
+ g.append("path").attr("d", d).attr("stroke", flatWater).attr("stroke-width", 2 * line12.d).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
48888
+ }
48889
+ }
47329
48890
  function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims, activeGroupOverride) {
47330
48891
  d3Selection18.select(container).selectAll(":not([data-d3-tooltip])").remove();
47331
48892
  const width = exportDims?.width ?? container.clientWidth;
@@ -47338,6 +48899,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47338
48899
  {
47339
48900
  palette,
47340
48901
  isDark,
48902
+ // Export-only: forward the contain-fit request from mapExportDimensions so a
48903
+ // clamped/floored (off-aspect) export canvas letterboxes instead of
48904
+ // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48905
+ // keeps the global stretch-fill.
48906
+ preferContain: exportDims?.preferContain ?? false,
47341
48907
  ...activeGroupOverride !== void 0 && {
47342
48908
  activeGroup: activeGroupOverride
47343
48909
  }
@@ -47351,6 +48917,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47351
48917
  const gRegions = svg.append("g").attr("class", "dgmo-map-regions");
47352
48918
  const drawRegion = (g, r, strokeWidth) => {
47353
48919
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
48920
+ if (r.label) p.attr("data-region-name", r.label);
48921
+ if (r.id && r.id !== "lake") p.attr("data-iso", r.id);
48922
+ if (r.labelX !== void 0 && r.labelY !== void 0) {
48923
+ p.attr("data-label-x", r.labelX).attr("data-label-y", r.labelY);
48924
+ }
47354
48925
  if (r.layer !== "base") {
47355
48926
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
47356
48927
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -47380,28 +48951,112 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47380
48951
  const landClip = defs.append("clipPath").attr("id", landClipId);
47381
48952
  for (const r of layout.regions)
47382
48953
  if (r.id !== "lake") landClip.append("path").attr("d", r.d);
47383
- 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");
48954
+ const gRelief = svg.append("g").attr("clip-path", `url(#${landClipId})`).style("pointer-events", "none").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");
47384
48955
  for (let y = h.spacing; y < height; y += h.spacing) {
47385
48956
  gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
47386
48957
  }
47387
48958
  }
48959
+ if (layout.coastlineStyle) {
48960
+ const cs = layout.coastlineStyle;
48961
+ const maskId = "dgmo-map-water-mask";
48962
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48963
+ mask.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "white");
48964
+ const landD = layout.regions.filter((r) => r.id !== "lake").map((r) => r.d).join(" ");
48965
+ const lakeD = layout.regions.filter((r) => r.id === "lake").map((r) => r.d).join(" ");
48966
+ if (landD) mask.append("path").attr("d", landD).attr("fill", "black");
48967
+ if (lakeD) mask.append("path").attr("d", lakeD).attr("fill", "white");
48968
+ if (layout.insets.length) {
48969
+ const reach = Math.max(0, ...cs.lines.map((l) => l.d + l.thickness));
48970
+ for (const box of layout.insets) {
48971
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48972
+ mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
48973
+ }
48974
+ }
48975
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").style("pointer-events", "none").attr("mask", `url(#${maskId})`);
48976
+ appendWaterLines(
48977
+ gWater,
48978
+ // Pass the canvas frame so edges collinear with it (the antimeridian on a
48979
+ // world map, regional clipExtent cuts) don't get ringed as fake coast —
48980
+ // land runs cleanly to the render-area edge.
48981
+ coastlineOuterRings(layout.regions, cs.minExtent, {
48982
+ w: width,
48983
+ h: height
48984
+ }),
48985
+ cs,
48986
+ layout.background
48987
+ );
48988
+ const byStroke = /* @__PURE__ */ new Map();
48989
+ for (const r of layout.regions) {
48990
+ const arr = byStroke.get(r.stroke);
48991
+ if (arr) arr.push(r.d);
48992
+ else byStroke.set(r.stroke, [r.d]);
48993
+ }
48994
+ for (const [stroke2, ds] of byStroke)
48995
+ gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48996
+ }
47388
48997
  if (layout.rivers.length) {
47389
- const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
48998
+ const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none").style("pointer-events", "none");
47390
48999
  for (const r of layout.rivers) {
47391
49000
  gRivers.append("path").attr("d", r.d).attr("stroke", r.color).attr("stroke-width", r.width).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
47392
49001
  }
47393
49002
  }
47394
49003
  if (layout.insets.length) {
47395
49004
  const insetG = svg.append("g").attr("class", "dgmo-map-insets");
47396
- for (const box of layout.insets) {
49005
+ layout.insets.forEach((box, bi) => {
47397
49006
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
47398
49007
  insetG.append("path").attr("d", d).attr("fill", layout.background).attr("stroke", mix(palette.text, palette.bg, 55)).attr("stroke-width", 1).attr("stroke-linejoin", "round");
47399
- }
49008
+ if (box.contextLand) {
49009
+ const clipId = `dgmo-map-inset-clip-${bi}`;
49010
+ defs.append("clipPath").attr("id", clipId).append("path").attr("d", d);
49011
+ insetG.append("path").attr("d", box.contextLand.d).attr("fill", box.contextLand.fill).attr("clip-path", `url(#${clipId})`);
49012
+ }
49013
+ });
47400
49014
  for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
47401
- }
49015
+ if (layout.coastlineStyle) {
49016
+ const cs = layout.coastlineStyle;
49017
+ const maskId = "dgmo-map-inset-water-mask";
49018
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
49019
+ for (const box of layout.insets) {
49020
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
49021
+ mask.append("path").attr("d", d).attr("fill", "white");
49022
+ }
49023
+ layout.insets.forEach((box, bi) => {
49024
+ if (box.contextLand)
49025
+ mask.append("path").attr("d", box.contextLand.d).attr("fill", "black").attr("clip-path", `url(#dgmo-map-inset-clip-${bi})`);
49026
+ });
49027
+ for (const r of layout.insetRegions)
49028
+ if (r.id !== "lake")
49029
+ mask.append("path").attr("d", r.d).attr("fill", "black");
49030
+ for (const r of layout.insetRegions)
49031
+ if (r.id === "lake")
49032
+ mask.append("path").attr("d", r.d).attr("fill", "white");
49033
+ const clipId = "dgmo-map-inset-water-clip";
49034
+ const clip = defs.append("clipPath").attr("id", clipId);
49035
+ for (const box of layout.insets) {
49036
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
49037
+ clip.append("path").attr("d", d);
49038
+ }
49039
+ const gInsetWater = insetG.append("g").attr("clip-path", `url(#${clipId})`).append("g").attr("class", "dgmo-map-inset-water-lines").attr("fill", "none").style("pointer-events", "none").attr("mask", `url(#${maskId})`);
49040
+ appendWaterLines(
49041
+ gInsetWater,
49042
+ coastlineOuterRings(layout.insetRegions, cs.minExtent),
49043
+ cs,
49044
+ layout.background
49045
+ );
49046
+ for (const r of layout.insetRegions)
49047
+ gInsetWater.append("path").attr("d", r.d).attr("stroke", r.stroke).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
49048
+ }
49049
+ }
49050
+ const wireSync = (sel, lineNumber) => {
49051
+ if (lineNumber < 1) return;
49052
+ sel.attr("data-line-number", lineNumber);
49053
+ if (onClickItem)
49054
+ sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
49055
+ };
47402
49056
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
47403
49057
  layout.legs.forEach((leg, i) => {
47404
49058
  const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round");
49059
+ wireSync(p, leg.lineNumber);
47405
49060
  if (leg.arrow) {
47406
49061
  const id = `dgmo-map-arrow-${i}`;
47407
49062
  const s = arrowSize(leg.width);
@@ -47409,25 +49064,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47409
49064
  p.attr("marker-end", `url(#${id})`);
47410
49065
  }
47411
49066
  if (leg.label !== void 0 && leg.labelX !== void 0) {
47412
- emitText(
49067
+ const lt = emitText(
47413
49068
  gLegs,
47414
49069
  leg.labelX,
47415
49070
  leg.labelY ?? 0,
47416
49071
  leg.label,
47417
49072
  "middle",
47418
- palette.textMuted,
47419
- haloColor,
47420
- true,
49073
+ leg.labelColor ?? palette.textMuted,
49074
+ leg.labelHaloColor ?? haloColor,
49075
+ leg.labelHalo ?? true,
47421
49076
  LABEL_FONT - 1
47422
49077
  );
49078
+ wireSync(lt, leg.lineNumber);
47423
49079
  }
47424
49080
  });
49081
+ const gSpider = svg.append("g").attr("class", "dgmo-map-spider");
49082
+ for (const cl of layout.clusters) {
49083
+ if (!exportDims) {
49084
+ 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");
49085
+ }
49086
+ for (const leg of cl.legs) {
49087
+ 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");
49088
+ }
49089
+ 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");
49090
+ }
47425
49091
  const gPois = svg.append("g").attr("class", "dgmo-map-pois");
47426
49092
  for (const poi of layout.pois) {
47427
49093
  if (poi.isOrigin) {
47428
49094
  gPois.append("circle").attr("cx", poi.cx).attr("cy", poi.cy).attr("r", poi.r + 3).attr("fill", "none").attr("stroke", poi.stroke).attr("stroke-width", 1.5);
47429
49095
  }
47430
49096
  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);
49097
+ if (poi.clusterId !== void 0)
49098
+ c.attr("data-cluster-member", poi.clusterId);
47431
49099
  if (poi.tags) {
47432
49100
  for (const [group, value] of Object.entries(poi.tags)) {
47433
49101
  c.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
@@ -47455,12 +49123,32 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47455
49123
  }
47456
49124
  const gLabels = svg.append("g").attr("class", "dgmo-map-labels");
47457
49125
  for (const lab of layout.labels) {
49126
+ if (lab.hidden) {
49127
+ if (exportDims) continue;
49128
+ emitText(
49129
+ gLabels,
49130
+ lab.x,
49131
+ lab.y,
49132
+ lab.text,
49133
+ lab.anchor,
49134
+ lab.color,
49135
+ lab.haloColor,
49136
+ lab.halo,
49137
+ LABEL_FONT,
49138
+ lab.italic,
49139
+ lab.letterSpacing
49140
+ ).attr("data-poi", lab.poiId ?? null).attr("data-poi-hidden", "").style("opacity", 0).style("pointer-events", "none");
49141
+ continue;
49142
+ }
47458
49143
  if (lab.leader) {
47459
49144
  const line12 = gLabels.append("line").attr("x1", lab.leader.x1).attr("y1", lab.leader.y1).attr("x2", lab.leader.x2).attr("y2", lab.leader.y2).attr(
47460
49145
  "stroke",
47461
49146
  lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
47462
49147
  ).attr("stroke-width", lab.leaderColor ? 1 : 0.75);
47463
49148
  if (lab.poiId !== void 0) line12.attr("data-poi", lab.poiId);
49149
+ if (lab.clusterMember !== void 0)
49150
+ line12.attr("data-cluster-member", lab.clusterMember);
49151
+ wireSync(line12, lab.lineNumber);
47464
49152
  }
47465
49153
  const t = emitText(
47466
49154
  gLabels,
@@ -47471,11 +49159,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47471
49159
  lab.color,
47472
49160
  lab.haloColor,
47473
49161
  lab.halo,
47474
- LABEL_FONT
49162
+ LABEL_FONT,
49163
+ lab.italic,
49164
+ lab.letterSpacing,
49165
+ lab.lines
47475
49166
  );
47476
49167
  if (lab.poiId !== void 0) {
47477
49168
  t.attr("data-poi", lab.poiId).style("cursor", "default");
47478
49169
  }
49170
+ if (lab.clusterMember !== void 0) {
49171
+ t.attr("data-cluster-member", lab.clusterMember);
49172
+ }
49173
+ wireSync(t, lab.lineNumber);
49174
+ }
49175
+ if (!exportDims && layout.clusters.length) {
49176
+ const gBadge = svg.append("g").attr("class", "dgmo-map-cluster-badges");
49177
+ for (const cl of layout.clusters) {
49178
+ const g = gBadge.append("g").attr("data-cluster", cl.id).style("opacity", 0).style("pointer-events", "none");
49179
+ const R = 9;
49180
+ 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);
49181
+ 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);
49182
+ emitText(
49183
+ g,
49184
+ cl.cx,
49185
+ cl.cy + 3,
49186
+ String(cl.count),
49187
+ "middle",
49188
+ palette.text,
49189
+ palette.bg,
49190
+ false,
49191
+ LABEL_FONT
49192
+ );
49193
+ }
47479
49194
  }
47480
49195
  if (layout.legend) {
47481
49196
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
@@ -47512,7 +49227,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47512
49227
  svg.append("text").attr("class", "dgmo-map-title").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).attr("fill", palette.text).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 4).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.title);
47513
49228
  }
47514
49229
  if (layout.subtitle) {
47515
- svg.append("text").attr("x", width / 2).attr("y", TITLE_Y + TITLE_FONT_SIZE).attr("text-anchor", "middle").attr("font-size", LABEL_FONT + 1).attr("fill", palette.textMuted).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.subtitle);
49230
+ svg.append("text").attr("class", "dgmo-map-subtitle").attr("x", width / 2).attr("y", TITLE_Y + TITLE_FONT_SIZE).attr("text-anchor", "middle").attr("font-size", LABEL_FONT + 1).attr("fill", palette.textMuted).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.subtitle);
47516
49231
  }
47517
49232
  if (layout.caption) {
47518
49233
  svg.append("text").attr("x", width / 2).attr("y", height - 8).attr("text-anchor", "middle").attr("font-size", LABEL_FONT).attr("fill", palette.textMuted).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.caption);
@@ -47521,10 +49236,21 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47521
49236
  function renderMapForExport(container, resolved, data, palette, isDark, exportDims) {
47522
49237
  renderMap(container, resolved, data, palette, isDark, void 0, exportDims);
47523
49238
  }
47524
- function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize) {
47525
- const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color).text(text);
49239
+ function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize, italic, letterSpacing, lines) {
49240
+ const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color);
49241
+ if (lines && lines.length > 1) {
49242
+ const lineHeight = fontSize + 2;
49243
+ const startDy = -((lines.length - 1) / 2) * lineHeight;
49244
+ lines.forEach((ln, i) => {
49245
+ t.append("tspan").attr("x", x).attr("dy", i === 0 ? startDy : lineHeight).text(ln);
49246
+ });
49247
+ } else {
49248
+ t.text(text);
49249
+ }
49250
+ if (italic) t.attr("font-style", "italic");
49251
+ if (letterSpacing) t.attr("letter-spacing", letterSpacing);
47526
49252
  if (withHalo) {
47527
- t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7);
49253
+ t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 2).attr("stroke-linejoin", "round").attr("stroke-linecap", "round").attr("stroke-opacity", 0.55);
47528
49254
  }
47529
49255
  return t;
47530
49256
  }
@@ -47541,6 +49267,56 @@ var init_renderer16 = __esm({
47541
49267
  }
47542
49268
  });
47543
49269
 
49270
+ // src/map/dimensions.ts
49271
+ var dimensions_exports = {};
49272
+ __export(dimensions_exports, {
49273
+ mapContentAspect: () => mapContentAspect,
49274
+ mapExportDimensions: () => mapExportDimensions
49275
+ });
49276
+ import { geoPath as geoPath2 } from "d3-geo";
49277
+ function mapContentAspect(resolved, data, ref = REF) {
49278
+ const { projection, fitTarget } = buildMapProjection(resolved, data);
49279
+ projection.fitSize([ref, ref], fitTarget);
49280
+ const b = geoPath2(projection).bounds(fitTarget);
49281
+ const w = b[1][0] - b[0][0];
49282
+ const h = b[1][1] - b[0][1];
49283
+ const aspect = w / h;
49284
+ return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49285
+ }
49286
+ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49287
+ const raw = mapContentAspect(resolved, data);
49288
+ const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49289
+ const width = baseWidth;
49290
+ let height = Math.round(width / clamped);
49291
+ let chromeReserve = 0;
49292
+ if (resolved.title && resolved.pois.length > 0) {
49293
+ const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
49294
+ chromeReserve += Math.max(FIT_PAD2, bannerBottom + TITLE_GAP) - FIT_PAD2;
49295
+ }
49296
+ let floored = false;
49297
+ if (height - chromeReserve < MIN_MAP_BAND) {
49298
+ height = Math.round(chromeReserve + MIN_MAP_BAND);
49299
+ floored = true;
49300
+ }
49301
+ const preferContain = clamped !== raw || floored;
49302
+ return { width, height, preferContain };
49303
+ }
49304
+ var FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
49305
+ var init_dimensions = __esm({
49306
+ "src/map/dimensions.ts"() {
49307
+ "use strict";
49308
+ init_title_constants();
49309
+ init_layout15();
49310
+ FIT_PAD2 = 24;
49311
+ TITLE_GAP = 16;
49312
+ ASPECT_MAX = 3;
49313
+ ASPECT_MIN = 0.9;
49314
+ MIN_MAP_BAND = 200;
49315
+ FALLBACK_ASPECT = 1.5;
49316
+ REF = 1e3;
49317
+ }
49318
+ });
49319
+
47544
49320
  // src/map/load-data.ts
47545
49321
  var load_data_exports = {};
47546
49322
  __export(load_data_exports, {
@@ -47599,12 +49375,17 @@ function loadMapData() {
47599
49375
  mountainRanges,
47600
49376
  naLand,
47601
49377
  naLakes,
49378
+ waterBodies,
47602
49379
  gazetteer
47603
49380
  ] = await Promise.all([
49381
+ // worldCoarse (110m) is LOAD-BEARING but NOT a render source: the world
49382
+ // basemap renders from worldDetail (50m) at all scales (resolver pins
49383
+ // basemaps.world = 'detail'). Coarse stays as the authoritative region
49384
+ // name index + dominant-landmass bbox source in resolver.ts. Do not drop it.
47604
49385
  readJson(nb, dir, FILES.worldCoarse),
47605
49386
  readJson(nb, dir, FILES.worldDetail),
47606
49387
  readJson(nb, dir, FILES.usStates),
47607
- // Lakes/rivers/mountain/NA assets are optional — older bundles may predate them.
49388
+ // Lakes/rivers/mountain/NA/water assets are optional — older bundles may predate them.
47608
49389
  readJson(nb, dir, FILES.lakes).catch(() => void 0),
47609
49390
  readJson(nb, dir, FILES.rivers).catch(() => void 0),
47610
49391
  readJson(nb, dir, FILES.mountainRanges).catch(
@@ -47612,6 +49393,7 @@ function loadMapData() {
47612
49393
  ),
47613
49394
  readJson(nb, dir, FILES.naLand).catch(() => void 0),
47614
49395
  readJson(nb, dir, FILES.naLakes).catch(() => void 0),
49396
+ readJson(nb, dir, FILES.waterBodies).catch(() => void 0),
47615
49397
  readJson(nb, dir, FILES.gazetteer)
47616
49398
  ]);
47617
49399
  return validate({
@@ -47623,7 +49405,8 @@ function loadMapData() {
47623
49405
  ...rivers && { rivers },
47624
49406
  ...mountainRanges && { mountainRanges },
47625
49407
  ...naLand && { naLand },
47626
- ...naLakes && { naLakes }
49408
+ ...naLakes && { naLakes },
49409
+ ...waterBodies && { waterBodies }
47627
49410
  });
47628
49411
  })().catch((e) => {
47629
49412
  cache = void 0;
@@ -47644,6 +49427,7 @@ var init_load_data = __esm({
47644
49427
  mountainRanges: "mountain-ranges.json",
47645
49428
  naLand: "na-land.json",
47646
49429
  naLakes: "na-lakes.json",
49430
+ waterBodies: "water-bodies.json",
47647
49431
  gazetteer: "gazetteer.json"
47648
49432
  };
47649
49433
  CANDIDATE_DIRS = [
@@ -49657,8 +51441,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
49657
51441
  const lines = splitParticipantLabel(p.label, LABEL_MAX_CHARS);
49658
51442
  if (lines.length === 0) continue;
49659
51443
  const widest = Math.max(...lines.map((l) => l.length));
49660
- const labelWidth = widest * LABEL_CHAR_WIDTH + 10;
49661
- uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth);
51444
+ const labelWidth2 = widest * LABEL_CHAR_WIDTH + 10;
51445
+ uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth2);
49662
51446
  }
49663
51447
  uniformBoxWidth = Math.min(MAX_BOX_WIDTH, uniformBoxWidth);
49664
51448
  const effectiveGap = Math.max(PARTICIPANT_GAP, uniformBoxWidth + 30);
@@ -52357,15 +54141,15 @@ function renderArcDiagram(container, parsed, palette, _isDark, onClickItem, expo
52357
54141
  textColor,
52358
54142
  onClickItem
52359
54143
  );
52360
- const neighbors = /* @__PURE__ */ new Map();
52361
- for (const node of nodes) neighbors.set(node, /* @__PURE__ */ new Set());
54144
+ const neighbors2 = /* @__PURE__ */ new Map();
54145
+ for (const node of nodes) neighbors2.set(node, /* @__PURE__ */ new Set());
52362
54146
  for (const link of links) {
52363
- neighbors.get(link.source).add(link.target);
52364
- neighbors.get(link.target).add(link.source);
54147
+ neighbors2.get(link.source).add(link.target);
54148
+ neighbors2.get(link.target).add(link.source);
52365
54149
  }
52366
54150
  const FADE_OPACITY3 = 0.1;
52367
54151
  function handleMouseEnter(hovered) {
52368
- const connected = neighbors.get(hovered);
54152
+ const connected = neighbors2.get(hovered);
52369
54153
  g.selectAll(".arc-link").each(function() {
52370
54154
  const el = d3Selection23.select(this);
52371
54155
  const src = el.attr("data-source");
@@ -53273,10 +55057,12 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
53273
55057
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
53274
55058
  const eraLabelY = eraReserve ? -(topScaleH + markerReserve + ERA_ROW_H / 2) : 0;
53275
55059
  const innerWidth = width - margin.left - margin.right;
53276
- const innerHeight = height - margin.top - margin.bottom;
53277
- const rowH = Math.min(ctx.structural(28), innerHeight / sorted.length);
55060
+ const availInnerHeight = height - margin.top - margin.bottom;
55061
+ const rowH = Math.min(ctx.structural(28), availInnerHeight / sorted.length);
55062
+ const innerHeight = rowH * sorted.length;
55063
+ const usedHeight = margin.top + innerHeight + margin.bottom;
53278
55064
  const xScale = d3Scale2.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
53279
- const svg = d3Selection23.select(container).append("svg").attr("width", width).attr("height", height).attr("viewBox", `0 0 ${width} ${height}`).attr("preserveAspectRatio", "xMidYMin meet").style("background", bgColor);
55065
+ const svg = d3Selection23.select(container).append("svg").attr("width", width).attr("height", usedHeight).attr("viewBox", `0 0 ${width} ${usedHeight}`).attr("preserveAspectRatio", "xMidYMin meet").style("background", bgColor);
53280
55066
  if (ctx.isBelowFloor) {
53281
55067
  svg.attr("width", "100%");
53282
55068
  }
@@ -54300,7 +56086,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54300
56086
  8,
54301
56087
  Math.floor(OVERLAP_WRAP_TARGET_W / OVERLAP_CH_W)
54302
56088
  );
54303
- function wrapLabel2(text, maxChars) {
56089
+ function wrapLabel3(text, maxChars) {
54304
56090
  const words = text.split(/\s+/).filter(Boolean);
54305
56091
  const lines = [];
54306
56092
  let cur = "";
@@ -54346,7 +56132,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54346
56132
  if (!ov.label) continue;
54347
56133
  const idxs = ov.sets.map((s) => vennSets.findIndex((vs) => vs.name === s));
54348
56134
  if (idxs.some((idx) => idx < 0)) continue;
54349
- const lines = wrapLabel2(ov.label, MAX_WRAP_CHARS);
56135
+ const lines = wrapLabel3(ov.label, MAX_WRAP_CHARS);
54350
56136
  wrappedOverlapLabels.set(ov, lines);
54351
56137
  const dir = predictOverlapDirRaw(idxs);
54352
56138
  const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
@@ -55784,6 +57570,7 @@ async function renderForExport(content, theme, palette, viewState, options) {
55784
57570
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55785
57571
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55786
57572
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
57573
+ const { mapExportDimensions: mapExportDimensions2 } = await Promise.resolve().then(() => (init_dimensions(), dimensions_exports));
55787
57574
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55788
57575
  const mapParsed = parseMap2(content);
55789
57576
  let mapData = options?.mapData;
@@ -55796,14 +57583,15 @@ async function renderForExport(content, theme, palette, viewState, options) {
55796
57583
  }
55797
57584
  }
55798
57585
  const mapResolved = resolveMap2(mapParsed, mapData);
55799
- const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
57586
+ const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57587
+ const container2 = createExportContainer(dims2.width, dims2.height);
55800
57588
  renderMapForExport2(
55801
57589
  container2,
55802
57590
  mapResolved,
55803
57591
  mapData,
55804
57592
  effectivePalette2,
55805
57593
  theme === "dark",
55806
- { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
57594
+ dims2
55807
57595
  );
55808
57596
  return finalizeSvgExport(container2, theme, effectivePalette2);
55809
57597
  }
@@ -56645,7 +58433,8 @@ async function render(content, options) {
56645
58433
  ...options?.c4Container !== void 0 && {
56646
58434
  c4Container: options.c4Container
56647
58435
  },
56648
- ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup }
58436
+ ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup },
58437
+ ...options?.mapData !== void 0 && { mapData: options.mapData }
56649
58438
  });
56650
58439
  if (chartType === "map") {
56651
58440
  try {
@@ -56656,7 +58445,7 @@ async function render(content, options) {
56656
58445
  Promise.resolve().then(() => (init_load_data(), load_data_exports))
56657
58446
  ]
56658
58447
  );
56659
- const data = await loadMapData2();
58448
+ const data = options?.mapData ?? await loadMapData2();
56660
58449
  diagnostics = [...resolveMap2(parseMap2(content), data).diagnostics];
56661
58450
  } catch {
56662
58451
  }