@diagrammo/dgmo 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/advanced.cjs +238 -48
  2. package/dist/advanced.d.cts +17 -0
  3. package/dist/advanced.d.ts +17 -0
  4. package/dist/advanced.js +238 -48
  5. package/dist/auto.cjs +236 -42
  6. package/dist/auto.js +115 -115
  7. package/dist/auto.mjs +236 -42
  8. package/dist/cli.cjs +153 -153
  9. package/dist/editor.cjs +3 -0
  10. package/dist/editor.js +3 -0
  11. package/dist/highlight.cjs +3 -0
  12. package/dist/highlight.js +3 -0
  13. package/dist/index.cjs +232 -41
  14. package/dist/index.js +232 -41
  15. package/dist/internal.cjs +238 -48
  16. package/dist/internal.d.cts +17 -0
  17. package/dist/internal.d.ts +17 -0
  18. package/dist/internal.js +238 -48
  19. package/dist/map-data/PROVENANCE.json +1 -1
  20. package/dist/map-data/gazetteer.json +1 -1
  21. package/dist/map-data/mountain-ranges.json +1 -1
  22. package/dist/map-data/water-bodies.json +1 -1
  23. package/dist/map-data/world-coarse.json +1 -1
  24. package/dist/map-data/world-detail.json +1 -1
  25. package/docs/language-reference.md +35 -0
  26. package/gallery/fixtures/boxes-and-lines.dgmo +6 -4
  27. package/package.json +1 -1
  28. package/src/boxes-and-lines/parser.ts +39 -0
  29. package/src/boxes-and-lines/renderer.ts +171 -13
  30. package/src/boxes-and-lines/types.ts +9 -0
  31. package/src/completion.ts +4 -5
  32. package/src/d3.ts +12 -4
  33. package/src/editor/keywords.ts +3 -0
  34. package/src/map/data/PROVENANCE.json +1 -1
  35. package/src/map/data/README.md +6 -0
  36. package/src/map/data/gazetteer.json +1 -1
  37. package/src/map/data/mountain-ranges.json +1 -1
  38. package/src/map/data/water-bodies.json +1 -1
  39. package/src/map/data/world-coarse.json +1 -1
  40. package/src/map/data/world-detail.json +1 -1
  41. package/src/map/layout.ts +111 -18
  42. package/src/map/renderer.ts +95 -4
  43. package/src/utils/reserved-key-registry.ts +5 -3
package/dist/advanced.cjs CHANGED
@@ -917,9 +917,7 @@ var init_reserved_key_registry = __esm({
917
917
  BOXES_AND_LINES_REGISTRY = staticRegistry([
918
918
  "color",
919
919
  "description",
920
- "width",
921
- "split",
922
- "fanout"
920
+ "value"
923
921
  ]);
924
922
  TIMELINE_REGISTRY = staticRegistry([
925
923
  "color",
@@ -16873,6 +16871,21 @@ function parseBoxesAndLines(content) {
16873
16871
  }
16874
16872
  continue;
16875
16873
  }
16874
+ if (!contentStarted) {
16875
+ const metricMatch = trimmed.match(/^box-metric\s+(.+)$/i);
16876
+ if (metricMatch) {
16877
+ const { label, colorName } = peelTrailingColorName(
16878
+ metricMatch[1].trim()
16879
+ );
16880
+ result.boxMetric = label;
16881
+ if (colorName !== void 0) result.boxMetricColor = colorName;
16882
+ continue;
16883
+ }
16884
+ if (/^show-values$/i.test(trimmed)) {
16885
+ result.showValues = true;
16886
+ continue;
16887
+ }
16888
+ }
16876
16889
  if (!contentStarted) {
16877
16890
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
16878
16891
  if (optMatch) {
@@ -17251,6 +17264,19 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
17251
17264
  description = [metadata["description"]];
17252
17265
  delete metadata["description"];
17253
17266
  }
17267
+ let value;
17268
+ if (metadata["value"] !== void 0) {
17269
+ const raw = metadata["value"];
17270
+ const num = Number(raw);
17271
+ if (Number.isFinite(num)) {
17272
+ value = num;
17273
+ } else {
17274
+ diagnostics.push(
17275
+ makeDgmoError(lineNum, `value must be a number (got "${raw}")`, "error")
17276
+ );
17277
+ }
17278
+ delete metadata["value"];
17279
+ }
17254
17280
  if (split.alias) {
17255
17281
  nameAliasMap?.set(normalizeName(split.alias), label);
17256
17282
  }
@@ -17259,7 +17285,8 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
17259
17285
  label,
17260
17286
  lineNumber: lineNum,
17261
17287
  metadata,
17262
- ...description !== void 0 && { description }
17288
+ ...description !== void 0 && { description },
17289
+ ...value !== void 0 && { value }
17263
17290
  };
17264
17291
  }
17265
17292
  function splitTargetAndMeta(rest, metaAliasMap) {
@@ -26469,7 +26496,18 @@ function fitLabelToHeader(label, nodeWidth, maxLines) {
26469
26496
  const truncated = label.length > maxChars ? label.slice(0, maxChars - 1) + "\u2026" : label;
26470
26497
  return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
26471
26498
  }
26472
- function nodeColors(node, tagGroups, activeGroupName, palette, isDark, solid) {
26499
+ function nodeColors(node, tagGroups, activeGroupName, palette, isDark, value, solid) {
26500
+ const neutralFill = mix(palette.bg, palette.text, isDark ? 90 : 95);
26501
+ if (value.active) {
26502
+ const fill3 = node.value !== void 0 ? value.fillForValue(node.value) : neutralFill;
26503
+ const stroke3 = value.hue;
26504
+ const text2 = contrastText(
26505
+ fill3,
26506
+ palette.textOnFillLight,
26507
+ palette.textOnFillDark
26508
+ );
26509
+ return { fill: fill3, stroke: stroke3, text: text2 };
26510
+ }
26473
26511
  const tagColor = resolveTagColor(
26474
26512
  node.metadata,
26475
26513
  [...tagGroups],
@@ -26578,25 +26616,65 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26578
26616
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26579
26617
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26580
26618
  const sTitleY = sctx.structural(TITLE_Y);
26619
+ const nodeValues = parsed.nodes.filter((n) => n.value !== void 0).map((n) => n.value);
26620
+ const hasRamp = nodeValues.length > 0;
26621
+ const allNonNegative = hasRamp && nodeValues.every((v) => v >= 0);
26622
+ const rampMin = allNonNegative ? 0 : Math.min(...nodeValues);
26623
+ const rampMax = Math.max(...nodeValues);
26624
+ const rampHue = resolveColor(parsed.boxMetricColor ?? "", palette) ?? palette.primary;
26625
+ const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
26626
+ const fillForValue = (v) => {
26627
+ const t = rampMax > rampMin ? (v - rampMin) / (rampMax - rampMin) : 1;
26628
+ const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
26629
+ return mix(rampHue, rampBase, pct);
26630
+ };
26631
+ const VALUE_NAME = hasRamp ? parsed.boxMetric?.trim() || "Value" : null;
26632
+ const matchColorGroup = (v) => {
26633
+ const lv = v.trim().toLowerCase();
26634
+ if (lv === "none") return null;
26635
+ const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
26636
+ if (tg) return tg.name;
26637
+ if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
26638
+ return v;
26639
+ };
26640
+ const override = activeTagGroup;
26641
+ let activeGroup;
26642
+ if (override !== void 0) {
26643
+ activeGroup = override === null ? null : matchColorGroup(override);
26644
+ } else if (parsed.options["active-tag"] !== void 0) {
26645
+ activeGroup = matchColorGroup(parsed.options["active-tag"]);
26646
+ } else {
26647
+ activeGroup = VALUE_NAME ?? (parsed.tagGroups.length > 0 ? parsed.tagGroups[0].name : null);
26648
+ }
26649
+ const activeIsValue = VALUE_NAME !== null && activeGroup === VALUE_NAME;
26650
+ const valueGroup = VALUE_NAME !== null ? {
26651
+ name: VALUE_NAME,
26652
+ entries: [],
26653
+ gradient: {
26654
+ min: rampMin,
26655
+ max: rampMax,
26656
+ hue: rampHue,
26657
+ base: rampBase
26658
+ }
26659
+ } : null;
26660
+ const legendGroups = [
26661
+ ...valueGroup ? [valueGroup] : [],
26662
+ ...parsed.tagGroups
26663
+ ];
26581
26664
  const reserveHasDescriptions = parsed.nodes.some(
26582
26665
  (n) => n.description && n.description.length > 0
26583
26666
  );
26584
- const willRenderLegend = parsed.tagGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26667
+ const willRenderLegend = legendGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26585
26668
  const sLegendHeight = willRenderLegend ? sctx.structural(
26586
26669
  getMaxLegendReservedHeight(
26587
26670
  {
26588
- groups: parsed.tagGroups,
26671
+ groups: legendGroups,
26589
26672
  position: { placement: "top-center", titleRelation: "below-title" },
26590
26673
  mode: exportMode ? "export" : "preview"
26591
26674
  },
26592
26675
  width
26593
26676
  )
26594
26677
  ) : 0;
26595
- const activeGroup = resolveActiveTagGroup(
26596
- parsed.tagGroups,
26597
- parsed.options["active-tag"],
26598
- activeTagGroup
26599
- );
26600
26678
  const hidden = hiddenTagValues ?? parsed.initialHiddenTagValues;
26601
26679
  const nodeMap = /* @__PURE__ */ new Map();
26602
26680
  for (const node of parsed.nodes) nodeMap.set(node.label, node);
@@ -26607,7 +26685,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26607
26685
  const hasAnyDescriptions = parsed.nodes.some(
26608
26686
  (n) => n.description && n.description.length > 0
26609
26687
  );
26610
- const needsLegend = parsed.tagGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26688
+ const needsLegend = legendGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26611
26689
  const legendH = needsLegend ? sLegendHeight + 8 : 0;
26612
26690
  const groupLabelsSet = new Set(layout.groups.map((g) => g.label));
26613
26691
  let labelZoneExtension = 0;
@@ -26813,12 +26891,16 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26813
26891
  activeGroup,
26814
26892
  palette,
26815
26893
  isDark,
26894
+ { active: activeIsValue, hue: rampHue, fillForValue },
26816
26895
  parsed.options["solid-fill"] === "on"
26817
26896
  );
26818
26897
  const nodeG = diagramG.append("g").attr("class", "bl-node").attr("transform", `translate(${ln.x},${ln.y})`).attr("data-line-number", node.lineNumber).attr("data-node-id", node.label).style("cursor", onClickItem ? "pointer" : "default").style("--bl-node-stroke", colors.stroke);
26819
26898
  for (const [key, val] of Object.entries(node.metadata)) {
26820
26899
  nodeG.attr(`data-tag-${key.toLowerCase()}`, val.toLowerCase());
26821
26900
  }
26901
+ if (node.value !== void 0) {
26902
+ nodeG.attr("data-value", node.value);
26903
+ }
26822
26904
  if (onClickItem) {
26823
26905
  nodeG.on("click", (event) => {
26824
26906
  const target = event.target;
@@ -26902,11 +26984,27 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26902
26984
  nodeG.append("text").attr("x", 0).attr("y", -totalH / 2 + lineH / 2 + li * lineH).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", fitted.fontSize).attr("font-weight", "600").attr("fill", colors.text).text(fitted.lines[li]);
26903
26985
  }
26904
26986
  }
26987
+ if (parsed.showValues && node.value !== void 0) {
26988
+ const valueText = String(node.value);
26989
+ const descShown = !!(desc && desc.length > 0 && !hideDescriptions);
26990
+ if (descShown) {
26991
+ const padX = 6;
26992
+ const padY = 5;
26993
+ const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
26994
+ const bh = VALUE_FONT_SIZE + 4;
26995
+ const bx = ln.width / 2 - bw - 4;
26996
+ const by = -ln.height / 2 + 4;
26997
+ nodeG.append("rect").attr("x", bx).attr("y", by).attr("width", bw).attr("height", bh).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.85);
26998
+ nodeG.append("text").attr("class", "bl-node-value").attr("x", bx + bw - padX).attr("y", by + padY).attr("text-anchor", "end").attr("dominant-baseline", "central").attr("font-size", VALUE_FONT_SIZE).attr("font-weight", "600").attr("fill", palette.textMuted).text(valueText);
26999
+ } else {
27000
+ nodeG.append("text").attr("class", "bl-node-value").attr("x", 0).attr("y", ln.height / 2 - VALUE_FONT_SIZE).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", VALUE_FONT_SIZE).attr("font-weight", "600").attr("fill", colors.text).attr("opacity", 0.8).text(valueText);
27001
+ }
27002
+ }
26905
27003
  }
26906
27004
  const hasDescriptions = parsed.nodes.some(
26907
27005
  (n) => n.description && n.description.length > 0
26908
27006
  );
26909
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
27007
+ const hasLegend = legendGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26910
27008
  if (hasLegend) {
26911
27009
  let controlsGroup;
26912
27010
  if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
@@ -26924,7 +27022,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26924
27022
  };
26925
27023
  }
26926
27024
  const legendConfig = {
26927
- groups: parsed.tagGroups,
27025
+ groups: legendGroups,
26928
27026
  position: { placement: "top-center", titleRelation: "below-title" },
26929
27027
  mode: exportMode ? "export" : "preview",
26930
27028
  // Keep inactive sibling tag groups visible as collapsed pills so the user
@@ -26979,7 +27077,7 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
26979
27077
  }
26980
27078
  });
26981
27079
  }
26982
- var d3Selection6, d3Shape4, DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, DESC_FONT_SIZE, DESC_LINE_HEIGHT, MAX_DESC_LINES, CHAR_WIDTH_RATIO2, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, GROUP_LABEL_ZONE, lineGeneratorLR, lineGeneratorTB;
27080
+ var d3Selection6, d3Shape4, DIAGRAM_PADDING6, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, COLLAPSE_BAR_HEIGHT3, ARROWHEAD_W2, ARROWHEAD_H2, DESC_FONT_SIZE, DESC_LINE_HEIGHT, MAX_DESC_LINES, CHAR_WIDTH_RATIO2, NODE_TEXT_PADDING, GROUP_RX, GROUP_LABEL_FONT_SIZE, GROUP_LABEL_ZONE, RAMP_FLOOR, VALUE_FONT_SIZE, lineGeneratorLR, lineGeneratorTB;
26983
27081
  var init_renderer6 = __esm({
26984
27082
  "src/boxes-and-lines/renderer.ts"() {
26985
27083
  "use strict";
@@ -26990,6 +27088,7 @@ var init_renderer6 = __esm({
26990
27088
  init_legend_layout();
26991
27089
  init_title_constants();
26992
27090
  init_color_utils();
27091
+ init_colors();
26993
27092
  init_tag_groups();
26994
27093
  init_inline_markdown();
26995
27094
  init_wrapped_desc();
@@ -27012,6 +27111,8 @@ var init_renderer6 = __esm({
27012
27111
  GROUP_RX = 8;
27013
27112
  GROUP_LABEL_FONT_SIZE = 14;
27014
27113
  GROUP_LABEL_ZONE = 32;
27114
+ RAMP_FLOOR = 15;
27115
+ VALUE_FONT_SIZE = 11;
27015
27116
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
27016
27117
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
27017
27118
  }
@@ -47566,6 +47667,38 @@ function parsePathRings(d) {
47566
47667
  if (cur.length) rings.push(cur);
47567
47668
  return rings;
47568
47669
  }
47670
+ function dropAntimeridianWrapSlivers(d, width, height) {
47671
+ const rings = parsePathRings(d);
47672
+ if (rings.length <= 1) return d;
47673
+ const eps = 0.75;
47674
+ const minArea = 3e-3 * width * height;
47675
+ const ringArea = (r) => {
47676
+ let s = 0;
47677
+ for (let i = 0; i < r.length; i++) {
47678
+ const a = r[i];
47679
+ const b = r[(i + 1) % r.length];
47680
+ s += a[0] * b[1] - b[0] * a[1];
47681
+ }
47682
+ return Math.abs(s) / 2;
47683
+ };
47684
+ const areas = rings.map(ringArea);
47685
+ const maxArea = Math.max(...areas);
47686
+ const onVEdge = (a, b) => Math.abs(a[0]) <= eps && Math.abs(b[0]) <= eps || Math.abs(a[0] - width) <= eps && Math.abs(b[0] - width) <= eps;
47687
+ let dropped = false;
47688
+ const kept = rings.filter((r, idx) => {
47689
+ if (areas[idx] >= maxArea || areas[idx] >= minArea) return true;
47690
+ const touches = r.some((p, i) => onVEdge(p, r[(i + 1) % r.length]));
47691
+ if (touches) {
47692
+ dropped = true;
47693
+ return false;
47694
+ }
47695
+ return true;
47696
+ });
47697
+ if (!dropped) return d;
47698
+ return kept.map(
47699
+ (r) => r.map((p, i) => (i ? "L" : "M") + p[0] + "," + p[1]).join("") + "Z"
47700
+ ).join("");
47701
+ }
47569
47702
  function layoutMap(resolved, data, size, opts) {
47570
47703
  const { palette, isDark } = opts;
47571
47704
  const { width, height } = size;
@@ -47649,7 +47782,7 @@ function layoutMap(resolved, data, size, opts) {
47649
47782
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
47650
47783
  const fillForValue = (s) => {
47651
47784
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
47652
- const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
47785
+ const pct = RAMP_FLOOR2 + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR2);
47653
47786
  return mix(rampHue, rampBase, pct);
47654
47787
  };
47655
47788
  const tagFill = (tags, groupName) => {
@@ -47708,10 +47841,11 @@ function layoutMap(resolved, data, size, opts) {
47708
47841
  const by0 = cb[0][1];
47709
47842
  const cw = cb[1][0] - bx0;
47710
47843
  const ch = cb[1][1] - by0;
47711
- const ox = fitBox[0][0];
47712
- const oy = fitBox[0][1];
47713
- const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
47714
- const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47844
+ const topReserve = resolved.title && resolved.pois.length > 0 ? topPad : 0;
47845
+ const ox = 0;
47846
+ const oy = topReserve;
47847
+ const sx = cw > 0 ? width / cw : 1;
47848
+ const sy = ch > 0 ? (height - topReserve) / ch : 1;
47715
47849
  stretchParams = { sx, sy, ox, oy, bx0, by0 };
47716
47850
  const stretch = (x, y) => [
47717
47851
  ox + (x - bx0) * sx,
@@ -47984,7 +48118,8 @@ function layoutMap(resolved, data, size, opts) {
47984
48118
  const r = regionById.get(iso);
47985
48119
  const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
47986
48120
  if (!viewF) continue;
47987
- const d = path(viewF) ?? "";
48121
+ const raw = path(viewF) ?? "";
48122
+ const d = fitIsGlobal ? dropAntimeridianWrapSlivers(raw, width, height) : raw;
47988
48123
  if (!d) continue;
47989
48124
  const isThisLayer = r?.layer === layerKind;
47990
48125
  const isForeign = layerKind === "country" && usContext && iso !== "US";
@@ -48001,6 +48136,9 @@ function layoutMap(resolved, data, size, opts) {
48001
48136
  } else {
48002
48137
  label = f.properties?.name;
48003
48138
  }
48139
+ const labelAnchor = WORLD_LABEL_ANCHORS[iso];
48140
+ const c = labelAnchor ? project(labelAnchor[0], labelAnchor[1]) : path.centroid(viewF);
48141
+ const hasCentroid = c != null && Number.isFinite(c[0]) && Number.isFinite(c[1]);
48004
48142
  regions.push({
48005
48143
  id: iso,
48006
48144
  d,
@@ -48009,6 +48147,7 @@ function layoutMap(resolved, data, size, opts) {
48009
48147
  lineNumber,
48010
48148
  layer,
48011
48149
  ...label !== void 0 && { label },
48150
+ ...hasCentroid && { labelX: c[0], labelY: c[1] },
48012
48151
  ...isThisLayer && r.value !== void 0 && { value: r.value },
48013
48152
  ...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
48014
48153
  });
@@ -48449,10 +48588,6 @@ function layoutMap(resolved, data, size, opts) {
48449
48588
  lineNumber
48450
48589
  });
48451
48590
  };
48452
- const WORLD_LABEL_ANCHORS = {
48453
- US: [-98.5, 39.5]
48454
- // CONUS geographic centre (near Lebanon, Kansas)
48455
- };
48456
48591
  const REGION_LABEL_GAP = 2;
48457
48592
  const regionLabelRect = (cx, cy, text) => {
48458
48593
  const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
@@ -48818,7 +48953,7 @@ function layoutMap(resolved, data, size, opts) {
48818
48953
  diagnostics: []
48819
48954
  };
48820
48955
  }
48821
- var import_d3_geo2, import_topojson_client2, FIT_PAD, RAMP_FLOOR, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT2, MAX_CLUSTER_EXTENT_FACTOR, MAX_COLUMN_ROWS, REGION_LABEL_HALO_RATIO, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, COMPACT_WIDTH_PX, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, COASTLINE_RING_COUNT, COASTLINE_D0, COASTLINE_STEP, COASTLINE_THICKNESS, COASTLINE_OPACITY_NEAR, COASTLINE_OPACITY_FAR, COASTLINE_MIN_EXTENT, COASTLINE_MIN_EXTENT_GLOBAL, COASTLINE_STROKE_MIX, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, STACK_OVERLAP, STACK_RING_MAX, STACK_RING_GAP, FAN_STEP, ARC_CURVE_FRAC, decodeCache, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, inAlaska, inHawaii, FOREIGN_BORDER, US_NON_CONUS;
48956
+ var import_d3_geo2, import_topojson_client2, FIT_PAD, RAMP_FLOOR2, R_DEFAULT, R_MIN, R_MAX, W_MIN, W_MAX, FONT2, WORLD_LABEL_ANCHORS, MAX_CLUSTER_EXTENT_FACTOR, MAX_COLUMN_ROWS, REGION_LABEL_HALO_RATIO, LAND_TINT_LIGHT, LAND_TINT_DARK, TAG_TINT_LIGHT, TAG_TINT_DARK, WATER_TINT_LIGHT, WATER_TINT_DARK, RIVER_WIDTH, COMPACT_WIDTH_PX, RELIEF_MIN_AREA, RELIEF_MIN_DIM, RELIEF_HATCH_SPACING, RELIEF_HATCH_WIDTH, RELIEF_HATCH_STRENGTH, COASTLINE_RING_COUNT, COASTLINE_D0, COASTLINE_STEP, COASTLINE_THICKNESS, COASTLINE_OPACITY_NEAR, COASTLINE_OPACITY_FAR, COASTLINE_MIN_EXTENT, COASTLINE_MIN_EXTENT_GLOBAL, COASTLINE_STROKE_MIX, FOREIGN_TINT_LIGHT, FOREIGN_TINT_DARK, MUTED_FOREIGN_LIGHT, MUTED_FOREIGN_DARK, COLO_R, GOLDEN_ANGLE, STACK_OVERLAP, STACK_RING_MAX, STACK_RING_GAP, FAN_STEP, ARC_CURVE_FRAC, decodeCache, usConusProjection, alaskaProjection, hawaiiProjection, INSET_STATES, inAlaska, inHawaii, FOREIGN_BORDER, US_NON_CONUS;
48822
48957
  var init_layout15 = __esm({
48823
48958
  "src/map/layout.ts"() {
48824
48959
  "use strict";
@@ -48833,13 +48968,17 @@ var init_layout15 = __esm({
48833
48968
  init_title_constants();
48834
48969
  init_context_labels();
48835
48970
  FIT_PAD = 24;
48836
- RAMP_FLOOR = 15;
48971
+ RAMP_FLOOR2 = 15;
48837
48972
  R_DEFAULT = 6;
48838
48973
  R_MIN = 4;
48839
48974
  R_MAX = 22;
48840
48975
  W_MIN = 1.25;
48841
48976
  W_MAX = 8;
48842
48977
  FONT2 = 11;
48978
+ WORLD_LABEL_ANCHORS = {
48979
+ US: [-98.5, 39.5]
48980
+ // CONUS geographic centre (near Lebanon, Kansas)
48981
+ };
48843
48982
  MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48844
48983
  MAX_COLUMN_ROWS = 7;
48845
48984
  REGION_LABEL_HALO_RATIO = 4.5;
@@ -48853,9 +48992,9 @@ var init_layout15 = __esm({
48853
48992
  COMPACT_WIDTH_PX = 480;
48854
48993
  RELIEF_MIN_AREA = 12;
48855
48994
  RELIEF_MIN_DIM = 2;
48856
- RELIEF_HATCH_SPACING = 2;
48857
- RELIEF_HATCH_WIDTH = 0.15;
48858
- RELIEF_HATCH_STRENGTH = 32;
48995
+ RELIEF_HATCH_SPACING = 1.5;
48996
+ RELIEF_HATCH_WIDTH = 0.2;
48997
+ RELIEF_HATCH_STRENGTH = 26;
48859
48998
  COASTLINE_RING_COUNT = 5;
48860
48999
  COASTLINE_D0 = 16e-4;
48861
49000
  COASTLINE_STEP = 28e-4;
@@ -48933,7 +49072,47 @@ function ringToPath(ring) {
48933
49072
  d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48934
49073
  return d + "Z";
48935
49074
  }
48936
- function coastlineOuterRings(regions, minExtent) {
49075
+ function polylineToPath(pts) {
49076
+ let d = "";
49077
+ for (let i = 0; i < pts.length; i++)
49078
+ d += (i ? "L" : "M") + pts[i][0] + "," + pts[i][1];
49079
+ return d;
49080
+ }
49081
+ function ringToCoastPaths(ring, frame) {
49082
+ if (!frame) return [ringToPath(ring)];
49083
+ const n = ring.length;
49084
+ const eps = 0.75;
49085
+ const onL = (x) => Math.abs(x) <= eps;
49086
+ const onR = (x) => Math.abs(x - frame.w) <= eps;
49087
+ const onT = (y) => Math.abs(y) <= eps;
49088
+ const onB = (y) => Math.abs(y - frame.h) <= eps;
49089
+ const isFrameEdge = (a, b) => onL(a[0]) && onL(b[0]) || onR(a[0]) && onR(b[0]) || onT(a[1]) && onT(b[1]) || onB(a[1]) && onB(b[1]);
49090
+ let firstBreak = -1;
49091
+ for (let i = 0; i < n; i++)
49092
+ if (isFrameEdge(ring[i], ring[(i + 1) % n])) {
49093
+ firstBreak = i;
49094
+ break;
49095
+ }
49096
+ if (firstBreak === -1) return [ringToPath(ring)];
49097
+ const paths = [];
49098
+ let cur = [];
49099
+ const start = (firstBreak + 1) % n;
49100
+ for (let k = 0; k < n; k++) {
49101
+ const i = (start + k) % n;
49102
+ const a = ring[i];
49103
+ const b = ring[(i + 1) % n];
49104
+ if (isFrameEdge(a, b)) {
49105
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
49106
+ cur = [];
49107
+ continue;
49108
+ }
49109
+ if (cur.length === 0) cur.push(a);
49110
+ cur.push(b);
49111
+ }
49112
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
49113
+ return paths;
49114
+ }
49115
+ function coastlineOuterRings(regions, minExtent, frame) {
48937
49116
  const paths = [];
48938
49117
  for (const r of regions) {
48939
49118
  const rings = parsePathRings(r.d);
@@ -48956,7 +49135,7 @@ function coastlineOuterRings(regions, minExtent) {
48956
49135
  for (let j = 0; j < rings.length; j++)
48957
49136
  if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48958
49137
  if (depth % 2 === 1) continue;
48959
- paths.push(ringToPath(ring));
49138
+ paths.push(...ringToCoastPaths(ring, frame));
48960
49139
  }
48961
49140
  }
48962
49141
  return paths;
@@ -49000,6 +49179,10 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49000
49179
  const drawRegion = (g, r, strokeWidth) => {
49001
49180
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
49002
49181
  if (r.label) p.attr("data-region-name", r.label);
49182
+ if (r.id && r.id !== "lake") p.attr("data-iso", r.id);
49183
+ if (r.labelX !== void 0 && r.labelY !== void 0) {
49184
+ p.attr("data-label-x", r.labelX).attr("data-label-y", r.labelY);
49185
+ }
49003
49186
  if (r.layer !== "base") {
49004
49187
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
49005
49188
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -49029,7 +49212,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49029
49212
  const landClip = defs.append("clipPath").attr("id", landClipId);
49030
49213
  for (const r of layout.regions)
49031
49214
  if (r.id !== "lake") landClip.append("path").attr("d", r.d);
49032
- const gRelief = svg.append("g").attr("clip-path", `url(#${landClipId})`).append("g").attr("class", "dgmo-map-relief").attr("clip-path", `url(#${rangeClipId})`).attr("stroke", h.color).attr("stroke-width", h.width).attr("vector-effect", "non-scaling-stroke");
49215
+ const gRelief = svg.append("g").attr("clip-path", `url(#${landClipId})`).style("pointer-events", "none").append("g").attr("class", "dgmo-map-relief").attr("clip-path", `url(#${rangeClipId})`).attr("stroke", h.color).attr("stroke-width", h.width).attr("vector-effect", "non-scaling-stroke");
49033
49216
  for (let y = h.spacing; y < height; y += h.spacing) {
49034
49217
  gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
49035
49218
  }
@@ -49050,10 +49233,16 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49050
49233
  mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
49051
49234
  }
49052
49235
  }
49053
- const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
49236
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").style("pointer-events", "none").attr("mask", `url(#${maskId})`);
49054
49237
  appendWaterLines(
49055
49238
  gWater,
49056
- coastlineOuterRings(layout.regions, cs.minExtent),
49239
+ // Pass the canvas frame so edges collinear with it (the antimeridian on a
49240
+ // world map, regional clipExtent cuts) don't get ringed as fake coast —
49241
+ // land runs cleanly to the render-area edge.
49242
+ coastlineOuterRings(layout.regions, cs.minExtent, {
49243
+ w: width,
49244
+ h: height
49245
+ }),
49057
49246
  cs,
49058
49247
  layout.background
49059
49248
  );
@@ -49067,7 +49256,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49067
49256
  gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
49068
49257
  }
49069
49258
  if (layout.rivers.length) {
49070
- const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
49259
+ const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none").style("pointer-events", "none");
49071
49260
  for (const r of layout.rivers) {
49072
49261
  gRivers.append("path").attr("d", r.d).attr("stroke", r.color).attr("stroke-width", r.width).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
49073
49262
  }
@@ -49108,7 +49297,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49108
49297
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
49109
49298
  clip.append("path").attr("d", d);
49110
49299
  }
49111
- const gInsetWater = insetG.append("g").attr("clip-path", `url(#${clipId})`).append("g").attr("class", "dgmo-map-inset-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
49300
+ const gInsetWater = insetG.append("g").attr("clip-path", `url(#${clipId})`).append("g").attr("class", "dgmo-map-inset-water-lines").attr("fill", "none").style("pointer-events", "none").attr("mask", `url(#${maskId})`);
49112
49301
  appendWaterLines(
49113
49302
  gInsetWater,
49114
49303
  coastlineOuterRings(layout.insetRegions, cs.minExtent),
@@ -55126,10 +55315,12 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
55126
55315
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
55127
55316
  const eraLabelY = eraReserve ? -(topScaleH + markerReserve + ERA_ROW_H / 2) : 0;
55128
55317
  const innerWidth = width - margin.left - margin.right;
55129
- const innerHeight = height - margin.top - margin.bottom;
55130
- const rowH = Math.min(ctx.structural(28), innerHeight / sorted.length);
55318
+ const availInnerHeight = height - margin.top - margin.bottom;
55319
+ const rowH = Math.min(ctx.structural(28), availInnerHeight / sorted.length);
55320
+ const innerHeight = rowH * sorted.length;
55321
+ const usedHeight = margin.top + innerHeight + margin.bottom;
55131
55322
  const xScale = d3Scale2.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
55132
- const svg = d3Selection23.select(container).append("svg").attr("width", width).attr("height", height).attr("viewBox", `0 0 ${width} ${height}`).attr("preserveAspectRatio", "xMidYMin meet").style("background", bgColor);
55323
+ const svg = d3Selection23.select(container).append("svg").attr("width", width).attr("height", usedHeight).attr("viewBox", `0 0 ${width} ${usedHeight}`).attr("preserveAspectRatio", "xMidYMin meet").style("background", bgColor);
55133
55324
  if (ctx.isBelowFloor) {
55134
55325
  svg.attr("width", "100%");
55135
55326
  }
@@ -60504,7 +60695,9 @@ var COMPLETION_REGISTRY = /* @__PURE__ */ new Map([
60504
60695
  withGlobals({
60505
60696
  direction: { description: "Layout direction", values: ["LR", "TB"] },
60506
60697
  "active-tag": { description: "Active tag group name" },
60507
- hide: { description: "Hide tag:value pairs" }
60698
+ hide: { description: "Hide tag:value pairs" },
60699
+ "box-metric": { description: "Metric label for the value ramp" },
60700
+ "show-values": { description: "Print box values as text" }
60508
60701
  })
60509
60702
  ],
60510
60703
  [
@@ -60732,13 +60925,10 @@ var PIPE_METADATA = /* @__PURE__ */ new Map([
60732
60925
  "boxes-and-lines",
60733
60926
  {
60734
60927
  node: {
60735
- description: { description: "Node description text" }
60928
+ description: { description: "Node description text" },
60929
+ value: { description: "Numeric value for the metric ramp" }
60736
60930
  },
60737
- edge: {
60738
- width: { description: "Edge stroke width in pixels" },
60739
- split: { description: "Traffic split percentage" },
60740
- fanout: { description: "Fanout multiplier (integer >= 1)" }
60741
- }
60931
+ edge: {}
60742
60932
  }
60743
60933
  ],
60744
60934
  [
@@ -2500,6 +2500,9 @@ interface BLNode {
2500
2500
  readonly lineNumber: number;
2501
2501
  readonly metadata: Readonly<Record<string, string>>;
2502
2502
  readonly description?: readonly string[];
2503
+ /** Numeric measure lifted from `value: X` metadata (mirror of map's
2504
+ * `region.value`). Drives the value ramp / choropleth tinting. */
2505
+ readonly value?: number;
2503
2506
  }
2504
2507
  interface BLEdge {
2505
2508
  readonly source: string;
@@ -2527,6 +2530,12 @@ interface ParsedBoxesAndLines {
2527
2530
  readonly options: Readonly<Record<string, string>>;
2528
2531
  readonly initialHiddenTagValues: ReadonlyMap<string, ReadonlySet<string>>;
2529
2532
  readonly direction: 'LR' | 'TB';
2533
+ /** `box-metric <label> [color]` — names the value-ramp dimension and
2534
+ * optionally sets its hue. Mirror of map's `region-metric`. */
2535
+ readonly boxMetric?: string;
2536
+ readonly boxMetricColor?: string;
2537
+ /** `show-values` — print each box's numeric value as text (opt-in). */
2538
+ readonly showValues?: boolean;
2530
2539
  readonly diagnostics: readonly DgmoError[];
2531
2540
  readonly error: string | null;
2532
2541
  }
@@ -4672,6 +4681,14 @@ interface MapLayoutRegion {
4672
4681
  /** The region's tag values keyed by group (lowercased) — emitted as
4673
4682
  * `data-tag-<group>` so the app can highlight on legend-entry hover. */
4674
4683
  readonly tags?: Readonly<Record<string, string>>;
4684
+ /** Area-weighted screen centroid (px) of the DRAWN geometry — emitted as
4685
+ * `data-label-x`/`data-label-y` so the app can anchor the hover label here
4686
+ * instead of the path's bounding-box centre. The bbox centre breaks for
4687
+ * antimeridian crossers (Russia's wrapped Chukotka sliver pins the box's left
4688
+ * edge to the far side of the map, dropping the centre into the Atlantic); the
4689
+ * area-weighted centroid stays on the body. Honours WORLD_LABEL_ANCHORS. */
4690
+ readonly labelX?: number;
4691
+ readonly labelY?: number;
4675
4692
  }
4676
4693
  /** A framed inset "cutout" (albers-usa AK/HI), in screen px. The frame is a
4677
4694
  * quad whose TOP edge is angled to ride just under the conus southern coast,
@@ -2500,6 +2500,9 @@ interface BLNode {
2500
2500
  readonly lineNumber: number;
2501
2501
  readonly metadata: Readonly<Record<string, string>>;
2502
2502
  readonly description?: readonly string[];
2503
+ /** Numeric measure lifted from `value: X` metadata (mirror of map's
2504
+ * `region.value`). Drives the value ramp / choropleth tinting. */
2505
+ readonly value?: number;
2503
2506
  }
2504
2507
  interface BLEdge {
2505
2508
  readonly source: string;
@@ -2527,6 +2530,12 @@ interface ParsedBoxesAndLines {
2527
2530
  readonly options: Readonly<Record<string, string>>;
2528
2531
  readonly initialHiddenTagValues: ReadonlyMap<string, ReadonlySet<string>>;
2529
2532
  readonly direction: 'LR' | 'TB';
2533
+ /** `box-metric <label> [color]` — names the value-ramp dimension and
2534
+ * optionally sets its hue. Mirror of map's `region-metric`. */
2535
+ readonly boxMetric?: string;
2536
+ readonly boxMetricColor?: string;
2537
+ /** `show-values` — print each box's numeric value as text (opt-in). */
2538
+ readonly showValues?: boolean;
2530
2539
  readonly diagnostics: readonly DgmoError[];
2531
2540
  readonly error: string | null;
2532
2541
  }
@@ -4672,6 +4681,14 @@ interface MapLayoutRegion {
4672
4681
  /** The region's tag values keyed by group (lowercased) — emitted as
4673
4682
  * `data-tag-<group>` so the app can highlight on legend-entry hover. */
4674
4683
  readonly tags?: Readonly<Record<string, string>>;
4684
+ /** Area-weighted screen centroid (px) of the DRAWN geometry — emitted as
4685
+ * `data-label-x`/`data-label-y` so the app can anchor the hover label here
4686
+ * instead of the path's bounding-box centre. The bbox centre breaks for
4687
+ * antimeridian crossers (Russia's wrapped Chukotka sliver pins the box's left
4688
+ * edge to the far side of the map, dropping the centre into the Atlantic); the
4689
+ * area-weighted centroid stays on the body. Honours WORLD_LABEL_ANCHORS. */
4690
+ readonly labelX?: number;
4691
+ readonly labelY?: number;
4675
4692
  }
4676
4693
  /** A framed inset "cutout" (albers-usa AK/HI), in screen px. The frame is a
4677
4694
  * quad whose TOP edge is angled to ride just under the conus southern coast,