@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/advanced.cjs CHANGED
@@ -373,18 +373,18 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
373
373
  const results = [];
374
374
  for (let i = 0; i < points.length; i++) {
375
375
  const pt = points[i];
376
- const labelWidth = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
376
+ const labelWidth2 = pt.label.length * fontSize * CHAR_WIDTH_RATIO + 8;
377
377
  let best = null;
378
378
  const directions = [
379
379
  {
380
380
  // Above
381
381
  gen: (offset) => {
382
- const lx = pt.cx - labelWidth / 2;
382
+ const lx = pt.cx - labelWidth2 / 2;
383
383
  const ly = pt.cy - offset - labelHeight;
384
- if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
384
+ if (ly < chartBounds.top || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
385
385
  return null;
386
386
  return {
387
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
387
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
388
388
  textX: pt.cx,
389
389
  textY: ly + labelHeight / 2,
390
390
  anchor: "middle"
@@ -394,12 +394,12 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
394
394
  {
395
395
  // Below
396
396
  gen: (offset) => {
397
- const lx = pt.cx - labelWidth / 2;
397
+ const lx = pt.cx - labelWidth2 / 2;
398
398
  const ly = pt.cy + offset;
399
- if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth > chartBounds.right)
399
+ if (ly + labelHeight > chartBounds.bottom || lx < chartBounds.left || lx + labelWidth2 > chartBounds.right)
400
400
  return null;
401
401
  return {
402
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
402
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
403
403
  textX: pt.cx,
404
404
  textY: ly + labelHeight / 2,
405
405
  anchor: "middle"
@@ -411,10 +411,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
411
411
  gen: (offset) => {
412
412
  const lx = pt.cx + offset;
413
413
  const ly = pt.cy - labelHeight / 2;
414
- if (lx + labelWidth > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
414
+ if (lx + labelWidth2 > chartBounds.right || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
415
415
  return null;
416
416
  return {
417
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
417
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
418
418
  textX: lx,
419
419
  textY: pt.cy,
420
420
  anchor: "start"
@@ -424,13 +424,13 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
424
424
  {
425
425
  // Left
426
426
  gen: (offset) => {
427
- const lx = pt.cx - offset - labelWidth;
427
+ const lx = pt.cx - offset - labelWidth2;
428
428
  const ly = pt.cy - labelHeight / 2;
429
429
  if (lx < chartBounds.left || ly < chartBounds.top || ly + labelHeight > chartBounds.bottom)
430
430
  return null;
431
431
  return {
432
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
433
- textX: lx + labelWidth,
432
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
433
+ textX: lx + labelWidth2,
434
434
  textY: pt.cy,
435
435
  anchor: "end"
436
436
  };
@@ -480,10 +480,10 @@ function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius,
480
480
  }
481
481
  }
482
482
  if (!best) {
483
- const lx = pt.cx - labelWidth / 2;
483
+ const lx = pt.cx - labelWidth2 / 2;
484
484
  const ly = pt.cy - minGap - labelHeight;
485
485
  best = {
486
- rect: { x: lx, y: ly, w: labelWidth, h: labelHeight },
486
+ rect: { x: lx, y: ly, w: labelWidth2, h: labelHeight },
487
487
  textX: pt.cx,
488
488
  textY: ly + labelHeight / 2,
489
489
  anchor: "middle",
@@ -860,6 +860,9 @@ var init_reserved_key_registry = __esm({
860
860
  "value",
861
861
  "label",
862
862
  "style"
863
+ // `surface:` was removed in the 2026-06-02 defaults-on review — it is no longer
864
+ // a recognized metadata key (the route/edge surface feature was cut; §24B.7).
865
+ // A stray `surface: water` is no longer captured as a reserved key.
863
866
  ]);
864
867
  ORG_REGISTRY = staticRegistry([
865
868
  "color",
@@ -1922,77 +1925,266 @@ function getSegmentColors(palette, count) {
1922
1925
  (_, i) => hslToHex(Math.round((startHue + i * step) % 360), avgS, avgL)
1923
1926
  );
1924
1927
  }
1928
+ function politicalTints(palette, count, isDark) {
1929
+ if (count <= 0) return [];
1930
+ const base = isDark ? palette.surface : palette.bg;
1931
+ const c = palette.colors;
1932
+ const swatches = [
1933
+ .../* @__PURE__ */ new Set([
1934
+ c.green,
1935
+ c.yellow,
1936
+ c.orange,
1937
+ c.purple,
1938
+ c.red,
1939
+ c.teal,
1940
+ c.cyan,
1941
+ c.blue
1942
+ ])
1943
+ ];
1944
+ const bands = isDark ? POLITICAL_TINT_BANDS.dark : POLITICAL_TINT_BANDS.light;
1945
+ const out = [];
1946
+ for (const pct of bands) {
1947
+ if (out.length >= count) break;
1948
+ for (const s of swatches) out.push(mix(s, base, pct));
1949
+ }
1950
+ return out.slice(0, count);
1951
+ }
1952
+ var POLITICAL_TINT_BANDS;
1925
1953
  var init_color_utils = __esm({
1926
1954
  "src/palettes/color-utils.ts"() {
1927
1955
  "use strict";
1956
+ POLITICAL_TINT_BANDS = {
1957
+ light: [32, 48, 64, 80],
1958
+ dark: [44, 58, 72, 86]
1959
+ };
1928
1960
  }
1929
1961
  });
1930
1962
 
1931
- // src/palettes/bold.ts
1932
- var boldPalette;
1933
- var init_bold = __esm({
1934
- "src/palettes/bold.ts"() {
1963
+ // src/palettes/atlas.ts
1964
+ var atlasPalette;
1965
+ var init_atlas = __esm({
1966
+ "src/palettes/atlas.ts"() {
1935
1967
  "use strict";
1936
1968
  init_registry();
1937
- boldPalette = {
1938
- id: "bold",
1939
- name: "Bold",
1969
+ atlasPalette = {
1970
+ id: "atlas",
1971
+ name: "Atlas",
1940
1972
  light: {
1941
- bg: "#ffffff",
1942
- surface: "#f0f0f0",
1943
- overlay: "#f0f0f0",
1944
- border: "#cccccc",
1945
- text: "#000000",
1946
- textMuted: "#666666",
1947
- textOnFillLight: "#ffffff",
1948
- textOnFillDark: "#000000",
1949
- primary: "#0000ff",
1950
- secondary: "#ff00ff",
1951
- accent: "#00cccc",
1952
- destructive: "#ff0000",
1973
+ bg: "#f3ead3",
1974
+ // warm manila / parchment
1975
+ surface: "#ece0c0",
1976
+ // deeper paper (cards, panels)
1977
+ overlay: "#e8dab8",
1978
+ // popovers, dropdowns
1979
+ border: "#bcaa86",
1980
+ // muted sepia rule line
1981
+ text: "#463a26",
1982
+ // aged sepia-brown ink
1983
+ textMuted: "#7a6a4f",
1984
+ // faded annotation ink
1985
+ textOnFillLight: "#f7f1de",
1986
+ // parchment (light text on dark fills)
1987
+ textOnFillDark: "#3a2e1c",
1988
+ // deep ink (dark text on light fills)
1989
+ primary: "#5b7a99",
1990
+ // pull-down map ocean (steel-blue)
1991
+ secondary: "#7e9a6f",
1992
+ // lowland sage / celadon
1993
+ accent: "#b07f7c",
1994
+ // dusty rose
1995
+ destructive: "#b25a45",
1996
+ // brick / terracotta
1953
1997
  colors: {
1954
- red: "#ff0000",
1955
- orange: "#ff8000",
1956
- yellow: "#ffcc00",
1957
- green: "#00cc00",
1958
- blue: "#0000ff",
1959
- purple: "#cc00cc",
1960
- teal: "#008080",
1961
- cyan: "#00cccc",
1962
- gray: "#808080",
1963
- black: "#000000",
1964
- white: "#f0f0f0"
1998
+ red: "#bf6a52",
1999
+ // terracotta brick
2000
+ orange: "#cf9a5c",
2001
+ // map tan / ochre
2002
+ yellow: "#cdb35e",
2003
+ // straw / muted lemon
2004
+ green: "#7e9a6f",
2005
+ // sage / celadon lowland
2006
+ blue: "#5b7a99",
2007
+ // steel-blue ocean
2008
+ purple: "#9a7fa6",
2009
+ // dusty lilac / mauve
2010
+ teal: "#6fa094",
2011
+ // muted seafoam
2012
+ cyan: "#79a7b5",
2013
+ // shallow-water blue
2014
+ gray: "#8a7d68",
2015
+ // warm taupe
2016
+ black: "#463a26",
2017
+ // ink
2018
+ white: "#ece0c0"
2019
+ // paper
1965
2020
  }
1966
2021
  },
1967
2022
  dark: {
1968
- bg: "#000000",
1969
- surface: "#111111",
1970
- overlay: "#1a1a1a",
1971
- border: "#333333",
1972
- text: "#ffffff",
1973
- textMuted: "#aaaaaa",
1974
- textOnFillLight: "#ffffff",
1975
- textOnFillDark: "#000000",
1976
- primary: "#00ccff",
1977
- secondary: "#ff00ff",
1978
- accent: "#ffff00",
1979
- destructive: "#ff0000",
2023
+ bg: "#1e2a33",
2024
+ // deep map ocean (night globe)
2025
+ surface: "#27353f",
2026
+ // raised ocean
2027
+ overlay: "#2e3d48",
2028
+ // popovers, dropdowns
2029
+ border: "#3d4f5c",
2030
+ // depth-contour line
2031
+ text: "#e8dcc0",
2032
+ // parchment ink, inverted
2033
+ textMuted: "#a89a7d",
2034
+ // faded label
2035
+ textOnFillLight: "#f7f1de",
2036
+ // parchment
2037
+ textOnFillDark: "#1a242c",
2038
+ // deep ocean ink
2039
+ primary: "#7ba0bf",
2040
+ // brighter ocean
2041
+ secondary: "#9bb588",
2042
+ // sage, lifted
2043
+ accent: "#cf9a96",
2044
+ // dusty rose, lifted
2045
+ destructive: "#c9745c",
2046
+ // brick, lifted
2047
+ colors: {
2048
+ red: "#cf7a60",
2049
+ // terracotta
2050
+ orange: "#d9a96a",
2051
+ // tan / ochre
2052
+ yellow: "#d8c074",
2053
+ // straw
2054
+ green: "#9bb588",
2055
+ // sage lowland
2056
+ blue: "#7ba0bf",
2057
+ // ocean
2058
+ purple: "#b59ac0",
2059
+ // lilac / mauve
2060
+ teal: "#85b3a6",
2061
+ // seafoam
2062
+ cyan: "#92bccb",
2063
+ // shallow-water blue
2064
+ gray: "#9a8d76",
2065
+ // warm taupe
2066
+ black: "#27353f",
2067
+ // raised ocean
2068
+ white: "#e8dcc0"
2069
+ // parchment
2070
+ }
2071
+ }
2072
+ };
2073
+ registerPalette(atlasPalette);
2074
+ }
2075
+ });
2076
+
2077
+ // src/palettes/blueprint.ts
2078
+ var blueprintPalette;
2079
+ var init_blueprint = __esm({
2080
+ "src/palettes/blueprint.ts"() {
2081
+ "use strict";
2082
+ init_registry();
2083
+ blueprintPalette = {
2084
+ id: "blueprint",
2085
+ name: "Blueprint",
2086
+ light: {
2087
+ bg: "#f4f8fb",
2088
+ // pale drafting white (faint cyan)
2089
+ surface: "#e6eef4",
2090
+ // drafting panel
2091
+ overlay: "#dde9f1",
2092
+ // popovers, dropdowns
2093
+ border: "#aac3d6",
2094
+ // pale blue grid line
2095
+ text: "#123a5e",
2096
+ // blueprint navy ink
2097
+ textMuted: "#4f7390",
2098
+ // faint draft note
2099
+ textOnFillLight: "#f4f8fb",
2100
+ // drafting white
2101
+ textOnFillDark: "#0c2f4d",
2102
+ // deep blueprint navy
2103
+ primary: "#1f5e8c",
2104
+ // blueprint blue
2105
+ secondary: "#5b7d96",
2106
+ // steel
2107
+ accent: "#b08a3e",
2108
+ // draftsman's ochre highlight
2109
+ destructive: "#c0504d",
2110
+ // correction red
1980
2111
  colors: {
1981
- red: "#ff0000",
1982
- orange: "#ff8000",
1983
- yellow: "#ffff00",
1984
- green: "#00ff00",
1985
- blue: "#0066ff",
1986
- purple: "#ff00ff",
1987
- teal: "#00cccc",
1988
- cyan: "#00ffff",
1989
- gray: "#808080",
1990
- black: "#111111",
1991
- white: "#ffffff"
2112
+ red: "#c25a4e",
2113
+ // correction red
2114
+ orange: "#c2823e",
2115
+ // ochre
2116
+ yellow: "#c2a843",
2117
+ // pencil gold
2118
+ green: "#4f8a6b",
2119
+ // drafting green
2120
+ blue: "#1f5e8c",
2121
+ // blueprint blue
2122
+ purple: "#6f5e96",
2123
+ // indigo pencil
2124
+ teal: "#3a8a8a",
2125
+ // teal
2126
+ cyan: "#3f8fb5",
2127
+ // cyan
2128
+ gray: "#7e8e98",
2129
+ // graphite
2130
+ black: "#123a5e",
2131
+ // navy ink
2132
+ white: "#e6eef4"
2133
+ // panel
2134
+ }
2135
+ },
2136
+ dark: {
2137
+ bg: "#103a5e",
2138
+ // deep blueprint blue (cyanotype ground)
2139
+ surface: "#16466e",
2140
+ // raised sheet
2141
+ overlay: "#1c5180",
2142
+ // popovers, dropdowns
2143
+ border: "#3a6f96",
2144
+ // grid line
2145
+ text: "#eaf2f8",
2146
+ // chalk white
2147
+ textMuted: "#9fc0d6",
2148
+ // faint chalk note
2149
+ textOnFillLight: "#eaf2f8",
2150
+ // chalk white
2151
+ textOnFillDark: "#0c2f4d",
2152
+ // deep blueprint navy
2153
+ primary: "#7fb8d8",
2154
+ // chalk cyan
2155
+ secondary: "#9fb8c8",
2156
+ // pale steel
2157
+ accent: "#d8c27a",
2158
+ // chalk amber
2159
+ destructive: "#e08a7a",
2160
+ // chalk correction red
2161
+ colors: {
2162
+ red: "#e0907e",
2163
+ // chalk red
2164
+ orange: "#e0ab78",
2165
+ // chalk amber
2166
+ yellow: "#e3d089",
2167
+ // chalk gold
2168
+ green: "#93c79e",
2169
+ // chalk green
2170
+ blue: "#8ec3e0",
2171
+ // chalk cyan-blue
2172
+ purple: "#b6a6d8",
2173
+ // chalk indigo
2174
+ teal: "#84c7c2",
2175
+ // chalk teal
2176
+ cyan: "#9fd6e0",
2177
+ // chalk cyan
2178
+ gray: "#aebecb",
2179
+ // chalk graphite
2180
+ black: "#16466e",
2181
+ // raised sheet
2182
+ white: "#eaf2f8"
2183
+ // chalk white
1992
2184
  }
1993
2185
  }
1994
2186
  };
1995
- registerPalette(boldPalette);
2187
+ registerPalette(blueprintPalette);
1996
2188
  }
1997
2189
  });
1998
2190
 
@@ -2489,6 +2681,120 @@ var init_rose_pine = __esm({
2489
2681
  }
2490
2682
  });
2491
2683
 
2684
+ // src/palettes/slate.ts
2685
+ var slatePalette;
2686
+ var init_slate = __esm({
2687
+ "src/palettes/slate.ts"() {
2688
+ "use strict";
2689
+ init_registry();
2690
+ slatePalette = {
2691
+ id: "slate",
2692
+ name: "Slate",
2693
+ light: {
2694
+ bg: "#ffffff",
2695
+ // clean slide white
2696
+ surface: "#f3f5f8",
2697
+ // light cool-gray panel
2698
+ overlay: "#eaeef3",
2699
+ // popovers, dropdowns
2700
+ border: "#d4dae1",
2701
+ // hairline rule
2702
+ text: "#1f2933",
2703
+ // near-black slate (softer than pure black)
2704
+ textMuted: "#5b6672",
2705
+ // secondary label
2706
+ textOnFillLight: "#ffffff",
2707
+ // light text on dark fills
2708
+ textOnFillDark: "#1f2933",
2709
+ // dark text on light fills
2710
+ primary: "#3b6ea5",
2711
+ // confident corporate blue
2712
+ secondary: "#5b6672",
2713
+ // slate gray
2714
+ accent: "#3a9188",
2715
+ // muted teal accent
2716
+ destructive: "#c0504d",
2717
+ // brick red
2718
+ colors: {
2719
+ red: "#c0504d",
2720
+ // brick
2721
+ orange: "#cc7a33",
2722
+ // muted amber
2723
+ yellow: "#c9a227",
2724
+ // gold (not neon)
2725
+ green: "#5b9357",
2726
+ // forest / sage
2727
+ blue: "#3b6ea5",
2728
+ // corporate blue
2729
+ purple: "#7d5ba6",
2730
+ // muted violet
2731
+ teal: "#3a9188",
2732
+ // teal
2733
+ cyan: "#4f96c4",
2734
+ // steel cyan
2735
+ gray: "#7e8a97",
2736
+ // cool gray
2737
+ black: "#1f2933",
2738
+ // slate ink
2739
+ white: "#f3f5f8"
2740
+ // panel
2741
+ }
2742
+ },
2743
+ dark: {
2744
+ bg: "#161b22",
2745
+ // deep slate (keynote dark)
2746
+ surface: "#202833",
2747
+ // raised panel
2748
+ overlay: "#29323e",
2749
+ // popovers, dropdowns
2750
+ border: "#38424f",
2751
+ // divider
2752
+ text: "#e6eaef",
2753
+ // off-white
2754
+ textMuted: "#9aa5b1",
2755
+ // secondary label
2756
+ textOnFillLight: "#ffffff",
2757
+ // light text on dark fills
2758
+ textOnFillDark: "#161b22",
2759
+ // dark text on light fills
2760
+ primary: "#5b9bd5",
2761
+ // lifted corporate blue
2762
+ secondary: "#8593a3",
2763
+ // slate gray, lifted
2764
+ accent: "#45b3a3",
2765
+ // teal, lifted
2766
+ destructive: "#e07b6e",
2767
+ // brick, lifted
2768
+ colors: {
2769
+ red: "#e07b6e",
2770
+ // brick
2771
+ orange: "#e0975a",
2772
+ // amber
2773
+ yellow: "#d9bd5a",
2774
+ // gold
2775
+ green: "#74b56e",
2776
+ // forest / sage
2777
+ blue: "#5b9bd5",
2778
+ // corporate blue
2779
+ purple: "#a585c9",
2780
+ // violet
2781
+ teal: "#45b3a3",
2782
+ // teal
2783
+ cyan: "#62b0d9",
2784
+ // steel cyan
2785
+ gray: "#95a1ae",
2786
+ // cool gray
2787
+ black: "#202833",
2788
+ // raised panel
2789
+ white: "#e6eaef"
2790
+ // off-white
2791
+ }
2792
+ }
2793
+ };
2794
+ registerPalette(slatePalette);
2795
+ }
2796
+ });
2797
+
2492
2798
  // src/palettes/solarized.ts
2493
2799
  var solarizedPalette;
2494
2800
  var init_solarized = __esm({
@@ -2584,6 +2890,120 @@ var init_solarized = __esm({
2584
2890
  }
2585
2891
  });
2586
2892
 
2893
+ // src/palettes/tidewater.ts
2894
+ var tidewaterPalette;
2895
+ var init_tidewater = __esm({
2896
+ "src/palettes/tidewater.ts"() {
2897
+ "use strict";
2898
+ init_registry();
2899
+ tidewaterPalette = {
2900
+ id: "tidewater",
2901
+ name: "Tidewater",
2902
+ light: {
2903
+ bg: "#eceff0",
2904
+ // weathered sea-mist paper
2905
+ surface: "#e0e4e3",
2906
+ // worn deck panel
2907
+ overlay: "#dadfdf",
2908
+ // popovers, dropdowns
2909
+ border: "#a9b2b3",
2910
+ // muted slate rule
2911
+ text: "#18313f",
2912
+ // ship's-log navy ink
2913
+ textMuted: "#51636b",
2914
+ // faded log entry
2915
+ textOnFillLight: "#f3f5f3",
2916
+ // weathered white
2917
+ textOnFillDark: "#162c38",
2918
+ // deep navy
2919
+ primary: "#1f4e6b",
2920
+ // deep-sea navy
2921
+ secondary: "#b08a4f",
2922
+ // rope / manila tan
2923
+ accent: "#c69a3e",
2924
+ // brass
2925
+ destructive: "#c1433a",
2926
+ // signal-flag red
2927
+ colors: {
2928
+ red: "#c1433a",
2929
+ // signal-flag red
2930
+ orange: "#cc7a38",
2931
+ // weathered amber
2932
+ yellow: "#d6bf5a",
2933
+ // brass gold
2934
+ green: "#4f8a6b",
2935
+ // sea-glass green
2936
+ blue: "#1f4e6b",
2937
+ // deep-sea navy
2938
+ purple: "#6a5a8c",
2939
+ // twilight harbor
2940
+ teal: "#3d8c8c",
2941
+ // sea-glass teal
2942
+ cyan: "#4f9bb5",
2943
+ // shallow water
2944
+ gray: "#8a8d86",
2945
+ // driftwood gray
2946
+ black: "#18313f",
2947
+ // navy ink
2948
+ white: "#e0e4e3"
2949
+ // deck panel
2950
+ }
2951
+ },
2952
+ dark: {
2953
+ bg: "#0f2230",
2954
+ // night-harbor deep sea
2955
+ surface: "#16303f",
2956
+ // raised hull
2957
+ overlay: "#1d3a4a",
2958
+ // popovers, dropdowns
2959
+ border: "#2c4856",
2960
+ // rigging line
2961
+ text: "#e6ebe8",
2962
+ // weathered white
2963
+ textMuted: "#9aaab0",
2964
+ // faded label
2965
+ textOnFillLight: "#f3f5f3",
2966
+ // weathered white
2967
+ textOnFillDark: "#0f2230",
2968
+ // deep sea
2969
+ primary: "#4f9bc4",
2970
+ // lifted sea blue
2971
+ secondary: "#c9a46a",
2972
+ // rope tan, lifted
2973
+ accent: "#d9b25a",
2974
+ // brass, lifted
2975
+ destructive: "#e06a5e",
2976
+ // signal red, lifted
2977
+ colors: {
2978
+ red: "#e06a5e",
2979
+ // signal-flag red
2980
+ orange: "#df9a52",
2981
+ // amber
2982
+ yellow: "#e0c662",
2983
+ // brass gold
2984
+ green: "#6fb58c",
2985
+ // sea-glass green
2986
+ blue: "#4f9bc4",
2987
+ // sea blue
2988
+ purple: "#9486bf",
2989
+ // twilight harbor
2990
+ teal: "#5cb0ac",
2991
+ // sea-glass teal
2992
+ cyan: "#62b4cf",
2993
+ // shallow water
2994
+ gray: "#9aa39c",
2995
+ // driftwood gray
2996
+ black: "#16303f",
2997
+ // raised hull
2998
+ white: "#e6ebe8"
2999
+ // weathered white
3000
+ }
3001
+ }
3002
+ };
3003
+ registerPalette(tidewaterPalette);
3004
+ }
3005
+ });
3006
+
2587
3007
  // src/palettes/tokyo-night.ts
2588
3008
  var tokyoNightPalette;
2589
3009
  var init_tokyo_night = __esm({
@@ -2859,7 +3279,8 @@ var init_monokai = __esm({
2859
3279
  // src/palettes/index.ts
2860
3280
  var palettes_exports = {};
2861
3281
  __export(palettes_exports, {
2862
- boldPalette: () => boldPalette,
3282
+ atlasPalette: () => atlasPalette,
3283
+ blueprintPalette: () => blueprintPalette,
2863
3284
  catppuccinPalette: () => catppuccinPalette,
2864
3285
  contrastText: () => contrastText,
2865
3286
  draculaPalette: () => draculaPalette,
@@ -2880,7 +3301,9 @@ __export(palettes_exports, {
2880
3301
  rosePinePalette: () => rosePinePalette,
2881
3302
  shade: () => shade,
2882
3303
  shapeFill: () => shapeFill,
3304
+ slatePalette: () => slatePalette,
2883
3305
  solarizedPalette: () => solarizedPalette,
3306
+ tidewaterPalette: () => tidewaterPalette,
2884
3307
  tint: () => tint,
2885
3308
  tokyoNightPalette: () => tokyoNightPalette
2886
3309
  });
@@ -2890,17 +3313,21 @@ var init_palettes = __esm({
2890
3313
  "use strict";
2891
3314
  init_registry();
2892
3315
  init_color_utils();
2893
- init_bold();
3316
+ init_atlas();
3317
+ init_blueprint();
2894
3318
  init_catppuccin();
2895
3319
  init_gruvbox();
2896
3320
  init_nord();
2897
3321
  init_one_dark();
2898
3322
  init_rose_pine();
3323
+ init_slate();
2899
3324
  init_solarized();
3325
+ init_tidewater();
2900
3326
  init_tokyo_night();
2901
3327
  init_dracula();
2902
3328
  init_monokai();
2903
- init_bold();
3329
+ init_atlas();
3330
+ init_blueprint();
2904
3331
  init_catppuccin();
2905
3332
  init_dracula();
2906
3333
  init_gruvbox();
@@ -2908,9 +3335,15 @@ var init_palettes = __esm({
2908
3335
  init_nord();
2909
3336
  init_one_dark();
2910
3337
  init_rose_pine();
3338
+ init_slate();
2911
3339
  init_solarized();
3340
+ init_tidewater();
2912
3341
  init_tokyo_night();
2913
3342
  palettes = {
3343
+ atlas: atlasPalette,
3344
+ blueprint: blueprintPalette,
3345
+ slate: slatePalette,
3346
+ tidewater: tidewaterPalette,
2914
3347
  nord: nordPalette,
2915
3348
  catppuccin: catppuccinPalette,
2916
3349
  solarized: solarizedPalette,
@@ -2919,8 +3352,7 @@ var init_palettes = __esm({
2919
3352
  oneDark: oneDarkPalette,
2920
3353
  rosePine: rosePinePalette,
2921
3354
  dracula: draculaPalette,
2922
- monokai: monokaiPalette,
2923
- bold: boldPalette
3355
+ monokai: monokaiPalette
2924
3356
  };
2925
3357
  }
2926
3358
  });
@@ -3430,6 +3862,9 @@ function controlsGroupCapsuleWidth(toggles) {
3430
3862
  }
3431
3863
  return w;
3432
3864
  }
3865
+ function isAppHostedControls(config, isExport) {
3866
+ return !isExport && config.controlsHost === "app" && !!config.controlsGroup && config.controlsGroup.toggles.length > 0;
3867
+ }
3433
3868
  function buildControlsGroupLayout(config, state) {
3434
3869
  const cg = config.controlsGroup;
3435
3870
  if (!cg || cg.toggles.length === 0) return void 0;
@@ -3483,6 +3918,7 @@ function buildControlsGroupLayout(config, state) {
3483
3918
  function computeLegendLayout(config, state, containerWidth) {
3484
3919
  const { groups, controls: configControls, mode } = config;
3485
3920
  const isExport = mode === "export";
3921
+ const gated = isAppHostedControls(config, isExport);
3486
3922
  const activeGroupName = state.activeGroup?.toLowerCase() ?? null;
3487
3923
  if (isExport && !activeGroupName) {
3488
3924
  return {
@@ -3493,7 +3929,7 @@ function computeLegendLayout(config, state, containerWidth) {
3493
3929
  pills: []
3494
3930
  };
3495
3931
  }
3496
- const controlsGroupLayout = isExport ? void 0 : buildControlsGroupLayout(config, state);
3932
+ const controlsGroupLayout = isExport || gated ? void 0 : buildControlsGroupLayout(config, state);
3497
3933
  const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0 || !!g.gradient);
3498
3934
  if (visibleGroups.length === 0 && (!configControls || configControls.length === 0) && !controlsGroupLayout) {
3499
3935
  return {
@@ -8325,8 +8761,8 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8325
8761
  const pt = points[i];
8326
8762
  const ptSize = pt.size ?? symbolSize;
8327
8763
  const minGap = ptSize / 2 + 4;
8328
- const labelWidth = pt.name.length * fontSize * 0.6 + 8;
8329
- const labelX = pt.px - labelWidth / 2;
8764
+ const labelWidth2 = pt.name.length * fontSize * 0.6 + 8;
8765
+ const labelX = pt.px - labelWidth2 / 2;
8330
8766
  let bestLabelY = 0;
8331
8767
  let bestOffset = Infinity;
8332
8768
  let placed = false;
@@ -8338,7 +8774,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8338
8774
  const candidate = {
8339
8775
  x: labelX,
8340
8776
  y: labelY,
8341
- w: labelWidth,
8777
+ w: labelWidth2,
8342
8778
  h: labelHeight
8343
8779
  };
8344
8780
  let collision = false;
@@ -8380,7 +8816,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8380
8816
  const labelRect = {
8381
8817
  x: labelX,
8382
8818
  y: bestLabelY,
8383
- w: labelWidth,
8819
+ w: labelWidth2,
8384
8820
  h: labelHeight
8385
8821
  };
8386
8822
  placedLabels.push(labelRect);
@@ -8416,7 +8852,7 @@ function computeScatterLabelGraphics(points, chartBounds, fontSize, symbolSize,
8416
8852
  shape: {
8417
8853
  x: labelX - bgPad,
8418
8854
  y: bestLabelY - bgPad,
8419
- width: labelWidth + bgPad * 2,
8855
+ width: labelWidth2 + bgPad * 2,
8420
8856
  height: labelHeight + bgPad * 2
8421
8857
  },
8422
8858
  style: { fill: bg },
@@ -15856,10 +16292,6 @@ function parseMap(content) {
15856
16292
  handleTag(trimmed, lineNumber);
15857
16293
  continue;
15858
16294
  }
15859
- if ((firstWord === "muted" || firstWord === "natural") && trimmed === firstWord) {
15860
- handleDirective(firstWord, "", lineNumber);
15861
- continue;
15862
- }
15863
16295
  if (DIRECTIVE_SET.has(firstWord) && !trimmed.slice(firstWord.length).trimStart().startsWith(":")) {
15864
16296
  handleDirective(
15865
16297
  firstWord,
@@ -15906,28 +16338,13 @@ function parseMap(content) {
15906
16338
  pushWarning(line12, `Duplicate directive "${key}" \u2014 last value wins.`);
15907
16339
  };
15908
16340
  switch (key) {
15909
- case "region":
15910
- dup(d.region);
15911
- d.region = value;
15912
- break;
15913
- case "projection":
15914
- dup(d.projection);
15915
- if (value && ![
15916
- "equirectangular",
15917
- "natural-earth",
15918
- "albers-usa",
15919
- "mercator"
15920
- ].includes(value))
15921
- pushWarning(
15922
- line12,
15923
- `Unknown projection "${value}" (expected equirectangular | natural-earth | albers-usa | mercator).`
15924
- );
15925
- d.projection = value;
15926
- break;
15927
- case "region-metric":
16341
+ case "region-metric": {
15928
16342
  dup(d.regionMetric);
15929
- d.regionMetric = value;
16343
+ const { label: rmLabel, colorName: rmColor } = peelTrailingColorName(value);
16344
+ d.regionMetric = rmLabel;
16345
+ if (rmColor) d.regionMetricColor = rmColor;
15930
16346
  break;
16347
+ }
15931
16348
  case "poi-metric":
15932
16349
  dup(d.poiMetric);
15933
16350
  d.poiMetric = value;
@@ -15936,85 +16353,43 @@ function parseMap(content) {
15936
16353
  dup(d.flowMetric);
15937
16354
  d.flowMetric = value;
15938
16355
  break;
15939
- case "scale":
15940
- dup(d.scale);
15941
- {
15942
- const s = parseScale(value, line12);
15943
- if (s) d.scale = s;
15944
- }
15945
- break;
15946
- case "region-labels":
15947
- dup(d.regionLabels);
15948
- if (value && !["full", "abbrev", "off"].includes(value))
15949
- pushWarning(
15950
- line12,
15951
- `Unknown region-labels "${value}" (expected full | abbrev | off).`
15952
- );
15953
- d.regionLabels = value;
15954
- break;
15955
- case "poi-labels":
15956
- dup(d.poiLabels);
15957
- if (value && !["off", "auto", "all"].includes(value))
15958
- pushWarning(
15959
- line12,
15960
- `Unknown poi-labels "${value}" (expected off | auto | all).`
15961
- );
15962
- d.poiLabels = value;
15963
- break;
15964
- case "default-country":
15965
- dup(d.defaultCountry);
15966
- d.defaultCountry = value;
15967
- break;
15968
- case "default-state":
15969
- dup(d.defaultState);
15970
- d.defaultState = value;
16356
+ case "locale":
16357
+ dup(d.locale);
16358
+ d.locale = value;
15971
16359
  break;
15972
16360
  case "active-tag":
15973
16361
  dup(d.activeTag);
15974
16362
  d.activeTag = value;
15975
16363
  break;
16364
+ case "caption":
16365
+ dup(d.caption);
16366
+ d.caption = value;
16367
+ break;
16368
+ // ── Cosmetic `no-*` opt-outs: bare flags, idempotent (mirror `no-legend`,
16369
+ // no dup warning); each defaults the feature ON when absent. ──
15976
16370
  case "no-legend":
15977
16371
  d.noLegend = true;
15978
16372
  break;
15979
- case "muted":
15980
- case "natural":
15981
- if (d.basemapStyle !== void 0 && d.basemapStyle !== key)
15982
- pushWarning(
15983
- line12,
15984
- `Conflicting basemap dress \u2014 "${d.basemapStyle}" then "${key}"; last wins.`
15985
- );
15986
- d.basemapStyle = key;
16373
+ case "no-coastline":
16374
+ d.noCoastline = true;
15987
16375
  break;
15988
- case "subtitle":
15989
- dup(d.subtitle);
15990
- d.subtitle = value;
16376
+ case "no-relief":
16377
+ d.noRelief = true;
15991
16378
  break;
15992
- case "caption":
15993
- dup(d.caption);
15994
- d.caption = value;
16379
+ case "no-context-labels":
16380
+ d.noContextLabels = true;
16381
+ break;
16382
+ case "no-region-labels":
16383
+ d.noRegionLabels = true;
16384
+ break;
16385
+ case "no-poi-labels":
16386
+ d.noPoiLabels = true;
16387
+ break;
16388
+ case "no-colorize":
16389
+ d.noColorize = true;
15995
16390
  break;
15996
16391
  }
15997
16392
  }
15998
- function parseScale(value, line12) {
15999
- const toks = value.split(/\s+/).filter(Boolean);
16000
- const min = Number(toks[0]);
16001
- const max = Number(toks[1]);
16002
- if (!Number.isFinite(min) || !Number.isFinite(max)) {
16003
- pushError(line12, `scale requires numeric <min> <max> (got "${value}").`);
16004
- return null;
16005
- }
16006
- const scale = { min, max };
16007
- if (toks[2] === "center") {
16008
- const c = Number(toks[3]);
16009
- if (Number.isFinite(c)) scale.center = c;
16010
- else
16011
- pushError(
16012
- line12,
16013
- `scale center requires a number (got "${toks[3] ?? ""}").`
16014
- );
16015
- }
16016
- return scale;
16017
- }
16018
16393
  function handleTag(trimmed, line12) {
16019
16394
  const m = matchTagBlockHeading(trimmed);
16020
16395
  if (!m) {
@@ -16088,6 +16463,7 @@ function parseMap(content) {
16088
16463
  };
16089
16464
  if (regionScope !== void 0) region.scope = regionScope;
16090
16465
  if (valueNum !== void 0) region.value = valueNum;
16466
+ if (split.color) region.color = split.color;
16091
16467
  regions.push(region);
16092
16468
  }
16093
16469
  function handlePoi(rest, line12, indent) {
@@ -16112,6 +16488,7 @@ function parseMap(content) {
16112
16488
  const poi = { pos, tags, meta, lineNumber: line12 };
16113
16489
  if (split.alias) poi.alias = split.alias;
16114
16490
  if (label !== void 0) poi.label = label;
16491
+ if (split.color) poi.color = split.color;
16115
16492
  pois.push(poi);
16116
16493
  open.poi = { poi, indent };
16117
16494
  }
@@ -16212,13 +16589,15 @@ function parseMap(content) {
16212
16589
  pushError(line12, `Edge has an empty endpoint: "${trimmed}".`);
16213
16590
  continue;
16214
16591
  }
16215
- const meta = k === links.length - 1 ? lastSplit.meta : {};
16592
+ const isLast = k === links.length - 1;
16593
+ const meta = isLast ? lastSplit.meta : {};
16594
+ const style = links[k].style === "arc" ? "arc" : "straight";
16216
16595
  edges.push({
16217
16596
  from,
16218
16597
  to,
16219
16598
  ...links[k].label !== void 0 && { label: links[k].label },
16220
16599
  directed: links[k].directed,
16221
- style: links[k].style,
16600
+ style,
16222
16601
  meta,
16223
16602
  lineNumber: line12
16224
16603
  });
@@ -16304,20 +16683,19 @@ var init_parser12 = __esm({
16304
16683
  LEG_ARROW_RE = /^(-[^>]*?->|->|~[^>]*?~>|~>|--)\s+(.+)$/;
16305
16684
  AT_RE = /(^|[\s,])at\s*:/i;
16306
16685
  DIRECTIVE_SET = /* @__PURE__ */ new Set([
16307
- "region",
16308
- "projection",
16309
16686
  "region-metric",
16310
16687
  "poi-metric",
16311
16688
  "flow-metric",
16312
- "scale",
16313
- "region-labels",
16314
- "poi-labels",
16315
- "default-country",
16316
- "default-state",
16689
+ "locale",
16317
16690
  "active-tag",
16691
+ "caption",
16318
16692
  "no-legend",
16319
- "subtitle",
16320
- "caption"
16693
+ "no-coastline",
16694
+ "no-relief",
16695
+ "no-context-labels",
16696
+ "no-region-labels",
16697
+ "no-poi-labels",
16698
+ "no-colorize"
16321
16699
  ]);
16322
16700
  }
16323
16701
  });
@@ -24328,8 +24706,8 @@ function renderKanban(container, parsed, palette, isDark, options) {
24328
24706
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24329
24707
  for (const meta of tagMeta) {
24330
24708
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(`${meta.label}: `);
24331
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24332
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24709
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24710
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24333
24711
  metaY += sCardMetaLineHeight;
24334
24712
  }
24335
24713
  for (const detail of card.details) {
@@ -24673,8 +25051,8 @@ function renderSwimlaneCard(parent, cardLayout, tagGroups, activeTagGroup, palet
24673
25051
  let metaY = separatorY + sCardSeparatorGap + sCardMetaFontSize;
24674
25052
  for (const meta of tagMeta) {
24675
25053
  cg.append("text").attr("x", cx + sCardPaddingX).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", palette.textMuted).text(`${meta.label}: `);
24676
- const labelWidth = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
24677
- cg.append("text").attr("x", cx + sCardPaddingX + labelWidth).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
25054
+ const labelWidth2 = (meta.label.length + 2) * sCardMetaFontSize * 0.6;
25055
+ cg.append("text").attr("x", cx + sCardPaddingX + labelWidth2).attr("y", metaY).attr("font-size", sCardMetaFontSize).attr("fill", onCardText).text(meta.value);
24678
25056
  metaY += sCardMetaLineHeight;
24679
25057
  }
24680
25058
  for (const detail of card.details) {
@@ -25509,8 +25887,8 @@ function classifyEREntities(tables, relationships) {
25509
25887
  }
25510
25888
  }
25511
25889
  const mmParticipants = /* @__PURE__ */ new Set();
25512
- for (const [id, neighbors] of tableStarNeighbors) {
25513
- if (neighbors.size >= 2) mmParticipants.add(id);
25890
+ for (const [id, neighbors2] of tableStarNeighbors) {
25891
+ if (neighbors2.size >= 2) mmParticipants.add(id);
25514
25892
  }
25515
25893
  const indegreeValues = Object.values(indegreeMap);
25516
25894
  const mean = indegreeValues.reduce((a, b) => a + b, 0) / indegreeValues.length;
@@ -26181,7 +26559,8 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26181
26559
  controlsExpanded,
26182
26560
  onToggleDescriptions,
26183
26561
  onToggleControlsExpand,
26184
- exportMode = false
26562
+ exportMode = false,
26563
+ controlsHost
26185
26564
  } = options ?? {};
26186
26565
  d3Selection6.select(container).selectAll(":not([data-d3-tooltip])").remove();
26187
26566
  const width = exportDims?.width ?? container.clientWidth;
@@ -26199,7 +26578,11 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26199
26578
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26200
26579
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26201
26580
  const sTitleY = sctx.structural(TITLE_Y);
26202
- const sLegendHeight = sctx.structural(
26581
+ const reserveHasDescriptions = parsed.nodes.some(
26582
+ (n) => n.description && n.description.length > 0
26583
+ );
26584
+ const willRenderLegend = parsed.tagGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26585
+ const sLegendHeight = willRenderLegend ? sctx.structural(
26203
26586
  getMaxLegendReservedHeight(
26204
26587
  {
26205
26588
  groups: parsed.tagGroups,
@@ -26208,7 +26591,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26208
26591
  },
26209
26592
  width
26210
26593
  )
26211
- );
26594
+ ) : 0;
26212
26595
  const activeGroup = resolveActiveTagGroup(
26213
26596
  parsed.tagGroups,
26214
26597
  parsed.options["active-tag"],
@@ -26523,10 +26906,10 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26523
26906
  const hasDescriptions = parsed.nodes.some(
26524
26907
  (n) => n.description && n.description.length > 0
26525
26908
  );
26526
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions;
26909
+ const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26527
26910
  if (hasLegend) {
26528
26911
  let controlsGroup;
26529
- if (hasDescriptions && onToggleDescriptions) {
26912
+ if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
26530
26913
  controlsGroup = {
26531
26914
  toggles: [
26532
26915
  {
@@ -26544,7 +26927,14 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26544
26927
  groups: parsed.tagGroups,
26545
26928
  position: { placement: "top-center", titleRelation: "below-title" },
26546
26929
  mode: exportMode ? "export" : "preview",
26547
- ...controlsGroup !== void 0 && { controlsGroup }
26930
+ // Keep inactive sibling tag groups visible as collapsed pills so the user
26931
+ // can click one to flip the active colouring dimension (preview only —
26932
+ // export shows just the active group). Without this, declaring a second
26933
+ // tag group (e.g. Team) leaves it invisible whenever another group is
26934
+ // active. The app's BoxesAndLinesPreview already wires pill clicks.
26935
+ showInactivePills: true,
26936
+ ...controlsGroup !== void 0 && { controlsGroup },
26937
+ ...controlsHost !== void 0 && { controlsHost }
26548
26938
  };
26549
26939
  const legendState = {
26550
26940
  activeGroup,
@@ -27792,8 +28182,9 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27792
28182
  const containerHeight = exportDims?.height ?? (container.getBoundingClientRect().height || 600);
27793
28183
  d3Selection7.select(container).selectAll("*").remove();
27794
28184
  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);
28185
+ const appHosted = options?.controlsHost === "app";
27795
28186
  const hasControls = !!options?.onToggleColorByDepth || !!options?.onToggleDescriptions;
27796
- const hasLegend = parsed.tagGroups.length > 0 || hasControls;
28187
+ const hasLegend = parsed.tagGroups.length > 0 || hasControls && !appHosted;
27797
28188
  const fixedLegend = !isExport && hasLegend;
27798
28189
  const legendReserve = fixedLegend ? getMaxLegendReservedHeight(
27799
28190
  {
@@ -27887,7 +28278,10 @@ function renderMindmap(container, parsed, layout, palette, isDark, onClickItem,
27887
28278
  }),
27888
28279
  position: { placement: "top-center", titleRelation: "below-title" },
27889
28280
  mode: options?.exportMode ? "export" : "preview",
27890
- ...controlsToggles !== void 0 && { controlsGroup: controlsToggles }
28281
+ ...controlsToggles !== void 0 && { controlsGroup: controlsToggles },
28282
+ ...options?.controlsHost !== void 0 && {
28283
+ controlsHost: options.controlsHost
28284
+ }
27891
28285
  };
27892
28286
  const legendState = {
27893
28287
  activeGroup: options?.colorByDepth ? null : activeTagGroup !== void 0 ? activeTagGroup : parsed.options["active-tag"] ?? null,
@@ -28331,8 +28725,8 @@ function computeFieldAlignX(children) {
28331
28725
  for (const child of children) {
28332
28726
  if (child.metadata["_labelField"] === "true" && child.children.length >= 2) {
28333
28727
  const labelEl = child.children[0];
28334
- const labelWidth = labelEl.label.length * CHAR_WIDTH5;
28335
- maxLabelWidth = Math.max(maxLabelWidth, labelWidth);
28728
+ const labelWidth2 = labelEl.label.length * CHAR_WIDTH5;
28729
+ maxLabelWidth = Math.max(maxLabelWidth, labelWidth2);
28336
28730
  labelFieldCount++;
28337
28731
  }
28338
28732
  }
@@ -33296,7 +33690,7 @@ function hasRoles(node) {
33296
33690
  function computeNodeWidth2(node, expanded, options) {
33297
33691
  const badgeVal = node.computedConcurrentInvocations === 0 && node.computedInstances > 1 ? node.computedInstances : 0;
33298
33692
  const badgeLen = badgeVal > 0 ? `${badgeVal}x`.length + 2 : 0;
33299
- const labelWidth = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33693
+ const labelWidth2 = (node.label.length + badgeLen) * CHAR_WIDTH7 + PADDING_X3;
33300
33694
  const allKeys = [];
33301
33695
  if (node.computedRps > 0) allKeys.push("RPS");
33302
33696
  if (expanded) {
@@ -33340,7 +33734,7 @@ function computeNodeWidth2(node, expanded, options) {
33340
33734
  allKeys.push("overflow");
33341
33735
  }
33342
33736
  }
33343
- if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth);
33737
+ if (allKeys.length === 0) return Math.max(MIN_NODE_WIDTH2, labelWidth2);
33344
33738
  const maxKeyLen = Math.max(...allKeys.map((k) => k.length));
33345
33739
  let maxRowWidth = 0;
33346
33740
  if (node.computedRps > 0) {
@@ -33428,7 +33822,7 @@ function computeNodeWidth2(node, expanded, options) {
33428
33822
  truncated.length * META_CHAR_WIDTH3 + PADDING_X3
33429
33823
  );
33430
33824
  }
33431
- return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20, descWidth);
33825
+ return Math.max(MIN_NODE_WIDTH2, labelWidth2, maxRowWidth + 20, descWidth);
33432
33826
  }
33433
33827
  function computeNodeHeight2(node, expanded, options) {
33434
33828
  const propCount = countDisplayProps(node, expanded, options);
@@ -34977,8 +35371,9 @@ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
34977
35371
  }
34978
35372
  return groups;
34979
35373
  }
34980
- function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false) {
35374
+ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDark, activeGroup, playback, exportMode = false, controlsHost) {
34981
35375
  if (legendGroups.length === 0 && !playback) return;
35376
+ const appHostedPlayback = controlsHost === "app" && !!playback;
34982
35377
  const legendG = rootSvg.append("g").attr("transform", `translate(0, ${legendY})`);
34983
35378
  if (activeGroup) {
34984
35379
  legendG.attr("data-legend-active", activeGroup.toLowerCase());
@@ -34987,14 +35382,29 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
34987
35382
  name: g.name,
34988
35383
  entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
34989
35384
  }));
34990
- if (playback) {
35385
+ if (playback && !appHostedPlayback) {
34991
35386
  allGroups.push({ name: "Playback", entries: [] });
34992
35387
  }
34993
35388
  const legendConfig = {
34994
35389
  groups: allGroups,
34995
35390
  position: { placement: "top-center", titleRelation: "below-title" },
34996
35391
  mode: exportMode ? "export" : "preview",
34997
- showEmptyGroups: true
35392
+ showEmptyGroups: true,
35393
+ ...appHostedPlayback && {
35394
+ controlsHost: "app",
35395
+ controlsGroup: {
35396
+ toggles: [
35397
+ {
35398
+ id: "playback",
35399
+ type: "toggle",
35400
+ label: "Playback",
35401
+ active: true,
35402
+ onToggle: () => {
35403
+ }
35404
+ }
35405
+ ]
35406
+ }
35407
+ }
34998
35408
  };
34999
35409
  const legendState = { activeGroup };
35000
35410
  renderLegendD3(
@@ -35045,8 +35455,9 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
35045
35455
  }
35046
35456
  }
35047
35457
  }
35048
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
35458
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes, controlsHost) {
35049
35459
  d3Selection11.select(container).selectAll(":not([data-d3-tooltip])").remove();
35460
+ const appHostedPlayback = controlsHost === "app" && !!playback;
35050
35461
  const ctx = ScaleContext.identity();
35051
35462
  const sc = buildScaledConstants(ctx);
35052
35463
  const legendGroups = computeInfraLegendGroups(
@@ -35055,7 +35466,7 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35055
35466
  palette,
35056
35467
  layout.edges
35057
35468
  );
35058
- const hasLegend = legendGroups.length > 0 || !!playback;
35469
+ const hasLegend = legendGroups.length > 0 || !!playback && !appHostedPlayback;
35059
35470
  const fixedLegend = !exportMode && hasLegend;
35060
35471
  const legendDynamicH = hasLegend ? getMaxLegendReservedHeight(
35061
35472
  {
@@ -35199,7 +35610,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35199
35610
  isDark,
35200
35611
  activeGroup ?? null,
35201
35612
  playback ?? void 0,
35202
- exportMode
35613
+ exportMode,
35614
+ controlsHost
35203
35615
  );
35204
35616
  legendSvg.selectAll(".infra-legend-group").style("pointer-events", "auto");
35205
35617
  } else {
@@ -35212,7 +35624,8 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
35212
35624
  isDark,
35213
35625
  activeGroup ?? null,
35214
35626
  playback ?? void 0,
35215
- exportMode
35627
+ exportMode,
35628
+ controlsHost
35216
35629
  );
35217
35630
  }
35218
35631
  }
@@ -43070,6 +43483,9 @@ function renderTechRadar(container, parsed, palette, isDark, onClickItem, export
43070
43483
  onToggle: (active) => options.onToggleListing(active)
43071
43484
  }
43072
43485
  ]
43486
+ },
43487
+ ...options.controlsHost !== void 0 && {
43488
+ controlsHost: options.controlsHost
43073
43489
  }
43074
43490
  };
43075
43491
  const legendState = {
@@ -44893,7 +45309,7 @@ function computeCycleLayout(parsed, options) {
44893
45309
  const circleNodes = parsed.options["circle-nodes"] === "true";
44894
45310
  const nodeDims = parsed.nodes.map((node) => {
44895
45311
  const hasDesc = !hideDescriptions && node.description.length > 0;
44896
- const labelWidth = Math.max(
45312
+ const labelWidth2 = Math.max(
44897
45313
  MIN_NODE_WIDTH4,
44898
45314
  node.label.length * LABEL_CHAR_W + NODE_PAD_X * 2
44899
45315
  );
@@ -44902,12 +45318,12 @@ function computeCycleLayout(parsed, options) {
44902
45318
  }
44903
45319
  if (!hasDesc) {
44904
45320
  return {
44905
- width: Math.min(MAX_NODE_WIDTH3, labelWidth),
45321
+ width: Math.min(MAX_NODE_WIDTH3, labelWidth2),
44906
45322
  height: PLAIN_NODE_HEIGHT,
44907
45323
  wrappedDesc: []
44908
45324
  };
44909
45325
  }
44910
- return chooseDescribedRectDims(node.description, labelWidth);
45326
+ return chooseDescribedRectDims(node.description, labelWidth2);
44911
45327
  });
44912
45328
  if (circleNodes) {
44913
45329
  const maxDiam = Math.max(...nodeDims.map((d) => d.width));
@@ -45103,10 +45519,10 @@ function computeCycleLayout(parsed, options) {
45103
45519
  scale
45104
45520
  };
45105
45521
  }
45106
- function chooseDescribedRectDims(description, labelWidth) {
45522
+ function chooseDescribedRectDims(description, labelWidth2) {
45107
45523
  const minW = Math.min(
45108
45524
  MAX_NODE_WIDTH3,
45109
- Math.max(MIN_NODE_WIDTH4, labelWidth, DESC_MIN_WIDTH)
45525
+ Math.max(MIN_NODE_WIDTH4, labelWidth2, DESC_MIN_WIDTH)
45110
45526
  );
45111
45527
  let best = null;
45112
45528
  let bestScore = Infinity;
@@ -45534,7 +45950,8 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45534
45950
  const hideDescriptions = (renderOptions?.hideDescriptions ?? false) || parsed.options["no-descriptions"] === "true" || viewState?.hd === true;
45535
45951
  const showDescriptions = !hideDescriptions;
45536
45952
  const hasDescriptions = parsed.nodes.some((n) => n.description.length > 0) || parsed.edges.some((e) => e.description.length > 0);
45537
- const hasLegend = hasDescriptions && !!renderOptions?.onToggleDescriptions;
45953
+ const appHostedControls = renderOptions?.controlsHost === "app";
45954
+ const hasLegend = !appHostedControls && hasDescriptions && !!renderOptions?.onToggleDescriptions;
45538
45955
  const showTitle = !!parsed.title && parsed.options["no-title"] !== "on";
45539
45956
  const legendOffset = hasLegend ? sLegendHeight : 0;
45540
45957
  const layoutHeight = height - (showTitle ? sTitleAreaHeight : 0) - legendOffset;
@@ -45571,7 +45988,10 @@ function renderCycle(container, parsed, palette, isDark, onClickItem, exportDims
45571
45988
  groups: [],
45572
45989
  position: { placement: "top-center", titleRelation: "below-title" },
45573
45990
  mode: renderOptions?.exportMode ? "export" : "preview",
45574
- controlsGroup
45991
+ controlsGroup,
45992
+ ...renderOptions?.controlsHost !== void 0 && {
45993
+ controlsHost: renderOptions.controlsHost
45994
+ }
45575
45995
  };
45576
45996
  const legendState = {
45577
45997
  activeGroup: null,
@@ -45842,6 +46262,107 @@ function featureIndex(topo) {
45842
46262
  }
45843
46263
  return idx;
45844
46264
  }
46265
+ function buildAdjacency(topo) {
46266
+ const cached = adjacencyCache.get(topo);
46267
+ if (cached) return cached;
46268
+ const geometries = geomObject(topo).geometries;
46269
+ const nb = (0, import_topojson_client.neighbors)(geometries);
46270
+ const sets = /* @__PURE__ */ new Map();
46271
+ geometries.forEach((g, i) => {
46272
+ if (!g.type || g.type === "null") return;
46273
+ let set = sets.get(g.id);
46274
+ if (!set) {
46275
+ set = /* @__PURE__ */ new Set();
46276
+ sets.set(g.id, set);
46277
+ }
46278
+ for (const j of nb[i] ?? []) {
46279
+ const nid = geometries[j]?.id;
46280
+ if (nid && nid !== g.id) set.add(nid);
46281
+ }
46282
+ });
46283
+ const out = /* @__PURE__ */ new Map();
46284
+ for (const [iso, set] of sets) out.set(iso, [...set].sort());
46285
+ adjacencyCache.set(topo, out);
46286
+ return out;
46287
+ }
46288
+ function decodeFeatures(topo) {
46289
+ return geomObject(topo).geometries.map((g) => {
46290
+ const f = (0, import_topojson_client.feature)(topo, g);
46291
+ return {
46292
+ type: "Feature",
46293
+ id: g.id,
46294
+ properties: g.properties,
46295
+ geometry: f.geometry
46296
+ };
46297
+ });
46298
+ }
46299
+ function pointInRing(lon, lat, ring) {
46300
+ let inside = false;
46301
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46302
+ const xi = ring[i][0];
46303
+ const yi = ring[i][1];
46304
+ const xj = ring[j][0];
46305
+ const yj = ring[j][1];
46306
+ const intersect = yi > lat !== yj > lat && lon < (xj - xi) * (lat - yi) / (yj - yi) + xi;
46307
+ if (intersect) inside = !inside;
46308
+ }
46309
+ return inside;
46310
+ }
46311
+ function pointOnRingEdge(lon, lat, ring) {
46312
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
46313
+ const xi = ring[i][0];
46314
+ const yi = ring[i][1];
46315
+ const xj = ring[j][0];
46316
+ const yj = ring[j][1];
46317
+ if (lon < Math.min(xi, xj) - EDGE_EPS || lon > Math.max(xi, xj) + EDGE_EPS)
46318
+ continue;
46319
+ if (lat < Math.min(yi, yj) - EDGE_EPS || lat > Math.max(yi, yj) + EDGE_EPS)
46320
+ continue;
46321
+ const cross = (xj - xi) * (lat - yi) - (yj - yi) * (lon - xi);
46322
+ if (Math.abs(cross) <= EDGE_EPS) return true;
46323
+ }
46324
+ return false;
46325
+ }
46326
+ function pointInGeometry(geometry, lon, lat) {
46327
+ const g = geometry;
46328
+ if (!g) return false;
46329
+ const polys = g.type === "Polygon" ? [g.coordinates] : g.type === "MultiPolygon" ? g.coordinates : [];
46330
+ for (const rings of polys) {
46331
+ if (!rings.length) continue;
46332
+ if (pointOnRingEdge(lon, lat, rings[0])) return true;
46333
+ if (!pointInRing(lon, lat, rings[0])) continue;
46334
+ let inHole = false;
46335
+ for (let h = 1; h < rings.length; h++) {
46336
+ if (pointInRing(lon, lat, rings[h]) && !pointOnRingEdge(lon, lat, rings[h])) {
46337
+ inHole = true;
46338
+ break;
46339
+ }
46340
+ }
46341
+ if (!inHole) return true;
46342
+ }
46343
+ return false;
46344
+ }
46345
+ function regionAt(lonLat, countries, states) {
46346
+ const lon = lonLat[0];
46347
+ const lat = lonLat[1];
46348
+ let country = null;
46349
+ for (const f of countries) {
46350
+ if (pointInGeometry(f.geometry, lon, lat)) {
46351
+ country = { iso: f.id, name: f.properties.name };
46352
+ break;
46353
+ }
46354
+ }
46355
+ let state = null;
46356
+ if (country?.iso === "US" && states) {
46357
+ for (const f of states) {
46358
+ if (pointInGeometry(f.geometry, lon, lat)) {
46359
+ state = { iso: f.id, name: f.properties.name };
46360
+ break;
46361
+ }
46362
+ }
46363
+ }
46364
+ return { country, state };
46365
+ }
45845
46366
  function featureBbox(topo, geomId) {
45846
46367
  const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
45847
46368
  if (!geom) return null;
@@ -45853,6 +46374,74 @@ function featureBbox(topo, geomId) {
45853
46374
  [b[1][0], b[1][1]]
45854
46375
  ];
45855
46376
  }
46377
+ function explodePolygons(gj) {
46378
+ const g = gj.geometry ?? gj;
46379
+ const t = g.type;
46380
+ const coords = g.coordinates;
46381
+ if (t === "Polygon") {
46382
+ return [
46383
+ { type: "Feature", geometry: { type: "Polygon", coordinates: coords } }
46384
+ ];
46385
+ }
46386
+ if (t === "MultiPolygon") {
46387
+ return coords.map((rings) => ({
46388
+ type: "Feature",
46389
+ geometry: { type: "Polygon", coordinates: rings }
46390
+ }));
46391
+ }
46392
+ return [];
46393
+ }
46394
+ function bboxGap(a, b) {
46395
+ const lonGap = Math.max(0, a[0][0] - b[1][0], b[0][0] - a[1][0]);
46396
+ const latGap = Math.max(0, a[0][1] - b[1][1], b[0][1] - a[1][1]);
46397
+ return Math.max(lonGap, latGap);
46398
+ }
46399
+ function featureBboxPrimary(topo, geomId) {
46400
+ const geom = geomObject(topo).geometries.find((g) => g.id === geomId);
46401
+ if (!geom) return null;
46402
+ const gj = (0, import_topojson_client.feature)(topo, geom);
46403
+ const parts = explodePolygons(gj);
46404
+ if (parts.length <= 1) return featureBbox(topo, geomId);
46405
+ const polys = parts.map((p) => {
46406
+ const b = (0, import_d3_geo.geoBounds)(p);
46407
+ if (!b || !Number.isFinite(b[0][0])) return null;
46408
+ const wraps = b[1][0] < b[0][0];
46409
+ const bbox = [
46410
+ [b[0][0], b[0][1]],
46411
+ [b[1][0], b[1][1]]
46412
+ ];
46413
+ return { bbox, area: (0, import_d3_geo.geoArea)(p), wraps };
46414
+ }).filter(
46415
+ (p) => p !== null
46416
+ );
46417
+ if (polys.length <= 1 || polys.some((p) => p.wraps))
46418
+ return featureBbox(topo, geomId);
46419
+ const maxArea = Math.max(...polys.map((p) => p.area));
46420
+ const anchor = polys.find((p) => p.area === maxArea);
46421
+ const cluster = [
46422
+ [anchor.bbox[0][0], anchor.bbox[0][1]],
46423
+ [anchor.bbox[1][0], anchor.bbox[1][1]]
46424
+ ];
46425
+ const remaining = polys.filter((p) => p !== anchor);
46426
+ let added = true;
46427
+ while (added) {
46428
+ added = false;
46429
+ for (let i = remaining.length - 1; i >= 0; i--) {
46430
+ const p = remaining[i];
46431
+ const near = bboxGap(p.bbox, cluster) <= DETACH_GAP_DEG;
46432
+ const large = p.area >= DETACH_AREA_FRAC * maxArea;
46433
+ if (near || large) {
46434
+ cluster[0][0] = Math.min(cluster[0][0], p.bbox[0][0]);
46435
+ cluster[0][1] = Math.min(cluster[0][1], p.bbox[0][1]);
46436
+ cluster[1][0] = Math.max(cluster[1][0], p.bbox[1][0]);
46437
+ cluster[1][1] = Math.max(cluster[1][1], p.bbox[1][1]);
46438
+ remaining.splice(i, 1);
46439
+ added = true;
46440
+ }
46441
+ }
46442
+ }
46443
+ return cluster;
46444
+ }
45856
46445
  function unionExtent(boxes, points) {
45857
46446
  const lats = [];
45858
46447
  const lons = [];
@@ -45891,13 +46480,17 @@ function unionLongitudes(lons) {
45891
46480
  }
45892
46481
  return { west: pts[gapIdx], east: pts[gapIdx - 1] + 360 };
45893
46482
  }
45894
- var import_topojson_client, import_d3_geo, fold;
46483
+ var import_topojson_client, import_d3_geo, fold, adjacencyCache, EDGE_EPS, DETACH_GAP_DEG, DETACH_AREA_FRAC;
45895
46484
  var init_geo = __esm({
45896
46485
  "src/map/geo.ts"() {
45897
46486
  "use strict";
45898
46487
  import_topojson_client = require("topojson-client");
45899
46488
  import_d3_geo = require("d3-geo");
45900
46489
  fold = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
46490
+ adjacencyCache = /* @__PURE__ */ new WeakMap();
46491
+ EDGE_EPS = 1e-9;
46492
+ DETACH_GAP_DEG = 10;
46493
+ DETACH_AREA_FRAC = 0.25;
45901
46494
  }
45902
46495
  });
45903
46496
 
@@ -45915,6 +46508,12 @@ function looksUS(lat, lon) {
45915
46508
  if (lat < 15 || lat > 72) return false;
45916
46509
  return lon >= -180 && lon <= -64 || lon >= 172;
45917
46510
  }
46511
+ function looksNorthAmericaNeighbor(lat, lon) {
46512
+ return lat >= 14 && lat <= 72 && lon >= -141 && lon <= -52;
46513
+ }
46514
+ function isWholeSphere(bb) {
46515
+ return bb[0][0] <= -179 && bb[1][0] >= 179 && bb[0][1] <= -89 && bb[1][1] >= 89;
46516
+ }
45918
46517
  function resolveMap(parsed, data) {
45919
46518
  const diagnostics = [...parsed.diagnostics];
45920
46519
  const err = (line12, message, code) => {
@@ -45925,9 +46524,6 @@ function resolveMap(parsed, data) {
45925
46524
  };
45926
46525
  const result = {
45927
46526
  title: parsed.title,
45928
- ...parsed.directives.subtitle !== void 0 && {
45929
- subtitle: parsed.directives.subtitle
45930
- },
45931
46527
  ...parsed.directives.caption !== void 0 && {
45932
46528
  caption: parsed.directives.caption
45933
46529
  },
@@ -45937,7 +46533,7 @@ function resolveMap(parsed, data) {
45937
46533
  // renderer's job (step 4) — the resolver only carries `tags` + `tagGroups`
45938
46534
  // through; it never resolves a tag value to a palette color (#10).
45939
46535
  directives: { ...parsed.directives },
45940
- basemaps: { world: "coarse", subdivisions: [] },
46536
+ basemaps: { world: "detail", subdivisions: [] },
45941
46537
  regions: [],
45942
46538
  pois: [],
45943
46539
  edges: [],
@@ -45946,7 +46542,8 @@ function resolveMap(parsed, data) {
45946
46542
  [-180, -85],
45947
46543
  [180, 85]
45948
46544
  ],
45949
- projection: "natural-earth",
46545
+ projection: "equirectangular",
46546
+ poiFrameContainers: [],
45950
46547
  diagnostics,
45951
46548
  error: parsed.error
45952
46549
  };
@@ -45956,7 +46553,10 @@ function resolveMap(parsed, data) {
45956
46553
  ...[...countryIndex.values()].map((v) => v.name),
45957
46554
  ...[...usStateIndex.values()].map((v) => v.name)
45958
46555
  ];
45959
- const usScoped = parsed.directives.region === "us-states" || parsed.directives.defaultCountry?.toUpperCase() === "US" || parsed.regions.some((r) => {
46556
+ const localeRaw = parsed.directives.locale?.toUpperCase();
46557
+ const localeCountry = localeRaw ? localeRaw.split("-")[0] : void 0;
46558
+ const localeSubdivision = localeRaw && /^[A-Z]{2}-/.test(localeRaw) ? localeRaw : void 0;
46559
+ const usScoped = localeCountry === "US" || parsed.regions.some((r) => {
45960
46560
  const f = fold(r.name);
45961
46561
  return usStateIndex.has(f) && !countryIndex.has(f);
45962
46562
  }) || parsed.regions.some(
@@ -46001,12 +46601,12 @@ function resolveMap(parsed, data) {
46001
46601
  chosen = { ...inState, layer: "us-state" };
46002
46602
  } else {
46003
46603
  chosen = { ...inCountry, layer: "country" };
46604
+ warn(
46605
+ r.lineNumber,
46606
+ `"${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}").`,
46607
+ "W_MAP_REGION_AMBIGUOUS"
46608
+ );
46004
46609
  }
46005
- warn(
46006
- r.lineNumber,
46007
- `"${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}").`,
46008
- "W_MAP_REGION_AMBIGUOUS"
46009
- );
46010
46610
  } else if (inState) {
46011
46611
  chosen = { ...inState, layer: "us-state" };
46012
46612
  } else if (inCountry) {
@@ -46030,6 +46630,7 @@ function resolveMap(parsed, data) {
46030
46630
  name: chosen.name,
46031
46631
  layer: chosen.layer,
46032
46632
  ...r.value !== void 0 && { value: r.value },
46633
+ ...r.color !== void 0 && { color: r.color },
46033
46634
  tags: r.tags,
46034
46635
  meta: r.meta,
46035
46636
  lineNumber: r.lineNumber
@@ -46106,7 +46707,7 @@ function resolveMap(parsed, data) {
46106
46707
  if (!scope)
46107
46708
  warn(
46108
46709
  line12,
46109
- `"${name}" is ambiguous \u2014 resolved to the most-populous match.`,
46710
+ `"${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.`,
46110
46711
  "W_MAP_AMBIGUOUS_NAME"
46111
46712
  );
46112
46713
  }
@@ -46119,17 +46720,21 @@ function resolveMap(parsed, data) {
46119
46720
  return fold(pos.name);
46120
46721
  };
46121
46722
  const poiCountries = [];
46122
- let anyNonUsPoi = false;
46723
+ let anyUsPoi = false;
46724
+ let anyNonNaPoi = false;
46123
46725
  const noteCountry = (iso) => {
46124
46726
  if (iso) {
46125
46727
  poiCountries.push(iso);
46126
- if (iso !== "US") anyNonUsPoi = true;
46728
+ if (iso === "US") anyUsPoi = true;
46729
+ if (iso !== "US" && iso !== "CA" && iso !== "MX") anyNonNaPoi = true;
46127
46730
  }
46128
46731
  };
46129
46732
  const deferred = [];
46130
46733
  for (const p of parsed.pois) {
46131
46734
  if (p.pos.kind === "coords") {
46132
- if (!looksUS(p.pos.lat, p.pos.lon)) anyNonUsPoi = true;
46735
+ if (looksUS(p.pos.lat, p.pos.lon)) anyUsPoi = true;
46736
+ else if (!looksNorthAmericaNeighbor(p.pos.lat, p.pos.lon))
46737
+ anyNonNaPoi = true;
46133
46738
  addResolvedPoi(p.pos.lat, p.pos.lon, p);
46134
46739
  continue;
46135
46740
  }
@@ -46147,14 +46752,15 @@ function resolveMap(parsed, data) {
46147
46752
  deferred.push(p);
46148
46753
  }
46149
46754
  }
46150
- const inferredCountry = parsed.directives.defaultCountry?.toUpperCase() ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46755
+ const inferredCountry = localeCountry ?? mostCommonCountry(regions, poiCountries) ?? void 0;
46756
+ const inferredScope = localeSubdivision ?? inferredCountry;
46151
46757
  for (const p of deferred) {
46152
46758
  if (p.pos.kind !== "name") continue;
46153
46759
  const got = lookupName(
46154
46760
  p.pos.name,
46155
46761
  p.pos.scope,
46156
46762
  p.lineNumber,
46157
- inferredCountry,
46763
+ inferredScope,
46158
46764
  true
46159
46765
  );
46160
46766
  if (got.kind === "ok") {
@@ -46171,6 +46777,7 @@ function resolveMap(parsed, data) {
46171
46777
  lat,
46172
46778
  lon,
46173
46779
  ...p.label !== void 0 && { label: p.label },
46780
+ ...p.color !== void 0 && { color: p.color },
46174
46781
  tags: p.tags,
46175
46782
  meta: p.meta,
46176
46783
  lineNumber: p.lineNumber
@@ -46223,7 +46830,8 @@ function resolveMap(parsed, data) {
46223
46830
  const meta = sizeValue !== void 0 ? { value: sizeValue } : {};
46224
46831
  if (pos.kind === "coords") {
46225
46832
  const id = alias ? fold(alias) : `@${pos.lat},${pos.lon}`;
46226
- if (!looksUS(pos.lat, pos.lon)) anyNonUsPoi = true;
46833
+ if (looksUS(pos.lat, pos.lon)) anyUsPoi = true;
46834
+ else if (!looksNorthAmericaNeighbor(pos.lat, pos.lon)) anyNonNaPoi = true;
46227
46835
  if (!registry.has(id)) {
46228
46836
  registerPoi(
46229
46837
  id,
@@ -46246,7 +46854,7 @@ function resolveMap(parsed, data) {
46246
46854
  if (registry.has(f)) return f;
46247
46855
  const aliased = declaredByName.get(f);
46248
46856
  if (aliased) return aliased;
46249
- const got = lookupName(pos.name, pos.scope, line12, inferredCountry, true);
46857
+ const got = lookupName(pos.name, pos.scope, line12, inferredScope, true);
46250
46858
  if (got.kind !== "ok") return null;
46251
46859
  noteCountry(got.iso);
46252
46860
  registerPoi(
@@ -46303,9 +46911,12 @@ function resolveMap(parsed, data) {
46303
46911
  }
46304
46912
  routes.push({ stopIds, legs, lineNumber: rt.lineNumber });
46305
46913
  }
46914
+ const hasUsContent = usSubdivisionReferenced || anyUsPoi || localeCountry === "US";
46915
+ const usOriented = !anyNonNaPoi && !regions.some(
46916
+ (r) => r.layer === "country" && !["US", "CA", "MX"].includes(r.iso)
46917
+ ) && hasUsContent;
46306
46918
  const subdivisions = [];
46307
- if (usSubdivisionReferenced || parsed.directives.region === "us-states")
46308
- subdivisions.push("us-states");
46919
+ if (usSubdivisionReferenced || usOriented) subdivisions.push("us-states");
46309
46920
  const regionBoxes = [];
46310
46921
  for (const ref of referencedRegionIds) {
46311
46922
  const bb = featureBbox(data.usStates, ref.id);
@@ -46313,7 +46924,7 @@ function resolveMap(parsed, data) {
46313
46924
  }
46314
46925
  for (const r of regions) {
46315
46926
  if (r.layer === "country") {
46316
- const bb = featureBbox(data.worldCoarse, r.iso);
46927
+ const bb = featureBboxPrimary(data.worldCoarse, r.iso);
46317
46928
  if (bb) regionBoxes.push(bb);
46318
46929
  }
46319
46930
  }
@@ -46323,23 +46934,56 @@ function resolveMap(parsed, data) {
46323
46934
  [-180, -85],
46324
46935
  [180, 85]
46325
46936
  ];
46326
- let extent2 = unioned ? pad(unioned, PAD_FRACTION) : DEFAULT_EXTENT;
46937
+ const basePad = regions.length > 0 ? REGION_PAD_FRACTION : PAD_FRACTION;
46938
+ let extent2 = unioned ? pad(unioned, basePad) : DEFAULT_EXTENT;
46939
+ const isPoiOnly = pois.length > 0 && regions.length === 0;
46940
+ const containerRegionIds = [];
46941
+ if (isPoiOnly) {
46942
+ const countries = decodeFeatures(data.worldDetail);
46943
+ const states = decodeFeatures(data.usStates);
46944
+ const seen = /* @__PURE__ */ new Set();
46945
+ const containerBoxes = [];
46946
+ for (const p of pois) {
46947
+ const { country, state } = regionAt([p.lon, p.lat], countries, states);
46948
+ const id = state?.iso ?? country?.iso;
46949
+ if (!id || seen.has(id)) continue;
46950
+ seen.add(id);
46951
+ containerRegionIds.push(id);
46952
+ const bb = state ? featureBbox(data.usStates, id) : featureBboxPrimary(data.worldCoarse, id);
46953
+ if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46954
+ }
46955
+ const containerUnion = unionExtent(containerBoxes, points);
46956
+ if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46957
+ }
46958
+ if (isPoiOnly) {
46959
+ const cx = (extent2[0][0] + extent2[1][0]) / 2;
46960
+ const cy = (extent2[0][1] + extent2[1][1]) / 2;
46961
+ const lon = extent2[1][0] - extent2[0][0];
46962
+ const lat = extent2[1][1] - extent2[0][1];
46963
+ const longer = Math.max(lon, lat);
46964
+ if (longer > 0 && longer < POI_ZOOM_FLOOR_DEG) {
46965
+ const k = POI_ZOOM_FLOOR_DEG / longer;
46966
+ const halfLon = lon * k / 2;
46967
+ const halfLat = lat * k / 2;
46968
+ extent2 = [
46969
+ [cx - halfLon, cy - halfLat],
46970
+ [cx + halfLon, cy + halfLat]
46971
+ ];
46972
+ }
46973
+ }
46327
46974
  const lonSpan = extent2[1][0] - extent2[0][0];
46328
46975
  const latSpan = extent2[1][1] - extent2[0][1];
46329
46976
  const span = Math.max(lonSpan, latSpan);
46330
- const usDominant = (subdivisions.includes("us-states") || regions.some((r) => r.layer === "us-state")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46977
+ const maxAbsLat = Math.max(Math.abs(extent2[0][1]), Math.abs(extent2[1][1]));
46331
46978
  let projection;
46332
- const override = parsed.directives.projection;
46333
- if (override === "equirectangular" || override === "natural-earth" || override === "albers-usa" || override === "mercator") {
46334
- projection = override;
46335
- } else if (usDominant) {
46979
+ if (isPoiOnly && usOriented && lonSpan < US_NATIONAL_LON_SPAN) {
46980
+ projection = "mercator";
46981
+ } else if (usOriented) {
46336
46982
  projection = "albers-usa";
46337
- } else if (span > WORLD_SPAN) {
46983
+ } else if (span > WORLD_SPAN || maxAbsLat > MERCATOR_MAX_LAT) {
46338
46984
  projection = "equirectangular";
46339
- } else if (span < MERCATOR_MAX_SPAN) {
46340
- projection = "mercator";
46341
46985
  } else {
46342
- projection = "equirectangular";
46986
+ projection = "mercator";
46343
46987
  }
46344
46988
  if (lonSpan >= 180) {
46345
46989
  extent2 = [
@@ -46352,11 +46996,20 @@ function resolveMap(parsed, data) {
46352
46996
  result.edges = edges;
46353
46997
  result.routes = routes;
46354
46998
  result.basemaps = {
46355
- world: span > WORLD_SPAN ? "coarse" : "detail",
46999
+ // Tier is intentionally pinned to detail (50m) at ALL scales. Diagrammo maps
47000
+ // are presentational (palette tints, relief hachures, POI hubs), not
47001
+ // survey-grade — recognizability > generalization: 110m coarse drops the
47002
+ // Italian boot to a stump at world scale. `WORLD_SPAN` lives on only for the
47003
+ // projection decision (the `usOriented`/`span > WORLD_SPAN` chain above); it
47004
+ // no longer gates basemap resolution.
47005
+ // `worldCoarse` is still loaded — it's the authoritative name/bbox index
47006
+ // (featureIndex, featureBboxPrimary), not dead code.
47007
+ world: "detail",
46356
47008
  subdivisions
46357
47009
  };
46358
47010
  result.extent = extent2;
46359
47011
  result.projection = projection;
47012
+ result.poiFrameContainers = containerRegionIds;
46360
47013
  result.error = parsed.error ?? firstError(diagnostics);
46361
47014
  return result;
46362
47015
  }
@@ -46393,17 +47046,20 @@ function firstError(diags) {
46393
47046
  const e = diags.find((d) => d.severity === "error");
46394
47047
  return e ? formatDgmoError(e) : null;
46395
47048
  }
46396
- var WORLD_SPAN, MERCATOR_MAX_SPAN, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES, US_STATE_POSTAL;
47049
+ 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;
46397
47050
  var init_resolver2 = __esm({
46398
47051
  "src/map/resolver.ts"() {
46399
47052
  "use strict";
46400
47053
  init_diagnostics();
46401
47054
  init_geo();
46402
47055
  WORLD_SPAN = 90;
46403
- MERCATOR_MAX_SPAN = 25;
47056
+ MERCATOR_MAX_LAT = 80;
46404
47057
  PAD_FRACTION = 0.05;
47058
+ REGION_PAD_FRACTION = 0.12;
46405
47059
  WORLD_LAT_SOUTH = -58;
46406
47060
  WORLD_LAT_NORTH = 78;
47061
+ POI_ZOOM_FLOOR_DEG = 7;
47062
+ US_NATIONAL_LON_SPAN = 48;
46407
47063
  REGION_ALIASES = {
46408
47064
  // Common everyday names → the Natural-Earth display name actually shipped.
46409
47065
  "united states": "united states of america",
@@ -46481,112 +47137,269 @@ var init_resolver2 = __esm({
46481
47137
  }
46482
47138
  });
46483
47139
 
46484
- // src/map/load-data.ts
46485
- var load_data_exports = {};
46486
- __export(load_data_exports, {
46487
- loadMapData: () => loadMapData
47140
+ // src/map/colorize.ts
47141
+ function assignColors(isos, adjacency) {
47142
+ const sorted = [...isos].sort();
47143
+ const byIso = /* @__PURE__ */ new Map();
47144
+ let maxIndex = -1;
47145
+ for (const iso of sorted) {
47146
+ const taken = /* @__PURE__ */ new Set();
47147
+ for (const n of adjacency.get(iso) ?? []) {
47148
+ const c = byIso.get(n);
47149
+ if (c !== void 0) taken.add(c);
47150
+ }
47151
+ let h = 0;
47152
+ while (taken.has(h)) h++;
47153
+ byIso.set(iso, h);
47154
+ if (h > maxIndex) maxIndex = h;
47155
+ }
47156
+ return { byIso, huesNeeded: maxIndex + 1 };
47157
+ }
47158
+ var init_colorize = __esm({
47159
+ "src/map/colorize.ts"() {
47160
+ "use strict";
47161
+ }
46488
47162
  });
46489
- async function loadNodeBuiltins() {
46490
- const [{ readFile }, { fileURLToPath }, { dirname: dirname2, resolve }] = await Promise.all([
46491
- import("fs/promises"),
46492
- import("url"),
46493
- import("path")
46494
- ]);
46495
- return { readFile, fileURLToPath, dirname: dirname2, resolve };
46496
- }
46497
- async function readJson(nb, dir, name) {
46498
- return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
46499
- }
46500
- async function firstExistingDir(nb, baseDir) {
46501
- for (const rel of CANDIDATE_DIRS) {
46502
- const dir = nb.resolve(baseDir, rel);
46503
- try {
46504
- await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
46505
- return dir;
46506
- } catch {
47163
+
47164
+ // src/map/context-labels.ts
47165
+ function tierBand(maxSpanDeg) {
47166
+ if (maxSpanDeg >= 90) return "world";
47167
+ if (maxSpanDeg >= 20) return "continental";
47168
+ if (maxSpanDeg >= 5) return "regional";
47169
+ return "local";
47170
+ }
47171
+ function labelBudget(width, height, band) {
47172
+ const bandCap = {
47173
+ world: 6,
47174
+ continental: 5,
47175
+ regional: 4,
47176
+ local: 3
47177
+ };
47178
+ const area2 = Math.floor(Math.sqrt(Math.max(0, width * height)) / 150);
47179
+ return Math.max(0, Math.min(area2, bandCap[band]));
47180
+ }
47181
+ function waterEligible(tier, kind, band) {
47182
+ switch (band) {
47183
+ case "world":
47184
+ return tier <= 1 && (kind === "ocean" || kind === "sea");
47185
+ case "continental":
47186
+ return tier <= 2;
47187
+ case "regional":
47188
+ return tier <= 3;
47189
+ case "local":
47190
+ return tier <= 4;
47191
+ }
47192
+ }
47193
+ function insideViewport(p, width, height) {
47194
+ return !!p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && p[0] >= 0 && p[0] <= width && p[1] >= 0 && p[1] <= height;
47195
+ }
47196
+ function labelWidth(text, letterSpacing) {
47197
+ const spacing = letterSpacing > 0 ? Math.max(0, text.length - 1) * letterSpacing : 0;
47198
+ return measureLegendText(text, FONT) + spacing + 2 * PADX;
47199
+ }
47200
+ function wrapLabel2(text, letterSpacing) {
47201
+ const words = text.split(/\s+/).filter(Boolean);
47202
+ if (words.length <= 1) return [text];
47203
+ const maxLines = words.length >= 4 ? 3 : 2;
47204
+ const n = words.length;
47205
+ let best = null;
47206
+ for (let mask = 0; mask < 1 << n - 1; mask++) {
47207
+ const lines = [];
47208
+ let cur = [words[0]];
47209
+ for (let i = 1; i < n; i++) {
47210
+ if (mask & 1 << i - 1) {
47211
+ lines.push(cur.join(" "));
47212
+ cur = [words[i]];
47213
+ } else cur.push(words[i]);
46507
47214
  }
47215
+ lines.push(cur.join(" "));
47216
+ if (lines.length > maxLines) continue;
47217
+ const cost = Math.round(
47218
+ Math.max(...lines.map((l) => labelWidth(l, letterSpacing)))
47219
+ );
47220
+ const head = labelWidth(lines[0], letterSpacing);
47221
+ if (!best || cost < best.cost || cost === best.cost && lines.length < best.lines.length || cost === best.cost && lines.length === best.lines.length && head > best.head)
47222
+ best = { lines, cost, head };
46508
47223
  }
46509
- throw new Error(
46510
- `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
46511
- );
47224
+ return best?.lines ?? [text];
46512
47225
  }
46513
- function validate(data) {
46514
- const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
46515
- if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
46516
- throw new Error("map data assets are malformed (failed shape validation)");
46517
- }
46518
- return data;
47226
+ function rectAround(cx, cy, lines, letterSpacing) {
47227
+ const w = Math.max(...lines.map((l) => labelWidth(l, letterSpacing)));
47228
+ const h = (lines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY;
47229
+ return { x: cx - w / 2, y: cy - h / 2, w, h };
46519
47230
  }
46520
- function moduleBaseDir(nb) {
46521
- try {
46522
- const url = import_meta.url;
46523
- if (url) return nb.dirname(nb.fileURLToPath(url));
46524
- } catch {
46525
- }
46526
- if (typeof __dirname !== "undefined") return __dirname;
46527
- return process.cwd();
47231
+ function rectFits(r, width, height) {
47232
+ return r.x >= 0 && r.y >= 0 && r.x + r.w <= width && r.y + r.h <= height;
46528
47233
  }
46529
- function loadMapData() {
46530
- cache ??= (async () => {
46531
- const nb = await loadNodeBuiltins();
46532
- const dir = await firstExistingDir(nb, moduleBaseDir(nb));
46533
- const [
46534
- worldCoarse,
46535
- worldDetail,
46536
- usStates,
46537
- lakes,
46538
- rivers,
46539
- naLand,
46540
- naLakes,
46541
- gazetteer
46542
- ] = await Promise.all([
46543
- readJson(nb, dir, FILES.worldCoarse),
46544
- readJson(nb, dir, FILES.worldDetail),
46545
- readJson(nb, dir, FILES.usStates),
46546
- // Lakes/rivers/NA assets are optional — older bundles may predate them.
46547
- readJson(nb, dir, FILES.lakes).catch(() => void 0),
46548
- readJson(nb, dir, FILES.rivers).catch(() => void 0),
46549
- readJson(nb, dir, FILES.naLand).catch(() => void 0),
46550
- readJson(nb, dir, FILES.naLakes).catch(() => void 0),
46551
- readJson(nb, dir, FILES.gazetteer)
46552
- ]);
46553
- return validate({
46554
- worldCoarse,
46555
- worldDetail,
46556
- usStates,
46557
- gazetteer,
46558
- ...lakes && { lakes },
46559
- ...rivers && { rivers },
46560
- ...naLand && { naLand },
46561
- ...naLakes && { naLakes }
47234
+ function overlapsPadded(a, b, pad2) {
47235
+ 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;
47236
+ }
47237
+ function placeContextLabels(args) {
47238
+ const {
47239
+ projection,
47240
+ dLonSpan,
47241
+ dLatSpan,
47242
+ width,
47243
+ height,
47244
+ waterBodies,
47245
+ countries,
47246
+ palette,
47247
+ project,
47248
+ collides,
47249
+ overLand
47250
+ } = args;
47251
+ void projection;
47252
+ const band = tierBand(Math.max(dLonSpan, dLatSpan));
47253
+ const budget = labelBudget(width, height, band);
47254
+ if (budget <= 0) return [];
47255
+ const waterColor = mix(palette.colors.blue, palette.textMuted, 50);
47256
+ const countryColor = palette.textMuted;
47257
+ const haloColor = palette.bg;
47258
+ const candidates = [];
47259
+ const center = [width / 2, height / 2];
47260
+ for (const e of waterBodies?.entries ?? []) {
47261
+ const [lat, lon, name, tier, kind, alt] = e;
47262
+ if (!waterEligible(tier, kind, band)) continue;
47263
+ const wlines = wrapLabel2(name, WATER_LETTER_SPACING);
47264
+ const anchorsLngLat = [[lon, lat]];
47265
+ for (const a of alt ?? []) anchorsLngLat.push([a[1], a[0]]);
47266
+ let best = null;
47267
+ let bestD = Infinity;
47268
+ let nearestProj = null;
47269
+ let nearestProjD = Infinity;
47270
+ for (const [aLon, aLat] of anchorsLngLat) {
47271
+ const p = project(aLon, aLat);
47272
+ if (!p || !Number.isFinite(p[0]) || !Number.isFinite(p[1])) continue;
47273
+ const d = (p[0] - center[0]) ** 2 + (p[1] - center[1]) ** 2;
47274
+ if (d < nearestProjD) {
47275
+ nearestProjD = d;
47276
+ nearestProj = p;
47277
+ }
47278
+ if (!insideViewport(p, width, height)) continue;
47279
+ if (d < bestD) {
47280
+ bestD = d;
47281
+ best = p;
47282
+ }
47283
+ }
47284
+ if (!best && tier === 0 && nearestProj) {
47285
+ const overX = Math.max(0, -nearestProj[0], nearestProj[0] - width);
47286
+ const overY = Math.max(0, -nearestProj[1], nearestProj[1] - height);
47287
+ if (overX <= width * EDGE_CLAMP_OVERSHOOT && overY <= height * EDGE_CLAMP_OVERSHOOT) {
47288
+ const halfW = Math.max(...wlines.map((l) => labelWidth(l, WATER_LETTER_SPACING))) / 2;
47289
+ const halfH = ((wlines.length - 1) * LINE_HEIGHT + FONT + 2 * PADY) / 2;
47290
+ const m = EDGE_CLAMP_MARGIN;
47291
+ best = [
47292
+ Math.min(Math.max(nearestProj[0], halfW + m), width - halfW - m),
47293
+ Math.min(Math.max(nearestProj[1], halfH + m), height - halfH - m)
47294
+ ];
47295
+ }
47296
+ }
47297
+ if (!best) continue;
47298
+ candidates.push({
47299
+ text: name,
47300
+ lines: wlines,
47301
+ cx: best[0],
47302
+ cy: best[1],
47303
+ italic: true,
47304
+ letterSpacing: WATER_LETTER_SPACING,
47305
+ color: waterColor,
47306
+ // Water before any country (×1000), then by tier, then kind, then name.
47307
+ sort: tier * 10 + KIND_ORDER[kind]
46562
47308
  });
46563
- })().catch((e) => {
46564
- cache = void 0;
46565
- throw e;
46566
- });
46567
- return cache;
47309
+ }
47310
+ const ranked = countries.map((c) => {
47311
+ const [x0, y0, x1, y1] = c.bbox;
47312
+ const w = x1 - x0;
47313
+ const h = y1 - y0;
47314
+ return { c, w, h, area: w * h };
47315
+ }).filter((r) => Number.isFinite(r.area) && r.area > 0).sort((a, b) => b.area - a.area);
47316
+ let ci = 0;
47317
+ for (const r of ranked) {
47318
+ const { c, w, h } = r;
47319
+ if (w > width * 0.66 || h > height * 0.66) continue;
47320
+ if (!insideViewport(c.anchor, width, height)) continue;
47321
+ const text = c.name;
47322
+ const tw = labelWidth(text, 0);
47323
+ if (tw > w || FONT + 2 * PADY > h) continue;
47324
+ candidates.push({
47325
+ text,
47326
+ lines: [text],
47327
+ cx: c.anchor[0],
47328
+ cy: c.anchor[1],
47329
+ italic: false,
47330
+ letterSpacing: 0,
47331
+ color: countryColor,
47332
+ // Always after every water body (+1e6); larger area = earlier.
47333
+ sort: 1e6 + ci++
47334
+ });
47335
+ }
47336
+ candidates.sort((a, b) => a.sort - b.sort);
47337
+ const placed = [];
47338
+ const placedRects = [];
47339
+ for (const cand of candidates) {
47340
+ if (placed.length >= budget) break;
47341
+ const rect = rectAround(cand.cx, cand.cy, cand.lines, cand.letterSpacing);
47342
+ if (!rectFits(rect, width, height)) continue;
47343
+ if (cand.italic && overLand) {
47344
+ const inset = 2;
47345
+ const top = cand.cy - (cand.lines.length - 1) / 2 * LINE_HEIGHT;
47346
+ const touchesLand = cand.lines.some((line12, li) => {
47347
+ const lw = labelWidth(line12, cand.letterSpacing);
47348
+ const x0 = cand.cx - lw / 2 + inset;
47349
+ const x1 = cand.cx + lw / 2 - inset;
47350
+ const xs = [x0, (x0 + cand.cx) / 2, cand.cx, (cand.cx + x1) / 2, x1];
47351
+ const base = top + li * LINE_HEIGHT;
47352
+ return [base, base - FONT * 0.4, base - FONT * 0.8].some(
47353
+ (y) => xs.some((x) => overLand(x, y))
47354
+ );
47355
+ });
47356
+ if (touchesLand) continue;
47357
+ }
47358
+ if (collides(rect)) continue;
47359
+ if (placedRects.some((r) => overlapsPadded(rect, r, CONTEXT_PAD))) continue;
47360
+ placedRects.push(rect);
47361
+ placed.push({
47362
+ x: cand.cx,
47363
+ y: cand.cy,
47364
+ text: cand.text,
47365
+ anchor: "middle",
47366
+ color: cand.color,
47367
+ // No halo: the bg-coloured outline reads as a ghost box behind the text
47368
+ // over the tinted water/land. Context labels are muted enough to sit
47369
+ // cleanly on the basemap without one.
47370
+ halo: false,
47371
+ haloColor,
47372
+ italic: cand.italic,
47373
+ letterSpacing: cand.letterSpacing,
47374
+ ...cand.lines.length > 1 ? { lines: cand.lines } : {},
47375
+ lineNumber: 0
47376
+ });
47377
+ }
47378
+ return placed;
46568
47379
  }
46569
- var import_meta, FILES, CANDIDATE_DIRS, cache;
46570
- var init_load_data = __esm({
46571
- "src/map/load-data.ts"() {
47380
+ var FONT, LINE_HEIGHT, PADX, PADY, WATER_LETTER_SPACING, CONTEXT_PAD, EDGE_CLAMP_MARGIN, EDGE_CLAMP_OVERSHOOT, KIND_ORDER;
47381
+ var init_context_labels = __esm({
47382
+ "src/map/context-labels.ts"() {
46572
47383
  "use strict";
46573
- import_meta = {};
46574
- FILES = {
46575
- worldCoarse: "world-coarse.json",
46576
- worldDetail: "world-detail.json",
46577
- usStates: "us-states.json",
46578
- lakes: "lakes.json",
46579
- rivers: "rivers.json",
46580
- naLand: "na-land.json",
46581
- naLakes: "na-lakes.json",
46582
- gazetteer: "gazetteer.json"
47384
+ init_color_utils();
47385
+ init_legend_constants();
47386
+ FONT = 11;
47387
+ LINE_HEIGHT = FONT + 2;
47388
+ PADX = 4;
47389
+ PADY = 3;
47390
+ WATER_LETTER_SPACING = 1.5;
47391
+ CONTEXT_PAD = 4;
47392
+ EDGE_CLAMP_MARGIN = 8;
47393
+ EDGE_CLAMP_OVERSHOOT = 0.35;
47394
+ KIND_ORDER = {
47395
+ ocean: 0,
47396
+ sea: 1,
47397
+ gulf: 2,
47398
+ bay: 3,
47399
+ strait: 4,
47400
+ channel: 5,
47401
+ sound: 6
46583
47402
  };
46584
- CANDIDATE_DIRS = [
46585
- "./data",
46586
- "./map-data",
46587
- "../map-data",
46588
- "../src/map/data"
46589
- ];
46590
47403
  }
46591
47404
  });
46592
47405
 
@@ -46595,12 +47408,34 @@ function geomObject2(topo) {
46595
47408
  const key = Object.keys(topo.objects)[0];
46596
47409
  return topo.objects[key];
46597
47410
  }
47411
+ function mergeFeatures(a, b) {
47412
+ const polysOf = (f) => {
47413
+ const g = f.geometry;
47414
+ if (!g) return null;
47415
+ if (g.type === "Polygon") return [g.coordinates];
47416
+ if (g.type === "MultiPolygon") return g.coordinates;
47417
+ return null;
47418
+ };
47419
+ const pa = polysOf(a);
47420
+ const pb = polysOf(b);
47421
+ if (!pa || !pb) return a;
47422
+ return {
47423
+ ...a,
47424
+ geometry: { type: "MultiPolygon", coordinates: [...pa, ...pb] }
47425
+ };
47426
+ }
46598
47427
  function decodeLayer(topo) {
47428
+ const cached = decodeCache.get(topo);
47429
+ if (cached) return cached;
46599
47430
  const out = /* @__PURE__ */ new Map();
46600
47431
  for (const g of geomObject2(topo).geometries) {
46601
47432
  const f = (0, import_topojson_client2.feature)(topo, g);
46602
- out.set(g.id, { ...f, id: g.id });
47433
+ if (!f.geometry) continue;
47434
+ const tagged = { ...f, id: g.id };
47435
+ const existing = out.get(g.id);
47436
+ out.set(g.id, existing ? mergeFeatures(existing, tagged) : tagged);
46603
47437
  }
47438
+ decodeCache.set(topo, out);
46604
47439
  return out;
46605
47440
  }
46606
47441
  function projectionFor(family) {
@@ -46609,38 +47444,35 @@ function projectionFor(family) {
46609
47444
  return usConusProjection();
46610
47445
  case "mercator":
46611
47446
  return (0, import_d3_geo2.geoMercator)();
47447
+ case "equal-earth":
47448
+ return (0, import_d3_geo2.geoEqualEarth)();
47449
+ case "equirectangular":
47450
+ return (0, import_d3_geo2.geoEquirectangular)();
46612
47451
  case "natural-earth":
46613
47452
  return (0, import_d3_geo2.geoNaturalEarth1)();
46614
- case "equirectangular":
46615
47453
  default:
46616
47454
  return (0, import_d3_geo2.geoEquirectangular)();
46617
47455
  }
46618
47456
  }
46619
- function mapBackgroundColor(palette, isDark = false, dataActive = false) {
46620
- if (dataActive)
46621
- return mix(
46622
- palette.colors.gray,
46623
- palette.bg,
46624
- isDark ? MUTED_WATER_DARK : MUTED_WATER_LIGHT
46625
- );
46626
- return mix(palette.colors.blue, palette.bg, WATER_TINT);
47457
+ function mapBackgroundColor(palette, isDark = false, _dataActive = false) {
47458
+ return mix(
47459
+ palette.colors.blue,
47460
+ palette.bg,
47461
+ isDark ? WATER_TINT_DARK : WATER_TINT_LIGHT
47462
+ );
46627
47463
  }
46628
- function mapNeutralLandColor(palette, isDark, dataActive = false) {
46629
- if (dataActive)
46630
- return isDark ? mix(palette.colors.gray, palette.bg, MUTED_LAND_DARK) : palette.bg;
47464
+ function mapNeutralLandColor(palette, isDark, _dataActive = false) {
46631
47465
  return mix(
46632
47466
  palette.colors.green,
46633
47467
  palette.bg,
46634
47468
  isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT
46635
47469
  );
46636
47470
  }
46637
- function layoutMap(resolved, data, size, opts) {
46638
- const { palette, isDark } = opts;
46639
- const { width, height } = size;
47471
+ function buildMapProjection(resolved, data) {
46640
47472
  const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
46641
- const usCrisp = resolved.projection === "albers-usa" && wantsUsStates && !!data.naLand;
47473
+ const usCrisp = (resolved.projection === "albers-usa" || resolved.projection === "mercator") && wantsUsStates && !!data.naLand;
46642
47474
  const worldTopo = usCrisp ? data.worldDetail : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46643
- const worldLayer = decodeLayer(worldTopo);
47475
+ const worldLayer = new Map(decodeLayer(worldTopo));
46644
47476
  if (usCrisp && data.naLand) {
46645
47477
  const [nbW, nbS, nbE, nbN] = [-140, 10, -52, 66];
46646
47478
  const crisp = decodeLayer(data.naLand);
@@ -46649,17 +47481,110 @@ function layoutMap(resolved, data, size, opts) {
46649
47481
  if (!base) continue;
46650
47482
  const [[bw, bs], [be, bn]] = (0, import_d3_geo2.geoBounds)(base);
46651
47483
  if (bw >= nbW && be <= nbE && bs >= nbS && bn <= nbN)
46652
- worldLayer.set(iso, cf);
47484
+ worldLayer.set(iso, { ...cf, properties: base.properties });
46653
47485
  }
46654
47486
  }
46655
47487
  const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
47488
+ const extentOutline = () => {
47489
+ const [[w, s], [e, n]] = resolved.extent;
47490
+ const N = 16;
47491
+ const coords = [];
47492
+ for (let i = 0; i <= N; i++) {
47493
+ const t = i / N;
47494
+ const lon = w + (e - w) * t;
47495
+ const lat = s + (n - s) * t;
47496
+ coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
47497
+ }
47498
+ return {
47499
+ type: "Feature",
47500
+ properties: {},
47501
+ geometry: { type: "MultiPoint", coordinates: coords }
47502
+ };
47503
+ };
47504
+ let fitFeatures;
47505
+ if (resolved.projection === "albers-usa" && usLayer) {
47506
+ fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
47507
+ const neighborPoints = resolved.pois.filter((p) => !inAlaska(p.lon, p.lat) && !inHawaii(p.lon, p.lat)).map((p) => [p.lon, p.lat]);
47508
+ if (neighborPoints.length > 0) {
47509
+ fitFeatures.push({
47510
+ type: "Feature",
47511
+ properties: {},
47512
+ geometry: { type: "MultiPoint", coordinates: neighborPoints }
47513
+ });
47514
+ }
47515
+ for (const r of resolved.regions) {
47516
+ if (r.layer === "country" && (r.iso === "CA" || r.iso === "MX")) {
47517
+ const cf = worldLayer.get(r.iso);
47518
+ if (cf) fitFeatures.push(cf);
47519
+ }
47520
+ }
47521
+ } else {
47522
+ fitFeatures = [extentOutline()];
47523
+ }
47524
+ const fitTarget = { type: "FeatureCollection", features: fitFeatures };
47525
+ const projection = projectionFor(resolved.projection);
47526
+ if (resolved.projection !== "albers-usa") {
47527
+ let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
47528
+ if (centerLon > 180) centerLon -= 360;
47529
+ projection.rotate([-centerLon, 0]);
47530
+ }
47531
+ const fitGB = (0, import_d3_geo2.geoBounds)(fitTarget);
47532
+ const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
47533
+ return {
47534
+ projection,
47535
+ fitTarget,
47536
+ fitIsGlobal,
47537
+ worldLayer,
47538
+ usLayer,
47539
+ usCrisp,
47540
+ wantsUsStates,
47541
+ worldTopo
47542
+ };
47543
+ }
47544
+ function parsePathRings(d) {
47545
+ const rings = [];
47546
+ let cur = [];
47547
+ const re = /([MLZ])([^MLZ]*)/g;
47548
+ let m;
47549
+ while (m = re.exec(d)) {
47550
+ if (m[1] === "Z") {
47551
+ if (cur.length) rings.push(cur);
47552
+ cur = [];
47553
+ continue;
47554
+ }
47555
+ if (m[1] === "M" && cur.length) {
47556
+ rings.push(cur);
47557
+ cur = [];
47558
+ }
47559
+ const nums = m[2].split(/[ ,]+/).map(Number);
47560
+ for (let i = 0; i + 1 < nums.length; i += 2) {
47561
+ const x = nums[i];
47562
+ const y = nums[i + 1];
47563
+ if (Number.isFinite(x) && Number.isFinite(y)) cur.push([x, y]);
47564
+ }
47565
+ }
47566
+ if (cur.length) rings.push(cur);
47567
+ return rings;
47568
+ }
47569
+ function layoutMap(resolved, data, size, opts) {
47570
+ const { palette, isDark } = opts;
47571
+ const { width, height } = size;
47572
+ const {
47573
+ projection,
47574
+ fitTarget,
47575
+ fitIsGlobal,
47576
+ worldLayer,
47577
+ usLayer,
47578
+ usCrisp,
47579
+ worldTopo
47580
+ } = buildMapProjection(resolved, data);
46656
47581
  const usContext = usLayer !== null;
46657
47582
  const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46658
47583
  const values = resolved.regions.filter((r) => r.value !== void 0).map((r) => r.value);
46659
- const scaleOverride = resolved.directives.scale;
46660
- const rampMin = scaleOverride ? scaleOverride.min : Math.min(...values);
46661
- const rampMax = scaleOverride ? scaleOverride.max : Math.max(...values);
46662
- const rampHue = palette.colors.red;
47584
+ const allNonNegative = values.length > 0 && values.every((v) => v >= 0);
47585
+ const rampMin = allNonNegative ? 0 : Math.min(...values);
47586
+ const rampMax = Math.max(...values);
47587
+ const rampHue = resolveColor(resolved.directives.regionMetricColor ?? "", palette) ?? palette.colors.red;
46663
47588
  const hasRamp = values.length > 0;
46664
47589
  const VALUE_NAME = hasRamp ? resolved.directives.regionMetric?.trim() || "Value" : null;
46665
47590
  const matchColorGroup = (v) => {
@@ -46679,14 +47604,48 @@ function layoutMap(resolved, data, size, opts) {
46679
47604
  activeGroup = VALUE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46680
47605
  }
46681
47606
  const activeIsScore = VALUE_NAME !== null && activeGroup === VALUE_NAME;
46682
- const mutedBasemap = resolved.directives.basemapStyle === "muted" ? true : resolved.directives.basemapStyle === "natural" ? false : activeGroup !== null;
47607
+ const mutedBasemap = activeGroup !== null;
46683
47608
  const neutralFill = mapNeutralLandColor(palette, isDark, mutedBasemap);
46684
47609
  const water = mapBackgroundColor(palette, isDark, mutedBasemap);
47610
+ const lakeStroke = mix(regionStroke, water, 45);
46685
47611
  const foreignFill = mix(
46686
47612
  palette.colors.gray,
46687
47613
  palette.bg,
46688
47614
  mutedBasemap ? isDark ? MUTED_FOREIGN_DARK : MUTED_FOREIGN_LIGHT : isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46689
47615
  );
47616
+ const colorizeActive = resolved.directives.noColorize !== true && !hasRamp && resolved.tagGroups.length === 0;
47617
+ const colorByIso = /* @__PURE__ */ new Map();
47618
+ if (colorizeActive) {
47619
+ const adjacency = /* @__PURE__ */ new Map();
47620
+ const addEdges = (src) => {
47621
+ for (const [iso, ns] of src) {
47622
+ const cur = adjacency.get(iso);
47623
+ if (cur) cur.push(...ns);
47624
+ else adjacency.set(iso, [...ns]);
47625
+ }
47626
+ };
47627
+ addEdges(buildAdjacency(worldTopo));
47628
+ if (usLayer) {
47629
+ addEdges(buildAdjacency(data.usStates));
47630
+ for (const [country, states] of Object.entries(FOREIGN_BORDER)) {
47631
+ const cn = adjacency.get(country);
47632
+ if (!cn) continue;
47633
+ for (const st of states) {
47634
+ const sn = adjacency.get(st);
47635
+ if (!sn) continue;
47636
+ cn.push(st);
47637
+ sn.push(country);
47638
+ }
47639
+ }
47640
+ }
47641
+ const { byIso, huesNeeded } = assignColors(
47642
+ [...adjacency.keys()],
47643
+ adjacency
47644
+ );
47645
+ const tints = politicalTints(palette, huesNeeded, isDark);
47646
+ for (const [iso, idx] of byIso) colorByIso.set(iso, tints[idx]);
47647
+ }
47648
+ const colorizeStroke = (fill2) => mix(fill2, palette.text, 35);
46690
47649
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46691
47650
  const fillForValue = (s) => {
46692
47651
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
@@ -46711,47 +47670,26 @@ function layoutMap(resolved, data, size, opts) {
46711
47670
  isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
46712
47671
  );
46713
47672
  };
47673
+ const directFill = (name) => {
47674
+ const hex = name ? resolveColor(name, palette) : null;
47675
+ if (!hex) return null;
47676
+ return mix(hex, palette.bg, isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT);
47677
+ };
46714
47678
  const regionFill = (r) => {
47679
+ const direct = directFill(r.color);
47680
+ if (direct) return direct;
46715
47681
  if (activeIsScore) {
46716
47682
  return r.value !== void 0 ? fillForValue(r.value) : neutralFill;
46717
47683
  }
47684
+ if (colorizeActive) return (r.iso && colorByIso.get(r.iso)) ?? neutralFill;
46718
47685
  return tagFill(r.tags, activeGroup) ?? neutralFill;
46719
47686
  };
46720
47687
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
46721
- const extentOutline = () => {
46722
- const [[w, s], [e, n]] = resolved.extent;
46723
- const N = 16;
46724
- const coords = [];
46725
- for (let i = 0; i <= N; i++) {
46726
- const t = i / N;
46727
- const lon = w + (e - w) * t;
46728
- const lat = s + (n - s) * t;
46729
- coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
46730
- }
46731
- return {
46732
- type: "Feature",
46733
- properties: {},
46734
- geometry: { type: "MultiPoint", coordinates: coords }
46735
- };
46736
- };
46737
- let fitFeatures;
46738
- if (resolved.projection === "albers-usa" && usLayer) {
46739
- fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
46740
- } else {
46741
- fitFeatures = [extentOutline()];
46742
- }
46743
- const fitTarget = { type: "FeatureCollection", features: fitFeatures };
46744
- const projection = projectionFor(resolved.projection);
46745
- if (resolved.projection !== "albers-usa") {
46746
- let centerLon = (resolved.extent[0][0] + resolved.extent[1][0]) / 2;
46747
- if (centerLon > 180) centerLon -= 360;
46748
- projection.rotate([-centerLon, 0]);
46749
- }
46750
- const TITLE_GAP = 16;
47688
+ const TITLE_GAP2 = 16;
46751
47689
  let topPad = FIT_PAD;
46752
47690
  if (resolved.title && resolved.pois.length > 0) {
46753
47691
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
46754
- topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
47692
+ topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
46755
47693
  }
46756
47694
  const fitBox = [
46757
47695
  [FIT_PAD, topPad],
@@ -46761,11 +47699,10 @@ function layoutMap(resolved, data, size, opts) {
46761
47699
  ]
46762
47700
  ];
46763
47701
  projection.fitExtent(fitBox, fitTarget);
46764
- const fitGB = (0, import_d3_geo2.geoBounds)(fitTarget);
46765
- const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46766
47702
  let path;
46767
47703
  let project;
46768
- if (fitIsGlobal) {
47704
+ let stretchParams = null;
47705
+ if (fitIsGlobal && !opts.preferContain) {
46769
47706
  const cb = (0, import_d3_geo2.geoPath)(projection).bounds(fitTarget);
46770
47707
  const bx0 = cb[0][0];
46771
47708
  const by0 = cb[0][1];
@@ -46775,6 +47712,7 @@ function layoutMap(resolved, data, size, opts) {
46775
47712
  const oy = fitBox[0][1];
46776
47713
  const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46777
47714
  const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47715
+ stretchParams = { sx, sy, ox, oy, bx0, by0 };
46778
47716
  const stretch = (x, y) => [
46779
47717
  ox + (x - bx0) * sx,
46780
47718
  oy + (y - by0) * sy
@@ -46806,7 +47744,9 @@ function layoutMap(resolved, data, size, opts) {
46806
47744
  const insets = [];
46807
47745
  const insetRegions = [];
46808
47746
  const insetLabelSeeds = [];
46809
- if (resolved.projection === "albers-usa" && usLayer) {
47747
+ const akRef = resolved.regions.some((r) => r.iso === "US-AK") || resolved.pois.some((p) => inAlaska(p.lon, p.lat));
47748
+ const hiRef = resolved.regions.some((r) => r.iso === "US-HI") || resolved.pois.some((p) => inHawaii(p.lon, p.lat));
47749
+ if (resolved.projection === "albers-usa" && usLayer && (akRef || hiRef)) {
46810
47750
  const PAD = 8;
46811
47751
  const GAP = 12;
46812
47752
  const yB = height - FIT_PAD;
@@ -46837,38 +47777,14 @@ function layoutMap(resolved, data, size, opts) {
46837
47777
  }
46838
47778
  return y;
46839
47779
  };
46840
- const coastTop = (x0, xr) => {
47780
+ const coastFloor = (x0, xr) => {
46841
47781
  const n = 24;
46842
- const pts = [];
46843
47782
  let maxY = -Infinity;
46844
47783
  for (let i = 0; i <= n; i++) {
46845
- const x = x0 + (xr - x0) * i / n;
46846
- const y = at(x);
46847
- if (y > -Infinity) {
46848
- pts.push([x, y]);
46849
- if (y > maxY) maxY = y;
46850
- }
46851
- }
46852
- if (pts.length === 0) return () => yB - height * 0.42;
46853
- let m = 0;
46854
- if (pts.length >= 2) {
46855
- let sx = 0, sy = 0, sxx = 0, sxy = 0;
46856
- for (const [x, y] of pts) {
46857
- sx += x;
46858
- sy += y;
46859
- sxx += x * x;
46860
- sxy += x * y;
46861
- }
46862
- const den = pts.length * sxx - sx * sx;
46863
- if (den !== 0) m = (pts.length * sxy - sx * sy) / den;
46864
- }
46865
- m = Math.max(-0.35, Math.min(0.35, m));
46866
- let c = -Infinity;
46867
- for (const [x, y] of pts) {
46868
- const need = y - m * x + GAP;
46869
- if (need > c) c = need;
46870
- }
46871
- return (x) => m * x + c;
47784
+ const y = at(x0 + (xr - x0) * i / n);
47785
+ if (y > maxY) maxY = y;
47786
+ }
47787
+ return maxY;
46872
47788
  };
46873
47789
  const placeInset = (iso, proj, boxX, iwReq) => {
46874
47790
  const f = usLayer.get(iso);
@@ -46877,19 +47793,15 @@ function layoutMap(resolved, data, size, opts) {
46877
47793
  const iw = Math.min(iwReq, width - FIT_PAD - x0 - 2 * PAD);
46878
47794
  if (iw < 24) return boxX;
46879
47795
  const xr = x0 + iw + 2 * PAD;
46880
- const top = coastTop(x0, xr);
46881
- const yL = top(x0);
46882
- const yR = top(xr);
47796
+ const floor = coastFloor(x0, xr);
47797
+ const topGuess = floor > -Infinity ? floor + GAP : yB - height * 0.42;
46883
47798
  proj.fitWidth(iw, f);
46884
47799
  const bb = (0, import_d3_geo2.geoPath)(proj).bounds(f);
46885
47800
  const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
46886
47801
  const needH = sh + 2 * PAD;
46887
- let topFit = Math.max(yL, yR);
47802
+ let topFit = topGuess;
46888
47803
  const bottom = Math.min(topFit + needH, yB);
46889
47804
  if (bottom - topFit < needH) topFit = bottom - needH;
46890
- const lift = topFit - Math.max(yL, yR);
46891
- const topL = yL + lift;
46892
- const topR = yR + lift;
46893
47805
  proj.fitExtent(
46894
47806
  [
46895
47807
  [x0 + PAD, topFit + PAD],
@@ -46899,8 +47811,18 @@ function layoutMap(resolved, data, size, opts) {
46899
47811
  );
46900
47812
  const d = (0, import_d3_geo2.geoPath)(proj)(f) ?? "";
46901
47813
  if (!d) return xr;
47814
+ let contextLand;
47815
+ if (iso === "US-AK") {
47816
+ const can = worldLayer.get("CA");
47817
+ const cd = can ? (0, import_d3_geo2.geoPath)(proj)(can) ?? "" : "";
47818
+ if (cd)
47819
+ contextLand = {
47820
+ d: cd,
47821
+ fill: colorizeActive ? colorByIso.get("CA") ?? foreignFill : foreignFill
47822
+ };
47823
+ }
46902
47824
  const r = regionById.get(iso);
46903
- let fill2 = neutralFill;
47825
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? neutralFill : neutralFill;
46904
47826
  let lineNumber = -1;
46905
47827
  if (r?.layer === "us-state") {
46906
47828
  fill2 = regionFill(r);
@@ -46908,21 +47830,25 @@ function layoutMap(resolved, data, size, opts) {
46908
47830
  }
46909
47831
  insets.push({
46910
47832
  x: x0,
46911
- y: Math.min(topL, topR),
47833
+ y: topFit,
46912
47834
  w: xr - x0,
46913
- h: bottom - Math.min(topL, topR),
47835
+ h: bottom - topFit,
46914
47836
  points: [
46915
- [x0, topL],
46916
- [xr, topR],
47837
+ [x0, topFit],
47838
+ [xr, topFit],
46917
47839
  [xr, bottom],
46918
47840
  [x0, bottom]
46919
- ]
47841
+ ],
47842
+ // The FITTED inset projection (just fit to this box) — captured so the
47843
+ // geo-query can invert pixels inside the frame back to AK/HI coords.
47844
+ projection: proj,
47845
+ ...contextLand && { contextLand }
46920
47846
  });
46921
47847
  insetRegions.push({
46922
47848
  id: iso,
46923
47849
  d,
46924
47850
  fill: fill2,
46925
- stroke: regionStroke,
47851
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
46926
47852
  lineNumber,
46927
47853
  layer: "us-state",
46928
47854
  ...r?.value !== void 0 && { value: r.value },
@@ -46935,13 +47861,16 @@ function layoutMap(resolved, data, size, opts) {
46935
47861
  }
46936
47862
  return xr;
46937
47863
  };
46938
- const akRight = placeInset(
46939
- "US-AK",
46940
- alaskaProjection(),
46941
- FIT_PAD,
46942
- width * 0.15
46943
- );
46944
- placeInset("US-HI", hawaiiProjection(), akRight + 24, width * 0.1);
47864
+ let akRight = FIT_PAD;
47865
+ if (akRef)
47866
+ akRight = placeInset("US-AK", alaskaProjection(), FIT_PAD, width * 0.15);
47867
+ if (hiRef)
47868
+ placeInset(
47869
+ "US-HI",
47870
+ hawaiiProjection(),
47871
+ akRef ? akRight + 24 : FIT_PAD,
47872
+ width * 0.1
47873
+ );
46945
47874
  }
46946
47875
  const conusFit = resolved.projection === "albers-usa" && !!usLayer;
46947
47876
  const classifyExtent = conusFit ? (0, import_d3_geo2.geoBounds)(fitTarget) : resolved.extent;
@@ -46957,15 +47886,24 @@ function layoutMap(resolved, data, size, opts) {
46957
47886
  };
46958
47887
  const ringOverlapsView = (ring) => {
46959
47888
  let loMin = Infinity, loMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
47889
+ const lons = [];
46960
47890
  for (const [rawLon] of ring) {
46961
47891
  const lon = normLon(rawLon);
47892
+ lons.push(lon);
46962
47893
  if (lon < loMin) loMin = lon;
46963
47894
  if (lon > loMax) loMax = lon;
46964
47895
  if (rawLon < rawMin) rawMin = rawLon;
46965
47896
  if (rawLon > rawMax) rawMax = rawLon;
46966
47897
  }
46967
- if (loMax - loMin > 270) return false;
46968
- if (rawMax - rawMin > 180 && loMax - loMin < 90) return false;
47898
+ lons.sort((a, b) => a - b);
47899
+ let maxGap = 0;
47900
+ for (let i = 1; i < lons.length; i++)
47901
+ maxGap = Math.max(maxGap, lons[i] - lons[i - 1]);
47902
+ if (lons.length > 1)
47903
+ maxGap = Math.max(maxGap, lons[0] + 360 - lons[lons.length - 1]);
47904
+ const occupiedArc = 360 - maxGap;
47905
+ if (occupiedArc > 270) return false;
47906
+ if (rawMax - rawMin > 180 && occupiedArc < 90) return false;
46969
47907
  let px0 = Infinity, py0 = Infinity, px1 = -Infinity, py1 = -Infinity, anyFinite = false;
46970
47908
  for (const [lon, lat] of ring) {
46971
47909
  const p = project(lon, lat);
@@ -47038,7 +47976,7 @@ function layoutMap(resolved, data, size, opts) {
47038
47976
  const regions = [];
47039
47977
  const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
47040
47978
  for (const [iso, f] of layerFeatures) {
47041
- if (layerKind === "us-state" && usContext && INSET_STATES.has(iso))
47979
+ if (layerKind === "us-state" && usContext && resolved.projection === "albers-usa" && INSET_STATES.has(iso))
47042
47980
  continue;
47043
47981
  if (layerKind === "country" && usContext && iso === "US") continue;
47044
47982
  if (layerKind === "country" && iso === "AQ" && !regionById.has("AQ"))
@@ -47050,7 +47988,8 @@ function layoutMap(resolved, data, size, opts) {
47050
47988
  if (!d) continue;
47051
47989
  const isThisLayer = r?.layer === layerKind;
47052
47990
  const isForeign = layerKind === "country" && usContext && iso !== "US";
47053
- let fill2 = isForeign ? foreignFill : neutralFill;
47991
+ const baseFill = isForeign ? foreignFill : neutralFill;
47992
+ let fill2 = colorizeActive ? colorByIso.get(iso) ?? baseFill : baseFill;
47054
47993
  let label;
47055
47994
  let lineNumber = -1;
47056
47995
  let layer = "base";
@@ -47059,12 +47998,14 @@ function layoutMap(resolved, data, size, opts) {
47059
47998
  lineNumber = r.lineNumber;
47060
47999
  layer = layerKind;
47061
48000
  label = r.name;
48001
+ } else {
48002
+ label = f.properties?.name;
47062
48003
  }
47063
48004
  regions.push({
47064
48005
  id: iso,
47065
48006
  d,
47066
48007
  fill: fill2,
47067
- stroke: regionStroke,
48008
+ stroke: colorizeActive ? colorizeStroke(fill2) : regionStroke,
47068
48009
  lineNumber,
47069
48010
  layer,
47070
48011
  ...label !== void 0 && { label },
@@ -47086,13 +48027,88 @@ function layoutMap(resolved, data, size, opts) {
47086
48027
  id: "lake",
47087
48028
  d,
47088
48029
  fill: water,
47089
- stroke: "none",
48030
+ stroke: lakeStroke,
47090
48031
  lineNumber: -1,
47091
48032
  layer: "base"
47092
48033
  });
47093
48034
  }
47094
48035
  }
47095
- const riverColor = water;
48036
+ const pointInRings = (px, py, rings) => {
48037
+ let inside = false;
48038
+ for (const ring of rings) {
48039
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
48040
+ const [xi, yi] = ring[i];
48041
+ const [xj, yj] = ring[j];
48042
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
48043
+ inside = !inside;
48044
+ }
48045
+ }
48046
+ return inside;
48047
+ };
48048
+ const fillHitTargets = [...regions, ...insetRegions].map((r) => ({
48049
+ fill: r.fill,
48050
+ rings: parsePathRings(r.d)
48051
+ }));
48052
+ const fillAt = (x, y) => {
48053
+ let hit = water;
48054
+ for (const t of fillHitTargets)
48055
+ if (pointInRings(x, y, t.rings)) hit = t.fill;
48056
+ return hit;
48057
+ };
48058
+ const labelOnFill = (fill2) => {
48059
+ const color = contrastRatio(fill2, palette.textOnFillDark) >= contrastRatio(fill2, palette.textOnFillLight) ? palette.textOnFillDark : palette.textOnFillLight;
48060
+ const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
48061
+ return {
48062
+ color,
48063
+ halo: contrastRatio(fill2, color) < REGION_LABEL_HALO_RATIO,
48064
+ haloColor
48065
+ };
48066
+ };
48067
+ const reliefAllowed = resolved.directives.noRelief !== true;
48068
+ const relief = [];
48069
+ let reliefHatch = null;
48070
+ if (reliefAllowed && data.mountainRanges) {
48071
+ for (const [, f] of decodeLayer(data.mountainRanges)) {
48072
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
48073
+ if (!viewF) continue;
48074
+ const area2 = path.area(viewF);
48075
+ if (!Number.isFinite(area2) || area2 < RELIEF_MIN_AREA) continue;
48076
+ const box = path.bounds(viewF);
48077
+ if (box[1][0] - box[0][0] < RELIEF_MIN_DIM || box[1][1] - box[0][1] < RELIEF_MIN_DIM)
48078
+ continue;
48079
+ const d = path(viewF) ?? "";
48080
+ if (!d) continue;
48081
+ relief.push({ d });
48082
+ }
48083
+ if (relief.length) {
48084
+ const darkTone = isDark ? palette.bg : palette.text;
48085
+ const lightTone = isDark ? palette.text : palette.bg;
48086
+ const reliefLandRef = colorizeActive ? isDark ? palette.surface : palette.bg : neutralFill;
48087
+ const landLum = relativeLuminance(reliefLandRef);
48088
+ const tone = Math.abs(landLum - relativeLuminance(darkTone)) > 0.04 ? darkTone : lightTone;
48089
+ reliefHatch = {
48090
+ color: mix(tone, reliefLandRef, RELIEF_HATCH_STRENGTH),
48091
+ spacing: RELIEF_HATCH_SPACING,
48092
+ width: RELIEF_HATCH_WIDTH
48093
+ };
48094
+ }
48095
+ }
48096
+ let coastlineStyle = null;
48097
+ if (resolved.directives.noCoastline !== true) {
48098
+ const minDim = Math.min(width, height);
48099
+ coastlineStyle = {
48100
+ color: mix(regionStroke, water, COASTLINE_STROKE_MIX),
48101
+ // N equal-width rings: distance steps outward by COASTLINE_STEP; opacity
48102
+ // fades linearly from NEAR (innermost) to FAR (outermost).
48103
+ lines: Array.from({ length: COASTLINE_RING_COUNT }, (_, k) => ({
48104
+ d: (COASTLINE_D0 + k * COASTLINE_STEP) * minDim,
48105
+ thickness: COASTLINE_THICKNESS * minDim,
48106
+ opacity: COASTLINE_OPACITY_NEAR + (COASTLINE_OPACITY_FAR - COASTLINE_OPACITY_NEAR) * k / (COASTLINE_RING_COUNT - 1)
48107
+ })),
48108
+ minExtent: (isGlobalView ? COASTLINE_MIN_EXTENT_GLOBAL : COASTLINE_MIN_EXTENT) * minDim
48109
+ };
48110
+ }
48111
+ const riverColor = mix(palette.colors.blue, water, 32);
47096
48112
  const rivers = [];
47097
48113
  if (data.rivers) {
47098
48114
  for (const [, f] of decodeLayer(data.rivers)) {
@@ -47113,6 +48129,9 @@ function layoutMap(resolved, data, size, opts) {
47113
48129
  return R_MIN + Math.max(0, Math.min(1, t)) * (R_MAX - R_MIN);
47114
48130
  };
47115
48131
  const poiFill = (p) => {
48132
+ const directHex = p.color ? resolveColor(p.color, palette) : null;
48133
+ if (directHex)
48134
+ return { fill: directHex, stroke: mix(directHex, palette.text, 18) };
47116
48135
  for (const group of resolved.tagGroups) {
47117
48136
  const val = p.tags[group.name.toLowerCase()];
47118
48137
  if (!val) continue;
@@ -47145,38 +48164,108 @@ function layoutMap(resolved, data, size, opts) {
47145
48164
  const xy = project(p.lon, p.lat);
47146
48165
  if (xy) projected.push({ p, xy });
47147
48166
  }
47148
- const coloGroups = /* @__PURE__ */ new Map();
48167
+ const placePoi = (e, cx, cy, clusterId) => {
48168
+ const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
48169
+ poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
48170
+ const num = routeNumberById.get(e.p.id);
48171
+ pois.push({
48172
+ id: e.p.id,
48173
+ cx,
48174
+ cy,
48175
+ r: radiusFor(e.p),
48176
+ fill: fill2,
48177
+ stroke: stroke2,
48178
+ lineNumber: e.p.lineNumber,
48179
+ implicit: !!e.p.implicit,
48180
+ isOrigin: originIds.has(e.p.id),
48181
+ ...num !== void 0 && { routeNumber: num },
48182
+ ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags },
48183
+ ...clusterId !== void 0 && { clusterId }
48184
+ });
48185
+ };
48186
+ const clusters = [];
48187
+ const connected = /* @__PURE__ */ new Set();
48188
+ for (const e of resolved.edges) {
48189
+ connected.add(e.fromId);
48190
+ connected.add(e.toId);
48191
+ }
48192
+ for (const rt of resolved.routes) {
48193
+ rt.stopIds.forEach((id) => connected.add(id));
48194
+ }
48195
+ const radiusOf = (e) => radiusFor(e.p);
47149
48196
  for (const e of projected) {
47150
- const key = `${Math.round(e.xy[0] / COLO_EPS)},${Math.round(e.xy[1] / COLO_EPS)}`;
47151
- const arr = coloGroups.get(key);
47152
- if (arr) arr.push(e);
47153
- else coloGroups.set(key, [e]);
47154
- }
47155
- for (const group of coloGroups.values()) {
47156
- group.forEach((e, i) => {
47157
- let cx = e.xy[0];
47158
- let cy = e.xy[1];
47159
- if (group.length > 1) {
47160
- const ang = i * GOLDEN_ANGLE;
47161
- cx += Math.cos(ang) * COLO_R;
47162
- cy += Math.sin(ang) * COLO_R;
47163
- }
47164
- const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
47165
- poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
47166
- const num = routeNumberById.get(e.p.id);
47167
- pois.push({
47168
- id: e.p.id,
47169
- cx,
47170
- cy,
47171
- r: radiusFor(e.p),
47172
- fill: fill2,
47173
- stroke: stroke2,
47174
- lineNumber: e.p.lineNumber,
47175
- implicit: !!e.p.implicit,
47176
- isOrigin: originIds.has(e.p.id),
47177
- ...num !== void 0 && { routeNumber: num },
47178
- ...Object.keys(e.p.tags).length > 0 && { tags: e.p.tags }
47179
- });
48197
+ if (connected.has(e.p.id)) placePoi(e, e.xy[0], e.xy[1]);
48198
+ }
48199
+ const groups = [];
48200
+ for (const e of projected) {
48201
+ if (connected.has(e.p.id)) continue;
48202
+ const r = radiusOf(e);
48203
+ const near = groups.find(
48204
+ (g) => g.some(
48205
+ (q) => Math.hypot(q.xy[0] - e.xy[0], q.xy[1] - e.xy[1]) < (r + radiusOf(q)) * STACK_OVERLAP
48206
+ )
48207
+ );
48208
+ if (near) near.push(e);
48209
+ else groups.push([e]);
48210
+ }
48211
+ for (const g of groups) {
48212
+ if (g.length === 1) {
48213
+ placePoi(g[0], g[0].xy[0], g[0].xy[1]);
48214
+ continue;
48215
+ }
48216
+ const clusterId = g[0].p.id;
48217
+ const cx0 = g.reduce((s, e) => s + e.xy[0], 0) / g.length;
48218
+ const cy0 = g.reduce((s, e) => s + e.xy[1], 0) / g.length;
48219
+ const maxR = Math.max(...g.map(radiusOf));
48220
+ const sep = 2 * maxR + STACK_RING_GAP;
48221
+ const ringR = Math.max(
48222
+ COLO_R,
48223
+ sep / (2 * Math.sin(Math.PI / Math.max(g.length, 2)))
48224
+ );
48225
+ const positions = g.map((e, i) => {
48226
+ if (g.length <= STACK_RING_MAX) {
48227
+ const ang2 = -Math.PI / 2 + i * 2 * Math.PI / g.length;
48228
+ return {
48229
+ e,
48230
+ mx: cx0 + Math.cos(ang2) * ringR,
48231
+ my: cy0 + Math.sin(ang2) * ringR
48232
+ };
48233
+ }
48234
+ const ang = i * GOLDEN_ANGLE;
48235
+ const rr = ringR * Math.sqrt((i + 1) / g.length);
48236
+ return { e, mx: cx0 + Math.cos(ang) * rr, my: cy0 + Math.sin(ang) * rr };
48237
+ });
48238
+ let minX = cx0 - maxR;
48239
+ let maxX = cx0 + maxR;
48240
+ let minY = cy0 - maxR;
48241
+ let maxY = cy0 + maxR;
48242
+ for (const { mx, my, e } of positions) {
48243
+ const r = radiusOf(e);
48244
+ minX = Math.min(minX, mx - r);
48245
+ maxX = Math.max(maxX, mx + r);
48246
+ minY = Math.min(minY, my - r);
48247
+ maxY = Math.max(maxY, my + r);
48248
+ }
48249
+ let dx = 0;
48250
+ let dy = 0;
48251
+ if (minX + dx < 2) dx = 2 - minX;
48252
+ if (maxX + dx > width - 2) dx = width - 2 - maxX;
48253
+ if (minY + dy < 2) dy = 2 - minY;
48254
+ if (maxY + dy > height - 2) dy = height - 2 - maxY;
48255
+ const legsOut = [];
48256
+ for (const { e, mx, my } of positions) {
48257
+ const fx = mx + dx;
48258
+ const fy = my + dy;
48259
+ placePoi(e, fx, fy, clusterId);
48260
+ legsOut.push({ x2: fx, y2: fy, color: poiFill(e.p).fill });
48261
+ }
48262
+ clusters.push({
48263
+ id: clusterId,
48264
+ cx: cx0 + dx,
48265
+ cy: cy0 + dy,
48266
+ count: g.length,
48267
+ hitR: ringR + maxR + 6,
48268
+ legs: legsOut
47180
48269
  });
47181
48270
  }
47182
48271
  const legs = [];
@@ -47226,16 +48315,26 @@ function layoutMap(resolved, data, size, opts) {
47226
48315
  if (!a || !b) continue;
47227
48316
  const mx = (a.cx + b.cx) / 2;
47228
48317
  const my = (a.cy + b.cy) / 2;
48318
+ const bow = {
48319
+ curved: leg.style === "arc",
48320
+ offset: 0,
48321
+ labelX: mx,
48322
+ labelY: my - 4
48323
+ };
48324
+ const routeLabelStyle = leg.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
47229
48325
  legs.push({
47230
- d: legPath(a, b, leg.style === "arc", 0),
48326
+ d: legPath(a, b, bow.curved, bow.offset),
47231
48327
  width: routeWidthFor(Number(leg.value)),
47232
48328
  color: mix(palette.text, palette.bg, 72),
47233
48329
  arrow: true,
47234
48330
  lineNumber: leg.lineNumber,
47235
48331
  ...leg.label !== void 0 && {
47236
48332
  label: leg.label,
47237
- labelX: mx,
47238
- labelY: my - 4
48333
+ labelX: bow.labelX,
48334
+ labelY: bow.labelY,
48335
+ labelColor: routeLabelStyle.color,
48336
+ labelHalo: routeLabelStyle.halo,
48337
+ labelHaloColor: routeLabelStyle.haloColor
47239
48338
  }
47240
48339
  });
47241
48340
  }
@@ -47263,20 +48362,29 @@ function layoutMap(resolved, data, size, opts) {
47263
48362
  const a = poiScreen.get(e.fromId);
47264
48363
  const b = poiScreen.get(e.toId);
47265
48364
  if (!a || !b) return;
47266
- const curved = e.style === "arc" || n > 1;
47267
- const offset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
48365
+ const fanOffset = n > 1 ? (i - (n - 1) / 2) * FAN_STEP : 0;
47268
48366
  const mx = (a.cx + b.cx) / 2;
47269
48367
  const my = (a.cy + b.cy) / 2;
48368
+ const bow = {
48369
+ curved: e.style === "arc" || n > 1,
48370
+ offset: fanOffset,
48371
+ labelX: mx,
48372
+ labelY: my - 4
48373
+ };
48374
+ const edgeLabelStyle = e.label !== void 0 ? labelOnFill(fillAt(bow.labelX, bow.labelY)) : void 0;
47270
48375
  legs.push({
47271
- d: legPath(a, b, curved, offset),
48376
+ d: legPath(a, b, bow.curved, bow.offset),
47272
48377
  width: widthFor(e),
47273
48378
  color: mix(palette.text, palette.bg, 66),
47274
48379
  arrow: e.directed,
47275
48380
  lineNumber: e.lineNumber,
47276
48381
  ...e.label !== void 0 && {
47277
48382
  label: e.label,
47278
- labelX: mx,
47279
- labelY: my - 4
48383
+ labelX: bow.labelX,
48384
+ labelY: bow.labelY,
48385
+ labelColor: edgeLabelStyle.color,
48386
+ labelHalo: edgeLabelStyle.halo,
48387
+ labelHaloColor: edgeLabelStyle.haloColor
47280
48388
  }
47281
48389
  });
47282
48390
  });
@@ -47318,25 +48426,25 @@ function layoutMap(resolved, data, size, opts) {
47318
48426
  }
47319
48427
  }
47320
48428
  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));
47321
- const regionLabelMode = resolved.directives.regionLabels ?? "off";
48429
+ const showRegionLabels = resolved.directives.noRegionLabels !== true;
48430
+ const isCompact = width < COMPACT_WIDTH_PX;
47322
48431
  const LABEL_PADX = 6;
47323
48432
  const LABEL_PADY = 3;
47324
- const labelW = (text) => measureLegendText(text, FONT) + 2 * LABEL_PADX;
47325
- const labelH = FONT + 2 * LABEL_PADY;
48433
+ const labelW = (text) => measureLegendText(text, FONT2) + 2 * LABEL_PADX;
48434
+ const labelH = FONT2 + 2 * LABEL_PADY;
47326
48435
  const pushRegionLabel = (x, y, text, fill2, lineNumber) => {
47327
- const color = contrastText(
47328
- fill2,
47329
- palette.textOnFillLight,
47330
- palette.textOnFillDark
48436
+ const { color, haloColor } = labelOnFill(fill2);
48437
+ const halfW = measureLegendText(text, FONT2) / 2;
48438
+ const overflows = [y - FONT2 * 0.55, y - FONT2 * 0.1].some(
48439
+ (sy) => fillAt(x - halfW, sy) !== fill2 || fillAt(x + halfW, sy) !== fill2
47331
48440
  );
47332
- const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47333
48441
  labels.push({
47334
48442
  x,
47335
48443
  y,
47336
48444
  text,
47337
48445
  anchor: "middle",
47338
48446
  color,
47339
- halo: true,
48447
+ halo: overflows,
47340
48448
  haloColor,
47341
48449
  lineNumber
47342
48450
  });
@@ -47345,21 +48453,50 @@ function layoutMap(resolved, data, size, opts) {
47345
48453
  US: [-98.5, 39.5]
47346
48454
  // CONUS geographic centre (near Lebanon, Kansas)
47347
48455
  };
47348
- if (regionLabelMode === "full" || regionLabelMode === "abbrev") {
47349
- for (const r of regions) {
47350
- if (r.layer === "base" || r.label === void 0) continue;
47351
- const f = r.layer === "us-state" ? usLayer?.get(r.id) : worldLayer.get(r.id);
47352
- if (!f) continue;
48456
+ const REGION_LABEL_GAP = 2;
48457
+ const regionLabelRect = (cx, cy, text) => {
48458
+ const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
48459
+ return { x: cx - w / 2, y: cy - FONT2 / 2, w, h: FONT2 };
48460
+ };
48461
+ if (showRegionLabels) {
48462
+ const frameContainers = new Set(resolved.poiFrameContainers);
48463
+ const entries = regions.map((r) => {
48464
+ const isContainer = frameContainers.has(r.id);
48465
+ if (r.layer === "base" && !isContainer || r.label === void 0)
48466
+ return null;
48467
+ const isUsState = r.layer === "us-state" || r.id.startsWith("US-");
48468
+ const f = isUsState ? usLayer?.get(r.id) : worldLayer.get(r.id);
48469
+ if (!f) return null;
47353
48470
  const [[x0, y0], [x1, y1]] = path.bounds(f);
47354
- const text = regionLabelMode === "abbrev" ? r.id.replace(/^US-/, "") : r.label;
47355
- if (labelW(text) > x1 - x0 || labelH > y1 - y0) continue;
47356
- const anchor = r.layer !== "us-state" ? WORLD_LABEL_ANCHORS[r.id] : void 0;
48471
+ const boxW = x1 - x0;
48472
+ const boxH = y1 - y0;
48473
+ const abbrev = isUsState ? r.id.replace(/^US-/, "") : void 0;
48474
+ const candidates = abbrev !== void 0 ? isCompact ? [abbrev, r.label] : [r.label, abbrev] : [r.label];
48475
+ const anchor = !isUsState ? WORLD_LABEL_ANCHORS[r.id] : void 0;
47357
48476
  const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
47358
- if (!c || !Number.isFinite(c[0])) continue;
48477
+ if (!c || !Number.isFinite(c[0])) return null;
48478
+ return { r, c, boxW, boxH, area: boxW * boxH, candidates };
48479
+ }).filter((e) => e !== null).sort((a, b) => b.area - a.area || a.r.lineNumber - b.r.lineNumber);
48480
+ const placedRegionRects = [];
48481
+ const POI_LABEL_PAD = 14;
48482
+ const poiObstacles = pois.map((p) => ({
48483
+ x: p.cx - p.r - POI_LABEL_PAD,
48484
+ y: p.cy - p.r - POI_LABEL_PAD,
48485
+ w: 2 * (p.r + POI_LABEL_PAD),
48486
+ h: 2 * (p.r + POI_LABEL_PAD)
48487
+ }));
48488
+ for (const { r, c, boxW, boxH, candidates } of entries) {
48489
+ const text = candidates.find((t) => {
48490
+ if (labelW(t) > boxW || labelH > boxH) return false;
48491
+ const rect = regionLabelRect(c[0], c[1], t);
48492
+ return !placedRegionRects.some((p) => rectsOverlap(rect, p)) && !poiObstacles.some((o) => rectsOverlap(rect, o));
48493
+ });
48494
+ if (text === void 0) continue;
48495
+ placedRegionRects.push(regionLabelRect(c[0], c[1], text));
47359
48496
  pushRegionLabel(c[0], c[1], text, r.fill, r.lineNumber);
47360
48497
  }
47361
48498
  for (const seed of insetLabelSeeds) {
47362
- const text = regionLabelMode === "abbrev" ? seed.iso.replace(/^US-/, "") : seed.name;
48499
+ const text = isCompact ? seed.iso.replace(/^US-/, "") : seed.name;
47363
48500
  const src = regionById.get(seed.iso);
47364
48501
  pushRegionLabel(
47365
48502
  seed.x,
@@ -47370,22 +48507,26 @@ function layoutMap(resolved, data, size, opts) {
47370
48507
  );
47371
48508
  }
47372
48509
  }
47373
- const poiLabelMode = resolved.directives.poiLabels ?? "auto";
47374
- if (poiLabelMode !== "off") {
47375
- const ordered = [...pois].sort(
47376
- (a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1)
47377
- );
48510
+ if (resolved.directives.noPoiLabels !== true) {
48511
+ const ordered = [...pois].filter((p) => p.clusterId === void 0).sort((a, b) => a.lineNumber - b.lineNumber || (a.id < b.id ? -1 : 1));
47378
48512
  const poiById = new Map(resolved.pois.map((q) => [q.id, q]));
47379
48513
  const labelText = (p) => {
47380
48514
  const src = poiById.get(p.id);
47381
48515
  return src?.label ?? src?.name ?? p.id;
47382
48516
  };
47383
- const poiLabH = FONT * 1.25;
48517
+ const poiLabH = FONT2 * 1.25;
47384
48518
  const labelInfo = (p) => {
47385
48519
  const text = labelText(p);
47386
- return { text, w: measureLegendText(text, FONT) };
48520
+ return { text, w: measureLegendText(text, FONT2) };
47387
48521
  };
47388
48522
  const GAP = 3;
48523
+ const clusterMembersById = /* @__PURE__ */ new Map();
48524
+ for (const p of pois) {
48525
+ if (p.clusterId === void 0) continue;
48526
+ const arr = clusterMembersById.get(p.clusterId);
48527
+ if (arr) arr.push(p);
48528
+ else clusterMembersById.set(p.clusterId, [p]);
48529
+ }
47389
48530
  const inlineRect = (p, w, side) => {
47390
48531
  switch (side) {
47391
48532
  case "right":
@@ -47415,11 +48556,11 @@ function layoutMap(resolved, data, size, opts) {
47415
48556
  const x = side === "right" ? rect.x : side === "left" ? rect.x + w : p.cx;
47416
48557
  labels.push({
47417
48558
  x,
47418
- y: rect.y + poiLabH / 2 + FONT / 3,
48559
+ y: rect.y + poiLabH / 2 + FONT2 / 3,
47419
48560
  text,
47420
48561
  anchor,
47421
48562
  color: palette.text,
47422
- halo: true,
48563
+ halo: false,
47423
48564
  haloColor: palette.bg,
47424
48565
  poiId: p.id,
47425
48566
  lineNumber: p.lineNumber
@@ -47430,43 +48571,60 @@ function layoutMap(resolved, data, size, opts) {
47430
48571
  return rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect);
47431
48572
  };
47432
48573
  const GROUP_R = 30;
47433
- const groups = [];
48574
+ const groups2 = [];
47434
48575
  for (const p of ordered) {
47435
- const near = groups.find(
48576
+ const near = groups2.find(
47436
48577
  (g) => g.some((q) => Math.hypot(q.cx - p.cx, q.cy - p.cy) < GROUP_R)
47437
48578
  );
47438
48579
  if (near) near.push(p);
47439
- else groups.push([p]);
48580
+ else groups2.push([p]);
47440
48581
  }
47441
48582
  const ROW_GAP2 = 3;
47442
48583
  const step = poiLabH + ROW_GAP2;
47443
48584
  const COL_GAP = 16;
47444
- const placeColumn = (group) => {
47445
- const items = group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48585
+ const makeItems = (group) => group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
48586
+ const columnRows = (items, side) => {
47446
48587
  const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
47447
48588
  const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
47448
- const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
47449
48589
  const maxW = Math.max(...items.map((o) => o.w));
47450
- const side = right + COL_GAP + maxW <= width - 2 ? "right" : "left";
47451
- const colX = side === "right" ? right + COL_GAP : left - COL_GAP;
48590
+ const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
48591
+ const colX = side === "right" ? Math.min(right + COL_GAP, width - 2 - maxW) : Math.max(left - COL_GAP, 2 + maxW);
47452
48592
  const totalH = items.length * step;
47453
48593
  let startY = cyMid - totalH / 2;
47454
48594
  startY = Math.max(2, Math.min(startY, height - totalH - 2));
47455
- items.forEach((o, i) => {
48595
+ return items.map((o, i) => {
47456
48596
  const rowCy = startY + i * step + step / 2;
47457
- obstacles.push({
47458
- x: side === "right" ? colX : colX - o.w,
47459
- y: rowCy - poiLabH / 2,
47460
- w: o.w,
47461
- h: poiLabH
47462
- });
48597
+ return {
48598
+ o,
48599
+ colX,
48600
+ rowCy,
48601
+ rect: {
48602
+ x: side === "right" ? colX : colX - o.w,
48603
+ y: rowCy - poiLabH / 2,
48604
+ w: o.w,
48605
+ h: poiLabH
48606
+ }
48607
+ };
48608
+ });
48609
+ };
48610
+ const wouldColumnBeClean = (items, side) => columnRows(items, side).every(
48611
+ ({ rect }) => rect.x >= 0 && rect.x + rect.w <= width && rect.y >= 0 && rect.y + rect.h <= height && !collides(rect)
48612
+ );
48613
+ const defaultColumnSide = (items) => {
48614
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48615
+ const maxW = Math.max(...items.map((o) => o.w));
48616
+ return right + COL_GAP + maxW <= width - 2 ? "right" : "left";
48617
+ };
48618
+ const commitColumn = (items, side, clusterId) => {
48619
+ for (const { o, colX, rowCy, rect } of columnRows(items, side)) {
48620
+ obstacles.push(rect);
47463
48621
  labels.push({
47464
48622
  x: colX,
47465
- y: rowCy + FONT / 3,
48623
+ y: rowCy + FONT2 / 3,
47466
48624
  text: o.text,
47467
48625
  anchor: side === "right" ? "start" : "end",
47468
48626
  color: palette.text,
47469
- halo: true,
48627
+ halo: false,
47470
48628
  haloColor: palette.bg,
47471
48629
  leader: {
47472
48630
  x1: o.p.cx,
@@ -47476,24 +48634,141 @@ function layoutMap(resolved, data, size, opts) {
47476
48634
  },
47477
48635
  leaderColor: o.p.fill,
47478
48636
  poiId: o.p.id,
47479
- lineNumber: o.p.lineNumber
48637
+ lineNumber: o.p.lineNumber,
48638
+ ...clusterId !== void 0 && { clusterMember: clusterId }
47480
48639
  });
48640
+ }
48641
+ };
48642
+ const pushHidden = (p) => {
48643
+ const { text, w } = labelInfo(p);
48644
+ let x = p.cx + p.r + GAP;
48645
+ let anchor = "start";
48646
+ if (x + w > width) {
48647
+ x = p.cx - p.r - GAP - w;
48648
+ anchor = "end";
48649
+ }
48650
+ const y = Math.max(0, Math.min(p.cy - poiLabH / 2, height - poiLabH));
48651
+ labels.push({
48652
+ x: anchor === "start" ? x : x + w,
48653
+ y: y + poiLabH / 2 + FONT2 / 3,
48654
+ text,
48655
+ anchor,
48656
+ color: palette.text,
48657
+ halo: false,
48658
+ haloColor: palette.bg,
48659
+ poiId: p.id,
48660
+ hidden: true,
48661
+ lineNumber: p.lineNumber
47481
48662
  });
47482
48663
  };
47483
- for (const g of groups) {
48664
+ for (const [clusterId, members] of clusterMembersById) {
48665
+ if (members.length === 0) continue;
48666
+ const items = makeItems(members);
48667
+ const side = wouldColumnBeClean(items, "right") ? "right" : wouldColumnBeClean(items, "left") ? "left" : defaultColumnSide(items);
48668
+ commitColumn(items, side, clusterId);
48669
+ }
48670
+ const maxExtent = MAX_CLUSTER_EXTENT_FACTOR * Math.min(width, height);
48671
+ const clusterPending = [];
48672
+ for (const g of groups2) {
48673
+ const items = makeItems(g);
47484
48674
  if (g.length === 1) {
47485
- const p = g[0];
47486
- const { text, w } = labelInfo(p);
48675
+ const { p, text, w } = items[0];
47487
48676
  const side = ["right", "left", "above", "below"].find(
47488
48677
  (s) => inlineFits(p, w, s)
47489
48678
  );
47490
- if (side) {
47491
- pushInline(p, text, w, side);
47492
- continue;
48679
+ if (side) pushInline(p, text, w, side);
48680
+ else commitColumn(items, defaultColumnSide(items));
48681
+ continue;
48682
+ }
48683
+ const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
48684
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
48685
+ const minCy = Math.min(...items.map((o) => o.p.cy));
48686
+ const maxCy = Math.max(...items.map((o) => o.p.cy));
48687
+ const diag = Math.hypot(right - left, maxCy - minCy);
48688
+ if (diag > maxExtent || items.length > MAX_COLUMN_ROWS) {
48689
+ items.forEach((o) => pushHidden(o.p));
48690
+ } else {
48691
+ clusterPending.push(items);
48692
+ }
48693
+ }
48694
+ for (const items of clusterPending) {
48695
+ const side = ["right", "left"].find(
48696
+ (s) => wouldColumnBeClean(items, s)
48697
+ );
48698
+ if (side) commitColumn(items, side);
48699
+ else items.forEach((o) => pushHidden(o.p));
48700
+ }
48701
+ }
48702
+ if (resolved.directives.noContextLabels !== true) {
48703
+ for (const l of labels) {
48704
+ if (l.hidden) continue;
48705
+ const w = labelW(l.text);
48706
+ const x = l.anchor === "start" ? l.x : l.anchor === "end" ? l.x - w : l.x - w / 2;
48707
+ obstacles.push({ x, y: l.y - labelH / 2, w, h: labelH });
48708
+ }
48709
+ for (const box of insets)
48710
+ obstacles.push({ x: box.x, y: box.y, w: box.w, h: box.h });
48711
+ const countryCandidates = [];
48712
+ for (const f of worldLayer.values()) {
48713
+ const iso = typeof f.id === "string" ? f.id : String(f.id ?? "");
48714
+ if (!iso || regionById.has(iso)) continue;
48715
+ let hasReferencedSub = false;
48716
+ for (const k of regionById.keys())
48717
+ if (k.startsWith(iso + "-")) {
48718
+ hasReferencedSub = true;
48719
+ break;
47493
48720
  }
48721
+ if (hasReferencedSub) continue;
48722
+ const b = path.bounds(f);
48723
+ const [x0, y0] = b[0];
48724
+ const [x1, y1] = b[1];
48725
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48726
+ const anchorLngLat = WORLD_LABEL_ANCHORS[iso];
48727
+ const a = anchorLngLat ? project(anchorLngLat[0], anchorLngLat[1]) : path.centroid(f);
48728
+ countryCandidates.push({
48729
+ name: f.properties?.name ?? iso,
48730
+ bbox: [x0, y0, x1, y1],
48731
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48732
+ });
48733
+ }
48734
+ const framedStateContainers = (resolved.poiFrameContainers ?? []).some(
48735
+ (id) => id.startsWith("US-")
48736
+ );
48737
+ if (usLayer && framedStateContainers) {
48738
+ const containerSet = new Set(resolved.poiFrameContainers);
48739
+ for (const [iso, f] of usLayer) {
48740
+ if (containerSet.has(iso) || regionById.has(iso)) continue;
48741
+ const viewF = cullFeatureToView(f);
48742
+ if (!viewF) continue;
48743
+ const b = path.bounds(viewF);
48744
+ const [x0, y0] = b[0];
48745
+ const [x1, y1] = b[1];
48746
+ if (!Number.isFinite(x0) || !Number.isFinite(x1)) continue;
48747
+ const a = path.centroid(viewF);
48748
+ countryCandidates.push({
48749
+ name: f.properties?.name ?? iso,
48750
+ bbox: [x0, y0, x1, y1],
48751
+ anchor: a && Number.isFinite(a[0]) ? [a[0], a[1]] : null
48752
+ });
47494
48753
  }
47495
- placeColumn(g);
47496
48754
  }
48755
+ const contextLabels = placeContextLabels({
48756
+ projection: resolved.projection,
48757
+ dLonSpan,
48758
+ dLatSpan,
48759
+ width,
48760
+ height,
48761
+ waterBodies: data.waterBodies,
48762
+ countries: countryCandidates,
48763
+ palette,
48764
+ project,
48765
+ collides,
48766
+ // Water labels must stay over open water — `fillAt` returns the ocean
48767
+ // backdrop colour off-land and a region fill on-land (lakes/states count
48768
+ // as land here, which is the safe side for an ocean name).
48769
+ overLand: (x, y) => fillAt(x, y) !== water
48770
+ });
48771
+ labels.push(...contextLabels);
47497
48772
  }
47498
48773
  let legend = null;
47499
48774
  if (!resolved.directives.noLegend) {
@@ -47528,24 +48803,35 @@ function layoutMap(resolved, data, size, opts) {
47528
48803
  ...resolved.caption !== void 0 && { caption: resolved.caption },
47529
48804
  regions,
47530
48805
  rivers,
48806
+ relief,
48807
+ reliefHatch,
48808
+ coastlineStyle,
47531
48809
  legs,
47532
48810
  pois,
48811
+ clusters,
47533
48812
  labels,
47534
48813
  legend,
47535
48814
  insets,
47536
- insetRegions
48815
+ insetRegions,
48816
+ projection,
48817
+ stretch: stretchParams,
48818
+ diagnostics: []
47537
48819
  };
47538
48820
  }
47539
- var import_d3_geo2, import_topojson_client2, FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, COLO_EPS, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT, RIVER_WIDTH, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_WATER_LIGHT, MUTED_WATER_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, MUTED_LAND_DARK, COLO_R, GOLDEN_ANGLE, FAN_STEP, ARC_CURVE_FRAC, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, US_NON_CONUS;
48821
+ var import_d3_geo2, import_topojson_client2, FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT2, MAX_CLUSTER_EXTENT_FACTOR, MAX_COLUMN_ROWS, REGION_LABEL_HALO_RATIO, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, COMPACT_WIDTH_PX, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, COASTLINE_RING_COUNT, COASTLINE_D0, COASTLINE_STEP, COASTLINE_THICKNESS, COASTLINE_OPACITY_NEAR, COASTLINE_OPACITY_FAR, COASTLINE_MIN_EXTENT, COASTLINE_MIN_EXTENT_GLOBAL, COASTLINE_STROKE_MIX, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, STACK_OVERLAP, STACK_RING_MAX, STACK_RING_GAP, FAN_STEP, ARC_CURVE_FRAC, decodeCache, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, inAlaska, inHawaii, FOREIGN_BORDER, US_NON_CONUS;
47540
48822
  var init_layout15 = __esm({
47541
48823
  "src/map/layout.ts"() {
47542
48824
  "use strict";
47543
48825
  import_d3_geo2 = require("d3-geo");
47544
48826
  import_topojson_client2 = require("topojson-client");
47545
48827
  init_color_utils();
48828
+ init_geo();
48829
+ init_colorize();
48830
+ init_colors();
47546
48831
  init_label_layout();
47547
48832
  init_legend_constants();
47548
48833
  init_title_constants();
48834
+ init_context_labels();
47549
48835
  FIT_PAD = 24;
47550
48836
  RAMP_FLOOR = 15;
47551
48837
  R_DEFAULT = 6;
@@ -47553,29 +48839,66 @@ var init_layout15 = __esm({
47553
48839
  R_MAX = 22;
47554
48840
  W_MIN = 1.25;
47555
48841
  W_MAX = 8;
47556
- FONT = 11;
47557
- COLO_EPS = 1.5;
47558
- LAND_TINT_LIGHT = 58;
47559
- LAND_TINT_DARK = 75;
48842
+ FONT2 = 11;
48843
+ MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48844
+ MAX_COLUMN_ROWS = 7;
48845
+ REGION_LABEL_HALO_RATIO = 4.5;
48846
+ LAND_TINT_LIGHT = 12;
48847
+ LAND_TINT_DARK = 24;
47560
48848
  TAG_TINT_LIGHT = 60;
47561
48849
  TAG_TINT_DARK = 68;
47562
- WATER_TINT = 55;
48850
+ WATER_TINT_LIGHT = 24;
48851
+ WATER_TINT_DARK = 24;
47563
48852
  RIVER_WIDTH = 1.3;
48853
+ COMPACT_WIDTH_PX = 480;
48854
+ RELIEF_MIN_AREA = 12;
48855
+ RELIEF_MIN_DIM = 2;
48856
+ RELIEF_HATCH_SPACING = 2;
48857
+ RELIEF_HATCH_WIDTH = 0.15;
48858
+ RELIEF_HATCH_STRENGTH = 32;
48859
+ COASTLINE_RING_COUNT = 5;
48860
+ COASTLINE_D0 = 16e-4;
48861
+ COASTLINE_STEP = 28e-4;
48862
+ COASTLINE_THICKNESS = 14e-4;
48863
+ COASTLINE_OPACITY_NEAR = 0.5;
48864
+ COASTLINE_OPACITY_FAR = 0.1;
48865
+ COASTLINE_MIN_EXTENT = 6e-4;
48866
+ COASTLINE_MIN_EXTENT_GLOBAL = 6e-4;
48867
+ COASTLINE_STROKE_MIX = 32;
47564
48868
  FOREIGN_TINT_LIGHT = 30;
47565
48869
  FOREIGN_TINT_DARK = 62;
47566
- MUTED_WATER_LIGHT = 14;
47567
- MUTED_WATER_DARK = 10;
47568
48870
  MUTED_FOREIGN_LIGHT = 28;
47569
48871
  MUTED_FOREIGN_DARK = 16;
47570
- MUTED_LAND_DARK = 24;
47571
48872
  COLO_R = 9;
47572
48873
  GOLDEN_ANGLE = 2.399963229728653;
48874
+ STACK_OVERLAP = 1;
48875
+ STACK_RING_MAX = 8;
48876
+ STACK_RING_GAP = 4;
47573
48877
  FAN_STEP = 16;
47574
48878
  ARC_CURVE_FRAC = 0.18;
48879
+ decodeCache = /* @__PURE__ */ new WeakMap();
47575
48880
  usConusProjection = () => (0, import_d3_geo2.geoConicEqualArea)().parallels([29.5, 45.5]).rotate([96, 0]);
47576
48881
  alaskaProjection = () => (0, import_d3_geo2.geoConicEqualArea)().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
47577
48882
  hawaiiProjection = () => (0, import_d3_geo2.geoMercator)();
47578
48883
  INSET_STATES = /* @__PURE__ */ new Set(["US-AK", "US-HI"]);
48884
+ inAlaska = (lon, lat) => lat >= 51 && (lon <= -129 || lon >= 172);
48885
+ inHawaii = (lon, lat) => lat >= 18 && lat <= 23 && lon >= -161 && lon <= -154;
48886
+ FOREIGN_BORDER = {
48887
+ CA: [
48888
+ "US-AK",
48889
+ "US-WA",
48890
+ "US-ID",
48891
+ "US-MT",
48892
+ "US-ND",
48893
+ "US-MN",
48894
+ "US-MI",
48895
+ "US-NY",
48896
+ "US-VT",
48897
+ "US-NH",
48898
+ "US-ME"
48899
+ ],
48900
+ MX: ["US-CA", "US-AZ", "US-NM", "US-TX"]
48901
+ };
47579
48902
  US_NON_CONUS = /* @__PURE__ */ new Set([
47580
48903
  "US-AK",
47581
48904
  "US-HI",
@@ -47594,6 +48917,58 @@ __export(renderer_exports16, {
47594
48917
  renderMap: () => renderMap,
47595
48918
  renderMapForExport: () => renderMapForExport
47596
48919
  });
48920
+ function pointInRing2(px, py, ring) {
48921
+ let inside = false;
48922
+ for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
48923
+ const [xi, yi] = ring[i];
48924
+ const [xj, yj] = ring[j];
48925
+ if (yi > py !== yj > py && px < (xj - xi) * (py - yi) / (yj - yi) + xi)
48926
+ inside = !inside;
48927
+ }
48928
+ return inside;
48929
+ }
48930
+ function ringToPath(ring) {
48931
+ let d = "";
48932
+ for (let i = 0; i < ring.length; i++)
48933
+ d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48934
+ return d + "Z";
48935
+ }
48936
+ function coastlineOuterRings(regions, minExtent) {
48937
+ const paths = [];
48938
+ for (const r of regions) {
48939
+ const rings = parsePathRings(r.d);
48940
+ for (let i = 0; i < rings.length; i++) {
48941
+ const ring = rings[i];
48942
+ if (ring.length < 3) continue;
48943
+ let minX = Infinity;
48944
+ let minY = Infinity;
48945
+ let maxX = -Infinity;
48946
+ let maxY = -Infinity;
48947
+ for (const [x, y] of ring) {
48948
+ if (x < minX) minX = x;
48949
+ if (x > maxX) maxX = x;
48950
+ if (y < minY) minY = y;
48951
+ if (y > maxY) maxY = y;
48952
+ }
48953
+ if (Math.max(maxX - minX, maxY - minY) < minExtent) continue;
48954
+ const [fx, fy] = ring[0];
48955
+ let depth = 0;
48956
+ for (let j = 0; j < rings.length; j++)
48957
+ if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48958
+ if (depth % 2 === 1) continue;
48959
+ paths.push(ringToPath(ring));
48960
+ }
48961
+ }
48962
+ return paths;
48963
+ }
48964
+ function appendWaterLines(g, outerRings, style, flatWater) {
48965
+ const d = outerRings.join(" ");
48966
+ const linesOuterFirst = [...style.lines].sort((a, b) => b.d - a.d);
48967
+ for (const line12 of linesOuterFirst) {
48968
+ 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");
48969
+ g.append("path").attr("d", d).attr("stroke", flatWater).attr("stroke-width", 2 * line12.d).attr("stroke-linejoin", "round").attr("stroke-linecap", "round");
48970
+ }
48971
+ }
47597
48972
  function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims, activeGroupOverride) {
47598
48973
  d3Selection18.select(container).selectAll(":not([data-d3-tooltip])").remove();
47599
48974
  const width = exportDims?.width ?? container.clientWidth;
@@ -47606,6 +48981,11 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47606
48981
  {
47607
48982
  palette,
47608
48983
  isDark,
48984
+ // Export-only: forward the contain-fit request from mapExportDimensions so a
48985
+ // clamped/floored (off-aspect) export canvas letterboxes instead of
48986
+ // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48987
+ // keeps the global stretch-fill.
48988
+ preferContain: exportDims?.preferContain ?? false,
47609
48989
  ...activeGroupOverride !== void 0 && {
47610
48990
  activeGroup: activeGroupOverride
47611
48991
  }
@@ -47619,6 +48999,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47619
48999
  const gRegions = svg.append("g").attr("class", "dgmo-map-regions");
47620
49000
  const drawRegion = (g, r, strokeWidth) => {
47621
49001
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
49002
+ if (r.label) p.attr("data-region-name", r.label);
47622
49003
  if (r.layer !== "base") {
47623
49004
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
47624
49005
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -47639,6 +49020,52 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47639
49020
  }
47640
49021
  };
47641
49022
  for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
49023
+ if (layout.relief.length && layout.reliefHatch) {
49024
+ const h = layout.reliefHatch;
49025
+ const rangeClipId = "dgmo-relief-clip";
49026
+ const landClipId = "dgmo-relief-land";
49027
+ const rangeClip = defs.append("clipPath").attr("id", rangeClipId);
49028
+ for (const s of layout.relief) rangeClip.append("path").attr("d", s.d);
49029
+ const landClip = defs.append("clipPath").attr("id", landClipId);
49030
+ for (const r of layout.regions)
49031
+ if (r.id !== "lake") landClip.append("path").attr("d", r.d);
49032
+ 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");
49033
+ for (let y = h.spacing; y < height; y += h.spacing) {
49034
+ gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
49035
+ }
49036
+ }
49037
+ if (layout.coastlineStyle) {
49038
+ const cs = layout.coastlineStyle;
49039
+ const maskId = "dgmo-map-water-mask";
49040
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
49041
+ mask.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "white");
49042
+ const landD = layout.regions.filter((r) => r.id !== "lake").map((r) => r.d).join(" ");
49043
+ const lakeD = layout.regions.filter((r) => r.id === "lake").map((r) => r.d).join(" ");
49044
+ if (landD) mask.append("path").attr("d", landD).attr("fill", "black");
49045
+ if (lakeD) mask.append("path").attr("d", lakeD).attr("fill", "white");
49046
+ if (layout.insets.length) {
49047
+ const reach = Math.max(0, ...cs.lines.map((l) => l.d + l.thickness));
49048
+ for (const box of layout.insets) {
49049
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
49050
+ mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
49051
+ }
49052
+ }
49053
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
49054
+ appendWaterLines(
49055
+ gWater,
49056
+ coastlineOuterRings(layout.regions, cs.minExtent),
49057
+ cs,
49058
+ layout.background
49059
+ );
49060
+ const byStroke = /* @__PURE__ */ new Map();
49061
+ for (const r of layout.regions) {
49062
+ const arr = byStroke.get(r.stroke);
49063
+ if (arr) arr.push(r.d);
49064
+ else byStroke.set(r.stroke, [r.d]);
49065
+ }
49066
+ for (const [stroke2, ds] of byStroke)
49067
+ gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
49068
+ }
47642
49069
  if (layout.rivers.length) {
47643
49070
  const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47644
49071
  for (const r of layout.rivers) {
@@ -47647,15 +49074,61 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47647
49074
  }
47648
49075
  if (layout.insets.length) {
47649
49076
  const insetG = svg.append("g").attr("class", "dgmo-map-insets");
47650
- for (const box of layout.insets) {
49077
+ layout.insets.forEach((box, bi) => {
47651
49078
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
47652
49079
  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");
47653
- }
49080
+ if (box.contextLand) {
49081
+ const clipId = `dgmo-map-inset-clip-${bi}`;
49082
+ defs.append("clipPath").attr("id", clipId).append("path").attr("d", d);
49083
+ insetG.append("path").attr("d", box.contextLand.d).attr("fill", box.contextLand.fill).attr("clip-path", `url(#${clipId})`);
49084
+ }
49085
+ });
47654
49086
  for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
47655
- }
49087
+ if (layout.coastlineStyle) {
49088
+ const cs = layout.coastlineStyle;
49089
+ const maskId = "dgmo-map-inset-water-mask";
49090
+ const mask = defs.append("mask").attr("id", maskId).attr("maskUnits", "userSpaceOnUse").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height);
49091
+ for (const box of layout.insets) {
49092
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
49093
+ mask.append("path").attr("d", d).attr("fill", "white");
49094
+ }
49095
+ layout.insets.forEach((box, bi) => {
49096
+ if (box.contextLand)
49097
+ mask.append("path").attr("d", box.contextLand.d).attr("fill", "black").attr("clip-path", `url(#dgmo-map-inset-clip-${bi})`);
49098
+ });
49099
+ for (const r of layout.insetRegions)
49100
+ if (r.id !== "lake")
49101
+ mask.append("path").attr("d", r.d).attr("fill", "black");
49102
+ for (const r of layout.insetRegions)
49103
+ if (r.id === "lake")
49104
+ mask.append("path").attr("d", r.d).attr("fill", "white");
49105
+ const clipId = "dgmo-map-inset-water-clip";
49106
+ const clip = defs.append("clipPath").attr("id", clipId);
49107
+ for (const box of layout.insets) {
49108
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
49109
+ clip.append("path").attr("d", d);
49110
+ }
49111
+ 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})`);
49112
+ appendWaterLines(
49113
+ gInsetWater,
49114
+ coastlineOuterRings(layout.insetRegions, cs.minExtent),
49115
+ cs,
49116
+ layout.background
49117
+ );
49118
+ for (const r of layout.insetRegions)
49119
+ gInsetWater.append("path").attr("d", r.d).attr("stroke", r.stroke).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
49120
+ }
49121
+ }
49122
+ const wireSync = (sel, lineNumber) => {
49123
+ if (lineNumber < 1) return;
49124
+ sel.attr("data-line-number", lineNumber);
49125
+ if (onClickItem)
49126
+ sel.style("cursor", "pointer").on("click", () => onClickItem(lineNumber));
49127
+ };
47656
49128
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
47657
49129
  layout.legs.forEach((leg, i) => {
47658
49130
  const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round");
49131
+ wireSync(p, leg.lineNumber);
47659
49132
  if (leg.arrow) {
47660
49133
  const id = `dgmo-map-arrow-${i}`;
47661
49134
  const s = arrowSize(leg.width);
@@ -47663,25 +49136,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47663
49136
  p.attr("marker-end", `url(#${id})`);
47664
49137
  }
47665
49138
  if (leg.label !== void 0 && leg.labelX !== void 0) {
47666
- emitText(
49139
+ const lt = emitText(
47667
49140
  gLegs,
47668
49141
  leg.labelX,
47669
49142
  leg.labelY ?? 0,
47670
49143
  leg.label,
47671
49144
  "middle",
47672
- palette.textMuted,
47673
- haloColor,
47674
- true,
49145
+ leg.labelColor ?? palette.textMuted,
49146
+ leg.labelHaloColor ?? haloColor,
49147
+ leg.labelHalo ?? true,
47675
49148
  LABEL_FONT - 1
47676
49149
  );
49150
+ wireSync(lt, leg.lineNumber);
47677
49151
  }
47678
49152
  });
49153
+ const gSpider = svg.append("g").attr("class", "dgmo-map-spider");
49154
+ for (const cl of layout.clusters) {
49155
+ if (!exportDims) {
49156
+ 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");
49157
+ }
49158
+ for (const leg of cl.legs) {
49159
+ 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");
49160
+ }
49161
+ 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");
49162
+ }
47679
49163
  const gPois = svg.append("g").attr("class", "dgmo-map-pois");
47680
49164
  for (const poi of layout.pois) {
47681
49165
  if (poi.isOrigin) {
47682
49166
  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);
47683
49167
  }
47684
49168
  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);
49169
+ if (poi.clusterId !== void 0)
49170
+ c.attr("data-cluster-member", poi.clusterId);
47685
49171
  if (poi.tags) {
47686
49172
  for (const [group, value] of Object.entries(poi.tags)) {
47687
49173
  c.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
@@ -47709,12 +49195,32 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47709
49195
  }
47710
49196
  const gLabels = svg.append("g").attr("class", "dgmo-map-labels");
47711
49197
  for (const lab of layout.labels) {
49198
+ if (lab.hidden) {
49199
+ if (exportDims) continue;
49200
+ emitText(
49201
+ gLabels,
49202
+ lab.x,
49203
+ lab.y,
49204
+ lab.text,
49205
+ lab.anchor,
49206
+ lab.color,
49207
+ lab.haloColor,
49208
+ lab.halo,
49209
+ LABEL_FONT,
49210
+ lab.italic,
49211
+ lab.letterSpacing
49212
+ ).attr("data-poi", lab.poiId ?? null).attr("data-poi-hidden", "").style("opacity", 0).style("pointer-events", "none");
49213
+ continue;
49214
+ }
47712
49215
  if (lab.leader) {
47713
49216
  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(
47714
49217
  "stroke",
47715
49218
  lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
47716
49219
  ).attr("stroke-width", lab.leaderColor ? 1 : 0.75);
47717
49220
  if (lab.poiId !== void 0) line12.attr("data-poi", lab.poiId);
49221
+ if (lab.clusterMember !== void 0)
49222
+ line12.attr("data-cluster-member", lab.clusterMember);
49223
+ wireSync(line12, lab.lineNumber);
47718
49224
  }
47719
49225
  const t = emitText(
47720
49226
  gLabels,
@@ -47725,11 +49231,38 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47725
49231
  lab.color,
47726
49232
  lab.haloColor,
47727
49233
  lab.halo,
47728
- LABEL_FONT
49234
+ LABEL_FONT,
49235
+ lab.italic,
49236
+ lab.letterSpacing,
49237
+ lab.lines
47729
49238
  );
47730
49239
  if (lab.poiId !== void 0) {
47731
49240
  t.attr("data-poi", lab.poiId).style("cursor", "default");
47732
49241
  }
49242
+ if (lab.clusterMember !== void 0) {
49243
+ t.attr("data-cluster-member", lab.clusterMember);
49244
+ }
49245
+ wireSync(t, lab.lineNumber);
49246
+ }
49247
+ if (!exportDims && layout.clusters.length) {
49248
+ const gBadge = svg.append("g").attr("class", "dgmo-map-cluster-badges");
49249
+ for (const cl of layout.clusters) {
49250
+ const g = gBadge.append("g").attr("data-cluster", cl.id).style("opacity", 0).style("pointer-events", "none");
49251
+ const R = 9;
49252
+ 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);
49253
+ 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);
49254
+ emitText(
49255
+ g,
49256
+ cl.cx,
49257
+ cl.cy + 3,
49258
+ String(cl.count),
49259
+ "middle",
49260
+ palette.text,
49261
+ palette.bg,
49262
+ false,
49263
+ LABEL_FONT
49264
+ );
49265
+ }
47733
49266
  }
47734
49267
  if (layout.legend) {
47735
49268
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
@@ -47763,10 +49296,10 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47763
49296
  }
47764
49297
  }
47765
49298
  if (layout.title) {
47766
- 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);
49299
+ 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);
47767
49300
  }
47768
49301
  if (layout.subtitle) {
47769
- 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);
49302
+ 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);
47770
49303
  }
47771
49304
  if (layout.caption) {
47772
49305
  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);
@@ -47775,10 +49308,21 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
47775
49308
  function renderMapForExport(container, resolved, data, palette, isDark, exportDims) {
47776
49309
  renderMap(container, resolved, data, palette, isDark, void 0, exportDims);
47777
49310
  }
47778
- function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize) {
47779
- const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color).text(text);
49311
+ function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize, italic, letterSpacing, lines) {
49312
+ const t = g.append("text").attr("x", x).attr("y", y).attr("text-anchor", anchor).attr("font-size", fontSize).attr("fill", color);
49313
+ if (lines && lines.length > 1) {
49314
+ const lineHeight = fontSize + 2;
49315
+ const startDy = -((lines.length - 1) / 2) * lineHeight;
49316
+ lines.forEach((ln, i) => {
49317
+ t.append("tspan").attr("x", x).attr("dy", i === 0 ? startDy : lineHeight).text(ln);
49318
+ });
49319
+ } else {
49320
+ t.text(text);
49321
+ }
49322
+ if (italic) t.attr("font-style", "italic");
49323
+ if (letterSpacing) t.attr("letter-spacing", letterSpacing);
47780
49324
  if (withHalo) {
47781
- t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7);
49325
+ 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);
47782
49326
  }
47783
49327
  return t;
47784
49328
  }
@@ -47796,6 +49340,179 @@ var init_renderer16 = __esm({
47796
49340
  }
47797
49341
  });
47798
49342
 
49343
+ // src/map/dimensions.ts
49344
+ var dimensions_exports = {};
49345
+ __export(dimensions_exports, {
49346
+ mapContentAspect: () => mapContentAspect,
49347
+ mapExportDimensions: () => mapExportDimensions
49348
+ });
49349
+ function mapContentAspect(resolved, data, ref = REF) {
49350
+ const { projection, fitTarget } = buildMapProjection(resolved, data);
49351
+ projection.fitSize([ref, ref], fitTarget);
49352
+ const b = (0, import_d3_geo3.geoPath)(projection).bounds(fitTarget);
49353
+ const w = b[1][0] - b[0][0];
49354
+ const h = b[1][1] - b[0][1];
49355
+ const aspect = w / h;
49356
+ return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49357
+ }
49358
+ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49359
+ const raw = mapContentAspect(resolved, data);
49360
+ const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49361
+ const width = baseWidth;
49362
+ let height = Math.round(width / clamped);
49363
+ let chromeReserve = 0;
49364
+ if (resolved.title && resolved.pois.length > 0) {
49365
+ const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
49366
+ chromeReserve += Math.max(FIT_PAD2, bannerBottom + TITLE_GAP) - FIT_PAD2;
49367
+ }
49368
+ let floored = false;
49369
+ if (height - chromeReserve < MIN_MAP_BAND) {
49370
+ height = Math.round(chromeReserve + MIN_MAP_BAND);
49371
+ floored = true;
49372
+ }
49373
+ const preferContain = clamped !== raw || floored;
49374
+ return { width, height, preferContain };
49375
+ }
49376
+ var import_d3_geo3, FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
49377
+ var init_dimensions = __esm({
49378
+ "src/map/dimensions.ts"() {
49379
+ "use strict";
49380
+ import_d3_geo3 = require("d3-geo");
49381
+ init_title_constants();
49382
+ init_layout15();
49383
+ FIT_PAD2 = 24;
49384
+ TITLE_GAP = 16;
49385
+ ASPECT_MAX = 3;
49386
+ ASPECT_MIN = 0.9;
49387
+ MIN_MAP_BAND = 200;
49388
+ FALLBACK_ASPECT = 1.5;
49389
+ REF = 1e3;
49390
+ }
49391
+ });
49392
+
49393
+ // src/map/load-data.ts
49394
+ var load_data_exports = {};
49395
+ __export(load_data_exports, {
49396
+ loadMapData: () => loadMapData
49397
+ });
49398
+ async function loadNodeBuiltins() {
49399
+ const [{ readFile }, { fileURLToPath }, { dirname: dirname2, resolve }] = await Promise.all([
49400
+ import("fs/promises"),
49401
+ import("url"),
49402
+ import("path")
49403
+ ]);
49404
+ return { readFile, fileURLToPath, dirname: dirname2, resolve };
49405
+ }
49406
+ async function readJson(nb, dir, name) {
49407
+ return JSON.parse(await nb.readFile(nb.resolve(dir, name), "utf8"));
49408
+ }
49409
+ async function firstExistingDir(nb, baseDir) {
49410
+ for (const rel of CANDIDATE_DIRS) {
49411
+ const dir = nb.resolve(baseDir, rel);
49412
+ try {
49413
+ await nb.readFile(nb.resolve(dir, FILES.gazetteer), "utf8");
49414
+ return dir;
49415
+ } catch {
49416
+ }
49417
+ }
49418
+ throw new Error(
49419
+ `map data assets not found near ${baseDir} (looked in ${CANDIDATE_DIRS.join(", ")}). Run \`pnpm build:map-data\` and \`pnpm build\`.`
49420
+ );
49421
+ }
49422
+ function validate(data) {
49423
+ const topoOk = (t) => !!t && t.type === "Topology" && !!t.objects;
49424
+ if (!topoOk(data.worldCoarse) || !topoOk(data.worldDetail) || !topoOk(data.usStates) || !data.gazetteer || !Array.isArray(data.gazetteer.cities) || !data.gazetteer.byName) {
49425
+ throw new Error("map data assets are malformed (failed shape validation)");
49426
+ }
49427
+ return data;
49428
+ }
49429
+ function moduleBaseDir(nb) {
49430
+ try {
49431
+ const url = import_meta.url;
49432
+ if (url) return nb.dirname(nb.fileURLToPath(url));
49433
+ } catch {
49434
+ }
49435
+ if (typeof __dirname !== "undefined") return __dirname;
49436
+ return process.cwd();
49437
+ }
49438
+ function loadMapData() {
49439
+ cache ??= (async () => {
49440
+ const nb = await loadNodeBuiltins();
49441
+ const dir = await firstExistingDir(nb, moduleBaseDir(nb));
49442
+ const [
49443
+ worldCoarse,
49444
+ worldDetail,
49445
+ usStates,
49446
+ lakes,
49447
+ rivers,
49448
+ mountainRanges,
49449
+ naLand,
49450
+ naLakes,
49451
+ waterBodies,
49452
+ gazetteer
49453
+ ] = await Promise.all([
49454
+ // worldCoarse (110m) is LOAD-BEARING but NOT a render source: the world
49455
+ // basemap renders from worldDetail (50m) at all scales (resolver pins
49456
+ // basemaps.world = 'detail'). Coarse stays as the authoritative region
49457
+ // name index + dominant-landmass bbox source in resolver.ts. Do not drop it.
49458
+ readJson(nb, dir, FILES.worldCoarse),
49459
+ readJson(nb, dir, FILES.worldDetail),
49460
+ readJson(nb, dir, FILES.usStates),
49461
+ // Lakes/rivers/mountain/NA/water assets are optional — older bundles may predate them.
49462
+ readJson(nb, dir, FILES.lakes).catch(() => void 0),
49463
+ readJson(nb, dir, FILES.rivers).catch(() => void 0),
49464
+ readJson(nb, dir, FILES.mountainRanges).catch(
49465
+ () => void 0
49466
+ ),
49467
+ readJson(nb, dir, FILES.naLand).catch(() => void 0),
49468
+ readJson(nb, dir, FILES.naLakes).catch(() => void 0),
49469
+ readJson(nb, dir, FILES.waterBodies).catch(() => void 0),
49470
+ readJson(nb, dir, FILES.gazetteer)
49471
+ ]);
49472
+ return validate({
49473
+ worldCoarse,
49474
+ worldDetail,
49475
+ usStates,
49476
+ gazetteer,
49477
+ ...lakes && { lakes },
49478
+ ...rivers && { rivers },
49479
+ ...mountainRanges && { mountainRanges },
49480
+ ...naLand && { naLand },
49481
+ ...naLakes && { naLakes },
49482
+ ...waterBodies && { waterBodies }
49483
+ });
49484
+ })().catch((e) => {
49485
+ cache = void 0;
49486
+ throw e;
49487
+ });
49488
+ return cache;
49489
+ }
49490
+ var import_meta, FILES, CANDIDATE_DIRS, cache;
49491
+ var init_load_data = __esm({
49492
+ "src/map/load-data.ts"() {
49493
+ "use strict";
49494
+ import_meta = {};
49495
+ FILES = {
49496
+ worldCoarse: "world-coarse.json",
49497
+ worldDetail: "world-detail.json",
49498
+ usStates: "us-states.json",
49499
+ lakes: "lakes.json",
49500
+ rivers: "rivers.json",
49501
+ mountainRanges: "mountain-ranges.json",
49502
+ naLand: "na-land.json",
49503
+ naLakes: "na-lakes.json",
49504
+ waterBodies: "water-bodies.json",
49505
+ gazetteer: "gazetteer.json"
49506
+ };
49507
+ CANDIDATE_DIRS = [
49508
+ "./data",
49509
+ "./map-data",
49510
+ "../map-data",
49511
+ "../src/map/data"
49512
+ ];
49513
+ }
49514
+ });
49515
+
47799
49516
  // src/pyramid/renderer.ts
47800
49517
  var renderer_exports17 = {};
47801
49518
  __export(renderer_exports17, {
@@ -49797,8 +51514,8 @@ function renderSequenceDiagram(container, parsed, palette, isDark, _onNavigateTo
49797
51514
  const lines = splitParticipantLabel(p.label, LABEL_MAX_CHARS);
49798
51515
  if (lines.length === 0) continue;
49799
51516
  const widest = Math.max(...lines.map((l) => l.length));
49800
- const labelWidth = widest * LABEL_CHAR_WIDTH + 10;
49801
- uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth);
51517
+ const labelWidth2 = widest * LABEL_CHAR_WIDTH + 10;
51518
+ uniformBoxWidth = Math.max(uniformBoxWidth, labelWidth2);
49802
51519
  }
49803
51520
  uniformBoxWidth = Math.min(MAX_BOX_WIDTH, uniformBoxWidth);
49804
51521
  const effectiveGap = Math.max(PARTICIPANT_GAP, uniformBoxWidth + 30);
@@ -52493,15 +54210,15 @@ function renderArcDiagram(container, parsed, palette, _isDark, onClickItem, expo
52493
54210
  textColor,
52494
54211
  onClickItem
52495
54212
  );
52496
- const neighbors = /* @__PURE__ */ new Map();
52497
- for (const node of nodes) neighbors.set(node, /* @__PURE__ */ new Set());
54213
+ const neighbors2 = /* @__PURE__ */ new Map();
54214
+ for (const node of nodes) neighbors2.set(node, /* @__PURE__ */ new Set());
52498
54215
  for (const link of links) {
52499
- neighbors.get(link.source).add(link.target);
52500
- neighbors.get(link.target).add(link.source);
54216
+ neighbors2.get(link.source).add(link.target);
54217
+ neighbors2.get(link.target).add(link.source);
52501
54218
  }
52502
54219
  const FADE_OPACITY3 = 0.1;
52503
54220
  function handleMouseEnter(hovered) {
52504
- const connected = neighbors.get(hovered);
54221
+ const connected = neighbors2.get(hovered);
52505
54222
  g.selectAll(".arc-link").each(function() {
52506
54223
  const el = d3Selection23.select(this);
52507
54224
  const src = el.attr("data-source");
@@ -54492,7 +56209,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54492
56209
  8,
54493
56210
  Math.floor(OVERLAP_WRAP_TARGET_W / OVERLAP_CH_W)
54494
56211
  );
54495
- function wrapLabel2(text, maxChars) {
56212
+ function wrapLabel3(text, maxChars) {
54496
56213
  const words = text.split(/\s+/).filter(Boolean);
54497
56214
  const lines = [];
54498
56215
  let cur = "";
@@ -54538,7 +56255,7 @@ function renderVenn(container, parsed, palette, _isDark, onClickItem, exportDims
54538
56255
  if (!ov.label) continue;
54539
56256
  const idxs = ov.sets.map((s) => vennSets.findIndex((vs) => vs.name === s));
54540
56257
  if (idxs.some((idx) => idx < 0)) continue;
54541
- const lines = wrapLabel2(ov.label, MAX_WRAP_CHARS);
56258
+ const lines = wrapLabel3(ov.label, MAX_WRAP_CHARS);
54542
56259
  wrappedOverlapLabels.set(ov, lines);
54543
56260
  const dir = predictOverlapDirRaw(idxs);
54544
56261
  const longest = lines.reduce((m, l) => Math.max(m, l.length), 0);
@@ -55975,25 +57692,29 @@ async function renderForExport(content, theme, palette, viewState, options) {
55975
57692
  if (detectedType === "map") {
55976
57693
  const { parseMap: parseMap2 } = await Promise.resolve().then(() => (init_parser12(), parser_exports11));
55977
57694
  const { resolveMap: resolveMap2 } = await Promise.resolve().then(() => (init_resolver2(), resolver_exports));
55978
- const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
55979
57695
  const { renderMapForExport: renderMapForExport2 } = await Promise.resolve().then(() => (init_renderer16(), renderer_exports16));
57696
+ const { mapExportDimensions: mapExportDimensions2 } = await Promise.resolve().then(() => (init_dimensions(), dimensions_exports));
55980
57697
  const effectivePalette2 = await resolveExportPalette(theme, palette);
55981
57698
  const mapParsed = parseMap2(content);
55982
- let mapData;
55983
- try {
55984
- mapData = await loadMapData2();
55985
- } catch {
55986
- return "";
57699
+ let mapData = options?.mapData;
57700
+ if (!mapData) {
57701
+ const { loadMapData: loadMapData2 } = await Promise.resolve().then(() => (init_load_data(), load_data_exports));
57702
+ try {
57703
+ mapData = await loadMapData2();
57704
+ } catch {
57705
+ return "";
57706
+ }
55987
57707
  }
55988
57708
  const mapResolved = resolveMap2(mapParsed, mapData);
55989
- const container2 = createExportContainer(EXPORT_WIDTH, EXPORT_HEIGHT);
57709
+ const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57710
+ const container2 = createExportContainer(dims2.width, dims2.height);
55990
57711
  renderMapForExport2(
55991
57712
  container2,
55992
57713
  mapResolved,
55993
57714
  mapData,
55994
57715
  effectivePalette2,
55995
57716
  theme === "dark",
55996
- { width: EXPORT_WIDTH, height: EXPORT_HEIGHT }
57717
+ dims2
55997
57718
  );
55998
57719
  return finalizeSvgExport(container2, theme, effectivePalette2);
55999
57720
  }
@@ -56800,7 +58521,8 @@ __export(advanced_exports, {
56800
58521
  applyCollapseProjection: () => applyCollapseProjection,
56801
58522
  applyGroupOrdering: () => applyGroupOrdering,
56802
58523
  applyPositionOverrides: () => applyPositionOverrides,
56803
- boldPalette: () => boldPalette,
58524
+ atlasPalette: () => atlasPalette,
58525
+ blueprintPalette: () => blueprintPalette,
56804
58526
  buildExtendedChartOption: () => buildExtendedChartOption,
56805
58527
  buildNoteMessageMap: () => buildNoteMessageMap,
56806
58528
  buildRenderSequence: () => buildRenderSequence,
@@ -56834,6 +58556,7 @@ __export(advanced_exports, {
56834
58556
  computeTimeTicks: () => computeTimeTicks,
56835
58557
  contrastText: () => contrastText,
56836
58558
  controlsGroupCapsuleWidth: () => controlsGroupCapsuleWidth,
58559
+ createMapGeoQuery: () => createMapGeoQuery,
56837
58560
  decodeDiagramUrl: () => decodeDiagramUrl,
56838
58561
  decodeViewState: () => decodeViewState,
56839
58562
  displayName: () => displayName,
@@ -56902,6 +58625,8 @@ __export(advanced_exports, {
56902
58625
  looksLikeState: () => looksLikeState,
56903
58626
  makeDgmoError: () => makeDgmoError,
56904
58627
  mapBackgroundColor: () => mapBackgroundColor,
58628
+ mapContentAspect: () => mapContentAspect,
58629
+ mapExportDimensions: () => mapExportDimensions,
56905
58630
  mapNeutralLandColor: () => mapNeutralLandColor,
56906
58631
  matchesContiguously: () => matchesContiguously,
56907
58632
  measurePertAnalysisBlock: () => measurePertAnalysisBlock,
@@ -57037,9 +58762,11 @@ __export(advanced_exports, {
57037
58762
  shapeFill: () => shapeFill,
57038
58763
  simulateCanonical: () => simulateCanonical,
57039
58764
  simulateFast: () => simulateFast,
58765
+ slatePalette: () => slatePalette,
57040
58766
  solarizedPalette: () => solarizedPalette,
57041
58767
  suggestChartTypes: () => suggestChartTypes,
57042
58768
  themes: () => themes,
58769
+ tidewaterPalette: () => tidewaterPalette,
57043
58770
  tint: () => tint,
57044
58771
  tokyoNightPalette: () => tokyoNightPalette,
57045
58772
  transformLine: () => transformLine,
@@ -57123,7 +58850,8 @@ async function render(content, options) {
57123
58850
  ...options?.c4Container !== void 0 && {
57124
58851
  c4Container: options.c4Container
57125
58852
  },
57126
- ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup }
58853
+ ...options?.tagGroup !== void 0 && { tagGroup: options.tagGroup },
58854
+ ...options?.mapData !== void 0 && { mapData: options.mapData }
57127
58855
  });
57128
58856
  if (chartType === "map") {
57129
58857
  try {
@@ -57134,7 +58862,7 @@ async function render(content, options) {
57134
58862
  Promise.resolve().then(() => (init_load_data(), load_data_exports))
57135
58863
  ]
57136
58864
  );
57137
- const data = await loadMapData2();
58865
+ const data = options?.mapData ?? await loadMapData2();
57138
58866
  diagnostics = [...resolveMap2(parseMap2(content), data).diagnostics];
57139
58867
  } catch {
57140
58868
  }
@@ -57373,8 +59101,8 @@ function detectCycles(parsed) {
57373
59101
  const parent = /* @__PURE__ */ new Map();
57374
59102
  function dfs(nodeId3) {
57375
59103
  color.set(nodeId3, 1);
57376
- const neighbors = adj.get(nodeId3) ?? [];
57377
- for (const next of neighbors) {
59104
+ const neighbors2 = adj.get(nodeId3) ?? [];
59105
+ for (const next of neighbors2) {
57378
59106
  const c = color.get(next) ?? 0;
57379
59107
  if (c === 1) {
57380
59108
  const lineKey = `${nodeId3}->${next}`;
@@ -57559,6 +59287,163 @@ init_resolver2();
57559
59287
  init_load_data();
57560
59288
  init_layout15();
57561
59289
  init_renderer16();
59290
+ init_dimensions();
59291
+
59292
+ // src/map/geo-query.ts
59293
+ init_parser12();
59294
+ init_resolver2();
59295
+ init_layout15();
59296
+ init_geo();
59297
+
59298
+ // src/map/invert.ts
59299
+ function inInsetFrame(inset, px, py) {
59300
+ return px >= inset.x && px <= inset.x + inset.w && py >= inset.y && py <= inset.y + inset.h;
59301
+ }
59302
+ function unstretch(layout, px, py) {
59303
+ const s = layout.stretch;
59304
+ return [
59305
+ s.bx0 + (s.sx !== 0 ? (px - s.ox) / s.sx : 0),
59306
+ s.by0 + (s.sy !== 0 ? (py - s.oy) / s.sy : 0)
59307
+ ];
59308
+ }
59309
+ function applyStretch(layout, x, y) {
59310
+ const s = layout.stretch;
59311
+ return [s.ox + (x - s.bx0) * s.sx, s.oy + (y - s.by0) * s.sy];
59312
+ }
59313
+ function pixelToLonLat(layout, px, py) {
59314
+ for (const inset of layout.insets) {
59315
+ if (inInsetFrame(inset, px, py)) {
59316
+ const ll2 = inset.projection.invert?.([px, py]);
59317
+ return ll2 && Number.isFinite(ll2[0]) && Number.isFinite(ll2[1]) ? [ll2[0], ll2[1]] : null;
59318
+ }
59319
+ }
59320
+ const [x, y] = layout.stretch ? unstretch(layout, px, py) : [px, py];
59321
+ const ll = layout.projection.invert?.([x, y]);
59322
+ return ll && Number.isFinite(ll[0]) && Number.isFinite(ll[1]) ? [ll[0], ll[1]] : null;
59323
+ }
59324
+ function lonLatToPixel(layout, lonLat) {
59325
+ const pt = [lonLat[0], lonLat[1]];
59326
+ const main = layout.projection(pt);
59327
+ const mainPx = main && Number.isFinite(main[0]) && Number.isFinite(main[1]) ? layout.stretch ? applyStretch(layout, main[0], main[1]) : [main[0], main[1]] : null;
59328
+ const onCanvas = !!mainPx && mainPx[0] >= 0 && mainPx[0] <= layout.width && mainPx[1] >= 0 && mainPx[1] <= layout.height;
59329
+ if (onCanvas) return mainPx;
59330
+ for (const inset of layout.insets) {
59331
+ const p = inset.projection(pt);
59332
+ if (p && Number.isFinite(p[0]) && Number.isFinite(p[1]) && inInsetFrame(inset, p[0], p[1]))
59333
+ return [p[0], p[1]];
59334
+ }
59335
+ return mainPx;
59336
+ }
59337
+
59338
+ // src/map/geo-query.ts
59339
+ var EARTH_R_KM = 6371;
59340
+ var DEG = Math.PI / 180;
59341
+ function haversineKm(lat1, lon1, lat2, lon2) {
59342
+ const dLat = (lat2 - lat1) * DEG;
59343
+ const dLon = (lon2 - lon1) * DEG;
59344
+ const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * DEG) * Math.cos(lat2 * DEG) * Math.sin(dLon / 2) ** 2;
59345
+ return 2 * EARTH_R_KM * Math.asin(Math.min(1, Math.sqrt(a)));
59346
+ }
59347
+ var POP_PULL_KM = 12;
59348
+ function nearestCity(lonLat, gazetteer) {
59349
+ const [lon, lat] = lonLat;
59350
+ let best = null;
59351
+ const cities = gazetteer.cities;
59352
+ for (let i = 0; i < cities.length; i++) {
59353
+ const c2 = cities[i];
59354
+ const dist = haversineKm(lat, lon, c2[0], c2[1]);
59355
+ const score = dist - POP_PULL_KM * Math.log10((c2[3] || 0) + 1);
59356
+ if (!best || score < best.score) best = { score, idx: i, dist };
59357
+ }
59358
+ if (!best) return null;
59359
+ const c = cities[best.idx];
59360
+ return {
59361
+ name: c[4],
59362
+ iso: c[2],
59363
+ ...c[5] !== void 0 && { sub: c[5] },
59364
+ distanceKm: best.dist,
59365
+ lat: c[0],
59366
+ lon: c[1]
59367
+ };
59368
+ }
59369
+ function roundCoord(n) {
59370
+ return Number(n.toFixed(2));
59371
+ }
59372
+ function buildTokens(lonLat, region, city) {
59373
+ const coordPoiLine = `poi ${roundCoord(lonLat[1])} ${roundCoord(lonLat[0])}`;
59374
+ let stateTok = null;
59375
+ if (region.state) {
59376
+ const { iso, name } = region.state;
59377
+ stateTok = { primary: `${name} ${iso}`, alternates: [iso, name] };
59378
+ }
59379
+ let countryTok = null;
59380
+ if (region.country) {
59381
+ const { iso, name } = region.country;
59382
+ countryTok = { primary: name, alternates: [iso] };
59383
+ }
59384
+ let cityTok = null;
59385
+ if (city) {
59386
+ const scope = city.sub ?? (city.iso || "");
59387
+ cityTok = scope ? { token: `poi ${city.name} ${scope}`, ambiguous: false } : { token: `poi ${city.name}`, ambiguous: true };
59388
+ }
59389
+ return { coordPoiLine, state: stateTok, country: countryTok, city: cityTok };
59390
+ }
59391
+ var MAX_CITY_DOTS = 250;
59392
+ function createMapGeoQuery(opts) {
59393
+ const { content, width, height, data, palette, isDark } = opts;
59394
+ const resolved = resolveMap(parseMap(content), data);
59395
+ const layout = layoutMap(
59396
+ resolved,
59397
+ data,
59398
+ { width, height },
59399
+ { palette, isDark }
59400
+ );
59401
+ const countries = decodeFeatures(data.worldDetail);
59402
+ const states = decodeFeatures(data.usStates);
59403
+ const gazetteer = data.gazetteer;
59404
+ const invert = (px, py) => pixelToLonLat(layout, px, py);
59405
+ const project = (lonLat) => lonLatToPixel(layout, lonLat);
59406
+ const locate = (px, py) => {
59407
+ const lonLat = invert(px, py);
59408
+ if (!lonLat) return null;
59409
+ const region = regionAt(lonLat, countries, states);
59410
+ const city = nearestCity(lonLat, gazetteer);
59411
+ return {
59412
+ lonLat,
59413
+ country: region.country,
59414
+ state: region.state,
59415
+ nearestCity: city,
59416
+ tokens: buildTokens(lonLat, region, city)
59417
+ };
59418
+ };
59419
+ const cities = (extent2) => {
59420
+ const sorted = [...gazetteer.cities].sort((a, b) => b[3] - a[3]);
59421
+ const out = [];
59422
+ for (const c of sorted) {
59423
+ const [lat, lon, iso, pop, name, sub] = c;
59424
+ if (extent2) {
59425
+ const [[w, s], [e, n]] = extent2;
59426
+ if (lon < w || lon > e || lat < s || lat > n) continue;
59427
+ }
59428
+ const p = project([lon, lat]);
59429
+ if (!p) continue;
59430
+ if (p[0] < 0 || p[0] > width || p[1] < 0 || p[1] > height) continue;
59431
+ out.push({
59432
+ name,
59433
+ iso,
59434
+ ...sub !== void 0 && { sub },
59435
+ lon,
59436
+ lat,
59437
+ px: p[0],
59438
+ py: p[1],
59439
+ pop
59440
+ });
59441
+ if (out.length >= MAX_CITY_DOTS) break;
59442
+ }
59443
+ return out;
59444
+ };
59445
+ return { invert, project, locate, cities, diagnostics: layout.diagnostics };
59446
+ }
57562
59447
 
57563
59448
  // src/map/completion.ts
57564
59449
  var fold2 = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
@@ -58283,9 +60168,12 @@ var GLOBAL_DIRECTIVES = {
58283
60168
  "gruvbox",
58284
60169
  "tokyo-night",
58285
60170
  "one-dark",
58286
- "bold",
58287
60171
  "dracula",
58288
- "monokai"
60172
+ "monokai",
60173
+ "atlas",
60174
+ "blueprint",
60175
+ "slate",
60176
+ "tidewater"
58289
60177
  ]
58290
60178
  },
58291
60179
  theme: {
@@ -58681,18 +60569,12 @@ var COMPLETION_REGISTRY = /* @__PURE__ */ new Map([
58681
60569
  ],
58682
60570
  [
58683
60571
  "map",
58684
- // Geographic map directives (§24B.2/.7). `poi`/`route` are content
58685
- // keywords, not directives; metadata keys (value/label/style) live in the
58686
- // reserved-key registry.
60572
+ // Geographic map directives (§24B.2/.7). Cosmetics are ON by default — the
60573
+ // only switches are bare `no-*` opt-outs, surfaced proactively so a
60574
+ // zero-config map still hints at what can be turned off. `poi`/`route` are
60575
+ // content keywords, not directives; metadata keys (value/label/style) live
60576
+ // in the reserved-key registry.
58687
60577
  withGlobals({
58688
- region: {
58689
- description: "Basemap: us-states (force US state mesh + scoping) | world (inert \u2014 already the default)",
58690
- values: ["us-states", "world"]
58691
- },
58692
- projection: {
58693
- description: "Override the auto projection",
58694
- values: ["equirectangular", "natural-earth", "albers-usa", "mercator"]
58695
- },
58696
60578
  "region-metric": { description: "Label for the region value ramp" },
58697
60579
  "poi-metric": {
58698
60580
  description: "Label for the POI value (marker size) channel"
@@ -58700,20 +60582,30 @@ var COMPLETION_REGISTRY = /* @__PURE__ */ new Map([
58700
60582
  "flow-metric": {
58701
60583
  description: "Label for the edge/leg value (thickness) channel"
58702
60584
  },
58703
- scale: { description: "Override value ramp anchors: scale <min> <max>" },
58704
- "region-labels": {
58705
- description: "Subdivision name labels",
58706
- values: ["full", "abbrev", "off"]
60585
+ locale: {
60586
+ description: "Default country/state for bare place names, e.g. locale US-GA"
58707
60587
  },
58708
- "poi-labels": {
58709
- description: "POI labels/values",
58710
- values: ["off", "auto", "all"]
60588
+ "active-tag": {
60589
+ description: "Which tag group leads when several are present"
58711
60590
  },
58712
- "default-country": { description: "ISO scope for bare city resolution" },
58713
- "default-state": { description: "ISO subdivision scope" },
60591
+ caption: { description: "Caption line (data-source attribution)" },
58714
60592
  "no-legend": { description: "Suppress the legend" },
58715
- subtitle: { description: "Subtitle line" },
58716
- caption: { description: "Caption line" }
60593
+ "no-coastline": {
60594
+ description: "Turn off coastal water-lines (on by default)"
60595
+ },
60596
+ "no-relief": {
60597
+ description: "Turn off mountain-range relief shading (on by default)"
60598
+ },
60599
+ "no-context-labels": {
60600
+ description: "Turn off orientation labels for water + nearby countries"
60601
+ },
60602
+ "no-region-labels": {
60603
+ description: "Turn off subdivision name labels (on by default)"
60604
+ },
60605
+ "no-poi-labels": { description: "Turn off POI labels (on by default)" },
60606
+ "no-colorize": {
60607
+ description: "Force plain green-land reference dress (regions are auto-coloured by default)"
60608
+ }
58717
60609
  })
58718
60610
  ]
58719
60611
  ]);
@@ -60183,7 +62075,8 @@ function formatLineDiff(path, original, migrated) {
60183
62075
  applyCollapseProjection,
60184
62076
  applyGroupOrdering,
60185
62077
  applyPositionOverrides,
60186
- boldPalette,
62078
+ atlasPalette,
62079
+ blueprintPalette,
60187
62080
  buildExtendedChartOption,
60188
62081
  buildNoteMessageMap,
60189
62082
  buildRenderSequence,
@@ -60217,6 +62110,7 @@ function formatLineDiff(path, original, migrated) {
60217
62110
  computeTimeTicks,
60218
62111
  contrastText,
60219
62112
  controlsGroupCapsuleWidth,
62113
+ createMapGeoQuery,
60220
62114
  decodeDiagramUrl,
60221
62115
  decodeViewState,
60222
62116
  displayName,
@@ -60285,6 +62179,8 @@ function formatLineDiff(path, original, migrated) {
60285
62179
  looksLikeState,
60286
62180
  makeDgmoError,
60287
62181
  mapBackgroundColor,
62182
+ mapContentAspect,
62183
+ mapExportDimensions,
60288
62184
  mapNeutralLandColor,
60289
62185
  matchesContiguously,
60290
62186
  measurePertAnalysisBlock,
@@ -60420,9 +62316,11 @@ function formatLineDiff(path, original, migrated) {
60420
62316
  shapeFill,
60421
62317
  simulateCanonical,
60422
62318
  simulateFast,
62319
+ slatePalette,
60423
62320
  solarizedPalette,
60424
62321
  suggestChartTypes,
60425
62322
  themes,
62323
+ tidewaterPalette,
60426
62324
  tint,
60427
62325
  tokyoNightPalette,
60428
62326
  transformLine,