@diagrammo/dgmo 0.22.0 → 0.24.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 (51) hide show
  1. package/dist/advanced.cjs +372 -103
  2. package/dist/advanced.d.cts +52 -19
  3. package/dist/advanced.d.ts +52 -19
  4. package/dist/advanced.js +372 -103
  5. package/dist/auto.cjs +370 -97
  6. package/dist/auto.js +117 -117
  7. package/dist/auto.mjs +370 -97
  8. package/dist/cli.cjs +151 -151
  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 +498 -96
  14. package/dist/index.d.cts +37 -1
  15. package/dist/index.d.ts +37 -1
  16. package/dist/index.js +496 -96
  17. package/dist/internal.cjs +372 -103
  18. package/dist/internal.d.cts +52 -19
  19. package/dist/internal.d.ts +52 -19
  20. package/dist/internal.js +372 -103
  21. package/dist/map-data/PROVENANCE.json +1 -1
  22. package/dist/map-data/gazetteer.json +1 -1
  23. package/dist/map-data/mountain-ranges.json +1 -1
  24. package/dist/map-data/water-bodies.json +1 -1
  25. package/dist/map-data/world-coarse.json +1 -1
  26. package/dist/map-data/world-detail.json +1 -1
  27. package/docs/language-reference.md +38 -2
  28. package/gallery/fixtures/boxes-and-lines.dgmo +6 -4
  29. package/package.json +1 -1
  30. package/src/boxes-and-lines/parser.ts +39 -0
  31. package/src/boxes-and-lines/renderer.ts +219 -14
  32. package/src/boxes-and-lines/types.ts +9 -0
  33. package/src/completion.ts +4 -5
  34. package/src/d3.ts +26 -6
  35. package/src/editor/keywords.ts +3 -0
  36. package/src/index.ts +8 -0
  37. package/src/map/data/PROVENANCE.json +1 -1
  38. package/src/map/data/README.md +6 -0
  39. package/src/map/data/gazetteer.json +1 -1
  40. package/src/map/data/mountain-ranges.json +1 -1
  41. package/src/map/data/water-bodies.json +1 -1
  42. package/src/map/data/world-coarse.json +1 -1
  43. package/src/map/data/world-detail.json +1 -1
  44. package/src/map/dimensions.ts +21 -5
  45. package/src/map/layout.ts +167 -63
  46. package/src/map/legend-band.ts +99 -0
  47. package/src/map/renderer.ts +105 -32
  48. package/src/map/resolver.ts +43 -1
  49. package/src/map/types.ts +20 -0
  50. package/src/utils/reserved-key-registry.ts +5 -3
  51. package/src/utils/svg-embed.ts +193 -0
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 === "" || 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;
@@ -26890,6 +26972,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26890
26972
  const tooltipText = fullText.length > 200 ? fullText.slice(0, 199) + "\u2026" : fullText;
26891
26973
  nodeG.append("title").text(tooltipText);
26892
26974
  }
26975
+ } else if (parsed.showValues && node.value !== void 0) {
26976
+ const valueLabel = parsed.boxMetric ? `${parsed.boxMetric}: ${node.value}` : String(node.value);
26977
+ const headerH = ln.height / 2;
26978
+ const sepY = -ln.height / 2 + headerH;
26979
+ const fitted = fitLabelToHeader(node.label, ln.width, 2);
26980
+ const labelLineH = fitted.fontSize * 1.3;
26981
+ const labelTotalH = fitted.lines.length * labelLineH;
26982
+ const headerCenterY = -ln.height / 2 + headerH / 2;
26983
+ for (let li = 0; li < fitted.lines.length; li++) {
26984
+ nodeG.append("text").attr("x", 0).attr(
26985
+ "y",
26986
+ headerCenterY - labelTotalH / 2 + labelLineH / 2 + li * labelLineH
26987
+ ).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]);
26988
+ }
26989
+ nodeG.append("line").attr("x1", -ln.width / 2).attr("y1", sepY).attr("x2", ln.width / 2).attr("y2", sepY).attr("stroke", colors.stroke).attr("stroke-opacity", 0.3).attr("stroke-width", 1);
26990
+ nodeG.append("text").attr("class", "bl-node-value").attr("x", 0).attr("y", (sepY + ln.height / 2) / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", VALUE_FONT_SIZE).attr("fill", colors.text).attr("opacity", 0.85).text(valueLabel);
26893
26991
  } else {
26894
26992
  const maxLabelLines = Math.max(
26895
26993
  2,
@@ -26902,11 +27000,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26902
27000
  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
27001
  }
26904
27002
  }
27003
+ if (parsed.showValues && node.value !== void 0 && desc && desc.length > 0 && !hideDescriptions) {
27004
+ const valueText = String(node.value);
27005
+ const padX = 6;
27006
+ const padY = 5;
27007
+ const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
27008
+ const bh = VALUE_FONT_SIZE + 4;
27009
+ const bx = Math.max(-ln.width / 2 + 4, ln.width / 2 - bw - 4);
27010
+ const by = -ln.height / 2 + 4;
27011
+ 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);
27012
+ 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);
27013
+ }
26905
27014
  }
26906
27015
  const hasDescriptions = parsed.nodes.some(
26907
27016
  (n) => n.description && n.description.length > 0
26908
27017
  );
26909
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
27018
+ const hasLegend = legendGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26910
27019
  if (hasLegend) {
26911
27020
  let controlsGroup;
26912
27021
  if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
@@ -26924,7 +27033,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26924
27033
  };
26925
27034
  }
26926
27035
  const legendConfig = {
26927
- groups: parsed.tagGroups,
27036
+ groups: legendGroups,
26928
27037
  position: { placement: "top-center", titleRelation: "below-title" },
26929
27038
  mode: exportMode ? "export" : "preview",
26930
27039
  // Keep inactive sibling tag groups visible as collapsed pills so the user
@@ -26979,7 +27088,7 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
26979
27088
  }
26980
27089
  });
26981
27090
  }
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;
27091
+ 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
27092
  var init_renderer6 = __esm({
26984
27093
  "src/boxes-and-lines/renderer.ts"() {
26985
27094
  "use strict";
@@ -26990,12 +27099,13 @@ var init_renderer6 = __esm({
26990
27099
  init_legend_layout();
26991
27100
  init_title_constants();
26992
27101
  init_color_utils();
27102
+ init_colors();
26993
27103
  init_tag_groups();
26994
27104
  init_inline_markdown();
26995
27105
  init_wrapped_desc();
26996
27106
  init_scaling();
26997
27107
  DIAGRAM_PADDING6 = 20;
26998
- NODE_FONT_SIZE = 13;
27108
+ NODE_FONT_SIZE = 11;
26999
27109
  MIN_NODE_FONT_SIZE = 9;
27000
27110
  EDGE_LABEL_FONT_SIZE4 = 11;
27001
27111
  EDGE_STROKE_WIDTH5 = 1.5;
@@ -27012,6 +27122,8 @@ var init_renderer6 = __esm({
27012
27122
  GROUP_RX = 8;
27013
27123
  GROUP_LABEL_FONT_SIZE = 14;
27014
27124
  GROUP_LABEL_ZONE = 32;
27125
+ RAMP_FLOOR = 15;
27126
+ VALUE_FONT_SIZE = 11;
27015
27127
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
27016
27128
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
27017
27129
  }
@@ -46953,7 +47065,11 @@ function resolveMap(parsed, data) {
46953
47065
  if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46954
47066
  }
46955
47067
  const containerUnion = unionExtent(containerBoxes, points);
46956
- if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
47068
+ if (containerUnion)
47069
+ extent2 = pad(
47070
+ clampContainerToCluster(containerUnion, points),
47071
+ PAD_FRACTION
47072
+ );
46957
47073
  }
46958
47074
  if (isPoiOnly) {
46959
47075
  const cx = (extent2[0][0] + extent2[1][0]) / 2;
@@ -47034,6 +47150,22 @@ function mostCommonCountry(regions, poiCountries) {
47034
47150
  }
47035
47151
  return best;
47036
47152
  }
47153
+ function clampContainerToCluster(container, points) {
47154
+ const poi = unionExtent([], points);
47155
+ if (!poi) return container;
47156
+ let [[west, south], [east, north]] = container;
47157
+ const [[pWest, pSouth], [pEast, pNorth]] = poi;
47158
+ south = Math.max(south, pSouth - CONTAINER_OVERSHOOT_DEG);
47159
+ north = Math.min(north, pNorth + CONTAINER_OVERSHOOT_DEG);
47160
+ if (east <= 180 && pEast <= 180) {
47161
+ west = Math.max(west, pWest - CONTAINER_OVERSHOOT_DEG);
47162
+ east = Math.min(east, pEast + CONTAINER_OVERSHOOT_DEG);
47163
+ }
47164
+ return [
47165
+ [west, south],
47166
+ [east, north]
47167
+ ];
47168
+ }
47037
47169
  function pad(e, frac) {
47038
47170
  const dLon = (e[1][0] - e[0][0]) * frac || 1;
47039
47171
  const dLat = (e[1][1] - e[0][1]) * frac || 1;
@@ -47046,7 +47178,7 @@ function firstError(diags) {
47046
47178
  const e = diags.find((d) => d.severity === "error");
47047
47179
  return e ? formatDgmoError(e) : null;
47048
47180
  }
47049
- var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, REGION_PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, POI_ZOOM_FLOOR_DEG, US_NATIONAL_LON_SPAN, REGION_ALIASES, US_STATE_POSTAL;
47181
+ var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, REGION_PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, POI_ZOOM_FLOOR_DEG, CONTAINER_OVERSHOOT_DEG, US_NATIONAL_LON_SPAN, REGION_ALIASES, US_STATE_POSTAL;
47050
47182
  var init_resolver2 = __esm({
47051
47183
  "src/map/resolver.ts"() {
47052
47184
  "use strict";
@@ -47059,6 +47191,7 @@ var init_resolver2 = __esm({
47059
47191
  WORLD_LAT_SOUTH = -58;
47060
47192
  WORLD_LAT_NORTH = 78;
47061
47193
  POI_ZOOM_FLOOR_DEG = 7;
47194
+ CONTAINER_OVERSHOOT_DEG = 8;
47062
47195
  US_NATIONAL_LON_SPAN = 48;
47063
47196
  REGION_ALIASES = {
47064
47197
  // Common everyday names → the Natural-Earth display name actually shipped.
@@ -47137,6 +47270,55 @@ var init_resolver2 = __esm({
47137
47270
  }
47138
47271
  });
47139
47272
 
47273
+ // src/map/legend-band.ts
47274
+ function mapLegendGroups(legend) {
47275
+ const ramp = legend.ramp;
47276
+ const scoreGroup = ramp ? {
47277
+ name: ramp.metric?.trim() || "Value",
47278
+ entries: [],
47279
+ gradient: {
47280
+ min: ramp.min,
47281
+ max: ramp.max,
47282
+ hue: ramp.hue,
47283
+ base: ramp.base
47284
+ }
47285
+ } : null;
47286
+ const tagGroups = legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
47287
+ return [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
47288
+ }
47289
+ function mapLegendConfig(groups, mode) {
47290
+ return {
47291
+ groups,
47292
+ position: { placement: "top-center", titleRelation: "below-title" },
47293
+ mode,
47294
+ showEmptyGroups: false,
47295
+ showInactivePills: true
47296
+ };
47297
+ }
47298
+ function mapLegendTop(hasTitle, hasSubtitle) {
47299
+ return (hasTitle ? TITLE_Y + TITLE_FONT_SIZE : 0) + (hasSubtitle ? TITLE_FONT_SIZE : 0) + LEGEND_TOP_GAP2;
47300
+ }
47301
+ function mapLegendBand(legend, opts) {
47302
+ if (!legend) return 0;
47303
+ const groups = mapLegendGroups(legend);
47304
+ if (groups.length === 0) return 0;
47305
+ const config = mapLegendConfig(groups, opts.mode);
47306
+ const state = { activeGroup: legend.activeGroup };
47307
+ const { height } = computeLegendLayout(config, state, opts.width);
47308
+ if (height <= 0) return 0;
47309
+ return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
47310
+ }
47311
+ var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
47312
+ var init_legend_band = __esm({
47313
+ "src/map/legend-band.ts"() {
47314
+ "use strict";
47315
+ init_legend_layout();
47316
+ init_title_constants();
47317
+ LEGEND_TOP_GAP2 = 8;
47318
+ LEGEND_BOTTOM_GAP2 = 10;
47319
+ }
47320
+ });
47321
+
47140
47322
  // src/map/colorize.ts
47141
47323
  function assignColors(isos, adjacency) {
47142
47324
  const sorted = [...isos].sort();
@@ -47566,6 +47748,38 @@ function parsePathRings(d) {
47566
47748
  if (cur.length) rings.push(cur);
47567
47749
  return rings;
47568
47750
  }
47751
+ function dropAntimeridianWrapSlivers(d, width, height) {
47752
+ const rings = parsePathRings(d);
47753
+ if (rings.length <= 1) return d;
47754
+ const eps = 0.75;
47755
+ const minArea = 3e-3 * width * height;
47756
+ const ringArea = (r) => {
47757
+ let s = 0;
47758
+ for (let i = 0; i < r.length; i++) {
47759
+ const a = r[i];
47760
+ const b = r[(i + 1) % r.length];
47761
+ s += a[0] * b[1] - b[0] * a[1];
47762
+ }
47763
+ return Math.abs(s) / 2;
47764
+ };
47765
+ const areas = rings.map(ringArea);
47766
+ const maxArea = Math.max(...areas);
47767
+ 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;
47768
+ let dropped = false;
47769
+ const kept = rings.filter((r, idx) => {
47770
+ if (areas[idx] >= maxArea || areas[idx] >= minArea) return true;
47771
+ const touches = r.some((p, i) => onVEdge(p, r[(i + 1) % r.length]));
47772
+ if (touches) {
47773
+ dropped = true;
47774
+ return false;
47775
+ }
47776
+ return true;
47777
+ });
47778
+ if (!dropped) return d;
47779
+ return kept.map(
47780
+ (r) => r.map((p, i) => (i ? "L" : "M") + p[0] + "," + p[1]).join("") + "Z"
47781
+ ).join("");
47782
+ }
47569
47783
  function layoutMap(resolved, data, size, opts) {
47570
47784
  const { palette, isDark } = opts;
47571
47785
  const { width, height } = size;
@@ -47649,7 +47863,7 @@ function layoutMap(resolved, data, size, opts) {
47649
47863
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
47650
47864
  const fillForValue = (s) => {
47651
47865
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
47652
- const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
47866
+ const pct = RAMP_FLOOR2 + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR2);
47653
47867
  return mix(rampHue, rampBase, pct);
47654
47868
  };
47655
47869
  const tagFill = (tags, groupName) => {
@@ -47685,12 +47899,43 @@ function layoutMap(resolved, data, size, opts) {
47685
47899
  return tagFill(r.tags, activeGroup) ?? neutralFill;
47686
47900
  };
47687
47901
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
47902
+ let legend = null;
47903
+ if (!resolved.directives.noLegend) {
47904
+ const legendTagGroups = resolved.tagGroups.map((g) => ({
47905
+ name: g.name,
47906
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
47907
+ }));
47908
+ if (legendTagGroups.length > 0 || hasRamp) {
47909
+ legend = {
47910
+ tagGroups: legendTagGroups,
47911
+ activeGroup,
47912
+ ...hasRamp && {
47913
+ ramp: {
47914
+ ...resolved.directives.regionMetric !== void 0 && {
47915
+ metric: resolved.directives.regionMetric
47916
+ },
47917
+ min: rampMin,
47918
+ max: rampMax,
47919
+ hue: rampHue,
47920
+ base: rampBase
47921
+ }
47922
+ }
47923
+ };
47924
+ }
47925
+ }
47688
47926
  const TITLE_GAP2 = 16;
47689
47927
  let topPad = FIT_PAD;
47690
47928
  if (resolved.title && resolved.pois.length > 0) {
47691
47929
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
47692
47930
  topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
47693
47931
  }
47932
+ const legendBand = mapLegendBand(legend, {
47933
+ width,
47934
+ mode: opts.legendMode ?? "preview",
47935
+ hasTitle: Boolean(resolved.title),
47936
+ hasSubtitle: Boolean(resolved.subtitle)
47937
+ });
47938
+ if (legendBand > topPad) topPad = legendBand;
47694
47939
  const fitBox = [
47695
47940
  [FIT_PAD, topPad],
47696
47941
  [
@@ -47708,10 +47953,11 @@ function layoutMap(resolved, data, size, opts) {
47708
47953
  const by0 = cb[0][1];
47709
47954
  const cw = cb[1][0] - bx0;
47710
47955
  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;
47956
+ const topReserve = resolved.title && resolved.pois.length > 0 || legendBand > 0 ? topPad : 0;
47957
+ const ox = 0;
47958
+ const oy = topReserve;
47959
+ const sx = cw > 0 ? width / cw : 1;
47960
+ const sy = ch > 0 ? (height - topReserve) / ch : 1;
47715
47961
  stretchParams = { sx, sy, ox, oy, bx0, by0 };
47716
47962
  const stretch = (x, y) => [
47717
47963
  ox + (x - bx0) * sx,
@@ -47984,7 +48230,8 @@ function layoutMap(resolved, data, size, opts) {
47984
48230
  const r = regionById.get(iso);
47985
48231
  const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
47986
48232
  if (!viewF) continue;
47987
- const d = path(viewF) ?? "";
48233
+ const raw = path(viewF) ?? "";
48234
+ const d = fitIsGlobal ? dropAntimeridianWrapSlivers(raw, width, height) : raw;
47988
48235
  if (!d) continue;
47989
48236
  const isThisLayer = r?.layer === layerKind;
47990
48237
  const isForeign = layerKind === "country" && usContext && iso !== "US";
@@ -48001,6 +48248,9 @@ function layoutMap(resolved, data, size, opts) {
48001
48248
  } else {
48002
48249
  label = f.properties?.name;
48003
48250
  }
48251
+ const labelAnchor = WORLD_LABEL_ANCHORS[iso];
48252
+ const c = labelAnchor ? project(labelAnchor[0], labelAnchor[1]) : path.centroid(viewF);
48253
+ const hasCentroid = c != null && Number.isFinite(c[0]) && Number.isFinite(c[1]);
48004
48254
  regions.push({
48005
48255
  id: iso,
48006
48256
  d,
@@ -48009,6 +48259,7 @@ function layoutMap(resolved, data, size, opts) {
48009
48259
  lineNumber,
48010
48260
  layer,
48011
48261
  ...label !== void 0 && { label },
48262
+ ...hasCentroid && { labelX: c[0], labelY: c[1] },
48012
48263
  ...isThisLayer && r.value !== void 0 && { value: r.value },
48013
48264
  ...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
48014
48265
  });
@@ -48449,10 +48700,6 @@ function layoutMap(resolved, data, size, opts) {
48449
48700
  lineNumber
48450
48701
  });
48451
48702
  };
48452
- const WORLD_LABEL_ANCHORS = {
48453
- US: [-98.5, 39.5]
48454
- // CONUS geographic centre (near Lebanon, Kansas)
48455
- };
48456
48703
  const REGION_LABEL_GAP = 2;
48457
48704
  const regionLabelRect = (cx, cy, text) => {
48458
48705
  const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
@@ -48770,30 +49017,6 @@ function layoutMap(resolved, data, size, opts) {
48770
49017
  });
48771
49018
  labels.push(...contextLabels);
48772
49019
  }
48773
- let legend = null;
48774
- if (!resolved.directives.noLegend) {
48775
- const tagGroups = resolved.tagGroups.map((g) => ({
48776
- name: g.name,
48777
- entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
48778
- }));
48779
- if (tagGroups.length > 0 || hasRamp) {
48780
- legend = {
48781
- tagGroups,
48782
- activeGroup,
48783
- ...hasRamp && {
48784
- ramp: {
48785
- ...resolved.directives.regionMetric !== void 0 && {
48786
- metric: resolved.directives.regionMetric
48787
- },
48788
- min: rampMin,
48789
- max: rampMax,
48790
- hue: rampHue,
48791
- base: rampBase
48792
- }
48793
- }
48794
- };
48795
- }
48796
- }
48797
49020
  return {
48798
49021
  width,
48799
49022
  height,
@@ -48818,7 +49041,7 @@ function layoutMap(resolved, data, size, opts) {
48818
49041
  diagnostics: []
48819
49042
  };
48820
49043
  }
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;
49044
+ 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
49045
  var init_layout15 = __esm({
48823
49046
  "src/map/layout.ts"() {
48824
49047
  "use strict";
@@ -48831,15 +49054,20 @@ var init_layout15 = __esm({
48831
49054
  init_label_layout();
48832
49055
  init_legend_constants();
48833
49056
  init_title_constants();
49057
+ init_legend_band();
48834
49058
  init_context_labels();
48835
49059
  FIT_PAD = 24;
48836
- RAMP_FLOOR = 15;
49060
+ RAMP_FLOOR2 = 15;
48837
49061
  R_DEFAULT = 6;
48838
49062
  R_MIN = 4;
48839
49063
  R_MAX = 22;
48840
49064
  W_MIN = 1.25;
48841
49065
  W_MAX = 8;
48842
49066
  FONT2 = 11;
49067
+ WORLD_LABEL_ANCHORS = {
49068
+ US: [-98.5, 39.5]
49069
+ // CONUS geographic centre (near Lebanon, Kansas)
49070
+ };
48843
49071
  MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48844
49072
  MAX_COLUMN_ROWS = 7;
48845
49073
  REGION_LABEL_HALO_RATIO = 4.5;
@@ -48853,9 +49081,9 @@ var init_layout15 = __esm({
48853
49081
  COMPACT_WIDTH_PX = 480;
48854
49082
  RELIEF_MIN_AREA = 12;
48855
49083
  RELIEF_MIN_DIM = 2;
48856
- RELIEF_HATCH_SPACING = 2;
48857
- RELIEF_HATCH_WIDTH = 0.15;
48858
- RELIEF_HATCH_STRENGTH = 32;
49084
+ RELIEF_HATCH_SPACING = 1.5;
49085
+ RELIEF_HATCH_WIDTH = 0.2;
49086
+ RELIEF_HATCH_STRENGTH = 26;
48859
49087
  COASTLINE_RING_COUNT = 5;
48860
49088
  COASTLINE_D0 = 16e-4;
48861
49089
  COASTLINE_STEP = 28e-4;
@@ -48933,7 +49161,47 @@ function ringToPath(ring) {
48933
49161
  d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48934
49162
  return d + "Z";
48935
49163
  }
48936
- function coastlineOuterRings(regions, minExtent) {
49164
+ function polylineToPath(pts) {
49165
+ let d = "";
49166
+ for (let i = 0; i < pts.length; i++)
49167
+ d += (i ? "L" : "M") + pts[i][0] + "," + pts[i][1];
49168
+ return d;
49169
+ }
49170
+ function ringToCoastPaths(ring, frame) {
49171
+ if (!frame) return [ringToPath(ring)];
49172
+ const n = ring.length;
49173
+ const eps = 0.75;
49174
+ const onL = (x) => Math.abs(x) <= eps;
49175
+ const onR = (x) => Math.abs(x - frame.w) <= eps;
49176
+ const onT = (y) => Math.abs(y) <= eps;
49177
+ const onB = (y) => Math.abs(y - frame.h) <= eps;
49178
+ 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]);
49179
+ let firstBreak = -1;
49180
+ for (let i = 0; i < n; i++)
49181
+ if (isFrameEdge(ring[i], ring[(i + 1) % n])) {
49182
+ firstBreak = i;
49183
+ break;
49184
+ }
49185
+ if (firstBreak === -1) return [ringToPath(ring)];
49186
+ const paths = [];
49187
+ let cur = [];
49188
+ const start = (firstBreak + 1) % n;
49189
+ for (let k = 0; k < n; k++) {
49190
+ const i = (start + k) % n;
49191
+ const a = ring[i];
49192
+ const b = ring[(i + 1) % n];
49193
+ if (isFrameEdge(a, b)) {
49194
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
49195
+ cur = [];
49196
+ continue;
49197
+ }
49198
+ if (cur.length === 0) cur.push(a);
49199
+ cur.push(b);
49200
+ }
49201
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
49202
+ return paths;
49203
+ }
49204
+ function coastlineOuterRings(regions, minExtent, frame) {
48937
49205
  const paths = [];
48938
49206
  for (const r of regions) {
48939
49207
  const rings = parsePathRings(r.d);
@@ -48956,7 +49224,7 @@ function coastlineOuterRings(regions, minExtent) {
48956
49224
  for (let j = 0; j < rings.length; j++)
48957
49225
  if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48958
49226
  if (depth % 2 === 1) continue;
48959
- paths.push(ringToPath(ring));
49227
+ paths.push(...ringToCoastPaths(ring, frame));
48960
49228
  }
48961
49229
  }
48962
49230
  return paths;
@@ -48986,6 +49254,9 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48986
49254
  // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48987
49255
  // keeps the global stretch-fill.
48988
49256
  preferContain: exportDims?.preferContain ?? false,
49257
+ // Reserve the legend band for the mode actually drawn below (export shows
49258
+ // only the active group; preview keeps the inactive pills).
49259
+ legendMode: exportDims ? "export" : "preview",
48989
49260
  ...activeGroupOverride !== void 0 && {
48990
49261
  activeGroup: activeGroupOverride
48991
49262
  }
@@ -49000,6 +49271,10 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49000
49271
  const drawRegion = (g, r, strokeWidth) => {
49001
49272
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
49002
49273
  if (r.label) p.attr("data-region-name", r.label);
49274
+ if (r.id && r.id !== "lake") p.attr("data-iso", r.id);
49275
+ if (r.labelX !== void 0 && r.labelY !== void 0) {
49276
+ p.attr("data-label-x", r.labelX).attr("data-label-y", r.labelY);
49277
+ }
49003
49278
  if (r.layer !== "base") {
49004
49279
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
49005
49280
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -49029,7 +49304,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49029
49304
  const landClip = defs.append("clipPath").attr("id", landClipId);
49030
49305
  for (const r of layout.regions)
49031
49306
  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");
49307
+ 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
49308
  for (let y = h.spacing; y < height; y += h.spacing) {
49034
49309
  gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
49035
49310
  }
@@ -49050,10 +49325,16 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49050
49325
  mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
49051
49326
  }
49052
49327
  }
49053
- const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
49328
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").style("pointer-events", "none").attr("mask", `url(#${maskId})`);
49054
49329
  appendWaterLines(
49055
49330
  gWater,
49056
- coastlineOuterRings(layout.regions, cs.minExtent),
49331
+ // Pass the canvas frame so edges collinear with it (the antimeridian on a
49332
+ // world map, regional clipExtent cuts) don't get ringed as fake coast —
49333
+ // land runs cleanly to the render-area edge.
49334
+ coastlineOuterRings(layout.regions, cs.minExtent, {
49335
+ w: width,
49336
+ h: height
49337
+ }),
49057
49338
  cs,
49058
49339
  layout.background
49059
49340
  );
@@ -49067,7 +49348,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49067
49348
  gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
49068
49349
  }
49069
49350
  if (layout.rivers.length) {
49070
- const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
49351
+ const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none").style("pointer-events", "none");
49071
49352
  for (const r of layout.rivers) {
49072
49353
  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
49354
  }
@@ -49108,7 +49389,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49108
49389
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
49109
49390
  clip.append("path").attr("d", d);
49110
49391
  }
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})`);
49392
+ 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
49393
  appendWaterLines(
49113
49394
  gInsetWater,
49114
49395
  coastlineOuterRings(layout.insetRegions, cs.minExtent),
@@ -49267,30 +49548,12 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49267
49548
  if (layout.legend) {
49268
49549
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
49269
49550
  const legendG = svg.append("g").attr("class", "dgmo-map-legend").attr("transform", `translate(0, ${legendY})`);
49270
- const ramp = layout.legend.ramp;
49271
- const scoreGroup = ramp ? {
49272
- name: ramp.metric?.trim() || "Value",
49273
- entries: [],
49274
- gradient: {
49275
- min: ramp.min,
49276
- max: ramp.max,
49277
- hue: ramp.hue,
49278
- base: ramp.base
49279
- }
49280
- } : null;
49281
- const tagGroups = layout.legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
49282
- const groups = [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
49551
+ const groups = mapLegendGroups(layout.legend);
49283
49552
  if (groups.length > 0) {
49284
- const config = {
49553
+ const config = mapLegendConfig(
49285
49554
  groups,
49286
- position: { placement: "top-center", titleRelation: "below-title" },
49287
- mode: exportDims ? "export" : "preview",
49288
- showEmptyGroups: false,
49289
- // Keep inactive siblings visible as pills so the user can click to flip
49290
- // the active colouring dimension (preview only — export shows just the
49291
- // active group).
49292
- showInactivePills: true
49293
- };
49555
+ exportDims ? "export" : "preview"
49556
+ );
49294
49557
  const state = { activeGroup: layout.legend.activeGroup };
49295
49558
  renderLegendD3(legendG, config, state, palette, isDark, void 0, width);
49296
49559
  }
@@ -49335,6 +49598,7 @@ var init_renderer16 = __esm({
49335
49598
  init_title_constants();
49336
49599
  init_color_utils();
49337
49600
  init_legend_d3();
49601
+ init_legend_band();
49338
49602
  init_layout15();
49339
49603
  LABEL_FONT = 11;
49340
49604
  }
@@ -49355,9 +49619,10 @@ function mapContentAspect(resolved, data, ref = REF) {
49355
49619
  const aspect = w / h;
49356
49620
  return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49357
49621
  }
49358
- function mapExportDimensions(resolved, data, baseWidth = 1200) {
49359
- const raw = mapContentAspect(resolved, data);
49360
- const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49622
+ function mapExportDimensions(resolved, data, baseWidth = 1200, aspectOverride) {
49623
+ const useOverride = aspectOverride !== void 0 && Number.isFinite(aspectOverride) && aspectOverride > 0;
49624
+ const raw = useOverride ? aspectOverride : mapContentAspect(resolved, data);
49625
+ const clamped = useOverride ? raw : Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49361
49626
  const width = baseWidth;
49362
49627
  let height = Math.round(width / clamped);
49363
49628
  let chromeReserve = 0;
@@ -49370,7 +49635,7 @@ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49370
49635
  height = Math.round(chromeReserve + MIN_MAP_BAND);
49371
49636
  floored = true;
49372
49637
  }
49373
- const preferContain = clamped !== raw || floored;
49638
+ const preferContain = useOverride ? floored : clamped !== raw || floored;
49374
49639
  return { width, height, preferContain };
49375
49640
  }
49376
49641
  var import_d3_geo3, FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
@@ -55077,7 +55342,6 @@ function renderTimelineTagLegendOverlay(container, parsed, palette, isDark, setu
55077
55342
  function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, setup, hovers, onClickItem, _exportDims, _swimlaneTagGroup, _activeTagGroup, _onTagStateChange, _viewMode) {
55078
55343
  const {
55079
55344
  width,
55080
- height,
55081
55345
  tooltip,
55082
55346
  solid,
55083
55347
  textColor,
@@ -55126,10 +55390,11 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
55126
55390
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
55127
55391
  const eraLabelY = eraReserve ? -(topScaleH + markerReserve + ERA_ROW_H / 2) : 0;
55128
55392
  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);
55393
+ const rowH = ctx.structural(28);
55394
+ const innerHeight = rowH * sorted.length;
55395
+ const usedHeight = margin.top + innerHeight + margin.bottom;
55131
55396
  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);
55397
+ 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
55398
  if (ctx.isBelowFloor) {
55134
55399
  svg.attr("width", "100%");
55135
55400
  }
@@ -57706,7 +57971,12 @@ async function renderForExport(content, theme, palette, viewState, options) {
57706
57971
  }
57707
57972
  }
57708
57973
  const mapResolved = resolveMap2(mapParsed, mapData);
57709
- const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57974
+ const dims2 = mapExportDimensions2(
57975
+ mapResolved,
57976
+ mapData,
57977
+ EXPORT_WIDTH,
57978
+ options?.mapAspect
57979
+ );
57710
57980
  const container2 = createExportContainer(dims2.width, dims2.height);
57711
57981
  renderMapForExport2(
57712
57982
  container2,
@@ -60504,7 +60774,9 @@ var COMPLETION_REGISTRY = /* @__PURE__ */ new Map([
60504
60774
  withGlobals({
60505
60775
  direction: { description: "Layout direction", values: ["LR", "TB"] },
60506
60776
  "active-tag": { description: "Active tag group name" },
60507
- hide: { description: "Hide tag:value pairs" }
60777
+ hide: { description: "Hide tag:value pairs" },
60778
+ "box-metric": { description: "Metric label for the value ramp" },
60779
+ "show-values": { description: "Print box values as text" }
60508
60780
  })
60509
60781
  ],
60510
60782
  [
@@ -60732,13 +61004,10 @@ var PIPE_METADATA = /* @__PURE__ */ new Map([
60732
61004
  "boxes-and-lines",
60733
61005
  {
60734
61006
  node: {
60735
- description: { description: "Node description text" }
61007
+ description: { description: "Node description text" },
61008
+ value: { description: "Numeric value for the metric ramp" }
60736
61009
  },
60737
- edge: {
60738
- width: { description: "Edge stroke width in pixels" },
60739
- split: { description: "Traffic split percentage" },
60740
- fanout: { description: "Fanout multiplier (integer >= 1)" }
60741
- }
61010
+ edge: {}
60742
61011
  }
60743
61012
  ],
60744
61013
  [