@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/internal.js CHANGED
@@ -915,9 +915,7 @@ var init_reserved_key_registry = __esm({
915
915
  BOXES_AND_LINES_REGISTRY = staticRegistry([
916
916
  "color",
917
917
  "description",
918
- "width",
919
- "split",
920
- "fanout"
918
+ "value"
921
919
  ]);
922
920
  TIMELINE_REGISTRY = staticRegistry([
923
921
  "color",
@@ -16889,6 +16887,21 @@ function parseBoxesAndLines(content) {
16889
16887
  }
16890
16888
  continue;
16891
16889
  }
16890
+ if (!contentStarted) {
16891
+ const metricMatch = trimmed.match(/^box-metric\s+(.+)$/i);
16892
+ if (metricMatch) {
16893
+ const { label, colorName } = peelTrailingColorName(
16894
+ metricMatch[1].trim()
16895
+ );
16896
+ result.boxMetric = label;
16897
+ if (colorName !== void 0) result.boxMetricColor = colorName;
16898
+ continue;
16899
+ }
16900
+ if (/^show-values$/i.test(trimmed)) {
16901
+ result.showValues = true;
16902
+ continue;
16903
+ }
16904
+ }
16892
16905
  if (!contentStarted) {
16893
16906
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
16894
16907
  if (optMatch) {
@@ -17267,6 +17280,19 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
17267
17280
  description = [metadata["description"]];
17268
17281
  delete metadata["description"];
17269
17282
  }
17283
+ let value;
17284
+ if (metadata["value"] !== void 0) {
17285
+ const raw = metadata["value"];
17286
+ const num = Number(raw);
17287
+ if (Number.isFinite(num)) {
17288
+ value = num;
17289
+ } else {
17290
+ diagnostics.push(
17291
+ makeDgmoError(lineNum, `value must be a number (got "${raw}")`, "error")
17292
+ );
17293
+ }
17294
+ delete metadata["value"];
17295
+ }
17270
17296
  if (split.alias) {
17271
17297
  nameAliasMap?.set(normalizeName(split.alias), label);
17272
17298
  }
@@ -17275,7 +17301,8 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
17275
17301
  label,
17276
17302
  lineNumber: lineNum,
17277
17303
  metadata,
17278
- ...description !== void 0 && { description }
17304
+ ...description !== void 0 && { description },
17305
+ ...value !== void 0 && { value }
17279
17306
  };
17280
17307
  }
17281
17308
  function splitTargetAndMeta(rest, metaAliasMap) {
@@ -26487,7 +26514,18 @@ function fitLabelToHeader(label, nodeWidth, maxLines) {
26487
26514
  const truncated = label.length > maxChars ? label.slice(0, maxChars - 1) + "\u2026" : label;
26488
26515
  return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
26489
26516
  }
26490
- function nodeColors(node, tagGroups, activeGroupName, palette, isDark, solid) {
26517
+ function nodeColors(node, tagGroups, activeGroupName, palette, isDark, value, solid) {
26518
+ const neutralFill = mix(palette.bg, palette.text, isDark ? 90 : 95);
26519
+ if (value.active) {
26520
+ const fill3 = node.value !== void 0 ? value.fillForValue(node.value) : neutralFill;
26521
+ const stroke3 = value.hue;
26522
+ const text2 = contrastText(
26523
+ fill3,
26524
+ palette.textOnFillLight,
26525
+ palette.textOnFillDark
26526
+ );
26527
+ return { fill: fill3, stroke: stroke3, text: text2 };
26528
+ }
26491
26529
  const tagColor = resolveTagColor(
26492
26530
  node.metadata,
26493
26531
  [...tagGroups],
@@ -26596,25 +26634,65 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26596
26634
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26597
26635
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26598
26636
  const sTitleY = sctx.structural(TITLE_Y);
26637
+ const nodeValues = parsed.nodes.filter((n) => n.value !== void 0).map((n) => n.value);
26638
+ const hasRamp = nodeValues.length > 0;
26639
+ const allNonNegative = hasRamp && nodeValues.every((v) => v >= 0);
26640
+ const rampMin = allNonNegative ? 0 : Math.min(...nodeValues);
26641
+ const rampMax = Math.max(...nodeValues);
26642
+ const rampHue = resolveColor(parsed.boxMetricColor ?? "", palette) ?? palette.primary;
26643
+ const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
26644
+ const fillForValue = (v) => {
26645
+ const t = rampMax > rampMin ? (v - rampMin) / (rampMax - rampMin) : 1;
26646
+ const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
26647
+ return mix(rampHue, rampBase, pct);
26648
+ };
26649
+ const VALUE_NAME = hasRamp ? parsed.boxMetric?.trim() || "Value" : null;
26650
+ const matchColorGroup = (v) => {
26651
+ const lv = v.trim().toLowerCase();
26652
+ if (lv === "" || lv === "none") return null;
26653
+ const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
26654
+ if (tg) return tg.name;
26655
+ if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
26656
+ return v;
26657
+ };
26658
+ const override = activeTagGroup;
26659
+ let activeGroup;
26660
+ if (override !== void 0) {
26661
+ activeGroup = override === null ? null : matchColorGroup(override);
26662
+ } else if (parsed.options["active-tag"] !== void 0) {
26663
+ activeGroup = matchColorGroup(parsed.options["active-tag"]);
26664
+ } else {
26665
+ activeGroup = VALUE_NAME ?? (parsed.tagGroups.length > 0 ? parsed.tagGroups[0].name : null);
26666
+ }
26667
+ const activeIsValue = VALUE_NAME !== null && activeGroup === VALUE_NAME;
26668
+ const valueGroup = VALUE_NAME !== null ? {
26669
+ name: VALUE_NAME,
26670
+ entries: [],
26671
+ gradient: {
26672
+ min: rampMin,
26673
+ max: rampMax,
26674
+ hue: rampHue,
26675
+ base: rampBase
26676
+ }
26677
+ } : null;
26678
+ const legendGroups = [
26679
+ ...valueGroup ? [valueGroup] : [],
26680
+ ...parsed.tagGroups
26681
+ ];
26599
26682
  const reserveHasDescriptions = parsed.nodes.some(
26600
26683
  (n) => n.description && n.description.length > 0
26601
26684
  );
26602
- const willRenderLegend = parsed.tagGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26685
+ const willRenderLegend = legendGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26603
26686
  const sLegendHeight = willRenderLegend ? sctx.structural(
26604
26687
  getMaxLegendReservedHeight(
26605
26688
  {
26606
- groups: parsed.tagGroups,
26689
+ groups: legendGroups,
26607
26690
  position: { placement: "top-center", titleRelation: "below-title" },
26608
26691
  mode: exportMode ? "export" : "preview"
26609
26692
  },
26610
26693
  width
26611
26694
  )
26612
26695
  ) : 0;
26613
- const activeGroup = resolveActiveTagGroup(
26614
- parsed.tagGroups,
26615
- parsed.options["active-tag"],
26616
- activeTagGroup
26617
- );
26618
26696
  const hidden = hiddenTagValues ?? parsed.initialHiddenTagValues;
26619
26697
  const nodeMap = /* @__PURE__ */ new Map();
26620
26698
  for (const node of parsed.nodes) nodeMap.set(node.label, node);
@@ -26625,7 +26703,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26625
26703
  const hasAnyDescriptions = parsed.nodes.some(
26626
26704
  (n) => n.description && n.description.length > 0
26627
26705
  );
26628
- const needsLegend = parsed.tagGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26706
+ const needsLegend = legendGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26629
26707
  const legendH = needsLegend ? sLegendHeight + 8 : 0;
26630
26708
  const groupLabelsSet = new Set(layout.groups.map((g) => g.label));
26631
26709
  let labelZoneExtension = 0;
@@ -26831,12 +26909,16 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26831
26909
  activeGroup,
26832
26910
  palette,
26833
26911
  isDark,
26912
+ { active: activeIsValue, hue: rampHue, fillForValue },
26834
26913
  parsed.options["solid-fill"] === "on"
26835
26914
  );
26836
26915
  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);
26837
26916
  for (const [key, val] of Object.entries(node.metadata)) {
26838
26917
  nodeG.attr(`data-tag-${key.toLowerCase()}`, val.toLowerCase());
26839
26918
  }
26919
+ if (node.value !== void 0) {
26920
+ nodeG.attr("data-value", node.value);
26921
+ }
26840
26922
  if (onClickItem) {
26841
26923
  nodeG.on("click", (event) => {
26842
26924
  const target = event.target;
@@ -26908,6 +26990,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26908
26990
  const tooltipText = fullText.length > 200 ? fullText.slice(0, 199) + "\u2026" : fullText;
26909
26991
  nodeG.append("title").text(tooltipText);
26910
26992
  }
26993
+ } else if (parsed.showValues && node.value !== void 0) {
26994
+ const valueLabel = parsed.boxMetric ? `${parsed.boxMetric}: ${node.value}` : String(node.value);
26995
+ const headerH = ln.height / 2;
26996
+ const sepY = -ln.height / 2 + headerH;
26997
+ const fitted = fitLabelToHeader(node.label, ln.width, 2);
26998
+ const labelLineH = fitted.fontSize * 1.3;
26999
+ const labelTotalH = fitted.lines.length * labelLineH;
27000
+ const headerCenterY = -ln.height / 2 + headerH / 2;
27001
+ for (let li = 0; li < fitted.lines.length; li++) {
27002
+ nodeG.append("text").attr("x", 0).attr(
27003
+ "y",
27004
+ headerCenterY - labelTotalH / 2 + labelLineH / 2 + li * labelLineH
27005
+ ).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]);
27006
+ }
27007
+ 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);
27008
+ 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);
26911
27009
  } else {
26912
27010
  const maxLabelLines = Math.max(
26913
27011
  2,
@@ -26920,11 +27018,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26920
27018
  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]);
26921
27019
  }
26922
27020
  }
27021
+ if (parsed.showValues && node.value !== void 0 && desc && desc.length > 0 && !hideDescriptions) {
27022
+ const valueText = String(node.value);
27023
+ const padX = 6;
27024
+ const padY = 5;
27025
+ const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
27026
+ const bh = VALUE_FONT_SIZE + 4;
27027
+ const bx = Math.max(-ln.width / 2 + 4, ln.width / 2 - bw - 4);
27028
+ const by = -ln.height / 2 + 4;
27029
+ 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);
27030
+ 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);
27031
+ }
26923
27032
  }
26924
27033
  const hasDescriptions = parsed.nodes.some(
26925
27034
  (n) => n.description && n.description.length > 0
26926
27035
  );
26927
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
27036
+ const hasLegend = legendGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26928
27037
  if (hasLegend) {
26929
27038
  let controlsGroup;
26930
27039
  if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
@@ -26942,7 +27051,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26942
27051
  };
26943
27052
  }
26944
27053
  const legendConfig = {
26945
- groups: parsed.tagGroups,
27054
+ groups: legendGroups,
26946
27055
  position: { placement: "top-center", titleRelation: "below-title" },
26947
27056
  mode: exportMode ? "export" : "preview",
26948
27057
  // Keep inactive sibling tag groups visible as collapsed pills so the user
@@ -26997,7 +27106,7 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
26997
27106
  }
26998
27107
  });
26999
27108
  }
27000
- var 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;
27109
+ var 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;
27001
27110
  var init_renderer6 = __esm({
27002
27111
  "src/boxes-and-lines/renderer.ts"() {
27003
27112
  "use strict";
@@ -27006,12 +27115,13 @@ var init_renderer6 = __esm({
27006
27115
  init_legend_layout();
27007
27116
  init_title_constants();
27008
27117
  init_color_utils();
27118
+ init_colors();
27009
27119
  init_tag_groups();
27010
27120
  init_inline_markdown();
27011
27121
  init_wrapped_desc();
27012
27122
  init_scaling();
27013
27123
  DIAGRAM_PADDING6 = 20;
27014
- NODE_FONT_SIZE = 13;
27124
+ NODE_FONT_SIZE = 11;
27015
27125
  MIN_NODE_FONT_SIZE = 9;
27016
27126
  EDGE_LABEL_FONT_SIZE4 = 11;
27017
27127
  EDGE_STROKE_WIDTH5 = 1.5;
@@ -27028,6 +27138,8 @@ var init_renderer6 = __esm({
27028
27138
  GROUP_RX = 8;
27029
27139
  GROUP_LABEL_FONT_SIZE = 14;
27030
27140
  GROUP_LABEL_ZONE = 32;
27141
+ RAMP_FLOOR = 15;
27142
+ VALUE_FONT_SIZE = 11;
27031
27143
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
27032
27144
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
27033
27145
  }
@@ -46969,7 +47081,11 @@ function resolveMap(parsed, data) {
46969
47081
  if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46970
47082
  }
46971
47083
  const containerUnion = unionExtent(containerBoxes, points);
46972
- if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
47084
+ if (containerUnion)
47085
+ extent2 = pad(
47086
+ clampContainerToCluster(containerUnion, points),
47087
+ PAD_FRACTION
47088
+ );
46973
47089
  }
46974
47090
  if (isPoiOnly) {
46975
47091
  const cx = (extent2[0][0] + extent2[1][0]) / 2;
@@ -47050,6 +47166,22 @@ function mostCommonCountry(regions, poiCountries) {
47050
47166
  }
47051
47167
  return best;
47052
47168
  }
47169
+ function clampContainerToCluster(container, points) {
47170
+ const poi = unionExtent([], points);
47171
+ if (!poi) return container;
47172
+ let [[west, south], [east, north]] = container;
47173
+ const [[pWest, pSouth], [pEast, pNorth]] = poi;
47174
+ south = Math.max(south, pSouth - CONTAINER_OVERSHOOT_DEG);
47175
+ north = Math.min(north, pNorth + CONTAINER_OVERSHOOT_DEG);
47176
+ if (east <= 180 && pEast <= 180) {
47177
+ west = Math.max(west, pWest - CONTAINER_OVERSHOOT_DEG);
47178
+ east = Math.min(east, pEast + CONTAINER_OVERSHOOT_DEG);
47179
+ }
47180
+ return [
47181
+ [west, south],
47182
+ [east, north]
47183
+ ];
47184
+ }
47053
47185
  function pad(e, frac) {
47054
47186
  const dLon = (e[1][0] - e[0][0]) * frac || 1;
47055
47187
  const dLat = (e[1][1] - e[0][1]) * frac || 1;
@@ -47062,7 +47194,7 @@ function firstError(diags) {
47062
47194
  const e = diags.find((d) => d.severity === "error");
47063
47195
  return e ? formatDgmoError(e) : null;
47064
47196
  }
47065
- 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;
47197
+ 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;
47066
47198
  var init_resolver2 = __esm({
47067
47199
  "src/map/resolver.ts"() {
47068
47200
  "use strict";
@@ -47075,6 +47207,7 @@ var init_resolver2 = __esm({
47075
47207
  WORLD_LAT_SOUTH = -58;
47076
47208
  WORLD_LAT_NORTH = 78;
47077
47209
  POI_ZOOM_FLOOR_DEG = 7;
47210
+ CONTAINER_OVERSHOOT_DEG = 8;
47078
47211
  US_NATIONAL_LON_SPAN = 48;
47079
47212
  REGION_ALIASES = {
47080
47213
  // Common everyday names → the Natural-Earth display name actually shipped.
@@ -47153,6 +47286,55 @@ var init_resolver2 = __esm({
47153
47286
  }
47154
47287
  });
47155
47288
 
47289
+ // src/map/legend-band.ts
47290
+ function mapLegendGroups(legend) {
47291
+ const ramp = legend.ramp;
47292
+ const scoreGroup = ramp ? {
47293
+ name: ramp.metric?.trim() || "Value",
47294
+ entries: [],
47295
+ gradient: {
47296
+ min: ramp.min,
47297
+ max: ramp.max,
47298
+ hue: ramp.hue,
47299
+ base: ramp.base
47300
+ }
47301
+ } : null;
47302
+ const tagGroups = legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
47303
+ return [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
47304
+ }
47305
+ function mapLegendConfig(groups, mode) {
47306
+ return {
47307
+ groups,
47308
+ position: { placement: "top-center", titleRelation: "below-title" },
47309
+ mode,
47310
+ showEmptyGroups: false,
47311
+ showInactivePills: true
47312
+ };
47313
+ }
47314
+ function mapLegendTop(hasTitle, hasSubtitle) {
47315
+ return (hasTitle ? TITLE_Y + TITLE_FONT_SIZE : 0) + (hasSubtitle ? TITLE_FONT_SIZE : 0) + LEGEND_TOP_GAP2;
47316
+ }
47317
+ function mapLegendBand(legend, opts) {
47318
+ if (!legend) return 0;
47319
+ const groups = mapLegendGroups(legend);
47320
+ if (groups.length === 0) return 0;
47321
+ const config = mapLegendConfig(groups, opts.mode);
47322
+ const state = { activeGroup: legend.activeGroup };
47323
+ const { height } = computeLegendLayout(config, state, opts.width);
47324
+ if (height <= 0) return 0;
47325
+ return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
47326
+ }
47327
+ var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
47328
+ var init_legend_band = __esm({
47329
+ "src/map/legend-band.ts"() {
47330
+ "use strict";
47331
+ init_legend_layout();
47332
+ init_title_constants();
47333
+ LEGEND_TOP_GAP2 = 8;
47334
+ LEGEND_BOTTOM_GAP2 = 10;
47335
+ }
47336
+ });
47337
+
47156
47338
  // src/map/colorize.ts
47157
47339
  function assignColors(isos, adjacency) {
47158
47340
  const sorted = [...isos].sort();
@@ -47593,6 +47775,38 @@ function parsePathRings(d) {
47593
47775
  if (cur.length) rings.push(cur);
47594
47776
  return rings;
47595
47777
  }
47778
+ function dropAntimeridianWrapSlivers(d, width, height) {
47779
+ const rings = parsePathRings(d);
47780
+ if (rings.length <= 1) return d;
47781
+ const eps = 0.75;
47782
+ const minArea = 3e-3 * width * height;
47783
+ const ringArea = (r) => {
47784
+ let s = 0;
47785
+ for (let i = 0; i < r.length; i++) {
47786
+ const a = r[i];
47787
+ const b = r[(i + 1) % r.length];
47788
+ s += a[0] * b[1] - b[0] * a[1];
47789
+ }
47790
+ return Math.abs(s) / 2;
47791
+ };
47792
+ const areas = rings.map(ringArea);
47793
+ const maxArea = Math.max(...areas);
47794
+ 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;
47795
+ let dropped = false;
47796
+ const kept = rings.filter((r, idx) => {
47797
+ if (areas[idx] >= maxArea || areas[idx] >= minArea) return true;
47798
+ const touches = r.some((p, i) => onVEdge(p, r[(i + 1) % r.length]));
47799
+ if (touches) {
47800
+ dropped = true;
47801
+ return false;
47802
+ }
47803
+ return true;
47804
+ });
47805
+ if (!dropped) return d;
47806
+ return kept.map(
47807
+ (r) => r.map((p, i) => (i ? "L" : "M") + p[0] + "," + p[1]).join("") + "Z"
47808
+ ).join("");
47809
+ }
47596
47810
  function layoutMap(resolved, data, size, opts) {
47597
47811
  const { palette, isDark } = opts;
47598
47812
  const { width, height } = size;
@@ -47676,7 +47890,7 @@ function layoutMap(resolved, data, size, opts) {
47676
47890
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
47677
47891
  const fillForValue = (s) => {
47678
47892
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
47679
- const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
47893
+ const pct = RAMP_FLOOR2 + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR2);
47680
47894
  return mix(rampHue, rampBase, pct);
47681
47895
  };
47682
47896
  const tagFill = (tags, groupName) => {
@@ -47712,12 +47926,43 @@ function layoutMap(resolved, data, size, opts) {
47712
47926
  return tagFill(r.tags, activeGroup) ?? neutralFill;
47713
47927
  };
47714
47928
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
47929
+ let legend = null;
47930
+ if (!resolved.directives.noLegend) {
47931
+ const legendTagGroups = resolved.tagGroups.map((g) => ({
47932
+ name: g.name,
47933
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
47934
+ }));
47935
+ if (legendTagGroups.length > 0 || hasRamp) {
47936
+ legend = {
47937
+ tagGroups: legendTagGroups,
47938
+ activeGroup,
47939
+ ...hasRamp && {
47940
+ ramp: {
47941
+ ...resolved.directives.regionMetric !== void 0 && {
47942
+ metric: resolved.directives.regionMetric
47943
+ },
47944
+ min: rampMin,
47945
+ max: rampMax,
47946
+ hue: rampHue,
47947
+ base: rampBase
47948
+ }
47949
+ }
47950
+ };
47951
+ }
47952
+ }
47715
47953
  const TITLE_GAP2 = 16;
47716
47954
  let topPad = FIT_PAD;
47717
47955
  if (resolved.title && resolved.pois.length > 0) {
47718
47956
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
47719
47957
  topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
47720
47958
  }
47959
+ const legendBand = mapLegendBand(legend, {
47960
+ width,
47961
+ mode: opts.legendMode ?? "preview",
47962
+ hasTitle: Boolean(resolved.title),
47963
+ hasSubtitle: Boolean(resolved.subtitle)
47964
+ });
47965
+ if (legendBand > topPad) topPad = legendBand;
47721
47966
  const fitBox = [
47722
47967
  [FIT_PAD, topPad],
47723
47968
  [
@@ -47735,10 +47980,11 @@ function layoutMap(resolved, data, size, opts) {
47735
47980
  const by0 = cb[0][1];
47736
47981
  const cw = cb[1][0] - bx0;
47737
47982
  const ch = cb[1][1] - by0;
47738
- const ox = fitBox[0][0];
47739
- const oy = fitBox[0][1];
47740
- const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
47741
- const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47983
+ const topReserve = resolved.title && resolved.pois.length > 0 || legendBand > 0 ? topPad : 0;
47984
+ const ox = 0;
47985
+ const oy = topReserve;
47986
+ const sx = cw > 0 ? width / cw : 1;
47987
+ const sy = ch > 0 ? (height - topReserve) / ch : 1;
47742
47988
  stretchParams = { sx, sy, ox, oy, bx0, by0 };
47743
47989
  const stretch = (x, y) => [
47744
47990
  ox + (x - bx0) * sx,
@@ -48011,7 +48257,8 @@ function layoutMap(resolved, data, size, opts) {
48011
48257
  const r = regionById.get(iso);
48012
48258
  const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
48013
48259
  if (!viewF) continue;
48014
- const d = path(viewF) ?? "";
48260
+ const raw = path(viewF) ?? "";
48261
+ const d = fitIsGlobal ? dropAntimeridianWrapSlivers(raw, width, height) : raw;
48015
48262
  if (!d) continue;
48016
48263
  const isThisLayer = r?.layer === layerKind;
48017
48264
  const isForeign = layerKind === "country" && usContext && iso !== "US";
@@ -48028,6 +48275,9 @@ function layoutMap(resolved, data, size, opts) {
48028
48275
  } else {
48029
48276
  label = f.properties?.name;
48030
48277
  }
48278
+ const labelAnchor = WORLD_LABEL_ANCHORS[iso];
48279
+ const c = labelAnchor ? project(labelAnchor[0], labelAnchor[1]) : path.centroid(viewF);
48280
+ const hasCentroid = c != null && Number.isFinite(c[0]) && Number.isFinite(c[1]);
48031
48281
  regions.push({
48032
48282
  id: iso,
48033
48283
  d,
@@ -48036,6 +48286,7 @@ function layoutMap(resolved, data, size, opts) {
48036
48286
  lineNumber,
48037
48287
  layer,
48038
48288
  ...label !== void 0 && { label },
48289
+ ...hasCentroid && { labelX: c[0], labelY: c[1] },
48039
48290
  ...isThisLayer && r.value !== void 0 && { value: r.value },
48040
48291
  ...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
48041
48292
  });
@@ -48476,10 +48727,6 @@ function layoutMap(resolved, data, size, opts) {
48476
48727
  lineNumber
48477
48728
  });
48478
48729
  };
48479
- const WORLD_LABEL_ANCHORS = {
48480
- US: [-98.5, 39.5]
48481
- // CONUS geographic centre (near Lebanon, Kansas)
48482
- };
48483
48730
  const REGION_LABEL_GAP = 2;
48484
48731
  const regionLabelRect = (cx, cy, text) => {
48485
48732
  const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
@@ -48797,30 +49044,6 @@ function layoutMap(resolved, data, size, opts) {
48797
49044
  });
48798
49045
  labels.push(...contextLabels);
48799
49046
  }
48800
- let legend = null;
48801
- if (!resolved.directives.noLegend) {
48802
- const tagGroups = resolved.tagGroups.map((g) => ({
48803
- name: g.name,
48804
- entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
48805
- }));
48806
- if (tagGroups.length > 0 || hasRamp) {
48807
- legend = {
48808
- tagGroups,
48809
- activeGroup,
48810
- ...hasRamp && {
48811
- ramp: {
48812
- ...resolved.directives.regionMetric !== void 0 && {
48813
- metric: resolved.directives.regionMetric
48814
- },
48815
- min: rampMin,
48816
- max: rampMax,
48817
- hue: rampHue,
48818
- base: rampBase
48819
- }
48820
- }
48821
- };
48822
- }
48823
- }
48824
49047
  return {
48825
49048
  width,
48826
49049
  height,
@@ -48845,7 +49068,7 @@ function layoutMap(resolved, data, size, opts) {
48845
49068
  diagnostics: []
48846
49069
  };
48847
49070
  }
48848
- var 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;
49071
+ var 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;
48849
49072
  var init_layout15 = __esm({
48850
49073
  "src/map/layout.ts"() {
48851
49074
  "use strict";
@@ -48856,15 +49079,20 @@ var init_layout15 = __esm({
48856
49079
  init_label_layout();
48857
49080
  init_legend_constants();
48858
49081
  init_title_constants();
49082
+ init_legend_band();
48859
49083
  init_context_labels();
48860
49084
  FIT_PAD = 24;
48861
- RAMP_FLOOR = 15;
49085
+ RAMP_FLOOR2 = 15;
48862
49086
  R_DEFAULT = 6;
48863
49087
  R_MIN = 4;
48864
49088
  R_MAX = 22;
48865
49089
  W_MIN = 1.25;
48866
49090
  W_MAX = 8;
48867
49091
  FONT2 = 11;
49092
+ WORLD_LABEL_ANCHORS = {
49093
+ US: [-98.5, 39.5]
49094
+ // CONUS geographic centre (near Lebanon, Kansas)
49095
+ };
48868
49096
  MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48869
49097
  MAX_COLUMN_ROWS = 7;
48870
49098
  REGION_LABEL_HALO_RATIO = 4.5;
@@ -48878,9 +49106,9 @@ var init_layout15 = __esm({
48878
49106
  COMPACT_WIDTH_PX = 480;
48879
49107
  RELIEF_MIN_AREA = 12;
48880
49108
  RELIEF_MIN_DIM = 2;
48881
- RELIEF_HATCH_SPACING = 2;
48882
- RELIEF_HATCH_WIDTH = 0.15;
48883
- RELIEF_HATCH_STRENGTH = 32;
49109
+ RELIEF_HATCH_SPACING = 1.5;
49110
+ RELIEF_HATCH_WIDTH = 0.2;
49111
+ RELIEF_HATCH_STRENGTH = 26;
48884
49112
  COASTLINE_RING_COUNT = 5;
48885
49113
  COASTLINE_D0 = 16e-4;
48886
49114
  COASTLINE_STEP = 28e-4;
@@ -48959,7 +49187,47 @@ function ringToPath(ring) {
48959
49187
  d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48960
49188
  return d + "Z";
48961
49189
  }
48962
- function coastlineOuterRings(regions, minExtent) {
49190
+ function polylineToPath(pts) {
49191
+ let d = "";
49192
+ for (let i = 0; i < pts.length; i++)
49193
+ d += (i ? "L" : "M") + pts[i][0] + "," + pts[i][1];
49194
+ return d;
49195
+ }
49196
+ function ringToCoastPaths(ring, frame) {
49197
+ if (!frame) return [ringToPath(ring)];
49198
+ const n = ring.length;
49199
+ const eps = 0.75;
49200
+ const onL = (x) => Math.abs(x) <= eps;
49201
+ const onR = (x) => Math.abs(x - frame.w) <= eps;
49202
+ const onT = (y) => Math.abs(y) <= eps;
49203
+ const onB = (y) => Math.abs(y - frame.h) <= eps;
49204
+ 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]);
49205
+ let firstBreak = -1;
49206
+ for (let i = 0; i < n; i++)
49207
+ if (isFrameEdge(ring[i], ring[(i + 1) % n])) {
49208
+ firstBreak = i;
49209
+ break;
49210
+ }
49211
+ if (firstBreak === -1) return [ringToPath(ring)];
49212
+ const paths = [];
49213
+ let cur = [];
49214
+ const start = (firstBreak + 1) % n;
49215
+ for (let k = 0; k < n; k++) {
49216
+ const i = (start + k) % n;
49217
+ const a = ring[i];
49218
+ const b = ring[(i + 1) % n];
49219
+ if (isFrameEdge(a, b)) {
49220
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
49221
+ cur = [];
49222
+ continue;
49223
+ }
49224
+ if (cur.length === 0) cur.push(a);
49225
+ cur.push(b);
49226
+ }
49227
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
49228
+ return paths;
49229
+ }
49230
+ function coastlineOuterRings(regions, minExtent, frame) {
48963
49231
  const paths = [];
48964
49232
  for (const r of regions) {
48965
49233
  const rings = parsePathRings(r.d);
@@ -48982,7 +49250,7 @@ function coastlineOuterRings(regions, minExtent) {
48982
49250
  for (let j = 0; j < rings.length; j++)
48983
49251
  if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48984
49252
  if (depth % 2 === 1) continue;
48985
- paths.push(ringToPath(ring));
49253
+ paths.push(...ringToCoastPaths(ring, frame));
48986
49254
  }
48987
49255
  }
48988
49256
  return paths;
@@ -49012,6 +49280,9 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49012
49280
  // stretch-distorting. The in-app preview pane passes no exportDims → unset →
49013
49281
  // keeps the global stretch-fill.
49014
49282
  preferContain: exportDims?.preferContain ?? false,
49283
+ // Reserve the legend band for the mode actually drawn below (export shows
49284
+ // only the active group; preview keeps the inactive pills).
49285
+ legendMode: exportDims ? "export" : "preview",
49015
49286
  ...activeGroupOverride !== void 0 && {
49016
49287
  activeGroup: activeGroupOverride
49017
49288
  }
@@ -49026,6 +49297,10 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49026
49297
  const drawRegion = (g, r, strokeWidth) => {
49027
49298
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
49028
49299
  if (r.label) p.attr("data-region-name", r.label);
49300
+ if (r.id && r.id !== "lake") p.attr("data-iso", r.id);
49301
+ if (r.labelX !== void 0 && r.labelY !== void 0) {
49302
+ p.attr("data-label-x", r.labelX).attr("data-label-y", r.labelY);
49303
+ }
49029
49304
  if (r.layer !== "base") {
49030
49305
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
49031
49306
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -49055,7 +49330,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49055
49330
  const landClip = defs.append("clipPath").attr("id", landClipId);
49056
49331
  for (const r of layout.regions)
49057
49332
  if (r.id !== "lake") landClip.append("path").attr("d", r.d);
49058
- 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");
49333
+ 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");
49059
49334
  for (let y = h.spacing; y < height; y += h.spacing) {
49060
49335
  gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
49061
49336
  }
@@ -49076,10 +49351,16 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49076
49351
  mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
49077
49352
  }
49078
49353
  }
49079
- const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
49354
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").style("pointer-events", "none").attr("mask", `url(#${maskId})`);
49080
49355
  appendWaterLines(
49081
49356
  gWater,
49082
- coastlineOuterRings(layout.regions, cs.minExtent),
49357
+ // Pass the canvas frame so edges collinear with it (the antimeridian on a
49358
+ // world map, regional clipExtent cuts) don't get ringed as fake coast —
49359
+ // land runs cleanly to the render-area edge.
49360
+ coastlineOuterRings(layout.regions, cs.minExtent, {
49361
+ w: width,
49362
+ h: height
49363
+ }),
49083
49364
  cs,
49084
49365
  layout.background
49085
49366
  );
@@ -49093,7 +49374,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49093
49374
  gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
49094
49375
  }
49095
49376
  if (layout.rivers.length) {
49096
- const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
49377
+ const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none").style("pointer-events", "none");
49097
49378
  for (const r of layout.rivers) {
49098
49379
  gRivers.append("path").attr("d", r.d).attr("stroke", r.color).attr("stroke-width", r.width).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
49099
49380
  }
@@ -49134,7 +49415,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49134
49415
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
49135
49416
  clip.append("path").attr("d", d);
49136
49417
  }
49137
- 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})`);
49418
+ 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})`);
49138
49419
  appendWaterLines(
49139
49420
  gInsetWater,
49140
49421
  coastlineOuterRings(layout.insetRegions, cs.minExtent),
@@ -49293,30 +49574,12 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49293
49574
  if (layout.legend) {
49294
49575
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
49295
49576
  const legendG = svg.append("g").attr("class", "dgmo-map-legend").attr("transform", `translate(0, ${legendY})`);
49296
- const ramp = layout.legend.ramp;
49297
- const scoreGroup = ramp ? {
49298
- name: ramp.metric?.trim() || "Value",
49299
- entries: [],
49300
- gradient: {
49301
- min: ramp.min,
49302
- max: ramp.max,
49303
- hue: ramp.hue,
49304
- base: ramp.base
49305
- }
49306
- } : null;
49307
- const tagGroups = layout.legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
49308
- const groups = [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
49577
+ const groups = mapLegendGroups(layout.legend);
49309
49578
  if (groups.length > 0) {
49310
- const config = {
49579
+ const config = mapLegendConfig(
49311
49580
  groups,
49312
- position: { placement: "top-center", titleRelation: "below-title" },
49313
- mode: exportDims ? "export" : "preview",
49314
- showEmptyGroups: false,
49315
- // Keep inactive siblings visible as pills so the user can click to flip
49316
- // the active colouring dimension (preview only — export shows just the
49317
- // active group).
49318
- showInactivePills: true
49319
- };
49581
+ exportDims ? "export" : "preview"
49582
+ );
49320
49583
  const state = { activeGroup: layout.legend.activeGroup };
49321
49584
  renderLegendD3(legendG, config, state, palette, isDark, void 0, width);
49322
49585
  }
@@ -49360,6 +49623,7 @@ var init_renderer16 = __esm({
49360
49623
  init_title_constants();
49361
49624
  init_color_utils();
49362
49625
  init_legend_d3();
49626
+ init_legend_band();
49363
49627
  init_layout15();
49364
49628
  LABEL_FONT = 11;
49365
49629
  }
@@ -49381,9 +49645,10 @@ function mapContentAspect(resolved, data, ref = REF) {
49381
49645
  const aspect = w / h;
49382
49646
  return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49383
49647
  }
49384
- function mapExportDimensions(resolved, data, baseWidth = 1200) {
49385
- const raw = mapContentAspect(resolved, data);
49386
- const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49648
+ function mapExportDimensions(resolved, data, baseWidth = 1200, aspectOverride) {
49649
+ const useOverride = aspectOverride !== void 0 && Number.isFinite(aspectOverride) && aspectOverride > 0;
49650
+ const raw = useOverride ? aspectOverride : mapContentAspect(resolved, data);
49651
+ const clamped = useOverride ? raw : Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49387
49652
  const width = baseWidth;
49388
49653
  let height = Math.round(width / clamped);
49389
49654
  let chromeReserve = 0;
@@ -49396,7 +49661,7 @@ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49396
49661
  height = Math.round(chromeReserve + MIN_MAP_BAND);
49397
49662
  floored = true;
49398
49663
  }
49399
- const preferContain = clamped !== raw || floored;
49664
+ const preferContain = useOverride ? floored : clamped !== raw || floored;
49400
49665
  return { width, height, preferContain };
49401
49666
  }
49402
49667
  var FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
@@ -55106,7 +55371,6 @@ function renderTimelineTagLegendOverlay(container, parsed, palette, isDark, setu
55106
55371
  function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, setup, hovers, onClickItem, _exportDims, _swimlaneTagGroup, _activeTagGroup, _onTagStateChange, _viewMode) {
55107
55372
  const {
55108
55373
  width,
55109
- height,
55110
55374
  tooltip,
55111
55375
  solid,
55112
55376
  textColor,
@@ -55155,10 +55419,11 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
55155
55419
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
55156
55420
  const eraLabelY = eraReserve ? -(topScaleH + markerReserve + ERA_ROW_H / 2) : 0;
55157
55421
  const innerWidth = width - margin.left - margin.right;
55158
- const innerHeight = height - margin.top - margin.bottom;
55159
- const rowH = Math.min(ctx.structural(28), innerHeight / sorted.length);
55422
+ const rowH = ctx.structural(28);
55423
+ const innerHeight = rowH * sorted.length;
55424
+ const usedHeight = margin.top + innerHeight + margin.bottom;
55160
55425
  const xScale = d3Scale2.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
55161
- 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);
55426
+ 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);
55162
55427
  if (ctx.isBelowFloor) {
55163
55428
  svg.attr("width", "100%");
55164
55429
  }
@@ -57735,7 +58000,12 @@ async function renderForExport(content, theme, palette, viewState, options) {
57735
58000
  }
57736
58001
  }
57737
58002
  const mapResolved = resolveMap2(mapParsed, mapData);
57738
- const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
58003
+ const dims2 = mapExportDimensions2(
58004
+ mapResolved,
58005
+ mapData,
58006
+ EXPORT_WIDTH,
58007
+ options?.mapAspect
58008
+ );
57739
58009
  const container2 = createExportContainer(dims2.width, dims2.height);
57740
58010
  renderMapForExport2(
57741
58011
  container2,
@@ -60244,7 +60514,9 @@ var COMPLETION_REGISTRY = /* @__PURE__ */ new Map([
60244
60514
  withGlobals({
60245
60515
  direction: { description: "Layout direction", values: ["LR", "TB"] },
60246
60516
  "active-tag": { description: "Active tag group name" },
60247
- hide: { description: "Hide tag:value pairs" }
60517
+ hide: { description: "Hide tag:value pairs" },
60518
+ "box-metric": { description: "Metric label for the value ramp" },
60519
+ "show-values": { description: "Print box values as text" }
60248
60520
  })
60249
60521
  ],
60250
60522
  [
@@ -60472,13 +60744,10 @@ var PIPE_METADATA = /* @__PURE__ */ new Map([
60472
60744
  "boxes-and-lines",
60473
60745
  {
60474
60746
  node: {
60475
- description: { description: "Node description text" }
60747
+ description: { description: "Node description text" },
60748
+ value: { description: "Numeric value for the metric ramp" }
60476
60749
  },
60477
- edge: {
60478
- width: { description: "Edge stroke width in pixels" },
60479
- split: { description: "Traffic split percentage" },
60480
- fanout: { description: "Fanout multiplier (integer >= 1)" }
60481
- }
60750
+ edge: {}
60482
60751
  }
60483
60752
  ],
60484
60753
  [