@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/auto.cjs CHANGED
@@ -820,9 +820,7 @@ var init_reserved_key_registry = __esm({
820
820
  BOXES_AND_LINES_REGISTRY = staticRegistry([
821
821
  "color",
822
822
  "description",
823
- "width",
824
- "split",
825
- "fanout"
823
+ "value"
826
824
  ]);
827
825
  TIMELINE_REGISTRY = staticRegistry([
828
826
  "color",
@@ -16824,6 +16822,21 @@ function parseBoxesAndLines(content) {
16824
16822
  }
16825
16823
  continue;
16826
16824
  }
16825
+ if (!contentStarted) {
16826
+ const metricMatch = trimmed.match(/^box-metric\s+(.+)$/i);
16827
+ if (metricMatch) {
16828
+ const { label, colorName } = peelTrailingColorName(
16829
+ metricMatch[1].trim()
16830
+ );
16831
+ result.boxMetric = label;
16832
+ if (colorName !== void 0) result.boxMetricColor = colorName;
16833
+ continue;
16834
+ }
16835
+ if (/^show-values$/i.test(trimmed)) {
16836
+ result.showValues = true;
16837
+ continue;
16838
+ }
16839
+ }
16827
16840
  if (!contentStarted) {
16828
16841
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
16829
16842
  if (optMatch) {
@@ -17202,6 +17215,19 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
17202
17215
  description = [metadata["description"]];
17203
17216
  delete metadata["description"];
17204
17217
  }
17218
+ let value;
17219
+ if (metadata["value"] !== void 0) {
17220
+ const raw = metadata["value"];
17221
+ const num = Number(raw);
17222
+ if (Number.isFinite(num)) {
17223
+ value = num;
17224
+ } else {
17225
+ diagnostics.push(
17226
+ makeDgmoError(lineNum, `value must be a number (got "${raw}")`, "error")
17227
+ );
17228
+ }
17229
+ delete metadata["value"];
17230
+ }
17205
17231
  if (split.alias) {
17206
17232
  nameAliasMap?.set(normalizeName(split.alias), label);
17207
17233
  }
@@ -17210,7 +17236,8 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
17210
17236
  label,
17211
17237
  lineNumber: lineNum,
17212
17238
  metadata,
17213
- ...description !== void 0 && { description }
17239
+ ...description !== void 0 && { description },
17240
+ ...value !== void 0 && { value }
17214
17241
  };
17215
17242
  }
17216
17243
  function splitTargetAndMeta(rest, metaAliasMap) {
@@ -26330,7 +26357,18 @@ function fitLabelToHeader(label, nodeWidth, maxLines) {
26330
26357
  const truncated = label.length > maxChars ? label.slice(0, maxChars - 1) + "\u2026" : label;
26331
26358
  return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
26332
26359
  }
26333
- function nodeColors(node, tagGroups, activeGroupName, palette, isDark, solid) {
26360
+ function nodeColors(node, tagGroups, activeGroupName, palette, isDark, value, solid) {
26361
+ const neutralFill = mix(palette.bg, palette.text, isDark ? 90 : 95);
26362
+ if (value.active) {
26363
+ const fill3 = node.value !== void 0 ? value.fillForValue(node.value) : neutralFill;
26364
+ const stroke3 = value.hue;
26365
+ const text2 = contrastText(
26366
+ fill3,
26367
+ palette.textOnFillLight,
26368
+ palette.textOnFillDark
26369
+ );
26370
+ return { fill: fill3, stroke: stroke3, text: text2 };
26371
+ }
26334
26372
  const tagColor = resolveTagColor(
26335
26373
  node.metadata,
26336
26374
  [...tagGroups],
@@ -26439,25 +26477,65 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26439
26477
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26440
26478
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26441
26479
  const sTitleY = sctx.structural(TITLE_Y);
26480
+ const nodeValues = parsed.nodes.filter((n) => n.value !== void 0).map((n) => n.value);
26481
+ const hasRamp = nodeValues.length > 0;
26482
+ const allNonNegative = hasRamp && nodeValues.every((v) => v >= 0);
26483
+ const rampMin = allNonNegative ? 0 : Math.min(...nodeValues);
26484
+ const rampMax = Math.max(...nodeValues);
26485
+ const rampHue = resolveColor(parsed.boxMetricColor ?? "", palette) ?? palette.primary;
26486
+ const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
26487
+ const fillForValue = (v) => {
26488
+ const t = rampMax > rampMin ? (v - rampMin) / (rampMax - rampMin) : 1;
26489
+ const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
26490
+ return mix(rampHue, rampBase, pct);
26491
+ };
26492
+ const VALUE_NAME = hasRamp ? parsed.boxMetric?.trim() || "Value" : null;
26493
+ const matchColorGroup = (v) => {
26494
+ const lv = v.trim().toLowerCase();
26495
+ if (lv === "" || lv === "none") return null;
26496
+ const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
26497
+ if (tg) return tg.name;
26498
+ if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
26499
+ return v;
26500
+ };
26501
+ const override = activeTagGroup;
26502
+ let activeGroup;
26503
+ if (override !== void 0) {
26504
+ activeGroup = override === null ? null : matchColorGroup(override);
26505
+ } else if (parsed.options["active-tag"] !== void 0) {
26506
+ activeGroup = matchColorGroup(parsed.options["active-tag"]);
26507
+ } else {
26508
+ activeGroup = VALUE_NAME ?? (parsed.tagGroups.length > 0 ? parsed.tagGroups[0].name : null);
26509
+ }
26510
+ const activeIsValue = VALUE_NAME !== null && activeGroup === VALUE_NAME;
26511
+ const valueGroup = VALUE_NAME !== null ? {
26512
+ name: VALUE_NAME,
26513
+ entries: [],
26514
+ gradient: {
26515
+ min: rampMin,
26516
+ max: rampMax,
26517
+ hue: rampHue,
26518
+ base: rampBase
26519
+ }
26520
+ } : null;
26521
+ const legendGroups = [
26522
+ ...valueGroup ? [valueGroup] : [],
26523
+ ...parsed.tagGroups
26524
+ ];
26442
26525
  const reserveHasDescriptions = parsed.nodes.some(
26443
26526
  (n) => n.description && n.description.length > 0
26444
26527
  );
26445
- const willRenderLegend = parsed.tagGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26528
+ const willRenderLegend = legendGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26446
26529
  const sLegendHeight = willRenderLegend ? sctx.structural(
26447
26530
  getMaxLegendReservedHeight(
26448
26531
  {
26449
- groups: parsed.tagGroups,
26532
+ groups: legendGroups,
26450
26533
  position: { placement: "top-center", titleRelation: "below-title" },
26451
26534
  mode: exportMode ? "export" : "preview"
26452
26535
  },
26453
26536
  width
26454
26537
  )
26455
26538
  ) : 0;
26456
- const activeGroup = resolveActiveTagGroup(
26457
- parsed.tagGroups,
26458
- parsed.options["active-tag"],
26459
- activeTagGroup
26460
- );
26461
26539
  const hidden = hiddenTagValues ?? parsed.initialHiddenTagValues;
26462
26540
  const nodeMap = /* @__PURE__ */ new Map();
26463
26541
  for (const node of parsed.nodes) nodeMap.set(node.label, node);
@@ -26468,7 +26546,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26468
26546
  const hasAnyDescriptions = parsed.nodes.some(
26469
26547
  (n) => n.description && n.description.length > 0
26470
26548
  );
26471
- const needsLegend = parsed.tagGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26549
+ const needsLegend = legendGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26472
26550
  const legendH = needsLegend ? sLegendHeight + 8 : 0;
26473
26551
  const groupLabelsSet = new Set(layout.groups.map((g) => g.label));
26474
26552
  let labelZoneExtension = 0;
@@ -26674,12 +26752,16 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26674
26752
  activeGroup,
26675
26753
  palette,
26676
26754
  isDark,
26755
+ { active: activeIsValue, hue: rampHue, fillForValue },
26677
26756
  parsed.options["solid-fill"] === "on"
26678
26757
  );
26679
26758
  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);
26680
26759
  for (const [key, val] of Object.entries(node.metadata)) {
26681
26760
  nodeG.attr(`data-tag-${key.toLowerCase()}`, val.toLowerCase());
26682
26761
  }
26762
+ if (node.value !== void 0) {
26763
+ nodeG.attr("data-value", node.value);
26764
+ }
26683
26765
  if (onClickItem) {
26684
26766
  nodeG.on("click", (event) => {
26685
26767
  const target = event.target;
@@ -26751,6 +26833,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26751
26833
  const tooltipText = fullText.length > 200 ? fullText.slice(0, 199) + "\u2026" : fullText;
26752
26834
  nodeG.append("title").text(tooltipText);
26753
26835
  }
26836
+ } else if (parsed.showValues && node.value !== void 0) {
26837
+ const valueLabel = parsed.boxMetric ? `${parsed.boxMetric}: ${node.value}` : String(node.value);
26838
+ const headerH = ln.height / 2;
26839
+ const sepY = -ln.height / 2 + headerH;
26840
+ const fitted = fitLabelToHeader(node.label, ln.width, 2);
26841
+ const labelLineH = fitted.fontSize * 1.3;
26842
+ const labelTotalH = fitted.lines.length * labelLineH;
26843
+ const headerCenterY = -ln.height / 2 + headerH / 2;
26844
+ for (let li = 0; li < fitted.lines.length; li++) {
26845
+ nodeG.append("text").attr("x", 0).attr(
26846
+ "y",
26847
+ headerCenterY - labelTotalH / 2 + labelLineH / 2 + li * labelLineH
26848
+ ).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]);
26849
+ }
26850
+ 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);
26851
+ 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);
26754
26852
  } else {
26755
26853
  const maxLabelLines = Math.max(
26756
26854
  2,
@@ -26763,11 +26861,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26763
26861
  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]);
26764
26862
  }
26765
26863
  }
26864
+ if (parsed.showValues && node.value !== void 0 && desc && desc.length > 0 && !hideDescriptions) {
26865
+ const valueText = String(node.value);
26866
+ const padX = 6;
26867
+ const padY = 5;
26868
+ const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
26869
+ const bh = VALUE_FONT_SIZE + 4;
26870
+ const bx = Math.max(-ln.width / 2 + 4, ln.width / 2 - bw - 4);
26871
+ const by = -ln.height / 2 + 4;
26872
+ 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);
26873
+ 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);
26874
+ }
26766
26875
  }
26767
26876
  const hasDescriptions = parsed.nodes.some(
26768
26877
  (n) => n.description && n.description.length > 0
26769
26878
  );
26770
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26879
+ const hasLegend = legendGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26771
26880
  if (hasLegend) {
26772
26881
  let controlsGroup;
26773
26882
  if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
@@ -26785,7 +26894,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26785
26894
  };
26786
26895
  }
26787
26896
  const legendConfig = {
26788
- groups: parsed.tagGroups,
26897
+ groups: legendGroups,
26789
26898
  position: { placement: "top-center", titleRelation: "below-title" },
26790
26899
  mode: exportMode ? "export" : "preview",
26791
26900
  // Keep inactive sibling tag groups visible as collapsed pills so the user
@@ -26840,7 +26949,7 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
26840
26949
  }
26841
26950
  });
26842
26951
  }
26843
- 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;
26952
+ 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;
26844
26953
  var init_renderer6 = __esm({
26845
26954
  "src/boxes-and-lines/renderer.ts"() {
26846
26955
  "use strict";
@@ -26851,12 +26960,13 @@ var init_renderer6 = __esm({
26851
26960
  init_legend_layout();
26852
26961
  init_title_constants();
26853
26962
  init_color_utils();
26963
+ init_colors();
26854
26964
  init_tag_groups();
26855
26965
  init_inline_markdown();
26856
26966
  init_wrapped_desc();
26857
26967
  init_scaling();
26858
26968
  DIAGRAM_PADDING6 = 20;
26859
- NODE_FONT_SIZE = 13;
26969
+ NODE_FONT_SIZE = 11;
26860
26970
  MIN_NODE_FONT_SIZE = 9;
26861
26971
  EDGE_LABEL_FONT_SIZE4 = 11;
26862
26972
  EDGE_STROKE_WIDTH5 = 1.5;
@@ -26873,6 +26983,8 @@ var init_renderer6 = __esm({
26873
26983
  GROUP_RX = 8;
26874
26984
  GROUP_LABEL_FONT_SIZE = 14;
26875
26985
  GROUP_LABEL_ZONE = 32;
26986
+ RAMP_FLOOR = 15;
26987
+ VALUE_FONT_SIZE = 11;
26876
26988
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
26877
26989
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
26878
26990
  }
@@ -46591,7 +46703,11 @@ function resolveMap(parsed, data) {
46591
46703
  if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46592
46704
  }
46593
46705
  const containerUnion = unionExtent(containerBoxes, points);
46594
- if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46706
+ if (containerUnion)
46707
+ extent2 = pad(
46708
+ clampContainerToCluster(containerUnion, points),
46709
+ PAD_FRACTION
46710
+ );
46595
46711
  }
46596
46712
  if (isPoiOnly) {
46597
46713
  const cx = (extent2[0][0] + extent2[1][0]) / 2;
@@ -46672,6 +46788,22 @@ function mostCommonCountry(regions, poiCountries) {
46672
46788
  }
46673
46789
  return best;
46674
46790
  }
46791
+ function clampContainerToCluster(container, points) {
46792
+ const poi = unionExtent([], points);
46793
+ if (!poi) return container;
46794
+ let [[west, south], [east, north]] = container;
46795
+ const [[pWest, pSouth], [pEast, pNorth]] = poi;
46796
+ south = Math.max(south, pSouth - CONTAINER_OVERSHOOT_DEG);
46797
+ north = Math.min(north, pNorth + CONTAINER_OVERSHOOT_DEG);
46798
+ if (east <= 180 && pEast <= 180) {
46799
+ west = Math.max(west, pWest - CONTAINER_OVERSHOOT_DEG);
46800
+ east = Math.min(east, pEast + CONTAINER_OVERSHOOT_DEG);
46801
+ }
46802
+ return [
46803
+ [west, south],
46804
+ [east, north]
46805
+ ];
46806
+ }
46675
46807
  function pad(e, frac) {
46676
46808
  const dLon = (e[1][0] - e[0][0]) * frac || 1;
46677
46809
  const dLat = (e[1][1] - e[0][1]) * frac || 1;
@@ -46684,7 +46816,7 @@ function firstError(diags) {
46684
46816
  const e = diags.find((d) => d.severity === "error");
46685
46817
  return e ? formatDgmoError(e) : null;
46686
46818
  }
46687
- 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;
46819
+ 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;
46688
46820
  var init_resolver2 = __esm({
46689
46821
  "src/map/resolver.ts"() {
46690
46822
  "use strict";
@@ -46697,6 +46829,7 @@ var init_resolver2 = __esm({
46697
46829
  WORLD_LAT_SOUTH = -58;
46698
46830
  WORLD_LAT_NORTH = 78;
46699
46831
  POI_ZOOM_FLOOR_DEG = 7;
46832
+ CONTAINER_OVERSHOOT_DEG = 8;
46700
46833
  US_NATIONAL_LON_SPAN = 48;
46701
46834
  REGION_ALIASES = {
46702
46835
  // Common everyday names → the Natural-Earth display name actually shipped.
@@ -46775,6 +46908,55 @@ var init_resolver2 = __esm({
46775
46908
  }
46776
46909
  });
46777
46910
 
46911
+ // src/map/legend-band.ts
46912
+ function mapLegendGroups(legend) {
46913
+ const ramp = legend.ramp;
46914
+ const scoreGroup = ramp ? {
46915
+ name: ramp.metric?.trim() || "Value",
46916
+ entries: [],
46917
+ gradient: {
46918
+ min: ramp.min,
46919
+ max: ramp.max,
46920
+ hue: ramp.hue,
46921
+ base: ramp.base
46922
+ }
46923
+ } : null;
46924
+ const tagGroups = legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
46925
+ return [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
46926
+ }
46927
+ function mapLegendConfig(groups, mode) {
46928
+ return {
46929
+ groups,
46930
+ position: { placement: "top-center", titleRelation: "below-title" },
46931
+ mode,
46932
+ showEmptyGroups: false,
46933
+ showInactivePills: true
46934
+ };
46935
+ }
46936
+ function mapLegendTop(hasTitle, hasSubtitle) {
46937
+ return (hasTitle ? TITLE_Y + TITLE_FONT_SIZE : 0) + (hasSubtitle ? TITLE_FONT_SIZE : 0) + LEGEND_TOP_GAP2;
46938
+ }
46939
+ function mapLegendBand(legend, opts) {
46940
+ if (!legend) return 0;
46941
+ const groups = mapLegendGroups(legend);
46942
+ if (groups.length === 0) return 0;
46943
+ const config = mapLegendConfig(groups, opts.mode);
46944
+ const state = { activeGroup: legend.activeGroup };
46945
+ const { height } = computeLegendLayout(config, state, opts.width);
46946
+ if (height <= 0) return 0;
46947
+ return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
46948
+ }
46949
+ var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
46950
+ var init_legend_band = __esm({
46951
+ "src/map/legend-band.ts"() {
46952
+ "use strict";
46953
+ init_legend_layout();
46954
+ init_title_constants();
46955
+ LEGEND_TOP_GAP2 = 8;
46956
+ LEGEND_BOTTOM_GAP2 = 10;
46957
+ }
46958
+ });
46959
+
46778
46960
  // src/map/colorize.ts
46779
46961
  function assignColors(isos, adjacency) {
46780
46962
  const sorted = [...isos].sort();
@@ -47204,6 +47386,38 @@ function parsePathRings(d) {
47204
47386
  if (cur.length) rings.push(cur);
47205
47387
  return rings;
47206
47388
  }
47389
+ function dropAntimeridianWrapSlivers(d, width, height) {
47390
+ const rings = parsePathRings(d);
47391
+ if (rings.length <= 1) return d;
47392
+ const eps = 0.75;
47393
+ const minArea = 3e-3 * width * height;
47394
+ const ringArea = (r) => {
47395
+ let s = 0;
47396
+ for (let i = 0; i < r.length; i++) {
47397
+ const a = r[i];
47398
+ const b = r[(i + 1) % r.length];
47399
+ s += a[0] * b[1] - b[0] * a[1];
47400
+ }
47401
+ return Math.abs(s) / 2;
47402
+ };
47403
+ const areas = rings.map(ringArea);
47404
+ const maxArea = Math.max(...areas);
47405
+ 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;
47406
+ let dropped = false;
47407
+ const kept = rings.filter((r, idx) => {
47408
+ if (areas[idx] >= maxArea || areas[idx] >= minArea) return true;
47409
+ const touches = r.some((p, i) => onVEdge(p, r[(i + 1) % r.length]));
47410
+ if (touches) {
47411
+ dropped = true;
47412
+ return false;
47413
+ }
47414
+ return true;
47415
+ });
47416
+ if (!dropped) return d;
47417
+ return kept.map(
47418
+ (r) => r.map((p, i) => (i ? "L" : "M") + p[0] + "," + p[1]).join("") + "Z"
47419
+ ).join("");
47420
+ }
47207
47421
  function layoutMap(resolved, data, size, opts) {
47208
47422
  const { palette, isDark } = opts;
47209
47423
  const { width, height } = size;
@@ -47287,7 +47501,7 @@ function layoutMap(resolved, data, size, opts) {
47287
47501
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
47288
47502
  const fillForValue = (s) => {
47289
47503
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
47290
- const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
47504
+ const pct = RAMP_FLOOR2 + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR2);
47291
47505
  return mix(rampHue, rampBase, pct);
47292
47506
  };
47293
47507
  const tagFill = (tags, groupName) => {
@@ -47323,12 +47537,43 @@ function layoutMap(resolved, data, size, opts) {
47323
47537
  return tagFill(r.tags, activeGroup) ?? neutralFill;
47324
47538
  };
47325
47539
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
47540
+ let legend = null;
47541
+ if (!resolved.directives.noLegend) {
47542
+ const legendTagGroups = resolved.tagGroups.map((g) => ({
47543
+ name: g.name,
47544
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
47545
+ }));
47546
+ if (legendTagGroups.length > 0 || hasRamp) {
47547
+ legend = {
47548
+ tagGroups: legendTagGroups,
47549
+ activeGroup,
47550
+ ...hasRamp && {
47551
+ ramp: {
47552
+ ...resolved.directives.regionMetric !== void 0 && {
47553
+ metric: resolved.directives.regionMetric
47554
+ },
47555
+ min: rampMin,
47556
+ max: rampMax,
47557
+ hue: rampHue,
47558
+ base: rampBase
47559
+ }
47560
+ }
47561
+ };
47562
+ }
47563
+ }
47326
47564
  const TITLE_GAP2 = 16;
47327
47565
  let topPad = FIT_PAD;
47328
47566
  if (resolved.title && resolved.pois.length > 0) {
47329
47567
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
47330
47568
  topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
47331
47569
  }
47570
+ const legendBand = mapLegendBand(legend, {
47571
+ width,
47572
+ mode: opts.legendMode ?? "preview",
47573
+ hasTitle: Boolean(resolved.title),
47574
+ hasSubtitle: Boolean(resolved.subtitle)
47575
+ });
47576
+ if (legendBand > topPad) topPad = legendBand;
47332
47577
  const fitBox = [
47333
47578
  [FIT_PAD, topPad],
47334
47579
  [
@@ -47346,10 +47591,11 @@ function layoutMap(resolved, data, size, opts) {
47346
47591
  const by0 = cb[0][1];
47347
47592
  const cw = cb[1][0] - bx0;
47348
47593
  const ch = cb[1][1] - by0;
47349
- const ox = fitBox[0][0];
47350
- const oy = fitBox[0][1];
47351
- const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
47352
- const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47594
+ const topReserve = resolved.title && resolved.pois.length > 0 || legendBand > 0 ? topPad : 0;
47595
+ const ox = 0;
47596
+ const oy = topReserve;
47597
+ const sx = cw > 0 ? width / cw : 1;
47598
+ const sy = ch > 0 ? (height - topReserve) / ch : 1;
47353
47599
  stretchParams = { sx, sy, ox, oy, bx0, by0 };
47354
47600
  const stretch = (x, y) => [
47355
47601
  ox + (x - bx0) * sx,
@@ -47622,7 +47868,8 @@ function layoutMap(resolved, data, size, opts) {
47622
47868
  const r = regionById.get(iso);
47623
47869
  const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
47624
47870
  if (!viewF) continue;
47625
- const d = path(viewF) ?? "";
47871
+ const raw = path(viewF) ?? "";
47872
+ const d = fitIsGlobal ? dropAntimeridianWrapSlivers(raw, width, height) : raw;
47626
47873
  if (!d) continue;
47627
47874
  const isThisLayer = r?.layer === layerKind;
47628
47875
  const isForeign = layerKind === "country" && usContext && iso !== "US";
@@ -47639,6 +47886,9 @@ function layoutMap(resolved, data, size, opts) {
47639
47886
  } else {
47640
47887
  label = f.properties?.name;
47641
47888
  }
47889
+ const labelAnchor = WORLD_LABEL_ANCHORS[iso];
47890
+ const c = labelAnchor ? project(labelAnchor[0], labelAnchor[1]) : path.centroid(viewF);
47891
+ const hasCentroid = c != null && Number.isFinite(c[0]) && Number.isFinite(c[1]);
47642
47892
  regions.push({
47643
47893
  id: iso,
47644
47894
  d,
@@ -47647,6 +47897,7 @@ function layoutMap(resolved, data, size, opts) {
47647
47897
  lineNumber,
47648
47898
  layer,
47649
47899
  ...label !== void 0 && { label },
47900
+ ...hasCentroid && { labelX: c[0], labelY: c[1] },
47650
47901
  ...isThisLayer && r.value !== void 0 && { value: r.value },
47651
47902
  ...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
47652
47903
  });
@@ -48087,10 +48338,6 @@ function layoutMap(resolved, data, size, opts) {
48087
48338
  lineNumber
48088
48339
  });
48089
48340
  };
48090
- const WORLD_LABEL_ANCHORS = {
48091
- US: [-98.5, 39.5]
48092
- // CONUS geographic centre (near Lebanon, Kansas)
48093
- };
48094
48341
  const REGION_LABEL_GAP = 2;
48095
48342
  const regionLabelRect = (cx, cy, text) => {
48096
48343
  const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
@@ -48408,30 +48655,6 @@ function layoutMap(resolved, data, size, opts) {
48408
48655
  });
48409
48656
  labels.push(...contextLabels);
48410
48657
  }
48411
- let legend = null;
48412
- if (!resolved.directives.noLegend) {
48413
- const tagGroups = resolved.tagGroups.map((g) => ({
48414
- name: g.name,
48415
- entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
48416
- }));
48417
- if (tagGroups.length > 0 || hasRamp) {
48418
- legend = {
48419
- tagGroups,
48420
- activeGroup,
48421
- ...hasRamp && {
48422
- ramp: {
48423
- ...resolved.directives.regionMetric !== void 0 && {
48424
- metric: resolved.directives.regionMetric
48425
- },
48426
- min: rampMin,
48427
- max: rampMax,
48428
- hue: rampHue,
48429
- base: rampBase
48430
- }
48431
- }
48432
- };
48433
- }
48434
- }
48435
48658
  return {
48436
48659
  width,
48437
48660
  height,
@@ -48456,7 +48679,7 @@ function layoutMap(resolved, data, size, opts) {
48456
48679
  diagnostics: []
48457
48680
  };
48458
48681
  }
48459
- 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;
48682
+ 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;
48460
48683
  var init_layout15 = __esm({
48461
48684
  "src/map/layout.ts"() {
48462
48685
  "use strict";
@@ -48469,15 +48692,20 @@ var init_layout15 = __esm({
48469
48692
  init_label_layout();
48470
48693
  init_legend_constants();
48471
48694
  init_title_constants();
48695
+ init_legend_band();
48472
48696
  init_context_labels();
48473
48697
  FIT_PAD = 24;
48474
- RAMP_FLOOR = 15;
48698
+ RAMP_FLOOR2 = 15;
48475
48699
  R_DEFAULT = 6;
48476
48700
  R_MIN = 4;
48477
48701
  R_MAX = 22;
48478
48702
  W_MIN = 1.25;
48479
48703
  W_MAX = 8;
48480
48704
  FONT2 = 11;
48705
+ WORLD_LABEL_ANCHORS = {
48706
+ US: [-98.5, 39.5]
48707
+ // CONUS geographic centre (near Lebanon, Kansas)
48708
+ };
48481
48709
  MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48482
48710
  MAX_COLUMN_ROWS = 7;
48483
48711
  REGION_LABEL_HALO_RATIO = 4.5;
@@ -48491,9 +48719,9 @@ var init_layout15 = __esm({
48491
48719
  COMPACT_WIDTH_PX = 480;
48492
48720
  RELIEF_MIN_AREA = 12;
48493
48721
  RELIEF_MIN_DIM = 2;
48494
- RELIEF_HATCH_SPACING = 2;
48495
- RELIEF_HATCH_WIDTH = 0.15;
48496
- RELIEF_HATCH_STRENGTH = 32;
48722
+ RELIEF_HATCH_SPACING = 1.5;
48723
+ RELIEF_HATCH_WIDTH = 0.2;
48724
+ RELIEF_HATCH_STRENGTH = 26;
48497
48725
  COASTLINE_RING_COUNT = 5;
48498
48726
  COASTLINE_D0 = 16e-4;
48499
48727
  COASTLINE_STEP = 28e-4;
@@ -48571,7 +48799,47 @@ function ringToPath(ring) {
48571
48799
  d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48572
48800
  return d + "Z";
48573
48801
  }
48574
- function coastlineOuterRings(regions, minExtent) {
48802
+ function polylineToPath(pts) {
48803
+ let d = "";
48804
+ for (let i = 0; i < pts.length; i++)
48805
+ d += (i ? "L" : "M") + pts[i][0] + "," + pts[i][1];
48806
+ return d;
48807
+ }
48808
+ function ringToCoastPaths(ring, frame) {
48809
+ if (!frame) return [ringToPath(ring)];
48810
+ const n = ring.length;
48811
+ const eps = 0.75;
48812
+ const onL = (x) => Math.abs(x) <= eps;
48813
+ const onR = (x) => Math.abs(x - frame.w) <= eps;
48814
+ const onT = (y) => Math.abs(y) <= eps;
48815
+ const onB = (y) => Math.abs(y - frame.h) <= eps;
48816
+ 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]);
48817
+ let firstBreak = -1;
48818
+ for (let i = 0; i < n; i++)
48819
+ if (isFrameEdge(ring[i], ring[(i + 1) % n])) {
48820
+ firstBreak = i;
48821
+ break;
48822
+ }
48823
+ if (firstBreak === -1) return [ringToPath(ring)];
48824
+ const paths = [];
48825
+ let cur = [];
48826
+ const start = (firstBreak + 1) % n;
48827
+ for (let k = 0; k < n; k++) {
48828
+ const i = (start + k) % n;
48829
+ const a = ring[i];
48830
+ const b = ring[(i + 1) % n];
48831
+ if (isFrameEdge(a, b)) {
48832
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
48833
+ cur = [];
48834
+ continue;
48835
+ }
48836
+ if (cur.length === 0) cur.push(a);
48837
+ cur.push(b);
48838
+ }
48839
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
48840
+ return paths;
48841
+ }
48842
+ function coastlineOuterRings(regions, minExtent, frame) {
48575
48843
  const paths = [];
48576
48844
  for (const r of regions) {
48577
48845
  const rings = parsePathRings(r.d);
@@ -48594,7 +48862,7 @@ function coastlineOuterRings(regions, minExtent) {
48594
48862
  for (let j = 0; j < rings.length; j++)
48595
48863
  if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48596
48864
  if (depth % 2 === 1) continue;
48597
- paths.push(ringToPath(ring));
48865
+ paths.push(...ringToCoastPaths(ring, frame));
48598
48866
  }
48599
48867
  }
48600
48868
  return paths;
@@ -48624,6 +48892,9 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48624
48892
  // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48625
48893
  // keeps the global stretch-fill.
48626
48894
  preferContain: exportDims?.preferContain ?? false,
48895
+ // Reserve the legend band for the mode actually drawn below (export shows
48896
+ // only the active group; preview keeps the inactive pills).
48897
+ legendMode: exportDims ? "export" : "preview",
48627
48898
  ...activeGroupOverride !== void 0 && {
48628
48899
  activeGroup: activeGroupOverride
48629
48900
  }
@@ -48638,6 +48909,10 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48638
48909
  const drawRegion = (g, r, strokeWidth) => {
48639
48910
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
48640
48911
  if (r.label) p.attr("data-region-name", r.label);
48912
+ if (r.id && r.id !== "lake") p.attr("data-iso", r.id);
48913
+ if (r.labelX !== void 0 && r.labelY !== void 0) {
48914
+ p.attr("data-label-x", r.labelX).attr("data-label-y", r.labelY);
48915
+ }
48641
48916
  if (r.layer !== "base") {
48642
48917
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
48643
48918
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -48667,7 +48942,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48667
48942
  const landClip = defs.append("clipPath").attr("id", landClipId);
48668
48943
  for (const r of layout.regions)
48669
48944
  if (r.id !== "lake") landClip.append("path").attr("d", r.d);
48670
- 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");
48945
+ 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");
48671
48946
  for (let y = h.spacing; y < height; y += h.spacing) {
48672
48947
  gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
48673
48948
  }
@@ -48688,10 +48963,16 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48688
48963
  mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
48689
48964
  }
48690
48965
  }
48691
- const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
48966
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").style("pointer-events", "none").attr("mask", `url(#${maskId})`);
48692
48967
  appendWaterLines(
48693
48968
  gWater,
48694
- coastlineOuterRings(layout.regions, cs.minExtent),
48969
+ // Pass the canvas frame so edges collinear with it (the antimeridian on a
48970
+ // world map, regional clipExtent cuts) don't get ringed as fake coast —
48971
+ // land runs cleanly to the render-area edge.
48972
+ coastlineOuterRings(layout.regions, cs.minExtent, {
48973
+ w: width,
48974
+ h: height
48975
+ }),
48695
48976
  cs,
48696
48977
  layout.background
48697
48978
  );
@@ -48705,7 +48986,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48705
48986
  gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48706
48987
  }
48707
48988
  if (layout.rivers.length) {
48708
- const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
48989
+ const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none").style("pointer-events", "none");
48709
48990
  for (const r of layout.rivers) {
48710
48991
  gRivers.append("path").attr("d", r.d).attr("stroke", r.color).attr("stroke-width", r.width).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
48711
48992
  }
@@ -48746,7 +49027,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48746
49027
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48747
49028
  clip.append("path").attr("d", d);
48748
49029
  }
48749
- 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})`);
49030
+ 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})`);
48750
49031
  appendWaterLines(
48751
49032
  gInsetWater,
48752
49033
  coastlineOuterRings(layout.insetRegions, cs.minExtent),
@@ -48905,30 +49186,12 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48905
49186
  if (layout.legend) {
48906
49187
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
48907
49188
  const legendG = svg.append("g").attr("class", "dgmo-map-legend").attr("transform", `translate(0, ${legendY})`);
48908
- const ramp = layout.legend.ramp;
48909
- const scoreGroup = ramp ? {
48910
- name: ramp.metric?.trim() || "Value",
48911
- entries: [],
48912
- gradient: {
48913
- min: ramp.min,
48914
- max: ramp.max,
48915
- hue: ramp.hue,
48916
- base: ramp.base
48917
- }
48918
- } : null;
48919
- const tagGroups = layout.legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
48920
- const groups = [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
49189
+ const groups = mapLegendGroups(layout.legend);
48921
49190
  if (groups.length > 0) {
48922
- const config = {
49191
+ const config = mapLegendConfig(
48923
49192
  groups,
48924
- position: { placement: "top-center", titleRelation: "below-title" },
48925
- mode: exportDims ? "export" : "preview",
48926
- showEmptyGroups: false,
48927
- // Keep inactive siblings visible as pills so the user can click to flip
48928
- // the active colouring dimension (preview only — export shows just the
48929
- // active group).
48930
- showInactivePills: true
48931
- };
49193
+ exportDims ? "export" : "preview"
49194
+ );
48932
49195
  const state = { activeGroup: layout.legend.activeGroup };
48933
49196
  renderLegendD3(legendG, config, state, palette, isDark, void 0, width);
48934
49197
  }
@@ -48973,6 +49236,7 @@ var init_renderer16 = __esm({
48973
49236
  init_title_constants();
48974
49237
  init_color_utils();
48975
49238
  init_legend_d3();
49239
+ init_legend_band();
48976
49240
  init_layout15();
48977
49241
  LABEL_FONT = 11;
48978
49242
  }
@@ -48993,9 +49257,10 @@ function mapContentAspect(resolved, data, ref = REF) {
48993
49257
  const aspect = w / h;
48994
49258
  return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
48995
49259
  }
48996
- function mapExportDimensions(resolved, data, baseWidth = 1200) {
48997
- const raw = mapContentAspect(resolved, data);
48998
- const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49260
+ function mapExportDimensions(resolved, data, baseWidth = 1200, aspectOverride) {
49261
+ const useOverride = aspectOverride !== void 0 && Number.isFinite(aspectOverride) && aspectOverride > 0;
49262
+ const raw = useOverride ? aspectOverride : mapContentAspect(resolved, data);
49263
+ const clamped = useOverride ? raw : Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
48999
49264
  const width = baseWidth;
49000
49265
  let height = Math.round(width / clamped);
49001
49266
  let chromeReserve = 0;
@@ -49008,7 +49273,7 @@ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49008
49273
  height = Math.round(chromeReserve + MIN_MAP_BAND);
49009
49274
  floored = true;
49010
49275
  }
49011
- const preferContain = clamped !== raw || floored;
49276
+ const preferContain = useOverride ? floored : clamped !== raw || floored;
49012
49277
  return { width, height, preferContain };
49013
49278
  }
49014
49279
  var import_d3_geo3, FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
@@ -54715,7 +54980,6 @@ function renderTimelineTagLegendOverlay(container, parsed, palette, isDark, setu
54715
54980
  function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, setup, hovers, onClickItem, _exportDims, _swimlaneTagGroup, _activeTagGroup, _onTagStateChange, _viewMode) {
54716
54981
  const {
54717
54982
  width,
54718
- height,
54719
54983
  tooltip,
54720
54984
  solid,
54721
54985
  textColor,
@@ -54764,10 +55028,11 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
54764
55028
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
54765
55029
  const eraLabelY = eraReserve ? -(topScaleH + markerReserve + ERA_ROW_H / 2) : 0;
54766
55030
  const innerWidth = width - margin.left - margin.right;
54767
- const innerHeight = height - margin.top - margin.bottom;
54768
- const rowH = Math.min(ctx.structural(28), innerHeight / sorted.length);
55031
+ const rowH = ctx.structural(28);
55032
+ const innerHeight = rowH * sorted.length;
55033
+ const usedHeight = margin.top + innerHeight + margin.bottom;
54769
55034
  const xScale = d3Scale2.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
54770
- 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);
55035
+ 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);
54771
55036
  if (ctx.isBelowFloor) {
54772
55037
  svg.attr("width", "100%");
54773
55038
  }
@@ -57288,7 +57553,12 @@ async function renderForExport(content, theme, palette, viewState, options) {
57288
57553
  }
57289
57554
  }
57290
57555
  const mapResolved = resolveMap2(mapParsed, mapData);
57291
- const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57556
+ const dims2 = mapExportDimensions2(
57557
+ mapResolved,
57558
+ mapData,
57559
+ EXPORT_WIDTH,
57560
+ options?.mapAspect
57561
+ );
57292
57562
  const container2 = createExportContainer(dims2.width, dims2.height);
57293
57563
  renderMapForExport2(
57294
57564
  container2,
@@ -58298,6 +58568,9 @@ var DIRECTIVE_KEYWORDS = /* @__PURE__ */ new Set([
58298
58568
  "hide",
58299
58569
  "mode",
58300
58570
  "direction",
58571
+ // Boxes-and-lines
58572
+ "box-metric",
58573
+ "show-values",
58301
58574
  // ER
58302
58575
  "notation",
58303
58576
  // Class
@@ -59020,7 +59293,7 @@ pre.dgmo, code.language-dgmo, pre > code.language-dgmo,
59020
59293
 
59021
59294
  // src/auto/index.ts
59022
59295
  init_safe_href();
59023
- var VERSION = "0.22.0";
59296
+ var VERSION = "0.24.0";
59024
59297
  var DEFAULTS = {
59025
59298
  theme: "auto",
59026
59299
  palette: "nord",