@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.mjs CHANGED
@@ -818,9 +818,7 @@ var init_reserved_key_registry = __esm({
818
818
  BOXES_AND_LINES_REGISTRY = staticRegistry([
819
819
  "color",
820
820
  "description",
821
- "width",
822
- "split",
823
- "fanout"
821
+ "value"
824
822
  ]);
825
823
  TIMELINE_REGISTRY = staticRegistry([
826
824
  "color",
@@ -16840,6 +16838,21 @@ function parseBoxesAndLines(content) {
16840
16838
  }
16841
16839
  continue;
16842
16840
  }
16841
+ if (!contentStarted) {
16842
+ const metricMatch = trimmed.match(/^box-metric\s+(.+)$/i);
16843
+ if (metricMatch) {
16844
+ const { label, colorName } = peelTrailingColorName(
16845
+ metricMatch[1].trim()
16846
+ );
16847
+ result.boxMetric = label;
16848
+ if (colorName !== void 0) result.boxMetricColor = colorName;
16849
+ continue;
16850
+ }
16851
+ if (/^show-values$/i.test(trimmed)) {
16852
+ result.showValues = true;
16853
+ continue;
16854
+ }
16855
+ }
16843
16856
  if (!contentStarted) {
16844
16857
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
16845
16858
  if (optMatch) {
@@ -17218,6 +17231,19 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
17218
17231
  description = [metadata["description"]];
17219
17232
  delete metadata["description"];
17220
17233
  }
17234
+ let value;
17235
+ if (metadata["value"] !== void 0) {
17236
+ const raw = metadata["value"];
17237
+ const num = Number(raw);
17238
+ if (Number.isFinite(num)) {
17239
+ value = num;
17240
+ } else {
17241
+ diagnostics.push(
17242
+ makeDgmoError(lineNum, `value must be a number (got "${raw}")`, "error")
17243
+ );
17244
+ }
17245
+ delete metadata["value"];
17246
+ }
17221
17247
  if (split.alias) {
17222
17248
  nameAliasMap?.set(normalizeName(split.alias), label);
17223
17249
  }
@@ -17226,7 +17252,8 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
17226
17252
  label,
17227
17253
  lineNumber: lineNum,
17228
17254
  metadata,
17229
- ...description !== void 0 && { description }
17255
+ ...description !== void 0 && { description },
17256
+ ...value !== void 0 && { value }
17230
17257
  };
17231
17258
  }
17232
17259
  function splitTargetAndMeta(rest, metaAliasMap) {
@@ -26348,7 +26375,18 @@ function fitLabelToHeader(label, nodeWidth, maxLines) {
26348
26375
  const truncated = label.length > maxChars ? label.slice(0, maxChars - 1) + "\u2026" : label;
26349
26376
  return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
26350
26377
  }
26351
- function nodeColors(node, tagGroups, activeGroupName, palette, isDark, solid) {
26378
+ function nodeColors(node, tagGroups, activeGroupName, palette, isDark, value, solid) {
26379
+ const neutralFill = mix(palette.bg, palette.text, isDark ? 90 : 95);
26380
+ if (value.active) {
26381
+ const fill3 = node.value !== void 0 ? value.fillForValue(node.value) : neutralFill;
26382
+ const stroke3 = value.hue;
26383
+ const text2 = contrastText(
26384
+ fill3,
26385
+ palette.textOnFillLight,
26386
+ palette.textOnFillDark
26387
+ );
26388
+ return { fill: fill3, stroke: stroke3, text: text2 };
26389
+ }
26352
26390
  const tagColor = resolveTagColor(
26353
26391
  node.metadata,
26354
26392
  [...tagGroups],
@@ -26457,25 +26495,65 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26457
26495
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26458
26496
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26459
26497
  const sTitleY = sctx.structural(TITLE_Y);
26498
+ const nodeValues = parsed.nodes.filter((n) => n.value !== void 0).map((n) => n.value);
26499
+ const hasRamp = nodeValues.length > 0;
26500
+ const allNonNegative = hasRamp && nodeValues.every((v) => v >= 0);
26501
+ const rampMin = allNonNegative ? 0 : Math.min(...nodeValues);
26502
+ const rampMax = Math.max(...nodeValues);
26503
+ const rampHue = resolveColor(parsed.boxMetricColor ?? "", palette) ?? palette.primary;
26504
+ const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
26505
+ const fillForValue = (v) => {
26506
+ const t = rampMax > rampMin ? (v - rampMin) / (rampMax - rampMin) : 1;
26507
+ const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
26508
+ return mix(rampHue, rampBase, pct);
26509
+ };
26510
+ const VALUE_NAME = hasRamp ? parsed.boxMetric?.trim() || "Value" : null;
26511
+ const matchColorGroup = (v) => {
26512
+ const lv = v.trim().toLowerCase();
26513
+ if (lv === "" || lv === "none") return null;
26514
+ const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
26515
+ if (tg) return tg.name;
26516
+ if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
26517
+ return v;
26518
+ };
26519
+ const override = activeTagGroup;
26520
+ let activeGroup;
26521
+ if (override !== void 0) {
26522
+ activeGroup = override === null ? null : matchColorGroup(override);
26523
+ } else if (parsed.options["active-tag"] !== void 0) {
26524
+ activeGroup = matchColorGroup(parsed.options["active-tag"]);
26525
+ } else {
26526
+ activeGroup = VALUE_NAME ?? (parsed.tagGroups.length > 0 ? parsed.tagGroups[0].name : null);
26527
+ }
26528
+ const activeIsValue = VALUE_NAME !== null && activeGroup === VALUE_NAME;
26529
+ const valueGroup = VALUE_NAME !== null ? {
26530
+ name: VALUE_NAME,
26531
+ entries: [],
26532
+ gradient: {
26533
+ min: rampMin,
26534
+ max: rampMax,
26535
+ hue: rampHue,
26536
+ base: rampBase
26537
+ }
26538
+ } : null;
26539
+ const legendGroups = [
26540
+ ...valueGroup ? [valueGroup] : [],
26541
+ ...parsed.tagGroups
26542
+ ];
26460
26543
  const reserveHasDescriptions = parsed.nodes.some(
26461
26544
  (n) => n.description && n.description.length > 0
26462
26545
  );
26463
- const willRenderLegend = parsed.tagGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26546
+ const willRenderLegend = legendGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26464
26547
  const sLegendHeight = willRenderLegend ? sctx.structural(
26465
26548
  getMaxLegendReservedHeight(
26466
26549
  {
26467
- groups: parsed.tagGroups,
26550
+ groups: legendGroups,
26468
26551
  position: { placement: "top-center", titleRelation: "below-title" },
26469
26552
  mode: exportMode ? "export" : "preview"
26470
26553
  },
26471
26554
  width
26472
26555
  )
26473
26556
  ) : 0;
26474
- const activeGroup = resolveActiveTagGroup(
26475
- parsed.tagGroups,
26476
- parsed.options["active-tag"],
26477
- activeTagGroup
26478
- );
26479
26557
  const hidden = hiddenTagValues ?? parsed.initialHiddenTagValues;
26480
26558
  const nodeMap = /* @__PURE__ */ new Map();
26481
26559
  for (const node of parsed.nodes) nodeMap.set(node.label, node);
@@ -26486,7 +26564,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26486
26564
  const hasAnyDescriptions = parsed.nodes.some(
26487
26565
  (n) => n.description && n.description.length > 0
26488
26566
  );
26489
- const needsLegend = parsed.tagGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26567
+ const needsLegend = legendGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26490
26568
  const legendH = needsLegend ? sLegendHeight + 8 : 0;
26491
26569
  const groupLabelsSet = new Set(layout.groups.map((g) => g.label));
26492
26570
  let labelZoneExtension = 0;
@@ -26692,12 +26770,16 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26692
26770
  activeGroup,
26693
26771
  palette,
26694
26772
  isDark,
26773
+ { active: activeIsValue, hue: rampHue, fillForValue },
26695
26774
  parsed.options["solid-fill"] === "on"
26696
26775
  );
26697
26776
  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);
26698
26777
  for (const [key, val] of Object.entries(node.metadata)) {
26699
26778
  nodeG.attr(`data-tag-${key.toLowerCase()}`, val.toLowerCase());
26700
26779
  }
26780
+ if (node.value !== void 0) {
26781
+ nodeG.attr("data-value", node.value);
26782
+ }
26701
26783
  if (onClickItem) {
26702
26784
  nodeG.on("click", (event) => {
26703
26785
  const target = event.target;
@@ -26769,6 +26851,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26769
26851
  const tooltipText = fullText.length > 200 ? fullText.slice(0, 199) + "\u2026" : fullText;
26770
26852
  nodeG.append("title").text(tooltipText);
26771
26853
  }
26854
+ } else if (parsed.showValues && node.value !== void 0) {
26855
+ const valueLabel = parsed.boxMetric ? `${parsed.boxMetric}: ${node.value}` : String(node.value);
26856
+ const headerH = ln.height / 2;
26857
+ const sepY = -ln.height / 2 + headerH;
26858
+ const fitted = fitLabelToHeader(node.label, ln.width, 2);
26859
+ const labelLineH = fitted.fontSize * 1.3;
26860
+ const labelTotalH = fitted.lines.length * labelLineH;
26861
+ const headerCenterY = -ln.height / 2 + headerH / 2;
26862
+ for (let li = 0; li < fitted.lines.length; li++) {
26863
+ nodeG.append("text").attr("x", 0).attr(
26864
+ "y",
26865
+ headerCenterY - labelTotalH / 2 + labelLineH / 2 + li * labelLineH
26866
+ ).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]);
26867
+ }
26868
+ 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);
26869
+ 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);
26772
26870
  } else {
26773
26871
  const maxLabelLines = Math.max(
26774
26872
  2,
@@ -26781,11 +26879,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26781
26879
  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]);
26782
26880
  }
26783
26881
  }
26882
+ if (parsed.showValues && node.value !== void 0 && desc && desc.length > 0 && !hideDescriptions) {
26883
+ const valueText = String(node.value);
26884
+ const padX = 6;
26885
+ const padY = 5;
26886
+ const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
26887
+ const bh = VALUE_FONT_SIZE + 4;
26888
+ const bx = Math.max(-ln.width / 2 + 4, ln.width / 2 - bw - 4);
26889
+ const by = -ln.height / 2 + 4;
26890
+ 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);
26891
+ 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);
26892
+ }
26784
26893
  }
26785
26894
  const hasDescriptions = parsed.nodes.some(
26786
26895
  (n) => n.description && n.description.length > 0
26787
26896
  );
26788
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26897
+ const hasLegend = legendGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26789
26898
  if (hasLegend) {
26790
26899
  let controlsGroup;
26791
26900
  if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
@@ -26803,7 +26912,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26803
26912
  };
26804
26913
  }
26805
26914
  const legendConfig = {
26806
- groups: parsed.tagGroups,
26915
+ groups: legendGroups,
26807
26916
  position: { placement: "top-center", titleRelation: "below-title" },
26808
26917
  mode: exportMode ? "export" : "preview",
26809
26918
  // Keep inactive sibling tag groups visible as collapsed pills so the user
@@ -26858,7 +26967,7 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
26858
26967
  }
26859
26968
  });
26860
26969
  }
26861
- 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;
26970
+ 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;
26862
26971
  var init_renderer6 = __esm({
26863
26972
  "src/boxes-and-lines/renderer.ts"() {
26864
26973
  "use strict";
@@ -26867,12 +26976,13 @@ var init_renderer6 = __esm({
26867
26976
  init_legend_layout();
26868
26977
  init_title_constants();
26869
26978
  init_color_utils();
26979
+ init_colors();
26870
26980
  init_tag_groups();
26871
26981
  init_inline_markdown();
26872
26982
  init_wrapped_desc();
26873
26983
  init_scaling();
26874
26984
  DIAGRAM_PADDING6 = 20;
26875
- NODE_FONT_SIZE = 13;
26985
+ NODE_FONT_SIZE = 11;
26876
26986
  MIN_NODE_FONT_SIZE = 9;
26877
26987
  EDGE_LABEL_FONT_SIZE4 = 11;
26878
26988
  EDGE_STROKE_WIDTH5 = 1.5;
@@ -26889,6 +26999,8 @@ var init_renderer6 = __esm({
26889
26999
  GROUP_RX = 8;
26890
27000
  GROUP_LABEL_FONT_SIZE = 14;
26891
27001
  GROUP_LABEL_ZONE = 32;
27002
+ RAMP_FLOOR = 15;
27003
+ VALUE_FONT_SIZE = 11;
26892
27004
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
26893
27005
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
26894
27006
  }
@@ -46607,7 +46719,11 @@ function resolveMap(parsed, data) {
46607
46719
  if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46608
46720
  }
46609
46721
  const containerUnion = unionExtent(containerBoxes, points);
46610
- if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46722
+ if (containerUnion)
46723
+ extent2 = pad(
46724
+ clampContainerToCluster(containerUnion, points),
46725
+ PAD_FRACTION
46726
+ );
46611
46727
  }
46612
46728
  if (isPoiOnly) {
46613
46729
  const cx = (extent2[0][0] + extent2[1][0]) / 2;
@@ -46688,6 +46804,22 @@ function mostCommonCountry(regions, poiCountries) {
46688
46804
  }
46689
46805
  return best;
46690
46806
  }
46807
+ function clampContainerToCluster(container, points) {
46808
+ const poi = unionExtent([], points);
46809
+ if (!poi) return container;
46810
+ let [[west, south], [east, north]] = container;
46811
+ const [[pWest, pSouth], [pEast, pNorth]] = poi;
46812
+ south = Math.max(south, pSouth - CONTAINER_OVERSHOOT_DEG);
46813
+ north = Math.min(north, pNorth + CONTAINER_OVERSHOOT_DEG);
46814
+ if (east <= 180 && pEast <= 180) {
46815
+ west = Math.max(west, pWest - CONTAINER_OVERSHOOT_DEG);
46816
+ east = Math.min(east, pEast + CONTAINER_OVERSHOOT_DEG);
46817
+ }
46818
+ return [
46819
+ [west, south],
46820
+ [east, north]
46821
+ ];
46822
+ }
46691
46823
  function pad(e, frac) {
46692
46824
  const dLon = (e[1][0] - e[0][0]) * frac || 1;
46693
46825
  const dLat = (e[1][1] - e[0][1]) * frac || 1;
@@ -46700,7 +46832,7 @@ function firstError(diags) {
46700
46832
  const e = diags.find((d) => d.severity === "error");
46701
46833
  return e ? formatDgmoError(e) : null;
46702
46834
  }
46703
- 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;
46835
+ 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;
46704
46836
  var init_resolver2 = __esm({
46705
46837
  "src/map/resolver.ts"() {
46706
46838
  "use strict";
@@ -46713,6 +46845,7 @@ var init_resolver2 = __esm({
46713
46845
  WORLD_LAT_SOUTH = -58;
46714
46846
  WORLD_LAT_NORTH = 78;
46715
46847
  POI_ZOOM_FLOOR_DEG = 7;
46848
+ CONTAINER_OVERSHOOT_DEG = 8;
46716
46849
  US_NATIONAL_LON_SPAN = 48;
46717
46850
  REGION_ALIASES = {
46718
46851
  // Common everyday names → the Natural-Earth display name actually shipped.
@@ -46791,6 +46924,55 @@ var init_resolver2 = __esm({
46791
46924
  }
46792
46925
  });
46793
46926
 
46927
+ // src/map/legend-band.ts
46928
+ function mapLegendGroups(legend) {
46929
+ const ramp = legend.ramp;
46930
+ const scoreGroup = ramp ? {
46931
+ name: ramp.metric?.trim() || "Value",
46932
+ entries: [],
46933
+ gradient: {
46934
+ min: ramp.min,
46935
+ max: ramp.max,
46936
+ hue: ramp.hue,
46937
+ base: ramp.base
46938
+ }
46939
+ } : null;
46940
+ const tagGroups = legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
46941
+ return [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
46942
+ }
46943
+ function mapLegendConfig(groups, mode) {
46944
+ return {
46945
+ groups,
46946
+ position: { placement: "top-center", titleRelation: "below-title" },
46947
+ mode,
46948
+ showEmptyGroups: false,
46949
+ showInactivePills: true
46950
+ };
46951
+ }
46952
+ function mapLegendTop(hasTitle, hasSubtitle) {
46953
+ return (hasTitle ? TITLE_Y + TITLE_FONT_SIZE : 0) + (hasSubtitle ? TITLE_FONT_SIZE : 0) + LEGEND_TOP_GAP2;
46954
+ }
46955
+ function mapLegendBand(legend, opts) {
46956
+ if (!legend) return 0;
46957
+ const groups = mapLegendGroups(legend);
46958
+ if (groups.length === 0) return 0;
46959
+ const config = mapLegendConfig(groups, opts.mode);
46960
+ const state = { activeGroup: legend.activeGroup };
46961
+ const { height } = computeLegendLayout(config, state, opts.width);
46962
+ if (height <= 0) return 0;
46963
+ return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
46964
+ }
46965
+ var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
46966
+ var init_legend_band = __esm({
46967
+ "src/map/legend-band.ts"() {
46968
+ "use strict";
46969
+ init_legend_layout();
46970
+ init_title_constants();
46971
+ LEGEND_TOP_GAP2 = 8;
46972
+ LEGEND_BOTTOM_GAP2 = 10;
46973
+ }
46974
+ });
46975
+
46794
46976
  // src/map/colorize.ts
46795
46977
  function assignColors(isos, adjacency) {
46796
46978
  const sorted = [...isos].sort();
@@ -47231,6 +47413,38 @@ function parsePathRings(d) {
47231
47413
  if (cur.length) rings.push(cur);
47232
47414
  return rings;
47233
47415
  }
47416
+ function dropAntimeridianWrapSlivers(d, width, height) {
47417
+ const rings = parsePathRings(d);
47418
+ if (rings.length <= 1) return d;
47419
+ const eps = 0.75;
47420
+ const minArea = 3e-3 * width * height;
47421
+ const ringArea = (r) => {
47422
+ let s = 0;
47423
+ for (let i = 0; i < r.length; i++) {
47424
+ const a = r[i];
47425
+ const b = r[(i + 1) % r.length];
47426
+ s += a[0] * b[1] - b[0] * a[1];
47427
+ }
47428
+ return Math.abs(s) / 2;
47429
+ };
47430
+ const areas = rings.map(ringArea);
47431
+ const maxArea = Math.max(...areas);
47432
+ 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;
47433
+ let dropped = false;
47434
+ const kept = rings.filter((r, idx) => {
47435
+ if (areas[idx] >= maxArea || areas[idx] >= minArea) return true;
47436
+ const touches = r.some((p, i) => onVEdge(p, r[(i + 1) % r.length]));
47437
+ if (touches) {
47438
+ dropped = true;
47439
+ return false;
47440
+ }
47441
+ return true;
47442
+ });
47443
+ if (!dropped) return d;
47444
+ return kept.map(
47445
+ (r) => r.map((p, i) => (i ? "L" : "M") + p[0] + "," + p[1]).join("") + "Z"
47446
+ ).join("");
47447
+ }
47234
47448
  function layoutMap(resolved, data, size, opts) {
47235
47449
  const { palette, isDark } = opts;
47236
47450
  const { width, height } = size;
@@ -47314,7 +47528,7 @@ function layoutMap(resolved, data, size, opts) {
47314
47528
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
47315
47529
  const fillForValue = (s) => {
47316
47530
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
47317
- const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
47531
+ const pct = RAMP_FLOOR2 + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR2);
47318
47532
  return mix(rampHue, rampBase, pct);
47319
47533
  };
47320
47534
  const tagFill = (tags, groupName) => {
@@ -47350,12 +47564,43 @@ function layoutMap(resolved, data, size, opts) {
47350
47564
  return tagFill(r.tags, activeGroup) ?? neutralFill;
47351
47565
  };
47352
47566
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
47567
+ let legend = null;
47568
+ if (!resolved.directives.noLegend) {
47569
+ const legendTagGroups = resolved.tagGroups.map((g) => ({
47570
+ name: g.name,
47571
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
47572
+ }));
47573
+ if (legendTagGroups.length > 0 || hasRamp) {
47574
+ legend = {
47575
+ tagGroups: legendTagGroups,
47576
+ activeGroup,
47577
+ ...hasRamp && {
47578
+ ramp: {
47579
+ ...resolved.directives.regionMetric !== void 0 && {
47580
+ metric: resolved.directives.regionMetric
47581
+ },
47582
+ min: rampMin,
47583
+ max: rampMax,
47584
+ hue: rampHue,
47585
+ base: rampBase
47586
+ }
47587
+ }
47588
+ };
47589
+ }
47590
+ }
47353
47591
  const TITLE_GAP2 = 16;
47354
47592
  let topPad = FIT_PAD;
47355
47593
  if (resolved.title && resolved.pois.length > 0) {
47356
47594
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
47357
47595
  topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
47358
47596
  }
47597
+ const legendBand = mapLegendBand(legend, {
47598
+ width,
47599
+ mode: opts.legendMode ?? "preview",
47600
+ hasTitle: Boolean(resolved.title),
47601
+ hasSubtitle: Boolean(resolved.subtitle)
47602
+ });
47603
+ if (legendBand > topPad) topPad = legendBand;
47359
47604
  const fitBox = [
47360
47605
  [FIT_PAD, topPad],
47361
47606
  [
@@ -47373,10 +47618,11 @@ function layoutMap(resolved, data, size, opts) {
47373
47618
  const by0 = cb[0][1];
47374
47619
  const cw = cb[1][0] - bx0;
47375
47620
  const ch = cb[1][1] - by0;
47376
- const ox = fitBox[0][0];
47377
- const oy = fitBox[0][1];
47378
- const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
47379
- const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47621
+ const topReserve = resolved.title && resolved.pois.length > 0 || legendBand > 0 ? topPad : 0;
47622
+ const ox = 0;
47623
+ const oy = topReserve;
47624
+ const sx = cw > 0 ? width / cw : 1;
47625
+ const sy = ch > 0 ? (height - topReserve) / ch : 1;
47380
47626
  stretchParams = { sx, sy, ox, oy, bx0, by0 };
47381
47627
  const stretch = (x, y) => [
47382
47628
  ox + (x - bx0) * sx,
@@ -47649,7 +47895,8 @@ function layoutMap(resolved, data, size, opts) {
47649
47895
  const r = regionById.get(iso);
47650
47896
  const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
47651
47897
  if (!viewF) continue;
47652
- const d = path(viewF) ?? "";
47898
+ const raw = path(viewF) ?? "";
47899
+ const d = fitIsGlobal ? dropAntimeridianWrapSlivers(raw, width, height) : raw;
47653
47900
  if (!d) continue;
47654
47901
  const isThisLayer = r?.layer === layerKind;
47655
47902
  const isForeign = layerKind === "country" && usContext && iso !== "US";
@@ -47666,6 +47913,9 @@ function layoutMap(resolved, data, size, opts) {
47666
47913
  } else {
47667
47914
  label = f.properties?.name;
47668
47915
  }
47916
+ const labelAnchor = WORLD_LABEL_ANCHORS[iso];
47917
+ const c = labelAnchor ? project(labelAnchor[0], labelAnchor[1]) : path.centroid(viewF);
47918
+ const hasCentroid = c != null && Number.isFinite(c[0]) && Number.isFinite(c[1]);
47669
47919
  regions.push({
47670
47920
  id: iso,
47671
47921
  d,
@@ -47674,6 +47924,7 @@ function layoutMap(resolved, data, size, opts) {
47674
47924
  lineNumber,
47675
47925
  layer,
47676
47926
  ...label !== void 0 && { label },
47927
+ ...hasCentroid && { labelX: c[0], labelY: c[1] },
47677
47928
  ...isThisLayer && r.value !== void 0 && { value: r.value },
47678
47929
  ...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
47679
47930
  });
@@ -48114,10 +48365,6 @@ function layoutMap(resolved, data, size, opts) {
48114
48365
  lineNumber
48115
48366
  });
48116
48367
  };
48117
- const WORLD_LABEL_ANCHORS = {
48118
- US: [-98.5, 39.5]
48119
- // CONUS geographic centre (near Lebanon, Kansas)
48120
- };
48121
48368
  const REGION_LABEL_GAP = 2;
48122
48369
  const regionLabelRect = (cx, cy, text) => {
48123
48370
  const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
@@ -48435,30 +48682,6 @@ function layoutMap(resolved, data, size, opts) {
48435
48682
  });
48436
48683
  labels.push(...contextLabels);
48437
48684
  }
48438
- let legend = null;
48439
- if (!resolved.directives.noLegend) {
48440
- const tagGroups = resolved.tagGroups.map((g) => ({
48441
- name: g.name,
48442
- entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
48443
- }));
48444
- if (tagGroups.length > 0 || hasRamp) {
48445
- legend = {
48446
- tagGroups,
48447
- activeGroup,
48448
- ...hasRamp && {
48449
- ramp: {
48450
- ...resolved.directives.regionMetric !== void 0 && {
48451
- metric: resolved.directives.regionMetric
48452
- },
48453
- min: rampMin,
48454
- max: rampMax,
48455
- hue: rampHue,
48456
- base: rampBase
48457
- }
48458
- }
48459
- };
48460
- }
48461
- }
48462
48685
  return {
48463
48686
  width,
48464
48687
  height,
@@ -48483,7 +48706,7 @@ function layoutMap(resolved, data, size, opts) {
48483
48706
  diagnostics: []
48484
48707
  };
48485
48708
  }
48486
- 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;
48709
+ 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;
48487
48710
  var init_layout15 = __esm({
48488
48711
  "src/map/layout.ts"() {
48489
48712
  "use strict";
@@ -48494,15 +48717,20 @@ var init_layout15 = __esm({
48494
48717
  init_label_layout();
48495
48718
  init_legend_constants();
48496
48719
  init_title_constants();
48720
+ init_legend_band();
48497
48721
  init_context_labels();
48498
48722
  FIT_PAD = 24;
48499
- RAMP_FLOOR = 15;
48723
+ RAMP_FLOOR2 = 15;
48500
48724
  R_DEFAULT = 6;
48501
48725
  R_MIN = 4;
48502
48726
  R_MAX = 22;
48503
48727
  W_MIN = 1.25;
48504
48728
  W_MAX = 8;
48505
48729
  FONT2 = 11;
48730
+ WORLD_LABEL_ANCHORS = {
48731
+ US: [-98.5, 39.5]
48732
+ // CONUS geographic centre (near Lebanon, Kansas)
48733
+ };
48506
48734
  MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48507
48735
  MAX_COLUMN_ROWS = 7;
48508
48736
  REGION_LABEL_HALO_RATIO = 4.5;
@@ -48516,9 +48744,9 @@ var init_layout15 = __esm({
48516
48744
  COMPACT_WIDTH_PX = 480;
48517
48745
  RELIEF_MIN_AREA = 12;
48518
48746
  RELIEF_MIN_DIM = 2;
48519
- RELIEF_HATCH_SPACING = 2;
48520
- RELIEF_HATCH_WIDTH = 0.15;
48521
- RELIEF_HATCH_STRENGTH = 32;
48747
+ RELIEF_HATCH_SPACING = 1.5;
48748
+ RELIEF_HATCH_WIDTH = 0.2;
48749
+ RELIEF_HATCH_STRENGTH = 26;
48522
48750
  COASTLINE_RING_COUNT = 5;
48523
48751
  COASTLINE_D0 = 16e-4;
48524
48752
  COASTLINE_STEP = 28e-4;
@@ -48597,7 +48825,47 @@ function ringToPath(ring) {
48597
48825
  d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48598
48826
  return d + "Z";
48599
48827
  }
48600
- function coastlineOuterRings(regions, minExtent) {
48828
+ function polylineToPath(pts) {
48829
+ let d = "";
48830
+ for (let i = 0; i < pts.length; i++)
48831
+ d += (i ? "L" : "M") + pts[i][0] + "," + pts[i][1];
48832
+ return d;
48833
+ }
48834
+ function ringToCoastPaths(ring, frame) {
48835
+ if (!frame) return [ringToPath(ring)];
48836
+ const n = ring.length;
48837
+ const eps = 0.75;
48838
+ const onL = (x) => Math.abs(x) <= eps;
48839
+ const onR = (x) => Math.abs(x - frame.w) <= eps;
48840
+ const onT = (y) => Math.abs(y) <= eps;
48841
+ const onB = (y) => Math.abs(y - frame.h) <= eps;
48842
+ 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]);
48843
+ let firstBreak = -1;
48844
+ for (let i = 0; i < n; i++)
48845
+ if (isFrameEdge(ring[i], ring[(i + 1) % n])) {
48846
+ firstBreak = i;
48847
+ break;
48848
+ }
48849
+ if (firstBreak === -1) return [ringToPath(ring)];
48850
+ const paths = [];
48851
+ let cur = [];
48852
+ const start = (firstBreak + 1) % n;
48853
+ for (let k = 0; k < n; k++) {
48854
+ const i = (start + k) % n;
48855
+ const a = ring[i];
48856
+ const b = ring[(i + 1) % n];
48857
+ if (isFrameEdge(a, b)) {
48858
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
48859
+ cur = [];
48860
+ continue;
48861
+ }
48862
+ if (cur.length === 0) cur.push(a);
48863
+ cur.push(b);
48864
+ }
48865
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
48866
+ return paths;
48867
+ }
48868
+ function coastlineOuterRings(regions, minExtent, frame) {
48601
48869
  const paths = [];
48602
48870
  for (const r of regions) {
48603
48871
  const rings = parsePathRings(r.d);
@@ -48620,7 +48888,7 @@ function coastlineOuterRings(regions, minExtent) {
48620
48888
  for (let j = 0; j < rings.length; j++)
48621
48889
  if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48622
48890
  if (depth % 2 === 1) continue;
48623
- paths.push(ringToPath(ring));
48891
+ paths.push(...ringToCoastPaths(ring, frame));
48624
48892
  }
48625
48893
  }
48626
48894
  return paths;
@@ -48650,6 +48918,9 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48650
48918
  // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48651
48919
  // keeps the global stretch-fill.
48652
48920
  preferContain: exportDims?.preferContain ?? false,
48921
+ // Reserve the legend band for the mode actually drawn below (export shows
48922
+ // only the active group; preview keeps the inactive pills).
48923
+ legendMode: exportDims ? "export" : "preview",
48653
48924
  ...activeGroupOverride !== void 0 && {
48654
48925
  activeGroup: activeGroupOverride
48655
48926
  }
@@ -48664,6 +48935,10 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48664
48935
  const drawRegion = (g, r, strokeWidth) => {
48665
48936
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
48666
48937
  if (r.label) p.attr("data-region-name", r.label);
48938
+ if (r.id && r.id !== "lake") p.attr("data-iso", r.id);
48939
+ if (r.labelX !== void 0 && r.labelY !== void 0) {
48940
+ p.attr("data-label-x", r.labelX).attr("data-label-y", r.labelY);
48941
+ }
48667
48942
  if (r.layer !== "base") {
48668
48943
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
48669
48944
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -48693,7 +48968,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48693
48968
  const landClip = defs.append("clipPath").attr("id", landClipId);
48694
48969
  for (const r of layout.regions)
48695
48970
  if (r.id !== "lake") landClip.append("path").attr("d", r.d);
48696
- 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");
48971
+ 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");
48697
48972
  for (let y = h.spacing; y < height; y += h.spacing) {
48698
48973
  gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
48699
48974
  }
@@ -48714,10 +48989,16 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48714
48989
  mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
48715
48990
  }
48716
48991
  }
48717
- const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
48992
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").style("pointer-events", "none").attr("mask", `url(#${maskId})`);
48718
48993
  appendWaterLines(
48719
48994
  gWater,
48720
- coastlineOuterRings(layout.regions, cs.minExtent),
48995
+ // Pass the canvas frame so edges collinear with it (the antimeridian on a
48996
+ // world map, regional clipExtent cuts) don't get ringed as fake coast —
48997
+ // land runs cleanly to the render-area edge.
48998
+ coastlineOuterRings(layout.regions, cs.minExtent, {
48999
+ w: width,
49000
+ h: height
49001
+ }),
48721
49002
  cs,
48722
49003
  layout.background
48723
49004
  );
@@ -48731,7 +49012,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48731
49012
  gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48732
49013
  }
48733
49014
  if (layout.rivers.length) {
48734
- const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
49015
+ const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none").style("pointer-events", "none");
48735
49016
  for (const r of layout.rivers) {
48736
49017
  gRivers.append("path").attr("d", r.d).attr("stroke", r.color).attr("stroke-width", r.width).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
48737
49018
  }
@@ -48772,7 +49053,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48772
49053
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48773
49054
  clip.append("path").attr("d", d);
48774
49055
  }
48775
- 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})`);
49056
+ 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})`);
48776
49057
  appendWaterLines(
48777
49058
  gInsetWater,
48778
49059
  coastlineOuterRings(layout.insetRegions, cs.minExtent),
@@ -48931,30 +49212,12 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48931
49212
  if (layout.legend) {
48932
49213
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
48933
49214
  const legendG = svg.append("g").attr("class", "dgmo-map-legend").attr("transform", `translate(0, ${legendY})`);
48934
- const ramp = layout.legend.ramp;
48935
- const scoreGroup = ramp ? {
48936
- name: ramp.metric?.trim() || "Value",
48937
- entries: [],
48938
- gradient: {
48939
- min: ramp.min,
48940
- max: ramp.max,
48941
- hue: ramp.hue,
48942
- base: ramp.base
48943
- }
48944
- } : null;
48945
- const tagGroups = layout.legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
48946
- const groups = [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
49215
+ const groups = mapLegendGroups(layout.legend);
48947
49216
  if (groups.length > 0) {
48948
- const config = {
49217
+ const config = mapLegendConfig(
48949
49218
  groups,
48950
- position: { placement: "top-center", titleRelation: "below-title" },
48951
- mode: exportDims ? "export" : "preview",
48952
- showEmptyGroups: false,
48953
- // Keep inactive siblings visible as pills so the user can click to flip
48954
- // the active colouring dimension (preview only — export shows just the
48955
- // active group).
48956
- showInactivePills: true
48957
- };
49219
+ exportDims ? "export" : "preview"
49220
+ );
48958
49221
  const state = { activeGroup: layout.legend.activeGroup };
48959
49222
  renderLegendD3(legendG, config, state, palette, isDark, void 0, width);
48960
49223
  }
@@ -48998,6 +49261,7 @@ var init_renderer16 = __esm({
48998
49261
  init_title_constants();
48999
49262
  init_color_utils();
49000
49263
  init_legend_d3();
49264
+ init_legend_band();
49001
49265
  init_layout15();
49002
49266
  LABEL_FONT = 11;
49003
49267
  }
@@ -49019,9 +49283,10 @@ function mapContentAspect(resolved, data, ref = REF) {
49019
49283
  const aspect = w / h;
49020
49284
  return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49021
49285
  }
49022
- function mapExportDimensions(resolved, data, baseWidth = 1200) {
49023
- const raw = mapContentAspect(resolved, data);
49024
- const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49286
+ function mapExportDimensions(resolved, data, baseWidth = 1200, aspectOverride) {
49287
+ const useOverride = aspectOverride !== void 0 && Number.isFinite(aspectOverride) && aspectOverride > 0;
49288
+ const raw = useOverride ? aspectOverride : mapContentAspect(resolved, data);
49289
+ const clamped = useOverride ? raw : Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49025
49290
  const width = baseWidth;
49026
49291
  let height = Math.round(width / clamped);
49027
49292
  let chromeReserve = 0;
@@ -49034,7 +49299,7 @@ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49034
49299
  height = Math.round(chromeReserve + MIN_MAP_BAND);
49035
49300
  floored = true;
49036
49301
  }
49037
- const preferContain = clamped !== raw || floored;
49302
+ const preferContain = useOverride ? floored : clamped !== raw || floored;
49038
49303
  return { width, height, preferContain };
49039
49304
  }
49040
49305
  var FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
@@ -54744,7 +55009,6 @@ function renderTimelineTagLegendOverlay(container, parsed, palette, isDark, setu
54744
55009
  function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, setup, hovers, onClickItem, _exportDims, _swimlaneTagGroup, _activeTagGroup, _onTagStateChange, _viewMode) {
54745
55010
  const {
54746
55011
  width,
54747
- height,
54748
55012
  tooltip,
54749
55013
  solid,
54750
55014
  textColor,
@@ -54793,10 +55057,11 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
54793
55057
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
54794
55058
  const eraLabelY = eraReserve ? -(topScaleH + markerReserve + ERA_ROW_H / 2) : 0;
54795
55059
  const innerWidth = width - margin.left - margin.right;
54796
- const innerHeight = height - margin.top - margin.bottom;
54797
- const rowH = Math.min(ctx.structural(28), innerHeight / sorted.length);
55060
+ const rowH = ctx.structural(28);
55061
+ const innerHeight = rowH * sorted.length;
55062
+ const usedHeight = margin.top + innerHeight + margin.bottom;
54798
55063
  const xScale = d3Scale2.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
54799
- 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);
55064
+ 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);
54800
55065
  if (ctx.isBelowFloor) {
54801
55066
  svg.attr("width", "100%");
54802
55067
  }
@@ -57317,7 +57582,12 @@ async function renderForExport(content, theme, palette, viewState, options) {
57317
57582
  }
57318
57583
  }
57319
57584
  const mapResolved = resolveMap2(mapParsed, mapData);
57320
- const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57585
+ const dims2 = mapExportDimensions2(
57586
+ mapResolved,
57587
+ mapData,
57588
+ EXPORT_WIDTH,
57589
+ options?.mapAspect
57590
+ );
57321
57591
  const container2 = createExportContainer(dims2.width, dims2.height);
57322
57592
  renderMapForExport2(
57323
57593
  container2,
@@ -58308,6 +58578,9 @@ var DIRECTIVE_KEYWORDS = /* @__PURE__ */ new Set([
58308
58578
  "hide",
58309
58579
  "mode",
58310
58580
  "direction",
58581
+ // Boxes-and-lines
58582
+ "box-metric",
58583
+ "show-values",
58311
58584
  // ER
58312
58585
  "notation",
58313
58586
  // Class
@@ -59030,7 +59303,7 @@ pre.dgmo, code.language-dgmo, pre > code.language-dgmo,
59030
59303
 
59031
59304
  // src/auto/index.ts
59032
59305
  init_safe_href();
59033
- var VERSION = "0.22.0";
59306
+ var VERSION = "0.24.0";
59034
59307
  var DEFAULTS = {
59035
59308
  theme: "auto",
59036
59309
  palette: "nord",