@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/advanced.js CHANGED
@@ -331,6 +331,33 @@ function rectCircleOverlap(rect, circle) {
331
331
  const dy = nearestY - circle.cy;
332
332
  return dx * dx + dy * dy < circle.r * circle.r;
333
333
  }
334
+ function segmentRectOverlap(x0, y0, x1, y1, rect) {
335
+ const dx = x1 - x0;
336
+ const dy = y1 - y0;
337
+ let t0 = 0;
338
+ let t1 = 1;
339
+ const edges = [
340
+ [-dx, x0 - rect.x],
341
+ [dx, rect.x + rect.w - x0],
342
+ [-dy, y0 - rect.y],
343
+ [dy, rect.y + rect.h - y0]
344
+ ];
345
+ for (const [p, q] of edges) {
346
+ if (p === 0) {
347
+ if (q < 0) return false;
348
+ } else {
349
+ const t = q / p;
350
+ if (p < 0) {
351
+ if (t > t1) return false;
352
+ if (t > t0) t0 = t;
353
+ } else {
354
+ if (t < t0) return false;
355
+ if (t < t1) t1 = t;
356
+ }
357
+ }
358
+ }
359
+ return true;
360
+ }
334
361
  function computeQuadrantPointLabels(points, chartBounds, obstacles, pointRadius, fontSize) {
335
362
  const labelHeight = fontSize + 4;
336
363
  const stepSize = labelHeight + 2;
@@ -3276,6 +3303,57 @@ var init_legend_constants = __esm({
3276
3303
  });
3277
3304
 
3278
3305
  // src/utils/legend-layout.ts
3306
+ function fmtRamp(n) {
3307
+ return Number.isInteger(n) ? String(n) : String(Math.round(n * 10) / 10);
3308
+ }
3309
+ function gradientCapsuleWidth(name, gradient) {
3310
+ const pw = pillWidth(name);
3311
+ const minW = measureLegendText(fmtRamp(gradient.min), LEGEND_ENTRY_FONT_SIZE);
3312
+ const maxW = measureLegendText(fmtRamp(gradient.max), LEGEND_ENTRY_FONT_SIZE);
3313
+ return LEGEND_CAPSULE_PAD + pw + 4 + minW + RAMP_LABEL_GAP + RAMP_LEGEND_W + RAMP_LABEL_GAP + maxW + LEGEND_CAPSULE_PAD;
3314
+ }
3315
+ function buildGradientCapsuleLayout(group, gradient) {
3316
+ const pw = pillWidth(group.name);
3317
+ const minText = fmtRamp(gradient.min);
3318
+ const maxText = fmtRamp(gradient.max);
3319
+ const minW = measureLegendText(minText, LEGEND_ENTRY_FONT_SIZE);
3320
+ const gx = LEGEND_CAPSULE_PAD + pw + 4;
3321
+ const minX = gx;
3322
+ const rampX = gx + minW + RAMP_LABEL_GAP;
3323
+ const maxX = rampX + RAMP_LEGEND_W + RAMP_LABEL_GAP;
3324
+ const width = gradientCapsuleWidth(group.name, gradient);
3325
+ return {
3326
+ groupName: group.name,
3327
+ x: 0,
3328
+ y: 0,
3329
+ width,
3330
+ height: LEGEND_HEIGHT,
3331
+ pill: {
3332
+ groupName: group.name,
3333
+ x: LEGEND_CAPSULE_PAD,
3334
+ y: LEGEND_CAPSULE_PAD,
3335
+ width: pw,
3336
+ height: LEGEND_HEIGHT - LEGEND_CAPSULE_PAD * 2,
3337
+ isActive: true
3338
+ },
3339
+ entries: [],
3340
+ gradient: {
3341
+ rampX,
3342
+ rampY: (LEGEND_HEIGHT - RAMP_LEGEND_H) / 2,
3343
+ rampW: RAMP_LEGEND_W,
3344
+ rampH: RAMP_LEGEND_H,
3345
+ min: gradient.min,
3346
+ max: gradient.max,
3347
+ minText,
3348
+ minX,
3349
+ maxText,
3350
+ maxX,
3351
+ textY: LEGEND_HEIGHT / 2,
3352
+ hue: gradient.hue,
3353
+ base: gradient.base
3354
+ }
3355
+ };
3356
+ }
3279
3357
  function pillWidth(name) {
3280
3358
  return measureLegendText(name, LEGEND_PILL_FONT_SIZE) + LEGEND_PILL_PAD;
3281
3359
  }
@@ -3418,7 +3496,7 @@ function computeLegendLayout(config, state, containerWidth) {
3418
3496
  };
3419
3497
  }
3420
3498
  const controlsGroupLayout = isExport ? void 0 : buildControlsGroupLayout(config, state);
3421
- const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0);
3499
+ const visibleGroups = config.showEmptyGroups ? groups : groups.filter((g) => g.entries.length > 0 || !!g.gradient);
3422
3500
  if (visibleGroups.length === 0 && (!configControls || configControls.length === 0) && !controlsGroupLayout) {
3423
3501
  return {
3424
3502
  height: 0,
@@ -3497,7 +3575,7 @@ function computeLegendLayout(config, state, containerWidth) {
3497
3575
  groupAvailW,
3498
3576
  config.capsulePillAddonWidth ?? 0
3499
3577
  );
3500
- } else if (!activeGroupName) {
3578
+ } else if (!activeGroupName || config.showInactivePills) {
3501
3579
  const pw = pillWidth(g.name);
3502
3580
  pills.push({
3503
3581
  groupName: g.name,
@@ -3535,6 +3613,7 @@ function computeLegendLayout(config, state, containerWidth) {
3535
3613
  };
3536
3614
  }
3537
3615
  function buildCapsuleLayout(group, containerWidth, addonWidth = 0) {
3616
+ if (group.gradient) return buildGradientCapsuleLayout(group, group.gradient);
3538
3617
  const pw = pillWidth(group.name);
3539
3618
  const info = capsuleWidth(
3540
3619
  group.name,
@@ -3709,7 +3788,7 @@ function getMaxLegendReservedHeight(config, containerWidth) {
3709
3788
  }
3710
3789
  return max;
3711
3790
  }
3712
- var CONTROL_PILL_PAD, CONTROL_FONT_SIZE, CONTROL_ICON_GAP, CONTROL_GAP;
3791
+ var CONTROL_PILL_PAD, CONTROL_FONT_SIZE, CONTROL_ICON_GAP, CONTROL_GAP, RAMP_LEGEND_W, RAMP_LEGEND_H, RAMP_LABEL_GAP;
3713
3792
  var init_legend_layout = __esm({
3714
3793
  "src/utils/legend-layout.ts"() {
3715
3794
  "use strict";
@@ -3718,6 +3797,9 @@ var init_legend_layout = __esm({
3718
3797
  CONTROL_FONT_SIZE = 11;
3719
3798
  CONTROL_ICON_GAP = 4;
3720
3799
  CONTROL_GAP = 8;
3800
+ RAMP_LEGEND_W = 80;
3801
+ RAMP_LEGEND_H = 8;
3802
+ RAMP_LABEL_GAP = 6;
3721
3803
  }
3722
3804
  });
3723
3805
 
@@ -3805,6 +3887,16 @@ function renderCapsule(parent, capsule, palette, groupBg, pillBorder, _isDark, c
3805
3887
  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);
3806
3888
  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);
3807
3889
  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);
3890
+ if (capsule.gradient) {
3891
+ const gr = capsule.gradient;
3892
+ const gradId = `dgmo-legend-ramp-${capsule.groupName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
3893
+ const def = g.append("defs").append("linearGradient").attr("id", gradId);
3894
+ def.append("stop").attr("offset", "0%").attr("stop-color", mix(gr.hue, gr.base, 15));
3895
+ def.append("stop").attr("offset", "100%").attr("stop-color", gr.hue);
3896
+ 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);
3897
+ 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})`);
3898
+ 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);
3899
+ }
3808
3900
  for (const entry of capsule.entries) {
3809
3901
  const entryG = g.append("g").attr("data-legend-entry", entry.value.toLowerCase()).attr("data-series-name", entry.value).style("cursor", "pointer");
3810
3902
  entryG.append("circle").attr("cx", entry.dotCx).attr("cy", entry.dotCy).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
@@ -11244,23 +11336,22 @@ function parseC4(content, palette) {
11244
11336
  }
11245
11337
  }
11246
11338
  const parent = findParentElement(indent, stack);
11247
- if (parent) {
11248
- const descResult = tryStripDescriptionKeyword(trimmed);
11339
+ const descResult = tryStripDescriptionKeyword(trimmed);
11340
+ if (parent && descResult.isKeyword) {
11249
11341
  if (descResult.needsColon) {
11250
11342
  pushError(
11251
11343
  lineNumber,
11252
- `Use "description: ${descResult.text}" \u2014 colon is required.`,
11344
+ `Use "description: ${descResult.text}" \u2014 bare "description" is deprecated.`,
11253
11345
  "warning"
11254
11346
  );
11255
11347
  }
11256
- const descText = descResult.isKeyword ? descResult.text : trimmed;
11257
11348
  let desc = elementDescription.get(parent.element);
11258
11349
  if (!desc) {
11259
11350
  desc = [];
11260
11351
  elementDescription.set(parent.element, desc);
11261
11352
  parent.element.description = desc;
11262
11353
  }
11263
- desc.push(descText);
11354
+ desc.push(descResult.text);
11264
11355
  } else {
11265
11356
  pushError(lineNumber, `Unexpected content: "${trimmed}"`);
11266
11357
  }
@@ -11727,7 +11818,7 @@ function parseSitemap(content, palette) {
11727
11818
  if (descResult.needsColon) {
11728
11819
  pushWarning(
11729
11820
  lineNumber,
11730
- `Use "description: ${descResult.text}" \u2014 colon is required.`
11821
+ `Use "description: ${descResult.text}" \u2014 bare "description" is deprecated.`
11731
11822
  );
11732
11823
  }
11733
11824
  const parent = findParentNode(indent, indentStack);
@@ -12555,23 +12646,22 @@ function parseInfra(content) {
12555
12646
  }
12556
12647
  }
12557
12648
  const descResult = tryStripDescriptionKeyword(trimmed);
12558
- if (descResult.isKeyword && currentNode.isEdge) {
12559
- continue;
12560
- }
12561
- if (!currentNode.isEdge) {
12649
+ if (descResult.isKeyword) {
12650
+ if (currentNode.isEdge) {
12651
+ continue;
12652
+ }
12562
12653
  if (descResult.needsColon) {
12563
12654
  warn(
12564
12655
  lineNumber,
12565
- `Use "description: ${descResult.text}" \u2014 colon is required.`
12656
+ `Use "description: ${descResult.text}" \u2014 bare "description" is deprecated.`
12566
12657
  );
12567
12658
  }
12568
- const descText = descResult.isKeyword ? descResult.text : trimmed;
12569
- pushDescription(currentNode, descText);
12659
+ pushDescription(currentNode, descResult.text);
12570
12660
  continue;
12571
12661
  }
12572
12662
  warn(
12573
12663
  lineNumber,
12574
- `Unexpected line inside component '${currentNode.label}'. Expected a property (key: value), connection (-> Target), or description text.`
12664
+ `Unexpected line inside component '${currentNode.label}'. Expected a property (key: value), connection (-> Target), or a description (description: text).`
12575
12665
  );
12576
12666
  continue;
12577
12667
  }
@@ -15708,9 +15798,6 @@ function parseMap(content) {
15708
15798
  const pushWarning = (line12, message) => {
15709
15799
  diagnostics.push(makeDgmoError(line12, message, "warning"));
15710
15800
  };
15711
- const pushInfo = (line12, message) => {
15712
- diagnostics.push(makeDgmoError(line12, message, "warning"));
15713
- };
15714
15801
  const lines = content.split("\n");
15715
15802
  let firstIdx = 0;
15716
15803
  while (firstIdx < lines.length) {
@@ -15840,10 +15927,15 @@ function parseMap(content) {
15840
15927
  break;
15841
15928
  case "projection":
15842
15929
  dup(d.projection);
15843
- if (value && !["natural-earth", "albers-usa", "mercator"].includes(value))
15930
+ if (value && ![
15931
+ "equirectangular",
15932
+ "natural-earth",
15933
+ "albers-usa",
15934
+ "mercator"
15935
+ ].includes(value))
15844
15936
  pushWarning(
15845
15937
  line12,
15846
- `Unknown projection "${value}" (expected natural-earth | albers-usa | mercator).`
15938
+ `Unknown projection "${value}" (expected equirectangular | natural-earth | albers-usa | mercator).`
15847
15939
  );
15848
15940
  d.projection = value;
15849
15941
  break;
@@ -15982,17 +16074,21 @@ function parseMap(content) {
15982
16074
  scoreNum = void 0;
15983
16075
  }
15984
16076
  }
15985
- if (scoreNum !== void 0 && Object.keys(tags).length)
15986
- pushInfo(
15987
- line12,
15988
- "A region has both `score:` and a tag value \u2014 v1 renders only the score (bivariate is a future seam)."
15989
- );
16077
+ let regionName = split.name;
16078
+ let regionScope;
16079
+ const rToks = regionName.split(/\s+/);
16080
+ const rLast = rToks[rToks.length - 1];
16081
+ if (rToks.length > 1 && SCOPE_RE.test(rLast)) {
16082
+ regionName = rToks.slice(0, -1).join(" ");
16083
+ regionScope = rLast;
16084
+ }
15990
16085
  const region = {
15991
- name: split.name,
16086
+ name: regionName,
15992
16087
  tags,
15993
16088
  meta,
15994
16089
  lineNumber: line12
15995
16090
  };
16091
+ if (regionScope !== void 0) region.scope = regionScope;
15996
16092
  if (scoreNum !== void 0) region.score = scoreNum;
15997
16093
  regions.push(region);
15998
16094
  }
@@ -17116,7 +17212,7 @@ function parseMindmap(content, palette) {
17116
17212
  if (descResult.needsColon) {
17117
17213
  pushWarning(
17118
17214
  lineNumber,
17119
- `Use "description: ${descResult.text}" \u2014 colon is required.`
17215
+ `Use "description: ${descResult.text}" \u2014 bare "description" is deprecated.`
17120
17216
  );
17121
17217
  }
17122
17218
  const parent = findMetadataParent2(indent, indentStack);
@@ -18904,7 +19000,7 @@ function parseJourneyMap(content, palette) {
18904
19000
  if (descResult.needsColon) {
18905
19001
  warn(
18906
19002
  lineNumber,
18907
- `Use "description: ${descResult.text}" \u2014 colon is required.`
19003
+ `Use "description: ${descResult.text}" \u2014 bare "description" is deprecated.`
18908
19004
  );
18909
19005
  }
18910
19006
  currentStep.description = descResult.text;
@@ -45806,7 +45902,9 @@ function resolveMap(parsed, data) {
45806
45902
  const usScoped = parsed.directives.region === "us-states" || parsed.directives.defaultCountry?.toUpperCase() === "US" || parsed.regions.some((r) => {
45807
45903
  const f = fold(r.name);
45808
45904
  return usStateIndex.has(f) && !countryIndex.has(f);
45809
- }) || parsed.pois.some(
45905
+ }) || parsed.regions.some(
45906
+ (r) => r.scope === "US" || r.scope?.startsWith("US-")
45907
+ ) || parsed.pois.some(
45810
45908
  (p) => p.pos.kind === "name" && p.pos.scope?.startsWith("US-")
45811
45909
  );
45812
45910
  const regions = [];
@@ -45818,7 +45916,30 @@ function resolveMap(parsed, data) {
45818
45916
  const inCountry = countryIndex.get(f) ?? (REGION_ALIASES[f] ? countryIndex.get(REGION_ALIASES[f]) : void 0);
45819
45917
  const inState = usStateIndex.get(f);
45820
45918
  let chosen = null;
45821
- if (inCountry && inState) {
45919
+ const scope = r.scope;
45920
+ if (scope) {
45921
+ const wantsState = scope === "US" || scope.startsWith("US-");
45922
+ if (wantsState && inState) {
45923
+ if (scope.startsWith("US-") && inState.id !== scope) {
45924
+ err(
45925
+ r.lineNumber,
45926
+ `No subdivision "${r.name}" in scope ${scope} (it is ${inState.id}).`,
45927
+ "E_MAP_SCOPE_MISS"
45928
+ );
45929
+ continue;
45930
+ }
45931
+ chosen = { ...inState, layer: "us-state" };
45932
+ } else if (!wantsState && inCountry) {
45933
+ chosen = { ...inCountry, layer: "country" };
45934
+ } else {
45935
+ err(
45936
+ r.lineNumber,
45937
+ `No region "${r.name}" found in scope ${scope}.`,
45938
+ "E_MAP_SCOPE_MISS"
45939
+ );
45940
+ continue;
45941
+ }
45942
+ } else if (inCountry && inState) {
45822
45943
  if (usScoped) {
45823
45944
  chosen = { ...inState, layer: "us-state" };
45824
45945
  } else {
@@ -45826,7 +45947,7 @@ function resolveMap(parsed, data) {
45826
45947
  }
45827
45948
  warn(
45828
45949
  r.lineNumber,
45829
- `"${r.name}" is both a country and a US state \u2014 resolved as ${chosen.layer} (${chosen.id}).`,
45950
+ `"${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}").`,
45830
45951
  "W_MAP_REGION_AMBIGUOUS"
45831
45952
  );
45832
45953
  } else if (inState) {
@@ -45998,9 +46119,17 @@ function resolveMap(parsed, data) {
45998
46119
  };
45999
46120
  registerPoi(id, poi, p.lineNumber);
46000
46121
  }
46122
+ const declaredByName = /* @__PURE__ */ new Map();
46123
+ for (const p of pois) {
46124
+ const fn = p.name ? fold(p.name) : void 0;
46125
+ if (fn && fn !== p.id && !declaredByName.has(fn))
46126
+ declaredByName.set(fn, p.id);
46127
+ }
46001
46128
  const resolveEndpoint2 = (ref, line12) => {
46002
46129
  const f = fold(ref);
46003
46130
  if (registry.has(f)) return f;
46131
+ const aliased = declaredByName.get(f);
46132
+ if (aliased) return aliased;
46004
46133
  const got = lookupName(ref, void 0, line12, inferredCountry, true);
46005
46134
  if (got.kind !== "ok") return null;
46006
46135
  noteCountry(got.iso);
@@ -46080,23 +46209,29 @@ function resolveMap(parsed, data) {
46080
46209
  [-180, -85],
46081
46210
  [180, 85]
46082
46211
  ];
46083
- const extent2 = unioned ? pad(unioned, PAD_FRACTION) : DEFAULT_EXTENT;
46212
+ let extent2 = unioned ? pad(unioned, PAD_FRACTION) : DEFAULT_EXTENT;
46084
46213
  const lonSpan = extent2[1][0] - extent2[0][0];
46085
46214
  const latSpan = extent2[1][1] - extent2[0][1];
46086
46215
  const span = Math.max(lonSpan, latSpan);
46087
46216
  const usDominant = (inferredCountry === "US" || subdivisions.includes("us-states")) && !regions.some((r) => r.layer === "country" && r.iso !== "US") && !anyNonUsPoi;
46088
46217
  let projection;
46089
46218
  const override = parsed.directives.projection;
46090
- if (override === "natural-earth" || override === "albers-usa" || override === "mercator") {
46219
+ if (override === "equirectangular" || override === "natural-earth" || override === "albers-usa" || override === "mercator") {
46091
46220
  projection = override;
46092
46221
  } else if (usDominant) {
46093
46222
  projection = "albers-usa";
46094
46223
  } else if (span > WORLD_SPAN) {
46095
- projection = "natural-earth";
46224
+ projection = "equirectangular";
46096
46225
  } else if (span < MERCATOR_MAX_SPAN) {
46097
46226
  projection = "mercator";
46098
46227
  } else {
46099
- projection = "natural-earth";
46228
+ projection = "equirectangular";
46229
+ }
46230
+ if (lonSpan >= 180) {
46231
+ extent2 = [
46232
+ [-180, Math.min(extent2[0][1], WORLD_LAT_SOUTH)],
46233
+ [180, Math.max(extent2[1][1], WORLD_LAT_NORTH)]
46234
+ ];
46100
46235
  }
46101
46236
  result.regions = regions;
46102
46237
  result.pois = pois;
@@ -46144,7 +46279,7 @@ function firstError(diags) {
46144
46279
  const e = diags.find((d) => d.severity === "error");
46145
46280
  return e ? formatDgmoError(e) : null;
46146
46281
  }
46147
- var WORLD_SPAN, MERCATOR_MAX_SPAN, PAD_FRACTION, REGION_ALIASES;
46282
+ var WORLD_SPAN, MERCATOR_MAX_SPAN, PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, REGION_ALIASES;
46148
46283
  var init_resolver2 = __esm({
46149
46284
  "src/map/resolver.ts"() {
46150
46285
  "use strict";
@@ -46153,6 +46288,8 @@ var init_resolver2 = __esm({
46153
46288
  WORLD_SPAN = 90;
46154
46289
  MERCATOR_MAX_SPAN = 25;
46155
46290
  PAD_FRACTION = 0.05;
46291
+ WORLD_LAT_SOUTH = -58;
46292
+ WORLD_LAT_NORTH = 78;
46156
46293
  REGION_ALIASES = {
46157
46294
  // Common everyday names → the Natural-Earth display name actually shipped.
46158
46295
  "united states": "united states of america",
@@ -46220,13 +46357,36 @@ function moduleBaseDir() {
46220
46357
  function loadMapData() {
46221
46358
  cache ??= (async () => {
46222
46359
  const dir = await firstExistingDir(moduleBaseDir());
46223
- const [worldCoarse, worldDetail, usStates, gazetteer] = await Promise.all([
46360
+ const [
46361
+ worldCoarse,
46362
+ worldDetail,
46363
+ usStates,
46364
+ lakes,
46365
+ rivers,
46366
+ naLand,
46367
+ naLakes,
46368
+ gazetteer
46369
+ ] = await Promise.all([
46224
46370
  readJson(dir, FILES.worldCoarse),
46225
46371
  readJson(dir, FILES.worldDetail),
46226
46372
  readJson(dir, FILES.usStates),
46373
+ // Lakes/rivers/NA assets are optional — older bundles may predate them.
46374
+ readJson(dir, FILES.lakes).catch(() => void 0),
46375
+ readJson(dir, FILES.rivers).catch(() => void 0),
46376
+ readJson(dir, FILES.naLand).catch(() => void 0),
46377
+ readJson(dir, FILES.naLakes).catch(() => void 0),
46227
46378
  readJson(dir, FILES.gazetteer)
46228
46379
  ]);
46229
- return validate({ worldCoarse, worldDetail, usStates, gazetteer });
46380
+ return validate({
46381
+ worldCoarse,
46382
+ worldDetail,
46383
+ usStates,
46384
+ gazetteer,
46385
+ ...lakes && { lakes },
46386
+ ...rivers && { rivers },
46387
+ ...naLand && { naLand },
46388
+ ...naLakes && { naLakes }
46389
+ });
46230
46390
  })().catch((e) => {
46231
46391
  cache = void 0;
46232
46392
  throw e;
@@ -46241,6 +46401,10 @@ var init_load_data = __esm({
46241
46401
  worldCoarse: "world-coarse.json",
46242
46402
  worldDetail: "world-detail.json",
46243
46403
  usStates: "us-states.json",
46404
+ lakes: "lakes.json",
46405
+ rivers: "rivers.json",
46406
+ naLand: "na-land.json",
46407
+ naLakes: "na-lakes.json",
46244
46408
  gazetteer: "gazetteer.json"
46245
46409
  };
46246
46410
  CANDIDATE_DIRS = [
@@ -46256,8 +46420,11 @@ var init_load_data = __esm({
46256
46420
  import {
46257
46421
  geoPath,
46258
46422
  geoNaturalEarth1,
46259
- geoAlbersUsa,
46260
- geoMercator
46423
+ geoEquirectangular,
46424
+ geoConicEqualArea,
46425
+ geoMercator,
46426
+ geoBounds as geoBounds2,
46427
+ geoTransform
46261
46428
  } from "d3-geo";
46262
46429
  import { feature as feature2 } from "topojson-client";
46263
46430
  function geomObject2(topo) {
@@ -46275,36 +46442,74 @@ function decodeLayer(topo) {
46275
46442
  function projectionFor(family) {
46276
46443
  switch (family) {
46277
46444
  case "albers-usa":
46278
- return geoAlbersUsa();
46445
+ return usConusProjection();
46279
46446
  case "mercator":
46280
46447
  return geoMercator();
46281
46448
  case "natural-earth":
46282
- default:
46283
46449
  return geoNaturalEarth1();
46450
+ case "equirectangular":
46451
+ default:
46452
+ return geoEquirectangular();
46284
46453
  }
46285
46454
  }
46455
+ function mapBackgroundColor(palette) {
46456
+ return mix(palette.colors.blue, palette.bg, WATER_TINT);
46457
+ }
46458
+ function mapNeutralLandColor(palette, isDark) {
46459
+ return mix(
46460
+ palette.colors.green,
46461
+ palette.bg,
46462
+ isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT
46463
+ );
46464
+ }
46286
46465
  function layoutMap(resolved, data, size, opts) {
46287
46466
  const { palette, isDark } = opts;
46288
46467
  const { width, height } = size;
46289
- const worldTopo = resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46468
+ const wantsUsStates = resolved.basemaps.subdivisions.includes("us-states");
46469
+ const usCrisp = resolved.projection === "albers-usa" && wantsUsStates && !!data.naLand;
46470
+ const worldTopo = usCrisp ? data.naLand : resolved.basemaps.world === "detail" ? data.worldDetail : data.worldCoarse;
46290
46471
  const worldLayer = decodeLayer(worldTopo);
46291
- const usLayer = resolved.basemaps.subdivisions.includes("us-states") ? decodeLayer(data.usStates) : null;
46292
- const neutralFill = mix(palette.border, palette.bg, 32);
46293
- const regionStroke = mix(palette.border, palette.bg, 70);
46472
+ const usLayer = wantsUsStates ? decodeLayer(data.usStates) : null;
46473
+ const landTint = isDark ? LAND_TINT_DARK : LAND_TINT_LIGHT;
46474
+ const neutralFill = mix(palette.colors.green, palette.bg, landTint);
46475
+ const water = mapBackgroundColor(palette);
46476
+ const usContext = usLayer !== null;
46477
+ const foreignFill = mix(
46478
+ palette.colors.gray,
46479
+ palette.bg,
46480
+ isDark ? FOREIGN_TINT_DARK : FOREIGN_TINT_LIGHT
46481
+ );
46482
+ const regionStroke = isDark ? mix(palette.bg, palette.text, 78) : mix(palette.text, palette.bg, 78);
46294
46483
  const scores = resolved.regions.filter((r) => r.score !== void 0).map((r) => r.score);
46295
46484
  const scaleOverride = resolved.directives.scale;
46296
46485
  const rampMin = scaleOverride ? scaleOverride.min : Math.min(...scores);
46297
46486
  const rampMax = scaleOverride ? scaleOverride.max : Math.max(...scores);
46298
- const rampHue = palette.primary;
46487
+ const rampHue = palette.colors.red;
46299
46488
  const hasRamp = scores.length > 0;
46300
- const activeGroup = resolveActiveTagGroup(
46301
- resolved.tagGroups,
46302
- resolved.directives.activeTag
46303
- );
46489
+ const SCORE_NAME = hasRamp ? resolved.directives.metric?.trim() || "Score" : null;
46490
+ const matchColorGroup = (v) => {
46491
+ const lv = v.trim().toLowerCase();
46492
+ if (lv === "none") return null;
46493
+ if (SCORE_NAME && (lv === "score" || lv === SCORE_NAME.toLowerCase()))
46494
+ return SCORE_NAME;
46495
+ const tg = resolved.tagGroups.find((g) => g.name.toLowerCase() === lv);
46496
+ return tg ? tg.name : v;
46497
+ };
46498
+ const override = opts.activeGroup;
46499
+ let activeGroup;
46500
+ if (override !== void 0) {
46501
+ activeGroup = override === null ? null : matchColorGroup(override);
46502
+ } else if (resolved.directives.activeTag !== void 0) {
46503
+ activeGroup = matchColorGroup(resolved.directives.activeTag);
46504
+ } else {
46505
+ activeGroup = SCORE_NAME ?? (resolved.tagGroups.length > 0 ? resolved.tagGroups[0].name : null);
46506
+ }
46507
+ const activeIsScore = SCORE_NAME !== null && activeGroup === SCORE_NAME;
46508
+ const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
46304
46509
  const fillForScore = (s) => {
46305
46510
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
46306
46511
  const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
46307
- return mix(rampHue, isDark ? palette.surface : palette.bg, pct);
46512
+ return mix(rampHue, rampBase, pct);
46308
46513
  };
46309
46514
  const tagFill = (tags, groupName) => {
46310
46515
  if (!groupName) return null;
@@ -46318,40 +46523,40 @@ function layoutMap(resolved, data, size, opts) {
46318
46523
  (e) => e.value.toLowerCase() === val.toLowerCase()
46319
46524
  );
46320
46525
  if (!entry?.color) return null;
46321
- return shapeFill(palette, entry.color, isDark);
46526
+ return mix(
46527
+ entry.color,
46528
+ palette.bg,
46529
+ isDark ? TAG_TINT_DARK : TAG_TINT_LIGHT
46530
+ );
46531
+ };
46532
+ const regionFill = (r) => {
46533
+ if (activeIsScore) {
46534
+ return r.score !== void 0 ? fillForScore(r.score) : neutralFill;
46535
+ }
46536
+ return tagFill(r.tags, activeGroup) ?? neutralFill;
46322
46537
  };
46323
46538
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
46324
- const regionFeatures = [];
46325
- for (const r of resolved.regions) {
46326
- const f = r.layer === "us-state" ? usLayer?.get(r.iso) : worldLayer.get(r.iso);
46327
- if (f) regionFeatures.push(f);
46328
- }
46329
- const extentCorners = () => {
46539
+ const extentOutline = () => {
46330
46540
  const [[w, s], [e, n]] = resolved.extent;
46541
+ const N = 16;
46542
+ const coords = [];
46543
+ for (let i = 0; i <= N; i++) {
46544
+ const t = i / N;
46545
+ const lon = w + (e - w) * t;
46546
+ const lat = s + (n - s) * t;
46547
+ coords.push([lon, s], [lon, n], [w, lat], [e, lat]);
46548
+ }
46331
46549
  return {
46332
46550
  type: "Feature",
46333
46551
  properties: {},
46334
- geometry: {
46335
- type: "MultiPoint",
46336
- coordinates: [
46337
- [w, s],
46338
- [e, s],
46339
- [e, n],
46340
- [w, n]
46341
- ]
46342
- }
46552
+ geometry: { type: "MultiPoint", coordinates: coords }
46343
46553
  };
46344
46554
  };
46345
46555
  let fitFeatures;
46346
- if (resolved.projection === "albers-usa") {
46347
- if (regionFeatures.length > 0) fitFeatures = regionFeatures;
46348
- else if (usLayer) fitFeatures = [...usLayer.values()];
46349
- else {
46350
- const us = worldLayer.get("US");
46351
- fitFeatures = us ? [us] : [...worldLayer.values()];
46352
- }
46556
+ if (resolved.projection === "albers-usa" && usLayer) {
46557
+ fitFeatures = [...usLayer.entries()].filter(([iso]) => !US_NON_CONUS.has(iso)).map(([, f]) => f);
46353
46558
  } else {
46354
- fitFeatures = [extentCorners()];
46559
+ fitFeatures = [extentOutline()];
46355
46560
  }
46356
46561
  const fitTarget = { type: "FeatureCollection", features: fitFeatures };
46357
46562
  const projection = projectionFor(resolved.projection);
@@ -46360,32 +46565,311 @@ function layoutMap(resolved, data, size, opts) {
46360
46565
  if (centerLon > 180) centerLon -= 360;
46361
46566
  projection.rotate([-centerLon, 0]);
46362
46567
  }
46363
- projection.fitExtent(
46568
+ const TITLE_GAP = 16;
46569
+ let topPad = FIT_PAD;
46570
+ if (resolved.title && resolved.pois.length > 0) {
46571
+ const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
46572
+ topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP);
46573
+ }
46574
+ const fitBox = [
46575
+ [FIT_PAD, topPad],
46364
46576
  [
46365
- [FIT_PAD, FIT_PAD],
46366
- [
46367
- Math.max(FIT_PAD + 1, width - FIT_PAD),
46368
- Math.max(FIT_PAD + 1, height - FIT_PAD)
46369
- ]
46370
- ],
46371
- fitTarget
46372
- );
46373
- const path = geoPath(projection);
46374
- const project = (lon, lat) => projection([lon, lat]) ?? null;
46577
+ Math.max(FIT_PAD + 1, width - FIT_PAD),
46578
+ Math.max(topPad + 1, height - FIT_PAD)
46579
+ ]
46580
+ ];
46581
+ projection.fitExtent(fitBox, fitTarget);
46582
+ const fitGB = geoBounds2(fitTarget);
46583
+ const fitIsGlobal = fitGB[1][0] - fitGB[0][0] >= 270 || fitGB[1][1] - fitGB[0][1] >= 130;
46584
+ let path;
46585
+ let project;
46586
+ if (fitIsGlobal) {
46587
+ const cb = geoPath(projection).bounds(fitTarget);
46588
+ const bx0 = cb[0][0];
46589
+ const by0 = cb[0][1];
46590
+ const cw = cb[1][0] - bx0;
46591
+ const ch = cb[1][1] - by0;
46592
+ const ox = fitBox[0][0];
46593
+ const oy = fitBox[0][1];
46594
+ const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
46595
+ const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
46596
+ const stretch = (x, y) => [
46597
+ ox + (x - bx0) * sx,
46598
+ oy + (y - by0) * sy
46599
+ ];
46600
+ const baseProjection = projection;
46601
+ const tx = geoTransform({
46602
+ point(x, y) {
46603
+ const [px, py] = stretch(x, y);
46604
+ this.stream.point(px, py);
46605
+ }
46606
+ });
46607
+ path = geoPath({
46608
+ stream: (s) => baseProjection.stream(
46609
+ tx.stream(s)
46610
+ )
46611
+ });
46612
+ project = (lon, lat) => {
46613
+ const p = baseProjection([lon, lat]);
46614
+ return p ? stretch(p[0], p[1]) : null;
46615
+ };
46616
+ } else {
46617
+ path = geoPath(projection);
46618
+ project = (lon, lat) => projection([lon, lat]) ?? null;
46619
+ }
46620
+ const insets = [];
46621
+ const insetRegions = [];
46622
+ const insetLabelSeeds = [];
46623
+ if (resolved.projection === "albers-usa" && usLayer) {
46624
+ const PAD = 8;
46625
+ const GAP = 12;
46626
+ const yB = height - FIT_PAD;
46627
+ const BW = 8;
46628
+ const coast = /* @__PURE__ */ new Map();
46629
+ const addPt = (lon, lat) => {
46630
+ const p = projection([lon, lat]);
46631
+ if (!p) return;
46632
+ const bi = Math.floor(p[0] / BW);
46633
+ const cur = coast.get(bi);
46634
+ if (cur === void 0 || p[1] > cur) coast.set(bi, p[1]);
46635
+ };
46636
+ const walk = (co) => {
46637
+ if (Array.isArray(co) && typeof co[0] === "number")
46638
+ addPt(co[0], co[1]);
46639
+ else if (Array.isArray(co)) for (const c of co) walk(c);
46640
+ };
46641
+ for (const [iso, f] of usLayer) {
46642
+ if (US_NON_CONUS.has(iso)) continue;
46643
+ walk(f.geometry.coordinates);
46644
+ }
46645
+ const at = (x) => {
46646
+ const bi = Math.floor(x / BW);
46647
+ let y = -Infinity;
46648
+ for (let k = bi - 1; k <= bi + 1; k++) {
46649
+ const v = coast.get(k);
46650
+ if (v !== void 0 && v > y) y = v;
46651
+ }
46652
+ return y;
46653
+ };
46654
+ const coastTop = (x0, xr) => {
46655
+ const n = 24;
46656
+ const pts = [];
46657
+ let maxY = -Infinity;
46658
+ for (let i = 0; i <= n; i++) {
46659
+ const x = x0 + (xr - x0) * i / n;
46660
+ const y = at(x);
46661
+ if (y > -Infinity) {
46662
+ pts.push([x, y]);
46663
+ if (y > maxY) maxY = y;
46664
+ }
46665
+ }
46666
+ if (pts.length === 0) return () => yB - height * 0.42;
46667
+ let m = 0;
46668
+ if (pts.length >= 2) {
46669
+ let sx = 0, sy = 0, sxx = 0, sxy = 0;
46670
+ for (const [x, y] of pts) {
46671
+ sx += x;
46672
+ sy += y;
46673
+ sxx += x * x;
46674
+ sxy += x * y;
46675
+ }
46676
+ const den = pts.length * sxx - sx * sx;
46677
+ if (den !== 0) m = (pts.length * sxy - sx * sy) / den;
46678
+ }
46679
+ m = Math.max(-0.35, Math.min(0.35, m));
46680
+ let c = -Infinity;
46681
+ for (const [x, y] of pts) {
46682
+ const need = y - m * x + GAP;
46683
+ if (need > c) c = need;
46684
+ }
46685
+ return (x) => m * x + c;
46686
+ };
46687
+ const placeInset = (iso, proj, boxX, iwReq) => {
46688
+ const f = usLayer.get(iso);
46689
+ if (!f) return boxX;
46690
+ const x0 = boxX;
46691
+ const iw = Math.min(iwReq, width - FIT_PAD - x0 - 2 * PAD);
46692
+ if (iw < 24) return boxX;
46693
+ const xr = x0 + iw + 2 * PAD;
46694
+ const top = coastTop(x0, xr);
46695
+ const yL = top(x0);
46696
+ const yR = top(xr);
46697
+ proj.fitWidth(iw, f);
46698
+ const bb = geoPath(proj).bounds(f);
46699
+ const sh = Number.isFinite(bb[0][0]) ? bb[1][1] - bb[0][1] : iw;
46700
+ const needH = sh + 2 * PAD;
46701
+ let topFit = Math.max(yL, yR);
46702
+ const bottom = Math.min(topFit + needH, yB);
46703
+ if (bottom - topFit < needH) topFit = bottom - needH;
46704
+ const lift = topFit - Math.max(yL, yR);
46705
+ const topL = yL + lift;
46706
+ const topR = yR + lift;
46707
+ proj.fitExtent(
46708
+ [
46709
+ [x0 + PAD, topFit + PAD],
46710
+ [xr - PAD, bottom - PAD]
46711
+ ],
46712
+ f
46713
+ );
46714
+ const d = geoPath(proj)(f) ?? "";
46715
+ if (!d) return xr;
46716
+ const r = regionById.get(iso);
46717
+ let fill2 = neutralFill;
46718
+ let lineNumber = -1;
46719
+ if (r?.layer === "us-state") {
46720
+ fill2 = regionFill(r);
46721
+ lineNumber = r.lineNumber;
46722
+ }
46723
+ insets.push({
46724
+ x: x0,
46725
+ y: Math.min(topL, topR),
46726
+ w: xr - x0,
46727
+ h: bottom - Math.min(topL, topR),
46728
+ points: [
46729
+ [x0, topL],
46730
+ [xr, topR],
46731
+ [xr, bottom],
46732
+ [x0, bottom]
46733
+ ]
46734
+ });
46735
+ insetRegions.push({
46736
+ id: iso,
46737
+ d,
46738
+ fill: fill2,
46739
+ stroke: regionStroke,
46740
+ lineNumber,
46741
+ layer: "us-state",
46742
+ ...r?.score !== void 0 && { score: r.score },
46743
+ ...r && Object.keys(r.tags).length > 0 && { tags: r.tags }
46744
+ });
46745
+ const ctr = geoPath(proj).centroid(f);
46746
+ if (Number.isFinite(ctr[0])) {
46747
+ const name = f.properties?.name ?? iso;
46748
+ insetLabelSeeds.push({ x: ctr[0], y: ctr[1], iso, name, lineNumber });
46749
+ }
46750
+ return xr;
46751
+ };
46752
+ const akRight = placeInset(
46753
+ "US-AK",
46754
+ alaskaProjection(),
46755
+ FIT_PAD,
46756
+ width * 0.15
46757
+ );
46758
+ placeInset("US-HI", hawaiiProjection(), akRight + 24, width * 0.1);
46759
+ }
46760
+ const conusFit = resolved.projection === "albers-usa" && !!usLayer;
46761
+ const cullExtent = conusFit ? geoBounds2(fitTarget) : resolved.extent;
46762
+ const [[exW, exS], [exE, exN]] = cullExtent;
46763
+ const lonSpan = exE - exW;
46764
+ const latSpan = exN - exS;
46765
+ const isGlobalView = lonSpan >= 270 || latSpan >= 130;
46766
+ const padLon = Math.max(8, lonSpan * 0.35);
46767
+ const padLat = Math.max(8, latSpan * 0.35);
46768
+ const vW = exW - padLon;
46769
+ const vE = exE + padLon;
46770
+ const vS = exS - padLat;
46771
+ const vN = exN + padLat;
46772
+ const vLonCenter = (exW + exE) / 2;
46773
+ const normLon = (lon) => {
46774
+ let L = lon;
46775
+ while (L < vLonCenter - 180) L += 360;
46776
+ while (L > vLonCenter + 180) L -= 360;
46777
+ return L;
46778
+ };
46779
+ const ringOverlapsView = (ring) => {
46780
+ let anyIn = false;
46781
+ let loMin = Infinity, loMax = -Infinity, laMin = Infinity, laMax = -Infinity, rawMin = Infinity, rawMax = -Infinity;
46782
+ for (const [rawLon, lat] of ring) {
46783
+ const lon = normLon(rawLon);
46784
+ if (lon >= vW && lon <= vE && lat >= vS && lat <= vN) anyIn = true;
46785
+ if (lon < loMin) loMin = lon;
46786
+ if (lon > loMax) loMax = lon;
46787
+ if (rawLon < rawMin) rawMin = rawLon;
46788
+ if (rawLon > rawMax) rawMax = rawLon;
46789
+ if (lat < laMin) laMin = lat;
46790
+ if (lat > laMax) laMax = lat;
46791
+ }
46792
+ if (loMax - loMin > 270) return false;
46793
+ if (rawMax - rawMin > 180 && loMax - loMin < 90) return false;
46794
+ if (anyIn) return true;
46795
+ if (loMax - loMin > 180) return false;
46796
+ return !(loMax < vW || loMin > vE || laMax < vS || laMin > vN);
46797
+ };
46798
+ const cullFeatureToView = (f) => {
46799
+ if (isGlobalView) return f;
46800
+ const g = f.geometry;
46801
+ if (!g) return f;
46802
+ if (g.type === "Polygon") {
46803
+ const ring = g.coordinates[0];
46804
+ return ringOverlapsView(ring) ? f : null;
46805
+ }
46806
+ if (g.type === "MultiPolygon") {
46807
+ const polys = g.coordinates;
46808
+ const keep = polys.filter(
46809
+ (p) => ringOverlapsView(p[0])
46810
+ );
46811
+ if (!keep.length) return null;
46812
+ if (keep.length === polys.length) return f;
46813
+ return { ...f, geometry: { ...g, coordinates: keep } };
46814
+ }
46815
+ return f;
46816
+ };
46817
+ const SEAM_SLIVER_MAX_SPAN = 100;
46818
+ const ringIsFrameFiller = (ring) => {
46819
+ const lons = ring.map(([lon]) => lon).sort((a, b) => a - b);
46820
+ if (lons.length < 2) return false;
46821
+ let maxGap = -1;
46822
+ let gapIdx = 0;
46823
+ for (let i = 1; i < lons.length; i++) {
46824
+ const g = lons[i] - lons[i - 1];
46825
+ if (g > maxGap) {
46826
+ maxGap = g;
46827
+ gapIdx = i;
46828
+ }
46829
+ }
46830
+ const wrapGap = lons[0] + 360 - lons[lons.length - 1];
46831
+ if (wrapGap >= maxGap) return false;
46832
+ const span = 360 - maxGap;
46833
+ const east = lons[gapIdx - 1] + 360;
46834
+ return east > 180 && span < SEAM_SLIVER_MAX_SPAN;
46835
+ };
46836
+ const dropFrameFillers = (f) => {
46837
+ const g = f.geometry;
46838
+ if (!g) return f;
46839
+ if (g.type === "Polygon") {
46840
+ const ring = g.coordinates[0];
46841
+ return ringIsFrameFiller(ring) ? null : f;
46842
+ }
46843
+ if (g.type === "MultiPolygon") {
46844
+ const polys = g.coordinates;
46845
+ const keep = polys.filter(
46846
+ (p) => !ringIsFrameFiller(p[0])
46847
+ );
46848
+ if (!keep.length) return null;
46849
+ if (keep.length === polys.length) return f;
46850
+ return { ...f, geometry: { ...g, coordinates: keep } };
46851
+ }
46852
+ return f;
46853
+ };
46375
46854
  const regions = [];
46376
- const pushRegionLayer = (layerFeatures, layerKind) => {
46855
+ const pushRegionLayer = (layerFeatures, layerKind, shouldCull) => {
46377
46856
  for (const [iso, f] of layerFeatures) {
46378
- const d = path(f) ?? "";
46379
- if (!d) continue;
46857
+ if (layerKind === "us-state" && usContext && INSET_STATES.has(iso))
46858
+ continue;
46859
+ if (layerKind === "country" && usContext && iso === "US") continue;
46380
46860
  const r = regionById.get(iso);
46861
+ const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
46862
+ if (!viewF) continue;
46863
+ const d = path(viewF) ?? "";
46864
+ if (!d) continue;
46381
46865
  const isThisLayer = r?.layer === layerKind;
46382
- let fill2 = neutralFill;
46866
+ const isForeign = layerKind === "country" && usContext && iso !== "US";
46867
+ let fill2 = isForeign ? foreignFill : neutralFill;
46383
46868
  let label;
46384
46869
  let lineNumber = -1;
46385
46870
  let layer = "base";
46386
46871
  if (isThisLayer) {
46387
- if (r.score !== void 0) fill2 = fillForScore(r.score);
46388
- else fill2 = tagFill(r.tags, activeGroup) ?? neutralFill;
46872
+ fill2 = regionFill(r);
46389
46873
  lineNumber = r.lineNumber;
46390
46874
  layer = layerKind;
46391
46875
  label = r.name;
@@ -46397,12 +46881,42 @@ function layoutMap(resolved, data, size, opts) {
46397
46881
  stroke: regionStroke,
46398
46882
  lineNumber,
46399
46883
  layer,
46400
- ...label !== void 0 && { label }
46884
+ ...label !== void 0 && { label },
46885
+ ...isThisLayer && r.score !== void 0 && { score: r.score },
46886
+ ...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
46401
46887
  });
46402
46888
  }
46403
46889
  };
46404
- pushRegionLayer(worldLayer, "country");
46405
- if (usLayer) pushRegionLayer(usLayer, "us-state");
46890
+ pushRegionLayer(worldLayer, "country", !isGlobalView);
46891
+ if (usLayer) pushRegionLayer(usLayer, "us-state", !conusFit && !isGlobalView);
46892
+ const lakesTopo = usCrisp && data.naLakes ? data.naLakes : data.lakes;
46893
+ if (lakesTopo) {
46894
+ for (const [, f] of decodeLayer(lakesTopo)) {
46895
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
46896
+ if (!viewF) continue;
46897
+ const d = path(viewF) ?? "";
46898
+ if (!d) continue;
46899
+ regions.push({
46900
+ id: "lake",
46901
+ d,
46902
+ fill: water,
46903
+ stroke: "none",
46904
+ lineNumber: -1,
46905
+ layer: "base"
46906
+ });
46907
+ }
46908
+ }
46909
+ const riverColor = water;
46910
+ const rivers = [];
46911
+ if (data.rivers) {
46912
+ for (const [, f] of decodeLayer(data.rivers)) {
46913
+ const viewF = isGlobalView ? dropFrameFillers(f) : cullFeatureToView(f);
46914
+ if (!viewF) continue;
46915
+ const d = path(viewF) ?? "";
46916
+ if (!d) continue;
46917
+ rivers.push({ d, color: riverColor, width: RIVER_WIDTH });
46918
+ }
46919
+ }
46406
46920
  const sizeVals = resolved.pois.map((p) => Number(p.meta["size"])).filter((n) => Number.isFinite(n) && n > 0);
46407
46921
  const sizeMin = sizeVals.length ? Math.min(...sizeVals) : 0;
46408
46922
  const sizeMax = sizeVals.length ? Math.max(...sizeVals) : 0;
@@ -46423,8 +46937,8 @@ function layoutMap(resolved, data, size, opts) {
46423
46937
  if (hex) return { fill: hex, stroke: mix(hex, palette.text, 18) };
46424
46938
  }
46425
46939
  return {
46426
- fill: palette.accent,
46427
- stroke: mix(palette.accent, palette.text, 18)
46940
+ fill: palette.colors.orange,
46941
+ stroke: mix(palette.colors.orange, palette.text, 18)
46428
46942
  };
46429
46943
  };
46430
46944
  const routeNumberById = /* @__PURE__ */ new Map();
@@ -46462,7 +46976,7 @@ function layoutMap(resolved, data, size, opts) {
46462
46976
  cy += Math.sin(ang) * COLO_R;
46463
46977
  }
46464
46978
  const { fill: fill2, stroke: stroke2 } = poiFill(e.p);
46465
- poiScreen.set(e.p.id, { cx, cy });
46979
+ poiScreen.set(e.p.id, { cx, cy, r: radiusFor(e.p) });
46466
46980
  const num = routeNumberById.get(e.p.id);
46467
46981
  pois.push({
46468
46982
  id: e.p.id,
@@ -46479,17 +46993,36 @@ function layoutMap(resolved, data, size, opts) {
46479
46993
  });
46480
46994
  }
46481
46995
  const legs = [];
46996
+ const RIM_GAP = 1.5;
46482
46997
  const legPath = (a, b, curved, offset) => {
46483
- if (!curved && offset === 0) return `M${a.cx},${a.cy}L${b.cx},${b.cy}`;
46484
46998
  const mx = (a.cx + b.cx) / 2;
46485
46999
  const my = (a.cy + b.cy) / 2;
46486
47000
  const dx = b.cx - a.cx;
46487
47001
  const dy = b.cy - a.cy;
46488
47002
  const len = Math.hypot(dx, dy) || 1;
47003
+ const trimA = Math.min(a.r + RIM_GAP, len * 0.45);
47004
+ const trimB = Math.min(b.r + RIM_GAP, len * 0.45);
47005
+ if (!curved && offset === 0) {
47006
+ const ux = dx / len;
47007
+ const uy = dy / len;
47008
+ const ax2 = a.cx + ux * trimA;
47009
+ const ay2 = a.cy + uy * trimA;
47010
+ const bx2 = b.cx - ux * trimB;
47011
+ const by2 = b.cy - uy * trimB;
47012
+ return `M${ax2},${ay2}L${bx2},${by2}`;
47013
+ }
46489
47014
  const nx = -dy / len;
46490
47015
  const ny = dx / len;
46491
47016
  const bow = offset !== 0 ? offset : len * ARC_CURVE_FRAC;
46492
- return `M${a.cx},${a.cy}Q${mx + nx * bow},${my + ny * bow} ${b.cx},${b.cy}`;
47017
+ const px = mx + nx * bow;
47018
+ const py = my + ny * bow;
47019
+ const ta = Math.hypot(px - a.cx, py - a.cy) || 1;
47020
+ const tb = Math.hypot(b.cx - px, b.cy - py) || 1;
47021
+ const ax = a.cx + (px - a.cx) / ta * trimA;
47022
+ const ay = a.cy + (py - a.cy) / ta * trimA;
47023
+ const bx = b.cx - (b.cx - px) / tb * trimB;
47024
+ const by = b.cy - (b.cy - py) / tb * trimB;
47025
+ return `M${ax},${ay}Q${px},${py} ${bx},${by}`;
46493
47026
  };
46494
47027
  for (const rt of resolved.routes) {
46495
47028
  const curved = rt.meta["style"] === "arc";
@@ -46500,7 +47033,7 @@ function layoutMap(resolved, data, size, opts) {
46500
47033
  legs.push({
46501
47034
  d: legPath(a, b, curved, 0),
46502
47035
  width: W_MIN,
46503
- color: mix(palette.text, palette.bg, 55),
47036
+ color: mix(palette.text, palette.bg, 72),
46504
47037
  arrow: true,
46505
47038
  lineNumber: rt.lineNumber
46506
47039
  });
@@ -46536,7 +47069,7 @@ function layoutMap(resolved, data, size, opts) {
46536
47069
  legs.push({
46537
47070
  d: legPath(a, b, curved, offset),
46538
47071
  width: widthFor(e),
46539
- color: mix(palette.text, palette.bg, 45),
47072
+ color: mix(palette.text, palette.bg, 66),
46540
47073
  arrow: e.directed,
46541
47074
  lineNumber: e.lineNumber,
46542
47075
  ...e.label !== void 0 && {
@@ -46548,38 +47081,92 @@ function layoutMap(resolved, data, size, opts) {
46548
47081
  });
46549
47082
  }
46550
47083
  const labels = [];
46551
- const pinList = [];
46552
47084
  const obstacles = [];
46553
47085
  const markers = pois.map((p) => ({
46554
47086
  cx: p.cx,
46555
47087
  cy: p.cy,
46556
47088
  r: p.r
46557
47089
  }));
46558
- const collides = (rect) => markers.some((m) => rectCircleOverlap(rect, m)) || obstacles.some((o) => rectsOverlap(rect, o));
47090
+ const legSegments = [];
47091
+ for (const leg of legs) {
47092
+ const m = /^M(-?[\d.]+),(-?[\d.]+)(?:L(-?[\d.]+),(-?[\d.]+)|Q(-?[\d.]+),(-?[\d.]+) (-?[\d.]+),(-?[\d.]+))$/.exec(
47093
+ leg.d
47094
+ );
47095
+ if (!m) continue;
47096
+ const x0 = +m[1];
47097
+ const y0 = +m[2];
47098
+ if (m[3] !== void 0) {
47099
+ legSegments.push([x0, y0, +m[3], +m[4]]);
47100
+ } else {
47101
+ const cx = +m[5];
47102
+ const cy = +m[6];
47103
+ const ex = +m[7];
47104
+ const ey = +m[8];
47105
+ const N = 8;
47106
+ let px = x0;
47107
+ let py = y0;
47108
+ for (let i = 1; i <= N; i++) {
47109
+ const t = i / N;
47110
+ const u = 1 - t;
47111
+ const qx = u * u * x0 + 2 * u * t * cx + t * t * ex;
47112
+ const qy = u * u * y0 + 2 * u * t * cy + t * t * ey;
47113
+ legSegments.push([px, py, qx, qy]);
47114
+ px = qx;
47115
+ py = qy;
47116
+ }
47117
+ }
47118
+ }
47119
+ 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));
46559
47120
  const regionLabelMode = resolved.directives.regionLabels ?? "off";
47121
+ const LABEL_PADX = 6;
47122
+ const LABEL_PADY = 3;
47123
+ const labelW = (text) => measureLegendText(text, FONT) + 2 * LABEL_PADX;
47124
+ const labelH = FONT + 2 * LABEL_PADY;
47125
+ const pushRegionLabel = (x, y, text, fill2, lineNumber) => {
47126
+ const color = contrastText(
47127
+ fill2,
47128
+ palette.textOnFillLight,
47129
+ palette.textOnFillDark
47130
+ );
47131
+ const haloColor = color === palette.textOnFillLight ? palette.textOnFillDark : palette.textOnFillLight;
47132
+ labels.push({
47133
+ x,
47134
+ y,
47135
+ text,
47136
+ anchor: "middle",
47137
+ color,
47138
+ halo: true,
47139
+ haloColor,
47140
+ lineNumber
47141
+ });
47142
+ };
47143
+ const WORLD_LABEL_ANCHORS = {
47144
+ US: [-98.5, 39.5]
47145
+ // CONUS geographic centre (near Lebanon, Kansas)
47146
+ };
46560
47147
  if (regionLabelMode === "full" || regionLabelMode === "abbrev") {
46561
47148
  for (const r of regions) {
46562
47149
  if (r.layer === "base" || r.label === void 0) continue;
46563
47150
  const f = r.layer === "us-state" ? usLayer?.get(r.id) : worldLayer.get(r.id);
46564
47151
  if (!f) continue;
46565
47152
  const [[x0, y0], [x1, y1]] = path.bounds(f);
46566
- if ((x1 - x0) * (y1 - y0) < TINY_REGION_AREA) continue;
46567
- const c = path.centroid(f);
46568
- if (!Number.isFinite(c[0])) continue;
46569
47153
  const text = regionLabelMode === "abbrev" ? r.id.replace(/^US-/, "") : r.label;
46570
- labels.push({
46571
- x: c[0],
46572
- y: c[1],
47154
+ if (labelW(text) > x1 - x0 || labelH > y1 - y0) continue;
47155
+ const anchor = r.layer !== "us-state" ? WORLD_LABEL_ANCHORS[r.id] : void 0;
47156
+ const c = anchor ? project(anchor[0], anchor[1]) : path.centroid(f);
47157
+ if (!c || !Number.isFinite(c[0])) continue;
47158
+ pushRegionLabel(c[0], c[1], text, r.fill, r.lineNumber);
47159
+ }
47160
+ for (const seed of insetLabelSeeds) {
47161
+ const text = regionLabelMode === "abbrev" ? seed.iso.replace(/^US-/, "") : seed.name;
47162
+ const src = regionById.get(seed.iso);
47163
+ pushRegionLabel(
47164
+ seed.x,
47165
+ seed.y,
46573
47166
  text,
46574
- anchor: "middle",
46575
- color: contrastText(
46576
- r.fill,
46577
- palette.textOnFillLight,
46578
- palette.textOnFillDark
46579
- ),
46580
- halo: true,
46581
- lineNumber: r.lineNumber
46582
- });
47167
+ src ? regionFill(src) : neutralFill,
47168
+ seed.lineNumber
47169
+ );
46583
47170
  }
46584
47171
  }
46585
47172
  const poiLabelMode = resolved.directives.poiLabels ?? "auto";
@@ -46592,68 +47179,106 @@ function layoutMap(resolved, data, size, opts) {
46592
47179
  const src = poiById.get(p.id);
46593
47180
  return src?.label ?? src?.name ?? p.id;
46594
47181
  };
46595
- let pinCounter = 0;
46596
- for (const p of ordered) {
47182
+ const poiLabH = FONT * 1.25;
47183
+ const labelInfo = (p) => {
46597
47184
  const text = labelText(p);
46598
- const w = measureLegendText(text, FONT);
46599
- const h = FONT * 1.25;
46600
- const inline = { x: p.cx + p.r + 3, y: p.cy - h / 2, w, h };
46601
- if (!collides(inline)) {
46602
- obstacles.push(inline);
47185
+ return { text, w: measureLegendText(text, FONT) };
47186
+ };
47187
+ const pushInline = (p, text, w, side) => {
47188
+ const tx = side === "right" ? p.cx + p.r + 3 : p.cx - p.r - 3;
47189
+ obstacles.push({
47190
+ x: side === "right" ? tx : tx - w,
47191
+ y: p.cy - poiLabH / 2,
47192
+ w,
47193
+ h: poiLabH
47194
+ });
47195
+ labels.push({
47196
+ x: tx,
47197
+ y: p.cy + FONT / 3,
47198
+ text,
47199
+ anchor: side === "right" ? "start" : "end",
47200
+ color: palette.text,
47201
+ halo: true,
47202
+ haloColor: palette.bg,
47203
+ poiId: p.id,
47204
+ lineNumber: p.lineNumber
47205
+ });
47206
+ };
47207
+ const inlineFits = (p, w, side) => {
47208
+ const tx = side === "right" ? p.cx + p.r + 3 : p.cx - p.r - 3;
47209
+ const rect = {
47210
+ x: side === "right" ? tx : tx - w,
47211
+ y: p.cy - poiLabH / 2,
47212
+ w,
47213
+ h: poiLabH
47214
+ };
47215
+ return rect.x >= 0 && rect.x + rect.w <= width && !collides(rect);
47216
+ };
47217
+ const GROUP_R = 30;
47218
+ const groups = [];
47219
+ for (const p of ordered) {
47220
+ const near = groups.find(
47221
+ (g) => g.some((q) => Math.hypot(q.cx - p.cx, q.cy - p.cy) < GROUP_R)
47222
+ );
47223
+ if (near) near.push(p);
47224
+ else groups.push([p]);
47225
+ }
47226
+ const ROW_GAP2 = 3;
47227
+ const step = poiLabH + ROW_GAP2;
47228
+ const COL_GAP = 16;
47229
+ const placeColumn = (group) => {
47230
+ const items = group.map((p) => ({ p, ...labelInfo(p) })).sort((a, b) => a.p.cy - b.p.cy || (a.text < b.text ? -1 : 1));
47231
+ const left = Math.min(...items.map((o) => o.p.cx - o.p.r));
47232
+ const right = Math.max(...items.map((o) => o.p.cx + o.p.r));
47233
+ const cyMid = (Math.min(...items.map((o) => o.p.cy)) + Math.max(...items.map((o) => o.p.cy))) / 2;
47234
+ const maxW = Math.max(...items.map((o) => o.w));
47235
+ const side = right + COL_GAP + maxW <= width - 2 ? "right" : "left";
47236
+ const colX = side === "right" ? right + COL_GAP : left - COL_GAP;
47237
+ const totalH = items.length * step;
47238
+ let startY = cyMid - totalH / 2;
47239
+ startY = Math.max(2, Math.min(startY, height - totalH - 2));
47240
+ items.forEach((o, i) => {
47241
+ const rowCy = startY + i * step + step / 2;
47242
+ obstacles.push({
47243
+ x: side === "right" ? colX : colX - o.w,
47244
+ y: rowCy - poiLabH / 2,
47245
+ w: o.w,
47246
+ h: poiLabH
47247
+ });
46603
47248
  labels.push({
46604
- x: inline.x,
46605
- y: p.cy + FONT / 3,
46606
- text,
46607
- anchor: "start",
47249
+ x: colX,
47250
+ y: rowCy + FONT / 3,
47251
+ text: o.text,
47252
+ anchor: side === "right" ? "start" : "end",
46608
47253
  color: palette.text,
46609
47254
  halo: true,
46610
- lineNumber: p.lineNumber
47255
+ haloColor: palette.bg,
47256
+ leader: {
47257
+ x1: o.p.cx,
47258
+ y1: o.p.cy,
47259
+ x2: side === "right" ? colX - 2 : colX + 2,
47260
+ y2: rowCy
47261
+ },
47262
+ leaderColor: o.p.fill,
47263
+ poiId: o.p.id,
47264
+ lineNumber: o.p.lineNumber
46611
47265
  });
46612
- continue;
46613
- }
46614
- let placed = false;
46615
- for (let k = 1; k <= 2 && !placed; k++) {
46616
- for (const [dx, dy] of RING_DIRS) {
46617
- const cx = p.cx + dx * LEADER_STEP * k;
46618
- const cy = p.cy + dy * LEADER_STEP * k;
46619
- const rect = {
46620
- x: dx >= 0 ? cx : cx - w,
46621
- y: cy - h / 2,
46622
- w,
46623
- h
46624
- };
46625
- if (rect.x < 0 || rect.x + rect.w > width || rect.y < 0 || rect.y + rect.h > height) {
46626
- continue;
46627
- }
46628
- if (collides(rect)) continue;
46629
- obstacles.push(rect);
46630
- labels.push({
46631
- x: cx,
46632
- y: cy + FONT / 3,
46633
- text,
46634
- anchor: dx >= 0 ? "start" : "end",
46635
- color: palette.text,
46636
- halo: true,
46637
- leader: { x1: p.cx, y1: p.cy, x2: cx, y2: cy },
46638
- lineNumber: p.lineNumber
46639
- });
46640
- placed = true;
46641
- break;
47266
+ });
47267
+ };
47268
+ for (const g of groups) {
47269
+ if (g.length === 1) {
47270
+ const p = g[0];
47271
+ const { text, w } = labelInfo(p);
47272
+ if (inlineFits(p, w, "right")) {
47273
+ pushInline(p, text, w, "right");
47274
+ continue;
47275
+ }
47276
+ if (inlineFits(p, w, "left")) {
47277
+ pushInline(p, text, w, "left");
47278
+ continue;
46642
47279
  }
46643
47280
  }
46644
- if (placed) continue;
46645
- pinCounter += 1;
46646
- pinList.push({ pin: pinCounter, label: text });
46647
- labels.push({
46648
- x: p.cx + p.r + 2,
46649
- y: p.cy - p.r,
46650
- text: String(pinCounter),
46651
- anchor: "start",
46652
- color: palette.text,
46653
- halo: true,
46654
- pin: pinCounter,
46655
- lineNumber: p.lineNumber
46656
- });
47281
+ placeColumn(g);
46657
47282
  }
46658
47283
  }
46659
47284
  let legend = null;
@@ -46662,8 +47287,7 @@ function layoutMap(resolved, data, size, opts) {
46662
47287
  name: g.name,
46663
47288
  entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
46664
47289
  }));
46665
- const hasAnything = tagGroups.length > 0 || hasRamp || sizeVals.length > 0 || weightVals.length > 0;
46666
- if (hasAnything) {
47290
+ if (tagGroups.length > 0 || hasRamp) {
46667
47291
  legend = {
46668
47292
  tagGroups,
46669
47293
  activeGroup,
@@ -46674,20 +47298,9 @@ function layoutMap(resolved, data, size, opts) {
46674
47298
  },
46675
47299
  min: rampMin,
46676
47300
  max: rampMax,
46677
- hue: rampHue
47301
+ hue: rampHue,
47302
+ base: rampBase
46678
47303
  }
46679
- },
46680
- ...sizeVals.length > 0 && {
46681
- size: {
46682
- ...resolved.directives.sizeMetric !== void 0 && {
46683
- metric: resolved.directives.sizeMetric
46684
- },
46685
- min: sizeMin,
46686
- max: sizeMax
46687
- }
46688
- },
46689
- ...weightVals.length > 0 && {
46690
- weight: { min: wMin, max: wMax }
46691
47304
  }
46692
47305
  };
46693
47306
  }
@@ -46695,26 +47308,28 @@ function layoutMap(resolved, data, size, opts) {
46695
47308
  return {
46696
47309
  width,
46697
47310
  height,
46698
- background: palette.bg,
47311
+ background: water,
46699
47312
  title: resolved.title,
46700
47313
  ...resolved.subtitle !== void 0 && { subtitle: resolved.subtitle },
46701
47314
  ...resolved.caption !== void 0 && { caption: resolved.caption },
46702
47315
  regions,
47316
+ rivers,
46703
47317
  legs,
46704
47318
  pois,
46705
47319
  labels,
46706
- pinList,
46707
- legend
47320
+ legend,
47321
+ insets,
47322
+ insetRegions
46708
47323
  };
46709
47324
  }
46710
- 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;
47325
+ 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;
46711
47326
  var init_layout15 = __esm({
46712
47327
  "src/map/layout.ts"() {
46713
47328
  "use strict";
46714
47329
  init_color_utils();
46715
- init_tag_groups();
46716
47330
  init_label_layout();
46717
47331
  init_legend_constants();
47332
+ init_title_constants();
46718
47333
  FIT_PAD = 24;
46719
47334
  RAMP_FLOOR = 15;
46720
47335
  R_DEFAULT = 6;
@@ -46723,23 +47338,32 @@ var init_layout15 = __esm({
46723
47338
  W_MIN = 1.25;
46724
47339
  W_MAX = 8;
46725
47340
  FONT = 11;
46726
- LEADER_STEP = 14;
46727
47341
  COLO_EPS = 1.5;
47342
+ LAND_TINT_LIGHT = 58;
47343
+ LAND_TINT_DARK = 75;
47344
+ TAG_TINT_LIGHT = 60;
47345
+ TAG_TINT_DARK = 68;
47346
+ WATER_TINT = 55;
47347
+ RIVER_WIDTH = 1.3;
47348
+ FOREIGN_TINT_LIGHT = 30;
47349
+ FOREIGN_TINT_DARK = 62;
46728
47350
  COLO_R = 9;
46729
47351
  GOLDEN_ANGLE = 2.399963229728653;
46730
47352
  FAN_STEP = 16;
46731
- TINY_REGION_AREA = 600;
46732
47353
  ARC_CURVE_FRAC = 0.18;
46733
- RING_DIRS = [
46734
- [1, 0],
46735
- [0, 1],
46736
- [-1, 0],
46737
- [0, -1],
46738
- [1, 1],
46739
- [-1, 1],
46740
- [-1, -1],
46741
- [1, -1]
46742
- ];
47354
+ usConusProjection = () => geoConicEqualArea().parallels([29.5, 45.5]).rotate([96, 0]);
47355
+ alaskaProjection = () => geoConicEqualArea().rotate([154, 0]).center([-2, 58.5]).parallels([55, 65]);
47356
+ hawaiiProjection = () => geoMercator();
47357
+ INSET_STATES = /* @__PURE__ */ new Set(["US-AK", "US-HI"]);
47358
+ US_NON_CONUS = /* @__PURE__ */ new Set([
47359
+ "US-AK",
47360
+ "US-HI",
47361
+ "US-AS",
47362
+ "US-GU",
47363
+ "US-MP",
47364
+ "US-PR",
47365
+ "US-VI"
47366
+ ]);
46743
47367
  }
46744
47368
  });
46745
47369
 
@@ -46750,7 +47374,7 @@ __export(renderer_exports16, {
46750
47374
  renderMapForExport: () => renderMapForExport
46751
47375
  });
46752
47376
  import * as d3Selection18 from "d3-selection";
46753
- function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims) {
47377
+ function renderMap(container, resolved, data, palette, isDark, onClickItem, exportDims, activeGroupOverride) {
46754
47378
  d3Selection18.select(container).selectAll(":not([data-d3-tooltip])").remove();
46755
47379
  const width = exportDims?.width ?? container.clientWidth;
46756
47380
  const height = exportDims?.height ?? container.clientHeight;
@@ -46761,27 +47385,29 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
46761
47385
  { width, height },
46762
47386
  {
46763
47387
  palette,
46764
- isDark
47388
+ isDark,
47389
+ ...activeGroupOverride !== void 0 && {
47390
+ activeGroup: activeGroupOverride
47391
+ }
46765
47392
  }
46766
47393
  );
46767
- 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);
47394
+ 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);
46768
47395
  svg.append("rect").attr("width", width).attr("height", height).attr("fill", layout.background);
46769
- const arrowColor = mix(palette.text, palette.bg, 50);
46770
47396
  const defs = svg.append("defs");
46771
- 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);
46772
- const haloColor = layout.background;
46773
- if (layout.title) {
46774
- 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);
46775
- }
46776
- if (layout.subtitle) {
46777
- 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);
46778
- }
46779
- if (layout.caption) {
46780
- 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);
46781
- }
47397
+ const arrowSize = (w) => Math.min(15, 7 + w * 0.95);
47398
+ const haloColor = palette.bg;
46782
47399
  const gRegions = svg.append("g").attr("class", "dgmo-map-regions");
46783
- for (const r of layout.regions) {
46784
- const p = gRegions.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", 0.5);
47400
+ const drawRegion = (g, r, strokeWidth) => {
47401
+ const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
47402
+ if (r.layer !== "base") {
47403
+ p.classed("dgmo-map-region", true).attr("data-region", r.id);
47404
+ if (r.score !== void 0) p.attr("data-score", r.score);
47405
+ if (r.tags) {
47406
+ for (const [group, value] of Object.entries(r.tags)) {
47407
+ p.attr(`data-tag-${group.toLowerCase()}`, value.toLowerCase());
47408
+ }
47409
+ }
47410
+ }
46785
47411
  if (r.lineNumber >= 0) {
46786
47412
  p.attr("data-line-number", r.lineNumber);
46787
47413
  if (onClickItem) {
@@ -46791,11 +47417,31 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
46791
47417
  );
46792
47418
  }
46793
47419
  }
47420
+ };
47421
+ for (const r of layout.regions) drawRegion(gRegions, r, 0.5);
47422
+ if (layout.rivers.length) {
47423
+ const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
47424
+ for (const r of layout.rivers) {
47425
+ gRivers.append("path").attr("d", r.d).attr("stroke", r.color).attr("stroke-width", r.width).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
47426
+ }
47427
+ }
47428
+ if (layout.insets.length) {
47429
+ const insetG = svg.append("g").attr("class", "dgmo-map-insets");
47430
+ for (const box of layout.insets) {
47431
+ const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
47432
+ 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");
47433
+ }
47434
+ for (const r of layout.insetRegions) drawRegion(insetG, r, 0.5);
46794
47435
  }
46795
47436
  const gLegs = svg.append("g").attr("class", "dgmo-map-legs").attr("fill", "none");
46796
- for (const leg of layout.legs) {
47437
+ layout.legs.forEach((leg, i) => {
46797
47438
  const p = gLegs.append("path").attr("d", leg.d).attr("stroke", leg.color).attr("stroke-width", leg.width).attr("stroke-linecap", "round");
46798
- if (leg.arrow) p.attr("marker-end", "url(#dgmo-map-arrow)");
47439
+ if (leg.arrow) {
47440
+ const id = `dgmo-map-arrow-${i}`;
47441
+ const s = arrowSize(leg.width);
47442
+ 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);
47443
+ p.attr("marker-end", `url(#${id})`);
47444
+ }
46799
47445
  if (leg.label !== void 0 && leg.labelX !== void 0) {
46800
47446
  emitText(
46801
47447
  gLegs,
@@ -46809,13 +47455,13 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
46809
47455
  LABEL_FONT - 1
46810
47456
  );
46811
47457
  }
46812
- }
47458
+ });
46813
47459
  const gPois = svg.append("g").attr("class", "dgmo-map-pois");
46814
47460
  for (const poi of layout.pois) {
46815
47461
  if (poi.isOrigin) {
46816
47462
  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);
46817
47463
  }
46818
- 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);
47464
+ 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);
46819
47465
  if (onClickItem) {
46820
47466
  c.style("cursor", "pointer").on(
46821
47467
  "click",
@@ -46839,48 +47485,66 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
46839
47485
  const gLabels = svg.append("g").attr("class", "dgmo-map-labels");
46840
47486
  for (const lab of layout.labels) {
46841
47487
  if (lab.leader) {
46842
- 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);
47488
+ 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(
47489
+ "stroke",
47490
+ lab.leaderColor ?? mix(palette.textMuted, palette.bg, 60)
47491
+ ).attr("stroke-width", lab.leaderColor ? 1 : 0.75);
47492
+ if (lab.poiId !== void 0) line12.attr("data-poi", lab.poiId);
46843
47493
  }
46844
- if (lab.pin !== void 0) {
46845
- 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);
46846
- }
46847
- emitText(
47494
+ const t = emitText(
46848
47495
  gLabels,
46849
47496
  lab.x,
46850
47497
  lab.y,
46851
47498
  lab.text,
46852
47499
  lab.anchor,
46853
47500
  lab.color,
46854
- haloColor,
47501
+ lab.haloColor,
46855
47502
  lab.halo,
46856
47503
  LABEL_FONT
46857
47504
  );
46858
- }
46859
- if (layout.pinList.length > 0) {
46860
- const gPins = svg.append("g").attr("class", "dgmo-map-pin-list").attr(
46861
- "transform",
46862
- `translate(12, ${height - layout.pinList.length * 14 - 8})`
46863
- );
46864
- layout.pinList.forEach((entry, i) => {
46865
- 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}`);
46866
- });
47505
+ if (lab.poiId !== void 0) {
47506
+ t.attr("data-poi", lab.poiId).style("cursor", "default");
47507
+ }
46867
47508
  }
46868
47509
  if (layout.legend) {
46869
47510
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
46870
47511
  const legendG = svg.append("g").attr("class", "dgmo-map-legend").attr("transform", `translate(0, ${legendY})`);
46871
- const groups = layout.legend.tagGroups.filter((g) => g.entries.length > 0);
47512
+ const ramp = layout.legend.ramp;
47513
+ const scoreGroup = ramp ? {
47514
+ name: ramp.metric?.trim() || "Score",
47515
+ entries: [],
47516
+ gradient: {
47517
+ min: ramp.min,
47518
+ max: ramp.max,
47519
+ hue: ramp.hue,
47520
+ base: ramp.base
47521
+ }
47522
+ } : null;
47523
+ const tagGroups = layout.legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
47524
+ const groups = [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
46872
47525
  if (groups.length > 0) {
46873
47526
  const config = {
46874
- groups: groups.map((g) => ({ name: g.name, entries: [...g.entries] })),
47527
+ groups,
46875
47528
  position: { placement: "top-center", titleRelation: "below-title" },
46876
47529
  mode: exportDims ? "export" : "preview",
46877
- showEmptyGroups: false
47530
+ showEmptyGroups: false,
47531
+ // Keep inactive siblings visible as pills so the user can click to flip
47532
+ // the active colouring dimension (preview only — export shows just the
47533
+ // active group).
47534
+ showInactivePills: true
46878
47535
  };
46879
47536
  const state = { activeGroup: layout.legend.activeGroup };
46880
47537
  renderLegendD3(legendG, config, state, palette, isDark, void 0, width);
46881
47538
  }
46882
- const pinGap = layout.pinList.length ? layout.pinList.length * 14 + 14 : 0;
46883
- emitExtraLegend(svg, layout, palette, height, pinGap);
47539
+ }
47540
+ if (layout.title) {
47541
+ 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);
47542
+ }
47543
+ if (layout.subtitle) {
47544
+ 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);
47545
+ }
47546
+ if (layout.caption) {
47547
+ 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);
46884
47548
  }
46885
47549
  }
46886
47550
  function renderMapForExport(container, resolved, data, palette, isDark, exportDims) {
@@ -46891,54 +47555,7 @@ function emitText(g, x, y, text, anchor, color, halo, withHalo, fontSize) {
46891
47555
  if (withHalo) {
46892
47556
  t.attr("paint-order", "stroke fill").attr("stroke", halo).attr("stroke-width", 3).attr("stroke-linejoin", "round").attr("stroke-opacity", 0.7);
46893
47557
  }
46894
- }
46895
- function emitExtraLegend(svg, layout, palette, height, bottomGap) {
46896
- const { legend } = layout;
46897
- if (!legend) return;
46898
- if (!legend.ramp && !legend.size && !legend.weight) return;
46899
- const blocks = [];
46900
- const g = svg.append("g").attr("class", "dgmo-map-legend-keys").attr("transform", `translate(12, ${height - 56 - bottomGap})`);
46901
- let xCursor = 0;
46902
- if (legend.ramp) {
46903
- const ramp = legend.ramp;
46904
- blocks.push(() => {
46905
- const block = g.append("g").attr("transform", `translate(${xCursor},0)`);
46906
- const gradId = "dgmo-map-ramp";
46907
- const grad = block.append("defs").append("linearGradient").attr("id", gradId).attr("x1", "0%").attr("x2", "100%");
46908
- grad.append("stop").attr("offset", "0%").attr("stop-color", mix(ramp.hue, palette.bg, 15));
46909
- grad.append("stop").attr("offset", "100%").attr("stop-color", ramp.hue);
46910
- block.append("rect").attr("width", 80).attr("height", 8).attr("fill", `url(#${gradId})`);
46911
- block.append("text").attr("x", 0).attr("y", 22).attr("font-size", 9).attr("fill", palette.textMuted).text(String(ramp.min));
46912
- 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));
46913
- if (ramp.metric) {
46914
- block.append("text").attr("x", 0).attr("y", -4).attr("font-size", 9).attr("fill", palette.textMuted).text(ramp.metric);
46915
- }
46916
- xCursor += 110;
46917
- });
46918
- }
46919
- if (legend.size) {
46920
- const sz = legend.size;
46921
- blocks.push(() => {
46922
- const block = g.append("g").attr("transform", `translate(${xCursor},0)`);
46923
- [3, 6, 10].forEach((r, i) => {
46924
- block.append("circle").attr("cx", i * 26 + r).attr("cy", 8).attr("r", r).attr("fill", "none").attr("stroke", palette.textMuted);
46925
- });
46926
- block.append("text").attr("x", 0).attr("y", -4).attr("font-size", 9).attr("fill", palette.textMuted).text(sz.metric ?? "size");
46927
- xCursor += 110;
46928
- });
46929
- }
46930
- if (legend.weight) {
46931
- const wt = legend.weight;
46932
- blocks.push(() => {
46933
- const block = g.append("g").attr("transform", `translate(${xCursor},0)`);
46934
- [1, 3, 6].forEach((w, i) => {
46935
- 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);
46936
- });
46937
- block.append("text").attr("x", 0).attr("y", -4).attr("font-size", 9).attr("fill", palette.textMuted).text(wt.metric ?? "weight");
46938
- xCursor += 110;
46939
- });
46940
- }
46941
- for (const draw of blocks) draw();
47558
+ return t;
46942
47559
  }
46943
47560
  var LABEL_FONT;
46944
47561
  var init_renderer16 = __esm({
@@ -57564,10 +58181,13 @@ var COMPLETION_REGISTRY = /* @__PURE__ */ new Map([
57564
58181
  // keywords, not directives; metadata keys (score/size/label) live in the
57565
58182
  // reserved-key registry.
57566
58183
  withGlobals({
57567
- region: { description: "Force a basemap/extent (world | us-states)" },
58184
+ region: {
58185
+ description: "Basemap: us-states (force US state mesh + scoping) | world (inert \u2014 already the default)",
58186
+ values: ["us-states", "world"]
58187
+ },
57568
58188
  projection: {
57569
58189
  description: "Override the auto projection",
57570
- values: ["natural-earth", "albers-usa", "mercator"]
58190
+ values: ["equirectangular", "natural-earth", "albers-usa", "mercator"]
57571
58191
  },
57572
58192
  metric: { description: "Label for the region score ramp" },
57573
58193
  "size-metric": { description: "Label for the POI size channel" },
@@ -59160,6 +59780,8 @@ export {
59160
59780
  looksLikeSitemap,
59161
59781
  looksLikeState,
59162
59782
  makeDgmoError,
59783
+ mapBackgroundColor,
59784
+ mapNeutralLandColor,
59163
59785
  matchesContiguously,
59164
59786
  measurePertAnalysisBlock,
59165
59787
  migrateContent,