@diagrammo/dgmo 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/README.md +16 -6
  2. package/dist/advanced.cjs +2521 -623
  3. package/dist/advanced.d.cts +917 -534
  4. package/dist/advanced.d.ts +917 -534
  5. package/dist/advanced.js +2516 -623
  6. package/dist/auto.cjs +2333 -608
  7. package/dist/auto.js +119 -119
  8. package/dist/auto.mjs +2335 -609
  9. package/dist/cli.cjs +168 -168
  10. package/dist/editor.cjs +13 -15
  11. package/dist/editor.js +13 -15
  12. package/dist/highlight.cjs +15 -12
  13. package/dist/highlight.js +15 -12
  14. package/dist/index.cjs +2317 -595
  15. package/dist/index.d.cts +4 -1
  16. package/dist/index.d.ts +4 -1
  17. package/dist/index.js +2319 -596
  18. package/dist/internal.cjs +2521 -623
  19. package/dist/internal.d.cts +917 -534
  20. package/dist/internal.d.ts +917 -534
  21. package/dist/internal.js +2516 -623
  22. package/dist/map-data/PROVENANCE.json +1 -1
  23. package/dist/map-data/mountain-ranges.json +1 -0
  24. package/dist/map-data/water-bodies.json +1 -0
  25. package/docs/language-reference.md +44 -31
  26. package/gallery/fixtures/map-categorical-world.dgmo +16 -0
  27. package/gallery/fixtures/map-categorical.dgmo +0 -1
  28. package/gallery/fixtures/map-choropleth.dgmo +0 -1
  29. package/gallery/fixtures/map-coastline.dgmo +7 -0
  30. package/gallery/fixtures/map-colorize.dgmo +11 -0
  31. package/gallery/fixtures/map-direct-color.dgmo +9 -0
  32. package/gallery/fixtures/map-reference-world.dgmo +11 -0
  33. package/gallery/fixtures/map-region-scope.dgmo +0 -3
  34. package/gallery/fixtures/map-route.dgmo +0 -1
  35. package/package.json +1 -1
  36. package/src/advanced.ts +26 -1
  37. package/src/boxes-and-lines/renderer.ts +39 -12
  38. package/src/cli.ts +1 -1
  39. package/src/completion.ts +32 -24
  40. package/src/cycle/renderer.ts +14 -1
  41. package/src/d3.ts +23 -11
  42. package/src/editor/highlight-api.ts +4 -0
  43. package/src/editor/keywords.ts +13 -15
  44. package/src/infra/renderer.ts +35 -7
  45. package/src/map/colorize.ts +54 -0
  46. package/src/map/context-labels.ts +429 -0
  47. package/src/map/data/PROVENANCE.json +1 -1
  48. package/src/map/data/mountain-ranges.json +1 -0
  49. package/src/map/data/types.ts +34 -0
  50. package/src/map/data/water-bodies.json +1 -0
  51. package/src/map/dimensions.ts +117 -0
  52. package/src/map/geo-query.ts +295 -0
  53. package/src/map/geo.ts +305 -2
  54. package/src/map/invert.ts +111 -0
  55. package/src/map/layout.ts +1504 -335
  56. package/src/map/load-data.ts +16 -2
  57. package/src/map/parser.ts +57 -111
  58. package/src/map/renderer.ts +556 -13
  59. package/src/map/resolved-types.ts +24 -2
  60. package/src/map/resolver.ts +237 -67
  61. package/src/map/types.ts +39 -23
  62. package/src/mindmap/renderer.ts +10 -1
  63. package/src/palettes/atlas.ts +77 -0
  64. package/src/palettes/blueprint.ts +73 -0
  65. package/src/palettes/color-utils.ts +58 -1
  66. package/src/palettes/index.ts +12 -3
  67. package/src/palettes/slate.ts +73 -0
  68. package/src/palettes/tidewater.ts +73 -0
  69. package/src/render.ts +8 -1
  70. package/src/tech-radar/renderer.ts +3 -0
  71. package/src/tech-radar/types.ts +3 -0
  72. package/src/utils/d3-types.ts +5 -0
  73. package/src/utils/legend-layout.ts +21 -4
  74. package/src/utils/legend-types.ts +7 -0
  75. package/src/utils/reserved-key-registry.ts +3 -0
  76. package/src/palettes/bold.ts +0 -67
package/dist/internal.js CHANGED
@@ -371,18 +371,18 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
371
371
  const results = [];
372
372
  for (let i = 0; i < points.length; i++) {
373
373
  const pt = points[i];
374
- const labelWidth = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
374
+ const labelWidth2 = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
375
375
  let best = null;
376
376
  const directions = [
377
377
  {
378
378
  // Above
379
379
  gen: (offset) => {
380
- const lx = pt.cx - labelWidth / 2;
380
+ const lx = pt.cx - labelWidth2 / 2;
381
381
  const ly = pt.cy - offset - labelHeight;
382
- if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
382
+ if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
383
383
  return null;
384
384
  return {
385
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
385
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
386
386
  textX: pt.cx,
387
387
  textY: ly + labelHeight / 2,
388
388
  anchor: "middle"
@@ -392,12 +392,12 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
392
392
  {
393
393
  // Below
394
394
  gen: (offset) => {
395
- const lx = pt.cx - labelWidth / 2;
395
+ const lx = pt.cx - labelWidth2 / 2;
396
396
  const ly = pt.cy + offset;
397
- if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
397
+ if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
398
398
  return null;
399
399
  return {
400
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
400
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
401
401
  textX: pt.cx,
402
402
  textY: ly + labelHeight / 2,
403
403
  anchor: "middle"
@@ -409,10 +409,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
409
409
  gen: (offset) => {
410
410
  const lx = pt.cx + offset;
411
411
  const ly = pt.cy - labelHeight / 2;
412
- if (lx + labelWidth > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
412
+ if (lx + labelWidth2 > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
413
413
  return null;
414
414
  return {
415
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
415
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
416
416
  textX: lx,
417
417
  textY: pt.cy,
418
418
  anchor: "start"
@@ -422,13 +422,13 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
422
422
  {
423
423
  // Left
424
424
  gen: (offset) => {
425
- const lx = pt.cx - offset - labelWidth;
425
+ const lx = pt.cx - offset - labelWidth2;
426
426
  const ly = pt.cy - labelHeight / 2;
427
427
  if (lx < chartBounds.left || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
428
428
  return null;
429
429
  return {
430
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
431
- textX: lx + labelWidth,
430
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
431
+ textX: lx + labelWidth2,
432
432
  textY: pt.cy,
433
433
  anchor: "end"
434
434
  };
@@ -478,10 +478,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
478
478
  }
479
479
  }
480
480
  if (!best) {
481
- const lx = pt.cx - labelWidth / 2;
481
+ const lx = pt.cx - labelWidth2 / 2;
482
482
  const ly = pt.cy - minGap - labelHeight;
483
483
  best = {
484
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
484
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
485
485
  textX: pt.cx,
486
486
  textY: ly + labelHeight / 2,
487
487
  anchor: "middle",
@@ -858,6 +858,9 @@ var init_reserved_key_registry = __esm({
858
858
  "value",
859
859
  "label",
860
860
  "style"
861
+ // `surface:` was removed in the 2026-06-02 defaults-on review — it is no longer
862
+ // a recognized metadata key (the route/edge surface feature was cut; §24B.7).
863
+ // A stray `surface: water` is no longer captured as a reserved key.
861
864
  ]);
862
865
  ORG_REGISTRY = staticRegistry([
863
866
  "color",
@@ -1920,77 +1923,266 @@ function getSegmentColors(palette, count) {
1920
1923
  (_, i) => hslToHex(Math.round((startHue + i * step) % 360), avgS, avgL)
1921
1924
  );
1922
1925
  }
1926
+ function politicalTints(palette, count, isDark) {
1927
+ if (count <= 0) return [];
1928
+ const base = isDark ? palette.surface : palette.bg;
1929
+ const c = palette.colors;
1930
+ const swatches = [
1931
+ .../* @__PURE__ */ new Set([
1932
+ c.green,
1933
+ c.yellow,
1934
+ c.orange,
1935
+ c.purple,
1936
+ c.red,
1937
+ c.teal,
1938
+ c.cyan,
1939
+ c.blue
1940
+ ])
1941
+ ];
1942
+ const bands = isDark ? POLITICAL_TINT_BANDS.dark : POLITICAL_TINT_BANDS.light;
1943
+ const out = [];
1944
+ for (const pct of bands) {
1945
+ if (out.length >= count) break;
1946
+ for (const s of swatches) out.push(mix(s, base, pct));
1947
+ }
1948
+ return out.slice(0, count);
1949
+ }
1950
+ var POLITICAL_TINT_BANDS;
1923
1951
  var init_color_utils = __esm({
1924
1952
  "src/palettes/color-utils.ts"() {
1925
1953
  "use strict";
1954
+ POLITICAL_TINT_BANDS = {
1955
+ light: [32, 48, 64, 80],
1956
+ dark: [44, 58, 72, 86]
1957
+ };
1926
1958
  }
1927
1959
  });
1928
1960
 
1929
- // src/palettes/bold.ts
1930
- var boldPalette;
1931
- var init_bold = __esm({
1932
- "src/palettes/bold.ts"() {
1961
+ // src/palettes/atlas.ts
1962
+ var atlasPalette;
1963
+ var init_atlas = __esm({
1964
+ "src/palettes/atlas.ts"() {
1933
1965
  "use strict";
1934
1966
  init_registry();
1935
- boldPalette = {
1936
- id: "bold",
1937
- name: "Bold",
1967
+ atlasPalette = {
1968
+ id: "atlas",
1969
+ name: "Atlas",
1938
1970
  light: {
1939
- bg: "#ffffff",
1940
- surface: "#f0f0f0",
1941
- overlay: "#f0f0f0",
1942
- border: "#cccccc",
1943
- text: "#000000",
1944
- textMuted: "#666666",
1945
- textOnFillLight: "#ffffff",
1946
- textOnFillDark: "#000000",
1947
- primary: "#0000ff",
1948
- secondary: "#ff00ff",
1949
- accent: "#00cccc",
1950
- destructive: "#ff0000",
1971
+ bg: "#f3ead3",
1972
+ // warm manila / parchment
1973
+ surface: "#ece0c0",
1974
+ // deeper paper (cards, panels)
1975
+ overlay: "#e8dab8",
1976
+ // popovers, dropdowns
1977
+ border: "#bcaa86",
1978
+ // muted sepia rule line
1979
+ text: "#463a26",
1980
+ // aged sepia-brown ink
1981
+ textMuted: "#7a6a4f",
1982
+ // faded annotation ink
1983
+ textOnFillLight: "#f7f1de",
1984
+ // parchment (light text on dark fills)
1985
+ textOnFillDark: "#3a2e1c",
1986
+ // deep ink (dark text on light fills)
1987
+ primary: "#5b7a99",
1988
+ // pull-down map ocean (steel-blue)
1989
+ secondary: "#7e9a6f",
1990
+ // lowland sage / celadon
1991
+ accent: "#b07f7c",
1992
+ // dusty rose
1993
+ destructive: "#b25a45",
1994
+ // brick / terracotta
1951
1995
  colors: {
1952
- red: "#ff0000",
1953
- orange: "#ff8000",
1954
- yellow: "#ffcc00",
1955
- green: "#00cc00",
1956
- blue: "#0000ff",
1957
- purple: "#cc00cc",
1958
- teal: "#008080",
1959
- cyan: "#00cccc",
1960
- gray: "#808080",
1961
- black: "#000000",
1962
- white: "#f0f0f0"
1996
+ red: "#bf6a52",
1997
+ // terracotta brick
1998
+ orange: "#cf9a5c",
1999
+ // map tan / ochre
2000
+ yellow: "#cdb35e",
2001
+ // straw / muted lemon
2002
+ green: "#7e9a6f",
2003
+ // sage / celadon lowland
2004
+ blue: "#5b7a99",
2005
+ // steel-blue ocean
2006
+ purple: "#9a7fa6",
2007
+ // dusty lilac / mauve
2008
+ teal: "#6fa094",
2009
+ // muted seafoam
2010
+ cyan: "#79a7b5",
2011
+ // shallow-water blue
2012
+ gray: "#8a7d68",
2013
+ // warm taupe
2014
+ black: "#463a26",
2015
+ // ink
2016
+ white: "#ece0c0"
2017
+ // paper
1963
2018
  }
1964
2019
  },
1965
2020
  dark: {
1966
- bg: "#000000",
1967
- surface: "#111111",
1968
- overlay: "#1a1a1a",
1969
- border: "#333333",
1970
- text: "#ffffff",
1971
- textMuted: "#aaaaaa",
1972
- textOnFillLight: "#ffffff",
1973
- textOnFillDark: "#000000",
1974
- primary: "#00ccff",
1975
- secondary: "#ff00ff",
1976
- accent: "#ffff00",
1977
- destructive: "#ff0000",
2021
+ bg: "#1e2a33",
2022
+ // deep map ocean (night globe)
2023
+ surface: "#27353f",
2024
+ // raised ocean
2025
+ overlay: "#2e3d48",
2026
+ // popovers, dropdowns
2027
+ border: "#3d4f5c",
2028
+ // depth-contour line
2029
+ text: "#e8dcc0",
2030
+ // parchment ink, inverted
2031
+ textMuted: "#a89a7d",
2032
+ // faded label
2033
+ textOnFillLight: "#f7f1de",
2034
+ // parchment
2035
+ textOnFillDark: "#1a242c",
2036
+ // deep ocean ink
2037
+ primary: "#7ba0bf",
2038
+ // brighter ocean
2039
+ secondary: "#9bb588",
2040
+ // sage, lifted
2041
+ accent: "#cf9a96",
2042
+ // dusty rose, lifted
2043
+ destructive: "#c9745c",
2044
+ // brick, lifted
2045
+ colors: {
2046
+ red: "#cf7a60",
2047
+ // terracotta
2048
+ orange: "#d9a96a",
2049
+ // tan / ochre
2050
+ yellow: "#d8c074",
2051
+ // straw
2052
+ green: "#9bb588",
2053
+ // sage lowland
2054
+ blue: "#7ba0bf",
2055
+ // ocean
2056
+ purple: "#b59ac0",
2057
+ // lilac / mauve
2058
+ teal: "#85b3a6",
2059
+ // seafoam
2060
+ cyan: "#92bccb",
2061
+ // shallow-water blue
2062
+ gray: "#9a8d76",
2063
+ // warm taupe
2064
+ black: "#27353f",
2065
+ // raised ocean
2066
+ white: "#e8dcc0"
2067
+ // parchment
2068
+ }
2069
+ }
2070
+ };
2071
+ registerPalette(atlasPalette);
2072
+ }
2073
+ });
2074
+
2075
+ // src/palettes/blueprint.ts
2076
+ var blueprintPalette;
2077
+ var init_blueprint = __esm({
2078
+ "src/palettes/blueprint.ts"() {
2079
+ "use strict";
2080
+ init_registry();
2081
+ blueprintPalette = {
2082
+ id: "blueprint",
2083
+ name: "Blueprint",
2084
+ light: {
2085
+ bg: "#f4f8fb",
2086
+ // pale drafting white (faint cyan)
2087
+ surface: "#e6eef4",
2088
+ // drafting panel
2089
+ overlay: "#dde9f1",
2090
+ // popovers, dropdowns
2091
+ border: "#aac3d6",
2092
+ // pale blue grid line
2093
+ text: "#123a5e",
2094
+ // blueprint navy ink
2095
+ textMuted: "#4f7390",
2096
+ // faint draft note
2097
+ textOnFillLight: "#f4f8fb",
2098
+ // drafting white
2099
+ textOnFillDark: "#0c2f4d",
2100
+ // deep blueprint navy
2101
+ primary: "#1f5e8c",
2102
+ // blueprint blue
2103
+ secondary: "#5b7d96",
2104
+ // steel
2105
+ accent: "#b08a3e",
2106
+ // draftsman's ochre highlight
2107
+ destructive: "#c0504d",
2108
+ // correction red
1978
2109
  colors: {
1979
- red: "#ff0000",
1980
- orange: "#ff8000",
1981
- yellow: "#ffff00",
1982
- green: "#00ff00",
1983
- blue: "#0066ff",
1984
- purple: "#ff00ff",
1985
- teal: "#00cccc",
1986
- cyan: "#00ffff",
1987
- gray: "#808080",
1988
- black: "#111111",
1989
- white: "#ffffff"
2110
+ red: "#c25a4e",
2111
+ // correction red
2112
+ orange: "#c2823e",
2113
+ // ochre
2114
+ yellow: "#c2a843",
2115
+ // pencil gold
2116
+ green: "#4f8a6b",
2117
+ // drafting green
2118
+ blue: "#1f5e8c",
2119
+ // blueprint blue
2120
+ purple: "#6f5e96",
2121
+ // indigo pencil
2122
+ teal: "#3a8a8a",
2123
+ // teal
2124
+ cyan: "#3f8fb5",
2125
+ // cyan
2126
+ gray: "#7e8e98",
2127
+ // graphite
2128
+ black: "#123a5e",
2129
+ // navy ink
2130
+ white: "#e6eef4"
2131
+ // panel
2132
+ }
2133
+ },
2134
+ dark: {
2135
+ bg: "#103a5e",
2136
+ // deep blueprint blue (cyanotype ground)
2137
+ surface: "#16466e",
2138
+ // raised sheet
2139
+ overlay: "#1c5180",
2140
+ // popovers, dropdowns
2141
+ border: "#3a6f96",
2142
+ // grid line
2143
+ text: "#eaf2f8",
2144
+ // chalk white
2145
+ textMuted: "#9fc0d6",
2146
+ // faint chalk note
2147
+ textOnFillLight: "#eaf2f8",
2148
+ // chalk white
2149
+ textOnFillDark: "#0c2f4d",
2150
+ // deep blueprint navy
2151
+ primary: "#7fb8d8",
2152
+ // chalk cyan
2153
+ secondary: "#9fb8c8",
2154
+ // pale steel
2155
+ accent: "#d8c27a",
2156
+ // chalk amber
2157
+ destructive: "#e08a7a",
2158
+ // chalk correction red
2159
+ colors: {
2160
+ red: "#e0907e",
2161
+ // chalk red
2162
+ orange: "#e0ab78",
2163
+ // chalk amber
2164
+ yellow: "#e3d089",
2165
+ // chalk gold
2166
+ green: "#93c79e",
2167
+ // chalk green
2168
+ blue: "#8ec3e0",
2169
+ // chalk cyan-blue
2170
+ purple: "#b6a6d8",
2171
+ // chalk indigo
2172
+ teal: "#84c7c2",
2173
+ // chalk teal
2174
+ cyan: "#9fd6e0",
2175
+ // chalk cyan
2176
+ gray: "#aebecb",
2177
+ // chalk graphite
2178
+ black: "#16466e",
2179
+ // raised sheet
2180
+ white: "#eaf2f8"
2181
+ // chalk white
1990
2182
  }
1991
2183
  }
1992
2184
  };
1993
- registerPalette(boldPalette);
2185
+ registerPalette(blueprintPalette);
1994
2186
  }
1995
2187
  });
1996
2188
 
@@ -2487,6 +2679,120 @@ var init_rose_pine = __esm({
2487
2679
  }
2488
2680
  });
2489
2681
 
2682
+ // src/palettes/slate.ts
2683
+ var slatePalette;
2684
+ var init_slate = __esm({
2685
+ "src/palettes/slate.ts"() {
2686
+ "use strict";
2687
+ init_registry();
2688
+ slatePalette = {
2689
+ id: "slate",
2690
+ name: "Slate",
2691
+ light: {
2692
+ bg: "#ffffff",
2693
+ // clean slide white
2694
+ surface: "#f3f5f8",
2695
+ // light cool-gray panel
2696
+ overlay: "#eaeef3",
2697
+ // popovers, dropdowns
2698
+ border: "#d4dae1",
2699
+ // hairline rule
2700
+ text: "#1f2933",
2701
+ // near-black slate (softer than pure black)
2702
+ textMuted: "#5b6672",
2703
+ // secondary label
2704
+ textOnFillLight: "#ffffff",
2705
+ // light text on dark fills
2706
+ textOnFillDark: "#1f2933",
2707
+ // dark text on light fills
2708
+ primary: "#3b6ea5",
2709
+ // confident corporate blue
2710
+ secondary: "#5b6672",
2711
+ // slate gray
2712
+ accent: "#3a9188",
2713
+ // muted teal accent
2714
+ destructive: "#c0504d",
2715
+ // brick red
2716
+ colors: {
2717
+ red: "#c0504d",
2718
+ // brick
2719
+ orange: "#cc7a33",
2720
+ // muted amber
2721
+ yellow: "#c9a227",
2722
+ // gold (not neon)
2723
+ green: "#5b9357",
2724
+ // forest / sage
2725
+ blue: "#3b6ea5",
2726
+ // corporate blue
2727
+ purple: "#7d5ba6",
2728
+ // muted violet
2729
+ teal: "#3a9188",
2730
+ // teal
2731
+ cyan: "#4f96c4",
2732
+ // steel cyan
2733
+ gray: "#7e8a97",
2734
+ // cool gray
2735
+ black: "#1f2933",
2736
+ // slate ink
2737
+ white: "#f3f5f8"
2738
+ // panel
2739
+ }
2740
+ },
2741
+ dark: {
2742
+ bg: "#161b22",
2743
+ // deep slate (keynote dark)
2744
+ surface: "#202833",
2745
+ // raised panel
2746
+ overlay: "#29323e",
2747
+ // popovers, dropdowns
2748
+ border: "#38424f",
2749
+ // divider
2750
+ text: "#e6eaef",
2751
+ // off-white
2752
+ textMuted: "#9aa5b1",
2753
+ // secondary label
2754
+ textOnFillLight: "#ffffff",
2755
+ // light text on dark fills
2756
+ textOnFillDark: "#161b22",
2757
+ // dark text on light fills
2758
+ primary: "#5b9bd5",
2759
+ // lifted corporate blue
2760
+ secondary: "#8593a3",
2761
+ // slate gray, lifted
2762
+ accent: "#45b3a3",
2763
+ // teal, lifted
2764
+ destructive: "#e07b6e",
2765
+ // brick, lifted
2766
+ colors: {
2767
+ red: "#e07b6e",
2768
+ // brick
2769
+ orange: "#e0975a",
2770
+ // amber
2771
+ yellow: "#d9bd5a",
2772
+ // gold
2773
+ green: "#74b56e",
2774
+ // forest / sage
2775
+ blue: "#5b9bd5",
2776
+ // corporate blue
2777
+ purple: "#a585c9",
2778
+ // violet
2779
+ teal: "#45b3a3",
2780
+ // teal
2781
+ cyan: "#62b0d9",
2782
+ // steel cyan
2783
+ gray: "#95a1ae",
2784
+ // cool gray
2785
+ black: "#202833",
2786
+ // raised panel
2787
+ white: "#e6eaef"
2788
+ // off-white
2789
+ }
2790
+ }
2791
+ };
2792
+ registerPalette(slatePalette);
2793
+ }
2794
+ });
2795
+
2490
2796
  // src/palettes/solarized.ts
2491
2797
  var solarizedPalette;
2492
2798
  var init_solarized = __esm({
@@ -2582,6 +2888,120 @@ var init_solarized = __esm({
2582
2888
  }
2583
2889
  });
2584
2890
 
2891
+ // src/palettes/tidewater.ts
2892
+ var tidewaterPalette;
2893
+ var init_tidewater = __esm({
2894
+ "src/palettes/tidewater.ts"() {
2895
+ "use strict";
2896
+ init_registry();
2897
+ tidewaterPalette = {
2898
+ id: "tidewater",
2899
+ name: "Tidewater",
2900
+ light: {
2901
+ bg: "#eceff0",
2902
+ // weathered sea-mist paper
2903
+ surface: "#e0e4e3",
2904
+ // worn deck panel
2905
+ overlay: "#dadfdf",
2906
+ // popovers, dropdowns
2907
+ border: "#a9b2b3",
2908
+ // muted slate rule
2909
+ text: "#18313f",
2910
+ // ship's-log navy ink
2911
+ textMuted: "#51636b",
2912
+ // faded log entry
2913
+ textOnFillLight: "#f3f5f3",
2914
+ // weathered white
2915
+ textOnFillDark: "#162c38",
2916
+ // deep navy
2917
+ primary: "#1f4e6b",
2918
+ // deep-sea navy
2919
+ secondary: "#b08a4f",
2920
+ // rope / manila tan
2921
+ accent: "#c69a3e",
2922
+ // brass
2923
+ destructive: "#c1433a",
2924
+ // signal-flag red
2925
+ colors: {
2926
+ red: "#c1433a",
2927
+ // signal-flag red
2928
+ orange: "#cc7a38",
2929
+ // weathered amber
2930
+ yellow: "#d6bf5a",
2931
+ // brass gold
2932
+ green: "#4f8a6b",
2933
+ // sea-glass green
2934
+ blue: "#1f4e6b",
2935
+ // deep-sea navy
2936
+ purple: "#6a5a8c",
2937
+ // twilight harbor
2938
+ teal: "#3d8c8c",
2939
+ // sea-glass teal
2940
+ cyan: "#4f9bb5",
2941
+ // shallow water
2942
+ gray: "#8a8d86",
2943
+ // driftwood gray
2944
+ black: "#18313f",
2945
+ // navy ink
2946
+ white: "#e0e4e3"
2947
+ // deck panel
2948
+ }
2949
+ },
2950
+ dark: {
2951
+ bg: "#0f2230",
2952
+ // night-harbor deep sea
2953
+ surface: "#16303f",
2954
+ // raised hull
2955
+ overlay: "#1d3a4a",
2956
+ // popovers, dropdowns
2957
+ border: "#2c4856",
2958
+ // rigging line
2959
+ text: "#e6ebe8",
2960
+ // weathered white
2961
+ textMuted: "#9aaab0",
2962
+ // faded label
2963
+ textOnFillLight: "#f3f5f3",
2964
+ // weathered white
2965
+ textOnFillDark: "#0f2230",
2966
+ // deep sea
2967
+ primary: "#4f9bc4",
2968
+ // lifted sea blue
2969
+ secondary: "#c9a46a",
2970
+ // rope tan, lifted
2971
+ accent: "#d9b25a",
2972
+ // brass, lifted
2973
+ destructive: "#e06a5e",
2974
+ // signal red, lifted
2975
+ colors: {
2976
+ red: "#e06a5e",
2977
+ // signal-flag red
2978
+ orange: "#df9a52",
2979
+ // amber
2980
+ yellow: "#e0c662",
2981
+ // brass gold
2982
+ green: "#6fb58c",
2983
+ // sea-glass green
2984
+ blue: "#4f9bc4",
2985
+ // sea blue
2986
+ purple: "#9486bf",
2987
+ // twilight harbor
2988
+ teal: "#5cb0ac",
2989
+ // sea-glass teal
2990
+ cyan: "#62b4cf",
2991
+ // shallow water
2992
+ gray: "#9aa39c",
2993
+ // driftwood gray
2994
+ black: "#16303f",
2995
+ // raised hull
2996
+ white: "#e6ebe8"
2997
+ // weathered white
2998
+ }
2999
+ }
3000
+ };
3001
+ registerPalette(tidewaterPalette);
3002
+ }
3003
+ });
3004
+
2585
3005
  // src/palettes/tokyo-night.ts
2586
3006
  var tokyoNightPalette;
2587
3007
  var init_tokyo_night = __esm({
@@ -2857,7 +3277,8 @@ var init_monokai = __esm({
2857
3277
  // src/palettes/index.ts
2858
3278
  var palettes_exports = {};
2859
3279
  __export(palettes_exports, {
2860
- boldPalette: () => boldPalette,
3280
+ atlasPalette: () => atlasPalette,
3281
+ blueprintPalette: () => blueprintPalette,
2861
3282
  catppuccinPalette: () => catppuccinPalette,
2862
3283
  contrastText: () => contrastText,
2863
3284
  draculaPalette: () => draculaPalette,
@@ -2878,7 +3299,9 @@ __export(palettes_exports, {
2878
3299
  rosePinePalette: () => rosePinePalette,
2879
3300
  shade: () => shade,
2880
3301
  shapeFill: () => shapeFill,
3302
+ slatePalette: () => slatePalette,
2881
3303
  solarizedPalette: () => solarizedPalette,
3304
+ tidewaterPalette: () => tidewaterPalette,
2882
3305
  tint: () => tint,
2883
3306
  tokyoNightPalette: () => tokyoNightPalette
2884
3307
  });
@@ -2888,17 +3311,21 @@ var init_palettes = __esm({
2888
3311
  "use strict";
2889
3312
  init_registry();
2890
3313
  init_color_utils();
2891
- init_bold();
3314
+ init_atlas();
3315
+ init_blueprint();
2892
3316
  init_catppuccin();
2893
3317
  init_gruvbox();
2894
3318
  init_nord();
2895
3319
  init_one_dark();
2896
3320
  init_rose_pine();
3321
+ init_slate();
2897
3322
  init_solarized();
3323
+ init_tidewater();
2898
3324
  init_tokyo_night();
2899
3325
  init_dracula();
2900
3326
  init_monokai();
2901
- init_bold();
3327
+ init_atlas();
3328
+ init_blueprint();
2902
3329
  init_catppuccin();
2903
3330
  init_dracula();
2904
3331
  init_gruvbox();
@@ -2906,9 +3333,15 @@ var init_palettes = __esm({
2906
3333
  init_nord();
2907
3334
  init_one_dark();
2908
3335
  init_rose_pine();
3336
+ init_slate();
2909
3337
  init_solarized();
3338
+ init_tidewater();
2910
3339
  init_tokyo_night();
2911
3340
  palettes = {
3341
+ atlas: atlasPalette,
3342
+ blueprint: blueprintPalette,
3343
+ slate: slatePalette,
3344
+ tidewater: tidewaterPalette,
2912
3345
  nord: nordPalette,
2913
3346
  catppuccin: catppuccinPalette,
2914
3347
  solarized: solarizedPalette,
@@ -2917,8 +3350,7 @@ var init_palettes = __esm({
2917
3350
  oneDark: oneDarkPalette,
2918
3351
  rosePine: rosePinePalette,
2919
3352
  dracula: draculaPalette,
2920
- monokai: monokaiPalette,
2921
- bold: boldPalette
3353
+ monokai: monokaiPalette
2922
3354
  };
2923
3355
  }
2924
3356
  });
@@ -3428,6 +3860,9 @@ function controlsGroupCapsuleWidth(toggles) {
3428
3860
  }
3429
3861
  return w;
3430
3862
  }
3863
+ function isAppHostedControls(config, isExport) {
3864
+ return !isExport && config.controlsHost === "app" && !!config.controlsGroup && config.controlsGroup.toggles.length > 0;
3865
+ }
3431
3866
  function buildControlsGroupLayout(config, state) {
3432
3867
  const cg = config.controlsGroup;
3433
3868
  if (!cg || cg.toggles.length === 0) return void 0;
@@ -3481,6 +3916,7 @@ function buildControlsGroupLayout(config, state) {
3481
3916
  function computeLegendLayout(config, state, containerWidth) {
3482
3917
  const { groups, controls: configControls, mode } = config;
3483
3918
  const isExport = mode === "export";
3919
+ const gated = isAppHostedControls(config, isExport);
3484
3920
  const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
3485
3921
  if (isExport && !activeGroupName) {
3486
3922
  return {
@@ -3491,7 +3927,7 @@ function computeLegendLayout(config, state, containerWidth) {
3491
3927
  pills: []
3492
3928
  };
3493
3929
  }
3494
- const controlsGroupLayout = isExport ? void 0 : buildControlsGroupLayout(config, state);
3930
+ const controlsGroupLayout = isExport || gated ? void 0 : buildControlsGroupLayout(config, state);
3495
3931
  const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0 || !!g.gradient);
3496
3932
  if (visibleGroups.length === 0 && (!configControls || configControls.length === 0) && !controlsGroupLayout) {
3497
3933
  return {
@@ -8345,8 +8781,8 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8345
8781
  const pt = points[i];
8346
8782
  const ptSize = pt.size ?? symbolSize;
8347
8783
  const minGap = ptSize / 2 + 4;
8348
- const labelWidth = pt.name.length * fontSize * 0.6 + 8;
8349
- const labelX = pt.px - labelWidth / 2;
8784
+ const labelWidth2 = pt.name.length * fontSize * 0.6 + 8;
8785
+ const labelX = pt.px - labelWidth2 / 2;
8350
8786
  let bestLabelY = 0;
8351
8787
  let bestOffset = Infinity;
8352
8788
  let placed = false;
@@ -8358,7 +8794,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8358
8794
  const candidate = {
8359
8795
  x: labelX,
8360
8796
  y: labelY,
8361
- w: labelWidth,
8797
+ w: labelWidth2,
8362
8798
  h: labelHeight
8363
8799
  };
8364
8800
  let collision = false;
@@ -8400,7 +8836,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8400
8836
  const labelRect = {
8401
8837
  x: labelX,
8402
8838
  y: bestLabelY,
8403
- w: labelWidth,
8839
+ w: labelWidth2,
8404
8840
  h: labelHeight
8405
8841
  };
8406
8842
  placedLabels.push(labelRect);
@@ -8436,7 +8872,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8436
8872
  shape: {
8437
8873
  x: labelX - bgPad,
8438
8874
  y: bestLabelY - bgPad,
8439
- width: labelWidth + bgPad * 2,
8875
+ width: labelWidth2 + bgPad * 2,
8440
8876
  height: labelHeight + bgPad * 2
8441
8877
  },
8442
8878
  style: { fill: bg },
@@ -15872,10 +16308,6 @@ function parseMap(content) {
15872
16308
  handleTag(trimmed, lineNumber);
15873
16309
  continue;
15874
16310
  }
15875
- if ((firstWord === "muted" || firstWord === "natural") && trimmed === firstWord) {
15876
- handleDirective(firstWord, "", lineNumber);
15877
- continue;
15878
- }
15879
16311
  if (DIRECTIVE_SET.has(firstWord) && !trimmed.slice(firstWord.length).trimStart().startsWith(":")) {
15880
16312
  handleDirective(
15881
16313
  firstWord,
@@ -15922,28 +16354,13 @@ function parseMap(content) {
15922
16354
  pushWarning(line12, `Duplicate directive "${key}" \u2014 last value wins.`);
15923
16355
  };
15924
16356
  switch (key) {
15925
- case "region":
15926
- dup(d.region);
15927
- d.region = value;
15928
- break;
15929
- case "projection":
15930
- dup(d.projection);
15931
- if (value && ![
15932
- "equirectangular",
15933
- "natural-earth",
15934
- "albers-usa",
15935
- "mercator"
15936
- ].includes(value))
15937
- pushWarning(
15938
- line12,
15939
- `Unknown projection "${value}" (expected equirectangular | natural-earth | albers-usa | mercator).`
15940
- );
15941
- d.projection = value;
15942
- break;
15943
- case "region-metric":
16357
+ case "region-metric": {
15944
16358
  dup(d.regionMetric);
15945
- d.regionMetric = value;
16359
+ const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
16360
+ d.regionMetric = rmLabel;
16361
+ if (rmColor) d.regionMetricColor = rmColor;
15946
16362
  break;
16363
+ }
15947
16364
  case "poi-metric":
15948
16365
  dup(d.poiMetric);
15949
16366
  d.poiMetric = value;
@@ -15952,85 +16369,43 @@ function parseMap(content) {
15952
16369
  dup(d.flowMetric);
15953
16370
  d.flowMetric = value;
15954
16371
  break;
15955
- case "scale":
15956
- dup(d.scale);
15957
- {
15958
- const s = parseScale(value, line12);
15959
- if (s) d.scale = s;
15960
- }
15961
- break;
15962
- case "region-labels":
15963
- dup(d.regionLabels);
15964
- if (value && !["full", "abbrev", "off"].includes(value))
15965
- pushWarning(
15966
- line12,
15967
- `Unknown region-labels "${value}" (expected full | abbrev | off).`
15968
- );
15969
- d.regionLabels = value;
15970
- break;
15971
- case "poi-labels":
15972
- dup(d.poiLabels);
15973
- if (value && !["off", "auto", "all"].includes(value))
15974
- pushWarning(
15975
- line12,
15976
- `Unknown poi-labels "${value}" (expected off | auto | all).`
15977
- );
15978
- d.poiLabels = value;
15979
- break;
15980
- case "default-country":
15981
- dup(d.defaultCountry);
15982
- d.defaultCountry = value;
15983
- break;
15984
- case "default-state":
15985
- dup(d.defaultState);
15986
- d.defaultState = value;
16372
+ case "locale":
16373
+ dup(d.locale);
16374
+ d.locale = value;
15987
16375
  break;
15988
16376
  case "active-tag":
15989
16377
  dup(d.activeTag);
15990
16378
  d.activeTag = value;
15991
16379
  break;
16380
+ case "caption":
16381
+ dup(d.caption);
16382
+ d.caption = value;
16383
+ break;
16384
+ // ── Cosmetic `no-*` opt-outs: bare flags, idempotent (mirror `no-legend`,
16385
+ // no dup warning); each defaults the feature ON when absent. ──
15992
16386
  case "no-legend":
15993
16387
  d.noLegend = true;
15994
16388
  break;
15995
- case "muted":
15996
- case "natural":
15997
- if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
15998
- pushWarning(
15999
- line12,
16000
- `Conflicting basemap dress \u2014 "${d.basemapStyle}" then "${key}"; last wins.`
16001
- );
16002
- d.basemapStyle = key;
16389
+ case "no-coastline":
16390
+ d.noCoastline = true;
16003
16391
  break;
16004
- case "subtitle":
16005
- dup(d.subtitle);
16006
- d.subtitle = value;
16392
+ case "no-relief":
16393
+ d.noRelief = true;
16007
16394
  break;
16008
- case "caption":
16009
- dup(d.caption);
16010
- d.caption = value;
16395
+ case "no-context-labels":
16396
+ d.noContextLabels = true;
16397
+ break;
16398
+ case "no-region-labels":
16399
+ d.noRegionLabels = true;
16400
+ break;
16401
+ case "no-poi-labels":
16402
+ d.noPoiLabels = true;
16403
+ break;
16404
+ case "no-colorize":
16405
+ d.noColorize = true;
16011
16406
  break;
16012
16407
  }
16013
16408
  }
16014
- function parseScale(value, line12) {
16015
- const toks = value.split(/\s+/).filter(Boolean);
16016
- const min = Number(toks[0]);
16017
- const max = Number(toks[1]);
16018
- if (!Number.isFinite(min) || !Number.isFinite(max)) {
16019
- pushError(line12, `scale requires numeric <min> <max> (got "${value}").`);
16020
- return null;
16021
- }
16022
- const scale = { min, max };
16023
- if (toks[2] === "center") {
16024
- const c = Number(toks[3]);
16025
- if (Number.isFinite(c)) scale.center = c;
16026
- else
16027
- pushError(
16028
- line12,
16029
- `scale center requires a number (got "${toks[3] ?? ""}").`
16030
- );
16031
- }
16032
- return scale;
16033
- }
16034
16409
  function handleTag(trimmed, line12) {
16035
16410
  const m = matchTagBlockHeading(trimmed);
16036
16411
  if (!m) {
@@ -16104,6 +16479,7 @@ function parseMap(content) {
16104
16479
  };
16105
16480
  if (regionScope !== void 0) region.scope = regionScope;
16106
16481
  if (valueNum !== void 0) region.value = valueNum;
16482
+ if (split.color) region.color = split.color;
16107
16483
  regions.push(region);
16108
16484
  }
16109
16485
  function handlePoi(rest, line12, indent) {
@@ -16128,6 +16504,7 @@ function parseMap(content) {
16128
16504
  const poi = { pos, tags, meta, lineNumber: line12 };
16129
16505
  if (split.alias) poi.alias = split.alias;
16130
16506
  if (label !== void 0) poi.label = label;
16507
+ if (split.color) poi.color = split.color;
16131
16508
  pois.push(poi);
16132
16509
  open.poi = { poi, indent };
16133
16510
  }
@@ -16228,13 +16605,15 @@ function parseMap(content) {
16228
16605
  pushError(line12, `Edge has an empty endpoint: "${trimmed}".`);
16229
16606
  continue;
16230
16607
  }
16231
- const meta = k === links.length - 1 ? lastSplit.meta : {};
16608
+ const isLast = k === links.length - 1;
16609
+ const meta = isLast ? lastSplit.meta : {};
16610
+ const style = links[k].style === "arc" ? "arc" : "straight";
16232
16611
  edges.push({
16233
16612
  from,
16234
16613
  to,
16235
16614
  ...links[k].label !== void 0 && { label: links[k].label },
16236
16615
  directed: links[k].directed,
16237
- style: links[k].style,
16616
+ style,
16238
16617
  meta,
16239
16618
  lineNumber: line12
16240
16619
  });
@@ -16320,20 +16699,19 @@ var init_parser12 = __esm({
16320
16699
  LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
16321
16700
  AT_RE = /(^|[\s,])at\s*:/i;
16322
16701
  DIRECTIVE_SET = /* @__PURE__ */ new Set([
16323
- "region",
16324
- "projection",
16325
16702
  "region-metric",
16326
16703
  "poi-metric",
16327
16704
  "flow-metric",
16328
- "scale",
16329
- "region-labels",
16330
- "poi-labels",
16331
- "default-country",
16332
- "default-state",
16705
+ "locale",
16333
16706
  "active-tag",
16707
+ "caption",
16334
16708
  "no-legend",
16335
- "subtitle",
16336
- "caption"
16709
+ "no-coastline",
16710
+ "no-relief",
16711
+ "no-context-labels",
16712
+ "no-region-labels",
16713
+ "no-poi-labels",
16714
+ "no-colorize"
16337
16715
  ]);
16338
16716
  }
16339
16717
  });
@@ -24345,8 +24723,8 @@ function renderKanban(container, parsed, palette, isDark, options) {
24345
24723
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24346
24724
  for (const meta of tagMeta) {
24347
24725
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(`${meta.label}: `);
24348
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24349
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24726
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24727
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24350
24728
  metaY += sCardMetaLineHeight;
24351
24729
  }
24352
24730
  for (const detail of card.details) {
@@ -24690,8 +25068,8 @@ function renderSwimlaneCard(parent, cardLayout, tagGroups, activeTagGroup, palet
24690
25068
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24691
25069
  for (const meta of tagMeta) {
24692
25070
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", palette.textMuted).text(`${meta.label}: `);
24693
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24694
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
25071
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
25072
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24695
25073
  metaY += sCardMetaLineHeight;
24696
25074
  }
24697
25075
  for (const detail of card.details) {
@@ -25525,8 +25903,8 @@ function classifyEREntities(tables, relationships) {
25525
25903
  }
25526
25904
  }
25527
25905
  const mmParticipants = /* @__PURE__ */ new Set();
25528
- for (const [id, neighbors] of tableStarNeighbors) {
25529
- if (neighbors.size >= 2) mmParticipants.add(id);
25906
+ for (const [id, neighbors2] of tableStarNeighbors) {
25907
+ if (neighbors2.size >= 2) mmParticipants.add(id);
25530
25908
  }
25531
25909
  const indegreeValues = Object.values(indegreeMap);
25532
25910
  const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
@@ -26199,7 +26577,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26199
26577
  controlsExpanded,
26200
26578
  onToggleDescriptions,
26201
26579
  onToggleControlsExpand,
26202
- exportMode = false
26580
+ exportMode = false,
26581
+ controlsHost
26203
26582
  } = options ?? {};
26204
26583
  d3Selection6.select(container).selectAll(":not([data-d3-tooltip])").remove();
26205
26584
  const width = exportDims?.width ?? container.clientWidth;
@@ -26217,7 +26596,11 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26217
26596
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26218
26597
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26219
26598
  const sTitleY = sctx.structural(TITLE_Y);
26220
- const sLegendHeight = sctx.structural(
26599
+ const reserveHasDescriptions = parsed.nodes.some(
26600
+ (n) => n.description && n.description.length > 0
26601
+ );
26602
+ const willRenderLegend = parsed.tagGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26603
+ const sLegendHeight = willRenderLegend ? sctx.structural(
26221
26604
  getMaxLegendReservedHeight(
26222
26605
  {
26223
26606
  groups: parsed.tagGroups,
@@ -26226,7 +26609,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26226
26609
  },
26227
26610
  width
26228
26611
  )
26229
- );
26612
+ ) : 0;
26230
26613
  const activeGroup = resolveActiveTagGroup(
26231
26614
  parsed.tagGroups,
26232
26615
  parsed.options["active-tag"],
@@ -26541,10 +26924,10 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26541
26924
  const hasDescriptions = parsed.nodes.some(
26542
26925
  (n) => n.description && n.description.length > 0
26543
26926
  );
26544
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions;
26927
+ const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26545
26928
  if (hasLegend) {
26546
26929
  let controlsGroup;
26547
- if (hasDescriptions && onToggleDescriptions) {
26930
+ if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
26548
26931
  controlsGroup = {
26549
26932
  toggles: [
26550
26933
  {
@@ -26562,7 +26945,14 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26562
26945
  groups: parsed.tagGroups,
26563
26946
  position: { placement: "top-center", titleRelation: "below-title" },
26564
26947
  mode: exportMode ? "export" : "preview",
26565
- ...controlsGroup !== void 0 && { controlsGroup }
26948
+ // Keep inactive sibling tag groups visible as collapsed pills so the user
26949
+ // can click one to flip the active colouring dimension (preview only —
26950
+ // export shows just the active group). Without this, declaring a second
26951
+ // tag group (e.g. Team) leaves it invisible whenever another group is
26952
+ // active. The app's BoxesAndLinesPreview already wires pill clicks.
26953
+ showInactivePills: true,
26954
+ ...controlsGroup !== void 0 && { controlsGroup },
26955
+ ...controlsHost !== void 0 && { controlsHost }
26566
26956
  };
26567
26957
  const legendState = {
26568
26958
  activeGroup,
@@ -27809,8 +28199,9 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27809
28199
  const containerHeight = exportDims?.height ?? (container.getBoundingClientRect().height || 600);
27810
28200
  d3Selection7.select(container).selectAll("*").remove();
27811
28201
  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);
28202
+ const appHosted = options?.controlsHost === "app";
27812
28203
  const hasControls = !!options?.onToggleColorByDepth || !!options?.onToggleDescriptions;
27813
- const hasLegend = parsed.tagGroups.length > 0 || hasControls;
28204
+ const hasLegend = parsed.tagGroups.length > 0 || hasControls && !appHosted;
27814
28205
  const fixedLegend = !isExport && hasLegend;
27815
28206
  const legendReserve = fixedLegend ? getMaxLegendReservedHeight(
27816
28207
  {
@@ -27904,7 +28295,10 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27904
28295
  }),
27905
28296
  position: { placement: "top-center", titleRelation: "below-title" },
27906
28297
  mode: options?.exportMode ? "export" : "preview",
27907
- ...controlsToggles !== void 0 && { controlsGroup: controlsToggles }
28298
+ ...controlsToggles !== void 0 && { controlsGroup: controlsToggles },
28299
+ ...options?.controlsHost !== void 0 && {
28300
+ controlsHost: options.controlsHost
28301
+ }
27908
28302
  };
27909
28303
  const legendState = {
27910
28304
  activeGroup: options?.colorByDepth ? null : activeTagGroup !== void 0 ? activeTagGroup : parsed.options["active-tag"] ?? null,
@@ -28347,8 +28741,8 @@ function computeFieldAlignX(children) {
28347
28741
  for (const child of children) {
28348
28742
  if (child.metadata["_labelField"] === "true" && child.children.length >= 2) {
28349
28743
  const labelEl = child.children[0];
28350
- const labelWidth = labelEl.label.length * CHAR_WIDTH5;
28351
- maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
28744
+ const labelWidth2 = labelEl.label.length * CHAR_WIDTH5;
28745
+ maxLabelWidth = Math.max(maxLabelWidth, labelWidth2);
28352
28746
  labelFieldCount++;
28353
28747
  }
28354
28748
  }
@@ -33313,7 +33707,7 @@ function hasRoles(node) {
33313
33707
  function computeNodeWidth2(node, expanded, options) {
33314
33708
  const badgeVal = node.computedConcurrentInvocations === 0 && node.computedInstances > 1 ? node.computedInstances : 0;
33315
33709
  const badgeLen = badgeVal > 0 ? `${badgeVal}x`.length + 2 : 0;
33316
- const labelWidth = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33710
+ const labelWidth2 = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33317
33711
  const allKeys = [];
33318
33712
  if (node.computedRps > 0) allKeys.push("RPS");
33319
33713
  if (expanded) {
@@ -33357,7 +33751,7 @@ function computeNodeWidth2(node, expanded, options) {
33357
33751
  allKeys.push("overflow");
33358
33752
  }
33359
33753
  }
33360
- if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth);
33754
+ if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth2);
33361
33755
  const maxKeyLen = Math.max(...allKeys.map((k) => k.length));
33362
33756
  let maxRowWidth = 0;
33363
33757
  if (node.computedRps > 0) {
@@ -33445,7 +33839,7 @@ function computeNodeWidth2(node, expanded, options) {
33445
33839
  truncated.length * META_CHAR_WIDTH3 + PADDING_X3
33446
33840
  );
33447
33841
  }
33448
- return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20, descWidth);
33842
+ return Math.max(MIN_NODE_WIDTH2, labelWidth2, maxRowWidth + 20, descWidth);
33449
33843
  }
33450
33844
  function computeNodeHeight2(node, expanded, options) {
33451
33845
  const propCount = countDisplayProps(node, expanded, options);
@@ -34995,8 +35389,9 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
34995
35389
  }
34996
35390
  return groups;
34997
35391
  }
34998
- function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false) {
35392
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false, controlsHost) {
34999
35393
  if (legendGroups.length === 0 && !playback) return;
35394
+ const appHostedPlayback = controlsHost === "app" && !!playback;
35000
35395
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
35001
35396
  if (activeGroup) {
35002
35397
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
@@ -35005,14 +35400,29 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
35005
35400
  name: g.name,
35006
35401
  entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
35007
35402
  }));
35008
- if (playback) {
35403
+ if (playback && !appHostedPlayback) {
35009
35404
  allGroups.push({ name: "Playback", entries: [] });
35010
35405
  }
35011
35406
  const legendConfig = {
35012
35407
  groups: allGroups,
35013
35408
  position: { placement: "top-center", titleRelation: "below-title" },
35014
35409
  mode: exportMode ? "export" : "preview",
35015
- showEmptyGroups: true
35410
+ showEmptyGroups: true,
35411
+ ...appHostedPlayback && {
35412
+ controlsHost: "app",
35413
+ controlsGroup: {
35414
+ toggles: [
35415
+ {
35416
+ id: "playback",
35417
+ type: "toggle",
35418
+ label: "Playback",
35419
+ active: true,
35420
+ onToggle: () => {
35421
+ }
35422
+ }
35423
+ ]
35424
+ }
35425
+ }
35016
35426
  };
35017
35427
  const legendState = { activeGroup };
35018
35428
  renderLegendD3(
@@ -35063,8 +35473,9 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
35063
35473
  }
35064
35474
  }
35065
35475
  }
35066
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
35476
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes, controlsHost) {
35067
35477
  d3Selection11.select(container).selectAll(":not([data-d3-tooltip])").remove();
35478
+ const appHostedPlayback = controlsHost === "app" && !!playback;
35068
35479
  const ctx = ScaleContext.identity();
35069
35480
  const sc = buildScaledConstants(ctx);
35070
35481
  const legendGroups = computeInfraLegendGroups(
@@ -35073,7 +35484,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35073
35484
  palette,
35074
35485
  layout.edges
35075
35486
  );
35076
- const hasLegend = legendGroups.length > 0 || !!playback;
35487
+ const hasLegend = legendGroups.length > 0 || !!playback && !appHostedPlayback;
35077
35488
  const fixedLegend = !exportMode && hasLegend;
35078
35489
  const legendDynamicH = hasLegend ? getMaxLegendReservedHeight(
35079
35490
  {
@@ -35217,7 +35628,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35217
35628
  isDark,
35218
35629
  activeGroup ?? null,
35219
35630
  playback ?? void 0,
35220
- exportMode
35631
+ exportMode,
35632
+ controlsHost
35221
35633
  );
35222
35634
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
35223
35635
  } else {
@@ -35230,7 +35642,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35230
35642
  isDark,
35231
35643
  activeGroup ?? null,
35232
35644
  playback ?? void 0,
35233
- exportMode
35645
+ exportMode,
35646
+ controlsHost
35234
35647
  );
35235
35648
  }
35236
35649
  }
@@ -43087,6 +43500,9 @@ function renderTechRadar(container, parsed, palette, isDark, onClickItem, export
43087
43500
  onToggle: (active) => options.onToggleListing(active)
43088
43501
  }
43089
43502
  ]
43503
+ },
43504
+ ...options.controlsHost !== void 0 && {
43505
+ controlsHost: options.controlsHost
43090
43506
  }
43091
43507
  };
43092
43508
  const legendState = {
@@ -44909,7 +45325,7 @@ function computeCycleLayout(parsed, options) {
44909
45325
  const circleNodes = parsed.options["circle-nodes"] === "true";
44910
45326
  const nodeDims = parsed.nodes.map((node) => {
44911
45327
  const hasDesc = !hideDescriptions && node.description.length > 0;
44912
- const labelWidth = Math.max(
45328
+ const labelWidth2 = Math.max(
44913
45329
  MIN_NODE_WIDTH4,
44914
45330
  node.label.length * LABEL_CHAR_W + NODE_PAD_X * 2
44915
45331
  );
@@ -44918,12 +45334,12 @@ function computeCycleLayout(parsed, options) {
44918
45334
  }
44919
45335
  if (!hasDesc) {
44920
45336
  return {
44921
- width: Math.min(MAX_NODE_WIDTH3, labelWidth),
45337
+ width: Math.min(MAX_NODE_WIDTH3, labelWidth2),
44922
45338
  height: PLAIN_NODE_HEIGHT,
44923
45339
  wrappedDesc: []
44924
45340
  };
44925
45341
  }
44926
- return chooseDescribedRectDims(node.description, labelWidth);
45342
+ return chooseDescribedRectDims(node.description, labelWidth2);
44927
45343
  });
44928
45344
  if (circleNodes) {
44929
45345
  const maxDiam = Math.max(...nodeDims.map((d) => d.width));
@@ -45119,10 +45535,10 @@ function computeCycleLayout(parsed, options) {
45119
45535
  scale
45120
45536
  };
45121
45537
  }
45122
- function chooseDescribedRectDims(description, labelWidth) {
45538
+ function chooseDescribedRectDims(description, labelWidth2) {
45123
45539
  const minW = Math.min(
45124
45540
  MAX_NODE_WIDTH3,
45125
- Math.max(MIN_NODE_WIDTH4, labelWidth, DESC_MIN_WIDTH)
45541
+ Math.max(MIN_NODE_WIDTH4, labelWidth2, DESC_MIN_WIDTH)
45126
45542
  );
45127
45543
  let best = null;
45128
45544
  let bestScore = Infinity;
@@ -45551,7 +45967,8 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45551
45967
  const hideDescriptions = (renderOptions?.hideDescriptions ?? false) || parsed.options["no-descriptions"] === "true" || viewState?.hd === true;
45552
45968
  const showDescriptions = !hideDescriptions;
45553
45969
  const hasDescriptions = parsed.nodes.some((n) => n.description.length > 0) || parsed.edges.some((e) => e.description.length > 0);
45554
- const hasLegend = hasDescriptions && !!renderOptions?.onToggleDescriptions;
45970
+ const appHostedControls = renderOptions?.controlsHost === "app";
45971
+ const hasLegend = !appHostedControls && hasDescriptions && !!renderOptions?.onToggleDescriptions;
45555
45972
  const showTitle = !!parsed.title && parsed.options["no-title"] !== "on";
45556
45973
  const legendOffset = hasLegend ? sLegendHeight : 0;
45557
45974
  const layoutHeight = height - (showTitle ? sTitleAreaHeight : 0) - legendOffset;
@@ -45588,7 +46005,10 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45588
46005
  groups: [],
45589
46006
  position: { placement: "top-center", titleRelation: "below-title" },
45590
46007
  mode: renderOptions?.exportMode ? "export" : "preview",
45591
- controlsGroup
46008
+ controlsGroup,
46009
+ ...renderOptions?.controlsHost !== void 0 && {
46010
+ controlsHost: renderOptions.controlsHost
46011
+ }
45592
46012
  };
45593
46013
  const legendState = {
45594
46014
  activeGroup: null,
@@ -45842,8 +46262,8 @@ var init_renderer15 = __esm({
45842
46262
  });
45843
46263
 
45844
46264
  // src/map/geo.ts
45845
- import { feature } from "topojson-client";
45846
- import { geoBounds } from "d3-geo";
46265
+ import { feature, neighbors } from "topojson-client";
46266
+ import { geoBounds, geoArea } from "d3-geo";
45847
46267
  function geomObject(topo) {
45848
46268
  const key = Object.keys(topo.objects)[0];
45849
46269
  return topo.objects[key];
@@ -45860,6 +46280,107 @@ function featureIndex(topo) {
45860
46280
  }
45861
46281
  return idx;
45862
46282
  }
46283
+ function buildAdjacency(topo) {
46284
+ const cached = adjacencyCache.get(topo);
46285
+ if (cached) return cached;
46286
+ const geometries = geomObject(topo).geometries;
46287
+ const nb = neighbors(geometries);
46288
+ const sets = /* @__PURE__ */ new Map();
46289
+ geometries.forEach((g, i) => {
46290
+ if (!g.type || g.type === "null") return;
46291
+ let set = sets.get(g.id);
46292
+ if (!set) {
46293
+ set = /* @__PURE__ */ new Set();
46294
+ sets.set(g.id, set);
46295
+ }
46296
+ for (const j of nb[i] ?? []) {
46297
+ const nid = geometries[j]?.id;
46298
+ if (nid && nid !== g.id) set.add(nid);
46299
+ }
46300
+ });
46301
+ const out = /* @__PURE__ */ new Map();
46302
+ for (const [iso, set] of sets) out.set(iso, [...set].sort());
46303
+ adjacencyCache.set(topo, out);
46304
+ return out;
46305
+ }
46306
+ function decodeFeatures(topo) {
46307
+ return geomObject(topo).geometries.map((g) => {
46308
+ const f = feature(topo, g);
46309
+ return {
46310
+ type: "Feature",
46311
+ id: g.id,
46312
+ properties: g.properties,
46313
+ geometry: f.geometry
46314
+ };
46315
+ });
46316
+ }
46317
+ function pointInRing(lon, lat, ring) {
46318
+ let inside = false;
46319
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46320
+ const xi = ring[i][0];
46321
+ const yi = ring[i][1];
46322
+ const xj = ring[j][0];
46323
+ const yj = ring[j][1];
46324
+ const intersect = yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi;
46325
+ if (intersect) inside = !inside;
46326
+ }
46327
+ return inside;
46328
+ }
46329
+ function pointOnRingEdge(lon, lat, ring) {
46330
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46331
+ const xi = ring[i][0];
46332
+ const yi = ring[i][1];
46333
+ const xj = ring[j][0];
46334
+ const yj = ring[j][1];
46335
+ if (lon < Math.min(xi, xj) - EDGE_EPS || lon > Math.max(xi, xj) + EDGE_EPS)
46336
+ continue;
46337
+ if (lat < Math.min(yi, yj) - EDGE_EPS || lat > Math.max(yi, yj) + EDGE_EPS)
46338
+ continue;
46339
+ const cross = (xj - xi) * (lat - yi) - (yj - yi) * (lon - xi);
46340
+ if (Math.abs(cross) <= EDGE_EPS) return true;
46341
+ }
46342
+ return false;
46343
+ }
46344
+ function pointInGeometry(geometry, lon, lat) {
46345
+ const g = geometry;
46346
+ if (!g) return false;
46347
+ const polys = g.type === "Polygon" ? [g.coordinates] : g.type === "MultiPolygon" ? g.coordinates : [];
46348
+ for (const rings of polys) {
46349
+ if (!rings.length) continue;
46350
+ if (pointOnRingEdge(lon, lat, rings[0])) return true;
46351
+ if (!pointInRing(lon, lat, rings[0])) continue;
46352
+ let inHole = false;
46353
+ for (let h = 1; h < rings.length; h++) {
46354
+ if (pointInRing(lon, lat, rings[h]) && !pointOnRingEdge(lon, lat, rings[h])) {
46355
+ inHole = true;
46356
+ break;
46357
+ }
46358
+ }
46359
+ if (!inHole) return true;
46360
+ }
46361
+ return false;
46362
+ }
46363
+ function regionAt(lonLat, countries, states) {
46364
+ const lon = lonLat[0];
46365
+ const lat = lonLat[1];
46366
+ let country = null;
46367
+ for (const f of countries) {
46368
+ if (pointInGeometry(f.geometry, lon, lat)) {
46369
+ country = { iso: f.id, name: f.properties.name };
46370
+ break;
46371
+ }
46372
+ }
46373
+ let state = null;
46374
+ if (country?.iso === "US" && states) {
46375
+ for (const f of states) {
46376
+ if (pointInGeometry(f.geometry, lon, lat)) {
46377
+ state = { iso: f.id, name: f.properties.name };
46378
+ break;
46379
+ }
46380
+ }
46381
+ }
46382
+ return { country, state };
46383
+ }
45863
46384
  function featureBbox(topo, geomId) {
45864
46385
  const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45865
46386
  if (!geom) return null;
@@ -45871,6 +46392,74 @@ function featureBbox(topo, geomId) {
45871
46392
  [b[1][0], b[1][1]]
45872
46393
  ];
45873
46394
  }
46395
+ function explodePolygons(gj) {
46396
+ const g = gj.geometry ?? gj;
46397
+ const t = g.type;
46398
+ const coords = g.coordinates;
46399
+ if (t === "Polygon") {
46400
+ return [
46401
+ { type: "Feature", geometry: { type: "Polygon", coordinates: coords } }
46402
+ ];
46403
+ }
46404
+ if (t === "MultiPolygon") {
46405
+ return coords.map((rings) => ({
46406
+ type: "Feature",
46407
+ geometry: { type: "Polygon", coordinates: rings }
46408
+ }));
46409
+ }
46410
+ return [];
46411
+ }
46412
+ function bboxGap(a, b) {
46413
+ const lonGap = Math.max(0, a[0][0] - b[1][0], b[0][0] - a[1][0]);
46414
+ const latGap = Math.max(0, a[0][1] - b[1][1], b[0][1] - a[1][1]);
46415
+ return Math.max(lonGap, latGap);
46416
+ }
46417
+ function featureBboxPrimary(topo, geomId) {
46418
+ const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
46419
+ if (!geom) return null;
46420
+ const gj = feature(topo, geom);
46421
+ const parts = explodePolygons(gj);
46422
+ if (parts.length <= 1) return featureBbox(topo, geomId);
46423
+ const polys = parts.map((p) => {
46424
+ const b = geoBounds(p);
46425
+ if (!b || !Number.isFinite(b[0][0])) return null;
46426
+ const wraps = b[1][0] < b[0][0];
46427
+ const bbox = [
46428
+ [b[0][0], b[0][1]],
46429
+ [b[1][0], b[1][1]]
46430
+ ];
46431
+ return { bbox, area: geoArea(p), wraps };
46432
+ }).filter(
46433
+ (p) => p !== null
46434
+ );
46435
+ if (polys.length <= 1 || polys.some((p) => p.wraps))
46436
+ return featureBbox(topo, geomId);
46437
+ const maxArea = Math.max(...polys.map((p) => p.area));
46438
+ const anchor = polys.find((p) => p.area === maxArea);
46439
+ const cluster = [
46440
+ [anchor.bbox[0][0], anchor.bbox[0][1]],
46441
+ [anchor.bbox[1][0], anchor.bbox[1][1]]
46442
+ ];
46443
+ const remaining = polys.filter((p) => p !== anchor);
46444
+ let added = true;
46445
+ while (added) {
46446
+ added = false;
46447
+ for (let i = remaining.length - 1; i >= 0; i--) {
46448
+ const p = remaining[i];
46449
+ const near = bboxGap(p.bbox, cluster) <= DETACH_GAP_DEG;
46450
+ const large = p.area >= DETACH_AREA_FRAC * maxArea;
46451
+ if (near || large) {
46452
+ cluster[0][0] = Math.min(cluster[0][0], p.bbox[0][0]);
46453
+ cluster[0][1] = Math.min(cluster[0][1], p.bbox[0][1]);
46454
+ cluster[1][0] = Math.max(cluster[1][0], p.bbox[1][0]);
46455
+ cluster[1][1] = Math.max(cluster[1][1], p.bbox[1][1]);
46456
+ remaining.splice(i, 1);
46457
+ added = true;
46458
+ }
46459
+ }
46460
+ }
46461
+ return cluster;
46462
+ }
45874
46463
  function unionExtent(boxes, points) {
45875
46464
  const lats = [];
45876
46465
  const lons = [];
@@ -45909,11 +46498,15 @@ function unionLongitudes(lons) {
45909
46498
  }
45910
46499
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45911
46500
  }
45912
- var fold;
46501
+ var fold, adjacencyCache, EDGE_EPS, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45913
46502
  var init_geo = __esm({
45914
46503
  "src/map/geo.ts"() {
45915
46504
  "use strict";
45916
46505
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
46506
+ adjacencyCache = /* @__PURE__ */ new WeakMap();
46507
+ EDGE_EPS = 1e-9;
46508
+ DETACH_GAP_DEG = 10;
46509
+ DETACH_AREA_FRAC = 0.25;
45917
46510
  }
45918
46511
  });
45919
46512
 
@@ -45931,6 +46524,12 @@ function looksUS(lat, lon) {
45931
46524
  if (lat < 15 || lat > 72) return false;
45932
46525
  return lon >= -180 && lon <= -64 || lon >= 172;
45933
46526
  }
46527
+ function looksNorthAmericaNeighbor(lat, lon) {
46528
+ return lat >= 14 && lat <= 72 && lon >= -141 && lon <= -52;
46529
+ }
46530
+ function isWholeSphere(bb) {
46531
+ return bb[0][0] <= -179 && bb[1][0] >= 179 && bb[0][1] <= -89 && bb[1][1] >= 89;
46532
+ }
45934
46533
  function resolveMap(parsed, data) {
45935
46534
  const diagnostics = [...parsed.diagnostics];
45936
46535
  const err = (line12, message, code) => {
@@ -45941,9 +46540,6 @@ function resolveMap(parsed, data) {
45941
46540
  };
45942
46541
  const result = {
45943
46542
  title: parsed.title,
45944
- ...parsed.directives.subtitle !== void 0 && {
45945
- subtitle: parsed.directives.subtitle
45946
- },
45947
46543
  ...parsed.directives.caption !== void 0 && {
45948
46544
  caption: parsed.directives.caption
45949
46545
  },
@@ -45953,7 +46549,7 @@ function resolveMap(parsed, data) {
45953
46549
  // renderer's job (step 4) — the resolver only carries `tags` + `tagGroups`
45954
46550
  // through; it never resolves a tag value to a palette color (#10).
45955
46551
  directives: { ...parsed.directives },
45956
- basemaps: { world: "coarse", subdivisions: [] },
46552
+ basemaps: { world: "detail", subdivisions: [] },
45957
46553
  regions: [],
45958
46554
  pois: [],
45959
46555
  edges: [],
@@ -45962,7 +46558,8 @@ function resolveMap(parsed, data) {
45962
46558
  [-180, -85],
45963
46559
  [180, 85]
45964
46560
  ],
45965
- projection: "natural-earth",
46561
+ projection: "equirectangular",
46562
+ poiFrameContainers: [],
45966
46563
  diagnostics,
45967
46564
  error: parsed.error
45968
46565
  };
@@ -45972,7 +46569,10 @@ function resolveMap(parsed, data) {
45972
46569
  ...[...countryIndex.values()].map((v) => v.name),
45973
46570
  ...[...usStateIndex.values()].map((v) => v.name)
45974
46571
  ];
45975
- const usScoped = parsed.directives.region === "us-states" || parsed.directives.defaultCountry?.toUpperCase() === "US" || parsed.regions.some((r) => {
46572
+ const localeRaw = parsed.directives.locale?.toUpperCase();
46573
+ const localeCountry = localeRaw ? localeRaw.split("-")[0] : void 0;
46574
+ const localeSubdivision = localeRaw && /^[A-Z]{2}-/.test(localeRaw) ? localeRaw : void 0;
46575
+ const usScoped = localeCountry === "US" || parsed.regions.some((r) => {
45976
46576
  const f = fold(r.name);
45977
46577
  return usStateIndex.has(f) && !countryIndex.has(f);
45978
46578
  }) || parsed.regions.some(
@@ -46017,12 +46617,12 @@ function resolveMap(parsed, data) {
46017
46617
  chosen = { ...inState, layer: "us-state" };
46018
46618
  } else {
46019
46619
  chosen = { ...inCountry, layer: "country" };
46620
+ warn(
46621
+ r.lineNumber,
46622
+ `"${r.name}" is both a country and a US state \u2014 resolved as ${chosen.layer} (${chosen.id}). Pin it with an ISO code (${inState.id} / ${inCountry.id}) or name + scope ("${r.name} US" / "${r.name} ${inCountry.id}").`,
46623
+ "W_MAP_REGION_AMBIGUOUS"
46624
+ );
46020
46625
  }
46021
- warn(
46022
- r.lineNumber,
46023
- `"${r.name}" is both a country and a US state \u2014 resolved as ${chosen.layer} (${chosen.id}). Pin it with an ISO code (${inState.id} / ${inCountry.id}) or name + scope ("${r.name} US" / "${r.name} ${inCountry.id}").`,
46024
- "W_MAP_REGION_AMBIGUOUS"
46025
- );
46026
46626
  } else if (inState) {
46027
46627
  chosen = { ...inState, layer: "us-state" };
46028
46628
  } else if (inCountry) {
@@ -46046,6 +46646,7 @@ function resolveMap(parsed, data) {
46046
46646
  name: chosen.name,
46047
46647
  layer: chosen.layer,
46048
46648
  ...r.value !== void 0 && { value: r.value },
46649
+ ...r.color !== void 0 && { color: r.color },
46049
46650
  tags: r.tags,
46050
46651
  meta: r.meta,
46051
46652
  lineNumber: r.lineNumber
@@ -46122,7 +46723,7 @@ function resolveMap(parsed, data) {
46122
46723
  if (!scope)
46123
46724
  warn(
46124
46725
  line12,
46125
- `"${name}" is ambiguous \u2014 resolved to the most-populous match.`,
46726
+ `"${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.`,
46126
46727
  "W_MAP_AMBIGUOUS_NAME"
46127
46728
  );
46128
46729
  }
@@ -46135,17 +46736,21 @@ function resolveMap(parsed, data) {
46135
46736
  return fold(pos.name);
46136
46737
  };
46137
46738
  const poiCountries = [];
46138
- let anyNonUsPoi = false;
46739
+ let anyUsPoi = false;
46740
+ let anyNonNaPoi = false;
46139
46741
  const noteCountry = (iso) => {
46140
46742
  if (iso) {
46141
46743
  poiCountries.push(iso);
46142
- if (iso !== "US") anyNonUsPoi = true;
46744
+ if (iso === "US") anyUsPoi = true;
46745
+ if (iso !== "US" && iso !== "CA" && iso !== "MX") anyNonNaPoi = true;
46143
46746
  }
46144
46747
  };
46145
46748
  const deferred = [];
46146
46749
  for (const p of parsed.pois) {
46147
46750
  if (p.pos.kind === "coords") {
46148
- if (!looksUS(p.pos.lat, p.pos.lon)) anyNonUsPoi = true;
46751
+ if (looksUS(p.pos.lat, p.pos.lon)) anyUsPoi = true;
46752
+ else if (!looksNorthAmericaNeighbor(p.pos.lat, p.pos.lon))
46753
+ anyNonNaPoi = true;
46149
46754
  addResolvedPoi(p.pos.lat, p.pos.lon, p);
46150
46755
  continue;
46151
46756
  }
@@ -46163,14 +46768,15 @@ function resolveMap(parsed, data) {
46163
46768
  deferred.push(p);
46164
46769
  }
46165
46770
  }
46166
- const inferredCountry = parsed.directives.defaultCountry?.toUpperCase() ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46771
+ const inferredCountry = localeCountry ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46772
+ const inferredScope = localeSubdivision ?? inferredCountry;
46167
46773
  for (const p of deferred) {
46168
46774
  if (p.pos.kind !== "name") continue;
46169
46775
  const got = lookupName(
46170
46776
  p.pos.name,
46171
46777
  p.pos.scope,
46172
46778
  p.lineNumber,
46173
- inferredCountry,
46779
+ inferredScope,
46174
46780
  true
46175
46781
  );
46176
46782
  if (got.kind === "ok") {
@@ -46187,6 +46793,7 @@ function resolveMap(parsed, data) {
46187
46793
  lat,
46188
46794
  lon,
46189
46795
  ...p.label !== void 0 && { label: p.label },
46796
+ ...p.color !== void 0 && { color: p.color },
46190
46797
  tags: p.tags,
46191
46798
  meta: p.meta,
46192
46799
  lineNumber: p.lineNumber
@@ -46239,7 +46846,8 @@ function resolveMap(parsed, data) {
46239
46846
  const meta = sizeValue !== void 0 ? { value: sizeValue } : {};
46240
46847
  if (pos.kind === "coords") {
46241
46848
  const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
46242
- if (!looksUS(pos.lat, pos.lon)) anyNonUsPoi = true;
46849
+ if (looksUS(pos.lat, pos.lon)) anyUsPoi = true;
46850
+ else if (!looksNorthAmericaNeighbor(pos.lat, pos.lon)) anyNonNaPoi = true;
46243
46851
  if (!registry.has(id)) {
46244
46852
  registerPoi(
46245
46853
  id,
@@ -46262,7 +46870,7 @@ function resolveMap(parsed, data) {
46262
46870
  if (registry.has(f)) return f;
46263
46871
  const aliased = declaredByName.get(f);
46264
46872
  if (aliased) return aliased;
46265
- const got = lookupName(pos.name, pos.scope, line12, inferredCountry, true);
46873
+ const got = lookupName(pos.name, pos.scope, line12, inferredScope, true);
46266
46874
  if (got.kind !== "ok") return null;
46267
46875
  noteCountry(got.iso);
46268
46876
  registerPoi(
@@ -46319,9 +46927,12 @@ function resolveMap(parsed, data) {
46319
46927
  }
46320
46928
  routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
46321
46929
  }
46930
+ const hasUsContent = usSubdivisionReferenced || anyUsPoi || localeCountry === "US";
46931
+ const usOriented = !anyNonNaPoi && !regions.some(
46932
+ (r) => r.layer === "country" && !["US", "CA", "MX"].includes(r.iso)
46933
+ ) && hasUsContent;
46322
46934
  const subdivisions = [];
46323
- if (usSubdivisionReferenced || parsed.directives.region === "us-states")
46324
- subdivisions.push("us-states");
46935
+ if (usSubdivisionReferenced || usOriented) subdivisions.push("us-states");
46325
46936
  const regionBoxes = [];
46326
46937
  for (const ref of referencedRegionIds) {
46327
46938
  const bb = featureBbox(data.usStates, ref.id);
@@ -46329,7 +46940,7 @@ function resolveMap(parsed, data) {
46329
46940
  }
46330
46941
  for (const r of regions) {
46331
46942
  if (r.layer === "country") {
46332
- const bb = featureBbox(data.worldCoarse, r.iso);
46943
+ const bb = featureBboxPrimary(data.worldCoarse, r.iso);
46333
46944
  if (bb) regionBoxes.push(bb);
46334
46945
  }
46335
46946
  }
@@ -46339,23 +46950,56 @@ function resolveMap(parsed, data) {
46339
46950
  [-180, -85],
46340
46951
  [180, 85]
46341
46952
  ];
46342
- let extent2 = unioned ? pad(unioned, PAD_FRACTION) : DEFAULT_EXTENT;
46953
+ const basePad = regions.length > 0 ? REGION_PAD_FRACTION : PAD_FRACTION;
46954
+ let extent2 = unioned ? pad(unioned, basePad) : DEFAULT_EXTENT;
46955
+ const isPoiOnly = pois.length > 0 && regions.length === 0;
46956
+ const containerRegionIds = [];
46957
+ if (isPoiOnly) {
46958
+ const countries = decodeFeatures(data.worldDetail);
46959
+ const states = decodeFeatures(data.usStates);
46960
+ const seen = /* @__PURE__ */ new Set();
46961
+ const containerBoxes = [];
46962
+ for (const p of pois) {
46963
+ const { country, state } = regionAt([p.lon, p.lat], countries, states);
46964
+ const id = state?.iso ?? country?.iso;
46965
+ if (!id || seen.has(id)) continue;
46966
+ seen.add(id);
46967
+ containerRegionIds.push(id);
46968
+ const bb = state ? featureBbox(data.usStates, id) : featureBboxPrimary(data.worldCoarse, id);
46969
+ if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46970
+ }
46971
+ const containerUnion = unionExtent(containerBoxes, points);
46972
+ if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46973
+ }
46974
+ if (isPoiOnly) {
46975
+ const cx = (extent2[0][0] + extent2[1][0]) / 2;
46976
+ const cy = (extent2[0][1] + extent2[1][1]) / 2;
46977
+ const lon = extent2[1][0] - extent2[0][0];
46978
+ const lat = extent2[1][1] - extent2[0][1];
46979
+ const longer = Math.max(lon, lat);
46980
+ if (longer > 0 && longer < POI_ZOOM_FLOOR_DEG) {
46981
+ const k = POI_ZOOM_FLOOR_DEG / longer;
46982
+ const halfLon = lon * k / 2;
46983
+ const halfLat = lat * k / 2;
46984
+ extent2 = [
46985
+ [cx - halfLon, cy - halfLat],
46986
+ [cx + halfLon, cy + halfLat]
46987
+ ];
46988
+ }
46989
+ }
46343
46990
  const lonSpan = extent2[1][0] - extent2[0][0];
46344
46991
  const latSpan = extent2[1][1] - extent2[0][1];
46345
46992
  const span = Math.max(lonSpan, latSpan);
46346
- const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46993
+ const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
46347
46994
  let projection;
46348
- const override = parsed.directives.projection;
46349
- if (override === "equirectangular" || override === "natural-earth" || override === "albers-usa" || override === "mercator") {
46350
- projection = override;
46351
- } else if (usDominant) {
46995
+ if (isPoiOnly && usOriented && lonSpan < US_NATIONAL_LON_SPAN) {
46996
+ projection = "mercator";
46997
+ } else if (usOriented) {
46352
46998
  projection = "albers-usa";
46353
- } else if (span > WORLD_SPAN) {
46999
+ } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
46354
47000
  projection = "equirectangular";
46355
- } else if (span < MERCATOR_MAX_SPAN) {
46356
- projection = "mercator";
46357
47001
  } else {
46358
- projection = "equirectangular";
47002
+ projection = "mercator";
46359
47003
  }
46360
47004
  if (lonSpan >= 180) {
46361
47005
  extent2 = [
@@ -46368,11 +47012,20 @@ function resolveMap(parsed, data) {
46368
47012
  result.edges = edges;
46369
47013
  result.routes = routes;
46370
47014
  result.basemaps = {
46371
- world: span > WORLD_SPAN ? "coarse" : "detail",
47015
+ // Tier is intentionally pinned to detail (50m) at ALL scales. Diagrammo maps
47016
+ // are presentational (palette tints, relief hachures, POI hubs), not
47017
+ // survey-grade — recognizability > generalization: 110m coarse drops the
47018
+ // Italian boot to a stump at world scale. `WORLD_SPAN` lives on only for the
47019
+ // projection decision (the `usOriented`/`span > WORLD_SPAN` chain above); it
47020
+ // no longer gates basemap resolution.
47021
+ // `worldCoarse` is still loaded — it's the authoritative name/bbox index
47022
+ // (featureIndex, featureBboxPrimary), not dead code.
47023
+ world: "detail",
46372
47024
  subdivisions
46373
47025
  };
46374
47026
  result.extent = extent2;
46375
47027
  result.projection = projection;
47028
+ result.poiFrameContainers = containerRegionIds;
46376
47029
  result.error = parsed.error ?? firstError(diagnostics);
46377
47030
  return result;
46378
47031
  }
@@ -46409,17 +47062,20 @@ function firstError(diags) {
46409
47062
  const e = diags.find((d) => d.severity === "error");
46410
47063
  return e ? formatDgmoError(e) : null;
46411
47064
  }
46412
- var WORLD_SPAN, MERCATOR_MAX_SPAN, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
47065
+ 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;
46413
47066
  var init_resolver2 = __esm({
46414
47067
  "src/map/resolver.ts"() {
46415
47068
  "use strict";
46416
47069
  init_diagnostics();
46417
47070
  init_geo();
46418
47071
  WORLD_SPAN = 90;
46419
- MERCATOR_MAX_SPAN = 25;
47072
+ MERCATOR_MAX_LAT = 80;
46420
47073
  PAD_FRACTION = 0.05;
47074
+ REGION_PAD_FRACTION = 0.12;
46421
47075
  WORLD_LAT_SOUTH = -58;
46422
47076
  WORLD_LAT_NORTH = 78;
47077
+ POI_ZOOM_FLOOR_DEG = 7;
47078
+ US_NATIONAL_LON_SPAN = 48;
46423
47079
  REGION_ALIASES = {
46424
47080
  // Common everyday names → the Natural-Earth display name actually shipped.
46425
47081
  "united states": "united states of america",
@@ -46497,111 +47153,269 @@ var init_resolver2 = __esm({
46497
47153
  }
46498
47154
  });
46499
47155
 
46500
- // src/map/load-data.ts
46501
- var load_data_exports = {};
46502
- __export(load_data_exports, {
46503
- loadMapData: () => loadMapData
47156
+ // src/map/colorize.ts
47157
+ function assignColors(isos, adjacency) {
47158
+ const sorted = [...isos].sort();
47159
+ const byIso = /* @__PURE__ */ new Map();
47160
+ let maxIndex = -1;
47161
+ for (const iso of sorted) {
47162
+ const taken = /* @__PURE__ */ new Set();
47163
+ for (const n of adjacency.get(iso) ?? []) {
47164
+ const c = byIso.get(n);
47165
+ if (c !== void 0) taken.add(c);
47166
+ }
47167
+ let h = 0;
47168
+ while (taken.has(h)) h++;
47169
+ byIso.set(iso, h);
47170
+ if (h > maxIndex) maxIndex = h;
47171
+ }
47172
+ return { byIso, huesNeeded: maxIndex + 1 };
47173
+ }
47174
+ var init_colorize = __esm({
47175
+ "src/map/colorize.ts"() {
47176
+ "use strict";
47177
+ }
46504
47178
  });
46505
- async function loadNodeBuiltins() {
46506
- const [{ readFile }, { fileURLToPath }, { dirname: dirname2, resolve }] = await Promise.all([
46507
- import("fs/promises"),
46508
- import("url"),
46509
- import("path")
46510
- ]);
46511
- return { readFile, fileURLToPath, dirname: dirname2, resolve };
46512
- }
46513
- async function readJson(nb, dir, name) {
46514
- return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
46515
- }
46516
- async function firstExistingDir(nb, baseDir) {
46517
- for (const rel of CANDIDATE_DIRS) {
46518
- const dir = nb.resolve(baseDir, rel);
46519
- try {
46520
- await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
46521
- return dir;
46522
- } catch {
47179
+
47180
+ // src/map/context-labels.ts
47181
+ function tierBand(maxSpanDeg) {
47182
+ if (maxSpanDeg >= 90) return "world";
47183
+ if (maxSpanDeg >= 20) return "continental";
47184
+ if (maxSpanDeg >= 5) return "regional";
47185
+ return "local";
47186
+ }
47187
+ function labelBudget(width, height, band) {
47188
+ const bandCap = {
47189
+ world: 6,
47190
+ continental: 5,
47191
+ regional: 4,
47192
+ local: 3
47193
+ };
47194
+ const area2 = Math.floor(Math.sqrt(Math.max(0, width * height)) / 150);
47195
+ return Math.max(0, Math.min(area2, bandCap[band]));
47196
+ }
47197
+ function waterEligible(tier, kind, band) {
47198
+ switch (band) {
47199
+ case "world":
47200
+ return tier <= 1 && (kind === "ocean" || kind === "sea");
47201
+ case "continental":
47202
+ return tier <= 2;
47203
+ case "regional":
47204
+ return tier <= 3;
47205
+ case "local":
47206
+ return tier <= 4;
47207
+ }
47208
+ }
47209
+ function insideViewport(p, width, height) {
47210
+ return !!p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && p[0] >= 0 && p[0] <= width && p[1] >= 0 && p[1] <= height;
47211
+ }
47212
+ function labelWidth(text, letterSpacing) {
47213
+ const spacing = letterSpacing > 0 ? Math.max(0, text.length - 1) * letterSpacing : 0;
47214
+ return measureLegendText(text, FONT) + spacing + 2 * PADX;
47215
+ }
47216
+ function wrapLabel2(text, letterSpacing) {
47217
+ const words = text.split(/\s+/).filter(Boolean);
47218
+ if (words.length <= 1) return [text];
47219
+ const maxLines = words.length >= 4 ? 3 : 2;
47220
+ const n = words.length;
47221
+ let best = null;
47222
+ for (let mask = 0; mask < 1 << n - 1; mask++) {
47223
+ const lines = [];
47224
+ let cur = [words[0]];
47225
+ for (let i = 1; i < n; i++) {
47226
+ if (mask & 1 << i - 1) {
47227
+ lines.push(cur.join(" "));
47228
+ cur = [words[i]];
47229
+ } else cur.push(words[i]);
46523
47230
  }
47231
+ lines.push(cur.join(" "));
47232
+ if (lines.length > maxLines) continue;
47233
+ const cost = Math.round(
47234
+ Math.max(...lines.map((l) => labelWidth(l, letterSpacing)))
47235
+ );
47236
+ const head = labelWidth(lines[0], letterSpacing);
47237
+ if (!best || cost < best.cost || cost === best.cost && lines.length < best.lines.length || cost === best.cost && lines.length === best.lines.length && head > best.head)
47238
+ best = { lines, cost, head };
46524
47239
  }
46525
- throw new Error(
46526
- `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
46527
- );
47240
+ return best?.lines ?? [text];
46528
47241
  }
46529
- function validate(data) {
46530
- const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
46531
- if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
46532
- throw new Error("map data assets are malformed (failed shape validation)");
46533
- }
46534
- return data;
47242
+ function rectAround(cx, cy, lines, letterSpacing) {
47243
+ const w = Math.max(...lines.map((l) => labelWidth(l, letterSpacing)));
47244
+ const h = (lines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY;
47245
+ return { x: cx - w / 2, y: cy - h / 2, w, h };
46535
47246
  }
46536
- function moduleBaseDir(nb) {
46537
- try {
46538
- const url = import.meta.url;
46539
- if (url) return nb.dirname(nb.fileURLToPath(url));
46540
- } catch {
46541
- }
46542
- if (typeof __dirname !== "undefined") return __dirname;
46543
- return process.cwd();
47247
+ function rectFits(r, width, height) {
47248
+ return r.x >= 0 && r.y >= 0 && r.x + r.w <= width && r.y + r.h <= height;
46544
47249
  }
46545
- function loadMapData() {
46546
- cache ??= (async () => {
46547
- const nb = await loadNodeBuiltins();
46548
- const dir = await firstExistingDir(nb, moduleBaseDir(nb));
46549
- const [
46550
- worldCoarse,
46551
- worldDetail,
46552
- usStates,
46553
- lakes,
46554
- rivers,
46555
- naLand,
46556
- naLakes,
46557
- gazetteer
46558
- ] = await Promise.all([
46559
- readJson(nb, dir, FILES.worldCoarse),
46560
- readJson(nb, dir, FILES.worldDetail),
46561
- readJson(nb, dir, FILES.usStates),
46562
- // Lakes/rivers/NA assets are optional — older bundles may predate them.
46563
- readJson(nb, dir, FILES.lakes).catch(() => void 0),
46564
- readJson(nb, dir, FILES.rivers).catch(() => void 0),
46565
- readJson(nb, dir, FILES.naLand).catch(() => void 0),
46566
- readJson(nb, dir, FILES.naLakes).catch(() => void 0),
46567
- readJson(nb, dir, FILES.gazetteer)
46568
- ]);
46569
- return validate({
46570
- worldCoarse,
46571
- worldDetail,
46572
- usStates,
46573
- gazetteer,
46574
- ...lakes && { lakes },
46575
- ...rivers && { rivers },
46576
- ...naLand && { naLand },
46577
- ...naLakes && { naLakes }
47250
+ function overlapsPadded(a, b, pad2) {
47251
+ 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;
47252
+ }
47253
+ function placeContextLabels(args) {
47254
+ const {
47255
+ projection,
47256
+ dLonSpan,
47257
+ dLatSpan,
47258
+ width,
47259
+ height,
47260
+ waterBodies,
47261
+ countries,
47262
+ palette,
47263
+ project,
47264
+ collides,
47265
+ overLand
47266
+ } = args;
47267
+ void projection;
47268
+ const band = tierBand(Math.max(dLonSpan, dLatSpan));
47269
+ const budget = labelBudget(width, height, band);
47270
+ if (budget <= 0) return [];
47271
+ const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
47272
+ const countryColor = palette.textMuted;
47273
+ const haloColor = palette.bg;
47274
+ const candidates = [];
47275
+ const center = [width / 2, height / 2];
47276
+ for (const e of waterBodies?.entries ?? []) {
47277
+ const [lat, lon, name, tier, kind, alt] = e;
47278
+ if (!waterEligible(tier, kind, band)) continue;
47279
+ const wlines = wrapLabel2(name, WATER_LETTER_SPACING);
47280
+ const anchorsLngLat = [[lon, lat]];
47281
+ for (const a of alt ?? []) anchorsLngLat.push([a[1], a[0]]);
47282
+ let best = null;
47283
+ let bestD = Infinity;
47284
+ let nearestProj = null;
47285
+ let nearestProjD = Infinity;
47286
+ for (const [aLon, aLat] of anchorsLngLat) {
47287
+ const p = project(aLon, aLat);
47288
+ if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
47289
+ const d = (p[0] - center[0]) ** 2 + (p[1] - center[1]) ** 2;
47290
+ if (d < nearestProjD) {
47291
+ nearestProjD = d;
47292
+ nearestProj = p;
47293
+ }
47294
+ if (!insideViewport(p, width, height)) continue;
47295
+ if (d < bestD) {
47296
+ bestD = d;
47297
+ best = p;
47298
+ }
47299
+ }
47300
+ if (!best && tier === 0 && nearestProj) {
47301
+ const overX = Math.max(0, -nearestProj[0], nearestProj[0] - width);
47302
+ const overY = Math.max(0, -nearestProj[1], nearestProj[1] - height);
47303
+ if (overX <= width * EDGE_CLAMP_OVERSHOOT && overY <= height * EDGE_CLAMP_OVERSHOOT) {
47304
+ const halfW = Math.max(...wlines.map((l) => labelWidth(l, WATER_LETTER_SPACING))) / 2;
47305
+ const halfH = ((wlines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY) / 2;
47306
+ const m = EDGE_CLAMP_MARGIN;
47307
+ best = [
47308
+ Math.min(Math.max(nearestProj[0], halfW + m), width - halfW - m),
47309
+ Math.min(Math.max(nearestProj[1], halfH + m), height - halfH - m)
47310
+ ];
47311
+ }
47312
+ }
47313
+ if (!best) continue;
47314
+ candidates.push({
47315
+ text: name,
47316
+ lines: wlines,
47317
+ cx: best[0],
47318
+ cy: best[1],
47319
+ italic: true,
47320
+ letterSpacing: WATER_LETTER_SPACING,
47321
+ color: waterColor,
47322
+ // Water before any country (×1000), then by tier, then kind, then name.
47323
+ sort: tier * 10 + KIND_ORDER[kind]
46578
47324
  });
46579
- })().catch((e) => {
46580
- cache = void 0;
46581
- throw e;
46582
- });
46583
- return cache;
47325
+ }
47326
+ const ranked = countries.map((c) => {
47327
+ const [x0, y0, x1, y1] = c.bbox;
47328
+ const w = x1 - x0;
47329
+ const h = y1 - y0;
47330
+ return { c, w, h, area: w * h };
47331
+ }).filter((r) => Number.isFinite(r.area) && r.area > 0).sort((a, b) => b.area - a.area);
47332
+ let ci = 0;
47333
+ for (const r of ranked) {
47334
+ const { c, w, h } = r;
47335
+ if (w > width * 0.66 || h > height * 0.66) continue;
47336
+ if (!insideViewport(c.anchor, width, height)) continue;
47337
+ const text = c.name;
47338
+ const tw = labelWidth(text, 0);
47339
+ if (tw > w || FONT + 2 * PADY > h) continue;
47340
+ candidates.push({
47341
+ text,
47342
+ lines: [text],
47343
+ cx: c.anchor[0],
47344
+ cy: c.anchor[1],
47345
+ italic: false,
47346
+ letterSpacing: 0,
47347
+ color: countryColor,
47348
+ // Always after every water body (+1e6); larger area = earlier.
47349
+ sort: 1e6 + ci++
47350
+ });
47351
+ }
47352
+ candidates.sort((a, b) => a.sort - b.sort);
47353
+ const placed = [];
47354
+ const placedRects = [];
47355
+ for (const cand of candidates) {
47356
+ if (placed.length >= budget) break;
47357
+ const rect = rectAround(cand.cx, cand.cy, cand.lines, cand.letterSpacing);
47358
+ if (!rectFits(rect, width, height)) continue;
47359
+ if (cand.italic && overLand) {
47360
+ const inset = 2;
47361
+ const top = cand.cy - (cand.lines.length - 1) / 2 * LINE_HEIGHT;
47362
+ const touchesLand = cand.lines.some((line12, li) => {
47363
+ const lw = labelWidth(line12, cand.letterSpacing);
47364
+ const x0 = cand.cx - lw / 2 + inset;
47365
+ const x1 = cand.cx + lw / 2 - inset;
47366
+ const xs = [x0, (x0 + cand.cx) / 2, cand.cx, (cand.cx + x1) / 2, x1];
47367
+ const base = top + li * LINE_HEIGHT;
47368
+ return [base, base - FONT * 0.4, base - FONT * 0.8].some(
47369
+ (y) => xs.some((x) => overLand(x, y))
47370
+ );
47371
+ });
47372
+ if (touchesLand) continue;
47373
+ }
47374
+ if (collides(rect)) continue;
47375
+ if (placedRects.some((r) => overlapsPadded(rect, r, CONTEXT_PAD))) continue;
47376
+ placedRects.push(rect);
47377
+ placed.push({
47378
+ x: cand.cx,
47379
+ y: cand.cy,
47380
+ text: cand.text,
47381
+ anchor: "middle",
47382
+ color: cand.color,
47383
+ // No halo: the bg-coloured outline reads as a ghost box behind the text
47384
+ // over the tinted water/land. Context labels are muted enough to sit
47385
+ // cleanly on the basemap without one.
47386
+ halo: false,
47387
+ haloColor,
47388
+ italic: cand.italic,
47389
+ letterSpacing: cand.letterSpacing,
47390
+ ...cand.lines.length > 1 ? { lines: cand.lines } : {},
47391
+ lineNumber: 0
47392
+ });
47393
+ }
47394
+ return placed;
46584
47395
  }
46585
- var FILES, CANDIDATE_DIRS, cache;
46586
- var init_load_data = __esm({
46587
- "src/map/load-data.ts"() {
47396
+ var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, KIND_ORDER;
47397
+ var init_context_labels = __esm({
47398
+ "src/map/context-labels.ts"() {
46588
47399
  "use strict";
46589
- FILES = {
46590
- worldCoarse: "world-coarse.json",
46591
- worldDetail: "world-detail.json",
46592
- usStates: "us-states.json",
46593
- lakes: "lakes.json",
46594
- rivers: "rivers.json",
46595
- naLand: "na-land.json",
46596
- naLakes: "na-lakes.json",
46597
- gazetteer: "gazetteer.json"
47400
+ init_color_utils();
47401
+ init_legend_constants();
47402
+ FONT = 11;
47403
+ LINE_HEIGHT = FONT + 2;
47404
+ PADX = 4;
47405
+ PADY = 3;
47406
+ WATER_LETTER_SPACING = 1.5;
47407
+ CONTEXT_PAD = 4;
47408
+ EDGE_CLAMP_MARGIN = 8;
47409
+ EDGE_CLAMP_OVERSHOOT = 0.35;
47410
+ KIND_ORDER = {
47411
+ ocean: 0,
47412
+ sea: 1,
47413
+ gulf: 2,
47414
+ bay: 3,
47415
+ strait: 4,
47416
+ channel: 5,
47417
+ sound: 6
46598
47418
  };
46599
- CANDIDATE_DIRS = [
46600
- "./data",
46601
- "./map-data",
46602
- "../map-data",
46603
- "../src/map/data"
46604
- ];
46605
47419
  }
46606
47420
  });
46607
47421
 
@@ -46609,6 +47423,7 @@ var init_load_data = __esm({
46609
47423
  import {
46610
47424
  geoPath,
46611
47425
  geoNaturalEarth1,
47426
+ geoEqualEarth,
46612
47427
  geoEquirectangular,
46613
47428
  geoConicEqualArea,
46614
47429
  geoMercator,
@@ -46620,12 +47435,34 @@ function geomObject2(topo) {
46620
47435
  const key = Object.keys(topo.objects)[0];
46621
47436
  return topo.objects[key];
46622
47437
  }
47438
+ function mergeFeatures(a, b) {
47439
+ const polysOf = (f) => {
47440
+ const g = f.geometry;
47441
+ if (!g) return null;
47442
+ if (g.type === "Polygon") return [g.coordinates];
47443
+ if (g.type === "MultiPolygon") return g.coordinates;
47444
+ return null;
47445
+ };
47446
+ const pa = polysOf(a);
47447
+ const pb = polysOf(b);
47448
+ if (!pa || !pb) return a;
47449
+ return {
47450
+ ...a,
47451
+ geometry: { type: "MultiPolygon", coordinates: [...pa, ...pb] }
47452
+ };
47453
+ }
46623
47454
  function decodeLayer(topo) {
47455
+ const cached = decodeCache.get(topo);
47456
+ if (cached) return cached;
46624
47457
  const out = /* @__PURE__ */ new Map();
46625
47458
  for (const g of geomObject2(topo).geometries) {
46626
47459
  const f = feature2(topo, g);
46627
- out.set(g.id, { ...f, id: g.id });
47460
+ if (!f.geometry) continue;
47461
+ const tagged = { ...f, id: g.id };
47462
+ const existing = out.get(g.id);
47463
+ out.set(g.id, existing ? mergeFeatures(existing, tagged) : tagged);
46628
47464
  }
47465
+ decodeCache.set(topo, out);
46629
47466
  return out;
46630
47467
  }
46631
47468
  function projectionFor(family) {
@@ -46634,38 +47471,35 @@ function projectionFor(family) {
46634
47471
  return usConusProjection();
46635
47472
  case "mercator":
46636
47473
  return geoMercator();
47474
+ case "equal-earth":
47475
+ return geoEqualEarth();
47476
+ case "equirectangular":
47477
+ return geoEquirectangular();
46637
47478
  case "natural-earth":
46638
47479
  return geoNaturalEarth1();
46639
- case "equirectangular":
46640
47480
  default:
46641
47481
  return geoEquirectangular();
46642
47482
  }
46643
47483
  }
46644
- function mapBackgroundColor(palette, isDark = false, dataActive = false) {
46645
- if (dataActive)
46646
- return mix(
46647
- palette.colors.gray,
46648
- palette.bg,
46649
- isDark ? MUTED_WATER_DARK : MUTED_WATER_LIGHT
46650
- );
46651
- return mix(palette.colors.blue, palette.bg, WATER_TINT);
47484
+ function mapBackgroundColor(palette, isDark = false, _dataActive = false) {
47485
+ return mix(
47486
+ palette.colors.blue,
47487
+ palette.bg,
47488
+ isDark ? WATER_TINT_DARK : WATER_TINT_LIGHT
47489
+ );
46652
47490
  }
46653
- function mapNeutralLandColor(palette, isDark, dataActive = false) {
46654
- if (dataActive)
46655
- return isDark ? mix(palette.colors.gray, palette.bg, MUTED_LAND_DARK) : palette.bg;
47491
+ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46656
47492
  return mix(
46657
47493
  palette.colors.green,
46658
47494
  palette.bg,
46659
47495
  isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT
46660
47496
  );
46661
47497
  }
46662
- function layoutMap(resolved, data, size, opts) {
46663
- const { palette, isDark } = opts;
46664
- const { width, height } = size;
47498
+ function buildMapProjection(resolved, data) {
46665
47499
  const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
46666
- const usCrisp = resolved.projection === "albers-usa" && wantsUsStates && !!data.naLand;
47500
+ const usCrisp = (resolved.projection === "albers-usa" || resolved.projection === "mercator") && wantsUsStates && !!data.naLand;
46667
47501
  const worldTopo = usCrisp ? data.worldDetail : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46668
- const worldLayer = decodeLayer(worldTopo);
47502
+ const worldLayer = new Map(decodeLayer(worldTopo));
46669
47503
  if (usCrisp && data.naLand) {
46670
47504
  const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
46671
47505
  const crisp = decodeLayer(data.naLand);
@@ -46674,17 +47508,110 @@ function layoutMap(resolved, data, size, opts) {
46674
47508
  if (!base) continue;
46675
47509
  const [[bw, bs], [be, bn]] = geoBounds2(base);
46676
47510
  if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
46677
- worldLayer.set(iso, cf);
47511
+ worldLayer.set(iso, { ...cf, properties: base.properties });
46678
47512
  }
46679
47513
  }
46680
47514
  const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
47515
+ const extentOutline = () => {
47516
+ const [[w, s], [e, n]] = resolved.extent;
47517
+ const N = 16;
47518
+ const coords = [];
47519
+ for (let i = 0; i <= N; i++) {
47520
+ const t = i / N;
47521
+ const lon = w + (e - w) * t;
47522
+ const lat = s + (n - s) * t;
47523
+ coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
47524
+ }
47525
+ return {
47526
+ type: "Feature",
47527
+ properties: {},
47528
+ geometry: { type: "MultiPoint", coordinates: coords }
47529
+ };
47530
+ };
47531
+ let fitFeatures;
47532
+ if (resolved.projection === "albers-usa" && usLayer) {
47533
+ fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
47534
+ const neighborPoints = resolved.pois.filter((p) => !inAlaska(p.lon, p.lat) && !inHawaii(p.lon, p.lat)).map((p) => [p.lon, p.lat]);
47535
+ if (neighborPoints.length > 0) {
47536
+ fitFeatures.push({
47537
+ type: "Feature",
47538
+ properties: {},
47539
+ geometry: { type: "MultiPoint", coordinates: neighborPoints }
47540
+ });
47541
+ }
47542
+ for (const r of resolved.regions) {
47543
+ if (r.layer === "country" && (r.iso === "CA" || r.iso === "MX")) {
47544
+ const cf = worldLayer.get(r.iso);
47545
+ if (cf) fitFeatures.push(cf);
47546
+ }
47547
+ }
47548
+ } else {
47549
+ fitFeatures = [extentOutline()];
47550
+ }
47551
+ const fitTarget = { type: "FeatureCollection", features: fitFeatures };
47552
+ const projection = projectionFor(resolved.projection);
47553
+ if (resolved.projection !== "albers-usa") {
47554
+ let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
47555
+ if (centerLon > 180) centerLon -= 360;
47556
+ projection.rotate([-centerLon, 0]);
47557
+ }
47558
+ const fitGB = geoBounds2(fitTarget);
47559
+ const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
47560
+ return {
47561
+ projection,
47562
+ fitTarget,
47563
+ fitIsGlobal,
47564
+ worldLayer,
47565
+ usLayer,
47566
+ usCrisp,
47567
+ wantsUsStates,
47568
+ worldTopo
47569
+ };
47570
+ }
47571
+ function parsePathRings(d) {
47572
+ const rings = [];
47573
+ let cur = [];
47574
+ const re = /([MLZ])([^MLZ]*)/g;
47575
+ let m;
47576
+ while (m = re.exec(d)) {
47577
+ if (m[1] === "Z") {
47578
+ if (cur.length) rings.push(cur);
47579
+ cur = [];
47580
+ continue;
47581
+ }
47582
+ if (m[1] === "M" && cur.length) {
47583
+ rings.push(cur);
47584
+ cur = [];
47585
+ }
47586
+ const nums = m[2].split(/[ ,]+/).map(Number);
47587
+ for (let i = 0; i + 1 < nums.length; i += 2) {
47588
+ const x = nums[i];
47589
+ const y = nums[i + 1];
47590
+ if (Number.isFinite(x) && Number.isFinite(y)) cur.push([x, y]);
47591
+ }
47592
+ }
47593
+ if (cur.length) rings.push(cur);
47594
+ return rings;
47595
+ }
47596
+ function layoutMap(resolved, data, size, opts) {
47597
+ const { palette, isDark } = opts;
47598
+ const { width, height } = size;
47599
+ const {
47600
+ projection,
47601
+ fitTarget,
47602
+ fitIsGlobal,
47603
+ worldLayer,
47604
+ usLayer,
47605
+ usCrisp,
47606
+ worldTopo
47607
+ } = buildMapProjection(resolved, data);
46681
47608
  const usContext = usLayer !== null;
46682
47609
  const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46683
47610
  const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
46684
- const scaleOverride = resolved.directives.scale;
46685
- const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46686
- const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
46687
- const rampHue = palette.colors.red;
47611
+ const allNonNegative = values.length > 0 && values.every((v) => v >= 0);
47612
+ const rampMin = allNonNegative ? 0 : Math.min(...values);
47613
+ const rampMax = Math.max(...values);
47614
+ const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46688
47615
  const hasRamp = values.length > 0;
46689
47616
  const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
46690
47617
  const matchColorGroup = (v) => {
@@ -46704,14 +47631,48 @@ function layoutMap(resolved, data, size, opts) {
46704
47631
  activeGroup = VALUE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46705
47632
  }
46706
47633
  const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
46707
- const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
47634
+ const mutedBasemap = activeGroup !== null;
46708
47635
  const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46709
47636
  const water = mapBackgroundColor(palette, isDark, mutedBasemap);
47637
+ const lakeStroke = mix(regionStroke, water, 45);
46710
47638
  const foreignFill = mix(
46711
47639
  palette.colors.gray,
46712
47640
  palette.bg,
46713
47641
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46714
47642
  );
47643
+ const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && resolved.tagGroups.length === 0;
47644
+ const colorByIso = /* @__PURE__ */ new Map();
47645
+ if (colorizeActive) {
47646
+ const adjacency = /* @__PURE__ */ new Map();
47647
+ const addEdges = (src) => {
47648
+ for (const [iso, ns] of src) {
47649
+ const cur = adjacency.get(iso);
47650
+ if (cur) cur.push(...ns);
47651
+ else adjacency.set(iso, [...ns]);
47652
+ }
47653
+ };
47654
+ addEdges(buildAdjacency(worldTopo));
47655
+ if (usLayer) {
47656
+ addEdges(buildAdjacency(data.usStates));
47657
+ for (const [country, states] of Object.entries(FOREIGN_BORDER)) {
47658
+ const cn = adjacency.get(country);
47659
+ if (!cn) continue;
47660
+ for (const st of states) {
47661
+ const sn = adjacency.get(st);
47662
+ if (!sn) continue;
47663
+ cn.push(st);
47664
+ sn.push(country);
47665
+ }
47666
+ }
47667
+ }
47668
+ const { byIso, huesNeeded } = assignColors(
47669
+ [...adjacency.keys()],
47670
+ adjacency
47671
+ );
47672
+ const tints = politicalTints(palette, huesNeeded, isDark);
47673
+ for (const [iso, idx] of byIso) colorByIso.set(iso, tints[idx]);
47674
+ }
47675
+ const colorizeStroke = (fill2) => mix(fill2, palette.text, 35);
46715
47676
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46716
47677
  const fillForValue = (s) => {
46717
47678
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
@@ -46736,47 +47697,26 @@ function layoutMap(resolved, data, size, opts) {
46736
47697
  isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
46737
47698
  );
46738
47699
  };
47700
+ const directFill = (name) => {
47701
+ const hex = name ? resolveColor(name, palette) : null;
47702
+ if (!hex) return null;
47703
+ return mix(hex, palette.bg, isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT);
47704
+ };
46739
47705
  const regionFill = (r) => {
47706
+ const direct = directFill(r.color);
47707
+ if (direct) return direct;
46740
47708
  if (activeIsScore) {
46741
47709
  return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46742
47710
  }
47711
+ if (colorizeActive) return (r.iso && colorByIso.get(r.iso)) ?? neutralFill;
46743
47712
  return tagFill(r.tags, activeGroup) ?? neutralFill;
46744
47713
  };
46745
47714
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
46746
- const extentOutline = () => {
46747
- const [[w, s], [e, n]] = resolved.extent;
46748
- const N = 16;
46749
- const coords = [];
46750
- for (let i = 0; i <= N; i++) {
46751
- const t = i / N;
46752
- const lon = w + (e - w) * t;
46753
- const lat = s + (n - s) * t;
46754
- coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
46755
- }
46756
- return {
46757
- type: "Feature",
46758
- properties: {},
46759
- geometry: { type: "MultiPoint", coordinates: coords }
46760
- };
46761
- };
46762
- let fitFeatures;
46763
- if (resolved.projection === "albers-usa" && usLayer) {
46764
- fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
46765
- } else {
46766
- fitFeatures = [extentOutline()];
46767
- }
46768
- const fitTarget = { type: "FeatureCollection", features: fitFeatures };
46769
- const projection = projectionFor(resolved.projection);
46770
- if (resolved.projection !== "albers-usa") {
46771
- let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
46772
- if (centerLon > 180) centerLon -= 360;
46773
- projection.rotate([-centerLon, 0]);
46774
- }
46775
- const TITLE_GAP = 16;
47715
+ const TITLE_GAP2 = 16;
46776
47716
  let topPad = FIT_PAD;
46777
47717
  if (resolved.title && resolved.pois.length > 0) {
46778
47718
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
46779
- topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
47719
+ topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
46780
47720
  }
46781
47721
  const fitBox = [
46782
47722
  [FIT_PAD, topPad],
@@ -46786,11 +47726,10 @@ function layoutMap(resolved, data, size, opts) {
46786
47726
  ]
46787
47727
  ];
46788
47728
  projection.fitExtent(fitBox, fitTarget);
46789
- const fitGB = geoBounds2(fitTarget);
46790
- const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46791
47729
  let path;
46792
47730
  let project;
46793
- if (fitIsGlobal) {
47731
+ let stretchParams = null;
47732
+ if (fitIsGlobal && !opts.preferContain) {
46794
47733
  const cb = geoPath(projection).bounds(fitTarget);
46795
47734
  const bx0 = cb[0][0];
46796
47735
  const by0 = cb[0][1];
@@ -46800,6 +47739,7 @@ function layoutMap(resolved, data, size, opts) {
46800
47739
  const oy = fitBox[0][1];
46801
47740
  const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46802
47741
  const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47742
+ stretchParams = { sx, sy, ox, oy, bx0, by0 };
46803
47743
  const stretch = (x, y) => [
46804
47744
  ox + (x - bx0) * sx,
46805
47745
  oy + (y - by0) * sy
@@ -46831,7 +47771,9 @@ function layoutMap(resolved, data, size, opts) {
46831
47771
  const insets = [];
46832
47772
  const insetRegions = [];
46833
47773
  const insetLabelSeeds = [];
46834
- if (resolved.projection === "albers-usa" && usLayer) {
47774
+ const akRef = resolved.regions.some((r) => r.iso === "US-AK") || resolved.pois.some((p) => inAlaska(p.lon, p.lat));
47775
+ const hiRef = resolved.regions.some((r) => r.iso === "US-HI") || resolved.pois.some((p) => inHawaii(p.lon, p.lat));
47776
+ if (resolved.projection === "albers-usa" && usLayer && (akRef || hiRef)) {
46835
47777
  const PAD = 8;
46836
47778
  const GAP = 12;
46837
47779
  const yB = height - FIT_PAD;
@@ -46862,38 +47804,14 @@ function layoutMap(resolved, data, size, opts) {
46862
47804
  }
46863
47805
  return y;
46864
47806
  };
46865
- const coastTop = (x0, xr) => {
47807
+ const coastFloor = (x0, xr) => {
46866
47808
  const n = 24;
46867
- const pts = [];
46868
47809
  let maxY = -Infinity;
46869
47810
  for (let i = 0; i <= n; i++) {
46870
- const x = x0 + (xr - x0) * i / n;
46871
- const y = at(x);
46872
- if (y > -Infinity) {
46873
- pts.push([x, y]);
46874
- if (y > maxY) maxY = y;
46875
- }
46876
- }
46877
- if (pts.length === 0) return () => yB - height * 0.42;
46878
- let m = 0;
46879
- if (pts.length >= 2) {
46880
- let sx = 0, sy = 0, sxx = 0, sxy = 0;
46881
- for (const [x, y] of pts) {
46882
- sx += x;
46883
- sy += y;
46884
- sxx += x * x;
46885
- sxy += x * y;
46886
- }
46887
- const den = pts.length * sxx - sx * sx;
46888
- if (den !== 0) m = (pts.length * sxy - sx * sy) / den;
46889
- }
46890
- m = Math.max(-0.35, Math.min(0.35, m));
46891
- let c = -Infinity;
46892
- for (const [x, y] of pts) {
46893
- const need = y - m * x + GAP;
46894
- if (need > c) c = need;
46895
- }
46896
- return (x) => m * x + c;
47811
+ const y = at(x0 + (xr - x0) * i / n);
47812
+ if (y > maxY) maxY = y;
47813
+ }
47814
+ return maxY;
46897
47815
  };
46898
47816
  const placeInset = (iso, proj, boxX, iwReq) => {
46899
47817
  const f = usLayer.get(iso);
@@ -46902,19 +47820,15 @@ function layoutMap(resolved, data, size, opts) {
46902
47820
  const iw = Math.min(iwReq, width - FIT_PAD - x0 - 2 * PAD);
46903
47821
  if (iw < 24) return boxX;
46904
47822
  const xr = x0 + iw + 2 * PAD;
46905
- const top = coastTop(x0, xr);
46906
- const yL = top(x0);
46907
- const yR = top(xr);
47823
+ const floor = coastFloor(x0, xr);
47824
+ const topGuess = floor > -Infinity ? floor + GAP : yB - height * 0.42;
46908
47825
  proj.fitWidth(iw, f);
46909
47826
  const bb = geoPath(proj).bounds(f);
46910
47827
  const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
46911
47828
  const needH = sh + 2 * PAD;
46912
- let topFit = Math.max(yL, yR);
47829
+ let topFit = topGuess;
46913
47830
  const bottom = Math.min(topFit + needH, yB);
46914
47831
  if (bottom - topFit < needH) topFit = bottom - needH;
46915
- const lift = topFit - Math.max(yL, yR);
46916
- const topL = yL + lift;
46917
- const topR = yR + lift;
46918
47832
  proj.fitExtent(
46919
47833
  [
46920
47834
  [x0 + PAD, topFit + PAD],
@@ -46924,8 +47838,18 @@ function layoutMap(resolved, data, size, opts) {
46924
47838
  );
46925
47839
  const d = geoPath(proj)(f) ?? "";
46926
47840
  if (!d) return xr;
47841
+ let contextLand;
47842
+ if (iso === "US-AK") {
47843
+ const can = worldLayer.get("CA");
47844
+ const cd = can ? geoPath(proj)(can) ?? "" : "";
47845
+ if (cd)
47846
+ contextLand = {
47847
+ d: cd,
47848
+ fill: colorizeActive ? colorByIso.get("CA") ?? foreignFill : foreignFill
47849
+ };
47850
+ }
46927
47851
  const r = regionById.get(iso);
46928
- let fill2 = neutralFill;
47852
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? neutralFill : neutralFill;
46929
47853
  let lineNumber = -1;
46930
47854
  if (r?.layer === "us-state") {
46931
47855
  fill2 = regionFill(r);
@@ -46933,21 +47857,25 @@ function layoutMap(resolved, data, size, opts) {
46933
47857
  }
46934
47858
  insets.push({
46935
47859
  x: x0,
46936
- y: Math.min(topL, topR),
47860
+ y: topFit,
46937
47861
  w: xr - x0,
46938
- h: bottom - Math.min(topL, topR),
47862
+ h: bottom - topFit,
46939
47863
  points: [
46940
- [x0, topL],
46941
- [xr, topR],
47864
+ [x0, topFit],
47865
+ [xr, topFit],
46942
47866
  [xr, bottom],
46943
47867
  [x0, bottom]
46944
- ]
47868
+ ],
47869
+ // The FITTED inset projection (just fit to this box) — captured so the
47870
+ // geo-query can invert pixels inside the frame back to AK/HI coords.
47871
+ projection: proj,
47872
+ ...contextLand && { contextLand }
46945
47873
  });
46946
47874
  insetRegions.push({
46947
47875
  id: iso,
46948
47876
  d,
46949
47877
  fill: fill2,
46950
- stroke: regionStroke,
47878
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46951
47879
  lineNumber,
46952
47880
  layer: "us-state",
46953
47881
  ...r?.value !== void 0 && { value: r.value },
@@ -46960,13 +47888,16 @@ function layoutMap(resolved, data, size, opts) {
46960
47888
  }
46961
47889
  return xr;
46962
47890
  };
46963
- const akRight = placeInset(
46964
- "US-AK",
46965
- alaskaProjection(),
46966
- FIT_PAD,
46967
- width * 0.15
46968
- );
46969
- placeInset("US-HI", hawaiiProjection(), akRight + 24, width * 0.1);
47891
+ let akRight = FIT_PAD;
47892
+ if (akRef)
47893
+ akRight = placeInset("US-AK", alaskaProjection(), FIT_PAD, width * 0.15);
47894
+ if (hiRef)
47895
+ placeInset(
47896
+ "US-HI",
47897
+ hawaiiProjection(),
47898
+ akRef ? akRight + 24 : FIT_PAD,
47899
+ width * 0.1
47900
+ );
46970
47901
  }
46971
47902
  const conusFit = resolved.projection === "albers-usa" && !!usLayer;
46972
47903
  const classifyExtent = conusFit ? geoBounds2(fitTarget) : resolved.extent;
@@ -46982,15 +47913,24 @@ function layoutMap(resolved, data, size, opts) {
46982
47913
  };
46983
47914
  const ringOverlapsView = (ring) => {
46984
47915
  let loMin = Infinity, loMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
47916
+ const lons = [];
46985
47917
  for (const [rawLon] of ring) {
46986
47918
  const lon = normLon(rawLon);
47919
+ lons.push(lon);
46987
47920
  if (lon < loMin) loMin = lon;
46988
47921
  if (lon > loMax) loMax = lon;
46989
47922
  if (rawLon < rawMin) rawMin = rawLon;
46990
47923
  if (rawLon > rawMax) rawMax = rawLon;
46991
47924
  }
46992
- if (loMax - loMin > 270) return false;
46993
- if (rawMax - rawMin > 180 && loMax - loMin < 90) return false;
47925
+ lons.sort((a, b) => a - b);
47926
+ let maxGap = 0;
47927
+ for (let i = 1; i < lons.length; i++)
47928
+ maxGap = Math.max(maxGap, lons[i] - lons[i - 1]);
47929
+ if (lons.length > 1)
47930
+ maxGap = Math.max(maxGap, lons[0] + 360 - lons[lons.length - 1]);
47931
+ const occupiedArc = 360 - maxGap;
47932
+ if (occupiedArc > 270) return false;
47933
+ if (rawMax - rawMin > 180 && occupiedArc < 90) return false;
46994
47934
  let px0 = Infinity, py0 = Infinity, px1 = -Infinity, py1 = -Infinity, anyFinite = false;
46995
47935
  for (const [lon, lat] of ring) {
46996
47936
  const p = project(lon, lat);
@@ -47063,7 +48003,7 @@ function layoutMap(resolved, data, size, opts) {
47063
48003
  const regions = [];
47064
48004
  const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
47065
48005
  for (const [iso, f] of layerFeatures) {
47066
- if (layerKind === "us-state" && usContext && INSET_STATES.has(iso))
48006
+ if (layerKind === "us-state" && usContext && resolved.projection === "albers-usa" && INSET_STATES.has(iso))
47067
48007
  continue;
47068
48008
  if (layerKind === "country" && usContext && iso === "US") continue;
47069
48009
  if (layerKind === "country" && iso === "AQ" && !regionById.has("AQ"))
@@ -47075,7 +48015,8 @@ function layoutMap(resolved, data, size, opts) {
47075
48015
  if (!d) continue;
47076
48016
  const isThisLayer = r?.layer === layerKind;
47077
48017
  const isForeign = layerKind === "country" && usContext && iso !== "US";
47078
- let fill2 = isForeign ? foreignFill : neutralFill;
48018
+ const baseFill = isForeign ? foreignFill : neutralFill;
48019
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? baseFill : baseFill;
47079
48020
  let label;
47080
48021
  let lineNumber = -1;
47081
48022
  let layer = "base";
@@ -47084,12 +48025,14 @@ function layoutMap(resolved, data, size, opts) {
47084
48025
  lineNumber = r.lineNumber;
47085
48026
  layer = layerKind;
47086
48027
  label = r.name;
48028
+ } else {
48029
+ label = f.properties?.name;
47087
48030
  }
47088
48031
  regions.push({
47089
48032
  id: iso,
47090
48033
  d,
47091
48034
  fill: fill2,
47092
- stroke: regionStroke,
48035
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
47093
48036
  lineNumber,
47094
48037
  layer,
47095
48038
  ...label !== void 0 && { label },
@@ -47111,13 +48054,88 @@ function layoutMap(resolved, data, size, opts) {
47111
48054
  id: "lake",
47112
48055
  d,
47113
48056
  fill: water,
47114
- stroke: "none",
48057
+ stroke: lakeStroke,
47115
48058
  lineNumber: -1,
47116
48059
  layer: "base"
47117
48060
  });
47118
48061
  }
47119
48062
  }
47120
- const riverColor = water;
48063
+ const pointInRings = (px, py, rings) => {
48064
+ let inside = false;
48065
+ for (const ring of rings) {
48066
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
48067
+ const [xi, yi] = ring[i];
48068
+ const [xj, yj] = ring[j];
48069
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
48070
+ inside = !inside;
48071
+ }
48072
+ }
48073
+ return inside;
48074
+ };
48075
+ const fillHitTargets = [...regions, ...insetRegions].map((r) => ({
48076
+ fill: r.fill,
48077
+ rings: parsePathRings(r.d)
48078
+ }));
48079
+ const fillAt = (x, y) => {
48080
+ let hit = water;
48081
+ for (const t of fillHitTargets)
48082
+ if (pointInRings(x, y, t.rings)) hit = t.fill;
48083
+ return hit;
48084
+ };
48085
+ const labelOnFill = (fill2) => {
48086
+ const color = contrastRatio(fill2, palette.textOnFillDark) >= contrastRatio(fill2, palette.textOnFillLight) ? palette.textOnFillDark : palette.textOnFillLight;
48087
+ const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
48088
+ return {
48089
+ color,
48090
+ halo: contrastRatio(fill2, color) < REGION_LABEL_HALO_RATIO,
48091
+ haloColor
48092
+ };
48093
+ };
48094
+ const reliefAllowed = resolved.directives.noRelief !== true;
48095
+ const relief = [];
48096
+ let reliefHatch = null;
48097
+ if (reliefAllowed && data.mountainRanges) {
48098
+ for (const [, f] of decodeLayer(data.mountainRanges)) {
48099
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
48100
+ if (!viewF) continue;
48101
+ const area2 = path.area(viewF);
48102
+ if (!Number.isFinite(area2) || area2 < RELIEF_MIN_AREA) continue;
48103
+ const box = path.bounds(viewF);
48104
+ if (box[1][0] - box[0][0] < RELIEF_MIN_DIM || box[1][1] - box[0][1] < RELIEF_MIN_DIM)
48105
+ continue;
48106
+ const d = path(viewF) ?? "";
48107
+ if (!d) continue;
48108
+ relief.push({ d });
48109
+ }
48110
+ if (relief.length) {
48111
+ const darkTone = isDark ? palette.bg : palette.text;
48112
+ const lightTone = isDark ? palette.text : palette.bg;
48113
+ const reliefLandRef = colorizeActive ? isDark ? palette.surface : palette.bg : neutralFill;
48114
+ const landLum = relativeLuminance(reliefLandRef);
48115
+ const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
48116
+ reliefHatch = {
48117
+ color: mix(tone, reliefLandRef, RELIEF_HATCH_STRENGTH),
48118
+ spacing: RELIEF_HATCH_SPACING,
48119
+ width: RELIEF_HATCH_WIDTH
48120
+ };
48121
+ }
48122
+ }
48123
+ let coastlineStyle = null;
48124
+ if (resolved.directives.noCoastline !== true) {
48125
+ const minDim = Math.min(width, height);
48126
+ coastlineStyle = {
48127
+ color: mix(regionStroke, water, COASTLINE_STROKE_MIX),
48128
+ // N equal-width rings: distance steps outward by COASTLINE_STEP; opacity
48129
+ // fades linearly from NEAR (innermost) to FAR (outermost).
48130
+ lines: Array.from({ length: COASTLINE_RING_COUNT }, (_, k) => ({
48131
+ d: (COASTLINE_D0 + k * COASTLINE_STEP) * minDim,
48132
+ thickness: COASTLINE_THICKNESS * minDim,
48133
+ opacity: COASTLINE_OPACITY_NEAR + (COASTLINE_OPACITY_FAR - COASTLINE_OPACITY_NEAR) * k / (COASTLINE_RING_COUNT - 1)
48134
+ })),
48135
+ minExtent: (isGlobalView ? COASTLINE_MIN_EXTENT_GLOBAL : COASTLINE_MIN_EXTENT) * minDim
48136
+ };
48137
+ }
48138
+ const riverColor = mix(palette.colors.blue, water, 32);
47121
48139
  const rivers = [];
47122
48140
  if (data.rivers) {
47123
48141
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -47138,6 +48156,9 @@ function layoutMap(resolved, data, size, opts) {
47138
48156
  return R_MIN + Math.max(0, Math.min(1, t)) * (R_MAX - R_MIN);
47139
48157
  };
47140
48158
  const poiFill = (p) => {
48159
+ const directHex = p.color ? resolveColor(p.color, palette) : null;
48160
+ if (directHex)
48161
+ return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
47141
48162
  for (const group of resolved.tagGroups) {
47142
48163
  const val = p.tags[group.name.toLowerCase()];
47143
48164
  if (!val) continue;
@@ -47170,38 +48191,108 @@ function layoutMap(resolved, data, size, opts) {
47170
48191
  const xy = project(p.lon, p.lat);
47171
48192
  if (xy) projected.push({ p, xy });
47172
48193
  }
47173
- const coloGroups = /* @__PURE__ */ new Map();
48194
+ const placePoi = (e, cx, cy, clusterId) => {
48195
+ const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
48196
+ poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
48197
+ const num = routeNumberById.get(e.p.id);
48198
+ pois.push({
48199
+ id: e.p.id,
48200
+ cx,
48201
+ cy,
48202
+ r: radiusFor(e.p),
48203
+ fill: fill2,
48204
+ stroke: stroke2,
48205
+ lineNumber: e.p.lineNumber,
48206
+ implicit: !!e.p.implicit,
48207
+ isOrigin: originIds.has(e.p.id),
48208
+ ...num !== void 0 && { routeNumber: num },
48209
+ ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags },
48210
+ ...clusterId !== void 0 && { clusterId }
48211
+ });
48212
+ };
48213
+ const clusters = [];
48214
+ const connected = /* @__PURE__ */ new Set();
48215
+ for (const e of resolved.edges) {
48216
+ connected.add(e.fromId);
48217
+ connected.add(e.toId);
48218
+ }
48219
+ for (const rt of resolved.routes) {
48220
+ rt.stopIds.forEach((id) => connected.add(id));
48221
+ }
48222
+ const radiusOf = (e) => radiusFor(e.p);
47174
48223
  for (const e of projected) {
47175
- const key = `${Math.round(e.xy[0] / COLO_EPS)},${Math.round(e.xy[1] / COLO_EPS)}`;
47176
- const arr = coloGroups.get(key);
47177
- if (arr) arr.push(e);
47178
- else coloGroups.set(key, [e]);
47179
- }
47180
- for (const group of coloGroups.values()) {
47181
- group.forEach((e, i) => {
47182
- let cx = e.xy[0];
47183
- let cy = e.xy[1];
47184
- if (group.length > 1) {
47185
- const ang = i * GOLDEN_ANGLE;
47186
- cx += Math.cos(ang) * COLO_R;
47187
- cy += Math.sin(ang) * COLO_R;
47188
- }
47189
- const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
47190
- poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
47191
- const num = routeNumberById.get(e.p.id);
47192
- pois.push({
47193
- id: e.p.id,
47194
- cx,
47195
- cy,
47196
- r: radiusFor(e.p),
47197
- fill: fill2,
47198
- stroke: stroke2,
47199
- lineNumber: e.p.lineNumber,
47200
- implicit: !!e.p.implicit,
47201
- isOrigin: originIds.has(e.p.id),
47202
- ...num !== void 0 && { routeNumber: num },
47203
- ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags }
47204
- });
48224
+ if (connected.has(e.p.id)) placePoi(e, e.xy[0], e.xy[1]);
48225
+ }
48226
+ const groups = [];
48227
+ for (const e of projected) {
48228
+ if (connected.has(e.p.id)) continue;
48229
+ const r = radiusOf(e);
48230
+ const near = groups.find(
48231
+ (g) => g.some(
48232
+ (q) => Math.hypot(q.xy[0] - e.xy[0], q.xy[1] - e.xy[1]) < (r + radiusOf(q)) * STACK_OVERLAP
48233
+ )
48234
+ );
48235
+ if (near) near.push(e);
48236
+ else groups.push([e]);
48237
+ }
48238
+ for (const g of groups) {
48239
+ if (g.length === 1) {
48240
+ placePoi(g[0], g[0].xy[0], g[0].xy[1]);
48241
+ continue;
48242
+ }
48243
+ const clusterId = g[0].p.id;
48244
+ const cx0 = g.reduce((s, e) => s + e.xy[0], 0) / g.length;
48245
+ const cy0 = g.reduce((s, e) => s + e.xy[1], 0) / g.length;
48246
+ const maxR = Math.max(...g.map(radiusOf));
48247
+ const sep = 2 * maxR + STACK_RING_GAP;
48248
+ const ringR = Math.max(
48249
+ COLO_R,
48250
+ sep / (2 * Math.sin(Math.PI / Math.max(g.length, 2)))
48251
+ );
48252
+ const positions = g.map((e, i) => {
48253
+ if (g.length <= STACK_RING_MAX) {
48254
+ const ang2 = -Math.PI / 2 + i * 2 * Math.PI / g.length;
48255
+ return {
48256
+ e,
48257
+ mx: cx0 + Math.cos(ang2) * ringR,
48258
+ my: cy0 + Math.sin(ang2) * ringR
48259
+ };
48260
+ }
48261
+ const ang = i * GOLDEN_ANGLE;
48262
+ const rr = ringR * Math.sqrt((i + 1) / g.length);
48263
+ return { e, mx: cx0 + Math.cos(ang) * rr, my: cy0 + Math.sin(ang) * rr };
48264
+ });
48265
+ let minX = cx0 - maxR;
48266
+ let maxX = cx0 + maxR;
48267
+ let minY = cy0 - maxR;
48268
+ let maxY = cy0 + maxR;
48269
+ for (const { mx, my, e } of positions) {
48270
+ const r = radiusOf(e);
48271
+ minX = Math.min(minX, mx - r);
48272
+ maxX = Math.max(maxX, mx + r);
48273
+ minY = Math.min(minY, my - r);
48274
+ maxY = Math.max(maxY, my + r);
48275
+ }
48276
+ let dx = 0;
48277
+ let dy = 0;
48278
+ if (minX + dx < 2) dx = 2 - minX;
48279
+ if (maxX + dx > width - 2) dx = width - 2 - maxX;
48280
+ if (minY + dy < 2) dy = 2 - minY;
48281
+ if (maxY + dy > height - 2) dy = height - 2 - maxY;
48282
+ const legsOut = [];
48283
+ for (const { e, mx, my } of positions) {
48284
+ const fx = mx + dx;
48285
+ const fy = my + dy;
48286
+ placePoi(e, fx, fy, clusterId);
48287
+ legsOut.push({ x2: fx, y2: fy, color: poiFill(e.p).fill });
48288
+ }
48289
+ clusters.push({
48290
+ id: clusterId,
48291
+ cx: cx0 + dx,
48292
+ cy: cy0 + dy,
48293
+ count: g.length,
48294
+ hitR: ringR + maxR + 6,
48295
+ legs: legsOut
47205
48296
  });
47206
48297
  }
47207
48298
  const legs = [];
@@ -47251,16 +48342,26 @@ function layoutMap(resolved, data, size, opts) {
47251
48342
  if (!a || !b) continue;
47252
48343
  const mx = (a.cx + b.cx) / 2;
47253
48344
  const my = (a.cy + b.cy) / 2;
48345
+ const bow = {
48346
+ curved: leg.style === "arc",
48347
+ offset: 0,
48348
+ labelX: mx,
48349
+ labelY: my - 4
48350
+ };
48351
+ const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
47254
48352
  legs.push({
47255
- d: legPath(a, b, leg.style === "arc", 0),
48353
+ d: legPath(a, b, bow.curved, bow.offset),
47256
48354
  width: routeWidthFor(Number(leg.value)),
47257
48355
  color: mix(palette.text, palette.bg, 72),
47258
48356
  arrow: true,
47259
48357
  lineNumber: leg.lineNumber,
47260
48358
  ...leg.label !== void 0 && {
47261
48359
  label: leg.label,
47262
- labelX: mx,
47263
- labelY: my - 4
48360
+ labelX: bow.labelX,
48361
+ labelY: bow.labelY,
48362
+ labelColor: routeLabelStyle.color,
48363
+ labelHalo: routeLabelStyle.halo,
48364
+ labelHaloColor: routeLabelStyle.haloColor
47264
48365
  }
47265
48366
  });
47266
48367
  }
@@ -47288,20 +48389,29 @@ function layoutMap(resolved, data, size, opts) {
47288
48389
  const a = poiScreen.get(e.fromId);
47289
48390
  const b = poiScreen.get(e.toId);
47290
48391
  if (!a || !b) return;
47291
- const curved = e.style === "arc" || n > 1;
47292
- const offset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
48392
+ const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
47293
48393
  const mx = (a.cx + b.cx) / 2;
47294
48394
  const my = (a.cy + b.cy) / 2;
48395
+ const bow = {
48396
+ curved: e.style === "arc" || n > 1,
48397
+ offset: fanOffset,
48398
+ labelX: mx,
48399
+ labelY: my - 4
48400
+ };
48401
+ const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
47295
48402
  legs.push({
47296
- d: legPath(a, b, curved, offset),
48403
+ d: legPath(a, b, bow.curved, bow.offset),
47297
48404
  width: widthFor(e),
47298
48405
  color: mix(palette.text, palette.bg, 66),
47299
48406
  arrow: e.directed,
47300
48407
  lineNumber: e.lineNumber,
47301
48408
  ...e.label !== void 0 && {
47302
48409
  label: e.label,
47303
- labelX: mx,
47304
- labelY: my - 4
48410
+ labelX: bow.labelX,
48411
+ labelY: bow.labelY,
48412
+ labelColor: edgeLabelStyle.color,
48413
+ labelHalo: edgeLabelStyle.halo,
48414
+ labelHaloColor: edgeLabelStyle.haloColor
47305
48415
  }
47306
48416
  });
47307
48417
  });
@@ -47343,25 +48453,25 @@ function layoutMap(resolved, data, size, opts) {
47343
48453
  }
47344
48454
  }
47345
48455
  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));
47346
- const regionLabelMode = resolved.directives.regionLabels ?? "off";
48456
+ const showRegionLabels = resolved.directives.noRegionLabels !== true;
48457
+ const isCompact = width < COMPACT_WIDTH_PX;
47347
48458
  const LABEL_PADX = 6;
47348
48459
  const LABEL_PADY = 3;
47349
- const labelW = (text) => measureLegendText(text, FONT) + 2 * LABEL_PADX;
47350
- const labelH = FONT + 2 * LABEL_PADY;
48460
+ const labelW = (text) => measureLegendText(text, FONT2) + 2 * LABEL_PADX;
48461
+ const labelH = FONT2 + 2 * LABEL_PADY;
47351
48462
  const pushRegionLabel = (x, y, text, fill2, lineNumber) => {
47352
- const color = contrastText(
47353
- fill2,
47354
- palette.textOnFillLight,
47355
- palette.textOnFillDark
48463
+ const { color, haloColor } = labelOnFill(fill2);
48464
+ const halfW = measureLegendText(text, FONT2) / 2;
48465
+ const overflows = [y - FONT2 * 0.55, y - FONT2 * 0.1].some(
48466
+ (sy) => fillAt(x - halfW, sy) !== fill2 || fillAt(x + halfW, sy) !== fill2
47356
48467
  );
47357
- const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47358
48468
  labels.push({
47359
48469
  x,
47360
48470
  y,
47361
48471
  text,
47362
48472
  anchor: "middle",
47363
48473
  color,
47364
- halo: true,
48474
+ halo: overflows,
47365
48475
  haloColor,
47366
48476
  lineNumber
47367
48477
  });
@@ -47370,21 +48480,50 @@ function layoutMap(resolved, data, size, opts) {
47370
48480
  US: [-98.5, 39.5]
47371
48481
  // CONUS geographic centre (near Lebanon, Kansas)
47372
48482
  };
47373
- if (regionLabelMode === "full" || regionLabelMode === "abbrev") {
47374
- for (const r of regions) {
47375
- if (r.layer === "base" || r.label === void 0) continue;
47376
- const f = r.layer === "us-state" ? usLayer?.get(r.id) : worldLayer.get(r.id);
47377
- if (!f) continue;
48483
+ const REGION_LABEL_GAP = 2;
48484
+ const regionLabelRect = (cx, cy, text) => {
48485
+ const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
48486
+ return { x: cx - w / 2, y: cy - FONT2 / 2, w, h: FONT2 };
48487
+ };
48488
+ if (showRegionLabels) {
48489
+ const frameContainers = new Set(resolved.poiFrameContainers);
48490
+ const entries = regions.map((r) => {
48491
+ const isContainer = frameContainers.has(r.id);
48492
+ if (r.layer === "base" && !isContainer || r.label === void 0)
48493
+ return null;
48494
+ const isUsState = r.layer === "us-state" || r.id.startsWith("US-");
48495
+ const f = isUsState ? usLayer?.get(r.id) : worldLayer.get(r.id);
48496
+ if (!f) return null;
47378
48497
  const [[x0, y0], [x1, y1]] = path.bounds(f);
47379
- const text = regionLabelMode === "abbrev" ? r.id.replace(/^US-/, "") : r.label;
47380
- if (labelW(text) > x1 - x0 || labelH > y1 - y0) continue;
47381
- const anchor = r.layer !== "us-state" ? WORLD_LABEL_ANCHORS[r.id] : void 0;
48498
+ const boxW = x1 - x0;
48499
+ const boxH = y1 - y0;
48500
+ const abbrev = isUsState ? r.id.replace(/^US-/, "") : void 0;
48501
+ const candidates = abbrev !== void 0 ? isCompact ? [abbrev, r.label] : [r.label, abbrev] : [r.label];
48502
+ const anchor = !isUsState ? WORLD_LABEL_ANCHORS[r.id] : void 0;
47382
48503
  const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
47383
- if (!c || !Number.isFinite(c[0])) continue;
48504
+ if (!c || !Number.isFinite(c[0])) return null;
48505
+ return { r, c, boxW, boxH, area: boxW * boxH, candidates };
48506
+ }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
48507
+ const placedRegionRects = [];
48508
+ const POI_LABEL_PAD = 14;
48509
+ const poiObstacles = pois.map((p) => ({
48510
+ x: p.cx - p.r - POI_LABEL_PAD,
48511
+ y: p.cy - p.r - POI_LABEL_PAD,
48512
+ w: 2 * (p.r + POI_LABEL_PAD),
48513
+ h: 2 * (p.r + POI_LABEL_PAD)
48514
+ }));
48515
+ for (const { r, c, boxW, boxH, candidates } of entries) {
48516
+ const text = candidates.find((t) => {
48517
+ if (labelW(t) > boxW || labelH > boxH) return false;
48518
+ const rect = regionLabelRect(c[0], c[1], t);
48519
+ return !placedRegionRects.some((p) => rectsOverlap(rect, p)) && !poiObstacles.some((o) => rectsOverlap(rect, o));
48520
+ });
48521
+ if (text === void 0) continue;
48522
+ placedRegionRects.push(regionLabelRect(c[0], c[1], text));
47384
48523
  pushRegionLabel(c[0], c[1], text, r.fill, r.lineNumber);
47385
48524
  }
47386
48525
  for (const seed of insetLabelSeeds) {
47387
- const text = regionLabelMode === "abbrev" ? seed.iso.replace(/^US-/, "") : seed.name;
48526
+ const text = isCompact ? seed.iso.replace(/^US-/, "") : seed.name;
47388
48527
  const src = regionById.get(seed.iso);
47389
48528
  pushRegionLabel(
47390
48529
  seed.x,
@@ -47395,22 +48534,26 @@ function layoutMap(resolved, data, size, opts) {
47395
48534
  );
47396
48535
  }
47397
48536
  }
47398
- const poiLabelMode = resolved.directives.poiLabels ?? "auto";
47399
- if (poiLabelMode !== "off") {
47400
- const ordered = [...pois].sort(
47401
- (a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1)
47402
- );
48537
+ if (resolved.directives.noPoiLabels !== true) {
48538
+ const ordered = [...pois].filter((p) => p.clusterId === void 0).sort((a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1));
47403
48539
  const poiById = new Map(resolved.pois.map((q) => [q.id, q]));
47404
48540
  const labelText = (p) => {
47405
48541
  const src = poiById.get(p.id);
47406
48542
  return src?.label ?? src?.name ?? p.id;
47407
48543
  };
47408
- const poiLabH = FONT * 1.25;
48544
+ const poiLabH = FONT2 * 1.25;
47409
48545
  const labelInfo = (p) => {
47410
48546
  const text = labelText(p);
47411
- return { text, w: measureLegendText(text, FONT) };
48547
+ return { text, w: measureLegendText(text, FONT2) };
47412
48548
  };
47413
48549
  const GAP = 3;
48550
+ const clusterMembersById = /* @__PURE__ */ new Map();
48551
+ for (const p of pois) {
48552
+ if (p.clusterId === void 0) continue;
48553
+ const arr = clusterMembersById.get(p.clusterId);
48554
+ if (arr) arr.push(p);
48555
+ else clusterMembersById.set(p.clusterId, [p]);
48556
+ }
47414
48557
  const inlineRect = (p, w, side) => {
47415
48558
  switch (side) {
47416
48559
  case "right":
@@ -47440,11 +48583,11 @@ function layoutMap(resolved, data, size, opts) {
47440
48583
  const x = side === "right" ? rect.x : side === "left" ? rect.x + w : p.cx;
47441
48584
  labels.push({
47442
48585
  x,
47443
- y: rect.y + poiLabH / 2 + FONT / 3,
48586
+ y: rect.y + poiLabH / 2 + FONT2 / 3,
47444
48587
  text,
47445
48588
  anchor,
47446
48589
  color: palette.text,
47447
- halo: true,
48590
+ halo: false,
47448
48591
  haloColor: palette.bg,
47449
48592
  poiId: p.id,
47450
48593
  lineNumber: p.lineNumber
@@ -47455,43 +48598,60 @@ function layoutMap(resolved, data, size, opts) {
47455
48598
  return rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect);
47456
48599
  };
47457
48600
  const GROUP_R = 30;
47458
- const groups = [];
48601
+ const groups2 = [];
47459
48602
  for (const p of ordered) {
47460
- const near = groups.find(
48603
+ const near = groups2.find(
47461
48604
  (g) => g.some((q) => Math.hypot(q.cx - p.cx, q.cy - p.cy) < GROUP_R)
47462
48605
  );
47463
48606
  if (near) near.push(p);
47464
- else groups.push([p]);
48607
+ else groups2.push([p]);
47465
48608
  }
47466
48609
  const ROW_GAP2 = 3;
47467
48610
  const step = poiLabH + ROW_GAP2;
47468
48611
  const COL_GAP = 16;
47469
- const placeColumn = (group) => {
47470
- const items = group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48612
+ const makeItems = (group) => group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48613
+ const columnRows = (items, side) => {
47471
48614
  const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
47472
48615
  const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
47473
- const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
47474
48616
  const maxW = Math.max(...items.map((o) => o.w));
47475
- const side = right + COL_GAP + maxW <= width - 2 ? "right" : "left";
47476
- const colX = side === "right" ? right + COL_GAP : left - COL_GAP;
48617
+ const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
48618
+ const colX = side === "right" ? Math.min(right + COL_GAP, width - 2 - maxW) : Math.max(left - COL_GAP, 2 + maxW);
47477
48619
  const totalH = items.length * step;
47478
48620
  let startY = cyMid - totalH / 2;
47479
48621
  startY = Math.max(2, Math.min(startY, height - totalH - 2));
47480
- items.forEach((o, i) => {
48622
+ return items.map((o, i) => {
47481
48623
  const rowCy = startY + i * step + step / 2;
47482
- obstacles.push({
47483
- x: side === "right" ? colX : colX - o.w,
47484
- y: rowCy - poiLabH / 2,
47485
- w: o.w,
47486
- h: poiLabH
47487
- });
48624
+ return {
48625
+ o,
48626
+ colX,
48627
+ rowCy,
48628
+ rect: {
48629
+ x: side === "right" ? colX : colX - o.w,
48630
+ y: rowCy - poiLabH / 2,
48631
+ w: o.w,
48632
+ h: poiLabH
48633
+ }
48634
+ };
48635
+ });
48636
+ };
48637
+ const wouldColumnBeClean = (items, side) => columnRows(items, side).every(
48638
+ ({ rect }) => rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect)
48639
+ );
48640
+ const defaultColumnSide = (items) => {
48641
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48642
+ const maxW = Math.max(...items.map((o) => o.w));
48643
+ return right + COL_GAP + maxW <= width - 2 ? "right" : "left";
48644
+ };
48645
+ const commitColumn = (items, side, clusterId) => {
48646
+ for (const { o, colX, rowCy, rect } of columnRows(items, side)) {
48647
+ obstacles.push(rect);
47488
48648
  labels.push({
47489
48649
  x: colX,
47490
- y: rowCy + FONT / 3,
48650
+ y: rowCy + FONT2 / 3,
47491
48651
  text: o.text,
47492
48652
  anchor: side === "right" ? "start" : "end",
47493
48653
  color: palette.text,
47494
- halo: true,
48654
+ halo: false,
47495
48655
  haloColor: palette.bg,
47496
48656
  leader: {
47497
48657
  x1: o.p.cx,
@@ -47501,24 +48661,141 @@ function layoutMap(resolved, data, size, opts) {
47501
48661
  },
47502
48662
  leaderColor: o.p.fill,
47503
48663
  poiId: o.p.id,
47504
- lineNumber: o.p.lineNumber
48664
+ lineNumber: o.p.lineNumber,
48665
+ ...clusterId !== void 0 && { clusterMember: clusterId }
47505
48666
  });
48667
+ }
48668
+ };
48669
+ const pushHidden = (p) => {
48670
+ const { text, w } = labelInfo(p);
48671
+ let x = p.cx + p.r + GAP;
48672
+ let anchor = "start";
48673
+ if (x + w > width) {
48674
+ x = p.cx - p.r - GAP - w;
48675
+ anchor = "end";
48676
+ }
48677
+ const y = Math.max(0, Math.min(p.cy - poiLabH / 2, height - poiLabH));
48678
+ labels.push({
48679
+ x: anchor === "start" ? x : x + w,
48680
+ y: y + poiLabH / 2 + FONT2 / 3,
48681
+ text,
48682
+ anchor,
48683
+ color: palette.text,
48684
+ halo: false,
48685
+ haloColor: palette.bg,
48686
+ poiId: p.id,
48687
+ hidden: true,
48688
+ lineNumber: p.lineNumber
47506
48689
  });
47507
48690
  };
47508
- for (const g of groups) {
48691
+ for (const [clusterId, members] of clusterMembersById) {
48692
+ if (members.length === 0) continue;
48693
+ const items = makeItems(members);
48694
+ const side = wouldColumnBeClean(items, "right") ? "right" : wouldColumnBeClean(items, "left") ? "left" : defaultColumnSide(items);
48695
+ commitColumn(items, side, clusterId);
48696
+ }
48697
+ const maxExtent = MAX_CLUSTER_EXTENT_FACTOR * Math.min(width, height);
48698
+ const clusterPending = [];
48699
+ for (const g of groups2) {
48700
+ const items = makeItems(g);
47509
48701
  if (g.length === 1) {
47510
- const p = g[0];
47511
- const { text, w } = labelInfo(p);
48702
+ const { p, text, w } = items[0];
47512
48703
  const side = ["right", "left", "above", "below"].find(
47513
48704
  (s) => inlineFits(p, w, s)
47514
48705
  );
47515
- if (side) {
47516
- pushInline(p, text, w, side);
47517
- continue;
48706
+ if (side) pushInline(p, text, w, side);
48707
+ else commitColumn(items, defaultColumnSide(items));
48708
+ continue;
48709
+ }
48710
+ const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
48711
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48712
+ const minCy = Math.min(...items.map((o) => o.p.cy));
48713
+ const maxCy = Math.max(...items.map((o) => o.p.cy));
48714
+ const diag = Math.hypot(right - left, maxCy - minCy);
48715
+ if (diag > maxExtent || items.length > MAX_COLUMN_ROWS) {
48716
+ items.forEach((o) => pushHidden(o.p));
48717
+ } else {
48718
+ clusterPending.push(items);
48719
+ }
48720
+ }
48721
+ for (const items of clusterPending) {
48722
+ const side = ["right", "left"].find(
48723
+ (s) => wouldColumnBeClean(items, s)
48724
+ );
48725
+ if (side) commitColumn(items, side);
48726
+ else items.forEach((o) => pushHidden(o.p));
48727
+ }
48728
+ }
48729
+ if (resolved.directives.noContextLabels !== true) {
48730
+ for (const l of labels) {
48731
+ if (l.hidden) continue;
48732
+ const w = labelW(l.text);
48733
+ const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
48734
+ obstacles.push({ x, y: l.y - labelH / 2, w, h: labelH });
48735
+ }
48736
+ for (const box of insets)
48737
+ obstacles.push({ x: box.x, y: box.y, w: box.w, h: box.h });
48738
+ const countryCandidates = [];
48739
+ for (const f of worldLayer.values()) {
48740
+ const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
48741
+ if (!iso || regionById.has(iso)) continue;
48742
+ let hasReferencedSub = false;
48743
+ for (const k of regionById.keys())
48744
+ if (k.startsWith(iso + "-")) {
48745
+ hasReferencedSub = true;
48746
+ break;
47518
48747
  }
48748
+ if (hasReferencedSub) continue;
48749
+ const b = path.bounds(f);
48750
+ const [x0, y0] = b[0];
48751
+ const [x1, y1] = b[1];
48752
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48753
+ const anchorLngLat = WORLD_LABEL_ANCHORS[iso];
48754
+ const a = anchorLngLat ? project(anchorLngLat[0], anchorLngLat[1]) : path.centroid(f);
48755
+ countryCandidates.push({
48756
+ name: f.properties?.name ?? iso,
48757
+ bbox: [x0, y0, x1, y1],
48758
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48759
+ });
48760
+ }
48761
+ const framedStateContainers = (resolved.poiFrameContainers ?? []).some(
48762
+ (id) => id.startsWith("US-")
48763
+ );
48764
+ if (usLayer && framedStateContainers) {
48765
+ const containerSet = new Set(resolved.poiFrameContainers);
48766
+ for (const [iso, f] of usLayer) {
48767
+ if (containerSet.has(iso) || regionById.has(iso)) continue;
48768
+ const viewF = cullFeatureToView(f);
48769
+ if (!viewF) continue;
48770
+ const b = path.bounds(viewF);
48771
+ const [x0, y0] = b[0];
48772
+ const [x1, y1] = b[1];
48773
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48774
+ const a = path.centroid(viewF);
48775
+ countryCandidates.push({
48776
+ name: f.properties?.name ?? iso,
48777
+ bbox: [x0, y0, x1, y1],
48778
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48779
+ });
47519
48780
  }
47520
- placeColumn(g);
47521
48781
  }
48782
+ const contextLabels = placeContextLabels({
48783
+ projection: resolved.projection,
48784
+ dLonSpan,
48785
+ dLatSpan,
48786
+ width,
48787
+ height,
48788
+ waterBodies: data.waterBodies,
48789
+ countries: countryCandidates,
48790
+ palette,
48791
+ project,
48792
+ collides,
48793
+ // Water labels must stay over open water — `fillAt` returns the ocean
48794
+ // backdrop colour off-land and a region fill on-land (lakes/states count
48795
+ // as land here, which is the safe side for an ocean name).
48796
+ overLand: (x, y) => fillAt(x, y) !== water
48797
+ });
48798
+ labels.push(...contextLabels);
47522
48799
  }
47523
48800
  let legend = null;
47524
48801
  if (!resolved.directives.noLegend) {
@@ -47553,22 +48830,33 @@ function layoutMap(resolved, data, size, opts) {
47553
48830
  ...resolved.caption !== void 0 && { caption: resolved.caption },
47554
48831
  regions,
47555
48832
  rivers,
48833
+ relief,
48834
+ reliefHatch,
48835
+ coastlineStyle,
47556
48836
  legs,
47557
48837
  pois,
48838
+ clusters,
47558
48839
  labels,
47559
48840
  legend,
47560
48841
  insets,
47561
- insetRegions
48842
+ insetRegions,
48843
+ projection,
48844
+ stretch: stretchParams,
48845
+ diagnostics: []
47562
48846
  };
47563
48847
  }
47564
- var FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, COLO_EPS, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT, RIVER_WIDTH, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_WATER_LIGHT, MUTED_WATER_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, MUTED_LAND_DARK, COLO_R, GOLDEN_ANGLE, FAN_STEP, ARC_CURVE_FRAC, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, US_NON_CONUS;
48848
+ var FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT2, MAX_CLUSTER_EXTENT_FACTOR, MAX_COLUMN_ROWS, REGION_LABEL_HALO_RATIO, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, COMPACT_WIDTH_PX, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, COASTLINE_RING_COUNT, COASTLINE_D0, COASTLINE_STEP, COASTLINE_THICKNESS, COASTLINE_OPACITY_NEAR, COASTLINE_OPACITY_FAR, COASTLINE_MIN_EXTENT, COASTLINE_MIN_EXTENT_GLOBAL, COASTLINE_STROKE_MIX, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, STACK_OVERLAP, STACK_RING_MAX, STACK_RING_GAP, FAN_STEP, ARC_CURVE_FRAC, decodeCache, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, inAlaska, inHawaii, FOREIGN_BORDER, US_NON_CONUS;
47565
48849
  var init_layout15 = __esm({
47566
48850
  "src/map/layout.ts"() {
47567
48851
  "use strict";
47568
48852
  init_color_utils();
48853
+ init_geo();
48854
+ init_colorize();
48855
+ init_colors();
47569
48856
  init_label_layout();
47570
48857
  init_legend_constants();
47571
48858
  init_title_constants();
48859
+ init_context_labels();
47572
48860
  FIT_PAD = 24;
47573
48861
  RAMP_FLOOR = 15;
47574
48862
  R_DEFAULT = 6;
@@ -47576,29 +48864,66 @@ var init_layout15 = __esm({
47576
48864
  R_MAX = 22;
47577
48865
  W_MIN = 1.25;
47578
48866
  W_MAX = 8;
47579
- FONT = 11;
47580
- COLO_EPS = 1.5;
47581
- LAND_TINT_LIGHT = 58;
47582
- LAND_TINT_DARK = 75;
48867
+ FONT2 = 11;
48868
+ MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48869
+ MAX_COLUMN_ROWS = 7;
48870
+ REGION_LABEL_HALO_RATIO = 4.5;
48871
+ LAND_TINT_LIGHT = 12;
48872
+ LAND_TINT_DARK = 24;
47583
48873
  TAG_TINT_LIGHT = 60;
47584
48874
  TAG_TINT_DARK = 68;
47585
- WATER_TINT = 55;
48875
+ WATER_TINT_LIGHT = 24;
48876
+ WATER_TINT_DARK = 24;
47586
48877
  RIVER_WIDTH = 1.3;
48878
+ COMPACT_WIDTH_PX = 480;
48879
+ RELIEF_MIN_AREA = 12;
48880
+ RELIEF_MIN_DIM = 2;
48881
+ RELIEF_HATCH_SPACING = 2;
48882
+ RELIEF_HATCH_WIDTH = 0.15;
48883
+ RELIEF_HATCH_STRENGTH = 32;
48884
+ COASTLINE_RING_COUNT = 5;
48885
+ COASTLINE_D0 = 16e-4;
48886
+ COASTLINE_STEP = 28e-4;
48887
+ COASTLINE_THICKNESS = 14e-4;
48888
+ COASTLINE_OPACITY_NEAR = 0.5;
48889
+ COASTLINE_OPACITY_FAR = 0.1;
48890
+ COASTLINE_MIN_EXTENT = 6e-4;
48891
+ COASTLINE_MIN_EXTENT_GLOBAL = 6e-4;
48892
+ COASTLINE_STROKE_MIX = 32;
47587
48893
  FOREIGN_TINT_LIGHT = 30;
47588
48894
  FOREIGN_TINT_DARK = 62;
47589
- MUTED_WATER_LIGHT = 14;
47590
- MUTED_WATER_DARK = 10;
47591
48895
  MUTED_FOREIGN_LIGHT = 28;
47592
48896
  MUTED_FOREIGN_DARK = 16;
47593
- MUTED_LAND_DARK = 24;
47594
48897
  COLO_R = 9;
47595
48898
  GOLDEN_ANGLE = 2.399963229728653;
48899
+ STACK_OVERLAP = 1;
48900
+ STACK_RING_MAX = 8;
48901
+ STACK_RING_GAP = 4;
47596
48902
  FAN_STEP = 16;
47597
48903
  ARC_CURVE_FRAC = 0.18;
48904
+ decodeCache = /* @__PURE__ */ new WeakMap();
47598
48905
  usConusProjection = () => geoConicEqualArea().parallels([29.5, 45.5]).rotate([96, 0]);
47599
48906
  alaskaProjection = () => geoConicEqualArea().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
47600
48907
  hawaiiProjection = () => geoMercator();
47601
48908
  INSET_STATES = /* @__PURE__ */ new Set(["US-AK", "US-HI"]);
48909
+ inAlaska = (lon, lat) => lat >= 51 && (lon <= -129 || lon >= 172);
48910
+ inHawaii = (lon, lat) => lat >= 18 && lat <= 23 && lon >= -161 && lon <= -154;
48911
+ FOREIGN_BORDER = {
48912
+ CA: [
48913
+ "US-AK",
48914
+ "US-WA",
48915
+ "US-ID",
48916
+ "US-MT",
48917
+ "US-ND",
48918
+ "US-MN",
48919
+ "US-MI",
48920
+ "US-NY",
48921
+ "US-VT",
48922
+ "US-NH",
48923
+ "US-ME"
48924
+ ],
48925
+ MX: ["US-CA", "US-AZ", "US-NM", "US-TX"]
48926
+ };
47602
48927
  US_NON_CONUS = /* @__PURE__ */ new Set([
47603
48928
  "US-AK",
47604
48929
  "US-HI",
@@ -47618,6 +48943,58 @@ __export(renderer_exports16, {
47618
48943
  renderMapForExport: () => renderMapForExport
47619
48944
  });
47620
48945
  import * as d3Selection18 from "d3-selection";
48946
+ function pointInRing2(px, py, ring) {
48947
+ let inside = false;
48948
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
48949
+ const [xi, yi] = ring[i];
48950
+ const [xj, yj] = ring[j];
48951
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
48952
+ inside = !inside;
48953
+ }
48954
+ return inside;
48955
+ }
48956
+ function ringToPath(ring) {
48957
+ let d = "";
48958
+ for (let i = 0; i < ring.length; i++)
48959
+ d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48960
+ return d + "Z";
48961
+ }
48962
+ function coastlineOuterRings(regions, minExtent) {
48963
+ const paths = [];
48964
+ for (const r of regions) {
48965
+ const rings = parsePathRings(r.d);
48966
+ for (let i = 0; i < rings.length; i++) {
48967
+ const ring = rings[i];
48968
+ if (ring.length < 3) continue;
48969
+ let minX = Infinity;
48970
+ let minY = Infinity;
48971
+ let maxX = -Infinity;
48972
+ let maxY = -Infinity;
48973
+ for (const [x, y] of ring) {
48974
+ if (x < minX) minX = x;
48975
+ if (x > maxX) maxX = x;
48976
+ if (y < minY) minY = y;
48977
+ if (y > maxY) maxY = y;
48978
+ }
48979
+ if (Math.max(maxX - minX, maxY - minY) < minExtent) continue;
48980
+ const [fx, fy] = ring[0];
48981
+ let depth = 0;
48982
+ for (let j = 0; j < rings.length; j++)
48983
+ if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48984
+ if (depth % 2 === 1) continue;
48985
+ paths.push(ringToPath(ring));
48986
+ }
48987
+ }
48988
+ return paths;
48989
+ }
48990
+ function appendWaterLines(g, outerRings, style, flatWater) {
48991
+ const d = outerRings.join(" ");
48992
+ const linesOuterFirst = [...style.lines].sort((a, b) => b.d - a.d);
48993
+ for (const line12 of linesOuterFirst) {
48994
+ 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");
48995
+ g.append("path").attr("d", d).attr("stroke", flatWater).attr("stroke-width", 2 * line12.d).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
48996
+ }
48997
+ }
47621
48998
  function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims, activeGroupOverride) {
47622
48999
  d3Selection18.select(container).selectAll(":not([data-d3-tooltip])").remove();
47623
49000
  const width = exportDims?.width ?? container.clientWidth;
@@ -47630,6 +49007,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47630
49007
  {
47631
49008
  palette,
47632
49009
  isDark,
49010
+ // Export-only: forward the contain-fit request from mapExportDimensions so a
49011
+ // clamped/floored (off-aspect) export canvas letterboxes instead of
49012
+ // stretch-distorting. The in-app preview pane passes no exportDims → unset →
49013
+ // keeps the global stretch-fill.
49014
+ preferContain: exportDims?.preferContain ?? false,
47633
49015
  ...activeGroupOverride !== void 0 && {
47634
49016
  activeGroup: activeGroupOverride
47635
49017
  }
@@ -47643,6 +49025,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47643
49025
  const gRegions = svg.append("g").attr("class", "dgmo-map-regions");
47644
49026
  const drawRegion = (g, r, strokeWidth) => {
47645
49027
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
49028
+ if (r.label) p.attr("data-region-name", r.label);
47646
49029
  if (r.layer !== "base") {
47647
49030
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
47648
49031
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -47663,6 +49046,52 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47663
49046
  }
47664
49047
  };
47665
49048
  for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
49049
+ if (layout.relief.length && layout.reliefHatch) {
49050
+ const h = layout.reliefHatch;
49051
+ const rangeClipId = "dgmo-relief-clip";
49052
+ const landClipId = "dgmo-relief-land";
49053
+ const rangeClip = defs.append("clipPath").attr("id", rangeClipId);
49054
+ for (const s of layout.relief) rangeClip.append("path").attr("d", s.d);
49055
+ const landClip = defs.append("clipPath").attr("id", landClipId);
49056
+ for (const r of layout.regions)
49057
+ if (r.id !== "lake") landClip.append("path").attr("d", r.d);
49058
+ 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");
49059
+ for (let y = h.spacing; y < height; y += h.spacing) {
49060
+ gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
49061
+ }
49062
+ }
49063
+ if (layout.coastlineStyle) {
49064
+ const cs = layout.coastlineStyle;
49065
+ const maskId = "dgmo-map-water-mask";
49066
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
49067
+ mask.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "white");
49068
+ const landD = layout.regions.filter((r) => r.id !== "lake").map((r) => r.d).join(" ");
49069
+ const lakeD = layout.regions.filter((r) => r.id === "lake").map((r) => r.d).join(" ");
49070
+ if (landD) mask.append("path").attr("d", landD).attr("fill", "black");
49071
+ if (lakeD) mask.append("path").attr("d", lakeD).attr("fill", "white");
49072
+ if (layout.insets.length) {
49073
+ const reach = Math.max(0, ...cs.lines.map((l) => l.d + l.thickness));
49074
+ for (const box of layout.insets) {
49075
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
49076
+ mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
49077
+ }
49078
+ }
49079
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
49080
+ appendWaterLines(
49081
+ gWater,
49082
+ coastlineOuterRings(layout.regions, cs.minExtent),
49083
+ cs,
49084
+ layout.background
49085
+ );
49086
+ const byStroke = /* @__PURE__ */ new Map();
49087
+ for (const r of layout.regions) {
49088
+ const arr = byStroke.get(r.stroke);
49089
+ if (arr) arr.push(r.d);
49090
+ else byStroke.set(r.stroke, [r.d]);
49091
+ }
49092
+ for (const [stroke2, ds] of byStroke)
49093
+ gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
49094
+ }
47666
49095
  if (layout.rivers.length) {
47667
49096
  const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47668
49097
  for (const r of layout.rivers) {
@@ -47671,15 +49100,61 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47671
49100
  }
47672
49101
  if (layout.insets.length) {
47673
49102
  const insetG = svg.append("g").attr("class", "dgmo-map-insets");
47674
- for (const box of layout.insets) {
49103
+ layout.insets.forEach((box, bi) => {
47675
49104
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
47676
49105
  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");
47677
- }
49106
+ if (box.contextLand) {
49107
+ const clipId = `dgmo-map-inset-clip-${bi}`;
49108
+ defs.append("clipPath").attr("id", clipId).append("path").attr("d", d);
49109
+ insetG.append("path").attr("d", box.contextLand.d).attr("fill", box.contextLand.fill).attr("clip-path", `url(#${clipId})`);
49110
+ }
49111
+ });
47678
49112
  for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
47679
- }
49113
+ if (layout.coastlineStyle) {
49114
+ const cs = layout.coastlineStyle;
49115
+ const maskId = "dgmo-map-inset-water-mask";
49116
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
49117
+ for (const box of layout.insets) {
49118
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
49119
+ mask.append("path").attr("d", d).attr("fill", "white");
49120
+ }
49121
+ layout.insets.forEach((box, bi) => {
49122
+ if (box.contextLand)
49123
+ mask.append("path").attr("d", box.contextLand.d).attr("fill", "black").attr("clip-path", `url(#dgmo-map-inset-clip-${bi})`);
49124
+ });
49125
+ for (const r of layout.insetRegions)
49126
+ if (r.id !== "lake")
49127
+ mask.append("path").attr("d", r.d).attr("fill", "black");
49128
+ for (const r of layout.insetRegions)
49129
+ if (r.id === "lake")
49130
+ mask.append("path").attr("d", r.d).attr("fill", "white");
49131
+ const clipId = "dgmo-map-inset-water-clip";
49132
+ const clip = defs.append("clipPath").attr("id", clipId);
49133
+ for (const box of layout.insets) {
49134
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
49135
+ clip.append("path").attr("d", d);
49136
+ }
49137
+ const gInsetWater = insetG.append("g").attr("clip-path", `url(#${clipId})`).append("g").attr("class", "dgmo-map-inset-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
49138
+ appendWaterLines(
49139
+ gInsetWater,
49140
+ coastlineOuterRings(layout.insetRegions, cs.minExtent),
49141
+ cs,
49142
+ layout.background
49143
+ );
49144
+ for (const r of layout.insetRegions)
49145
+ gInsetWater.append("path").attr("d", r.d).attr("stroke", r.stroke).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
49146
+ }
49147
+ }
49148
+ const wireSync = (sel, lineNumber) => {
49149
+ if (lineNumber < 1) return;
49150
+ sel.attr("data-line-number", lineNumber);
49151
+ if (onClickItem)
49152
+ sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
49153
+ };
47680
49154
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
47681
49155
  layout.legs.forEach((leg, i) => {
47682
49156
  const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round");
49157
+ wireSync(p, leg.lineNumber);
47683
49158
  if (leg.arrow) {
47684
49159
  const id = `dgmo-map-arrow-${i}`;
47685
49160
  const s = arrowSize(leg.width);
@@ -47687,25 +49162,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47687
49162
  p.attr("marker-end", `url(#${id})`);
47688
49163
  }
47689
49164
  if (leg.label !== void 0 && leg.labelX !== void 0) {
47690
- emitText(
49165
+ const lt = emitText(
47691
49166
  gLegs,
47692
49167
  leg.labelX,
47693
49168
  leg.labelY ?? 0,
47694
49169
  leg.label,
47695
49170
  "middle",
47696
- palette.textMuted,
47697
- haloColor,
47698
- true,
49171
+ leg.labelColor ?? palette.textMuted,
49172
+ leg.labelHaloColor ?? haloColor,
49173
+ leg.labelHalo ?? true,
47699
49174
  LABEL_FONT - 1
47700
49175
  );
49176
+ wireSync(lt, leg.lineNumber);
47701
49177
  }
47702
49178
  });
49179
+ const gSpider = svg.append("g").attr("class", "dgmo-map-spider");
49180
+ for (const cl of layout.clusters) {
49181
+ if (!exportDims) {
49182
+ 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");
49183
+ }
49184
+ for (const leg of cl.legs) {
49185
+ 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");
49186
+ }
49187
+ 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");
49188
+ }
47703
49189
  const gPois = svg.append("g").attr("class", "dgmo-map-pois");
47704
49190
  for (const poi of layout.pois) {
47705
49191
  if (poi.isOrigin) {
47706
49192
  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);
47707
49193
  }
47708
49194
  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);
49195
+ if (poi.clusterId !== void 0)
49196
+ c.attr("data-cluster-member", poi.clusterId);
47709
49197
  if (poi.tags) {
47710
49198
  for (const [group, value] of Object.entries(poi.tags)) {
47711
49199
  c.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
@@ -47733,12 +49221,32 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47733
49221
  }
47734
49222
  const gLabels = svg.append("g").attr("class", "dgmo-map-labels");
47735
49223
  for (const lab of layout.labels) {
49224
+ if (lab.hidden) {
49225
+ if (exportDims) continue;
49226
+ emitText(
49227
+ gLabels,
49228
+ lab.x,
49229
+ lab.y,
49230
+ lab.text,
49231
+ lab.anchor,
49232
+ lab.color,
49233
+ lab.haloColor,
49234
+ lab.halo,
49235
+ LABEL_FONT,
49236
+ lab.italic,
49237
+ lab.letterSpacing
49238
+ ).attr("data-poi", lab.poiId ?? null).attr("data-poi-hidden", "").style("opacity", 0).style("pointer-events", "none");
49239
+ continue;
49240
+ }
47736
49241
  if (lab.leader) {
47737
49242
  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(
47738
49243
  "stroke",
47739
49244
  lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
47740
49245
  ).attr("stroke-width", lab.leaderColor ? 1 : 0.75);
47741
49246
  if (lab.poiId !== void 0) line12.attr("data-poi", lab.poiId);
49247
+ if (lab.clusterMember !== void 0)
49248
+ line12.attr("data-cluster-member", lab.clusterMember);
49249
+ wireSync(line12, lab.lineNumber);
47742
49250
  }
47743
49251
  const t = emitText(
47744
49252
  gLabels,
@@ -47749,11 +49257,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47749
49257
  lab.color,
47750
49258
  lab.haloColor,
47751
49259
  lab.halo,
47752
- LABEL_FONT
49260
+ LABEL_FONT,
49261
+ lab.italic,
49262
+ lab.letterSpacing,
49263
+ lab.lines
47753
49264
  );
47754
49265
  if (lab.poiId !== void 0) {
47755
49266
  t.attr("data-poi", lab.poiId).style("cursor", "default");
47756
49267
  }
49268
+ if (lab.clusterMember !== void 0) {
49269
+ t.attr("data-cluster-member", lab.clusterMember);
49270
+ }
49271
+ wireSync(t, lab.lineNumber);
49272
+ }
49273
+ if (!exportDims && layout.clusters.length) {
49274
+ const gBadge = svg.append("g").attr("class", "dgmo-map-cluster-badges");
49275
+ for (const cl of layout.clusters) {
49276
+ const g = gBadge.append("g").attr("data-cluster", cl.id).style("opacity", 0).style("pointer-events", "none");
49277
+ const R = 9;
49278
+ 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);
49279
+ 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);
49280
+ emitText(
49281
+ g,
49282
+ cl.cx,
49283
+ cl.cy + 3,
49284
+ String(cl.count),
49285
+ "middle",
49286
+ palette.text,
49287
+ palette.bg,
49288
+ false,
49289
+ LABEL_FONT
49290
+ );
49291
+ }
47757
49292
  }
47758
49293
  if (layout.legend) {
47759
49294
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
@@ -47787,10 +49322,10 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47787
49322
  }
47788
49323
  }
47789
49324
  if (layout.title) {
47790
- svg.append("text").attr("x", width / 2).attr("y", TITLE_Y).attr("text-anchor", "middle").attr("font-size", TITLE_FONT_SIZE).attr("font-weight", TITLE_FONT_WEIGHT).attr("fill", palette.text).attr("paint-order", "stroke fill").attr("stroke", palette.bg).attr("stroke-width", 4).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7).text(layout.title);
49325
+ 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);
47791
49326
  }
47792
49327
  if (layout.subtitle) {
47793
- 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);
49328
+ 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);
47794
49329
  }
47795
49330
  if (layout.caption) {
47796
49331
  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);
@@ -47799,10 +49334,21 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47799
49334
  function renderMapForExport(container, resolved, data, palette, isDark, exportDims) {
47800
49335
  renderMap(container, resolved, data, palette, isDark, void 0, exportDims);
47801
49336
  }
47802
- function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize) {
47803
- const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color).text(text);
49337
+ function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize, italic, letterSpacing, lines) {
49338
+ const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color);
49339
+ if (lines && lines.length > 1) {
49340
+ const lineHeight = fontSize + 2;
49341
+ const startDy = -((lines.length - 1) / 2) * lineHeight;
49342
+ lines.forEach((ln, i) => {
49343
+ t.append("tspan").attr("x", x).attr("dy", i === 0 ? startDy : lineHeight).text(ln);
49344
+ });
49345
+ } else {
49346
+ t.text(text);
49347
+ }
49348
+ if (italic) t.attr("font-style", "italic");
49349
+ if (letterSpacing) t.attr("letter-spacing", letterSpacing);
47804
49350
  if (withHalo) {
47805
- t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7);
49351
+ 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);
47806
49352
  }
47807
49353
  return t;
47808
49354
  }
@@ -47819,6 +49365,178 @@ var init_renderer16 = __esm({
47819
49365
  }
47820
49366
  });
47821
49367
 
49368
+ // src/map/dimensions.ts
49369
+ var dimensions_exports = {};
49370
+ __export(dimensions_exports, {
49371
+ mapContentAspect: () => mapContentAspect,
49372
+ mapExportDimensions: () => mapExportDimensions
49373
+ });
49374
+ import { geoPath as geoPath2 } from "d3-geo";
49375
+ function mapContentAspect(resolved, data, ref = REF) {
49376
+ const { projection, fitTarget } = buildMapProjection(resolved, data);
49377
+ projection.fitSize([ref, ref], fitTarget);
49378
+ const b = geoPath2(projection).bounds(fitTarget);
49379
+ const w = b[1][0] - b[0][0];
49380
+ const h = b[1][1] - b[0][1];
49381
+ const aspect = w / h;
49382
+ return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49383
+ }
49384
+ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49385
+ const raw = mapContentAspect(resolved, data);
49386
+ const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49387
+ const width = baseWidth;
49388
+ let height = Math.round(width / clamped);
49389
+ let chromeReserve = 0;
49390
+ if (resolved.title && resolved.pois.length > 0) {
49391
+ const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
49392
+ chromeReserve += Math.max(FIT_PAD2, bannerBottom + TITLE_GAP) - FIT_PAD2;
49393
+ }
49394
+ let floored = false;
49395
+ if (height - chromeReserve < MIN_MAP_BAND) {
49396
+ height = Math.round(chromeReserve + MIN_MAP_BAND);
49397
+ floored = true;
49398
+ }
49399
+ const preferContain = clamped !== raw || floored;
49400
+ return { width, height, preferContain };
49401
+ }
49402
+ var FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
49403
+ var init_dimensions = __esm({
49404
+ "src/map/dimensions.ts"() {
49405
+ "use strict";
49406
+ init_title_constants();
49407
+ init_layout15();
49408
+ FIT_PAD2 = 24;
49409
+ TITLE_GAP = 16;
49410
+ ASPECT_MAX = 3;
49411
+ ASPECT_MIN = 0.9;
49412
+ MIN_MAP_BAND = 200;
49413
+ FALLBACK_ASPECT = 1.5;
49414
+ REF = 1e3;
49415
+ }
49416
+ });
49417
+
49418
+ // src/map/load-data.ts
49419
+ var load_data_exports = {};
49420
+ __export(load_data_exports, {
49421
+ loadMapData: () => loadMapData
49422
+ });
49423
+ async function loadNodeBuiltins() {
49424
+ const [{ readFile }, { fileURLToPath }, { dirname: dirname2, resolve }] = await Promise.all([
49425
+ import("fs/promises"),
49426
+ import("url"),
49427
+ import("path")
49428
+ ]);
49429
+ return { readFile, fileURLToPath, dirname: dirname2, resolve };
49430
+ }
49431
+ async function readJson(nb, dir, name) {
49432
+ return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
49433
+ }
49434
+ async function firstExistingDir(nb, baseDir) {
49435
+ for (const rel of CANDIDATE_DIRS) {
49436
+ const dir = nb.resolve(baseDir, rel);
49437
+ try {
49438
+ await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
49439
+ return dir;
49440
+ } catch {
49441
+ }
49442
+ }
49443
+ throw new Error(
49444
+ `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
49445
+ );
49446
+ }
49447
+ function validate(data) {
49448
+ const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
49449
+ if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
49450
+ throw new Error("map data assets are malformed (failed shape validation)");
49451
+ }
49452
+ return data;
49453
+ }
49454
+ function moduleBaseDir(nb) {
49455
+ try {
49456
+ const url = import.meta.url;
49457
+ if (url) return nb.dirname(nb.fileURLToPath(url));
49458
+ } catch {
49459
+ }
49460
+ if (typeof __dirname !== "undefined") return __dirname;
49461
+ return process.cwd();
49462
+ }
49463
+ function loadMapData() {
49464
+ cache ??= (async () => {
49465
+ const nb = await loadNodeBuiltins();
49466
+ const dir = await firstExistingDir(nb, moduleBaseDir(nb));
49467
+ const [
49468
+ worldCoarse,
49469
+ worldDetail,
49470
+ usStates,
49471
+ lakes,
49472
+ rivers,
49473
+ mountainRanges,
49474
+ naLand,
49475
+ naLakes,
49476
+ waterBodies,
49477
+ gazetteer
49478
+ ] = await Promise.all([
49479
+ // worldCoarse (110m) is LOAD-BEARING but NOT a render source: the world
49480
+ // basemap renders from worldDetail (50m) at all scales (resolver pins
49481
+ // basemaps.world = 'detail'). Coarse stays as the authoritative region
49482
+ // name index + dominant-landmass bbox source in resolver.ts. Do not drop it.
49483
+ readJson(nb, dir, FILES.worldCoarse),
49484
+ readJson(nb, dir, FILES.worldDetail),
49485
+ readJson(nb, dir, FILES.usStates),
49486
+ // Lakes/rivers/mountain/NA/water assets are optional — older bundles may predate them.
49487
+ readJson(nb, dir, FILES.lakes).catch(() => void 0),
49488
+ readJson(nb, dir, FILES.rivers).catch(() => void 0),
49489
+ readJson(nb, dir, FILES.mountainRanges).catch(
49490
+ () => void 0
49491
+ ),
49492
+ readJson(nb, dir, FILES.naLand).catch(() => void 0),
49493
+ readJson(nb, dir, FILES.naLakes).catch(() => void 0),
49494
+ readJson(nb, dir, FILES.waterBodies).catch(() => void 0),
49495
+ readJson(nb, dir, FILES.gazetteer)
49496
+ ]);
49497
+ return validate({
49498
+ worldCoarse,
49499
+ worldDetail,
49500
+ usStates,
49501
+ gazetteer,
49502
+ ...lakes && { lakes },
49503
+ ...rivers && { rivers },
49504
+ ...mountainRanges && { mountainRanges },
49505
+ ...naLand && { naLand },
49506
+ ...naLakes && { naLakes },
49507
+ ...waterBodies && { waterBodies }
49508
+ });
49509
+ })().catch((e) => {
49510
+ cache = void 0;
49511
+ throw e;
49512
+ });
49513
+ return cache;
49514
+ }
49515
+ var FILES, CANDIDATE_DIRS, cache;
49516
+ var init_load_data = __esm({
49517
+ "src/map/load-data.ts"() {
49518
+ "use strict";
49519
+ FILES = {
49520
+ worldCoarse: "world-coarse.json",
49521
+ worldDetail: "world-detail.json",
49522
+ usStates: "us-states.json",
49523
+ lakes: "lakes.json",
49524
+ rivers: "rivers.json",
49525
+ mountainRanges: "mountain-ranges.json",
49526
+ naLand: "na-land.json",
49527
+ naLakes: "na-lakes.json",
49528
+ waterBodies: "water-bodies.json",
49529
+ gazetteer: "gazetteer.json"
49530
+ };
49531
+ CANDIDATE_DIRS = [
49532
+ "./data",
49533
+ "./map-data",
49534
+ "../map-data",
49535
+ "../src/map/data"
49536
+ ];
49537
+ }
49538
+ });
49539
+
47822
49540
  // src/pyramid/renderer.ts
47823
49541
  var renderer_exports17 = {};
47824
49542
  __export(renderer_exports17, {
@@ -49821,8 +51539,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
49821
51539
  const lines = splitParticipantLabel(p.label, LABEL_MAX_CHARS);
49822
51540
  if (lines.length === 0) continue;
49823
51541
  const widest = Math.max(...lines.map((l) => l.length));
49824
- const labelWidth = widest * LABEL_CHAR_WIDTH + 10;
49825
- uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth);
51542
+ const labelWidth2 = widest * LABEL_CHAR_WIDTH + 10;
51543
+ uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth2);
49826
51544
  }
49827
51545
  uniformBoxWidth = Math.min(MAX_BOX_WIDTH, uniformBoxWidth);
49828
51546
  const effectiveGap = Math.max(PARTICIPANT_GAP, uniformBoxWidth + 30);
@@ -52521,15 +54239,15 @@ function renderArcDiagram(container, parsed, palette, _isDark, onClickItem, expo
52521
54239
  textColor,
52522
54240
  onClickItem
52523
54241
  );
52524
- const neighbors = /* @__PURE__ */ new Map();
52525
- for (const node of nodes) neighbors.set(node, /* @__PURE__ */ new Set());
54242
+ const neighbors2 = /* @__PURE__ */ new Map();
54243
+ for (const node of nodes) neighbors2.set(node, /* @__PURE__ */ new Set());
52526
54244
  for (const link of links) {
52527
- neighbors.get(link.source).add(link.target);
52528
- neighbors.get(link.target).add(link.source);
54245
+ neighbors2.get(link.source).add(link.target);
54246
+ neighbors2.get(link.target).add(link.source);
52529
54247
  }
52530
54248
  const FADE_OPACITY3 = 0.1;
52531
54249
  function handleMouseEnter(hovered) {
52532
- const connected = neighbors.get(hovered);
54250
+ const connected = neighbors2.get(hovered);
52533
54251
  g.selectAll(".arc-link").each(function() {
52534
54252
  const el = d3Selection23.select(this);
52535
54253
  const src = el.attr("data-source");
@@ -54520,7 +56238,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54520
56238
  8,
54521
56239
  Math.floor(OVERLAP_WRAP_TARGET_W / OVERLAP_CH_W)
54522
56240
  );
54523
- function wrapLabel2(text, maxChars) {
56241
+ function wrapLabel3(text, maxChars) {
54524
56242
  const words = text.split(/\s+/).filter(Boolean);
54525
56243
  const lines = [];
54526
56244
  let cur = "";
@@ -54566,7 +56284,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54566
56284
  if (!ov.label) continue;
54567
56285
  const idxs = ov.sets.map((s) => vennSets.findIndex((vs) => vs.name === s));
54568
56286
  if (idxs.some((idx) => idx < 0)) continue;
54569
- const lines = wrapLabel2(ov.label, MAX_WRAP_CHARS);
56287
+ const lines = wrapLabel3(ov.label, MAX_WRAP_CHARS);
54570
56288
  wrappedOverlapLabels.set(ov, lines);
54571
56289
  const dir = predictOverlapDirRaw(idxs);
54572
56290
  const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
@@ -56003,25 +57721,29 @@ async function renderForExport(content, theme, palette, viewState, options) {
56003
57721
  if (detectedType === "map") {
56004
57722
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
56005
57723
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
56006
- const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
56007
57724
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
57725
+ const { mapExportDimensions: mapExportDimensions2 } = await Promise.resolve().then(() => (init_dimensions(), dimensions_exports));
56008
57726
  const effectivePalette2 = await resolveExportPalette(theme, palette);
56009
57727
  const mapParsed = parseMap2(content);
56010
- let mapData;
56011
- try {
56012
- mapData = await loadMapData2();
56013
- } catch {
56014
- return "";
57728
+ let mapData = options?.mapData;
57729
+ if (!mapData) {
57730
+ const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
57731
+ try {
57732
+ mapData = await loadMapData2();
57733
+ } catch {
57734
+ return "";
57735
+ }
56015
57736
  }
56016
57737
  const mapResolved = resolveMap2(mapParsed, mapData);
56017
- const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
57738
+ const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57739
+ const container2 = createExportContainer(dims2.width, dims2.height);
56018
57740
  renderMapForExport2(
56019
57741
  container2,
56020
57742
  mapResolved,
56021
57743
  mapData,
56022
57744
  effectivePalette2,
56023
57745
  theme === "dark",
56024
- { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
57746
+ dims2
56025
57747
  );
56026
57748
  return finalizeSvgExport(container2, theme, effectivePalette2);
56027
57749
  }
@@ -56868,7 +58590,8 @@ async function render(content, options) {
56868
58590
  ...options?.c4Container !== void 0 && {
56869
58591
  c4Container: options.c4Container
56870
58592
  },
56871
- ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup }
58593
+ ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup },
58594
+ ...options?.mapData !== void 0 && { mapData: options.mapData }
56872
58595
  });
56873
58596
  if (chartType === "map") {
56874
58597
  try {
@@ -56879,7 +58602,7 @@ async function render(content, options) {
56879
58602
  Promise.resolve().then(() => (init_load_data(), load_data_exports))
56880
58603
  ]
56881
58604
  );
56882
- const data = await loadMapData2();
58605
+ const data = options?.mapData ?? await loadMapData2();
56883
58606
  diagnostics = [...resolveMap2(parseMap2(content), data).diagnostics];
56884
58607
  } catch {
56885
58608
  }
@@ -57118,8 +58841,8 @@ function detectCycles(parsed) {
57118
58841
  const parent = /* @__PURE__ */ new Map();
57119
58842
  function dfs(nodeId3) {
57120
58843
  color.set(nodeId3, 1);
57121
- const neighbors = adj.get(nodeId3) ?? [];
57122
- for (const next of neighbors) {
58844
+ const neighbors2 = adj.get(nodeId3) ?? [];
58845
+ for (const next of neighbors2) {
57123
58846
  const c = color.get(next) ?? 0;
57124
58847
  if (c === 1) {
57125
58848
  const lineKey = `${nodeId3}->${next}`;
@@ -57304,6 +59027,163 @@ init_resolver2();
57304
59027
  init_load_data();
57305
59028
  init_layout15();
57306
59029
  init_renderer16();
59030
+ init_dimensions();
59031
+
59032
+ // src/map/geo-query.ts
59033
+ init_parser12();
59034
+ init_resolver2();
59035
+ init_layout15();
59036
+ init_geo();
59037
+
59038
+ // src/map/invert.ts
59039
+ function inInsetFrame(inset, px, py) {
59040
+ return px >= inset.x && px <= inset.x + inset.w && py >= inset.y && py <= inset.y + inset.h;
59041
+ }
59042
+ function unstretch(layout, px, py) {
59043
+ const s = layout.stretch;
59044
+ return [
59045
+ s.bx0 + (s.sx !== 0 ? (px - s.ox) / s.sx : 0),
59046
+ s.by0 + (s.sy !== 0 ? (py - s.oy) / s.sy : 0)
59047
+ ];
59048
+ }
59049
+ function applyStretch(layout, x, y) {
59050
+ const s = layout.stretch;
59051
+ return [s.ox + (x - s.bx0) * s.sx, s.oy + (y - s.by0) * s.sy];
59052
+ }
59053
+ function pixelToLonLat(layout, px, py) {
59054
+ for (const inset of layout.insets) {
59055
+ if (inInsetFrame(inset, px, py)) {
59056
+ const ll2 = inset.projection.invert?.([px, py]);
59057
+ return ll2 && Number.isFinite(ll2[0]) && Number.isFinite(ll2[1]) ? [ll2[0], ll2[1]] : null;
59058
+ }
59059
+ }
59060
+ const [x, y] = layout.stretch ? unstretch(layout, px, py) : [px, py];
59061
+ const ll = layout.projection.invert?.([x, y]);
59062
+ return ll && Number.isFinite(ll[0]) && Number.isFinite(ll[1]) ? [ll[0], ll[1]] : null;
59063
+ }
59064
+ function lonLatToPixel(layout, lonLat) {
59065
+ const pt = [lonLat[0], lonLat[1]];
59066
+ const main = layout.projection(pt);
59067
+ const mainPx = main && Number.isFinite(main[0]) && Number.isFinite(main[1]) ? layout.stretch ? applyStretch(layout, main[0], main[1]) : [main[0], main[1]] : null;
59068
+ const onCanvas = !!mainPx && mainPx[0] >= 0 && mainPx[0] <= layout.width && mainPx[1] >= 0 && mainPx[1] <= layout.height;
59069
+ if (onCanvas) return mainPx;
59070
+ for (const inset of layout.insets) {
59071
+ const p = inset.projection(pt);
59072
+ if (p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && inInsetFrame(inset, p[0], p[1]))
59073
+ return [p[0], p[1]];
59074
+ }
59075
+ return mainPx;
59076
+ }
59077
+
59078
+ // src/map/geo-query.ts
59079
+ var EARTH_R_KM = 6371;
59080
+ var DEG = Math.PI / 180;
59081
+ function haversineKm(lat1, lon1, lat2, lon2) {
59082
+ const dLat = (lat2 - lat1) * DEG;
59083
+ const dLon = (lon2 - lon1) * DEG;
59084
+ const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * DEG) * Math.cos(lat2 * DEG) * Math.sin(dLon / 2) ** 2;
59085
+ return 2 * EARTH_R_KM * Math.asin(Math.min(1, Math.sqrt(a)));
59086
+ }
59087
+ var POP_PULL_KM = 12;
59088
+ function nearestCity(lonLat, gazetteer) {
59089
+ const [lon, lat] = lonLat;
59090
+ let best = null;
59091
+ const cities = gazetteer.cities;
59092
+ for (let i = 0; i < cities.length; i++) {
59093
+ const c2 = cities[i];
59094
+ const dist = haversineKm(lat, lon, c2[0], c2[1]);
59095
+ const score = dist - POP_PULL_KM * Math.log10((c2[3] || 0) + 1);
59096
+ if (!best || score < best.score) best = { score, idx: i, dist };
59097
+ }
59098
+ if (!best) return null;
59099
+ const c = cities[best.idx];
59100
+ return {
59101
+ name: c[4],
59102
+ iso: c[2],
59103
+ ...c[5] !== void 0 && { sub: c[5] },
59104
+ distanceKm: best.dist,
59105
+ lat: c[0],
59106
+ lon: c[1]
59107
+ };
59108
+ }
59109
+ function roundCoord(n) {
59110
+ return Number(n.toFixed(2));
59111
+ }
59112
+ function buildTokens(lonLat, region, city) {
59113
+ const coordPoiLine = `poi ${roundCoord(lonLat[1])} ${roundCoord(lonLat[0])}`;
59114
+ let stateTok = null;
59115
+ if (region.state) {
59116
+ const { iso, name } = region.state;
59117
+ stateTok = { primary: `${name} ${iso}`, alternates: [iso, name] };
59118
+ }
59119
+ let countryTok = null;
59120
+ if (region.country) {
59121
+ const { iso, name } = region.country;
59122
+ countryTok = { primary: name, alternates: [iso] };
59123
+ }
59124
+ let cityTok = null;
59125
+ if (city) {
59126
+ const scope = city.sub ?? (city.iso || "");
59127
+ cityTok = scope ? { token: `poi ${city.name} ${scope}`, ambiguous: false } : { token: `poi ${city.name}`, ambiguous: true };
59128
+ }
59129
+ return { coordPoiLine, state: stateTok, country: countryTok, city: cityTok };
59130
+ }
59131
+ var MAX_CITY_DOTS = 250;
59132
+ function createMapGeoQuery(opts) {
59133
+ const { content, width, height, data, palette, isDark } = opts;
59134
+ const resolved = resolveMap(parseMap(content), data);
59135
+ const layout = layoutMap(
59136
+ resolved,
59137
+ data,
59138
+ { width, height },
59139
+ { palette, isDark }
59140
+ );
59141
+ const countries = decodeFeatures(data.worldDetail);
59142
+ const states = decodeFeatures(data.usStates);
59143
+ const gazetteer = data.gazetteer;
59144
+ const invert = (px, py) => pixelToLonLat(layout, px, py);
59145
+ const project = (lonLat) => lonLatToPixel(layout, lonLat);
59146
+ const locate = (px, py) => {
59147
+ const lonLat = invert(px, py);
59148
+ if (!lonLat) return null;
59149
+ const region = regionAt(lonLat, countries, states);
59150
+ const city = nearestCity(lonLat, gazetteer);
59151
+ return {
59152
+ lonLat,
59153
+ country: region.country,
59154
+ state: region.state,
59155
+ nearestCity: city,
59156
+ tokens: buildTokens(lonLat, region, city)
59157
+ };
59158
+ };
59159
+ const cities = (extent2) => {
59160
+ const sorted = [...gazetteer.cities].sort((a, b) => b[3] - a[3]);
59161
+ const out = [];
59162
+ for (const c of sorted) {
59163
+ const [lat, lon, iso, pop, name, sub] = c;
59164
+ if (extent2) {
59165
+ const [[w, s], [e, n]] = extent2;
59166
+ if (lon < w || lon > e || lat < s || lat > n) continue;
59167
+ }
59168
+ const p = project([lon, lat]);
59169
+ if (!p) continue;
59170
+ if (p[0] < 0 || p[0] > width || p[1] < 0 || p[1] > height) continue;
59171
+ out.push({
59172
+ name,
59173
+ iso,
59174
+ ...sub !== void 0 && { sub },
59175
+ lon,
59176
+ lat,
59177
+ px: p[0],
59178
+ py: p[1],
59179
+ pop
59180
+ });
59181
+ if (out.length >= MAX_CITY_DOTS) break;
59182
+ }
59183
+ return out;
59184
+ };
59185
+ return { invert, project, locate, cities, diagnostics: layout.diagnostics };
59186
+ }
57307
59187
 
57308
59188
  // src/map/completion.ts
57309
59189
  var fold2 = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
@@ -58028,9 +59908,12 @@ var GLOBAL_DIRECTIVES = {
58028
59908
  "gruvbox",
58029
59909
  "tokyo-night",
58030
59910
  "one-dark",
58031
- "bold",
58032
59911
  "dracula",
58033
- "monokai"
59912
+ "monokai",
59913
+ "atlas",
59914
+ "blueprint",
59915
+ "slate",
59916
+ "tidewater"
58034
59917
  ]
58035
59918
  },
58036
59919
  theme: {
@@ -58426,18 +60309,12 @@ var COMPLETION_REGISTRY = /* @__PURE__ */ new Map([
58426
60309
  ],
58427
60310
  [
58428
60311
  "map",
58429
- // Geographic map directives (§24B.2/.7). `poi`/`route` are content
58430
- // keywords, not directives; metadata keys (value/label/style) live in the
58431
- // reserved-key registry.
60312
+ // Geographic map directives (§24B.2/.7). Cosmetics are ON by default — the
60313
+ // only switches are bare `no-*` opt-outs, surfaced proactively so a
60314
+ // zero-config map still hints at what can be turned off. `poi`/`route` are
60315
+ // content keywords, not directives; metadata keys (value/label/style) live
60316
+ // in the reserved-key registry.
58432
60317
  withGlobals({
58433
- region: {
58434
- description: "Basemap: us-states (force US state mesh + scoping) | world (inert \u2014 already the default)",
58435
- values: ["us-states", "world"]
58436
- },
58437
- projection: {
58438
- description: "Override the auto projection",
58439
- values: ["equirectangular", "natural-earth", "albers-usa", "mercator"]
58440
- },
58441
60318
  "region-metric": { description: "Label for the region value ramp" },
58442
60319
  "poi-metric": {
58443
60320
  description: "Label for the POI value (marker size) channel"
@@ -58445,20 +60322,30 @@ var COMPLETION_REGISTRY = /* @__PURE__ */ new Map([
58445
60322
  "flow-metric": {
58446
60323
  description: "Label for the edge/leg value (thickness) channel"
58447
60324
  },
58448
- scale: { description: "Override value ramp anchors: scale <min> <max>" },
58449
- "region-labels": {
58450
- description: "Subdivision name labels",
58451
- values: ["full", "abbrev", "off"]
60325
+ locale: {
60326
+ description: "Default country/state for bare place names, e.g. locale US-GA"
58452
60327
  },
58453
- "poi-labels": {
58454
- description: "POI labels/values",
58455
- values: ["off", "auto", "all"]
60328
+ "active-tag": {
60329
+ description: "Which tag group leads when several are present"
58456
60330
  },
58457
- "default-country": { description: "ISO scope for bare city resolution" },
58458
- "default-state": { description: "ISO subdivision scope" },
60331
+ caption: { description: "Caption line (data-source attribution)" },
58459
60332
  "no-legend": { description: "Suppress the legend" },
58460
- subtitle: { description: "Subtitle line" },
58461
- caption: { description: "Caption line" }
60333
+ "no-coastline": {
60334
+ description: "Turn off coastal water-lines (on by default)"
60335
+ },
60336
+ "no-relief": {
60337
+ description: "Turn off mountain-range relief shading (on by default)"
60338
+ },
60339
+ "no-context-labels": {
60340
+ description: "Turn off orientation labels for water + nearby countries"
60341
+ },
60342
+ "no-region-labels": {
60343
+ description: "Turn off subdivision name labels (on by default)"
60344
+ },
60345
+ "no-poi-labels": { description: "Turn off POI labels (on by default)" },
60346
+ "no-colorize": {
60347
+ description: "Force plain green-land reference dress (regions are auto-coloured by default)"
60348
+ }
58462
60349
  })
58463
60350
  ]
58464
60351
  ]);
@@ -59933,7 +61820,8 @@ export {
59933
61820
  applyCollapseProjection,
59934
61821
  applyGroupOrdering,
59935
61822
  applyPositionOverrides,
59936
- boldPalette,
61823
+ atlasPalette,
61824
+ blueprintPalette,
59937
61825
  buildExtendedChartOption,
59938
61826
  buildNoteMessageMap,
59939
61827
  buildRenderSequence,
@@ -59967,6 +61855,7 @@ export {
59967
61855
  computeTimeTicks,
59968
61856
  contrastText,
59969
61857
  controlsGroupCapsuleWidth,
61858
+ createMapGeoQuery,
59970
61859
  decodeDiagramUrl,
59971
61860
  decodeViewState,
59972
61861
  displayName,
@@ -60035,6 +61924,8 @@ export {
60035
61924
  looksLikeState,
60036
61925
  makeDgmoError,
60037
61926
  mapBackgroundColor,
61927
+ mapContentAspect,
61928
+ mapExportDimensions,
60038
61929
  mapNeutralLandColor,
60039
61930
  matchesContiguously,
60040
61931
  measurePertAnalysisBlock,
@@ -60170,9 +62061,11 @@ export {
60170
62061
  shapeFill,
60171
62062
  simulateCanonical,
60172
62063
  simulateFast,
62064
+ slatePalette,
60173
62065
  solarizedPalette,
60174
62066
  suggestChartTypes,
60175
62067
  themes,
62068
+ tidewaterPalette,
60176
62069
  tint,
60177
62070
  tokyoNightPalette,
60178
62071
  transformLine,