@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.cjs CHANGED
@@ -93,18 +93,18 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
93
93
  const results = [];
94
94
  for (let i = 0; i < points.length; i++) {
95
95
  const pt = points[i];
96
- const labelWidth = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
96
+ const labelWidth2 = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
97
97
  let best = null;
98
98
  const directions = [
99
99
  {
100
100
  // Above
101
101
  gen: (offset) => {
102
- const lx = pt.cx - labelWidth / 2;
102
+ const lx = pt.cx - labelWidth2 / 2;
103
103
  const ly = pt.cy - offset - labelHeight;
104
- if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
104
+ if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
105
105
  return null;
106
106
  return {
107
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
107
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
108
108
  textX: pt.cx,
109
109
  textY: ly + labelHeight / 2,
110
110
  anchor: "middle"
@@ -114,12 +114,12 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
114
114
  {
115
115
  // Below
116
116
  gen: (offset) => {
117
- const lx = pt.cx - labelWidth / 2;
117
+ const lx = pt.cx - labelWidth2 / 2;
118
118
  const ly = pt.cy + offset;
119
- if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
119
+ if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
120
120
  return null;
121
121
  return {
122
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
122
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
123
123
  textX: pt.cx,
124
124
  textY: ly + labelHeight / 2,
125
125
  anchor: "middle"
@@ -131,10 +131,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
131
131
  gen: (offset) => {
132
132
  const lx = pt.cx + offset;
133
133
  const ly = pt.cy - labelHeight / 2;
134
- if (lx + labelWidth > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
134
+ if (lx + labelWidth2 > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
135
135
  return null;
136
136
  return {
137
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
137
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
138
138
  textX: lx,
139
139
  textY: pt.cy,
140
140
  anchor: "start"
@@ -144,13 +144,13 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
144
144
  {
145
145
  // Left
146
146
  gen: (offset) => {
147
- const lx = pt.cx - offset - labelWidth;
147
+ const lx = pt.cx - offset - labelWidth2;
148
148
  const ly = pt.cy - labelHeight / 2;
149
149
  if (lx < chartBounds.left || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
150
150
  return null;
151
151
  return {
152
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
153
- textX: lx + labelWidth,
152
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
153
+ textX: lx + labelWidth2,
154
154
  textY: pt.cy,
155
155
  anchor: "end"
156
156
  };
@@ -200,10 +200,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
200
200
  }
201
201
  }
202
202
  if (!best) {
203
- const lx = pt.cx - labelWidth / 2;
203
+ const lx = pt.cx - labelWidth2 / 2;
204
204
  const ly = pt.cy - minGap - labelHeight;
205
205
  best = {
206
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
206
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
207
207
  textX: pt.cx,
208
208
  textY: ly + labelHeight / 2,
209
209
  anchor: "middle",
@@ -838,6 +838,9 @@ var init_reserved_key_registry = __esm({
838
838
  "value",
839
839
  "label",
840
840
  "style"
841
+ // `surface:` was removed in the 2026-06-02 defaults-on review — it is no longer
842
+ // a recognized metadata key (the route/edge surface feature was cut; §24B.7).
843
+ // A stray `surface: water` is no longer captured as a reserved key.
841
844
  ]);
842
845
  ORG_REGISTRY = staticRegistry([
843
846
  "color",
@@ -892,9 +895,7 @@ var init_reserved_key_registry = __esm({
892
895
  BOXES_AND_LINES_REGISTRY = staticRegistry([
893
896
  "color",
894
897
  "description",
895
- "width",
896
- "split",
897
- "fanout"
898
+ "value"
898
899
  ]);
899
900
  TIMELINE_REGISTRY = staticRegistry([
900
901
  "color",
@@ -1900,77 +1901,266 @@ function getSegmentColors(palette, count) {
1900
1901
  (_, i) => hslToHex(Math.round((startHue + i * step) % 360), avgS, avgL)
1901
1902
  );
1902
1903
  }
1904
+ function politicalTints(palette, count, isDark) {
1905
+ if (count <= 0) return [];
1906
+ const base = isDark ? palette.surface : palette.bg;
1907
+ const c = palette.colors;
1908
+ const swatches = [
1909
+ .../* @__PURE__ */ new Set([
1910
+ c.green,
1911
+ c.yellow,
1912
+ c.orange,
1913
+ c.purple,
1914
+ c.red,
1915
+ c.teal,
1916
+ c.cyan,
1917
+ c.blue
1918
+ ])
1919
+ ];
1920
+ const bands = isDark ? POLITICAL_TINT_BANDS.dark : POLITICAL_TINT_BANDS.light;
1921
+ const out = [];
1922
+ for (const pct of bands) {
1923
+ if (out.length >= count) break;
1924
+ for (const s of swatches) out.push(mix(s, base, pct));
1925
+ }
1926
+ return out.slice(0, count);
1927
+ }
1928
+ var POLITICAL_TINT_BANDS;
1903
1929
  var init_color_utils = __esm({
1904
1930
  "src/palettes/color-utils.ts"() {
1905
1931
  "use strict";
1932
+ POLITICAL_TINT_BANDS = {
1933
+ light: [32, 48, 64, 80],
1934
+ dark: [44, 58, 72, 86]
1935
+ };
1906
1936
  }
1907
1937
  });
1908
1938
 
1909
- // src/palettes/bold.ts
1910
- var boldPalette;
1911
- var init_bold = __esm({
1912
- "src/palettes/bold.ts"() {
1939
+ // src/palettes/atlas.ts
1940
+ var atlasPalette;
1941
+ var init_atlas = __esm({
1942
+ "src/palettes/atlas.ts"() {
1913
1943
  "use strict";
1914
1944
  init_registry();
1915
- boldPalette = {
1916
- id: "bold",
1917
- name: "Bold",
1945
+ atlasPalette = {
1946
+ id: "atlas",
1947
+ name: "Atlas",
1918
1948
  light: {
1919
- bg: "#ffffff",
1920
- surface: "#f0f0f0",
1921
- overlay: "#f0f0f0",
1922
- border: "#cccccc",
1923
- text: "#000000",
1924
- textMuted: "#666666",
1925
- textOnFillLight: "#ffffff",
1926
- textOnFillDark: "#000000",
1927
- primary: "#0000ff",
1928
- secondary: "#ff00ff",
1929
- accent: "#00cccc",
1930
- destructive: "#ff0000",
1949
+ bg: "#f3ead3",
1950
+ // warm manila / parchment
1951
+ surface: "#ece0c0",
1952
+ // deeper paper (cards, panels)
1953
+ overlay: "#e8dab8",
1954
+ // popovers, dropdowns
1955
+ border: "#bcaa86",
1956
+ // muted sepia rule line
1957
+ text: "#463a26",
1958
+ // aged sepia-brown ink
1959
+ textMuted: "#7a6a4f",
1960
+ // faded annotation ink
1961
+ textOnFillLight: "#f7f1de",
1962
+ // parchment (light text on dark fills)
1963
+ textOnFillDark: "#3a2e1c",
1964
+ // deep ink (dark text on light fills)
1965
+ primary: "#5b7a99",
1966
+ // pull-down map ocean (steel-blue)
1967
+ secondary: "#7e9a6f",
1968
+ // lowland sage / celadon
1969
+ accent: "#b07f7c",
1970
+ // dusty rose
1971
+ destructive: "#b25a45",
1972
+ // brick / terracotta
1931
1973
  colors: {
1932
- red: "#ff0000",
1933
- orange: "#ff8000",
1934
- yellow: "#ffcc00",
1935
- green: "#00cc00",
1936
- blue: "#0000ff",
1937
- purple: "#cc00cc",
1938
- teal: "#008080",
1939
- cyan: "#00cccc",
1940
- gray: "#808080",
1941
- black: "#000000",
1942
- white: "#f0f0f0"
1974
+ red: "#bf6a52",
1975
+ // terracotta brick
1976
+ orange: "#cf9a5c",
1977
+ // map tan / ochre
1978
+ yellow: "#cdb35e",
1979
+ // straw / muted lemon
1980
+ green: "#7e9a6f",
1981
+ // sage / celadon lowland
1982
+ blue: "#5b7a99",
1983
+ // steel-blue ocean
1984
+ purple: "#9a7fa6",
1985
+ // dusty lilac / mauve
1986
+ teal: "#6fa094",
1987
+ // muted seafoam
1988
+ cyan: "#79a7b5",
1989
+ // shallow-water blue
1990
+ gray: "#8a7d68",
1991
+ // warm taupe
1992
+ black: "#463a26",
1993
+ // ink
1994
+ white: "#ece0c0"
1995
+ // paper
1943
1996
  }
1944
1997
  },
1945
1998
  dark: {
1946
- bg: "#000000",
1947
- surface: "#111111",
1948
- overlay: "#1a1a1a",
1949
- border: "#333333",
1950
- text: "#ffffff",
1951
- textMuted: "#aaaaaa",
1952
- textOnFillLight: "#ffffff",
1953
- textOnFillDark: "#000000",
1954
- primary: "#00ccff",
1955
- secondary: "#ff00ff",
1956
- accent: "#ffff00",
1957
- destructive: "#ff0000",
1999
+ bg: "#1e2a33",
2000
+ // deep map ocean (night globe)
2001
+ surface: "#27353f",
2002
+ // raised ocean
2003
+ overlay: "#2e3d48",
2004
+ // popovers, dropdowns
2005
+ border: "#3d4f5c",
2006
+ // depth-contour line
2007
+ text: "#e8dcc0",
2008
+ // parchment ink, inverted
2009
+ textMuted: "#a89a7d",
2010
+ // faded label
2011
+ textOnFillLight: "#f7f1de",
2012
+ // parchment
2013
+ textOnFillDark: "#1a242c",
2014
+ // deep ocean ink
2015
+ primary: "#7ba0bf",
2016
+ // brighter ocean
2017
+ secondary: "#9bb588",
2018
+ // sage, lifted
2019
+ accent: "#cf9a96",
2020
+ // dusty rose, lifted
2021
+ destructive: "#c9745c",
2022
+ // brick, lifted
1958
2023
  colors: {
1959
- red: "#ff0000",
1960
- orange: "#ff8000",
1961
- yellow: "#ffff00",
1962
- green: "#00ff00",
1963
- blue: "#0066ff",
1964
- purple: "#ff00ff",
1965
- teal: "#00cccc",
1966
- cyan: "#00ffff",
1967
- gray: "#808080",
1968
- black: "#111111",
1969
- white: "#ffffff"
2024
+ red: "#cf7a60",
2025
+ // terracotta
2026
+ orange: "#d9a96a",
2027
+ // tan / ochre
2028
+ yellow: "#d8c074",
2029
+ // straw
2030
+ green: "#9bb588",
2031
+ // sage lowland
2032
+ blue: "#7ba0bf",
2033
+ // ocean
2034
+ purple: "#b59ac0",
2035
+ // lilac / mauve
2036
+ teal: "#85b3a6",
2037
+ // seafoam
2038
+ cyan: "#92bccb",
2039
+ // shallow-water blue
2040
+ gray: "#9a8d76",
2041
+ // warm taupe
2042
+ black: "#27353f",
2043
+ // raised ocean
2044
+ white: "#e8dcc0"
2045
+ // parchment
1970
2046
  }
1971
2047
  }
1972
2048
  };
1973
- registerPalette(boldPalette);
2049
+ registerPalette(atlasPalette);
2050
+ }
2051
+ });
2052
+
2053
+ // src/palettes/blueprint.ts
2054
+ var blueprintPalette;
2055
+ var init_blueprint = __esm({
2056
+ "src/palettes/blueprint.ts"() {
2057
+ "use strict";
2058
+ init_registry();
2059
+ blueprintPalette = {
2060
+ id: "blueprint",
2061
+ name: "Blueprint",
2062
+ light: {
2063
+ bg: "#f4f8fb",
2064
+ // pale drafting white (faint cyan)
2065
+ surface: "#e6eef4",
2066
+ // drafting panel
2067
+ overlay: "#dde9f1",
2068
+ // popovers, dropdowns
2069
+ border: "#aac3d6",
2070
+ // pale blue grid line
2071
+ text: "#123a5e",
2072
+ // blueprint navy ink
2073
+ textMuted: "#4f7390",
2074
+ // faint draft note
2075
+ textOnFillLight: "#f4f8fb",
2076
+ // drafting white
2077
+ textOnFillDark: "#0c2f4d",
2078
+ // deep blueprint navy
2079
+ primary: "#1f5e8c",
2080
+ // blueprint blue
2081
+ secondary: "#5b7d96",
2082
+ // steel
2083
+ accent: "#b08a3e",
2084
+ // draftsman's ochre highlight
2085
+ destructive: "#c0504d",
2086
+ // correction red
2087
+ colors: {
2088
+ red: "#c25a4e",
2089
+ // correction red
2090
+ orange: "#c2823e",
2091
+ // ochre
2092
+ yellow: "#c2a843",
2093
+ // pencil gold
2094
+ green: "#4f8a6b",
2095
+ // drafting green
2096
+ blue: "#1f5e8c",
2097
+ // blueprint blue
2098
+ purple: "#6f5e96",
2099
+ // indigo pencil
2100
+ teal: "#3a8a8a",
2101
+ // teal
2102
+ cyan: "#3f8fb5",
2103
+ // cyan
2104
+ gray: "#7e8e98",
2105
+ // graphite
2106
+ black: "#123a5e",
2107
+ // navy ink
2108
+ white: "#e6eef4"
2109
+ // panel
2110
+ }
2111
+ },
2112
+ dark: {
2113
+ bg: "#103a5e",
2114
+ // deep blueprint blue (cyanotype ground)
2115
+ surface: "#16466e",
2116
+ // raised sheet
2117
+ overlay: "#1c5180",
2118
+ // popovers, dropdowns
2119
+ border: "#3a6f96",
2120
+ // grid line
2121
+ text: "#eaf2f8",
2122
+ // chalk white
2123
+ textMuted: "#9fc0d6",
2124
+ // faint chalk note
2125
+ textOnFillLight: "#eaf2f8",
2126
+ // chalk white
2127
+ textOnFillDark: "#0c2f4d",
2128
+ // deep blueprint navy
2129
+ primary: "#7fb8d8",
2130
+ // chalk cyan
2131
+ secondary: "#9fb8c8",
2132
+ // pale steel
2133
+ accent: "#d8c27a",
2134
+ // chalk amber
2135
+ destructive: "#e08a7a",
2136
+ // chalk correction red
2137
+ colors: {
2138
+ red: "#e0907e",
2139
+ // chalk red
2140
+ orange: "#e0ab78",
2141
+ // chalk amber
2142
+ yellow: "#e3d089",
2143
+ // chalk gold
2144
+ green: "#93c79e",
2145
+ // chalk green
2146
+ blue: "#8ec3e0",
2147
+ // chalk cyan-blue
2148
+ purple: "#b6a6d8",
2149
+ // chalk indigo
2150
+ teal: "#84c7c2",
2151
+ // chalk teal
2152
+ cyan: "#9fd6e0",
2153
+ // chalk cyan
2154
+ gray: "#aebecb",
2155
+ // chalk graphite
2156
+ black: "#16466e",
2157
+ // raised sheet
2158
+ white: "#eaf2f8"
2159
+ // chalk white
2160
+ }
2161
+ }
2162
+ };
2163
+ registerPalette(blueprintPalette);
1974
2164
  }
1975
2165
  });
1976
2166
 
@@ -2467,6 +2657,120 @@ var init_rose_pine = __esm({
2467
2657
  }
2468
2658
  });
2469
2659
 
2660
+ // src/palettes/slate.ts
2661
+ var slatePalette;
2662
+ var init_slate = __esm({
2663
+ "src/palettes/slate.ts"() {
2664
+ "use strict";
2665
+ init_registry();
2666
+ slatePalette = {
2667
+ id: "slate",
2668
+ name: "Slate",
2669
+ light: {
2670
+ bg: "#ffffff",
2671
+ // clean slide white
2672
+ surface: "#f3f5f8",
2673
+ // light cool-gray panel
2674
+ overlay: "#eaeef3",
2675
+ // popovers, dropdowns
2676
+ border: "#d4dae1",
2677
+ // hairline rule
2678
+ text: "#1f2933",
2679
+ // near-black slate (softer than pure black)
2680
+ textMuted: "#5b6672",
2681
+ // secondary label
2682
+ textOnFillLight: "#ffffff",
2683
+ // light text on dark fills
2684
+ textOnFillDark: "#1f2933",
2685
+ // dark text on light fills
2686
+ primary: "#3b6ea5",
2687
+ // confident corporate blue
2688
+ secondary: "#5b6672",
2689
+ // slate gray
2690
+ accent: "#3a9188",
2691
+ // muted teal accent
2692
+ destructive: "#c0504d",
2693
+ // brick red
2694
+ colors: {
2695
+ red: "#c0504d",
2696
+ // brick
2697
+ orange: "#cc7a33",
2698
+ // muted amber
2699
+ yellow: "#c9a227",
2700
+ // gold (not neon)
2701
+ green: "#5b9357",
2702
+ // forest / sage
2703
+ blue: "#3b6ea5",
2704
+ // corporate blue
2705
+ purple: "#7d5ba6",
2706
+ // muted violet
2707
+ teal: "#3a9188",
2708
+ // teal
2709
+ cyan: "#4f96c4",
2710
+ // steel cyan
2711
+ gray: "#7e8a97",
2712
+ // cool gray
2713
+ black: "#1f2933",
2714
+ // slate ink
2715
+ white: "#f3f5f8"
2716
+ // panel
2717
+ }
2718
+ },
2719
+ dark: {
2720
+ bg: "#161b22",
2721
+ // deep slate (keynote dark)
2722
+ surface: "#202833",
2723
+ // raised panel
2724
+ overlay: "#29323e",
2725
+ // popovers, dropdowns
2726
+ border: "#38424f",
2727
+ // divider
2728
+ text: "#e6eaef",
2729
+ // off-white
2730
+ textMuted: "#9aa5b1",
2731
+ // secondary label
2732
+ textOnFillLight: "#ffffff",
2733
+ // light text on dark fills
2734
+ textOnFillDark: "#161b22",
2735
+ // dark text on light fills
2736
+ primary: "#5b9bd5",
2737
+ // lifted corporate blue
2738
+ secondary: "#8593a3",
2739
+ // slate gray, lifted
2740
+ accent: "#45b3a3",
2741
+ // teal, lifted
2742
+ destructive: "#e07b6e",
2743
+ // brick, lifted
2744
+ colors: {
2745
+ red: "#e07b6e",
2746
+ // brick
2747
+ orange: "#e0975a",
2748
+ // amber
2749
+ yellow: "#d9bd5a",
2750
+ // gold
2751
+ green: "#74b56e",
2752
+ // forest / sage
2753
+ blue: "#5b9bd5",
2754
+ // corporate blue
2755
+ purple: "#a585c9",
2756
+ // violet
2757
+ teal: "#45b3a3",
2758
+ // teal
2759
+ cyan: "#62b0d9",
2760
+ // steel cyan
2761
+ gray: "#95a1ae",
2762
+ // cool gray
2763
+ black: "#202833",
2764
+ // raised panel
2765
+ white: "#e6eaef"
2766
+ // off-white
2767
+ }
2768
+ }
2769
+ };
2770
+ registerPalette(slatePalette);
2771
+ }
2772
+ });
2773
+
2470
2774
  // src/palettes/solarized.ts
2471
2775
  var solarizedPalette;
2472
2776
  var init_solarized = __esm({
@@ -2562,6 +2866,120 @@ var init_solarized = __esm({
2562
2866
  }
2563
2867
  });
2564
2868
 
2869
+ // src/palettes/tidewater.ts
2870
+ var tidewaterPalette;
2871
+ var init_tidewater = __esm({
2872
+ "src/palettes/tidewater.ts"() {
2873
+ "use strict";
2874
+ init_registry();
2875
+ tidewaterPalette = {
2876
+ id: "tidewater",
2877
+ name: "Tidewater",
2878
+ light: {
2879
+ bg: "#eceff0",
2880
+ // weathered sea-mist paper
2881
+ surface: "#e0e4e3",
2882
+ // worn deck panel
2883
+ overlay: "#dadfdf",
2884
+ // popovers, dropdowns
2885
+ border: "#a9b2b3",
2886
+ // muted slate rule
2887
+ text: "#18313f",
2888
+ // ship's-log navy ink
2889
+ textMuted: "#51636b",
2890
+ // faded log entry
2891
+ textOnFillLight: "#f3f5f3",
2892
+ // weathered white
2893
+ textOnFillDark: "#162c38",
2894
+ // deep navy
2895
+ primary: "#1f4e6b",
2896
+ // deep-sea navy
2897
+ secondary: "#b08a4f",
2898
+ // rope / manila tan
2899
+ accent: "#c69a3e",
2900
+ // brass
2901
+ destructive: "#c1433a",
2902
+ // signal-flag red
2903
+ colors: {
2904
+ red: "#c1433a",
2905
+ // signal-flag red
2906
+ orange: "#cc7a38",
2907
+ // weathered amber
2908
+ yellow: "#d6bf5a",
2909
+ // brass gold
2910
+ green: "#4f8a6b",
2911
+ // sea-glass green
2912
+ blue: "#1f4e6b",
2913
+ // deep-sea navy
2914
+ purple: "#6a5a8c",
2915
+ // twilight harbor
2916
+ teal: "#3d8c8c",
2917
+ // sea-glass teal
2918
+ cyan: "#4f9bb5",
2919
+ // shallow water
2920
+ gray: "#8a8d86",
2921
+ // driftwood gray
2922
+ black: "#18313f",
2923
+ // navy ink
2924
+ white: "#e0e4e3"
2925
+ // deck panel
2926
+ }
2927
+ },
2928
+ dark: {
2929
+ bg: "#0f2230",
2930
+ // night-harbor deep sea
2931
+ surface: "#16303f",
2932
+ // raised hull
2933
+ overlay: "#1d3a4a",
2934
+ // popovers, dropdowns
2935
+ border: "#2c4856",
2936
+ // rigging line
2937
+ text: "#e6ebe8",
2938
+ // weathered white
2939
+ textMuted: "#9aaab0",
2940
+ // faded label
2941
+ textOnFillLight: "#f3f5f3",
2942
+ // weathered white
2943
+ textOnFillDark: "#0f2230",
2944
+ // deep sea
2945
+ primary: "#4f9bc4",
2946
+ // lifted sea blue
2947
+ secondary: "#c9a46a",
2948
+ // rope tan, lifted
2949
+ accent: "#d9b25a",
2950
+ // brass, lifted
2951
+ destructive: "#e06a5e",
2952
+ // signal red, lifted
2953
+ colors: {
2954
+ red: "#e06a5e",
2955
+ // signal-flag red
2956
+ orange: "#df9a52",
2957
+ // amber
2958
+ yellow: "#e0c662",
2959
+ // brass gold
2960
+ green: "#6fb58c",
2961
+ // sea-glass green
2962
+ blue: "#4f9bc4",
2963
+ // sea blue
2964
+ purple: "#9486bf",
2965
+ // twilight harbor
2966
+ teal: "#5cb0ac",
2967
+ // sea-glass teal
2968
+ cyan: "#62b4cf",
2969
+ // shallow water
2970
+ gray: "#9aa39c",
2971
+ // driftwood gray
2972
+ black: "#16303f",
2973
+ // raised hull
2974
+ white: "#e6ebe8"
2975
+ // weathered white
2976
+ }
2977
+ }
2978
+ };
2979
+ registerPalette(tidewaterPalette);
2980
+ }
2981
+ });
2982
+
2565
2983
  // src/palettes/tokyo-night.ts
2566
2984
  var tokyoNightPalette;
2567
2985
  var init_tokyo_night = __esm({
@@ -2837,7 +3255,8 @@ var init_monokai = __esm({
2837
3255
  // src/palettes/index.ts
2838
3256
  var palettes_exports = {};
2839
3257
  __export(palettes_exports, {
2840
- boldPalette: () => boldPalette,
3258
+ atlasPalette: () => atlasPalette,
3259
+ blueprintPalette: () => blueprintPalette,
2841
3260
  catppuccinPalette: () => catppuccinPalette,
2842
3261
  contrastText: () => contrastText,
2843
3262
  draculaPalette: () => draculaPalette,
@@ -2858,7 +3277,9 @@ __export(palettes_exports, {
2858
3277
  rosePinePalette: () => rosePinePalette,
2859
3278
  shade: () => shade,
2860
3279
  shapeFill: () => shapeFill,
3280
+ slatePalette: () => slatePalette,
2861
3281
  solarizedPalette: () => solarizedPalette,
3282
+ tidewaterPalette: () => tidewaterPalette,
2862
3283
  tint: () => tint,
2863
3284
  tokyoNightPalette: () => tokyoNightPalette
2864
3285
  });
@@ -2868,17 +3289,21 @@ var init_palettes = __esm({
2868
3289
  "use strict";
2869
3290
  init_registry();
2870
3291
  init_color_utils();
2871
- init_bold();
3292
+ init_atlas();
3293
+ init_blueprint();
2872
3294
  init_catppuccin();
2873
3295
  init_gruvbox();
2874
3296
  init_nord();
2875
3297
  init_one_dark();
2876
3298
  init_rose_pine();
3299
+ init_slate();
2877
3300
  init_solarized();
3301
+ init_tidewater();
2878
3302
  init_tokyo_night();
2879
3303
  init_dracula();
2880
3304
  init_monokai();
2881
- init_bold();
3305
+ init_atlas();
3306
+ init_blueprint();
2882
3307
  init_catppuccin();
2883
3308
  init_dracula();
2884
3309
  init_gruvbox();
@@ -2886,9 +3311,15 @@ var init_palettes = __esm({
2886
3311
  init_nord();
2887
3312
  init_one_dark();
2888
3313
  init_rose_pine();
3314
+ init_slate();
2889
3315
  init_solarized();
3316
+ init_tidewater();
2890
3317
  init_tokyo_night();
2891
3318
  palettes = {
3319
+ atlas: atlasPalette,
3320
+ blueprint: blueprintPalette,
3321
+ slate: slatePalette,
3322
+ tidewater: tidewaterPalette,
2892
3323
  nord: nordPalette,
2893
3324
  catppuccin: catppuccinPalette,
2894
3325
  solarized: solarizedPalette,
@@ -2897,8 +3328,7 @@ var init_palettes = __esm({
2897
3328
  oneDark: oneDarkPalette,
2898
3329
  rosePine: rosePinePalette,
2899
3330
  dracula: draculaPalette,
2900
- monokai: monokaiPalette,
2901
- bold: boldPalette
3331
+ monokai: monokaiPalette
2902
3332
  };
2903
3333
  }
2904
3334
  });
@@ -3408,6 +3838,9 @@ function controlsGroupCapsuleWidth(toggles) {
3408
3838
  }
3409
3839
  return w;
3410
3840
  }
3841
+ function isAppHostedControls(config, isExport) {
3842
+ return !isExport && config.controlsHost === "app" && !!config.controlsGroup && config.controlsGroup.toggles.length > 0;
3843
+ }
3411
3844
  function buildControlsGroupLayout(config, state) {
3412
3845
  const cg = config.controlsGroup;
3413
3846
  if (!cg || cg.toggles.length === 0) return void 0;
@@ -3461,6 +3894,7 @@ function buildControlsGroupLayout(config, state) {
3461
3894
  function computeLegendLayout(config, state, containerWidth) {
3462
3895
  const { groups, controls: configControls, mode } = config;
3463
3896
  const isExport = mode === "export";
3897
+ const gated = isAppHostedControls(config, isExport);
3464
3898
  const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
3465
3899
  if (isExport && !activeGroupName) {
3466
3900
  return {
@@ -3471,7 +3905,7 @@ function computeLegendLayout(config, state, containerWidth) {
3471
3905
  pills: []
3472
3906
  };
3473
3907
  }
3474
- const controlsGroupLayout = isExport ? void 0 : buildControlsGroupLayout(config, state);
3908
+ const controlsGroupLayout = isExport || gated ? void 0 : buildControlsGroupLayout(config, state);
3475
3909
  const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0 || !!g.gradient);
3476
3910
  if (visibleGroups.length === 0 && (!configControls || configControls.length === 0) && !controlsGroupLayout) {
3477
3911
  return {
@@ -8351,8 +8785,8 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8351
8785
  const pt = points[i];
8352
8786
  const ptSize = pt.size ?? symbolSize;
8353
8787
  const minGap = ptSize / 2 + 4;
8354
- const labelWidth = pt.name.length * fontSize * 0.6 + 8;
8355
- const labelX = pt.px - labelWidth / 2;
8788
+ const labelWidth2 = pt.name.length * fontSize * 0.6 + 8;
8789
+ const labelX = pt.px - labelWidth2 / 2;
8356
8790
  let bestLabelY = 0;
8357
8791
  let bestOffset = Infinity;
8358
8792
  let placed = false;
@@ -8364,7 +8798,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8364
8798
  const candidate = {
8365
8799
  x: labelX,
8366
8800
  y: labelY,
8367
- w: labelWidth,
8801
+ w: labelWidth2,
8368
8802
  h: labelHeight
8369
8803
  };
8370
8804
  let collision = false;
@@ -8406,7 +8840,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8406
8840
  const labelRect = {
8407
8841
  x: labelX,
8408
8842
  y: bestLabelY,
8409
- w: labelWidth,
8843
+ w: labelWidth2,
8410
8844
  h: labelHeight
8411
8845
  };
8412
8846
  placedLabels.push(labelRect);
@@ -8442,7 +8876,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8442
8876
  shape: {
8443
8877
  x: labelX - bgPad,
8444
8878
  y: bestLabelY - bgPad,
8445
- width: labelWidth + bgPad * 2,
8879
+ width: labelWidth2 + bgPad * 2,
8446
8880
  height: labelHeight + bgPad * 2
8447
8881
  },
8448
8882
  style: { fill: bg },
@@ -15882,10 +16316,6 @@ function parseMap(content) {
15882
16316
  handleTag(trimmed, lineNumber);
15883
16317
  continue;
15884
16318
  }
15885
- if ((firstWord === "muted" || firstWord === "natural") && trimmed === firstWord) {
15886
- handleDirective(firstWord, "", lineNumber);
15887
- continue;
15888
- }
15889
16319
  if (DIRECTIVE_SET.has(firstWord) && !trimmed.slice(firstWord.length).trimStart().startsWith(":")) {
15890
16320
  handleDirective(
15891
16321
  firstWord,
@@ -15932,24 +16362,6 @@ function parseMap(content) {
15932
16362
  pushWarning(line12, `Duplicate directive "${key}" \u2014 last value wins.`);
15933
16363
  };
15934
16364
  switch (key) {
15935
- case "region":
15936
- dup(d.region);
15937
- d.region = value;
15938
- break;
15939
- case "projection":
15940
- dup(d.projection);
15941
- if (value && ![
15942
- "equirectangular",
15943
- "natural-earth",
15944
- "albers-usa",
15945
- "mercator"
15946
- ].includes(value))
15947
- pushWarning(
15948
- line12,
15949
- `Unknown projection "${value}" (expected equirectangular | natural-earth | albers-usa | mercator).`
15950
- );
15951
- d.projection = value;
15952
- break;
15953
16365
  case "region-metric": {
15954
16366
  dup(d.regionMetric);
15955
16367
  const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
@@ -15965,91 +16377,43 @@ function parseMap(content) {
15965
16377
  dup(d.flowMetric);
15966
16378
  d.flowMetric = value;
15967
16379
  break;
15968
- case "scale":
15969
- dup(d.scale);
15970
- {
15971
- const s = parseScale(value, line12);
15972
- if (s) d.scale = s;
15973
- }
15974
- break;
15975
- case "region-labels":
15976
- dup(d.regionLabels);
15977
- if (value && !["full", "abbrev", "off"].includes(value))
15978
- pushWarning(
15979
- line12,
15980
- `Unknown region-labels "${value}" (expected full | abbrev | off).`
15981
- );
15982
- d.regionLabels = value;
15983
- break;
15984
- case "poi-labels":
15985
- dup(d.poiLabels);
15986
- if (value && !["off", "auto", "all"].includes(value))
15987
- pushWarning(
15988
- line12,
15989
- `Unknown poi-labels "${value}" (expected off | auto | all).`
15990
- );
15991
- d.poiLabels = value;
15992
- break;
15993
- case "default-country":
15994
- dup(d.defaultCountry);
15995
- d.defaultCountry = value;
15996
- break;
15997
- case "default-state":
15998
- dup(d.defaultState);
15999
- d.defaultState = value;
16380
+ case "locale":
16381
+ dup(d.locale);
16382
+ d.locale = value;
16000
16383
  break;
16001
16384
  case "active-tag":
16002
16385
  dup(d.activeTag);
16003
16386
  d.activeTag = value;
16004
16387
  break;
16388
+ case "caption":
16389
+ dup(d.caption);
16390
+ d.caption = value;
16391
+ break;
16392
+ // ── Cosmetic `no-*` opt-outs: bare flags, idempotent (mirror `no-legend`,
16393
+ // no dup warning); each defaults the feature ON when absent. ──
16005
16394
  case "no-legend":
16006
16395
  d.noLegend = true;
16007
16396
  break;
16008
- case "no-insets":
16009
- d.noInsets = true;
16397
+ case "no-coastline":
16398
+ d.noCoastline = true;
16010
16399
  break;
16011
- case "relief":
16012
- d.relief = true;
16400
+ case "no-relief":
16401
+ d.noRelief = true;
16013
16402
  break;
16014
- case "muted":
16015
- case "natural":
16016
- if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
16017
- pushWarning(
16018
- line12,
16019
- `Conflicting basemap dress \u2014 "${d.basemapStyle}" then "${key}"; last wins.`
16020
- );
16021
- d.basemapStyle = key;
16403
+ case "no-context-labels":
16404
+ d.noContextLabels = true;
16022
16405
  break;
16023
- case "subtitle":
16024
- dup(d.subtitle);
16025
- d.subtitle = value;
16406
+ case "no-region-labels":
16407
+ d.noRegionLabels = true;
16026
16408
  break;
16027
- case "caption":
16028
- dup(d.caption);
16029
- d.caption = value;
16409
+ case "no-poi-labels":
16410
+ d.noPoiLabels = true;
16411
+ break;
16412
+ case "no-colorize":
16413
+ d.noColorize = true;
16030
16414
  break;
16031
16415
  }
16032
16416
  }
16033
- function parseScale(value, line12) {
16034
- const toks = value.split(/\s+/).filter(Boolean);
16035
- const min = Number(toks[0]);
16036
- const max = Number(toks[1]);
16037
- if (!Number.isFinite(min) || !Number.isFinite(max)) {
16038
- pushError(line12, `scale requires numeric <min> <max> (got "${value}").`);
16039
- return null;
16040
- }
16041
- const scale = { min, max };
16042
- if (toks[2] === "center") {
16043
- const c = Number(toks[3]);
16044
- if (Number.isFinite(c)) scale.center = c;
16045
- else
16046
- pushError(
16047
- line12,
16048
- `scale center requires a number (got "${toks[3] ?? ""}").`
16049
- );
16050
- }
16051
- return scale;
16052
- }
16053
16417
  function handleTag(trimmed, line12) {
16054
16418
  const m = matchTagBlockHeading(trimmed);
16055
16419
  if (!m) {
@@ -16249,13 +16613,15 @@ function parseMap(content) {
16249
16613
  pushError(line12, `Edge has an empty endpoint: "${trimmed}".`);
16250
16614
  continue;
16251
16615
  }
16252
- const meta = k === links.length - 1 ? lastSplit.meta : {};
16616
+ const isLast = k === links.length - 1;
16617
+ const meta = isLast ? lastSplit.meta : {};
16618
+ const style = links[k].style === "arc" ? "arc" : "straight";
16253
16619
  edges.push({
16254
16620
  from,
16255
16621
  to,
16256
16622
  ...links[k].label !== void 0 && { label: links[k].label },
16257
16623
  directed: links[k].directed,
16258
- style: links[k].style,
16624
+ style,
16259
16625
  meta,
16260
16626
  lineNumber: line12
16261
16627
  });
@@ -16341,22 +16707,19 @@ var init_parser12 = __esm({
16341
16707
  LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
16342
16708
  AT_RE = /(^|[\s,])at\s*:/i;
16343
16709
  DIRECTIVE_SET = /* @__PURE__ */ new Set([
16344
- "region",
16345
- "projection",
16346
16710
  "region-metric",
16347
16711
  "poi-metric",
16348
16712
  "flow-metric",
16349
- "scale",
16350
- "region-labels",
16351
- "poi-labels",
16352
- "default-country",
16353
- "default-state",
16713
+ "locale",
16354
16714
  "active-tag",
16715
+ "caption",
16355
16716
  "no-legend",
16356
- "no-insets",
16357
- "relief",
16358
- "subtitle",
16359
- "caption"
16717
+ "no-coastline",
16718
+ "no-relief",
16719
+ "no-context-labels",
16720
+ "no-region-labels",
16721
+ "no-poi-labels",
16722
+ "no-colorize"
16360
16723
  ]);
16361
16724
  }
16362
16725
  });
@@ -16534,6 +16897,21 @@ function parseBoxesAndLines(content) {
16534
16897
  }
16535
16898
  continue;
16536
16899
  }
16900
+ if (!contentStarted) {
16901
+ const metricMatch = trimmed.match(/^box-metric\s+(.+)$/i);
16902
+ if (metricMatch) {
16903
+ const { label, colorName } = peelTrailingColorName(
16904
+ metricMatch[1].trim()
16905
+ );
16906
+ result.boxMetric = label;
16907
+ if (colorName !== void 0) result.boxMetricColor = colorName;
16908
+ continue;
16909
+ }
16910
+ if (/^show-values$/i.test(trimmed)) {
16911
+ result.showValues = true;
16912
+ continue;
16913
+ }
16914
+ }
16537
16915
  if (!contentStarted) {
16538
16916
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
16539
16917
  if (optMatch) {
@@ -16912,6 +17290,19 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
16912
17290
  description = [metadata["description"]];
16913
17291
  delete metadata["description"];
16914
17292
  }
17293
+ let value;
17294
+ if (metadata["value"] !== void 0) {
17295
+ const raw = metadata["value"];
17296
+ const num = Number(raw);
17297
+ if (Number.isFinite(num)) {
17298
+ value = num;
17299
+ } else {
17300
+ diagnostics.push(
17301
+ makeDgmoError(lineNum, `value must be a number (got "${raw}")`, "error")
17302
+ );
17303
+ }
17304
+ delete metadata["value"];
17305
+ }
16915
17306
  if (split.alias) {
16916
17307
  nameAliasMap?.set(normalizeName(split.alias), label);
16917
17308
  }
@@ -16920,7 +17311,8 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
16920
17311
  label,
16921
17312
  lineNumber: lineNum,
16922
17313
  metadata,
16923
- ...description !== void 0 && { description }
17314
+ ...description !== void 0 && { description },
17315
+ ...value !== void 0 && { value }
16924
17316
  };
16925
17317
  }
16926
17318
  function splitTargetAndMeta(rest, metaAliasMap) {
@@ -24277,8 +24669,8 @@ function renderKanban(container, parsed, palette, isDark, options) {
24277
24669
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24278
24670
  for (const meta of tagMeta) {
24279
24671
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(`${meta.label}: `);
24280
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24281
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24672
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24673
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24282
24674
  metaY += sCardMetaLineHeight;
24283
24675
  }
24284
24676
  for (const detail of card.details) {
@@ -24622,8 +25014,8 @@ function renderSwimlaneCard(parent, cardLayout, tagGroups, activeTagGroup, palet
24622
25014
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24623
25015
  for (const meta of tagMeta) {
24624
25016
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", palette.textMuted).text(`${meta.label}: `);
24625
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24626
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
25017
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
25018
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24627
25019
  metaY += sCardMetaLineHeight;
24628
25020
  }
24629
25021
  for (const detail of card.details) {
@@ -25458,8 +25850,8 @@ function classifyEREntities(tables, relationships) {
25458
25850
  }
25459
25851
  }
25460
25852
  const mmParticipants = /* @__PURE__ */ new Set();
25461
- for (const [id, neighbors] of tableStarNeighbors) {
25462
- if (neighbors.size >= 2) mmParticipants.add(id);
25853
+ for (const [id, neighbors2] of tableStarNeighbors) {
25854
+ if (neighbors2.size >= 2) mmParticipants.add(id);
25463
25855
  }
25464
25856
  const indegreeValues = Object.values(indegreeMap);
25465
25857
  const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
@@ -26040,7 +26432,18 @@ function fitLabelToHeader(label, nodeWidth, maxLines) {
26040
26432
  const truncated = label.length > maxChars ? label.slice(0, maxChars - 1) + "\u2026" : label;
26041
26433
  return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
26042
26434
  }
26043
- function nodeColors(node, tagGroups, activeGroupName, palette, isDark, solid) {
26435
+ function nodeColors(node, tagGroups, activeGroupName, palette, isDark, value, solid) {
26436
+ const neutralFill = mix(palette.bg, palette.text, isDark ? 90 : 95);
26437
+ if (value.active) {
26438
+ const fill3 = node.value !== void 0 ? value.fillForValue(node.value) : neutralFill;
26439
+ const stroke3 = value.hue;
26440
+ const text2 = contrastText(
26441
+ fill3,
26442
+ palette.textOnFillLight,
26443
+ palette.textOnFillDark
26444
+ );
26445
+ return { fill: fill3, stroke: stroke3, text: text2 };
26446
+ }
26044
26447
  const tagColor = resolveTagColor(
26045
26448
  node.metadata,
26046
26449
  [...tagGroups],
@@ -26130,7 +26533,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26130
26533
  controlsExpanded,
26131
26534
  onToggleDescriptions,
26132
26535
  onToggleControlsExpand,
26133
- exportMode = false
26536
+ exportMode = false,
26537
+ controlsHost
26134
26538
  } = options ?? {};
26135
26539
  d3Selection6.select(container).selectAll(":not([data-d3-tooltip])").remove();
26136
26540
  const width = exportDims?.width ?? container.clientWidth;
@@ -26148,21 +26552,65 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26148
26552
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26149
26553
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26150
26554
  const sTitleY = sctx.structural(TITLE_Y);
26151
- const sLegendHeight = sctx.structural(
26555
+ const nodeValues = parsed.nodes.filter((n) => n.value !== void 0).map((n) => n.value);
26556
+ const hasRamp = nodeValues.length > 0;
26557
+ const allNonNegative = hasRamp && nodeValues.every((v) => v >= 0);
26558
+ const rampMin = allNonNegative ? 0 : Math.min(...nodeValues);
26559
+ const rampMax = Math.max(...nodeValues);
26560
+ const rampHue = resolveColor(parsed.boxMetricColor ?? "", palette) ?? palette.primary;
26561
+ const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
26562
+ const fillForValue = (v) => {
26563
+ const t = rampMax > rampMin ? (v - rampMin) / (rampMax - rampMin) : 1;
26564
+ const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
26565
+ return mix(rampHue, rampBase, pct);
26566
+ };
26567
+ const VALUE_NAME = hasRamp ? parsed.boxMetric?.trim() || "Value" : null;
26568
+ const matchColorGroup = (v) => {
26569
+ const lv = v.trim().toLowerCase();
26570
+ if (lv === "none") return null;
26571
+ const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
26572
+ if (tg) return tg.name;
26573
+ if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
26574
+ return v;
26575
+ };
26576
+ const override = activeTagGroup;
26577
+ let activeGroup;
26578
+ if (override !== void 0) {
26579
+ activeGroup = override === null ? null : matchColorGroup(override);
26580
+ } else if (parsed.options["active-tag"] !== void 0) {
26581
+ activeGroup = matchColorGroup(parsed.options["active-tag"]);
26582
+ } else {
26583
+ activeGroup = VALUE_NAME ?? (parsed.tagGroups.length > 0 ? parsed.tagGroups[0].name : null);
26584
+ }
26585
+ const activeIsValue = VALUE_NAME !== null && activeGroup === VALUE_NAME;
26586
+ const valueGroup = VALUE_NAME !== null ? {
26587
+ name: VALUE_NAME,
26588
+ entries: [],
26589
+ gradient: {
26590
+ min: rampMin,
26591
+ max: rampMax,
26592
+ hue: rampHue,
26593
+ base: rampBase
26594
+ }
26595
+ } : null;
26596
+ const legendGroups = [
26597
+ ...valueGroup ? [valueGroup] : [],
26598
+ ...parsed.tagGroups
26599
+ ];
26600
+ const reserveHasDescriptions = parsed.nodes.some(
26601
+ (n) => n.description && n.description.length > 0
26602
+ );
26603
+ const willRenderLegend = legendGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26604
+ const sLegendHeight = willRenderLegend ? sctx.structural(
26152
26605
  getMaxLegendReservedHeight(
26153
26606
  {
26154
- groups: parsed.tagGroups,
26607
+ groups: legendGroups,
26155
26608
  position: { placement: "top-center", titleRelation: "below-title" },
26156
26609
  mode: exportMode ? "export" : "preview"
26157
26610
  },
26158
26611
  width
26159
26612
  )
26160
- );
26161
- const activeGroup = resolveActiveTagGroup(
26162
- parsed.tagGroups,
26163
- parsed.options["active-tag"],
26164
- activeTagGroup
26165
- );
26613
+ ) : 0;
26166
26614
  const hidden = hiddenTagValues ?? parsed.initialHiddenTagValues;
26167
26615
  const nodeMap = /* @__PURE__ */ new Map();
26168
26616
  for (const node of parsed.nodes) nodeMap.set(node.label, node);
@@ -26173,7 +26621,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26173
26621
  const hasAnyDescriptions = parsed.nodes.some(
26174
26622
  (n) => n.description && n.description.length > 0
26175
26623
  );
26176
- const needsLegend = parsed.tagGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26624
+ const needsLegend = legendGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26177
26625
  const legendH = needsLegend ? sLegendHeight + 8 : 0;
26178
26626
  const groupLabelsSet = new Set(layout.groups.map((g) => g.label));
26179
26627
  let labelZoneExtension = 0;
@@ -26379,12 +26827,16 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26379
26827
  activeGroup,
26380
26828
  palette,
26381
26829
  isDark,
26830
+ { active: activeIsValue, hue: rampHue, fillForValue },
26382
26831
  parsed.options["solid-fill"] === "on"
26383
26832
  );
26384
26833
  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);
26385
26834
  for (const [key, val] of Object.entries(node.metadata)) {
26386
26835
  nodeG.attr(`data-tag-${key.toLowerCase()}`, val.toLowerCase());
26387
26836
  }
26837
+ if (node.value !== void 0) {
26838
+ nodeG.attr("data-value", node.value);
26839
+ }
26388
26840
  if (onClickItem) {
26389
26841
  nodeG.on("click", (event) => {
26390
26842
  const target = event.target;
@@ -26468,14 +26920,30 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26468
26920
  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]);
26469
26921
  }
26470
26922
  }
26923
+ if (parsed.showValues && node.value !== void 0) {
26924
+ const valueText = String(node.value);
26925
+ const descShown = !!(desc && desc.length > 0 && !hideDescriptions);
26926
+ if (descShown) {
26927
+ const padX = 6;
26928
+ const padY = 5;
26929
+ const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
26930
+ const bh = VALUE_FONT_SIZE + 4;
26931
+ const bx = ln.width / 2 - bw - 4;
26932
+ const by = -ln.height / 2 + 4;
26933
+ 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);
26934
+ 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);
26935
+ } else {
26936
+ 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);
26937
+ }
26938
+ }
26471
26939
  }
26472
26940
  const hasDescriptions = parsed.nodes.some(
26473
26941
  (n) => n.description && n.description.length > 0
26474
26942
  );
26475
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions;
26943
+ const hasLegend = legendGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26476
26944
  if (hasLegend) {
26477
26945
  let controlsGroup;
26478
- if (hasDescriptions && onToggleDescriptions) {
26946
+ if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
26479
26947
  controlsGroup = {
26480
26948
  toggles: [
26481
26949
  {
@@ -26490,10 +26958,17 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26490
26958
  };
26491
26959
  }
26492
26960
  const legendConfig = {
26493
- groups: parsed.tagGroups,
26961
+ groups: legendGroups,
26494
26962
  position: { placement: "top-center", titleRelation: "below-title" },
26495
26963
  mode: exportMode ? "export" : "preview",
26496
- ...controlsGroup !== void 0 && { controlsGroup }
26964
+ // Keep inactive sibling tag groups visible as collapsed pills so the user
26965
+ // can click one to flip the active colouring dimension (preview only —
26966
+ // export shows just the active group). Without this, declaring a second
26967
+ // tag group (e.g. Team) leaves it invisible whenever another group is
26968
+ // active. The app's BoxesAndLinesPreview already wires pill clicks.
26969
+ showInactivePills: true,
26970
+ ...controlsGroup !== void 0 && { controlsGroup },
26971
+ ...controlsHost !== void 0 && { controlsHost }
26497
26972
  };
26498
26973
  const legendState = {
26499
26974
  activeGroup,
@@ -26538,7 +27013,7 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
26538
27013
  }
26539
27014
  });
26540
27015
  }
26541
- var d3Selection6, d3Shape4, 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;
27016
+ var d3Selection6, d3Shape4, 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;
26542
27017
  var init_renderer6 = __esm({
26543
27018
  "src/boxes-and-lines/renderer.ts"() {
26544
27019
  "use strict";
@@ -26549,6 +27024,7 @@ var init_renderer6 = __esm({
26549
27024
  init_legend_layout();
26550
27025
  init_title_constants();
26551
27026
  init_color_utils();
27027
+ init_colors();
26552
27028
  init_tag_groups();
26553
27029
  init_inline_markdown();
26554
27030
  init_wrapped_desc();
@@ -26571,6 +27047,8 @@ var init_renderer6 = __esm({
26571
27047
  GROUP_RX = 8;
26572
27048
  GROUP_LABEL_FONT_SIZE = 14;
26573
27049
  GROUP_LABEL_ZONE = 32;
27050
+ RAMP_FLOOR = 15;
27051
+ VALUE_FONT_SIZE = 11;
26574
27052
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
26575
27053
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
26576
27054
  }
@@ -27741,8 +28219,9 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27741
28219
  const containerHeight = exportDims?.height ?? (container.getBoundingClientRect().height || 600);
27742
28220
  d3Selection7.select(container).selectAll("*").remove();
27743
28221
  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);
28222
+ const appHosted = options?.controlsHost === "app";
27744
28223
  const hasControls = !!options?.onToggleColorByDepth || !!options?.onToggleDescriptions;
27745
- const hasLegend = parsed.tagGroups.length > 0 || hasControls;
28224
+ const hasLegend = parsed.tagGroups.length > 0 || hasControls && !appHosted;
27746
28225
  const fixedLegend = !isExport && hasLegend;
27747
28226
  const legendReserve = fixedLegend ? getMaxLegendReservedHeight(
27748
28227
  {
@@ -27836,7 +28315,10 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27836
28315
  }),
27837
28316
  position: { placement: "top-center", titleRelation: "below-title" },
27838
28317
  mode: options?.exportMode ? "export" : "preview",
27839
- ...controlsToggles !== void 0 && { controlsGroup: controlsToggles }
28318
+ ...controlsToggles !== void 0 && { controlsGroup: controlsToggles },
28319
+ ...options?.controlsHost !== void 0 && {
28320
+ controlsHost: options.controlsHost
28321
+ }
27840
28322
  };
27841
28323
  const legendState = {
27842
28324
  activeGroup: options?.colorByDepth ? null : activeTagGroup !== void 0 ? activeTagGroup : parsed.options["active-tag"] ?? null,
@@ -28280,8 +28762,8 @@ function computeFieldAlignX(children) {
28280
28762
  for (const child of children) {
28281
28763
  if (child.metadata["_labelField"] === "true" && child.children.length >= 2) {
28282
28764
  const labelEl = child.children[0];
28283
- const labelWidth = labelEl.label.length * CHAR_WIDTH5;
28284
- maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
28765
+ const labelWidth2 = labelEl.label.length * CHAR_WIDTH5;
28766
+ maxLabelWidth = Math.max(maxLabelWidth, labelWidth2);
28285
28767
  labelFieldCount++;
28286
28768
  }
28287
28769
  }
@@ -33245,7 +33727,7 @@ function hasRoles(node) {
33245
33727
  function computeNodeWidth2(node, expanded, options) {
33246
33728
  const badgeVal = node.computedConcurrentInvocations === 0 && node.computedInstances > 1 ? node.computedInstances : 0;
33247
33729
  const badgeLen = badgeVal > 0 ? `${badgeVal}x`.length + 2 : 0;
33248
- const labelWidth = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33730
+ const labelWidth2 = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33249
33731
  const allKeys = [];
33250
33732
  if (node.computedRps > 0) allKeys.push("RPS");
33251
33733
  if (expanded) {
@@ -33289,7 +33771,7 @@ function computeNodeWidth2(node, expanded, options) {
33289
33771
  allKeys.push("overflow");
33290
33772
  }
33291
33773
  }
33292
- if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth);
33774
+ if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth2);
33293
33775
  const maxKeyLen = Math.max(...allKeys.map((k) => k.length));
33294
33776
  let maxRowWidth = 0;
33295
33777
  if (node.computedRps > 0) {
@@ -33377,7 +33859,7 @@ function computeNodeWidth2(node, expanded, options) {
33377
33859
  truncated.length * META_CHAR_WIDTH3 + PADDING_X3
33378
33860
  );
33379
33861
  }
33380
- return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20, descWidth);
33862
+ return Math.max(MIN_NODE_WIDTH2, labelWidth2, maxRowWidth + 20, descWidth);
33381
33863
  }
33382
33864
  function computeNodeHeight2(node, expanded, options) {
33383
33865
  const propCount = countDisplayProps(node, expanded, options);
@@ -34926,8 +35408,9 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
34926
35408
  }
34927
35409
  return groups;
34928
35410
  }
34929
- function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false) {
35411
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false, controlsHost) {
34930
35412
  if (legendGroups.length === 0 && !playback) return;
35413
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34931
35414
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
34932
35415
  if (activeGroup) {
34933
35416
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
@@ -34936,14 +35419,29 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34936
35419
  name: g.name,
34937
35420
  entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
34938
35421
  }));
34939
- if (playback) {
35422
+ if (playback && !appHostedPlayback) {
34940
35423
  allGroups.push({ name: "Playback", entries: [] });
34941
35424
  }
34942
35425
  const legendConfig = {
34943
35426
  groups: allGroups,
34944
35427
  position: { placement: "top-center", titleRelation: "below-title" },
34945
35428
  mode: exportMode ? "export" : "preview",
34946
- showEmptyGroups: true
35429
+ showEmptyGroups: true,
35430
+ ...appHostedPlayback && {
35431
+ controlsHost: "app",
35432
+ controlsGroup: {
35433
+ toggles: [
35434
+ {
35435
+ id: "playback",
35436
+ type: "toggle",
35437
+ label: "Playback",
35438
+ active: true,
35439
+ onToggle: () => {
35440
+ }
35441
+ }
35442
+ ]
35443
+ }
35444
+ }
34947
35445
  };
34948
35446
  const legendState = { activeGroup };
34949
35447
  renderLegendD3(
@@ -34994,8 +35492,9 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34994
35492
  }
34995
35493
  }
34996
35494
  }
34997
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
35495
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes, controlsHost) {
34998
35496
  d3Selection11.select(container).selectAll(":not([data-d3-tooltip])").remove();
35497
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34999
35498
  const ctx = ScaleContext.identity();
35000
35499
  const sc = buildScaledConstants(ctx);
35001
35500
  const legendGroups = computeInfraLegendGroups(
@@ -35004,7 +35503,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35004
35503
  palette,
35005
35504
  layout.edges
35006
35505
  );
35007
- const hasLegend = legendGroups.length > 0 || !!playback;
35506
+ const hasLegend = legendGroups.length > 0 || !!playback && !appHostedPlayback;
35008
35507
  const fixedLegend = !exportMode && hasLegend;
35009
35508
  const legendDynamicH = hasLegend ? getMaxLegendReservedHeight(
35010
35509
  {
@@ -35148,7 +35647,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35148
35647
  isDark,
35149
35648
  activeGroup ?? null,
35150
35649
  playback ?? void 0,
35151
- exportMode
35650
+ exportMode,
35651
+ controlsHost
35152
35652
  );
35153
35653
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
35154
35654
  } else {
@@ -35161,7 +35661,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35161
35661
  isDark,
35162
35662
  activeGroup ?? null,
35163
35663
  playback ?? void 0,
35164
- exportMode
35664
+ exportMode,
35665
+ controlsHost
35165
35666
  );
35166
35667
  }
35167
35668
  }
@@ -42796,6 +43297,9 @@ function renderTechRadar(container, parsed, palette, isDark, onClickItem, export
42796
43297
  onToggle: (active) => options.onToggleListing(active)
42797
43298
  }
42798
43299
  ]
43300
+ },
43301
+ ...options.controlsHost !== void 0 && {
43302
+ controlsHost: options.controlsHost
42799
43303
  }
42800
43304
  };
42801
43305
  const legendState = {
@@ -44619,7 +45123,7 @@ function computeCycleLayout(parsed, options) {
44619
45123
  const circleNodes = parsed.options["circle-nodes"] === "true";
44620
45124
  const nodeDims = parsed.nodes.map((node) => {
44621
45125
  const hasDesc = !hideDescriptions && node.description.length > 0;
44622
- const labelWidth = Math.max(
45126
+ const labelWidth2 = Math.max(
44623
45127
  MIN_NODE_WIDTH4,
44624
45128
  node.label.length * LABEL_CHAR_W + NODE_PAD_X * 2
44625
45129
  );
@@ -44628,12 +45132,12 @@ function computeCycleLayout(parsed, options) {
44628
45132
  }
44629
45133
  if (!hasDesc) {
44630
45134
  return {
44631
- width: Math.min(MAX_NODE_WIDTH3, labelWidth),
45135
+ width: Math.min(MAX_NODE_WIDTH3, labelWidth2),
44632
45136
  height: PLAIN_NODE_HEIGHT,
44633
45137
  wrappedDesc: []
44634
45138
  };
44635
45139
  }
44636
- return chooseDescribedRectDims(node.description, labelWidth);
45140
+ return chooseDescribedRectDims(node.description, labelWidth2);
44637
45141
  });
44638
45142
  if (circleNodes) {
44639
45143
  const maxDiam = Math.max(...nodeDims.map((d) => d.width));
@@ -44829,10 +45333,10 @@ function computeCycleLayout(parsed, options) {
44829
45333
  scale
44830
45334
  };
44831
45335
  }
44832
- function chooseDescribedRectDims(description, labelWidth) {
45336
+ function chooseDescribedRectDims(description, labelWidth2) {
44833
45337
  const minW = Math.min(
44834
45338
  MAX_NODE_WIDTH3,
44835
- Math.max(MIN_NODE_WIDTH4, labelWidth, DESC_MIN_WIDTH)
45339
+ Math.max(MIN_NODE_WIDTH4, labelWidth2, DESC_MIN_WIDTH)
44836
45340
  );
44837
45341
  let best = null;
44838
45342
  let bestScore = Infinity;
@@ -45260,7 +45764,8 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45260
45764
  const hideDescriptions = (renderOptions?.hideDescriptions ?? false) || parsed.options["no-descriptions"] === "true" || viewState?.hd === true;
45261
45765
  const showDescriptions = !hideDescriptions;
45262
45766
  const hasDescriptions = parsed.nodes.some((n) => n.description.length > 0) || parsed.edges.some((e) => e.description.length > 0);
45263
- const hasLegend = hasDescriptions && !!renderOptions?.onToggleDescriptions;
45767
+ const appHostedControls = renderOptions?.controlsHost === "app";
45768
+ const hasLegend = !appHostedControls && hasDescriptions && !!renderOptions?.onToggleDescriptions;
45264
45769
  const showTitle = !!parsed.title && parsed.options["no-title"] !== "on";
45265
45770
  const legendOffset = hasLegend ? sLegendHeight : 0;
45266
45771
  const layoutHeight = height - (showTitle ? sTitleAreaHeight : 0) - legendOffset;
@@ -45297,7 +45802,10 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45297
45802
  groups: [],
45298
45803
  position: { placement: "top-center", titleRelation: "below-title" },
45299
45804
  mode: renderOptions?.exportMode ? "export" : "preview",
45300
- controlsGroup
45805
+ controlsGroup,
45806
+ ...renderOptions?.controlsHost !== void 0 && {
45807
+ controlsHost: renderOptions.controlsHost
45808
+ }
45301
45809
  };
45302
45810
  const legendState = {
45303
45811
  activeGroup: null,
@@ -45568,6 +46076,107 @@ function featureIndex(topo) {
45568
46076
  }
45569
46077
  return idx;
45570
46078
  }
46079
+ function buildAdjacency(topo) {
46080
+ const cached = adjacencyCache.get(topo);
46081
+ if (cached) return cached;
46082
+ const geometries = geomObject(topo).geometries;
46083
+ const nb = (0, import_topojson_client.neighbors)(geometries);
46084
+ const sets = /* @__PURE__ */ new Map();
46085
+ geometries.forEach((g, i) => {
46086
+ if (!g.type || g.type === "null") return;
46087
+ let set = sets.get(g.id);
46088
+ if (!set) {
46089
+ set = /* @__PURE__ */ new Set();
46090
+ sets.set(g.id, set);
46091
+ }
46092
+ for (const j of nb[i] ?? []) {
46093
+ const nid = geometries[j]?.id;
46094
+ if (nid && nid !== g.id) set.add(nid);
46095
+ }
46096
+ });
46097
+ const out = /* @__PURE__ */ new Map();
46098
+ for (const [iso, set] of sets) out.set(iso, [...set].sort());
46099
+ adjacencyCache.set(topo, out);
46100
+ return out;
46101
+ }
46102
+ function decodeFeatures(topo) {
46103
+ return geomObject(topo).geometries.map((g) => {
46104
+ const f = (0, import_topojson_client.feature)(topo, g);
46105
+ return {
46106
+ type: "Feature",
46107
+ id: g.id,
46108
+ properties: g.properties,
46109
+ geometry: f.geometry
46110
+ };
46111
+ });
46112
+ }
46113
+ function pointInRing(lon, lat, ring) {
46114
+ let inside = false;
46115
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46116
+ const xi = ring[i][0];
46117
+ const yi = ring[i][1];
46118
+ const xj = ring[j][0];
46119
+ const yj = ring[j][1];
46120
+ const intersect = yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi;
46121
+ if (intersect) inside = !inside;
46122
+ }
46123
+ return inside;
46124
+ }
46125
+ function pointOnRingEdge(lon, lat, ring) {
46126
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46127
+ const xi = ring[i][0];
46128
+ const yi = ring[i][1];
46129
+ const xj = ring[j][0];
46130
+ const yj = ring[j][1];
46131
+ if (lon < Math.min(xi, xj) - EDGE_EPS || lon > Math.max(xi, xj) + EDGE_EPS)
46132
+ continue;
46133
+ if (lat < Math.min(yi, yj) - EDGE_EPS || lat > Math.max(yi, yj) + EDGE_EPS)
46134
+ continue;
46135
+ const cross = (xj - xi) * (lat - yi) - (yj - yi) * (lon - xi);
46136
+ if (Math.abs(cross) <= EDGE_EPS) return true;
46137
+ }
46138
+ return false;
46139
+ }
46140
+ function pointInGeometry(geometry, lon, lat) {
46141
+ const g = geometry;
46142
+ if (!g) return false;
46143
+ const polys = g.type === "Polygon" ? [g.coordinates] : g.type === "MultiPolygon" ? g.coordinates : [];
46144
+ for (const rings of polys) {
46145
+ if (!rings.length) continue;
46146
+ if (pointOnRingEdge(lon, lat, rings[0])) return true;
46147
+ if (!pointInRing(lon, lat, rings[0])) continue;
46148
+ let inHole = false;
46149
+ for (let h = 1; h < rings.length; h++) {
46150
+ if (pointInRing(lon, lat, rings[h]) && !pointOnRingEdge(lon, lat, rings[h])) {
46151
+ inHole = true;
46152
+ break;
46153
+ }
46154
+ }
46155
+ if (!inHole) return true;
46156
+ }
46157
+ return false;
46158
+ }
46159
+ function regionAt(lonLat, countries, states) {
46160
+ const lon = lonLat[0];
46161
+ const lat = lonLat[1];
46162
+ let country = null;
46163
+ for (const f of countries) {
46164
+ if (pointInGeometry(f.geometry, lon, lat)) {
46165
+ country = { iso: f.id, name: f.properties.name };
46166
+ break;
46167
+ }
46168
+ }
46169
+ let state = null;
46170
+ if (country?.iso === "US" && states) {
46171
+ for (const f of states) {
46172
+ if (pointInGeometry(f.geometry, lon, lat)) {
46173
+ state = { iso: f.id, name: f.properties.name };
46174
+ break;
46175
+ }
46176
+ }
46177
+ }
46178
+ return { country, state };
46179
+ }
45571
46180
  function featureBbox(topo, geomId) {
45572
46181
  const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45573
46182
  if (!geom) return null;
@@ -45685,13 +46294,15 @@ function unionLongitudes(lons) {
45685
46294
  }
45686
46295
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45687
46296
  }
45688
- var import_topojson_client, import_d3_geo, fold, DETACH_GAP_DEG, DETACH_AREA_FRAC;
46297
+ var import_topojson_client, import_d3_geo, fold, adjacencyCache, EDGE_EPS, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45689
46298
  var init_geo = __esm({
45690
46299
  "src/map/geo.ts"() {
45691
46300
  "use strict";
45692
46301
  import_topojson_client = require("topojson-client");
45693
46302
  import_d3_geo = require("d3-geo");
45694
46303
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
46304
+ adjacencyCache = /* @__PURE__ */ new WeakMap();
46305
+ EDGE_EPS = 1e-9;
45695
46306
  DETACH_GAP_DEG = 10;
45696
46307
  DETACH_AREA_FRAC = 0.25;
45697
46308
  }
@@ -45711,6 +46322,12 @@ function looksUS(lat, lon) {
45711
46322
  if (lat < 15 || lat > 72) return false;
45712
46323
  return lon >= -180 && lon <= -64 || lon >= 172;
45713
46324
  }
46325
+ function looksNorthAmericaNeighbor(lat, lon) {
46326
+ return lat >= 14 && lat <= 72 && lon >= -141 && lon <= -52;
46327
+ }
46328
+ function isWholeSphere(bb) {
46329
+ return bb[0][0] <= -179 && bb[1][0] >= 179 && bb[0][1] <= -89 && bb[1][1] >= 89;
46330
+ }
45714
46331
  function resolveMap(parsed, data) {
45715
46332
  const diagnostics = [...parsed.diagnostics];
45716
46333
  const err = (line12, message, code) => {
@@ -45721,9 +46338,6 @@ function resolveMap(parsed, data) {
45721
46338
  };
45722
46339
  const result = {
45723
46340
  title: parsed.title,
45724
- ...parsed.directives.subtitle !== void 0 && {
45725
- subtitle: parsed.directives.subtitle
45726
- },
45727
46341
  ...parsed.directives.caption !== void 0 && {
45728
46342
  caption: parsed.directives.caption
45729
46343
  },
@@ -45733,7 +46347,7 @@ function resolveMap(parsed, data) {
45733
46347
  // renderer's job (step 4) — the resolver only carries `tags` + `tagGroups`
45734
46348
  // through; it never resolves a tag value to a palette color (#10).
45735
46349
  directives: { ...parsed.directives },
45736
- basemaps: { world: "coarse", subdivisions: [] },
46350
+ basemaps: { world: "detail", subdivisions: [] },
45737
46351
  regions: [],
45738
46352
  pois: [],
45739
46353
  edges: [],
@@ -45742,7 +46356,8 @@ function resolveMap(parsed, data) {
45742
46356
  [-180, -85],
45743
46357
  [180, 85]
45744
46358
  ],
45745
- projection: "natural-earth",
46359
+ projection: "equirectangular",
46360
+ poiFrameContainers: [],
45746
46361
  diagnostics,
45747
46362
  error: parsed.error
45748
46363
  };
@@ -45752,7 +46367,10 @@ function resolveMap(parsed, data) {
45752
46367
  ...[...countryIndex.values()].map((v) => v.name),
45753
46368
  ...[...usStateIndex.values()].map((v) => v.name)
45754
46369
  ];
45755
- const usScoped = parsed.directives.region === "us-states" || parsed.directives.defaultCountry?.toUpperCase() === "US" || parsed.regions.some((r) => {
46370
+ const localeRaw = parsed.directives.locale?.toUpperCase();
46371
+ const localeCountry = localeRaw ? localeRaw.split("-")[0] : void 0;
46372
+ const localeSubdivision = localeRaw && /^[A-Z]{2}-/.test(localeRaw) ? localeRaw : void 0;
46373
+ const usScoped = localeCountry === "US" || parsed.regions.some((r) => {
45756
46374
  const f = fold(r.name);
45757
46375
  return usStateIndex.has(f) && !countryIndex.has(f);
45758
46376
  }) || parsed.regions.some(
@@ -45903,7 +46521,7 @@ function resolveMap(parsed, data) {
45903
46521
  if (!scope)
45904
46522
  warn(
45905
46523
  line12,
45906
- `"${name}" is ambiguous \u2014 resolved to the most-populous match.`,
46524
+ `"${name}" is ambiguous \u2014 resolved to the most-populous match. Set a default with \`locale <ISO>\` (e.g. \`locale US\` / \`locale US-GA\`) to steer it.`,
45907
46525
  "W_MAP_AMBIGUOUS_NAME"
45908
46526
  );
45909
46527
  }
@@ -45916,17 +46534,21 @@ function resolveMap(parsed, data) {
45916
46534
  return fold(pos.name);
45917
46535
  };
45918
46536
  const poiCountries = [];
45919
- let anyNonUsPoi = false;
46537
+ let anyUsPoi = false;
46538
+ let anyNonNaPoi = false;
45920
46539
  const noteCountry = (iso) => {
45921
46540
  if (iso) {
45922
46541
  poiCountries.push(iso);
45923
- if (iso !== "US") anyNonUsPoi = true;
46542
+ if (iso === "US") anyUsPoi = true;
46543
+ if (iso !== "US" && iso !== "CA" && iso !== "MX") anyNonNaPoi = true;
45924
46544
  }
45925
46545
  };
45926
46546
  const deferred = [];
45927
46547
  for (const p of parsed.pois) {
45928
46548
  if (p.pos.kind === "coords") {
45929
- if (!looksUS(p.pos.lat, p.pos.lon)) anyNonUsPoi = true;
46549
+ if (looksUS(p.pos.lat, p.pos.lon)) anyUsPoi = true;
46550
+ else if (!looksNorthAmericaNeighbor(p.pos.lat, p.pos.lon))
46551
+ anyNonNaPoi = true;
45930
46552
  addResolvedPoi(p.pos.lat, p.pos.lon, p);
45931
46553
  continue;
45932
46554
  }
@@ -45944,14 +46566,15 @@ function resolveMap(parsed, data) {
45944
46566
  deferred.push(p);
45945
46567
  }
45946
46568
  }
45947
- const inferredCountry = parsed.directives.defaultCountry?.toUpperCase() ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46569
+ const inferredCountry = localeCountry ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46570
+ const inferredScope = localeSubdivision ?? inferredCountry;
45948
46571
  for (const p of deferred) {
45949
46572
  if (p.pos.kind !== "name") continue;
45950
46573
  const got = lookupName(
45951
46574
  p.pos.name,
45952
46575
  p.pos.scope,
45953
46576
  p.lineNumber,
45954
- inferredCountry,
46577
+ inferredScope,
45955
46578
  true
45956
46579
  );
45957
46580
  if (got.kind === "ok") {
@@ -46021,7 +46644,8 @@ function resolveMap(parsed, data) {
46021
46644
  const meta = sizeValue !== void 0 ? { value: sizeValue } : {};
46022
46645
  if (pos.kind === "coords") {
46023
46646
  const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
46024
- if (!looksUS(pos.lat, pos.lon)) anyNonUsPoi = true;
46647
+ if (looksUS(pos.lat, pos.lon)) anyUsPoi = true;
46648
+ else if (!looksNorthAmericaNeighbor(pos.lat, pos.lon)) anyNonNaPoi = true;
46025
46649
  if (!registry.has(id)) {
46026
46650
  registerPoi(
46027
46651
  id,
@@ -46044,7 +46668,7 @@ function resolveMap(parsed, data) {
46044
46668
  if (registry.has(f)) return f;
46045
46669
  const aliased = declaredByName.get(f);
46046
46670
  if (aliased) return aliased;
46047
- const got = lookupName(pos.name, pos.scope, line12, inferredCountry, true);
46671
+ const got = lookupName(pos.name, pos.scope, line12, inferredScope, true);
46048
46672
  if (got.kind !== "ok") return null;
46049
46673
  noteCountry(got.iso);
46050
46674
  registerPoi(
@@ -46101,9 +46725,12 @@ function resolveMap(parsed, data) {
46101
46725
  }
46102
46726
  routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
46103
46727
  }
46728
+ const hasUsContent = usSubdivisionReferenced || anyUsPoi || localeCountry === "US";
46729
+ const usOriented = !anyNonNaPoi && !regions.some(
46730
+ (r) => r.layer === "country" && !["US", "CA", "MX"].includes(r.iso)
46731
+ ) && hasUsContent;
46104
46732
  const subdivisions = [];
46105
- if (usSubdivisionReferenced || parsed.directives.region === "us-states")
46106
- subdivisions.push("us-states");
46733
+ if (usSubdivisionReferenced || usOriented) subdivisions.push("us-states");
46107
46734
  const regionBoxes = [];
46108
46735
  for (const ref of referencedRegionIds) {
46109
46736
  const bb = featureBbox(data.usStates, ref.id);
@@ -46121,17 +46748,51 @@ function resolveMap(parsed, data) {
46121
46748
  [-180, -85],
46122
46749
  [180, 85]
46123
46750
  ];
46124
- let extent2 = unioned ? pad(unioned, PAD_FRACTION) : DEFAULT_EXTENT;
46751
+ const basePad = regions.length > 0 ? REGION_PAD_FRACTION : PAD_FRACTION;
46752
+ let extent2 = unioned ? pad(unioned, basePad) : DEFAULT_EXTENT;
46753
+ const isPoiOnly = pois.length > 0 && regions.length === 0;
46754
+ const containerRegionIds = [];
46755
+ if (isPoiOnly) {
46756
+ const countries = decodeFeatures(data.worldDetail);
46757
+ const states = decodeFeatures(data.usStates);
46758
+ const seen = /* @__PURE__ */ new Set();
46759
+ const containerBoxes = [];
46760
+ for (const p of pois) {
46761
+ const { country, state } = regionAt([p.lon, p.lat], countries, states);
46762
+ const id = state?.iso ?? country?.iso;
46763
+ if (!id || seen.has(id)) continue;
46764
+ seen.add(id);
46765
+ containerRegionIds.push(id);
46766
+ const bb = state ? featureBbox(data.usStates, id) : featureBboxPrimary(data.worldCoarse, id);
46767
+ if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46768
+ }
46769
+ const containerUnion = unionExtent(containerBoxes, points);
46770
+ if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46771
+ }
46772
+ if (isPoiOnly) {
46773
+ const cx = (extent2[0][0] + extent2[1][0]) / 2;
46774
+ const cy = (extent2[0][1] + extent2[1][1]) / 2;
46775
+ const lon = extent2[1][0] - extent2[0][0];
46776
+ const lat = extent2[1][1] - extent2[0][1];
46777
+ const longer = Math.max(lon, lat);
46778
+ if (longer > 0 && longer < POI_ZOOM_FLOOR_DEG) {
46779
+ const k = POI_ZOOM_FLOOR_DEG / longer;
46780
+ const halfLon = lon * k / 2;
46781
+ const halfLat = lat * k / 2;
46782
+ extent2 = [
46783
+ [cx - halfLon, cy - halfLat],
46784
+ [cx + halfLon, cy + halfLat]
46785
+ ];
46786
+ }
46787
+ }
46125
46788
  const lonSpan = extent2[1][0] - extent2[0][0];
46126
46789
  const latSpan = extent2[1][1] - extent2[0][1];
46127
46790
  const span = Math.max(lonSpan, latSpan);
46128
46791
  const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
46129
- const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46130
46792
  let projection;
46131
- const override = parsed.directives.projection;
46132
- if (override === "equirectangular" || override === "natural-earth" || override === "albers-usa" || override === "mercator") {
46133
- projection = override;
46134
- } else if (usDominant) {
46793
+ if (isPoiOnly && usOriented && lonSpan < US_NATIONAL_LON_SPAN) {
46794
+ projection = "mercator";
46795
+ } else if (usOriented) {
46135
46796
  projection = "albers-usa";
46136
46797
  } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
46137
46798
  projection = "equirectangular";
@@ -46149,11 +46810,20 @@ function resolveMap(parsed, data) {
46149
46810
  result.edges = edges;
46150
46811
  result.routes = routes;
46151
46812
  result.basemaps = {
46152
- world: span > WORLD_SPAN ? "coarse" : "detail",
46813
+ // Tier is intentionally pinned to detail (50m) at ALL scales. Diagrammo maps
46814
+ // are presentational (palette tints, relief hachures, POI hubs), not
46815
+ // survey-grade — recognizability > generalization: 110m coarse drops the
46816
+ // Italian boot to a stump at world scale. `WORLD_SPAN` lives on only for the
46817
+ // projection decision (the `usOriented`/`span > WORLD_SPAN` chain above); it
46818
+ // no longer gates basemap resolution.
46819
+ // `worldCoarse` is still loaded — it's the authoritative name/bbox index
46820
+ // (featureIndex, featureBboxPrimary), not dead code.
46821
+ world: "detail",
46153
46822
  subdivisions
46154
46823
  };
46155
46824
  result.extent = extent2;
46156
46825
  result.projection = projection;
46826
+ result.poiFrameContainers = containerRegionIds;
46157
46827
  result.error = parsed.error ?? firstError(diagnostics);
46158
46828
  return result;
46159
46829
  }
@@ -46190,7 +46860,7 @@ function firstError(diags) {
46190
46860
  const e = diags.find((d) => d.severity === "error");
46191
46861
  return e ? formatDgmoError(e) : null;
46192
46862
  }
46193
- var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
46863
+ var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, REGION_PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, POI_ZOOM_FLOOR_DEG, US_NATIONAL_LON_SPAN, REGION_ALIASES, US_STATE_POSTAL;
46194
46864
  var init_resolver2 = __esm({
46195
46865
  "src/map/resolver.ts"() {
46196
46866
  "use strict";
@@ -46199,8 +46869,11 @@ var init_resolver2 = __esm({
46199
46869
  WORLD_SPAN = 90;
46200
46870
  MERCATOR_MAX_LAT = 80;
46201
46871
  PAD_FRACTION = 0.05;
46872
+ REGION_PAD_FRACTION = 0.12;
46202
46873
  WORLD_LAT_SOUTH = -58;
46203
46874
  WORLD_LAT_NORTH = 78;
46875
+ POI_ZOOM_FLOOR_DEG = 7;
46876
+ US_NATIONAL_LON_SPAN = 48;
46204
46877
  REGION_ALIASES = {
46205
46878
  // Common everyday names → the Natural-Earth display name actually shipped.
46206
46879
  "united states": "united states of america",
@@ -46278,17 +46951,305 @@ var init_resolver2 = __esm({
46278
46951
  }
46279
46952
  });
46280
46953
 
46954
+ // src/map/colorize.ts
46955
+ function assignColors(isos, adjacency) {
46956
+ const sorted = [...isos].sort();
46957
+ const byIso = /* @__PURE__ */ new Map();
46958
+ let maxIndex = -1;
46959
+ for (const iso of sorted) {
46960
+ const taken = /* @__PURE__ */ new Set();
46961
+ for (const n of adjacency.get(iso) ?? []) {
46962
+ const c = byIso.get(n);
46963
+ if (c !== void 0) taken.add(c);
46964
+ }
46965
+ let h = 0;
46966
+ while (taken.has(h)) h++;
46967
+ byIso.set(iso, h);
46968
+ if (h > maxIndex) maxIndex = h;
46969
+ }
46970
+ return { byIso, huesNeeded: maxIndex + 1 };
46971
+ }
46972
+ var init_colorize = __esm({
46973
+ "src/map/colorize.ts"() {
46974
+ "use strict";
46975
+ }
46976
+ });
46977
+
46978
+ // src/map/context-labels.ts
46979
+ function tierBand(maxSpanDeg) {
46980
+ if (maxSpanDeg >= 90) return "world";
46981
+ if (maxSpanDeg >= 20) return "continental";
46982
+ if (maxSpanDeg >= 5) return "regional";
46983
+ return "local";
46984
+ }
46985
+ function labelBudget(width, height, band) {
46986
+ const bandCap = {
46987
+ world: 6,
46988
+ continental: 5,
46989
+ regional: 4,
46990
+ local: 3
46991
+ };
46992
+ const area2 = Math.floor(Math.sqrt(Math.max(0, width * height)) / 150);
46993
+ return Math.max(0, Math.min(area2, bandCap[band]));
46994
+ }
46995
+ function waterEligible(tier, kind, band) {
46996
+ switch (band) {
46997
+ case "world":
46998
+ return tier <= 1 && (kind === "ocean" || kind === "sea");
46999
+ case "continental":
47000
+ return tier <= 2;
47001
+ case "regional":
47002
+ return tier <= 3;
47003
+ case "local":
47004
+ return tier <= 4;
47005
+ }
47006
+ }
47007
+ function insideViewport(p, width, height) {
47008
+ return !!p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && p[0] >= 0 && p[0] <= width && p[1] >= 0 && p[1] <= height;
47009
+ }
47010
+ function labelWidth(text, letterSpacing) {
47011
+ const spacing = letterSpacing > 0 ? Math.max(0, text.length - 1) * letterSpacing : 0;
47012
+ return measureLegendText(text, FONT) + spacing + 2 * PADX;
47013
+ }
47014
+ function wrapLabel2(text, letterSpacing) {
47015
+ const words = text.split(/\s+/).filter(Boolean);
47016
+ if (words.length <= 1) return [text];
47017
+ const maxLines = words.length >= 4 ? 3 : 2;
47018
+ const n = words.length;
47019
+ let best = null;
47020
+ for (let mask = 0; mask < 1 << n - 1; mask++) {
47021
+ const lines = [];
47022
+ let cur = [words[0]];
47023
+ for (let i = 1; i < n; i++) {
47024
+ if (mask & 1 << i - 1) {
47025
+ lines.push(cur.join(" "));
47026
+ cur = [words[i]];
47027
+ } else cur.push(words[i]);
47028
+ }
47029
+ lines.push(cur.join(" "));
47030
+ if (lines.length > maxLines) continue;
47031
+ const cost = Math.round(
47032
+ Math.max(...lines.map((l) => labelWidth(l, letterSpacing)))
47033
+ );
47034
+ const head = labelWidth(lines[0], letterSpacing);
47035
+ if (!best || cost < best.cost || cost === best.cost && lines.length < best.lines.length || cost === best.cost && lines.length === best.lines.length && head > best.head)
47036
+ best = { lines, cost, head };
47037
+ }
47038
+ return best?.lines ?? [text];
47039
+ }
47040
+ function rectAround(cx, cy, lines, letterSpacing) {
47041
+ const w = Math.max(...lines.map((l) => labelWidth(l, letterSpacing)));
47042
+ const h = (lines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY;
47043
+ return { x: cx - w / 2, y: cy - h / 2, w, h };
47044
+ }
47045
+ function rectFits(r, width, height) {
47046
+ return r.x >= 0 && r.y >= 0 && r.x + r.w <= width && r.y + r.h <= height;
47047
+ }
47048
+ function overlapsPadded(a, b, pad2) {
47049
+ 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;
47050
+ }
47051
+ function placeContextLabels(args) {
47052
+ const {
47053
+ projection,
47054
+ dLonSpan,
47055
+ dLatSpan,
47056
+ width,
47057
+ height,
47058
+ waterBodies,
47059
+ countries,
47060
+ palette,
47061
+ project,
47062
+ collides,
47063
+ overLand
47064
+ } = args;
47065
+ void projection;
47066
+ const band = tierBand(Math.max(dLonSpan, dLatSpan));
47067
+ const budget = labelBudget(width, height, band);
47068
+ if (budget <= 0) return [];
47069
+ const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
47070
+ const countryColor = palette.textMuted;
47071
+ const haloColor = palette.bg;
47072
+ const candidates = [];
47073
+ const center = [width / 2, height / 2];
47074
+ for (const e of waterBodies?.entries ?? []) {
47075
+ const [lat, lon, name, tier, kind, alt] = e;
47076
+ if (!waterEligible(tier, kind, band)) continue;
47077
+ const wlines = wrapLabel2(name, WATER_LETTER_SPACING);
47078
+ const anchorsLngLat = [[lon, lat]];
47079
+ for (const a of alt ?? []) anchorsLngLat.push([a[1], a[0]]);
47080
+ let best = null;
47081
+ let bestD = Infinity;
47082
+ let nearestProj = null;
47083
+ let nearestProjD = Infinity;
47084
+ for (const [aLon, aLat] of anchorsLngLat) {
47085
+ const p = project(aLon, aLat);
47086
+ if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
47087
+ const d = (p[0] - center[0]) ** 2 + (p[1] - center[1]) ** 2;
47088
+ if (d < nearestProjD) {
47089
+ nearestProjD = d;
47090
+ nearestProj = p;
47091
+ }
47092
+ if (!insideViewport(p, width, height)) continue;
47093
+ if (d < bestD) {
47094
+ bestD = d;
47095
+ best = p;
47096
+ }
47097
+ }
47098
+ if (!best && tier === 0 && nearestProj) {
47099
+ const overX = Math.max(0, -nearestProj[0], nearestProj[0] - width);
47100
+ const overY = Math.max(0, -nearestProj[1], nearestProj[1] - height);
47101
+ if (overX <= width * EDGE_CLAMP_OVERSHOOT && overY <= height * EDGE_CLAMP_OVERSHOOT) {
47102
+ const halfW = Math.max(...wlines.map((l) => labelWidth(l, WATER_LETTER_SPACING))) / 2;
47103
+ const halfH = ((wlines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY) / 2;
47104
+ const m = EDGE_CLAMP_MARGIN;
47105
+ best = [
47106
+ Math.min(Math.max(nearestProj[0], halfW + m), width - halfW - m),
47107
+ Math.min(Math.max(nearestProj[1], halfH + m), height - halfH - m)
47108
+ ];
47109
+ }
47110
+ }
47111
+ if (!best) continue;
47112
+ candidates.push({
47113
+ text: name,
47114
+ lines: wlines,
47115
+ cx: best[0],
47116
+ cy: best[1],
47117
+ italic: true,
47118
+ letterSpacing: WATER_LETTER_SPACING,
47119
+ color: waterColor,
47120
+ // Water before any country (×1000), then by tier, then kind, then name.
47121
+ sort: tier * 10 + KIND_ORDER[kind]
47122
+ });
47123
+ }
47124
+ const ranked = countries.map((c) => {
47125
+ const [x0, y0, x1, y1] = c.bbox;
47126
+ const w = x1 - x0;
47127
+ const h = y1 - y0;
47128
+ return { c, w, h, area: w * h };
47129
+ }).filter((r) => Number.isFinite(r.area) && r.area > 0).sort((a, b) => b.area - a.area);
47130
+ let ci = 0;
47131
+ for (const r of ranked) {
47132
+ const { c, w, h } = r;
47133
+ if (w > width * 0.66 || h > height * 0.66) continue;
47134
+ if (!insideViewport(c.anchor, width, height)) continue;
47135
+ const text = c.name;
47136
+ const tw = labelWidth(text, 0);
47137
+ if (tw > w || FONT + 2 * PADY > h) continue;
47138
+ candidates.push({
47139
+ text,
47140
+ lines: [text],
47141
+ cx: c.anchor[0],
47142
+ cy: c.anchor[1],
47143
+ italic: false,
47144
+ letterSpacing: 0,
47145
+ color: countryColor,
47146
+ // Always after every water body (+1e6); larger area = earlier.
47147
+ sort: 1e6 + ci++
47148
+ });
47149
+ }
47150
+ candidates.sort((a, b) => a.sort - b.sort);
47151
+ const placed = [];
47152
+ const placedRects = [];
47153
+ for (const cand of candidates) {
47154
+ if (placed.length >= budget) break;
47155
+ const rect = rectAround(cand.cx, cand.cy, cand.lines, cand.letterSpacing);
47156
+ if (!rectFits(rect, width, height)) continue;
47157
+ if (cand.italic && overLand) {
47158
+ const inset = 2;
47159
+ const top = cand.cy - (cand.lines.length - 1) / 2 * LINE_HEIGHT;
47160
+ const touchesLand = cand.lines.some((line12, li) => {
47161
+ const lw = labelWidth(line12, cand.letterSpacing);
47162
+ const x0 = cand.cx - lw / 2 + inset;
47163
+ const x1 = cand.cx + lw / 2 - inset;
47164
+ const xs = [x0, (x0 + cand.cx) / 2, cand.cx, (cand.cx + x1) / 2, x1];
47165
+ const base = top + li * LINE_HEIGHT;
47166
+ return [base, base - FONT * 0.4, base - FONT * 0.8].some(
47167
+ (y) => xs.some((x) => overLand(x, y))
47168
+ );
47169
+ });
47170
+ if (touchesLand) continue;
47171
+ }
47172
+ if (collides(rect)) continue;
47173
+ if (placedRects.some((r) => overlapsPadded(rect, r, CONTEXT_PAD))) continue;
47174
+ placedRects.push(rect);
47175
+ placed.push({
47176
+ x: cand.cx,
47177
+ y: cand.cy,
47178
+ text: cand.text,
47179
+ anchor: "middle",
47180
+ color: cand.color,
47181
+ // No halo: the bg-coloured outline reads as a ghost box behind the text
47182
+ // over the tinted water/land. Context labels are muted enough to sit
47183
+ // cleanly on the basemap without one.
47184
+ halo: false,
47185
+ haloColor,
47186
+ italic: cand.italic,
47187
+ letterSpacing: cand.letterSpacing,
47188
+ ...cand.lines.length > 1 ? { lines: cand.lines } : {},
47189
+ lineNumber: 0
47190
+ });
47191
+ }
47192
+ return placed;
47193
+ }
47194
+ var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, KIND_ORDER;
47195
+ var init_context_labels = __esm({
47196
+ "src/map/context-labels.ts"() {
47197
+ "use strict";
47198
+ init_color_utils();
47199
+ init_legend_constants();
47200
+ FONT = 11;
47201
+ LINE_HEIGHT = FONT + 2;
47202
+ PADX = 4;
47203
+ PADY = 3;
47204
+ WATER_LETTER_SPACING = 1.5;
47205
+ CONTEXT_PAD = 4;
47206
+ EDGE_CLAMP_MARGIN = 8;
47207
+ EDGE_CLAMP_OVERSHOOT = 0.35;
47208
+ KIND_ORDER = {
47209
+ ocean: 0,
47210
+ sea: 1,
47211
+ gulf: 2,
47212
+ bay: 3,
47213
+ strait: 4,
47214
+ channel: 5,
47215
+ sound: 6
47216
+ };
47217
+ }
47218
+ });
47219
+
46281
47220
  // src/map/layout.ts
46282
47221
  function geomObject2(topo) {
46283
47222
  const key = Object.keys(topo.objects)[0];
46284
47223
  return topo.objects[key];
46285
47224
  }
47225
+ function mergeFeatures(a, b) {
47226
+ const polysOf = (f) => {
47227
+ const g = f.geometry;
47228
+ if (!g) return null;
47229
+ if (g.type === "Polygon") return [g.coordinates];
47230
+ if (g.type === "MultiPolygon") return g.coordinates;
47231
+ return null;
47232
+ };
47233
+ const pa = polysOf(a);
47234
+ const pb = polysOf(b);
47235
+ if (!pa || !pb) return a;
47236
+ return {
47237
+ ...a,
47238
+ geometry: { type: "MultiPolygon", coordinates: [...pa, ...pb] }
47239
+ };
47240
+ }
46286
47241
  function decodeLayer(topo) {
47242
+ const cached = decodeCache.get(topo);
47243
+ if (cached) return cached;
46287
47244
  const out = /* @__PURE__ */ new Map();
46288
47245
  for (const g of geomObject2(topo).geometries) {
46289
47246
  const f = (0, import_topojson_client2.feature)(topo, g);
46290
- out.set(g.id, { ...f, id: g.id });
47247
+ if (!f.geometry) continue;
47248
+ const tagged = { ...f, id: g.id };
47249
+ const existing = out.get(g.id);
47250
+ out.set(g.id, existing ? mergeFeatures(existing, tagged) : tagged);
46291
47251
  }
47252
+ decodeCache.set(topo, out);
46292
47253
  return out;
46293
47254
  }
46294
47255
  function projectionFor(family) {
@@ -46297,9 +47258,12 @@ function projectionFor(family) {
46297
47258
  return usConusProjection();
46298
47259
  case "mercator":
46299
47260
  return (0, import_d3_geo2.geoMercator)();
47261
+ case "equal-earth":
47262
+ return (0, import_d3_geo2.geoEqualEarth)();
47263
+ case "equirectangular":
47264
+ return (0, import_d3_geo2.geoEquirectangular)();
46300
47265
  case "natural-earth":
46301
47266
  return (0, import_d3_geo2.geoNaturalEarth1)();
46302
- case "equirectangular":
46303
47267
  default:
46304
47268
  return (0, import_d3_geo2.geoEquirectangular)();
46305
47269
  }
@@ -46318,13 +47282,11 @@ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46318
47282
  isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT
46319
47283
  );
46320
47284
  }
46321
- function layoutMap(resolved, data, size, opts) {
46322
- const { palette, isDark } = opts;
46323
- const { width, height } = size;
47285
+ function buildMapProjection(resolved, data) {
46324
47286
  const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
46325
- const usCrisp = resolved.projection === "albers-usa" && wantsUsStates && !!data.naLand;
47287
+ const usCrisp = (resolved.projection === "albers-usa" || resolved.projection === "mercator") && wantsUsStates && !!data.naLand;
46326
47288
  const worldTopo = usCrisp ? data.worldDetail : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46327
- const worldLayer = decodeLayer(worldTopo);
47289
+ const worldLayer = new Map(decodeLayer(worldTopo));
46328
47290
  if (usCrisp && data.naLand) {
46329
47291
  const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
46330
47292
  const crisp = decodeLayer(data.naLand);
@@ -46333,16 +47295,141 @@ function layoutMap(resolved, data, size, opts) {
46333
47295
  if (!base) continue;
46334
47296
  const [[bw, bs], [be, bn]] = (0, import_d3_geo2.geoBounds)(base);
46335
47297
  if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
46336
- worldLayer.set(iso, cf);
47298
+ worldLayer.set(iso, { ...cf, properties: base.properties });
46337
47299
  }
46338
47300
  }
46339
47301
  const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
47302
+ const extentOutline = () => {
47303
+ const [[w, s], [e, n]] = resolved.extent;
47304
+ const N = 16;
47305
+ const coords = [];
47306
+ for (let i = 0; i <= N; i++) {
47307
+ const t = i / N;
47308
+ const lon = w + (e - w) * t;
47309
+ const lat = s + (n - s) * t;
47310
+ coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
47311
+ }
47312
+ return {
47313
+ type: "Feature",
47314
+ properties: {},
47315
+ geometry: { type: "MultiPoint", coordinates: coords }
47316
+ };
47317
+ };
47318
+ let fitFeatures;
47319
+ if (resolved.projection === "albers-usa" && usLayer) {
47320
+ fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
47321
+ const neighborPoints = resolved.pois.filter((p) => !inAlaska(p.lon, p.lat) && !inHawaii(p.lon, p.lat)).map((p) => [p.lon, p.lat]);
47322
+ if (neighborPoints.length > 0) {
47323
+ fitFeatures.push({
47324
+ type: "Feature",
47325
+ properties: {},
47326
+ geometry: { type: "MultiPoint", coordinates: neighborPoints }
47327
+ });
47328
+ }
47329
+ for (const r of resolved.regions) {
47330
+ if (r.layer === "country" && (r.iso === "CA" || r.iso === "MX")) {
47331
+ const cf = worldLayer.get(r.iso);
47332
+ if (cf) fitFeatures.push(cf);
47333
+ }
47334
+ }
47335
+ } else {
47336
+ fitFeatures = [extentOutline()];
47337
+ }
47338
+ const fitTarget = { type: "FeatureCollection", features: fitFeatures };
47339
+ const projection = projectionFor(resolved.projection);
47340
+ if (resolved.projection !== "albers-usa") {
47341
+ let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
47342
+ if (centerLon > 180) centerLon -= 360;
47343
+ projection.rotate([-centerLon, 0]);
47344
+ }
47345
+ const fitGB = (0, import_d3_geo2.geoBounds)(fitTarget);
47346
+ const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
47347
+ return {
47348
+ projection,
47349
+ fitTarget,
47350
+ fitIsGlobal,
47351
+ worldLayer,
47352
+ usLayer,
47353
+ usCrisp,
47354
+ wantsUsStates,
47355
+ worldTopo
47356
+ };
47357
+ }
47358
+ function parsePathRings(d) {
47359
+ const rings = [];
47360
+ let cur = [];
47361
+ const re = /([MLZ])([^MLZ]*)/g;
47362
+ let m;
47363
+ while (m = re.exec(d)) {
47364
+ if (m[1] === "Z") {
47365
+ if (cur.length) rings.push(cur);
47366
+ cur = [];
47367
+ continue;
47368
+ }
47369
+ if (m[1] === "M" && cur.length) {
47370
+ rings.push(cur);
47371
+ cur = [];
47372
+ }
47373
+ const nums = m[2].split(/[ ,]+/).map(Number);
47374
+ for (let i = 0; i + 1 < nums.length; i += 2) {
47375
+ const x = nums[i];
47376
+ const y = nums[i + 1];
47377
+ if (Number.isFinite(x) && Number.isFinite(y)) cur.push([x, y]);
47378
+ }
47379
+ }
47380
+ if (cur.length) rings.push(cur);
47381
+ return rings;
47382
+ }
47383
+ function dropAntimeridianWrapSlivers(d, width, height) {
47384
+ const rings = parsePathRings(d);
47385
+ if (rings.length <= 1) return d;
47386
+ const eps = 0.75;
47387
+ const minArea = 3e-3 * width * height;
47388
+ const ringArea = (r) => {
47389
+ let s = 0;
47390
+ for (let i = 0; i < r.length; i++) {
47391
+ const a = r[i];
47392
+ const b = r[(i + 1) % r.length];
47393
+ s += a[0] * b[1] - b[0] * a[1];
47394
+ }
47395
+ return Math.abs(s) / 2;
47396
+ };
47397
+ const areas = rings.map(ringArea);
47398
+ const maxArea = Math.max(...areas);
47399
+ 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;
47400
+ let dropped = false;
47401
+ const kept = rings.filter((r, idx) => {
47402
+ if (areas[idx] >= maxArea || areas[idx] >= minArea) return true;
47403
+ const touches = r.some((p, i) => onVEdge(p, r[(i + 1) % r.length]));
47404
+ if (touches) {
47405
+ dropped = true;
47406
+ return false;
47407
+ }
47408
+ return true;
47409
+ });
47410
+ if (!dropped) return d;
47411
+ return kept.map(
47412
+ (r) => r.map((p, i) => (i ? "L" : "M") + p[0] + "," + p[1]).join("") + "Z"
47413
+ ).join("");
47414
+ }
47415
+ function layoutMap(resolved, data, size, opts) {
47416
+ const { palette, isDark } = opts;
47417
+ const { width, height } = size;
47418
+ const {
47419
+ projection,
47420
+ fitTarget,
47421
+ fitIsGlobal,
47422
+ worldLayer,
47423
+ usLayer,
47424
+ usCrisp,
47425
+ worldTopo
47426
+ } = buildMapProjection(resolved, data);
46340
47427
  const usContext = usLayer !== null;
46341
47428
  const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46342
47429
  const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
46343
- const scaleOverride = resolved.directives.scale;
46344
- const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46345
- const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
47430
+ const allNonNegative = values.length > 0 && values.every((v) => v >= 0);
47431
+ const rampMin = allNonNegative ? 0 : Math.min(...values);
47432
+ const rampMax = Math.max(...values);
46346
47433
  const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46347
47434
  const hasRamp = values.length > 0;
46348
47435
  const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
@@ -46363,7 +47450,7 @@ function layoutMap(resolved, data, size, opts) {
46363
47450
  activeGroup = VALUE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46364
47451
  }
46365
47452
  const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
46366
- const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
47453
+ const mutedBasemap = activeGroup !== null;
46367
47454
  const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46368
47455
  const water = mapBackgroundColor(palette, isDark, mutedBasemap);
46369
47456
  const lakeStroke = mix(regionStroke, water, 45);
@@ -46372,10 +47459,43 @@ function layoutMap(resolved, data, size, opts) {
46372
47459
  palette.bg,
46373
47460
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46374
47461
  );
47462
+ const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && resolved.tagGroups.length === 0;
47463
+ const colorByIso = /* @__PURE__ */ new Map();
47464
+ if (colorizeActive) {
47465
+ const adjacency = /* @__PURE__ */ new Map();
47466
+ const addEdges = (src) => {
47467
+ for (const [iso, ns] of src) {
47468
+ const cur = adjacency.get(iso);
47469
+ if (cur) cur.push(...ns);
47470
+ else adjacency.set(iso, [...ns]);
47471
+ }
47472
+ };
47473
+ addEdges(buildAdjacency(worldTopo));
47474
+ if (usLayer) {
47475
+ addEdges(buildAdjacency(data.usStates));
47476
+ for (const [country, states] of Object.entries(FOREIGN_BORDER)) {
47477
+ const cn = adjacency.get(country);
47478
+ if (!cn) continue;
47479
+ for (const st of states) {
47480
+ const sn = adjacency.get(st);
47481
+ if (!sn) continue;
47482
+ cn.push(st);
47483
+ sn.push(country);
47484
+ }
47485
+ }
47486
+ }
47487
+ const { byIso, huesNeeded } = assignColors(
47488
+ [...adjacency.keys()],
47489
+ adjacency
47490
+ );
47491
+ const tints = politicalTints(palette, huesNeeded, isDark);
47492
+ for (const [iso, idx] of byIso) colorByIso.set(iso, tints[idx]);
47493
+ }
47494
+ const colorizeStroke = (fill2) => mix(fill2, palette.text, 35);
46375
47495
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46376
47496
  const fillForValue = (s) => {
46377
47497
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
46378
- const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
47498
+ const pct = RAMP_FLOOR2 + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR2);
46379
47499
  return mix(rampHue, rampBase, pct);
46380
47500
  };
46381
47501
  const tagFill = (tags, groupName) => {
@@ -46407,43 +47527,15 @@ function layoutMap(resolved, data, size, opts) {
46407
47527
  if (activeIsScore) {
46408
47528
  return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46409
47529
  }
47530
+ if (colorizeActive) return (r.iso && colorByIso.get(r.iso)) ?? neutralFill;
46410
47531
  return tagFill(r.tags, activeGroup) ?? neutralFill;
46411
47532
  };
46412
47533
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
46413
- const extentOutline = () => {
46414
- const [[w, s], [e, n]] = resolved.extent;
46415
- const N = 16;
46416
- const coords = [];
46417
- for (let i = 0; i <= N; i++) {
46418
- const t = i / N;
46419
- const lon = w + (e - w) * t;
46420
- const lat = s + (n - s) * t;
46421
- coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
46422
- }
46423
- return {
46424
- type: "Feature",
46425
- properties: {},
46426
- geometry: { type: "MultiPoint", coordinates: coords }
46427
- };
46428
- };
46429
- let fitFeatures;
46430
- if (resolved.projection === "albers-usa" && usLayer) {
46431
- fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
46432
- } else {
46433
- fitFeatures = [extentOutline()];
46434
- }
46435
- const fitTarget = { type: "FeatureCollection", features: fitFeatures };
46436
- const projection = projectionFor(resolved.projection);
46437
- if (resolved.projection !== "albers-usa") {
46438
- let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
46439
- if (centerLon > 180) centerLon -= 360;
46440
- projection.rotate([-centerLon, 0]);
46441
- }
46442
- const TITLE_GAP = 16;
47534
+ const TITLE_GAP2 = 16;
46443
47535
  let topPad = FIT_PAD;
46444
47536
  if (resolved.title && resolved.pois.length > 0) {
46445
47537
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
46446
- topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
47538
+ topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
46447
47539
  }
46448
47540
  const fitBox = [
46449
47541
  [FIT_PAD, topPad],
@@ -46453,21 +47545,20 @@ function layoutMap(resolved, data, size, opts) {
46453
47545
  ]
46454
47546
  ];
46455
47547
  projection.fitExtent(fitBox, fitTarget);
46456
- const fitGB = (0, import_d3_geo2.geoBounds)(fitTarget);
46457
- const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46458
47548
  let path;
46459
47549
  let project;
46460
47550
  let stretchParams = null;
46461
- if (fitIsGlobal) {
47551
+ if (fitIsGlobal && !opts.preferContain) {
46462
47552
  const cb = (0, import_d3_geo2.geoPath)(projection).bounds(fitTarget);
46463
47553
  const bx0 = cb[0][0];
46464
47554
  const by0 = cb[0][1];
46465
47555
  const cw = cb[1][0] - bx0;
46466
47556
  const ch = cb[1][1] - by0;
46467
- const ox = fitBox[0][0];
46468
- const oy = fitBox[0][1];
46469
- const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46470
- const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47557
+ const topReserve = resolved.title && resolved.pois.length > 0 ? topPad : 0;
47558
+ const ox = 0;
47559
+ const oy = topReserve;
47560
+ const sx = cw > 0 ? width / cw : 1;
47561
+ const sy = ch > 0 ? (height - topReserve) / ch : 1;
46471
47562
  stretchParams = { sx, sy, ox, oy, bx0, by0 };
46472
47563
  const stretch = (x, y) => [
46473
47564
  ox + (x - bx0) * sx,
@@ -46500,7 +47591,9 @@ function layoutMap(resolved, data, size, opts) {
46500
47591
  const insets = [];
46501
47592
  const insetRegions = [];
46502
47593
  const insetLabelSeeds = [];
46503
- if (resolved.projection === "albers-usa" && usLayer && !resolved.directives.noInsets) {
47594
+ const akRef = resolved.regions.some((r) => r.iso === "US-AK") || resolved.pois.some((p) => inAlaska(p.lon, p.lat));
47595
+ const hiRef = resolved.regions.some((r) => r.iso === "US-HI") || resolved.pois.some((p) => inHawaii(p.lon, p.lat));
47596
+ if (resolved.projection === "albers-usa" && usLayer && (akRef || hiRef)) {
46504
47597
  const PAD = 8;
46505
47598
  const GAP = 12;
46506
47599
  const yB = height - FIT_PAD;
@@ -46565,8 +47658,18 @@ function layoutMap(resolved, data, size, opts) {
46565
47658
  );
46566
47659
  const d = (0, import_d3_geo2.geoPath)(proj)(f) ?? "";
46567
47660
  if (!d) return xr;
47661
+ let contextLand;
47662
+ if (iso === "US-AK") {
47663
+ const can = worldLayer.get("CA");
47664
+ const cd = can ? (0, import_d3_geo2.geoPath)(proj)(can) ?? "" : "";
47665
+ if (cd)
47666
+ contextLand = {
47667
+ d: cd,
47668
+ fill: colorizeActive ? colorByIso.get("CA") ?? foreignFill : foreignFill
47669
+ };
47670
+ }
46568
47671
  const r = regionById.get(iso);
46569
- let fill2 = neutralFill;
47672
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? neutralFill : neutralFill;
46570
47673
  let lineNumber = -1;
46571
47674
  if (r?.layer === "us-state") {
46572
47675
  fill2 = regionFill(r);
@@ -46585,13 +47688,14 @@ function layoutMap(resolved, data, size, opts) {
46585
47688
  ],
46586
47689
  // The FITTED inset projection (just fit to this box) — captured so the
46587
47690
  // geo-query can invert pixels inside the frame back to AK/HI coords.
46588
- projection: proj
47691
+ projection: proj,
47692
+ ...contextLand && { contextLand }
46589
47693
  });
46590
47694
  insetRegions.push({
46591
47695
  id: iso,
46592
47696
  d,
46593
47697
  fill: fill2,
46594
- stroke: regionStroke,
47698
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46595
47699
  lineNumber,
46596
47700
  layer: "us-state",
46597
47701
  ...r?.value !== void 0 && { value: r.value },
@@ -46604,13 +47708,16 @@ function layoutMap(resolved, data, size, opts) {
46604
47708
  }
46605
47709
  return xr;
46606
47710
  };
46607
- const akRight = placeInset(
46608
- "US-AK",
46609
- alaskaProjection(),
46610
- FIT_PAD,
46611
- width * 0.15
46612
- );
46613
- placeInset("US-HI", hawaiiProjection(), akRight + 24, width * 0.1);
47711
+ let akRight = FIT_PAD;
47712
+ if (akRef)
47713
+ akRight = placeInset("US-AK", alaskaProjection(), FIT_PAD, width * 0.15);
47714
+ if (hiRef)
47715
+ placeInset(
47716
+ "US-HI",
47717
+ hawaiiProjection(),
47718
+ akRef ? akRight + 24 : FIT_PAD,
47719
+ width * 0.1
47720
+ );
46614
47721
  }
46615
47722
  const conusFit = resolved.projection === "albers-usa" && !!usLayer;
46616
47723
  const classifyExtent = conusFit ? (0, import_d3_geo2.geoBounds)(fitTarget) : resolved.extent;
@@ -46626,15 +47733,24 @@ function layoutMap(resolved, data, size, opts) {
46626
47733
  };
46627
47734
  const ringOverlapsView = (ring) => {
46628
47735
  let loMin = Infinity, loMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
47736
+ const lons = [];
46629
47737
  for (const [rawLon] of ring) {
46630
47738
  const lon = normLon(rawLon);
47739
+ lons.push(lon);
46631
47740
  if (lon < loMin) loMin = lon;
46632
47741
  if (lon > loMax) loMax = lon;
46633
47742
  if (rawLon < rawMin) rawMin = rawLon;
46634
47743
  if (rawLon > rawMax) rawMax = rawLon;
46635
47744
  }
46636
- if (loMax - loMin > 270) return false;
46637
- if (rawMax - rawMin > 180 && loMax - loMin < 90) return false;
47745
+ lons.sort((a, b) => a - b);
47746
+ let maxGap = 0;
47747
+ for (let i = 1; i < lons.length; i++)
47748
+ maxGap = Math.max(maxGap, lons[i] - lons[i - 1]);
47749
+ if (lons.length > 1)
47750
+ maxGap = Math.max(maxGap, lons[0] + 360 - lons[lons.length - 1]);
47751
+ const occupiedArc = 360 - maxGap;
47752
+ if (occupiedArc > 270) return false;
47753
+ if (rawMax - rawMin > 180 && occupiedArc < 90) return false;
46638
47754
  let px0 = Infinity, py0 = Infinity, px1 = -Infinity, py1 = -Infinity, anyFinite = false;
46639
47755
  for (const [lon, lat] of ring) {
46640
47756
  const p = project(lon, lat);
@@ -46707,7 +47823,7 @@ function layoutMap(resolved, data, size, opts) {
46707
47823
  const regions = [];
46708
47824
  const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
46709
47825
  for (const [iso, f] of layerFeatures) {
46710
- if (layerKind === "us-state" && usContext && INSET_STATES.has(iso))
47826
+ if (layerKind === "us-state" && usContext && resolved.projection === "albers-usa" && INSET_STATES.has(iso))
46711
47827
  continue;
46712
47828
  if (layerKind === "country" && usContext && iso === "US") continue;
46713
47829
  if (layerKind === "country" && iso === "AQ" && !regionById.has("AQ"))
@@ -46715,11 +47831,13 @@ function layoutMap(resolved, data, size, opts) {
46715
47831
  const r = regionById.get(iso);
46716
47832
  const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
46717
47833
  if (!viewF) continue;
46718
- const d = path(viewF) ?? "";
47834
+ const raw = path(viewF) ?? "";
47835
+ const d = fitIsGlobal ? dropAntimeridianWrapSlivers(raw, width, height) : raw;
46719
47836
  if (!d) continue;
46720
47837
  const isThisLayer = r?.layer === layerKind;
46721
47838
  const isForeign = layerKind === "country" && usContext && iso !== "US";
46722
- let fill2 = isForeign ? foreignFill : neutralFill;
47839
+ const baseFill = isForeign ? foreignFill : neutralFill;
47840
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? baseFill : baseFill;
46723
47841
  let label;
46724
47842
  let lineNumber = -1;
46725
47843
  let layer = "base";
@@ -46728,15 +47846,21 @@ function layoutMap(resolved, data, size, opts) {
46728
47846
  lineNumber = r.lineNumber;
46729
47847
  layer = layerKind;
46730
47848
  label = r.name;
47849
+ } else {
47850
+ label = f.properties?.name;
46731
47851
  }
47852
+ const labelAnchor = WORLD_LABEL_ANCHORS[iso];
47853
+ const c = labelAnchor ? project(labelAnchor[0], labelAnchor[1]) : path.centroid(viewF);
47854
+ const hasCentroid = c != null && Number.isFinite(c[0]) && Number.isFinite(c[1]);
46732
47855
  regions.push({
46733
47856
  id: iso,
46734
47857
  d,
46735
47858
  fill: fill2,
46736
- stroke: regionStroke,
47859
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46737
47860
  lineNumber,
46738
47861
  layer,
46739
47862
  ...label !== void 0 && { label },
47863
+ ...hasCentroid && { labelX: c[0], labelY: c[1] },
46740
47864
  ...isThisLayer && r.value !== void 0 && { value: r.value },
46741
47865
  ...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
46742
47866
  });
@@ -46761,9 +47885,41 @@ function layoutMap(resolved, data, size, opts) {
46761
47885
  });
46762
47886
  }
46763
47887
  }
47888
+ const pointInRings = (px, py, rings) => {
47889
+ let inside = false;
47890
+ for (const ring of rings) {
47891
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
47892
+ const [xi, yi] = ring[i];
47893
+ const [xj, yj] = ring[j];
47894
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
47895
+ inside = !inside;
47896
+ }
47897
+ }
47898
+ return inside;
47899
+ };
47900
+ const fillHitTargets = [...regions, ...insetRegions].map((r) => ({
47901
+ fill: r.fill,
47902
+ rings: parsePathRings(r.d)
47903
+ }));
47904
+ const fillAt = (x, y) => {
47905
+ let hit = water;
47906
+ for (const t of fillHitTargets)
47907
+ if (pointInRings(x, y, t.rings)) hit = t.fill;
47908
+ return hit;
47909
+ };
47910
+ const labelOnFill = (fill2) => {
47911
+ const color = contrastRatio(fill2, palette.textOnFillDark) >= contrastRatio(fill2, palette.textOnFillLight) ? palette.textOnFillDark : palette.textOnFillLight;
47912
+ const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47913
+ return {
47914
+ color,
47915
+ halo: contrastRatio(fill2, color) < REGION_LABEL_HALO_RATIO,
47916
+ haloColor
47917
+ };
47918
+ };
47919
+ const reliefAllowed = resolved.directives.noRelief !== true;
46764
47920
  const relief = [];
46765
47921
  let reliefHatch = null;
46766
- if (resolved.directives.relief === true && data.mountainRanges) {
47922
+ if (reliefAllowed && data.mountainRanges) {
46767
47923
  for (const [, f] of decodeLayer(data.mountainRanges)) {
46768
47924
  const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
46769
47925
  if (!viewF) continue;
@@ -46779,16 +47935,32 @@ function layoutMap(resolved, data, size, opts) {
46779
47935
  if (relief.length) {
46780
47936
  const darkTone = isDark ? palette.bg : palette.text;
46781
47937
  const lightTone = isDark ? palette.text : palette.bg;
46782
- const landLum = relativeLuminance(neutralFill);
47938
+ const reliefLandRef = colorizeActive ? isDark ? palette.surface : palette.bg : neutralFill;
47939
+ const landLum = relativeLuminance(reliefLandRef);
46783
47940
  const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
46784
47941
  reliefHatch = {
46785
- color: mix(tone, neutralFill, RELIEF_HATCH_STRENGTH),
47942
+ color: mix(tone, reliefLandRef, RELIEF_HATCH_STRENGTH),
46786
47943
  spacing: RELIEF_HATCH_SPACING,
46787
47944
  width: RELIEF_HATCH_WIDTH
46788
47945
  };
46789
47946
  }
46790
47947
  }
46791
- const riverColor = mix(water, regionStroke, 16);
47948
+ let coastlineStyle = null;
47949
+ if (resolved.directives.noCoastline !== true) {
47950
+ const minDim = Math.min(width, height);
47951
+ coastlineStyle = {
47952
+ color: mix(regionStroke, water, COASTLINE_STROKE_MIX),
47953
+ // N equal-width rings: distance steps outward by COASTLINE_STEP; opacity
47954
+ // fades linearly from NEAR (innermost) to FAR (outermost).
47955
+ lines: Array.from({ length: COASTLINE_RING_COUNT }, (_, k) => ({
47956
+ d: (COASTLINE_D0 + k * COASTLINE_STEP) * minDim,
47957
+ thickness: COASTLINE_THICKNESS * minDim,
47958
+ opacity: COASTLINE_OPACITY_NEAR + (COASTLINE_OPACITY_FAR - COASTLINE_OPACITY_NEAR) * k / (COASTLINE_RING_COUNT - 1)
47959
+ })),
47960
+ minExtent: (isGlobalView ? COASTLINE_MIN_EXTENT_GLOBAL : COASTLINE_MIN_EXTENT) * minDim
47961
+ };
47962
+ }
47963
+ const riverColor = mix(palette.colors.blue, water, 32);
46792
47964
  const rivers = [];
46793
47965
  if (data.rivers) {
46794
47966
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -46844,38 +48016,108 @@ function layoutMap(resolved, data, size, opts) {
46844
48016
  const xy = project(p.lon, p.lat);
46845
48017
  if (xy) projected.push({ p, xy });
46846
48018
  }
46847
- const coloGroups = /* @__PURE__ */ new Map();
48019
+ const placePoi = (e, cx, cy, clusterId) => {
48020
+ const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
48021
+ poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
48022
+ const num = routeNumberById.get(e.p.id);
48023
+ pois.push({
48024
+ id: e.p.id,
48025
+ cx,
48026
+ cy,
48027
+ r: radiusFor(e.p),
48028
+ fill: fill2,
48029
+ stroke: stroke2,
48030
+ lineNumber: e.p.lineNumber,
48031
+ implicit: !!e.p.implicit,
48032
+ isOrigin: originIds.has(e.p.id),
48033
+ ...num !== void 0 && { routeNumber: num },
48034
+ ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags },
48035
+ ...clusterId !== void 0 && { clusterId }
48036
+ });
48037
+ };
48038
+ const clusters = [];
48039
+ const connected = /* @__PURE__ */ new Set();
48040
+ for (const e of resolved.edges) {
48041
+ connected.add(e.fromId);
48042
+ connected.add(e.toId);
48043
+ }
48044
+ for (const rt of resolved.routes) {
48045
+ rt.stopIds.forEach((id) => connected.add(id));
48046
+ }
48047
+ const radiusOf = (e) => radiusFor(e.p);
46848
48048
  for (const e of projected) {
46849
- const key = `${Math.round(e.xy[0] / COLO_EPS)},${Math.round(e.xy[1] / COLO_EPS)}`;
46850
- const arr = coloGroups.get(key);
46851
- if (arr) arr.push(e);
46852
- else coloGroups.set(key, [e]);
46853
- }
46854
- for (const group of coloGroups.values()) {
46855
- group.forEach((e, i) => {
46856
- let cx = e.xy[0];
46857
- let cy = e.xy[1];
46858
- if (group.length > 1) {
46859
- const ang = i * GOLDEN_ANGLE;
46860
- cx += Math.cos(ang) * COLO_R;
46861
- cy += Math.sin(ang) * COLO_R;
46862
- }
46863
- const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
46864
- poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
46865
- const num = routeNumberById.get(e.p.id);
46866
- pois.push({
46867
- id: e.p.id,
46868
- cx,
46869
- cy,
46870
- r: radiusFor(e.p),
46871
- fill: fill2,
46872
- stroke: stroke2,
46873
- lineNumber: e.p.lineNumber,
46874
- implicit: !!e.p.implicit,
46875
- isOrigin: originIds.has(e.p.id),
46876
- ...num !== void 0 && { routeNumber: num },
46877
- ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags }
46878
- });
48049
+ if (connected.has(e.p.id)) placePoi(e, e.xy[0], e.xy[1]);
48050
+ }
48051
+ const groups = [];
48052
+ for (const e of projected) {
48053
+ if (connected.has(e.p.id)) continue;
48054
+ const r = radiusOf(e);
48055
+ const near = groups.find(
48056
+ (g) => g.some(
48057
+ (q) => Math.hypot(q.xy[0] - e.xy[0], q.xy[1] - e.xy[1]) < (r + radiusOf(q)) * STACK_OVERLAP
48058
+ )
48059
+ );
48060
+ if (near) near.push(e);
48061
+ else groups.push([e]);
48062
+ }
48063
+ for (const g of groups) {
48064
+ if (g.length === 1) {
48065
+ placePoi(g[0], g[0].xy[0], g[0].xy[1]);
48066
+ continue;
48067
+ }
48068
+ const clusterId = g[0].p.id;
48069
+ const cx0 = g.reduce((s, e) => s + e.xy[0], 0) / g.length;
48070
+ const cy0 = g.reduce((s, e) => s + e.xy[1], 0) / g.length;
48071
+ const maxR = Math.max(...g.map(radiusOf));
48072
+ const sep = 2 * maxR + STACK_RING_GAP;
48073
+ const ringR = Math.max(
48074
+ COLO_R,
48075
+ sep / (2 * Math.sin(Math.PI / Math.max(g.length, 2)))
48076
+ );
48077
+ const positions = g.map((e, i) => {
48078
+ if (g.length <= STACK_RING_MAX) {
48079
+ const ang2 = -Math.PI / 2 + i * 2 * Math.PI / g.length;
48080
+ return {
48081
+ e,
48082
+ mx: cx0 + Math.cos(ang2) * ringR,
48083
+ my: cy0 + Math.sin(ang2) * ringR
48084
+ };
48085
+ }
48086
+ const ang = i * GOLDEN_ANGLE;
48087
+ const rr = ringR * Math.sqrt((i + 1) / g.length);
48088
+ return { e, mx: cx0 + Math.cos(ang) * rr, my: cy0 + Math.sin(ang) * rr };
48089
+ });
48090
+ let minX = cx0 - maxR;
48091
+ let maxX = cx0 + maxR;
48092
+ let minY = cy0 - maxR;
48093
+ let maxY = cy0 + maxR;
48094
+ for (const { mx, my, e } of positions) {
48095
+ const r = radiusOf(e);
48096
+ minX = Math.min(minX, mx - r);
48097
+ maxX = Math.max(maxX, mx + r);
48098
+ minY = Math.min(minY, my - r);
48099
+ maxY = Math.max(maxY, my + r);
48100
+ }
48101
+ let dx = 0;
48102
+ let dy = 0;
48103
+ if (minX + dx < 2) dx = 2 - minX;
48104
+ if (maxX + dx > width - 2) dx = width - 2 - maxX;
48105
+ if (minY + dy < 2) dy = 2 - minY;
48106
+ if (maxY + dy > height - 2) dy = height - 2 - maxY;
48107
+ const legsOut = [];
48108
+ for (const { e, mx, my } of positions) {
48109
+ const fx = mx + dx;
48110
+ const fy = my + dy;
48111
+ placePoi(e, fx, fy, clusterId);
48112
+ legsOut.push({ x2: fx, y2: fy, color: poiFill(e.p).fill });
48113
+ }
48114
+ clusters.push({
48115
+ id: clusterId,
48116
+ cx: cx0 + dx,
48117
+ cy: cy0 + dy,
48118
+ count: g.length,
48119
+ hitR: ringR + maxR + 6,
48120
+ legs: legsOut
46879
48121
  });
46880
48122
  }
46881
48123
  const legs = [];
@@ -46925,16 +48167,26 @@ function layoutMap(resolved, data, size, opts) {
46925
48167
  if (!a || !b) continue;
46926
48168
  const mx = (a.cx + b.cx) / 2;
46927
48169
  const my = (a.cy + b.cy) / 2;
48170
+ const bow = {
48171
+ curved: leg.style === "arc",
48172
+ offset: 0,
48173
+ labelX: mx,
48174
+ labelY: my - 4
48175
+ };
48176
+ const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46928
48177
  legs.push({
46929
- d: legPath(a, b, leg.style === "arc", 0),
48178
+ d: legPath(a, b, bow.curved, bow.offset),
46930
48179
  width: routeWidthFor(Number(leg.value)),
46931
48180
  color: mix(palette.text, palette.bg, 72),
46932
48181
  arrow: true,
46933
48182
  lineNumber: leg.lineNumber,
46934
48183
  ...leg.label !== void 0 && {
46935
48184
  label: leg.label,
46936
- labelX: mx,
46937
- labelY: my - 4
48185
+ labelX: bow.labelX,
48186
+ labelY: bow.labelY,
48187
+ labelColor: routeLabelStyle.color,
48188
+ labelHalo: routeLabelStyle.halo,
48189
+ labelHaloColor: routeLabelStyle.haloColor
46938
48190
  }
46939
48191
  });
46940
48192
  }
@@ -46962,20 +48214,29 @@ function layoutMap(resolved, data, size, opts) {
46962
48214
  const a = poiScreen.get(e.fromId);
46963
48215
  const b = poiScreen.get(e.toId);
46964
48216
  if (!a || !b) return;
46965
- const curved = e.style === "arc" || n > 1;
46966
- const offset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
48217
+ const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
46967
48218
  const mx = (a.cx + b.cx) / 2;
46968
48219
  const my = (a.cy + b.cy) / 2;
48220
+ const bow = {
48221
+ curved: e.style === "arc" || n > 1,
48222
+ offset: fanOffset,
48223
+ labelX: mx,
48224
+ labelY: my - 4
48225
+ };
48226
+ const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
46969
48227
  legs.push({
46970
- d: legPath(a, b, curved, offset),
48228
+ d: legPath(a, b, bow.curved, bow.offset),
46971
48229
  width: widthFor(e),
46972
48230
  color: mix(palette.text, palette.bg, 66),
46973
48231
  arrow: e.directed,
46974
48232
  lineNumber: e.lineNumber,
46975
48233
  ...e.label !== void 0 && {
46976
48234
  label: e.label,
46977
- labelX: mx,
46978
- labelY: my - 4
48235
+ labelX: bow.labelX,
48236
+ labelY: bow.labelY,
48237
+ labelColor: edgeLabelStyle.color,
48238
+ labelHalo: edgeLabelStyle.halo,
48239
+ labelHaloColor: edgeLabelStyle.haloColor
46979
48240
  }
46980
48241
  });
46981
48242
  });
@@ -47017,48 +48278,73 @@ function layoutMap(resolved, data, size, opts) {
47017
48278
  }
47018
48279
  }
47019
48280
  const collides = (rect) => markers.some((m) => rectCircleOverlap(rect, m)) || obstacles.some((o) => rectsOverlap(rect, o)) || legSegments.some((s) => segmentRectOverlap(s[0], s[1], s[2], s[3], rect));
47020
- const regionLabelMode = resolved.directives.regionLabels ?? "off";
48281
+ const showRegionLabels = resolved.directives.noRegionLabels !== true;
48282
+ const isCompact = width < COMPACT_WIDTH_PX;
47021
48283
  const LABEL_PADX = 6;
47022
48284
  const LABEL_PADY = 3;
47023
- const labelW = (text) => measureLegendText(text, FONT) + 2 * LABEL_PADX;
47024
- const labelH = FONT + 2 * LABEL_PADY;
48285
+ const labelW = (text) => measureLegendText(text, FONT2) + 2 * LABEL_PADX;
48286
+ const labelH = FONT2 + 2 * LABEL_PADY;
47025
48287
  const pushRegionLabel = (x, y, text, fill2, lineNumber) => {
47026
- const color = contrastText(
47027
- fill2,
47028
- palette.textOnFillLight,
47029
- palette.textOnFillDark
48288
+ const { color, haloColor } = labelOnFill(fill2);
48289
+ const halfW = measureLegendText(text, FONT2) / 2;
48290
+ const overflows = [y - FONT2 * 0.55, y - FONT2 * 0.1].some(
48291
+ (sy) => fillAt(x - halfW, sy) !== fill2 || fillAt(x + halfW, sy) !== fill2
47030
48292
  );
47031
- const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47032
48293
  labels.push({
47033
48294
  x,
47034
48295
  y,
47035
48296
  text,
47036
48297
  anchor: "middle",
47037
48298
  color,
47038
- halo: true,
48299
+ halo: overflows,
47039
48300
  haloColor,
47040
48301
  lineNumber
47041
48302
  });
47042
48303
  };
47043
- const WORLD_LABEL_ANCHORS = {
47044
- US: [-98.5, 39.5]
47045
- // CONUS geographic centre (near Lebanon, Kansas)
48304
+ const REGION_LABEL_GAP = 2;
48305
+ const regionLabelRect = (cx, cy, text) => {
48306
+ const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
48307
+ return { x: cx - w / 2, y: cy - FONT2 / 2, w, h: FONT2 };
47046
48308
  };
47047
- if (regionLabelMode === "full" || regionLabelMode === "abbrev") {
47048
- for (const r of regions) {
47049
- if (r.layer === "base" || r.label === void 0) continue;
47050
- const f = r.layer === "us-state" ? usLayer?.get(r.id) : worldLayer.get(r.id);
47051
- if (!f) continue;
48309
+ if (showRegionLabels) {
48310
+ const frameContainers = new Set(resolved.poiFrameContainers);
48311
+ const entries = regions.map((r) => {
48312
+ const isContainer = frameContainers.has(r.id);
48313
+ if (r.layer === "base" && !isContainer || r.label === void 0)
48314
+ return null;
48315
+ const isUsState = r.layer === "us-state" || r.id.startsWith("US-");
48316
+ const f = isUsState ? usLayer?.get(r.id) : worldLayer.get(r.id);
48317
+ if (!f) return null;
47052
48318
  const [[x0, y0], [x1, y1]] = path.bounds(f);
47053
- const text = regionLabelMode === "abbrev" ? r.id.replace(/^US-/, "") : r.label;
47054
- if (labelW(text) > x1 - x0 || labelH > y1 - y0) continue;
47055
- const anchor = r.layer !== "us-state" ? WORLD_LABEL_ANCHORS[r.id] : void 0;
48319
+ const boxW = x1 - x0;
48320
+ const boxH = y1 - y0;
48321
+ const abbrev = isUsState ? r.id.replace(/^US-/, "") : void 0;
48322
+ const candidates = abbrev !== void 0 ? isCompact ? [abbrev, r.label] : [r.label, abbrev] : [r.label];
48323
+ const anchor = !isUsState ? WORLD_LABEL_ANCHORS[r.id] : void 0;
47056
48324
  const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
47057
- if (!c || !Number.isFinite(c[0])) continue;
48325
+ if (!c || !Number.isFinite(c[0])) return null;
48326
+ return { r, c, boxW, boxH, area: boxW * boxH, candidates };
48327
+ }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
48328
+ const placedRegionRects = [];
48329
+ const POI_LABEL_PAD = 14;
48330
+ const poiObstacles = pois.map((p) => ({
48331
+ x: p.cx - p.r - POI_LABEL_PAD,
48332
+ y: p.cy - p.r - POI_LABEL_PAD,
48333
+ w: 2 * (p.r + POI_LABEL_PAD),
48334
+ h: 2 * (p.r + POI_LABEL_PAD)
48335
+ }));
48336
+ for (const { r, c, boxW, boxH, candidates } of entries) {
48337
+ const text = candidates.find((t) => {
48338
+ if (labelW(t) > boxW || labelH > boxH) return false;
48339
+ const rect = regionLabelRect(c[0], c[1], t);
48340
+ return !placedRegionRects.some((p) => rectsOverlap(rect, p)) && !poiObstacles.some((o) => rectsOverlap(rect, o));
48341
+ });
48342
+ if (text === void 0) continue;
48343
+ placedRegionRects.push(regionLabelRect(c[0], c[1], text));
47058
48344
  pushRegionLabel(c[0], c[1], text, r.fill, r.lineNumber);
47059
48345
  }
47060
48346
  for (const seed of insetLabelSeeds) {
47061
- const text = regionLabelMode === "abbrev" ? seed.iso.replace(/^US-/, "") : seed.name;
48347
+ const text = isCompact ? seed.iso.replace(/^US-/, "") : seed.name;
47062
48348
  const src = regionById.get(seed.iso);
47063
48349
  pushRegionLabel(
47064
48350
  seed.x,
@@ -47069,22 +48355,26 @@ function layoutMap(resolved, data, size, opts) {
47069
48355
  );
47070
48356
  }
47071
48357
  }
47072
- const poiLabelMode = resolved.directives.poiLabels ?? "auto";
47073
- if (poiLabelMode !== "off") {
47074
- const ordered = [...pois].sort(
47075
- (a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1)
47076
- );
48358
+ if (resolved.directives.noPoiLabels !== true) {
48359
+ const ordered = [...pois].filter((p) => p.clusterId === void 0).sort((a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1));
47077
48360
  const poiById = new Map(resolved.pois.map((q) => [q.id, q]));
47078
48361
  const labelText = (p) => {
47079
48362
  const src = poiById.get(p.id);
47080
48363
  return src?.label ?? src?.name ?? p.id;
47081
48364
  };
47082
- const poiLabH = FONT * 1.25;
48365
+ const poiLabH = FONT2 * 1.25;
47083
48366
  const labelInfo = (p) => {
47084
48367
  const text = labelText(p);
47085
- return { text, w: measureLegendText(text, FONT) };
48368
+ return { text, w: measureLegendText(text, FONT2) };
47086
48369
  };
47087
48370
  const GAP = 3;
48371
+ const clusterMembersById = /* @__PURE__ */ new Map();
48372
+ for (const p of pois) {
48373
+ if (p.clusterId === void 0) continue;
48374
+ const arr = clusterMembersById.get(p.clusterId);
48375
+ if (arr) arr.push(p);
48376
+ else clusterMembersById.set(p.clusterId, [p]);
48377
+ }
47088
48378
  const inlineRect = (p, w, side) => {
47089
48379
  switch (side) {
47090
48380
  case "right":
@@ -47114,11 +48404,11 @@ function layoutMap(resolved, data, size, opts) {
47114
48404
  const x = side === "right" ? rect.x : side === "left" ? rect.x + w : p.cx;
47115
48405
  labels.push({
47116
48406
  x,
47117
- y: rect.y + poiLabH / 2 + FONT / 3,
48407
+ y: rect.y + poiLabH / 2 + FONT2 / 3,
47118
48408
  text,
47119
48409
  anchor,
47120
48410
  color: palette.text,
47121
- halo: true,
48411
+ halo: false,
47122
48412
  haloColor: palette.bg,
47123
48413
  poiId: p.id,
47124
48414
  lineNumber: p.lineNumber
@@ -47129,43 +48419,60 @@ function layoutMap(resolved, data, size, opts) {
47129
48419
  return rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect);
47130
48420
  };
47131
48421
  const GROUP_R = 30;
47132
- const groups = [];
48422
+ const groups2 = [];
47133
48423
  for (const p of ordered) {
47134
- const near = groups.find(
48424
+ const near = groups2.find(
47135
48425
  (g) => g.some((q) => Math.hypot(q.cx - p.cx, q.cy - p.cy) < GROUP_R)
47136
48426
  );
47137
48427
  if (near) near.push(p);
47138
- else groups.push([p]);
48428
+ else groups2.push([p]);
47139
48429
  }
47140
48430
  const ROW_GAP2 = 3;
47141
48431
  const step = poiLabH + ROW_GAP2;
47142
48432
  const COL_GAP = 16;
47143
- const placeColumn = (group) => {
47144
- const items = group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48433
+ const makeItems = (group) => group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48434
+ const columnRows = (items, side) => {
47145
48435
  const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
47146
48436
  const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
47147
- const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
47148
48437
  const maxW = Math.max(...items.map((o) => o.w));
47149
- const side = right + COL_GAP + maxW <= width - 2 ? "right" : "left";
47150
- const colX = side === "right" ? right + COL_GAP : left - COL_GAP;
48438
+ const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
48439
+ const colX = side === "right" ? Math.min(right + COL_GAP, width - 2 - maxW) : Math.max(left - COL_GAP, 2 + maxW);
47151
48440
  const totalH = items.length * step;
47152
48441
  let startY = cyMid - totalH / 2;
47153
48442
  startY = Math.max(2, Math.min(startY, height - totalH - 2));
47154
- items.forEach((o, i) => {
48443
+ return items.map((o, i) => {
47155
48444
  const rowCy = startY + i * step + step / 2;
47156
- obstacles.push({
47157
- x: side === "right" ? colX : colX - o.w,
47158
- y: rowCy - poiLabH / 2,
47159
- w: o.w,
47160
- h: poiLabH
47161
- });
48445
+ return {
48446
+ o,
48447
+ colX,
48448
+ rowCy,
48449
+ rect: {
48450
+ x: side === "right" ? colX : colX - o.w,
48451
+ y: rowCy - poiLabH / 2,
48452
+ w: o.w,
48453
+ h: poiLabH
48454
+ }
48455
+ };
48456
+ });
48457
+ };
48458
+ const wouldColumnBeClean = (items, side) => columnRows(items, side).every(
48459
+ ({ rect }) => rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect)
48460
+ );
48461
+ const defaultColumnSide = (items) => {
48462
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48463
+ const maxW = Math.max(...items.map((o) => o.w));
48464
+ return right + COL_GAP + maxW <= width - 2 ? "right" : "left";
48465
+ };
48466
+ const commitColumn = (items, side, clusterId) => {
48467
+ for (const { o, colX, rowCy, rect } of columnRows(items, side)) {
48468
+ obstacles.push(rect);
47162
48469
  labels.push({
47163
48470
  x: colX,
47164
- y: rowCy + FONT / 3,
48471
+ y: rowCy + FONT2 / 3,
47165
48472
  text: o.text,
47166
48473
  anchor: side === "right" ? "start" : "end",
47167
48474
  color: palette.text,
47168
- halo: true,
48475
+ halo: false,
47169
48476
  haloColor: palette.bg,
47170
48477
  leader: {
47171
48478
  x1: o.p.cx,
@@ -47175,24 +48482,141 @@ function layoutMap(resolved, data, size, opts) {
47175
48482
  },
47176
48483
  leaderColor: o.p.fill,
47177
48484
  poiId: o.p.id,
47178
- lineNumber: o.p.lineNumber
48485
+ lineNumber: o.p.lineNumber,
48486
+ ...clusterId !== void 0 && { clusterMember: clusterId }
47179
48487
  });
48488
+ }
48489
+ };
48490
+ const pushHidden = (p) => {
48491
+ const { text, w } = labelInfo(p);
48492
+ let x = p.cx + p.r + GAP;
48493
+ let anchor = "start";
48494
+ if (x + w > width) {
48495
+ x = p.cx - p.r - GAP - w;
48496
+ anchor = "end";
48497
+ }
48498
+ const y = Math.max(0, Math.min(p.cy - poiLabH / 2, height - poiLabH));
48499
+ labels.push({
48500
+ x: anchor === "start" ? x : x + w,
48501
+ y: y + poiLabH / 2 + FONT2 / 3,
48502
+ text,
48503
+ anchor,
48504
+ color: palette.text,
48505
+ halo: false,
48506
+ haloColor: palette.bg,
48507
+ poiId: p.id,
48508
+ hidden: true,
48509
+ lineNumber: p.lineNumber
47180
48510
  });
47181
48511
  };
47182
- for (const g of groups) {
48512
+ for (const [clusterId, members] of clusterMembersById) {
48513
+ if (members.length === 0) continue;
48514
+ const items = makeItems(members);
48515
+ const side = wouldColumnBeClean(items, "right") ? "right" : wouldColumnBeClean(items, "left") ? "left" : defaultColumnSide(items);
48516
+ commitColumn(items, side, clusterId);
48517
+ }
48518
+ const maxExtent = MAX_CLUSTER_EXTENT_FACTOR * Math.min(width, height);
48519
+ const clusterPending = [];
48520
+ for (const g of groups2) {
48521
+ const items = makeItems(g);
47183
48522
  if (g.length === 1) {
47184
- const p = g[0];
47185
- const { text, w } = labelInfo(p);
48523
+ const { p, text, w } = items[0];
47186
48524
  const side = ["right", "left", "above", "below"].find(
47187
48525
  (s) => inlineFits(p, w, s)
47188
48526
  );
47189
- if (side) {
47190
- pushInline(p, text, w, side);
47191
- continue;
48527
+ if (side) pushInline(p, text, w, side);
48528
+ else commitColumn(items, defaultColumnSide(items));
48529
+ continue;
48530
+ }
48531
+ const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
48532
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48533
+ const minCy = Math.min(...items.map((o) => o.p.cy));
48534
+ const maxCy = Math.max(...items.map((o) => o.p.cy));
48535
+ const diag = Math.hypot(right - left, maxCy - minCy);
48536
+ if (diag > maxExtent || items.length > MAX_COLUMN_ROWS) {
48537
+ items.forEach((o) => pushHidden(o.p));
48538
+ } else {
48539
+ clusterPending.push(items);
48540
+ }
48541
+ }
48542
+ for (const items of clusterPending) {
48543
+ const side = ["right", "left"].find(
48544
+ (s) => wouldColumnBeClean(items, s)
48545
+ );
48546
+ if (side) commitColumn(items, side);
48547
+ else items.forEach((o) => pushHidden(o.p));
48548
+ }
48549
+ }
48550
+ if (resolved.directives.noContextLabels !== true) {
48551
+ for (const l of labels) {
48552
+ if (l.hidden) continue;
48553
+ const w = labelW(l.text);
48554
+ const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
48555
+ obstacles.push({ x, y: l.y - labelH / 2, w, h: labelH });
48556
+ }
48557
+ for (const box of insets)
48558
+ obstacles.push({ x: box.x, y: box.y, w: box.w, h: box.h });
48559
+ const countryCandidates = [];
48560
+ for (const f of worldLayer.values()) {
48561
+ const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
48562
+ if (!iso || regionById.has(iso)) continue;
48563
+ let hasReferencedSub = false;
48564
+ for (const k of regionById.keys())
48565
+ if (k.startsWith(iso + "-")) {
48566
+ hasReferencedSub = true;
48567
+ break;
47192
48568
  }
48569
+ if (hasReferencedSub) continue;
48570
+ const b = path.bounds(f);
48571
+ const [x0, y0] = b[0];
48572
+ const [x1, y1] = b[1];
48573
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48574
+ const anchorLngLat = WORLD_LABEL_ANCHORS[iso];
48575
+ const a = anchorLngLat ? project(anchorLngLat[0], anchorLngLat[1]) : path.centroid(f);
48576
+ countryCandidates.push({
48577
+ name: f.properties?.name ?? iso,
48578
+ bbox: [x0, y0, x1, y1],
48579
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48580
+ });
48581
+ }
48582
+ const framedStateContainers = (resolved.poiFrameContainers ?? []).some(
48583
+ (id) => id.startsWith("US-")
48584
+ );
48585
+ if (usLayer && framedStateContainers) {
48586
+ const containerSet = new Set(resolved.poiFrameContainers);
48587
+ for (const [iso, f] of usLayer) {
48588
+ if (containerSet.has(iso) || regionById.has(iso)) continue;
48589
+ const viewF = cullFeatureToView(f);
48590
+ if (!viewF) continue;
48591
+ const b = path.bounds(viewF);
48592
+ const [x0, y0] = b[0];
48593
+ const [x1, y1] = b[1];
48594
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48595
+ const a = path.centroid(viewF);
48596
+ countryCandidates.push({
48597
+ name: f.properties?.name ?? iso,
48598
+ bbox: [x0, y0, x1, y1],
48599
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48600
+ });
47193
48601
  }
47194
- placeColumn(g);
47195
48602
  }
48603
+ const contextLabels = placeContextLabels({
48604
+ projection: resolved.projection,
48605
+ dLonSpan,
48606
+ dLatSpan,
48607
+ width,
48608
+ height,
48609
+ waterBodies: data.waterBodies,
48610
+ countries: countryCandidates,
48611
+ palette,
48612
+ project,
48613
+ collides,
48614
+ // Water labels must stay over open water — `fillAt` returns the ocean
48615
+ // backdrop colour off-land and a region fill on-land (lakes/states count
48616
+ // as land here, which is the safe side for an ocean name).
48617
+ overLand: (x, y) => fillAt(x, y) !== water
48618
+ });
48619
+ labels.push(...contextLabels);
47196
48620
  }
47197
48621
  let legend = null;
47198
48622
  if (!resolved.directives.noLegend) {
@@ -47229,60 +48653,104 @@ function layoutMap(resolved, data, size, opts) {
47229
48653
  rivers,
47230
48654
  relief,
47231
48655
  reliefHatch,
48656
+ coastlineStyle,
47232
48657
  legs,
47233
48658
  pois,
48659
+ clusters,
47234
48660
  labels,
47235
48661
  legend,
47236
48662
  insets,
47237
48663
  insetRegions,
47238
48664
  projection,
47239
- stretch: stretchParams
48665
+ stretch: stretchParams,
48666
+ diagnostics: []
47240
48667
  };
47241
48668
  }
47242
- var import_d3_geo2, import_topojson_client2, FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, COLO_EPS, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, FAN_STEP, ARC_CURVE_FRAC, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, US_NON_CONUS;
48669
+ var import_d3_geo2, import_topojson_client2, 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;
47243
48670
  var init_layout15 = __esm({
47244
48671
  "src/map/layout.ts"() {
47245
48672
  "use strict";
47246
48673
  import_d3_geo2 = require("d3-geo");
47247
48674
  import_topojson_client2 = require("topojson-client");
47248
48675
  init_color_utils();
48676
+ init_geo();
48677
+ init_colorize();
47249
48678
  init_colors();
47250
48679
  init_label_layout();
47251
48680
  init_legend_constants();
47252
48681
  init_title_constants();
48682
+ init_context_labels();
47253
48683
  FIT_PAD = 24;
47254
- RAMP_FLOOR = 15;
48684
+ RAMP_FLOOR2 = 15;
47255
48685
  R_DEFAULT = 6;
47256
48686
  R_MIN = 4;
47257
48687
  R_MAX = 22;
47258
48688
  W_MIN = 1.25;
47259
48689
  W_MAX = 8;
47260
- FONT = 11;
47261
- COLO_EPS = 1.5;
48690
+ FONT2 = 11;
48691
+ WORLD_LABEL_ANCHORS = {
48692
+ US: [-98.5, 39.5]
48693
+ // CONUS geographic centre (near Lebanon, Kansas)
48694
+ };
48695
+ MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48696
+ MAX_COLUMN_ROWS = 7;
48697
+ REGION_LABEL_HALO_RATIO = 4.5;
47262
48698
  LAND_TINT_LIGHT = 12;
47263
48699
  LAND_TINT_DARK = 24;
47264
48700
  TAG_TINT_LIGHT = 60;
47265
48701
  TAG_TINT_DARK = 68;
47266
- WATER_TINT_LIGHT = 13;
47267
- WATER_TINT_DARK = 14;
48702
+ WATER_TINT_LIGHT = 24;
48703
+ WATER_TINT_DARK = 24;
47268
48704
  RIVER_WIDTH = 1.3;
48705
+ COMPACT_WIDTH_PX = 480;
47269
48706
  RELIEF_MIN_AREA = 12;
47270
48707
  RELIEF_MIN_DIM = 2;
47271
- RELIEF_HATCH_SPACING = 3;
47272
- RELIEF_HATCH_WIDTH = 0.25;
47273
- RELIEF_HATCH_STRENGTH = 32;
48708
+ RELIEF_HATCH_SPACING = 1.5;
48709
+ RELIEF_HATCH_WIDTH = 0.2;
48710
+ RELIEF_HATCH_STRENGTH = 26;
48711
+ COASTLINE_RING_COUNT = 5;
48712
+ COASTLINE_D0 = 16e-4;
48713
+ COASTLINE_STEP = 28e-4;
48714
+ COASTLINE_THICKNESS = 14e-4;
48715
+ COASTLINE_OPACITY_NEAR = 0.5;
48716
+ COASTLINE_OPACITY_FAR = 0.1;
48717
+ COASTLINE_MIN_EXTENT = 6e-4;
48718
+ COASTLINE_MIN_EXTENT_GLOBAL = 6e-4;
48719
+ COASTLINE_STROKE_MIX = 32;
47274
48720
  FOREIGN_TINT_LIGHT = 30;
47275
48721
  FOREIGN_TINT_DARK = 62;
47276
48722
  MUTED_FOREIGN_LIGHT = 28;
47277
48723
  MUTED_FOREIGN_DARK = 16;
47278
48724
  COLO_R = 9;
47279
48725
  GOLDEN_ANGLE = 2.399963229728653;
48726
+ STACK_OVERLAP = 1;
48727
+ STACK_RING_MAX = 8;
48728
+ STACK_RING_GAP = 4;
47280
48729
  FAN_STEP = 16;
47281
48730
  ARC_CURVE_FRAC = 0.18;
48731
+ decodeCache = /* @__PURE__ */ new WeakMap();
47282
48732
  usConusProjection = () => (0, import_d3_geo2.geoConicEqualArea)().parallels([29.5, 45.5]).rotate([96, 0]);
47283
48733
  alaskaProjection = () => (0, import_d3_geo2.geoConicEqualArea)().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
47284
48734
  hawaiiProjection = () => (0, import_d3_geo2.geoMercator)();
47285
48735
  INSET_STATES = /* @__PURE__ */ new Set(["US-AK", "US-HI"]);
48736
+ inAlaska = (lon, lat) => lat >= 51 && (lon <= -129 || lon >= 172);
48737
+ inHawaii = (lon, lat) => lat >= 18 && lat <= 23 && lon >= -161 && lon <= -154;
48738
+ FOREIGN_BORDER = {
48739
+ CA: [
48740
+ "US-AK",
48741
+ "US-WA",
48742
+ "US-ID",
48743
+ "US-MT",
48744
+ "US-ND",
48745
+ "US-MN",
48746
+ "US-MI",
48747
+ "US-NY",
48748
+ "US-VT",
48749
+ "US-NH",
48750
+ "US-ME"
48751
+ ],
48752
+ MX: ["US-CA", "US-AZ", "US-NM", "US-TX"]
48753
+ };
47286
48754
  US_NON_CONUS = /* @__PURE__ */ new Set([
47287
48755
  "US-AK",
47288
48756
  "US-HI",
@@ -47301,6 +48769,98 @@ __export(renderer_exports16, {
47301
48769
  renderMap: () => renderMap,
47302
48770
  renderMapForExport: () => renderMapForExport
47303
48771
  });
48772
+ function pointInRing2(px, py, ring) {
48773
+ let inside = false;
48774
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
48775
+ const [xi, yi] = ring[i];
48776
+ const [xj, yj] = ring[j];
48777
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
48778
+ inside = !inside;
48779
+ }
48780
+ return inside;
48781
+ }
48782
+ function ringToPath(ring) {
48783
+ let d = "";
48784
+ for (let i = 0; i < ring.length; i++)
48785
+ d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48786
+ return d + "Z";
48787
+ }
48788
+ function polylineToPath(pts) {
48789
+ let d = "";
48790
+ for (let i = 0; i < pts.length; i++)
48791
+ d += (i ? "L" : "M") + pts[i][0] + "," + pts[i][1];
48792
+ return d;
48793
+ }
48794
+ function ringToCoastPaths(ring, frame) {
48795
+ if (!frame) return [ringToPath(ring)];
48796
+ const n = ring.length;
48797
+ const eps = 0.75;
48798
+ const onL = (x) => Math.abs(x) <= eps;
48799
+ const onR = (x) => Math.abs(x - frame.w) <= eps;
48800
+ const onT = (y) => Math.abs(y) <= eps;
48801
+ const onB = (y) => Math.abs(y - frame.h) <= eps;
48802
+ 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]);
48803
+ let firstBreak = -1;
48804
+ for (let i = 0; i < n; i++)
48805
+ if (isFrameEdge(ring[i], ring[(i + 1) % n])) {
48806
+ firstBreak = i;
48807
+ break;
48808
+ }
48809
+ if (firstBreak === -1) return [ringToPath(ring)];
48810
+ const paths = [];
48811
+ let cur = [];
48812
+ const start = (firstBreak + 1) % n;
48813
+ for (let k = 0; k < n; k++) {
48814
+ const i = (start + k) % n;
48815
+ const a = ring[i];
48816
+ const b = ring[(i + 1) % n];
48817
+ if (isFrameEdge(a, b)) {
48818
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
48819
+ cur = [];
48820
+ continue;
48821
+ }
48822
+ if (cur.length === 0) cur.push(a);
48823
+ cur.push(b);
48824
+ }
48825
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
48826
+ return paths;
48827
+ }
48828
+ function coastlineOuterRings(regions, minExtent, frame) {
48829
+ const paths = [];
48830
+ for (const r of regions) {
48831
+ const rings = parsePathRings(r.d);
48832
+ for (let i = 0; i < rings.length; i++) {
48833
+ const ring = rings[i];
48834
+ if (ring.length < 3) continue;
48835
+ let minX = Infinity;
48836
+ let minY = Infinity;
48837
+ let maxX = -Infinity;
48838
+ let maxY = -Infinity;
48839
+ for (const [x, y] of ring) {
48840
+ if (x < minX) minX = x;
48841
+ if (x > maxX) maxX = x;
48842
+ if (y < minY) minY = y;
48843
+ if (y > maxY) maxY = y;
48844
+ }
48845
+ if (Math.max(maxX - minX, maxY - minY) < minExtent) continue;
48846
+ const [fx, fy] = ring[0];
48847
+ let depth = 0;
48848
+ for (let j = 0; j < rings.length; j++)
48849
+ if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48850
+ if (depth % 2 === 1) continue;
48851
+ paths.push(...ringToCoastPaths(ring, frame));
48852
+ }
48853
+ }
48854
+ return paths;
48855
+ }
48856
+ function appendWaterLines(g, outerRings, style, flatWater) {
48857
+ const d = outerRings.join(" ");
48858
+ const linesOuterFirst = [...style.lines].sort((a, b) => b.d - a.d);
48859
+ for (const line12 of linesOuterFirst) {
48860
+ 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");
48861
+ g.append("path").attr("d", d).attr("stroke", flatWater).attr("stroke-width", 2 * line12.d).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
48862
+ }
48863
+ }
47304
48864
  function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims, activeGroupOverride) {
47305
48865
  d3Selection18.select(container).selectAll(":not([data-d3-tooltip])").remove();
47306
48866
  const width = exportDims?.width ?? container.clientWidth;
@@ -47313,6 +48873,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47313
48873
  {
47314
48874
  palette,
47315
48875
  isDark,
48876
+ // Export-only: forward the contain-fit request from mapExportDimensions so a
48877
+ // clamped/floored (off-aspect) export canvas letterboxes instead of
48878
+ // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48879
+ // keeps the global stretch-fill.
48880
+ preferContain: exportDims?.preferContain ?? false,
47316
48881
  ...activeGroupOverride !== void 0 && {
47317
48882
  activeGroup: activeGroupOverride
47318
48883
  }
@@ -47326,6 +48891,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47326
48891
  const gRegions = svg.append("g").attr("class", "dgmo-map-regions");
47327
48892
  const drawRegion = (g, r, strokeWidth) => {
47328
48893
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
48894
+ if (r.label) p.attr("data-region-name", r.label);
48895
+ if (r.id && r.id !== "lake") p.attr("data-iso", r.id);
48896
+ if (r.labelX !== void 0 && r.labelY !== void 0) {
48897
+ p.attr("data-label-x", r.labelX).attr("data-label-y", r.labelY);
48898
+ }
47329
48899
  if (r.layer !== "base") {
47330
48900
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
47331
48901
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -47355,28 +48925,112 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47355
48925
  const landClip = defs.append("clipPath").attr("id", landClipId);
47356
48926
  for (const r of layout.regions)
47357
48927
  if (r.id !== "lake") landClip.append("path").attr("d", r.d);
47358
- 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");
48928
+ 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");
47359
48929
  for (let y = h.spacing; y < height; y += h.spacing) {
47360
48930
  gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
47361
48931
  }
47362
48932
  }
48933
+ if (layout.coastlineStyle) {
48934
+ const cs = layout.coastlineStyle;
48935
+ const maskId = "dgmo-map-water-mask";
48936
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48937
+ mask.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "white");
48938
+ const landD = layout.regions.filter((r) => r.id !== "lake").map((r) => r.d).join(" ");
48939
+ const lakeD = layout.regions.filter((r) => r.id === "lake").map((r) => r.d).join(" ");
48940
+ if (landD) mask.append("path").attr("d", landD).attr("fill", "black");
48941
+ if (lakeD) mask.append("path").attr("d", lakeD).attr("fill", "white");
48942
+ if (layout.insets.length) {
48943
+ const reach = Math.max(0, ...cs.lines.map((l) => l.d + l.thickness));
48944
+ for (const box of layout.insets) {
48945
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48946
+ mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
48947
+ }
48948
+ }
48949
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").style("pointer-events", "none").attr("mask", `url(#${maskId})`);
48950
+ appendWaterLines(
48951
+ gWater,
48952
+ // Pass the canvas frame so edges collinear with it (the antimeridian on a
48953
+ // world map, regional clipExtent cuts) don't get ringed as fake coast —
48954
+ // land runs cleanly to the render-area edge.
48955
+ coastlineOuterRings(layout.regions, cs.minExtent, {
48956
+ w: width,
48957
+ h: height
48958
+ }),
48959
+ cs,
48960
+ layout.background
48961
+ );
48962
+ const byStroke = /* @__PURE__ */ new Map();
48963
+ for (const r of layout.regions) {
48964
+ const arr = byStroke.get(r.stroke);
48965
+ if (arr) arr.push(r.d);
48966
+ else byStroke.set(r.stroke, [r.d]);
48967
+ }
48968
+ for (const [stroke2, ds] of byStroke)
48969
+ gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48970
+ }
47363
48971
  if (layout.rivers.length) {
47364
- const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
48972
+ const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none").style("pointer-events", "none");
47365
48973
  for (const r of layout.rivers) {
47366
48974
  gRivers.append("path").attr("d", r.d).attr("stroke", r.color).attr("stroke-width", r.width).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
47367
48975
  }
47368
48976
  }
47369
48977
  if (layout.insets.length) {
47370
48978
  const insetG = svg.append("g").attr("class", "dgmo-map-insets");
47371
- for (const box of layout.insets) {
48979
+ layout.insets.forEach((box, bi) => {
47372
48980
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
47373
48981
  insetG.append("path").attr("d", d).attr("fill", layout.background).attr("stroke", mix(palette.text, palette.bg, 55)).attr("stroke-width", 1).attr("stroke-linejoin", "round");
47374
- }
48982
+ if (box.contextLand) {
48983
+ const clipId = `dgmo-map-inset-clip-${bi}`;
48984
+ defs.append("clipPath").attr("id", clipId).append("path").attr("d", d);
48985
+ insetG.append("path").attr("d", box.contextLand.d).attr("fill", box.contextLand.fill).attr("clip-path", `url(#${clipId})`);
48986
+ }
48987
+ });
47375
48988
  for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
47376
- }
48989
+ if (layout.coastlineStyle) {
48990
+ const cs = layout.coastlineStyle;
48991
+ const maskId = "dgmo-map-inset-water-mask";
48992
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
48993
+ for (const box of layout.insets) {
48994
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48995
+ mask.append("path").attr("d", d).attr("fill", "white");
48996
+ }
48997
+ layout.insets.forEach((box, bi) => {
48998
+ if (box.contextLand)
48999
+ mask.append("path").attr("d", box.contextLand.d).attr("fill", "black").attr("clip-path", `url(#dgmo-map-inset-clip-${bi})`);
49000
+ });
49001
+ for (const r of layout.insetRegions)
49002
+ if (r.id !== "lake")
49003
+ mask.append("path").attr("d", r.d).attr("fill", "black");
49004
+ for (const r of layout.insetRegions)
49005
+ if (r.id === "lake")
49006
+ mask.append("path").attr("d", r.d).attr("fill", "white");
49007
+ const clipId = "dgmo-map-inset-water-clip";
49008
+ const clip = defs.append("clipPath").attr("id", clipId);
49009
+ for (const box of layout.insets) {
49010
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
49011
+ clip.append("path").attr("d", d);
49012
+ }
49013
+ 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})`);
49014
+ appendWaterLines(
49015
+ gInsetWater,
49016
+ coastlineOuterRings(layout.insetRegions, cs.minExtent),
49017
+ cs,
49018
+ layout.background
49019
+ );
49020
+ for (const r of layout.insetRegions)
49021
+ gInsetWater.append("path").attr("d", r.d).attr("stroke", r.stroke).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
49022
+ }
49023
+ }
49024
+ const wireSync = (sel, lineNumber) => {
49025
+ if (lineNumber < 1) return;
49026
+ sel.attr("data-line-number", lineNumber);
49027
+ if (onClickItem)
49028
+ sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
49029
+ };
47377
49030
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
47378
49031
  layout.legs.forEach((leg, i) => {
47379
49032
  const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round");
49033
+ wireSync(p, leg.lineNumber);
47380
49034
  if (leg.arrow) {
47381
49035
  const id = `dgmo-map-arrow-${i}`;
47382
49036
  const s = arrowSize(leg.width);
@@ -47384,25 +49038,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47384
49038
  p.attr("marker-end", `url(#${id})`);
47385
49039
  }
47386
49040
  if (leg.label !== void 0 && leg.labelX !== void 0) {
47387
- emitText(
49041
+ const lt = emitText(
47388
49042
  gLegs,
47389
49043
  leg.labelX,
47390
49044
  leg.labelY ?? 0,
47391
49045
  leg.label,
47392
49046
  "middle",
47393
- palette.textMuted,
47394
- haloColor,
47395
- true,
49047
+ leg.labelColor ?? palette.textMuted,
49048
+ leg.labelHaloColor ?? haloColor,
49049
+ leg.labelHalo ?? true,
47396
49050
  LABEL_FONT - 1
47397
49051
  );
49052
+ wireSync(lt, leg.lineNumber);
47398
49053
  }
47399
49054
  });
49055
+ const gSpider = svg.append("g").attr("class", "dgmo-map-spider");
49056
+ for (const cl of layout.clusters) {
49057
+ if (!exportDims) {
49058
+ 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");
49059
+ }
49060
+ for (const leg of cl.legs) {
49061
+ 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");
49062
+ }
49063
+ 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");
49064
+ }
47400
49065
  const gPois = svg.append("g").attr("class", "dgmo-map-pois");
47401
49066
  for (const poi of layout.pois) {
47402
49067
  if (poi.isOrigin) {
47403
49068
  gPois.append("circle").attr("cx", poi.cx).attr("cy", poi.cy).attr("r", poi.r + 3).attr("fill", "none").attr("stroke", poi.stroke).attr("stroke-width", 1.5);
47404
49069
  }
47405
49070
  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);
49071
+ if (poi.clusterId !== void 0)
49072
+ c.attr("data-cluster-member", poi.clusterId);
47406
49073
  if (poi.tags) {
47407
49074
  for (const [group, value] of Object.entries(poi.tags)) {
47408
49075
  c.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
@@ -47430,12 +49097,32 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47430
49097
  }
47431
49098
  const gLabels = svg.append("g").attr("class", "dgmo-map-labels");
47432
49099
  for (const lab of layout.labels) {
49100
+ if (lab.hidden) {
49101
+ if (exportDims) continue;
49102
+ emitText(
49103
+ gLabels,
49104
+ lab.x,
49105
+ lab.y,
49106
+ lab.text,
49107
+ lab.anchor,
49108
+ lab.color,
49109
+ lab.haloColor,
49110
+ lab.halo,
49111
+ LABEL_FONT,
49112
+ lab.italic,
49113
+ lab.letterSpacing
49114
+ ).attr("data-poi", lab.poiId ?? null).attr("data-poi-hidden", "").style("opacity", 0).style("pointer-events", "none");
49115
+ continue;
49116
+ }
47433
49117
  if (lab.leader) {
47434
49118
  const line12 = gLabels.append("line").attr("x1", lab.leader.x1).attr("y1", lab.leader.y1).attr("x2", lab.leader.x2).attr("y2", lab.leader.y2).attr(
47435
49119
  "stroke",
47436
49120
  lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
47437
49121
  ).attr("stroke-width", lab.leaderColor ? 1 : 0.75);
47438
49122
  if (lab.poiId !== void 0) line12.attr("data-poi", lab.poiId);
49123
+ if (lab.clusterMember !== void 0)
49124
+ line12.attr("data-cluster-member", lab.clusterMember);
49125
+ wireSync(line12, lab.lineNumber);
47439
49126
  }
47440
49127
  const t = emitText(
47441
49128
  gLabels,
@@ -47446,11 +49133,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47446
49133
  lab.color,
47447
49134
  lab.haloColor,
47448
49135
  lab.halo,
47449
- LABEL_FONT
49136
+ LABEL_FONT,
49137
+ lab.italic,
49138
+ lab.letterSpacing,
49139
+ lab.lines
47450
49140
  );
47451
49141
  if (lab.poiId !== void 0) {
47452
49142
  t.attr("data-poi", lab.poiId).style("cursor", "default");
47453
49143
  }
49144
+ if (lab.clusterMember !== void 0) {
49145
+ t.attr("data-cluster-member", lab.clusterMember);
49146
+ }
49147
+ wireSync(t, lab.lineNumber);
49148
+ }
49149
+ if (!exportDims && layout.clusters.length) {
49150
+ const gBadge = svg.append("g").attr("class", "dgmo-map-cluster-badges");
49151
+ for (const cl of layout.clusters) {
49152
+ const g = gBadge.append("g").attr("data-cluster", cl.id).style("opacity", 0).style("pointer-events", "none");
49153
+ const R = 9;
49154
+ 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);
49155
+ 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);
49156
+ emitText(
49157
+ g,
49158
+ cl.cx,
49159
+ cl.cy + 3,
49160
+ String(cl.count),
49161
+ "middle",
49162
+ palette.text,
49163
+ palette.bg,
49164
+ false,
49165
+ LABEL_FONT
49166
+ );
49167
+ }
47454
49168
  }
47455
49169
  if (layout.legend) {
47456
49170
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
@@ -47487,7 +49201,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47487
49201
  svg.append("text").attr("class", "dgmo-map-title").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).attr("fill", palette.text).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 4).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.title);
47488
49202
  }
47489
49203
  if (layout.subtitle) {
47490
- svg.append("text").attr("x", width / 2).attr("y", TITLE_Y + TITLE_FONT_SIZE).attr("text-anchor", "middle").attr("font-size", LABEL_FONT + 1).attr("fill", palette.textMuted).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.subtitle);
49204
+ svg.append("text").attr("class", "dgmo-map-subtitle").attr("x", width / 2).attr("y", TITLE_Y + TITLE_FONT_SIZE).attr("text-anchor", "middle").attr("font-size", LABEL_FONT + 1).attr("fill", palette.textMuted).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.subtitle);
47491
49205
  }
47492
49206
  if (layout.caption) {
47493
49207
  svg.append("text").attr("x", width / 2).attr("y", height - 8).attr("text-anchor", "middle").attr("font-size", LABEL_FONT).attr("fill", palette.textMuted).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.caption);
@@ -47496,10 +49210,21 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47496
49210
  function renderMapForExport(container, resolved, data, palette, isDark, exportDims) {
47497
49211
  renderMap(container, resolved, data, palette, isDark, void 0, exportDims);
47498
49212
  }
47499
- function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize) {
47500
- const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color).text(text);
49213
+ function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize, italic, letterSpacing, lines) {
49214
+ const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color);
49215
+ if (lines && lines.length > 1) {
49216
+ const lineHeight = fontSize + 2;
49217
+ const startDy = -((lines.length - 1) / 2) * lineHeight;
49218
+ lines.forEach((ln, i) => {
49219
+ t.append("tspan").attr("x", x).attr("dy", i === 0 ? startDy : lineHeight).text(ln);
49220
+ });
49221
+ } else {
49222
+ t.text(text);
49223
+ }
49224
+ if (italic) t.attr("font-style", "italic");
49225
+ if (letterSpacing) t.attr("letter-spacing", letterSpacing);
47501
49226
  if (withHalo) {
47502
- t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7);
49227
+ t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 2).attr("stroke-linejoin", "round").attr("stroke-linecap", "round").attr("stroke-opacity", 0.55);
47503
49228
  }
47504
49229
  return t;
47505
49230
  }
@@ -47517,6 +49242,56 @@ var init_renderer16 = __esm({
47517
49242
  }
47518
49243
  });
47519
49244
 
49245
+ // src/map/dimensions.ts
49246
+ var dimensions_exports = {};
49247
+ __export(dimensions_exports, {
49248
+ mapContentAspect: () => mapContentAspect,
49249
+ mapExportDimensions: () => mapExportDimensions
49250
+ });
49251
+ function mapContentAspect(resolved, data, ref = REF) {
49252
+ const { projection, fitTarget } = buildMapProjection(resolved, data);
49253
+ projection.fitSize([ref, ref], fitTarget);
49254
+ const b = (0, import_d3_geo3.geoPath)(projection).bounds(fitTarget);
49255
+ const w = b[1][0] - b[0][0];
49256
+ const h = b[1][1] - b[0][1];
49257
+ const aspect = w / h;
49258
+ return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49259
+ }
49260
+ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49261
+ const raw = mapContentAspect(resolved, data);
49262
+ const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49263
+ const width = baseWidth;
49264
+ let height = Math.round(width / clamped);
49265
+ let chromeReserve = 0;
49266
+ if (resolved.title && resolved.pois.length > 0) {
49267
+ const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
49268
+ chromeReserve += Math.max(FIT_PAD2, bannerBottom + TITLE_GAP) - FIT_PAD2;
49269
+ }
49270
+ let floored = false;
49271
+ if (height - chromeReserve < MIN_MAP_BAND) {
49272
+ height = Math.round(chromeReserve + MIN_MAP_BAND);
49273
+ floored = true;
49274
+ }
49275
+ const preferContain = clamped !== raw || floored;
49276
+ return { width, height, preferContain };
49277
+ }
49278
+ var import_d3_geo3, FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
49279
+ var init_dimensions = __esm({
49280
+ "src/map/dimensions.ts"() {
49281
+ "use strict";
49282
+ import_d3_geo3 = require("d3-geo");
49283
+ init_title_constants();
49284
+ init_layout15();
49285
+ FIT_PAD2 = 24;
49286
+ TITLE_GAP = 16;
49287
+ ASPECT_MAX = 3;
49288
+ ASPECT_MIN = 0.9;
49289
+ MIN_MAP_BAND = 200;
49290
+ FALLBACK_ASPECT = 1.5;
49291
+ REF = 1e3;
49292
+ }
49293
+ });
49294
+
47520
49295
  // src/map/load-data.ts
47521
49296
  var load_data_exports = {};
47522
49297
  __export(load_data_exports, {
@@ -47575,12 +49350,17 @@ function loadMapData() {
47575
49350
  mountainRanges,
47576
49351
  naLand,
47577
49352
  naLakes,
49353
+ waterBodies,
47578
49354
  gazetteer
47579
49355
  ] = await Promise.all([
49356
+ // worldCoarse (110m) is LOAD-BEARING but NOT a render source: the world
49357
+ // basemap renders from worldDetail (50m) at all scales (resolver pins
49358
+ // basemaps.world = 'detail'). Coarse stays as the authoritative region
49359
+ // name index + dominant-landmass bbox source in resolver.ts. Do not drop it.
47580
49360
  readJson(nb, dir, FILES.worldCoarse),
47581
49361
  readJson(nb, dir, FILES.worldDetail),
47582
49362
  readJson(nb, dir, FILES.usStates),
47583
- // Lakes/rivers/mountain/NA assets are optional — older bundles may predate them.
49363
+ // Lakes/rivers/mountain/NA/water assets are optional — older bundles may predate them.
47584
49364
  readJson(nb, dir, FILES.lakes).catch(() => void 0),
47585
49365
  readJson(nb, dir, FILES.rivers).catch(() => void 0),
47586
49366
  readJson(nb, dir, FILES.mountainRanges).catch(
@@ -47588,6 +49368,7 @@ function loadMapData() {
47588
49368
  ),
47589
49369
  readJson(nb, dir, FILES.naLand).catch(() => void 0),
47590
49370
  readJson(nb, dir, FILES.naLakes).catch(() => void 0),
49371
+ readJson(nb, dir, FILES.waterBodies).catch(() => void 0),
47591
49372
  readJson(nb, dir, FILES.gazetteer)
47592
49373
  ]);
47593
49374
  return validate({
@@ -47599,7 +49380,8 @@ function loadMapData() {
47599
49380
  ...rivers && { rivers },
47600
49381
  ...mountainRanges && { mountainRanges },
47601
49382
  ...naLand && { naLand },
47602
- ...naLakes && { naLakes }
49383
+ ...naLakes && { naLakes },
49384
+ ...waterBodies && { waterBodies }
47603
49385
  });
47604
49386
  })().catch((e) => {
47605
49387
  cache = void 0;
@@ -47621,6 +49403,7 @@ var init_load_data = __esm({
47621
49403
  mountainRanges: "mountain-ranges.json",
47622
49404
  naLand: "na-land.json",
47623
49405
  naLakes: "na-lakes.json",
49406
+ waterBodies: "water-bodies.json",
47624
49407
  gazetteer: "gazetteer.json"
47625
49408
  };
47626
49409
  CANDIDATE_DIRS = [
@@ -49633,8 +51416,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
49633
51416
  const lines = splitParticipantLabel(p.label, LABEL_MAX_CHARS);
49634
51417
  if (lines.length === 0) continue;
49635
51418
  const widest = Math.max(...lines.map((l) => l.length));
49636
- const labelWidth = widest * LABEL_CHAR_WIDTH + 10;
49637
- uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth);
51419
+ const labelWidth2 = widest * LABEL_CHAR_WIDTH + 10;
51420
+ uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth2);
49638
51421
  }
49639
51422
  uniformBoxWidth = Math.min(MAX_BOX_WIDTH, uniformBoxWidth);
49640
51423
  const effectiveGap = Math.max(PARTICIPANT_GAP, uniformBoxWidth + 30);
@@ -52329,15 +54112,15 @@ function renderArcDiagram(container, parsed, palette, _isDark, onClickItem, expo
52329
54112
  textColor,
52330
54113
  onClickItem
52331
54114
  );
52332
- const neighbors = /* @__PURE__ */ new Map();
52333
- for (const node of nodes) neighbors.set(node, /* @__PURE__ */ new Set());
54115
+ const neighbors2 = /* @__PURE__ */ new Map();
54116
+ for (const node of nodes) neighbors2.set(node, /* @__PURE__ */ new Set());
52334
54117
  for (const link of links) {
52335
- neighbors.get(link.source).add(link.target);
52336
- neighbors.get(link.target).add(link.source);
54118
+ neighbors2.get(link.source).add(link.target);
54119
+ neighbors2.get(link.target).add(link.source);
52337
54120
  }
52338
54121
  const FADE_OPACITY3 = 0.1;
52339
54122
  function handleMouseEnter(hovered) {
52340
- const connected = neighbors.get(hovered);
54123
+ const connected = neighbors2.get(hovered);
52341
54124
  g.selectAll(".arc-link").each(function() {
52342
54125
  const el = d3Selection23.select(this);
52343
54126
  const src = el.attr("data-source");
@@ -53245,10 +55028,12 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
53245
55028
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
53246
55029
  const eraLabelY = eraReserve ? -(topScaleH + markerReserve + ERA_ROW_H / 2) : 0;
53247
55030
  const innerWidth = width - margin.left - margin.right;
53248
- const innerHeight = height - margin.top - margin.bottom;
53249
- const rowH = Math.min(ctx.structural(28), innerHeight / sorted.length);
55031
+ const availInnerHeight = height - margin.top - margin.bottom;
55032
+ const rowH = Math.min(ctx.structural(28), availInnerHeight / sorted.length);
55033
+ const innerHeight = rowH * sorted.length;
55034
+ const usedHeight = margin.top + innerHeight + margin.bottom;
53250
55035
  const xScale = d3Scale2.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
53251
- 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);
55036
+ 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);
53252
55037
  if (ctx.isBelowFloor) {
53253
55038
  svg.attr("width", "100%");
53254
55039
  }
@@ -54272,7 +56057,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54272
56057
  8,
54273
56058
  Math.floor(OVERLAP_WRAP_TARGET_W / OVERLAP_CH_W)
54274
56059
  );
54275
- function wrapLabel2(text, maxChars) {
56060
+ function wrapLabel3(text, maxChars) {
54276
56061
  const words = text.split(/\s+/).filter(Boolean);
54277
56062
  const lines = [];
54278
56063
  let cur = "";
@@ -54318,7 +56103,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54318
56103
  if (!ov.label) continue;
54319
56104
  const idxs = ov.sets.map((s) => vennSets.findIndex((vs) => vs.name === s));
54320
56105
  if (idxs.some((idx) => idx < 0)) continue;
54321
- const lines = wrapLabel2(ov.label, MAX_WRAP_CHARS);
56106
+ const lines = wrapLabel3(ov.label, MAX_WRAP_CHARS);
54322
56107
  wrappedOverlapLabels.set(ov, lines);
54323
56108
  const dir = predictOverlapDirRaw(idxs);
54324
56109
  const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
@@ -55756,6 +57541,7 @@ async function renderForExport(content, theme, palette, viewState, options) {
55756
57541
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55757
57542
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55758
57543
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
57544
+ const { mapExportDimensions: mapExportDimensions2 } = await Promise.resolve().then(() => (init_dimensions(), dimensions_exports));
55759
57545
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55760
57546
  const mapParsed = parseMap2(content);
55761
57547
  let mapData = options?.mapData;
@@ -55768,14 +57554,15 @@ async function renderForExport(content, theme, palette, viewState, options) {
55768
57554
  }
55769
57555
  }
55770
57556
  const mapResolved = resolveMap2(mapParsed, mapData);
55771
- const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
57557
+ const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57558
+ const container2 = createExportContainer(dims2.width, dims2.height);
55772
57559
  renderMapForExport2(
55773
57560
  container2,
55774
57561
  mapResolved,
55775
57562
  mapData,
55776
57563
  effectivePalette2,
55777
57564
  theme === "dark",
55778
- { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
57565
+ dims2
55779
57566
  );
55780
57567
  return finalizeSvgExport(container2, theme, effectivePalette2);
55781
57568
  }
@@ -56639,7 +58426,8 @@ async function render(content, options) {
56639
58426
  ...options?.c4Container !== void 0 && {
56640
58427
  c4Container: options.c4Container
56641
58428
  },
56642
- ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup }
58429
+ ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup },
58430
+ ...options?.mapData !== void 0 && { mapData: options.mapData }
56643
58431
  });
56644
58432
  if (chartType === "map") {
56645
58433
  try {
@@ -56650,7 +58438,7 @@ async function render(content, options) {
56650
58438
  Promise.resolve().then(() => (init_load_data(), load_data_exports))
56651
58439
  ]
56652
58440
  );
56653
- const data = await loadMapData2();
58441
+ const data = options?.mapData ?? await loadMapData2();
56654
58442
  diagnostics = [...resolveMap2(parseMap2(content), data).diagnostics];
56655
58443
  } catch {
56656
58444
  }