@diagrammo/dgmo 0.19.0 → 0.20.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 (48) hide show
  1. package/dist/advanced.cjs +919 -298
  2. package/dist/advanced.d.cts +148 -54
  3. package/dist/advanced.d.ts +148 -54
  4. package/dist/advanced.js +922 -300
  5. package/dist/auto.cjs +904 -297
  6. package/dist/auto.js +117 -117
  7. package/dist/auto.mjs +909 -299
  8. package/dist/cli.cjs +159 -159
  9. package/dist/index.cjs +903 -296
  10. package/dist/index.js +908 -298
  11. package/dist/internal.cjs +919 -298
  12. package/dist/internal.d.cts +148 -54
  13. package/dist/internal.d.ts +148 -54
  14. package/dist/internal.js +922 -300
  15. package/dist/map-data/PROVENANCE.json +1 -1
  16. package/dist/map-data/lakes.json +1 -0
  17. package/dist/map-data/na-lakes.json +1 -0
  18. package/dist/map-data/na-land.json +1 -0
  19. package/dist/map-data/rivers.json +1 -0
  20. package/docs/language-reference.md +12 -7
  21. package/gallery/fixtures/map-region-scope.dgmo +15 -0
  22. package/package.json +4 -4
  23. package/src/advanced.ts +6 -2
  24. package/src/c4/parser.ts +6 -6
  25. package/src/completion.ts +6 -2
  26. package/src/echarts.ts +1 -1
  27. package/src/infra/parser.ts +10 -10
  28. package/src/journey-map/parser.ts +1 -1
  29. package/src/label-layout.ts +36 -0
  30. package/src/map/data/PROVENANCE.json +1 -1
  31. package/src/map/data/README.md +2 -0
  32. package/src/map/data/lakes.json +1 -0
  33. package/src/map/data/na-lakes.json +1 -0
  34. package/src/map/data/na-land.json +1 -0
  35. package/src/map/data/rivers.json +1 -0
  36. package/src/map/layout.ts +1022 -205
  37. package/src/map/load-data.ts +29 -2
  38. package/src/map/parser.ts +22 -13
  39. package/src/map/renderer.ts +200 -219
  40. package/src/map/resolved-types.ts +18 -1
  41. package/src/map/resolver.ts +79 -7
  42. package/src/map/types.ts +4 -0
  43. package/src/mindmap/parser.ts +1 -1
  44. package/src/sitemap/parser.ts +1 -1
  45. package/src/utils/legend-d3.ts +42 -0
  46. package/src/utils/legend-layout.ts +83 -3
  47. package/src/utils/legend-svg.ts +1 -8
  48. package/src/utils/legend-types.ts +44 -1
package/dist/index.js CHANGED
@@ -51,6 +51,33 @@ function rectCircleOverlap(rect, circle) {
51
51
  const dy = nearestY - circle.cy;
52
52
  return dx * dx + dy * dy < circle.r * circle.r;
53
53
  }
54
+ function segmentRectOverlap(x0, y0, x1, y1, rect) {
55
+ const dx = x1 - x0;
56
+ const dy = y1 - y0;
57
+ let t0 = 0;
58
+ let t1 = 1;
59
+ const edges = [
60
+ [-dx, x0 - rect.x],
61
+ [dx, rect.x + rect.w - x0],
62
+ [-dy, y0 - rect.y],
63
+ [dy, rect.y + rect.h - y0]
64
+ ];
65
+ for (const [p, q] of edges) {
66
+ if (p === 0) {
67
+ if (q < 0) return false;
68
+ } else {
69
+ const t = q / p;
70
+ if (p < 0) {
71
+ if (t > t1) return false;
72
+ if (t > t0) t0 = t;
73
+ } else {
74
+ if (t < t0) return false;
75
+ if (t < t1) t1 = t;
76
+ }
77
+ }
78
+ }
79
+ return true;
80
+ }
54
81
  function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius, fontSize) {
55
82
  const labelHeight = fontSize + 4;
56
83
  const stepSize = labelHeight + 2;
@@ -3254,6 +3281,57 @@ var init_legend_constants = __esm({
3254
3281
  });
3255
3282
 
3256
3283
  // src/utils/legend-layout.ts
3284
+ function fmtRamp(n) {
3285
+ return Number.isInteger(n) ? String(n) : String(Math.round(n * 10) / 10);
3286
+ }
3287
+ function gradientCapsuleWidth(name, gradient) {
3288
+ const pw = pillWidth(name);
3289
+ const minW = measureLegendText(fmtRamp(gradient.min), LEGEND_ENTRY_FONT_SIZE);
3290
+ const maxW = measureLegendText(fmtRamp(gradient.max), LEGEND_ENTRY_FONT_SIZE);
3291
+ return LEGEND_CAPSULE_PAD + pw + 4 + minW + RAMP_LABEL_GAP + RAMP_LEGEND_W + RAMP_LABEL_GAP + maxW + LEGEND_CAPSULE_PAD;
3292
+ }
3293
+ function buildGradientCapsuleLayout(group, gradient) {
3294
+ const pw = pillWidth(group.name);
3295
+ const minText = fmtRamp(gradient.min);
3296
+ const maxText = fmtRamp(gradient.max);
3297
+ const minW = measureLegendText(minText, LEGEND_ENTRY_FONT_SIZE);
3298
+ const gx = LEGEND_CAPSULE_PAD + pw + 4;
3299
+ const minX = gx;
3300
+ const rampX = gx + minW + RAMP_LABEL_GAP;
3301
+ const maxX = rampX + RAMP_LEGEND_W + RAMP_LABEL_GAP;
3302
+ const width = gradientCapsuleWidth(group.name, gradient);
3303
+ return {
3304
+ groupName: group.name,
3305
+ x: 0,
3306
+ y: 0,
3307
+ width,
3308
+ height: LEGEND_HEIGHT,
3309
+ pill: {
3310
+ groupName: group.name,
3311
+ x: LEGEND_CAPSULE_PAD,
3312
+ y: LEGEND_CAPSULE_PAD,
3313
+ width: pw,
3314
+ height: LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2,
3315
+ isActive: true
3316
+ },
3317
+ entries: [],
3318
+ gradient: {
3319
+ rampX,
3320
+ rampY: (LEGEND_HEIGHT - RAMP_LEGEND_H) / 2,
3321
+ rampW: RAMP_LEGEND_W,
3322
+ rampH: RAMP_LEGEND_H,
3323
+ min: gradient.min,
3324
+ max: gradient.max,
3325
+ minText,
3326
+ minX,
3327
+ maxText,
3328
+ maxX,
3329
+ textY: LEGEND_HEIGHT / 2,
3330
+ hue: gradient.hue,
3331
+ base: gradient.base
3332
+ }
3333
+ };
3334
+ }
3257
3335
  function pillWidth(name) {
3258
3336
  return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
3259
3337
  }
@@ -3396,7 +3474,7 @@ function computeLegendLayout(config, state, containerWidth) {
3396
3474
  };
3397
3475
  }
3398
3476
  const controlsGroupLayout = isExport ? void 0 : buildControlsGroupLayout(config, state);
3399
- const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0);
3477
+ const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0 || !!g.gradient);
3400
3478
  if (visibleGroups.length === 0 && (!configControls || configControls.length === 0) && !controlsGroupLayout) {
3401
3479
  return {
3402
3480
  height: 0,
@@ -3475,7 +3553,7 @@ function computeLegendLayout(config, state, containerWidth) {
3475
3553
  groupAvailW,
3476
3554
  config.capsulePillAddonWidth ?? 0
3477
3555
  );
3478
- } else if (!activeGroupName) {
3556
+ } else if (!activeGroupName || config.showInactivePills) {
3479
3557
  const pw = pillWidth(g.name);
3480
3558
  pills.push({
3481
3559
  groupName: g.name,
@@ -3513,6 +3591,7 @@ function computeLegendLayout(config, state, containerWidth) {
3513
3591
  };
3514
3592
  }
3515
3593
  function buildCapsuleLayout(group, containerWidth, addonWidth = 0) {
3594
+ if (group.gradient) return buildGradientCapsuleLayout(group, group.gradient);
3516
3595
  const pw = pillWidth(group.name);
3517
3596
  const info = capsuleWidth(
3518
3597
  group.name,
@@ -3683,7 +3762,7 @@ function getMaxLegendReservedHeight(config, containerWidth) {
3683
3762
  }
3684
3763
  return max;
3685
3764
  }
3686
- var CONTROL_PILL_PAD, CONTROL_FONT_SIZE, CONTROL_ICON_GAP, CONTROL_GAP;
3765
+ var CONTROL_PILL_PAD, CONTROL_FONT_SIZE, CONTROL_ICON_GAP, CONTROL_GAP, RAMP_LEGEND_W, RAMP_LEGEND_H, RAMP_LABEL_GAP;
3687
3766
  var init_legend_layout = __esm({
3688
3767
  "src/utils/legend-layout.ts"() {
3689
3768
  "use strict";
@@ -3692,6 +3771,9 @@ var init_legend_layout = __esm({
3692
3771
  CONTROL_FONT_SIZE = 11;
3693
3772
  CONTROL_ICON_GAP = 4;
3694
3773
  CONTROL_GAP = 8;
3774
+ RAMP_LEGEND_W = 80;
3775
+ RAMP_LEGEND_H = 8;
3776
+ RAMP_LABEL_GAP = 6;
3695
3777
  }
3696
3778
  });
3697
3779
 
@@ -3779,6 +3861,16 @@ function renderCapsule(parent, capsule, palette, groupBg, pillBorder, _isDark, c
3779
3861
  g.append("rect").attr("x", pill.x).attr("y", pill.y).attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", palette.bg);
3780
3862
  g.append("rect").attr("x", pill.x).attr("y", pill.y).attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", "none").attr("stroke", pillBorder).attr("stroke-width", 0.75);
3781
3863
  g.append("text").attr("x", pill.x + pill.width / 2).attr("y", LEGEND_HEIGHT / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.text).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(capsule.groupName);
3864
+ if (capsule.gradient) {
3865
+ const gr = capsule.gradient;
3866
+ const gradId = `dgmo-legend-ramp-${capsule.groupName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
3867
+ const def = g.append("defs").append("linearGradient").attr("id", gradId);
3868
+ def.append("stop").attr("offset", "0%").attr("stop-color", mix(gr.hue, gr.base, 15));
3869
+ def.append("stop").attr("offset", "100%").attr("stop-color", gr.hue);
3870
+ g.append("text").attr("x", gr.minX).attr("y", gr.textY).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(gr.minText);
3871
+ g.append("rect").attr("class", "dgmo-legend-gradient-ramp").attr("data-ramp-min", gr.min).attr("data-ramp-max", gr.max).attr("x", gr.rampX).attr("y", gr.rampY).attr("width", gr.rampW).attr("height", gr.rampH).attr("rx", 2).attr("fill", `url(#${gradId})`);
3872
+ g.append("text").attr("x", gr.maxX).attr("y", gr.textY).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(gr.maxText);
3873
+ }
3782
3874
  for (const entry of capsule.entries) {
3783
3875
  const entryG = g.append("g").attr("data-legend-entry", entry.value.toLowerCase()).attr("data-series-name", entry.value).style("cursor", "pointer");
3784
3876
  entryG.append("circle").attr("cx", entry.dotCx).attr("cy", entry.dotCy).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
@@ -11270,23 +11362,22 @@ function parseC4(content, palette) {
11270
11362
  }
11271
11363
  }
11272
11364
  const parent = findParentElement(indent, stack);
11273
- if (parent) {
11274
- const descResult = tryStripDescriptionKeyword(trimmed);
11365
+ const descResult = tryStripDescriptionKeyword(trimmed);
11366
+ if (parent && descResult.isKeyword) {
11275
11367
  if (descResult.needsColon) {
11276
11368
  pushError(
11277
11369
  lineNumber,
11278
- `Use "description: ${descResult.text}" \u2014 colon is required.`,
11370
+ `Use "description: ${descResult.text}" \u2014 bare "description" is deprecated.`,
11279
11371
  "warning"
11280
11372
  );
11281
11373
  }
11282
- const descText = descResult.isKeyword ? descResult.text : trimmed;
11283
11374
  let desc = elementDescription.get(parent.element);
11284
11375
  if (!desc) {
11285
11376
  desc = [];
11286
11377
  elementDescription.set(parent.element, desc);
11287
11378
  parent.element.description = desc;
11288
11379
  }
11289
- desc.push(descText);
11380
+ desc.push(descResult.text);
11290
11381
  } else {
11291
11382
  pushError(lineNumber, `Unexpected content: "${trimmed}"`);
11292
11383
  }
@@ -11753,7 +11844,7 @@ function parseSitemap(content, palette) {
11753
11844
  if (descResult.needsColon) {
11754
11845
  pushWarning(
11755
11846
  lineNumber,
11756
- `Use "description: ${descResult.text}" \u2014 colon is required.`
11847
+ `Use "description: ${descResult.text}" \u2014 bare "description" is deprecated.`
11757
11848
  );
11758
11849
  }
11759
11850
  const parent = findParentNode(indent, indentStack);
@@ -12581,23 +12672,22 @@ function parseInfra(content) {
12581
12672
  }
12582
12673
  }
12583
12674
  const descResult = tryStripDescriptionKeyword(trimmed);
12584
- if (descResult.isKeyword && currentNode.isEdge) {
12585
- continue;
12586
- }
12587
- if (!currentNode.isEdge) {
12675
+ if (descResult.isKeyword) {
12676
+ if (currentNode.isEdge) {
12677
+ continue;
12678
+ }
12588
12679
  if (descResult.needsColon) {
12589
12680
  warn(
12590
12681
  lineNumber,
12591
- `Use "description: ${descResult.text}" \u2014 colon is required.`
12682
+ `Use "description: ${descResult.text}" \u2014 bare "description" is deprecated.`
12592
12683
  );
12593
12684
  }
12594
- const descText = descResult.isKeyword ? descResult.text : trimmed;
12595
- pushDescription(currentNode, descText);
12685
+ pushDescription(currentNode, descResult.text);
12596
12686
  continue;
12597
12687
  }
12598
12688
  warn(
12599
12689
  lineNumber,
12600
- `Unexpected line inside component '${currentNode.label}'. Expected a property (key: value), connection (-> Target), or description text.`
12690
+ `Unexpected line inside component '${currentNode.label}'. Expected a property (key: value), connection (-> Target), or a description (description: text).`
12601
12691
  );
12602
12692
  continue;
12603
12693
  }
@@ -15734,9 +15824,6 @@ function parseMap(content) {
15734
15824
  const pushWarning = (line12, message) => {
15735
15825
  diagnostics.push(makeDgmoError(line12, message, "warning"));
15736
15826
  };
15737
- const pushInfo = (line12, message) => {
15738
- diagnostics.push(makeDgmoError(line12, message, "warning"));
15739
- };
15740
15827
  const lines = content.split("\n");
15741
15828
  let firstIdx = 0;
15742
15829
  while (firstIdx < lines.length) {
@@ -15866,10 +15953,15 @@ function parseMap(content) {
15866
15953
  break;
15867
15954
  case "projection":
15868
15955
  dup(d.projection);
15869
- if (value && !["natural-earth", "albers-usa", "mercator"].includes(value))
15956
+ if (value && ![
15957
+ "equirectangular",
15958
+ "natural-earth",
15959
+ "albers-usa",
15960
+ "mercator"
15961
+ ].includes(value))
15870
15962
  pushWarning(
15871
15963
  line12,
15872
- `Unknown projection "${value}" (expected natural-earth | albers-usa | mercator).`
15964
+ `Unknown projection "${value}" (expected equirectangular | natural-earth | albers-usa | mercator).`
15873
15965
  );
15874
15966
  d.projection = value;
15875
15967
  break;
@@ -16008,17 +16100,21 @@ function parseMap(content) {
16008
16100
  scoreNum = void 0;
16009
16101
  }
16010
16102
  }
16011
- if (scoreNum !== void 0 && Object.keys(tags).length)
16012
- pushInfo(
16013
- line12,
16014
- "A region has both `score:` and a tag value \u2014 v1 renders only the score (bivariate is a future seam)."
16015
- );
16103
+ let regionName = split.name;
16104
+ let regionScope;
16105
+ const rToks = regionName.split(/\s+/);
16106
+ const rLast = rToks[rToks.length - 1];
16107
+ if (rToks.length > 1 && SCOPE_RE.test(rLast)) {
16108
+ regionName = rToks.slice(0, -1).join(" ");
16109
+ regionScope = rLast;
16110
+ }
16016
16111
  const region = {
16017
- name: split.name,
16112
+ name: regionName,
16018
16113
  tags,
16019
16114
  meta,
16020
16115
  lineNumber: line12
16021
16116
  };
16117
+ if (regionScope !== void 0) region.scope = regionScope;
16022
16118
  if (scoreNum !== void 0) region.score = scoreNum;
16023
16119
  regions.push(region);
16024
16120
  }
@@ -17142,7 +17238,7 @@ function parseMindmap(content, palette) {
17142
17238
  if (descResult.needsColon) {
17143
17239
  pushWarning(
17144
17240
  lineNumber,
17145
- `Use "description: ${descResult.text}" \u2014 colon is required.`
17241
+ `Use "description: ${descResult.text}" \u2014 bare "description" is deprecated.`
17146
17242
  );
17147
17243
  }
17148
17244
  const parent = findMetadataParent2(indent, indentStack);
@@ -18930,7 +19026,7 @@ function parseJourneyMap(content, palette) {
18930
19026
  if (descResult.needsColon) {
18931
19027
  warn(
18932
19028
  lineNumber,
18933
- `Use "description: ${descResult.text}" \u2014 colon is required.`
19029
+ `Use "description: ${descResult.text}" \u2014 bare "description" is deprecated.`
18934
19030
  );
18935
19031
  }
18936
19032
  currentStep.description = descResult.text;
@@ -45519,7 +45615,9 @@ function resolveMap(parsed, data) {
45519
45615
  const usScoped = parsed.directives.region === "us-states" || parsed.directives.defaultCountry?.toUpperCase() === "US" || parsed.regions.some((r) => {
45520
45616
  const f = fold(r.name);
45521
45617
  return usStateIndex.has(f) && !countryIndex.has(f);
45522
- }) || parsed.pois.some(
45618
+ }) || parsed.regions.some(
45619
+ (r) => r.scope === "US" || r.scope?.startsWith("US-")
45620
+ ) || parsed.pois.some(
45523
45621
  (p) => p.pos.kind === "name" && p.pos.scope?.startsWith("US-")
45524
45622
  );
45525
45623
  const regions = [];
@@ -45531,7 +45629,30 @@ function resolveMap(parsed, data) {
45531
45629
  const inCountry = countryIndex.get(f) ?? (REGION_ALIASES[f] ? countryIndex.get(REGION_ALIASES[f]) : void 0);
45532
45630
  const inState = usStateIndex.get(f);
45533
45631
  let chosen = null;
45534
- if (inCountry && inState) {
45632
+ const scope = r.scope;
45633
+ if (scope) {
45634
+ const wantsState = scope === "US" || scope.startsWith("US-");
45635
+ if (wantsState && inState) {
45636
+ if (scope.startsWith("US-") && inState.id !== scope) {
45637
+ err(
45638
+ r.lineNumber,
45639
+ `No subdivision "${r.name}" in scope ${scope} (it is ${inState.id}).`,
45640
+ "E_MAP_SCOPE_MISS"
45641
+ );
45642
+ continue;
45643
+ }
45644
+ chosen = { ...inState, layer: "us-state" };
45645
+ } else if (!wantsState && inCountry) {
45646
+ chosen = { ...inCountry, layer: "country" };
45647
+ } else {
45648
+ err(
45649
+ r.lineNumber,
45650
+ `No region "${r.name}" found in scope ${scope}.`,
45651
+ "E_MAP_SCOPE_MISS"
45652
+ );
45653
+ continue;
45654
+ }
45655
+ } else if (inCountry && inState) {
45535
45656
  if (usScoped) {
45536
45657
  chosen = { ...inState, layer: "us-state" };
45537
45658
  } else {
@@ -45539,7 +45660,7 @@ function resolveMap(parsed, data) {
45539
45660
  }
45540
45661
  warn(
45541
45662
  r.lineNumber,
45542
- `"${r.name}" is both a country and a US state \u2014 resolved as ${chosen.layer} (${chosen.id}).`,
45663
+ `"${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}").`,
45543
45664
  "W_MAP_REGION_AMBIGUOUS"
45544
45665
  );
45545
45666
  } else if (inState) {
@@ -45711,9 +45832,17 @@ function resolveMap(parsed, data) {
45711
45832
  };
45712
45833
  registerPoi(id, poi, p.lineNumber);
45713
45834
  }
45835
+ const declaredByName = /* @__PURE__ */ new Map();
45836
+ for (const p of pois) {
45837
+ const fn = p.name ? fold(p.name) : void 0;
45838
+ if (fn && fn !== p.id && !declaredByName.has(fn))
45839
+ declaredByName.set(fn, p.id);
45840
+ }
45714
45841
  const resolveEndpoint2 = (ref, line12) => {
45715
45842
  const f = fold(ref);
45716
45843
  if (registry.has(f)) return f;
45844
+ const aliased = declaredByName.get(f);
45845
+ if (aliased) return aliased;
45717
45846
  const got = lookupName(ref, void 0, line12, inferredCountry, true);
45718
45847
  if (got.kind !== "ok") return null;
45719
45848
  noteCountry(got.iso);
@@ -45793,23 +45922,29 @@ function resolveMap(parsed, data) {
45793
45922
  [-180, -85],
45794
45923
  [180, 85]
45795
45924
  ];
45796
- const extent2 = unioned ? pad(unioned, PAD_FRACTION) : DEFAULT_EXTENT;
45925
+ let extent2 = unioned ? pad(unioned, PAD_FRACTION) : DEFAULT_EXTENT;
45797
45926
  const lonSpan = extent2[1][0] - extent2[0][0];
45798
45927
  const latSpan = extent2[1][1] - extent2[0][1];
45799
45928
  const span = Math.max(lonSpan, latSpan);
45800
45929
  const usDominant = (inferredCountry === "US" || subdivisions.includes("us-states")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
45801
45930
  let projection;
45802
45931
  const override = parsed.directives.projection;
45803
- if (override === "natural-earth" || override === "albers-usa" || override === "mercator") {
45932
+ if (override === "equirectangular" || override === "natural-earth" || override === "albers-usa" || override === "mercator") {
45804
45933
  projection = override;
45805
45934
  } else if (usDominant) {
45806
45935
  projection = "albers-usa";
45807
45936
  } else if (span > WORLD_SPAN) {
45808
- projection = "natural-earth";
45937
+ projection = "equirectangular";
45809
45938
  } else if (span < MERCATOR_MAX_SPAN) {
45810
45939
  projection = "mercator";
45811
45940
  } else {
45812
- projection = "natural-earth";
45941
+ projection = "equirectangular";
45942
+ }
45943
+ if (lonSpan >= 180) {
45944
+ extent2 = [
45945
+ [-180, Math.min(extent2[0][1], WORLD_LAT_SOUTH)],
45946
+ [180, Math.max(extent2[1][1], WORLD_LAT_NORTH)]
45947
+ ];
45813
45948
  }
45814
45949
  result.regions = regions;
45815
45950
  result.pois = pois;
@@ -45857,7 +45992,7 @@ function firstError(diags) {
45857
45992
  const e = diags.find((d) => d.severity === "error");
45858
45993
  return e ? formatDgmoError(e) : null;
45859
45994
  }
45860
- var WORLD_SPAN, MERCATOR_MAX_SPAN, PAD_FRACTION, REGION_ALIASES;
45995
+ var WORLD_SPAN, MERCATOR_MAX_SPAN, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES;
45861
45996
  var init_resolver2 = __esm({
45862
45997
  "src/map/resolver.ts"() {
45863
45998
  "use strict";
@@ -45866,6 +46001,8 @@ var init_resolver2 = __esm({
45866
46001
  WORLD_SPAN = 90;
45867
46002
  MERCATOR_MAX_SPAN = 25;
45868
46003
  PAD_FRACTION = 0.05;
46004
+ WORLD_LAT_SOUTH = -58;
46005
+ WORLD_LAT_NORTH = 78;
45869
46006
  REGION_ALIASES = {
45870
46007
  // Common everyday names → the Natural-Earth display name actually shipped.
45871
46008
  "united states": "united states of america",
@@ -45933,13 +46070,36 @@ function moduleBaseDir() {
45933
46070
  function loadMapData() {
45934
46071
  cache ??= (async () => {
45935
46072
  const dir = await firstExistingDir(moduleBaseDir());
45936
- const [worldCoarse, worldDetail, usStates, gazetteer] = await Promise.all([
46073
+ const [
46074
+ worldCoarse,
46075
+ worldDetail,
46076
+ usStates,
46077
+ lakes,
46078
+ rivers,
46079
+ naLand,
46080
+ naLakes,
46081
+ gazetteer
46082
+ ] = await Promise.all([
45937
46083
  readJson(dir, FILES.worldCoarse),
45938
46084
  readJson(dir, FILES.worldDetail),
45939
46085
  readJson(dir, FILES.usStates),
46086
+ // Lakes/rivers/NA assets are optional — older bundles may predate them.
46087
+ readJson(dir, FILES.lakes).catch(() => void 0),
46088
+ readJson(dir, FILES.rivers).catch(() => void 0),
46089
+ readJson(dir, FILES.naLand).catch(() => void 0),
46090
+ readJson(dir, FILES.naLakes).catch(() => void 0),
45940
46091
  readJson(dir, FILES.gazetteer)
45941
46092
  ]);
45942
- return validate({ worldCoarse, worldDetail, usStates, gazetteer });
46093
+ return validate({
46094
+ worldCoarse,
46095
+ worldDetail,
46096
+ usStates,
46097
+ gazetteer,
46098
+ ...lakes && { lakes },
46099
+ ...rivers && { rivers },
46100
+ ...naLand && { naLand },
46101
+ ...naLakes && { naLakes }
46102
+ });
45943
46103
  })().catch((e) => {
45944
46104
  cache = void 0;
45945
46105
  throw e;
@@ -45954,6 +46114,10 @@ var init_load_data = __esm({
45954
46114
  worldCoarse: "world-coarse.json",
45955
46115
  worldDetail: "world-detail.json",
45956
46116
  usStates: "us-states.json",
46117
+ lakes: "lakes.json",
46118
+ rivers: "rivers.json",
46119
+ naLand: "na-land.json",
46120
+ naLakes: "na-lakes.json",
45957
46121
  gazetteer: "gazetteer.json"
45958
46122
  };
45959
46123
  CANDIDATE_DIRS = [
@@ -45969,8 +46133,11 @@ var init_load_data = __esm({
45969
46133
  import {
45970
46134
  geoPath,
45971
46135
  geoNaturalEarth1,
45972
- geoAlbersUsa,
45973
- geoMercator
46136
+ geoEquirectangular,
46137
+ geoConicEqualArea,
46138
+ geoMercator,
46139
+ geoBounds as geoBounds2,
46140
+ geoTransform
45974
46141
  } from "d3-geo";
45975
46142
  import { feature as feature2 } from "topojson-client";
45976
46143
  function geomObject2(topo) {
@@ -45988,36 +46155,67 @@ function decodeLayer(topo) {
45988
46155
  function projectionFor(family) {
45989
46156
  switch (family) {
45990
46157
  case "albers-usa":
45991
- return geoAlbersUsa();
46158
+ return usConusProjection();
45992
46159
  case "mercator":
45993
46160
  return geoMercator();
45994
46161
  case "natural-earth":
45995
- default:
45996
46162
  return geoNaturalEarth1();
46163
+ case "equirectangular":
46164
+ default:
46165
+ return geoEquirectangular();
45997
46166
  }
45998
46167
  }
46168
+ function mapBackgroundColor(palette) {
46169
+ return mix(palette.colors.blue, palette.bg, WATER_TINT);
46170
+ }
45999
46171
  function layoutMap(resolved, data, size, opts) {
46000
46172
  const { palette, isDark } = opts;
46001
46173
  const { width, height } = size;
46002
- const worldTopo = resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46174
+ const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
46175
+ const usCrisp = resolved.projection === "albers-usa" && wantsUsStates && !!data.naLand;
46176
+ const worldTopo = usCrisp ? data.naLand : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46003
46177
  const worldLayer = decodeLayer(worldTopo);
46004
- const usLayer = resolved.basemaps.subdivisions.includes("us-states") ? decodeLayer(data.usStates) : null;
46005
- const neutralFill = mix(palette.border, palette.bg, 32);
46006
- const regionStroke = mix(palette.border, palette.bg, 70);
46178
+ const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
46179
+ const landTint = isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT;
46180
+ const neutralFill = mix(palette.colors.green, palette.bg, landTint);
46181
+ const water = mapBackgroundColor(palette);
46182
+ const usContext = usLayer !== null;
46183
+ const foreignFill = mix(
46184
+ palette.colors.gray,
46185
+ palette.bg,
46186
+ isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46187
+ );
46188
+ const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46007
46189
  const scores = resolved.regions.filter((r) => r.score !== void 0).map((r) => r.score);
46008
46190
  const scaleOverride = resolved.directives.scale;
46009
46191
  const rampMin = scaleOverride ? scaleOverride.min : Math.min(...scores);
46010
46192
  const rampMax = scaleOverride ? scaleOverride.max : Math.max(...scores);
46011
- const rampHue = palette.primary;
46193
+ const rampHue = palette.colors.red;
46012
46194
  const hasRamp = scores.length > 0;
46013
- const activeGroup = resolveActiveTagGroup(
46014
- resolved.tagGroups,
46015
- resolved.directives.activeTag
46016
- );
46195
+ const SCORE_NAME = hasRamp ? resolved.directives.metric?.trim() || "Score" : null;
46196
+ const matchColorGroup = (v) => {
46197
+ const lv = v.trim().toLowerCase();
46198
+ if (lv === "none") return null;
46199
+ if (SCORE_NAME && (lv === "score" || lv === SCORE_NAME.toLowerCase()))
46200
+ return SCORE_NAME;
46201
+ const tg = resolved.tagGroups.find((g) => g.name.toLowerCase() === lv);
46202
+ return tg ? tg.name : v;
46203
+ };
46204
+ const override = opts.activeGroup;
46205
+ let activeGroup;
46206
+ if (override !== void 0) {
46207
+ activeGroup = override === null ? null : matchColorGroup(override);
46208
+ } else if (resolved.directives.activeTag !== void 0) {
46209
+ activeGroup = matchColorGroup(resolved.directives.activeTag);
46210
+ } else {
46211
+ activeGroup = SCORE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46212
+ }
46213
+ const activeIsScore = SCORE_NAME !== null && activeGroup === SCORE_NAME;
46214
+ const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46017
46215
  const fillForScore = (s) => {
46018
46216
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
46019
46217
  const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
46020
- return mix(rampHue, isDark ? palette.surface : palette.bg, pct);
46218
+ return mix(rampHue, rampBase, pct);
46021
46219
  };
46022
46220
  const tagFill = (tags, groupName) => {
46023
46221
  if (!groupName) return null;
@@ -46031,40 +46229,40 @@ function layoutMap(resolved, data, size, opts) {
46031
46229
  (e) => e.value.toLowerCase() === val.toLowerCase()
46032
46230
  );
46033
46231
  if (!entry?.color) return null;
46034
- return shapeFill(palette, entry.color, isDark);
46232
+ return mix(
46233
+ entry.color,
46234
+ palette.bg,
46235
+ isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
46236
+ );
46237
+ };
46238
+ const regionFill = (r) => {
46239
+ if (activeIsScore) {
46240
+ return r.score !== void 0 ? fillForScore(r.score) : neutralFill;
46241
+ }
46242
+ return tagFill(r.tags, activeGroup) ?? neutralFill;
46035
46243
  };
46036
46244
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
46037
- const regionFeatures = [];
46038
- for (const r of resolved.regions) {
46039
- const f = r.layer === "us-state" ? usLayer?.get(r.iso) : worldLayer.get(r.iso);
46040
- if (f) regionFeatures.push(f);
46041
- }
46042
- const extentCorners = () => {
46245
+ const extentOutline = () => {
46043
46246
  const [[w, s], [e, n]] = resolved.extent;
46247
+ const N = 16;
46248
+ const coords = [];
46249
+ for (let i = 0; i <= N; i++) {
46250
+ const t = i / N;
46251
+ const lon = w + (e - w) * t;
46252
+ const lat = s + (n - s) * t;
46253
+ coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
46254
+ }
46044
46255
  return {
46045
46256
  type: "Feature",
46046
46257
  properties: {},
46047
- geometry: {
46048
- type: "MultiPoint",
46049
- coordinates: [
46050
- [w, s],
46051
- [e, s],
46052
- [e, n],
46053
- [w, n]
46054
- ]
46055
- }
46258
+ geometry: { type: "MultiPoint", coordinates: coords }
46056
46259
  };
46057
46260
  };
46058
46261
  let fitFeatures;
46059
- if (resolved.projection === "albers-usa") {
46060
- if (regionFeatures.length > 0) fitFeatures = regionFeatures;
46061
- else if (usLayer) fitFeatures = [...usLayer.values()];
46062
- else {
46063
- const us = worldLayer.get("US");
46064
- fitFeatures = us ? [us] : [...worldLayer.values()];
46065
- }
46262
+ if (resolved.projection === "albers-usa" && usLayer) {
46263
+ fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
46066
46264
  } else {
46067
- fitFeatures = [extentCorners()];
46265
+ fitFeatures = [extentOutline()];
46068
46266
  }
46069
46267
  const fitTarget = { type: "FeatureCollection", features: fitFeatures };
46070
46268
  const projection = projectionFor(resolved.projection);
@@ -46073,32 +46271,311 @@ function layoutMap(resolved, data, size, opts) {
46073
46271
  if (centerLon > 180) centerLon -= 360;
46074
46272
  projection.rotate([-centerLon, 0]);
46075
46273
  }
46076
- projection.fitExtent(
46274
+ const TITLE_GAP = 16;
46275
+ let topPad = FIT_PAD;
46276
+ if (resolved.title && resolved.pois.length > 0) {
46277
+ const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
46278
+ topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
46279
+ }
46280
+ const fitBox = [
46281
+ [FIT_PAD, topPad],
46077
46282
  [
46078
- [FIT_PAD, FIT_PAD],
46079
- [
46080
- Math.max(FIT_PAD + 1, width - FIT_PAD),
46081
- Math.max(FIT_PAD + 1, height - FIT_PAD)
46082
- ]
46083
- ],
46084
- fitTarget
46085
- );
46086
- const path = geoPath(projection);
46087
- const project = (lon, lat) => projection([lon, lat]) ?? null;
46283
+ Math.max(FIT_PAD + 1, width - FIT_PAD),
46284
+ Math.max(topPad + 1, height - FIT_PAD)
46285
+ ]
46286
+ ];
46287
+ projection.fitExtent(fitBox, fitTarget);
46288
+ const fitGB = geoBounds2(fitTarget);
46289
+ const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46290
+ let path;
46291
+ let project;
46292
+ if (fitIsGlobal) {
46293
+ const cb = geoPath(projection).bounds(fitTarget);
46294
+ const bx0 = cb[0][0];
46295
+ const by0 = cb[0][1];
46296
+ const cw = cb[1][0] - bx0;
46297
+ const ch = cb[1][1] - by0;
46298
+ const ox = fitBox[0][0];
46299
+ const oy = fitBox[0][1];
46300
+ const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46301
+ const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
46302
+ const stretch = (x, y) => [
46303
+ ox + (x - bx0) * sx,
46304
+ oy + (y - by0) * sy
46305
+ ];
46306
+ const baseProjection = projection;
46307
+ const tx = geoTransform({
46308
+ point(x, y) {
46309
+ const [px, py] = stretch(x, y);
46310
+ this.stream.point(px, py);
46311
+ }
46312
+ });
46313
+ path = geoPath({
46314
+ stream: (s) => baseProjection.stream(
46315
+ tx.stream(s)
46316
+ )
46317
+ });
46318
+ project = (lon, lat) => {
46319
+ const p = baseProjection([lon, lat]);
46320
+ return p ? stretch(p[0], p[1]) : null;
46321
+ };
46322
+ } else {
46323
+ path = geoPath(projection);
46324
+ project = (lon, lat) => projection([lon, lat]) ?? null;
46325
+ }
46326
+ const insets = [];
46327
+ const insetRegions = [];
46328
+ const insetLabelSeeds = [];
46329
+ if (resolved.projection === "albers-usa" && usLayer) {
46330
+ const PAD = 8;
46331
+ const GAP = 12;
46332
+ const yB = height - FIT_PAD;
46333
+ const BW = 8;
46334
+ const coast = /* @__PURE__ */ new Map();
46335
+ const addPt = (lon, lat) => {
46336
+ const p = projection([lon, lat]);
46337
+ if (!p) return;
46338
+ const bi = Math.floor(p[0] / BW);
46339
+ const cur = coast.get(bi);
46340
+ if (cur === void 0 || p[1] > cur) coast.set(bi, p[1]);
46341
+ };
46342
+ const walk = (co) => {
46343
+ if (Array.isArray(co) && typeof co[0] === "number")
46344
+ addPt(co[0], co[1]);
46345
+ else if (Array.isArray(co)) for (const c of co) walk(c);
46346
+ };
46347
+ for (const [iso, f] of usLayer) {
46348
+ if (US_NON_CONUS.has(iso)) continue;
46349
+ walk(f.geometry.coordinates);
46350
+ }
46351
+ const at = (x) => {
46352
+ const bi = Math.floor(x / BW);
46353
+ let y = -Infinity;
46354
+ for (let k = bi - 1; k <= bi + 1; k++) {
46355
+ const v = coast.get(k);
46356
+ if (v !== void 0 && v > y) y = v;
46357
+ }
46358
+ return y;
46359
+ };
46360
+ const coastTop = (x0, xr) => {
46361
+ const n = 24;
46362
+ const pts = [];
46363
+ let maxY = -Infinity;
46364
+ for (let i = 0; i <= n; i++) {
46365
+ const x = x0 + (xr - x0) * i / n;
46366
+ const y = at(x);
46367
+ if (y > -Infinity) {
46368
+ pts.push([x, y]);
46369
+ if (y > maxY) maxY = y;
46370
+ }
46371
+ }
46372
+ if (pts.length === 0) return () => yB - height * 0.42;
46373
+ let m = 0;
46374
+ if (pts.length >= 2) {
46375
+ let sx = 0, sy = 0, sxx = 0, sxy = 0;
46376
+ for (const [x, y] of pts) {
46377
+ sx += x;
46378
+ sy += y;
46379
+ sxx += x * x;
46380
+ sxy += x * y;
46381
+ }
46382
+ const den = pts.length * sxx - sx * sx;
46383
+ if (den !== 0) m = (pts.length * sxy - sx * sy) / den;
46384
+ }
46385
+ m = Math.max(-0.35, Math.min(0.35, m));
46386
+ let c = -Infinity;
46387
+ for (const [x, y] of pts) {
46388
+ const need = y - m * x + GAP;
46389
+ if (need > c) c = need;
46390
+ }
46391
+ return (x) => m * x + c;
46392
+ };
46393
+ const placeInset = (iso, proj, boxX, iwReq) => {
46394
+ const f = usLayer.get(iso);
46395
+ if (!f) return boxX;
46396
+ const x0 = boxX;
46397
+ const iw = Math.min(iwReq, width - FIT_PAD - x0 - 2 * PAD);
46398
+ if (iw < 24) return boxX;
46399
+ const xr = x0 + iw + 2 * PAD;
46400
+ const top = coastTop(x0, xr);
46401
+ const yL = top(x0);
46402
+ const yR = top(xr);
46403
+ proj.fitWidth(iw, f);
46404
+ const bb = geoPath(proj).bounds(f);
46405
+ const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
46406
+ const needH = sh + 2 * PAD;
46407
+ let topFit = Math.max(yL, yR);
46408
+ const bottom = Math.min(topFit + needH, yB);
46409
+ if (bottom - topFit < needH) topFit = bottom - needH;
46410
+ const lift = topFit - Math.max(yL, yR);
46411
+ const topL = yL + lift;
46412
+ const topR = yR + lift;
46413
+ proj.fitExtent(
46414
+ [
46415
+ [x0 + PAD, topFit + PAD],
46416
+ [xr - PAD, bottom - PAD]
46417
+ ],
46418
+ f
46419
+ );
46420
+ const d = geoPath(proj)(f) ?? "";
46421
+ if (!d) return xr;
46422
+ const r = regionById.get(iso);
46423
+ let fill2 = neutralFill;
46424
+ let lineNumber = -1;
46425
+ if (r?.layer === "us-state") {
46426
+ fill2 = regionFill(r);
46427
+ lineNumber = r.lineNumber;
46428
+ }
46429
+ insets.push({
46430
+ x: x0,
46431
+ y: Math.min(topL, topR),
46432
+ w: xr - x0,
46433
+ h: bottom - Math.min(topL, topR),
46434
+ points: [
46435
+ [x0, topL],
46436
+ [xr, topR],
46437
+ [xr, bottom],
46438
+ [x0, bottom]
46439
+ ]
46440
+ });
46441
+ insetRegions.push({
46442
+ id: iso,
46443
+ d,
46444
+ fill: fill2,
46445
+ stroke: regionStroke,
46446
+ lineNumber,
46447
+ layer: "us-state",
46448
+ ...r?.score !== void 0 && { score: r.score },
46449
+ ...r && Object.keys(r.tags).length > 0 && { tags: r.tags }
46450
+ });
46451
+ const ctr = geoPath(proj).centroid(f);
46452
+ if (Number.isFinite(ctr[0])) {
46453
+ const name = f.properties?.name ?? iso;
46454
+ insetLabelSeeds.push({ x: ctr[0], y: ctr[1], iso, name, lineNumber });
46455
+ }
46456
+ return xr;
46457
+ };
46458
+ const akRight = placeInset(
46459
+ "US-AK",
46460
+ alaskaProjection(),
46461
+ FIT_PAD,
46462
+ width * 0.15
46463
+ );
46464
+ placeInset("US-HI", hawaiiProjection(), akRight + 24, width * 0.1);
46465
+ }
46466
+ const conusFit = resolved.projection === "albers-usa" && !!usLayer;
46467
+ const cullExtent = conusFit ? geoBounds2(fitTarget) : resolved.extent;
46468
+ const [[exW, exS], [exE, exN]] = cullExtent;
46469
+ const lonSpan = exE - exW;
46470
+ const latSpan = exN - exS;
46471
+ const isGlobalView = lonSpan >= 270 || latSpan >= 130;
46472
+ const padLon = Math.max(8, lonSpan * 0.35);
46473
+ const padLat = Math.max(8, latSpan * 0.35);
46474
+ const vW = exW - padLon;
46475
+ const vE = exE + padLon;
46476
+ const vS = exS - padLat;
46477
+ const vN = exN + padLat;
46478
+ const vLonCenter = (exW + exE) / 2;
46479
+ const normLon = (lon) => {
46480
+ let L = lon;
46481
+ while (L < vLonCenter - 180) L += 360;
46482
+ while (L > vLonCenter + 180) L -= 360;
46483
+ return L;
46484
+ };
46485
+ const ringOverlapsView = (ring) => {
46486
+ let anyIn = false;
46487
+ let loMin = Infinity, loMax = -Infinity, laMin = Infinity, laMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
46488
+ for (const [rawLon, lat] of ring) {
46489
+ const lon = normLon(rawLon);
46490
+ if (lon >= vW && lon <= vE && lat >= vS && lat <= vN) anyIn = true;
46491
+ if (lon < loMin) loMin = lon;
46492
+ if (lon > loMax) loMax = lon;
46493
+ if (rawLon < rawMin) rawMin = rawLon;
46494
+ if (rawLon > rawMax) rawMax = rawLon;
46495
+ if (lat < laMin) laMin = lat;
46496
+ if (lat > laMax) laMax = lat;
46497
+ }
46498
+ if (loMax - loMin > 270) return false;
46499
+ if (rawMax - rawMin > 180 && loMax - loMin < 90) return false;
46500
+ if (anyIn) return true;
46501
+ if (loMax - loMin > 180) return false;
46502
+ return !(loMax < vW || loMin > vE || laMax < vS || laMin > vN);
46503
+ };
46504
+ const cullFeatureToView = (f) => {
46505
+ if (isGlobalView) return f;
46506
+ const g = f.geometry;
46507
+ if (!g) return f;
46508
+ if (g.type === "Polygon") {
46509
+ const ring = g.coordinates[0];
46510
+ return ringOverlapsView(ring) ? f : null;
46511
+ }
46512
+ if (g.type === "MultiPolygon") {
46513
+ const polys = g.coordinates;
46514
+ const keep = polys.filter(
46515
+ (p) => ringOverlapsView(p[0])
46516
+ );
46517
+ if (!keep.length) return null;
46518
+ if (keep.length === polys.length) return f;
46519
+ return { ...f, geometry: { ...g, coordinates: keep } };
46520
+ }
46521
+ return f;
46522
+ };
46523
+ const SEAM_SLIVER_MAX_SPAN = 100;
46524
+ const ringIsFrameFiller = (ring) => {
46525
+ const lons = ring.map(([lon]) => lon).sort((a, b) => a - b);
46526
+ if (lons.length < 2) return false;
46527
+ let maxGap = -1;
46528
+ let gapIdx = 0;
46529
+ for (let i = 1; i < lons.length; i++) {
46530
+ const g = lons[i] - lons[i - 1];
46531
+ if (g > maxGap) {
46532
+ maxGap = g;
46533
+ gapIdx = i;
46534
+ }
46535
+ }
46536
+ const wrapGap = lons[0] + 360 - lons[lons.length - 1];
46537
+ if (wrapGap >= maxGap) return false;
46538
+ const span = 360 - maxGap;
46539
+ const east = lons[gapIdx - 1] + 360;
46540
+ return east > 180 && span < SEAM_SLIVER_MAX_SPAN;
46541
+ };
46542
+ const dropFrameFillers = (f) => {
46543
+ const g = f.geometry;
46544
+ if (!g) return f;
46545
+ if (g.type === "Polygon") {
46546
+ const ring = g.coordinates[0];
46547
+ return ringIsFrameFiller(ring) ? null : f;
46548
+ }
46549
+ if (g.type === "MultiPolygon") {
46550
+ const polys = g.coordinates;
46551
+ const keep = polys.filter(
46552
+ (p) => !ringIsFrameFiller(p[0])
46553
+ );
46554
+ if (!keep.length) return null;
46555
+ if (keep.length === polys.length) return f;
46556
+ return { ...f, geometry: { ...g, coordinates: keep } };
46557
+ }
46558
+ return f;
46559
+ };
46088
46560
  const regions = [];
46089
- const pushRegionLayer = (layerFeatures, layerKind) => {
46561
+ const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
46090
46562
  for (const [iso, f] of layerFeatures) {
46091
- const d = path(f) ?? "";
46092
- if (!d) continue;
46563
+ if (layerKind === "us-state" && usContext && INSET_STATES.has(iso))
46564
+ continue;
46565
+ if (layerKind === "country" && usContext && iso === "US") continue;
46093
46566
  const r = regionById.get(iso);
46567
+ const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
46568
+ if (!viewF) continue;
46569
+ const d = path(viewF) ?? "";
46570
+ if (!d) continue;
46094
46571
  const isThisLayer = r?.layer === layerKind;
46095
- let fill2 = neutralFill;
46572
+ const isForeign = layerKind === "country" && usContext && iso !== "US";
46573
+ let fill2 = isForeign ? foreignFill : neutralFill;
46096
46574
  let label;
46097
46575
  let lineNumber = -1;
46098
46576
  let layer = "base";
46099
46577
  if (isThisLayer) {
46100
- if (r.score !== void 0) fill2 = fillForScore(r.score);
46101
- else fill2 = tagFill(r.tags, activeGroup) ?? neutralFill;
46578
+ fill2 = regionFill(r);
46102
46579
  lineNumber = r.lineNumber;
46103
46580
  layer = layerKind;
46104
46581
  label = r.name;
@@ -46110,12 +46587,42 @@ function layoutMap(resolved, data, size, opts) {
46110
46587
  stroke: regionStroke,
46111
46588
  lineNumber,
46112
46589
  layer,
46113
- ...label !== void 0 && { label }
46590
+ ...label !== void 0 && { label },
46591
+ ...isThisLayer && r.score !== void 0 && { score: r.score },
46592
+ ...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
46114
46593
  });
46115
46594
  }
46116
46595
  };
46117
- pushRegionLayer(worldLayer, "country");
46118
- if (usLayer) pushRegionLayer(usLayer, "us-state");
46596
+ pushRegionLayer(worldLayer, "country", !isGlobalView);
46597
+ if (usLayer) pushRegionLayer(usLayer, "us-state", !conusFit && !isGlobalView);
46598
+ const lakesTopo = usCrisp && data.naLakes ? data.naLakes : data.lakes;
46599
+ if (lakesTopo) {
46600
+ for (const [, f] of decodeLayer(lakesTopo)) {
46601
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
46602
+ if (!viewF) continue;
46603
+ const d = path(viewF) ?? "";
46604
+ if (!d) continue;
46605
+ regions.push({
46606
+ id: "lake",
46607
+ d,
46608
+ fill: water,
46609
+ stroke: "none",
46610
+ lineNumber: -1,
46611
+ layer: "base"
46612
+ });
46613
+ }
46614
+ }
46615
+ const riverColor = water;
46616
+ const rivers = [];
46617
+ if (data.rivers) {
46618
+ for (const [, f] of decodeLayer(data.rivers)) {
46619
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
46620
+ if (!viewF) continue;
46621
+ const d = path(viewF) ?? "";
46622
+ if (!d) continue;
46623
+ rivers.push({ d, color: riverColor, width: RIVER_WIDTH });
46624
+ }
46625
+ }
46119
46626
  const sizeVals = resolved.pois.map((p) => Number(p.meta["size"])).filter((n) => Number.isFinite(n) && n > 0);
46120
46627
  const sizeMin = sizeVals.length ? Math.min(...sizeVals) : 0;
46121
46628
  const sizeMax = sizeVals.length ? Math.max(...sizeVals) : 0;
@@ -46136,8 +46643,8 @@ function layoutMap(resolved, data, size, opts) {
46136
46643
  if (hex) return { fill: hex, stroke: mix(hex, palette.text, 18) };
46137
46644
  }
46138
46645
  return {
46139
- fill: palette.accent,
46140
- stroke: mix(palette.accent, palette.text, 18)
46646
+ fill: palette.colors.orange,
46647
+ stroke: mix(palette.colors.orange, palette.text, 18)
46141
46648
  };
46142
46649
  };
46143
46650
  const routeNumberById = /* @__PURE__ */ new Map();
@@ -46175,7 +46682,7 @@ function layoutMap(resolved, data, size, opts) {
46175
46682
  cy += Math.sin(ang) * COLO_R;
46176
46683
  }
46177
46684
  const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
46178
- poiScreen.set(e.p.id, { cx, cy });
46685
+ poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
46179
46686
  const num = routeNumberById.get(e.p.id);
46180
46687
  pois.push({
46181
46688
  id: e.p.id,
@@ -46192,17 +46699,36 @@ function layoutMap(resolved, data, size, opts) {
46192
46699
  });
46193
46700
  }
46194
46701
  const legs = [];
46702
+ const RIM_GAP = 1.5;
46195
46703
  const legPath = (a, b, curved, offset) => {
46196
- if (!curved && offset === 0) return `M${a.cx},${a.cy}L${b.cx},${b.cy}`;
46197
46704
  const mx = (a.cx + b.cx) / 2;
46198
46705
  const my = (a.cy + b.cy) / 2;
46199
46706
  const dx = b.cx - a.cx;
46200
46707
  const dy = b.cy - a.cy;
46201
46708
  const len = Math.hypot(dx, dy) || 1;
46709
+ const trimA = Math.min(a.r + RIM_GAP, len * 0.45);
46710
+ const trimB = Math.min(b.r + RIM_GAP, len * 0.45);
46711
+ if (!curved && offset === 0) {
46712
+ const ux = dx / len;
46713
+ const uy = dy / len;
46714
+ const ax2 = a.cx + ux * trimA;
46715
+ const ay2 = a.cy + uy * trimA;
46716
+ const bx2 = b.cx - ux * trimB;
46717
+ const by2 = b.cy - uy * trimB;
46718
+ return `M${ax2},${ay2}L${bx2},${by2}`;
46719
+ }
46202
46720
  const nx = -dy / len;
46203
46721
  const ny = dx / len;
46204
46722
  const bow = offset !== 0 ? offset : len * ARC_CURVE_FRAC;
46205
- return `M${a.cx},${a.cy}Q${mx + nx * bow},${my + ny * bow} ${b.cx},${b.cy}`;
46723
+ const px = mx + nx * bow;
46724
+ const py = my + ny * bow;
46725
+ const ta = Math.hypot(px - a.cx, py - a.cy) || 1;
46726
+ const tb = Math.hypot(b.cx - px, b.cy - py) || 1;
46727
+ const ax = a.cx + (px - a.cx) / ta * trimA;
46728
+ const ay = a.cy + (py - a.cy) / ta * trimA;
46729
+ const bx = b.cx - (b.cx - px) / tb * trimB;
46730
+ const by = b.cy - (b.cy - py) / tb * trimB;
46731
+ return `M${ax},${ay}Q${px},${py} ${bx},${by}`;
46206
46732
  };
46207
46733
  for (const rt of resolved.routes) {
46208
46734
  const curved = rt.meta["style"] === "arc";
@@ -46213,7 +46739,7 @@ function layoutMap(resolved, data, size, opts) {
46213
46739
  legs.push({
46214
46740
  d: legPath(a, b, curved, 0),
46215
46741
  width: W_MIN,
46216
- color: mix(palette.text, palette.bg, 55),
46742
+ color: mix(palette.text, palette.bg, 72),
46217
46743
  arrow: true,
46218
46744
  lineNumber: rt.lineNumber
46219
46745
  });
@@ -46249,7 +46775,7 @@ function layoutMap(resolved, data, size, opts) {
46249
46775
  legs.push({
46250
46776
  d: legPath(a, b, curved, offset),
46251
46777
  width: widthFor(e),
46252
- color: mix(palette.text, palette.bg, 45),
46778
+ color: mix(palette.text, palette.bg, 66),
46253
46779
  arrow: e.directed,
46254
46780
  lineNumber: e.lineNumber,
46255
46781
  ...e.label !== void 0 && {
@@ -46261,38 +46787,92 @@ function layoutMap(resolved, data, size, opts) {
46261
46787
  });
46262
46788
  }
46263
46789
  const labels = [];
46264
- const pinList = [];
46265
46790
  const obstacles = [];
46266
46791
  const markers = pois.map((p) => ({
46267
46792
  cx: p.cx,
46268
46793
  cy: p.cy,
46269
46794
  r: p.r
46270
46795
  }));
46271
- const collides = (rect) => markers.some((m) => rectCircleOverlap(rect, m)) || obstacles.some((o) => rectsOverlap(rect, o));
46796
+ const legSegments = [];
46797
+ for (const leg of legs) {
46798
+ const m = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
46799
+ leg.d
46800
+ );
46801
+ if (!m) continue;
46802
+ const x0 = +m[1];
46803
+ const y0 = +m[2];
46804
+ if (m[3] !== void 0) {
46805
+ legSegments.push([x0, y0, +m[3], +m[4]]);
46806
+ } else {
46807
+ const cx = +m[5];
46808
+ const cy = +m[6];
46809
+ const ex = +m[7];
46810
+ const ey = +m[8];
46811
+ const N = 8;
46812
+ let px = x0;
46813
+ let py = y0;
46814
+ for (let i = 1; i <= N; i++) {
46815
+ const t = i / N;
46816
+ const u = 1 - t;
46817
+ const qx = u * u * x0 + 2 * u * t * cx + t * t * ex;
46818
+ const qy = u * u * y0 + 2 * u * t * cy + t * t * ey;
46819
+ legSegments.push([px, py, qx, qy]);
46820
+ px = qx;
46821
+ py = qy;
46822
+ }
46823
+ }
46824
+ }
46825
+ 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));
46272
46826
  const regionLabelMode = resolved.directives.regionLabels ?? "off";
46827
+ const LABEL_PADX = 6;
46828
+ const LABEL_PADY = 3;
46829
+ const labelW = (text) => measureLegendText(text, FONT) + 2 * LABEL_PADX;
46830
+ const labelH = FONT + 2 * LABEL_PADY;
46831
+ const pushRegionLabel = (x, y, text, fill2, lineNumber) => {
46832
+ const color = contrastText(
46833
+ fill2,
46834
+ palette.textOnFillLight,
46835
+ palette.textOnFillDark
46836
+ );
46837
+ const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
46838
+ labels.push({
46839
+ x,
46840
+ y,
46841
+ text,
46842
+ anchor: "middle",
46843
+ color,
46844
+ halo: true,
46845
+ haloColor,
46846
+ lineNumber
46847
+ });
46848
+ };
46849
+ const WORLD_LABEL_ANCHORS = {
46850
+ US: [-98.5, 39.5]
46851
+ // CONUS geographic centre (near Lebanon, Kansas)
46852
+ };
46273
46853
  if (regionLabelMode === "full" || regionLabelMode === "abbrev") {
46274
46854
  for (const r of regions) {
46275
46855
  if (r.layer === "base" || r.label === void 0) continue;
46276
46856
  const f = r.layer === "us-state" ? usLayer?.get(r.id) : worldLayer.get(r.id);
46277
46857
  if (!f) continue;
46278
46858
  const [[x0, y0], [x1, y1]] = path.bounds(f);
46279
- if ((x1 - x0) * (y1 - y0) < TINY_REGION_AREA) continue;
46280
- const c = path.centroid(f);
46281
- if (!Number.isFinite(c[0])) continue;
46282
46859
  const text = regionLabelMode === "abbrev" ? r.id.replace(/^US-/, "") : r.label;
46283
- labels.push({
46284
- x: c[0],
46285
- y: c[1],
46860
+ if (labelW(text) > x1 - x0 || labelH > y1 - y0) continue;
46861
+ const anchor = r.layer !== "us-state" ? WORLD_LABEL_ANCHORS[r.id] : void 0;
46862
+ const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
46863
+ if (!c || !Number.isFinite(c[0])) continue;
46864
+ pushRegionLabel(c[0], c[1], text, r.fill, r.lineNumber);
46865
+ }
46866
+ for (const seed of insetLabelSeeds) {
46867
+ const text = regionLabelMode === "abbrev" ? seed.iso.replace(/^US-/, "") : seed.name;
46868
+ const src = regionById.get(seed.iso);
46869
+ pushRegionLabel(
46870
+ seed.x,
46871
+ seed.y,
46286
46872
  text,
46287
- anchor: "middle",
46288
- color: contrastText(
46289
- r.fill,
46290
- palette.textOnFillLight,
46291
- palette.textOnFillDark
46292
- ),
46293
- halo: true,
46294
- lineNumber: r.lineNumber
46295
- });
46873
+ src ? regionFill(src) : neutralFill,
46874
+ seed.lineNumber
46875
+ );
46296
46876
  }
46297
46877
  }
46298
46878
  const poiLabelMode = resolved.directives.poiLabels ?? "auto";
@@ -46305,68 +46885,106 @@ function layoutMap(resolved, data, size, opts) {
46305
46885
  const src = poiById.get(p.id);
46306
46886
  return src?.label ?? src?.name ?? p.id;
46307
46887
  };
46308
- let pinCounter = 0;
46309
- for (const p of ordered) {
46888
+ const poiLabH = FONT * 1.25;
46889
+ const labelInfo = (p) => {
46310
46890
  const text = labelText(p);
46311
- const w = measureLegendText(text, FONT);
46312
- const h = FONT * 1.25;
46313
- const inline = { x: p.cx + p.r + 3, y: p.cy - h / 2, w, h };
46314
- if (!collides(inline)) {
46315
- obstacles.push(inline);
46891
+ return { text, w: measureLegendText(text, FONT) };
46892
+ };
46893
+ const pushInline = (p, text, w, side) => {
46894
+ const tx = side === "right" ? p.cx + p.r + 3 : p.cx - p.r - 3;
46895
+ obstacles.push({
46896
+ x: side === "right" ? tx : tx - w,
46897
+ y: p.cy - poiLabH / 2,
46898
+ w,
46899
+ h: poiLabH
46900
+ });
46901
+ labels.push({
46902
+ x: tx,
46903
+ y: p.cy + FONT / 3,
46904
+ text,
46905
+ anchor: side === "right" ? "start" : "end",
46906
+ color: palette.text,
46907
+ halo: true,
46908
+ haloColor: palette.bg,
46909
+ poiId: p.id,
46910
+ lineNumber: p.lineNumber
46911
+ });
46912
+ };
46913
+ const inlineFits = (p, w, side) => {
46914
+ const tx = side === "right" ? p.cx + p.r + 3 : p.cx - p.r - 3;
46915
+ const rect = {
46916
+ x: side === "right" ? tx : tx - w,
46917
+ y: p.cy - poiLabH / 2,
46918
+ w,
46919
+ h: poiLabH
46920
+ };
46921
+ return rect.x >= 0 && rect.x + rect.w <= width && !collides(rect);
46922
+ };
46923
+ const GROUP_R = 30;
46924
+ const groups = [];
46925
+ for (const p of ordered) {
46926
+ const near = groups.find(
46927
+ (g) => g.some((q) => Math.hypot(q.cx - p.cx, q.cy - p.cy) < GROUP_R)
46928
+ );
46929
+ if (near) near.push(p);
46930
+ else groups.push([p]);
46931
+ }
46932
+ const ROW_GAP2 = 3;
46933
+ const step = poiLabH + ROW_GAP2;
46934
+ const COL_GAP = 16;
46935
+ const placeColumn = (group) => {
46936
+ const items = group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
46937
+ const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
46938
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
46939
+ const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
46940
+ const maxW = Math.max(...items.map((o) => o.w));
46941
+ const side = right + COL_GAP + maxW <= width - 2 ? "right" : "left";
46942
+ const colX = side === "right" ? right + COL_GAP : left - COL_GAP;
46943
+ const totalH = items.length * step;
46944
+ let startY = cyMid - totalH / 2;
46945
+ startY = Math.max(2, Math.min(startY, height - totalH - 2));
46946
+ items.forEach((o, i) => {
46947
+ const rowCy = startY + i * step + step / 2;
46948
+ obstacles.push({
46949
+ x: side === "right" ? colX : colX - o.w,
46950
+ y: rowCy - poiLabH / 2,
46951
+ w: o.w,
46952
+ h: poiLabH
46953
+ });
46316
46954
  labels.push({
46317
- x: inline.x,
46318
- y: p.cy + FONT / 3,
46319
- text,
46320
- anchor: "start",
46955
+ x: colX,
46956
+ y: rowCy + FONT / 3,
46957
+ text: o.text,
46958
+ anchor: side === "right" ? "start" : "end",
46321
46959
  color: palette.text,
46322
46960
  halo: true,
46323
- lineNumber: p.lineNumber
46961
+ haloColor: palette.bg,
46962
+ leader: {
46963
+ x1: o.p.cx,
46964
+ y1: o.p.cy,
46965
+ x2: side === "right" ? colX - 2 : colX + 2,
46966
+ y2: rowCy
46967
+ },
46968
+ leaderColor: o.p.fill,
46969
+ poiId: o.p.id,
46970
+ lineNumber: o.p.lineNumber
46324
46971
  });
46325
- continue;
46326
- }
46327
- let placed = false;
46328
- for (let k = 1; k <= 2 && !placed; k++) {
46329
- for (const [dx, dy] of RING_DIRS) {
46330
- const cx = p.cx + dx * LEADER_STEP * k;
46331
- const cy = p.cy + dy * LEADER_STEP * k;
46332
- const rect = {
46333
- x: dx >= 0 ? cx : cx - w,
46334
- y: cy - h / 2,
46335
- w,
46336
- h
46337
- };
46338
- if (rect.x < 0 || rect.x + rect.w > width || rect.y < 0 || rect.y + rect.h > height) {
46339
- continue;
46340
- }
46341
- if (collides(rect)) continue;
46342
- obstacles.push(rect);
46343
- labels.push({
46344
- x: cx,
46345
- y: cy + FONT / 3,
46346
- text,
46347
- anchor: dx >= 0 ? "start" : "end",
46348
- color: palette.text,
46349
- halo: true,
46350
- leader: { x1: p.cx, y1: p.cy, x2: cx, y2: cy },
46351
- lineNumber: p.lineNumber
46352
- });
46353
- placed = true;
46354
- break;
46972
+ });
46973
+ };
46974
+ for (const g of groups) {
46975
+ if (g.length === 1) {
46976
+ const p = g[0];
46977
+ const { text, w } = labelInfo(p);
46978
+ if (inlineFits(p, w, "right")) {
46979
+ pushInline(p, text, w, "right");
46980
+ continue;
46981
+ }
46982
+ if (inlineFits(p, w, "left")) {
46983
+ pushInline(p, text, w, "left");
46984
+ continue;
46355
46985
  }
46356
46986
  }
46357
- if (placed) continue;
46358
- pinCounter += 1;
46359
- pinList.push({ pin: pinCounter, label: text });
46360
- labels.push({
46361
- x: p.cx + p.r + 2,
46362
- y: p.cy - p.r,
46363
- text: String(pinCounter),
46364
- anchor: "start",
46365
- color: palette.text,
46366
- halo: true,
46367
- pin: pinCounter,
46368
- lineNumber: p.lineNumber
46369
- });
46987
+ placeColumn(g);
46370
46988
  }
46371
46989
  }
46372
46990
  let legend = null;
@@ -46375,8 +46993,7 @@ function layoutMap(resolved, data, size, opts) {
46375
46993
  name: g.name,
46376
46994
  entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
46377
46995
  }));
46378
- const hasAnything = tagGroups.length > 0 || hasRamp || sizeVals.length > 0 || weightVals.length > 0;
46379
- if (hasAnything) {
46996
+ if (tagGroups.length > 0 || hasRamp) {
46380
46997
  legend = {
46381
46998
  tagGroups,
46382
46999
  activeGroup,
@@ -46387,20 +47004,9 @@ function layoutMap(resolved, data, size, opts) {
46387
47004
  },
46388
47005
  min: rampMin,
46389
47006
  max: rampMax,
46390
- hue: rampHue
47007
+ hue: rampHue,
47008
+ base: rampBase
46391
47009
  }
46392
- },
46393
- ...sizeVals.length > 0 && {
46394
- size: {
46395
- ...resolved.directives.sizeMetric !== void 0 && {
46396
- metric: resolved.directives.sizeMetric
46397
- },
46398
- min: sizeMin,
46399
- max: sizeMax
46400
- }
46401
- },
46402
- ...weightVals.length > 0 && {
46403
- weight: { min: wMin, max: wMax }
46404
47010
  }
46405
47011
  };
46406
47012
  }
@@ -46408,26 +47014,28 @@ function layoutMap(resolved, data, size, opts) {
46408
47014
  return {
46409
47015
  width,
46410
47016
  height,
46411
- background: palette.bg,
47017
+ background: water,
46412
47018
  title: resolved.title,
46413
47019
  ...resolved.subtitle !== void 0 && { subtitle: resolved.subtitle },
46414
47020
  ...resolved.caption !== void 0 && { caption: resolved.caption },
46415
47021
  regions,
47022
+ rivers,
46416
47023
  legs,
46417
47024
  pois,
46418
47025
  labels,
46419
- pinList,
46420
- legend
47026
+ legend,
47027
+ insets,
47028
+ insetRegions
46421
47029
  };
46422
47030
  }
46423
- var FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, LEADER_STEP, COLO_EPS, COLO_R, GOLDEN_ANGLE, FAN_STEP, TINY_REGION_AREA, ARC_CURVE_FRAC, RING_DIRS;
47031
+ var FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT, COLO_EPS, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT, RIVER_WIDTH, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, COLO_R, GOLDEN_ANGLE, FAN_STEP, ARC_CURVE_FRAC, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, US_NON_CONUS;
46424
47032
  var init_layout15 = __esm({
46425
47033
  "src/map/layout.ts"() {
46426
47034
  "use strict";
46427
47035
  init_color_utils();
46428
- init_tag_groups();
46429
47036
  init_label_layout();
46430
47037
  init_legend_constants();
47038
+ init_title_constants();
46431
47039
  FIT_PAD = 24;
46432
47040
  RAMP_FLOOR = 15;
46433
47041
  R_DEFAULT = 6;
@@ -46436,23 +47044,32 @@ var init_layout15 = __esm({
46436
47044
  W_MIN = 1.25;
46437
47045
  W_MAX = 8;
46438
47046
  FONT = 11;
46439
- LEADER_STEP = 14;
46440
47047
  COLO_EPS = 1.5;
47048
+ LAND_TINT_LIGHT = 58;
47049
+ LAND_TINT_DARK = 75;
47050
+ TAG_TINT_LIGHT = 60;
47051
+ TAG_TINT_DARK = 68;
47052
+ WATER_TINT = 55;
47053
+ RIVER_WIDTH = 1.3;
47054
+ FOREIGN_TINT_LIGHT = 30;
47055
+ FOREIGN_TINT_DARK = 62;
46441
47056
  COLO_R = 9;
46442
47057
  GOLDEN_ANGLE = 2.399963229728653;
46443
47058
  FAN_STEP = 16;
46444
- TINY_REGION_AREA = 600;
46445
47059
  ARC_CURVE_FRAC = 0.18;
46446
- RING_DIRS = [
46447
- [1, 0],
46448
- [0, 1],
46449
- [-1, 0],
46450
- [0, -1],
46451
- [1, 1],
46452
- [-1, 1],
46453
- [-1, -1],
46454
- [1, -1]
46455
- ];
47060
+ usConusProjection = () => geoConicEqualArea().parallels([29.5, 45.5]).rotate([96, 0]);
47061
+ alaskaProjection = () => geoConicEqualArea().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
47062
+ hawaiiProjection = () => geoMercator();
47063
+ INSET_STATES = /* @__PURE__ */ new Set(["US-AK", "US-HI"]);
47064
+ US_NON_CONUS = /* @__PURE__ */ new Set([
47065
+ "US-AK",
47066
+ "US-HI",
47067
+ "US-AS",
47068
+ "US-GU",
47069
+ "US-MP",
47070
+ "US-PR",
47071
+ "US-VI"
47072
+ ]);
46456
47073
  }
46457
47074
  });
46458
47075
 
@@ -46463,7 +47080,7 @@ __export(renderer_exports16, {
46463
47080
  renderMapForExport: () => renderMapForExport
46464
47081
  });
46465
47082
  import * as d3Selection18 from "d3-selection";
46466
- function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims) {
47083
+ function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims, activeGroupOverride) {
46467
47084
  d3Selection18.select(container).selectAll(":not([data-d3-tooltip])").remove();
46468
47085
  const width = exportDims?.width ?? container.clientWidth;
46469
47086
  const height = exportDims?.height ?? container.clientHeight;
@@ -46474,27 +47091,29 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
46474
47091
  { width, height },
46475
47092
  {
46476
47093
  palette,
46477
- isDark
47094
+ isDark,
47095
+ ...activeGroupOverride !== void 0 && {
47096
+ activeGroup: activeGroupOverride
47097
+ }
46478
47098
  }
46479
47099
  );
46480
- const svg = d3Selection18.select(container).append("svg").attr("width", width).attr("height", height).attr("viewBox", `0 0 ${width} ${height}`).attr("preserveAspectRatio", "xMidYMin meet").attr("xmlns", "http://www.w3.org/2000/svg").style("font-family", FONT_FAMILY);
47100
+ const svg = d3Selection18.select(container).append("svg").attr("width", width).attr("height", height).attr("viewBox", `0 0 ${width} ${height}`).attr("preserveAspectRatio", "xMidYMin meet").attr("xmlns", "http://www.w3.org/2000/svg").style("font-family", FONT_FAMILY).style("background", layout.background);
46481
47101
  svg.append("rect").attr("width", width).attr("height", height).attr("fill", layout.background);
46482
- const arrowColor = mix(palette.text, palette.bg, 50);
46483
47102
  const defs = svg.append("defs");
46484
- defs.append("marker").attr("id", "dgmo-map-arrow").attr("viewBox", "0 0 10 10").attr("refX", 9).attr("refY", 5).attr("markerWidth", 7).attr("markerHeight", 7).attr("orient", "auto-start-reverse").append("path").attr("d", "M0,0L10,5L0,10z").attr("fill", arrowColor);
46485
- const haloColor = layout.background;
46486
- if (layout.title) {
46487
- 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).text(layout.title);
46488
- }
46489
- if (layout.subtitle) {
46490
- 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).text(layout.subtitle);
46491
- }
46492
- if (layout.caption) {
46493
- svg.append("text").attr("x", width / 2).attr("y", height - 8).attr("text-anchor", "middle").attr("font-size", LABEL_FONT).attr("fill", palette.textMuted).text(layout.caption);
46494
- }
47103
+ const arrowSize = (w) => Math.min(15, 7 + w * 0.95);
47104
+ const haloColor = palette.bg;
46495
47105
  const gRegions = svg.append("g").attr("class", "dgmo-map-regions");
46496
- for (const r of layout.regions) {
46497
- const p = gRegions.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", 0.5);
47106
+ const drawRegion = (g, r, strokeWidth) => {
47107
+ const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
47108
+ if (r.layer !== "base") {
47109
+ p.classed("dgmo-map-region", true).attr("data-region", r.id);
47110
+ if (r.score !== void 0) p.attr("data-score", r.score);
47111
+ if (r.tags) {
47112
+ for (const [group, value] of Object.entries(r.tags)) {
47113
+ p.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
47114
+ }
47115
+ }
47116
+ }
46498
47117
  if (r.lineNumber >= 0) {
46499
47118
  p.attr("data-line-number", r.lineNumber);
46500
47119
  if (onClickItem) {
@@ -46504,11 +47123,31 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
46504
47123
  );
46505
47124
  }
46506
47125
  }
47126
+ };
47127
+ for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
47128
+ if (layout.rivers.length) {
47129
+ const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47130
+ for (const r of layout.rivers) {
47131
+ gRivers.append("path").attr("d", r.d).attr("stroke", r.color).attr("stroke-width", r.width).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
47132
+ }
47133
+ }
47134
+ if (layout.insets.length) {
47135
+ const insetG = svg.append("g").attr("class", "dgmo-map-insets");
47136
+ for (const box of layout.insets) {
47137
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
47138
+ 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");
47139
+ }
47140
+ for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
46507
47141
  }
46508
47142
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
46509
- for (const leg of layout.legs) {
47143
+ layout.legs.forEach((leg, i) => {
46510
47144
  const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round");
46511
- if (leg.arrow) p.attr("marker-end", "url(#dgmo-map-arrow)");
47145
+ if (leg.arrow) {
47146
+ const id = `dgmo-map-arrow-${i}`;
47147
+ const s = arrowSize(leg.width);
47148
+ defs.append("marker").attr("id", id).attr("viewBox", "0 0 10 10").attr("refX", 10).attr("refY", 5).attr("markerUnits", "userSpaceOnUse").attr("markerWidth", s).attr("markerHeight", s).attr("orient", "auto-start-reverse").append("path").attr("d", "M0,0L10,5L0,10z").attr("fill", leg.color);
47149
+ p.attr("marker-end", `url(#${id})`);
47150
+ }
46512
47151
  if (leg.label !== void 0 && leg.labelX !== void 0) {
46513
47152
  emitText(
46514
47153
  gLegs,
@@ -46522,13 +47161,13 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
46522
47161
  LABEL_FONT - 1
46523
47162
  );
46524
47163
  }
46525
- }
47164
+ });
46526
47165
  const gPois = svg.append("g").attr("class", "dgmo-map-pois");
46527
47166
  for (const poi of layout.pois) {
46528
47167
  if (poi.isOrigin) {
46529
47168
  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);
46530
47169
  }
46531
- 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);
47170
+ 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);
46532
47171
  if (onClickItem) {
46533
47172
  c.style("cursor", "pointer").on(
46534
47173
  "click",
@@ -46552,48 +47191,66 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
46552
47191
  const gLabels = svg.append("g").attr("class", "dgmo-map-labels");
46553
47192
  for (const lab of layout.labels) {
46554
47193
  if (lab.leader) {
46555
- gLabels.append("line").attr("x1", lab.leader.x1).attr("y1", lab.leader.y1).attr("x2", lab.leader.x2).attr("y2", lab.leader.y2).attr("stroke", mix(palette.textMuted, palette.bg, 60)).attr("stroke-width", 0.75);
46556
- }
46557
- if (lab.pin !== void 0) {
46558
- gLabels.append("rect").attr("x", lab.x - 1).attr("y", lab.y - LABEL_FONT).attr("width", LABEL_FONT * 1.3).attr("height", LABEL_FONT * 1.3).attr("rx", 2).attr("fill", palette.surface).attr("stroke", palette.border);
47194
+ 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(
47195
+ "stroke",
47196
+ lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
47197
+ ).attr("stroke-width", lab.leaderColor ? 1 : 0.75);
47198
+ if (lab.poiId !== void 0) line12.attr("data-poi", lab.poiId);
46559
47199
  }
46560
- emitText(
47200
+ const t = emitText(
46561
47201
  gLabels,
46562
47202
  lab.x,
46563
47203
  lab.y,
46564
47204
  lab.text,
46565
47205
  lab.anchor,
46566
47206
  lab.color,
46567
- haloColor,
47207
+ lab.haloColor,
46568
47208
  lab.halo,
46569
47209
  LABEL_FONT
46570
47210
  );
46571
- }
46572
- if (layout.pinList.length > 0) {
46573
- const gPins = svg.append("g").attr("class", "dgmo-map-pin-list").attr(
46574
- "transform",
46575
- `translate(12, ${height - layout.pinList.length * 14 - 8})`
46576
- );
46577
- layout.pinList.forEach((entry, i) => {
46578
- gPins.append("text").attr("x", 0).attr("y", i * 14).attr("font-size", LABEL_FONT - 1).attr("fill", palette.textMuted).text(`${entry.pin} \u2014 ${entry.label}`);
46579
- });
47211
+ if (lab.poiId !== void 0) {
47212
+ t.attr("data-poi", lab.poiId).style("cursor", "default");
47213
+ }
46580
47214
  }
46581
47215
  if (layout.legend) {
46582
47216
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
46583
47217
  const legendG = svg.append("g").attr("class", "dgmo-map-legend").attr("transform", `translate(0, ${legendY})`);
46584
- const groups = layout.legend.tagGroups.filter((g) => g.entries.length > 0);
47218
+ const ramp = layout.legend.ramp;
47219
+ const scoreGroup = ramp ? {
47220
+ name: ramp.metric?.trim() || "Score",
47221
+ entries: [],
47222
+ gradient: {
47223
+ min: ramp.min,
47224
+ max: ramp.max,
47225
+ hue: ramp.hue,
47226
+ base: ramp.base
47227
+ }
47228
+ } : null;
47229
+ const tagGroups = layout.legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
47230
+ const groups = [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
46585
47231
  if (groups.length > 0) {
46586
47232
  const config = {
46587
- groups: groups.map((g) => ({ name: g.name, entries: [...g.entries] })),
47233
+ groups,
46588
47234
  position: { placement: "top-center", titleRelation: "below-title" },
46589
47235
  mode: exportDims ? "export" : "preview",
46590
- showEmptyGroups: false
47236
+ showEmptyGroups: false,
47237
+ // Keep inactive siblings visible as pills so the user can click to flip
47238
+ // the active colouring dimension (preview only — export shows just the
47239
+ // active group).
47240
+ showInactivePills: true
46591
47241
  };
46592
47242
  const state = { activeGroup: layout.legend.activeGroup };
46593
47243
  renderLegendD3(legendG, config, state, palette, isDark, void 0, width);
46594
47244
  }
46595
- const pinGap = layout.pinList.length ? layout.pinList.length * 14 + 14 : 0;
46596
- emitExtraLegend(svg, layout, palette, height, pinGap);
47245
+ }
47246
+ if (layout.title) {
47247
+ 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);
47248
+ }
47249
+ if (layout.subtitle) {
47250
+ 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);
47251
+ }
47252
+ if (layout.caption) {
47253
+ 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);
46597
47254
  }
46598
47255
  }
46599
47256
  function renderMapForExport(container, resolved, data, palette, isDark, exportDims) {
@@ -46604,54 +47261,7 @@ function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize) {
46604
47261
  if (withHalo) {
46605
47262
  t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7);
46606
47263
  }
46607
- }
46608
- function emitExtraLegend(svg, layout, palette, height, bottomGap) {
46609
- const { legend } = layout;
46610
- if (!legend) return;
46611
- if (!legend.ramp && !legend.size && !legend.weight) return;
46612
- const blocks = [];
46613
- const g = svg.append("g").attr("class", "dgmo-map-legend-keys").attr("transform", `translate(12, ${height - 56 - bottomGap})`);
46614
- let xCursor = 0;
46615
- if (legend.ramp) {
46616
- const ramp = legend.ramp;
46617
- blocks.push(() => {
46618
- const block = g.append("g").attr("transform", `translate(${xCursor},0)`);
46619
- const gradId = "dgmo-map-ramp";
46620
- const grad = block.append("defs").append("linearGradient").attr("id", gradId).attr("x1", "0%").attr("x2", "100%");
46621
- grad.append("stop").attr("offset", "0%").attr("stop-color", mix(ramp.hue, palette.bg, 15));
46622
- grad.append("stop").attr("offset", "100%").attr("stop-color", ramp.hue);
46623
- block.append("rect").attr("width", 80).attr("height", 8).attr("fill", `url(#${gradId})`);
46624
- block.append("text").attr("x", 0).attr("y", 22).attr("font-size", 9).attr("fill", palette.textMuted).text(String(ramp.min));
46625
- block.append("text").attr("x", 80).attr("y", 22).attr("text-anchor", "end").attr("font-size", 9).attr("fill", palette.textMuted).text(String(ramp.max));
46626
- if (ramp.metric) {
46627
- block.append("text").attr("x", 0).attr("y", -4).attr("font-size", 9).attr("fill", palette.textMuted).text(ramp.metric);
46628
- }
46629
- xCursor += 110;
46630
- });
46631
- }
46632
- if (legend.size) {
46633
- const sz = legend.size;
46634
- blocks.push(() => {
46635
- const block = g.append("g").attr("transform", `translate(${xCursor},0)`);
46636
- [3, 6, 10].forEach((r, i) => {
46637
- block.append("circle").attr("cx", i * 26 + r).attr("cy", 8).attr("r", r).attr("fill", "none").attr("stroke", palette.textMuted);
46638
- });
46639
- block.append("text").attr("x", 0).attr("y", -4).attr("font-size", 9).attr("fill", palette.textMuted).text(sz.metric ?? "size");
46640
- xCursor += 110;
46641
- });
46642
- }
46643
- if (legend.weight) {
46644
- const wt = legend.weight;
46645
- blocks.push(() => {
46646
- const block = g.append("g").attr("transform", `translate(${xCursor},0)`);
46647
- [1, 3, 6].forEach((w, i) => {
46648
- block.append("line").attr("x1", i * 26).attr("y1", 8).attr("x2", i * 26 + 20).attr("y2", 8).attr("stroke", palette.textMuted).attr("stroke-width", w);
46649
- });
46650
- block.append("text").attr("x", 0).attr("y", -4).attr("font-size", 9).attr("fill", palette.textMuted).text(wt.metric ?? "weight");
46651
- xCursor += 110;
46652
- });
46653
- }
46654
- for (const draw of blocks) draw();
47264
+ return t;
46655
47265
  }
46656
47266
  var LABEL_FONT;
46657
47267
  var init_renderer16 = __esm({