@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/index.cjs CHANGED
@@ -895,9 +895,7 @@ var init_reserved_key_registry = __esm({
895
895
  BOXES_AND_LINES_REGISTRY = staticRegistry([
896
896
  "color",
897
897
  "description",
898
- "width",
899
- "split",
900
- "fanout"
898
+ "value"
901
899
  ]);
902
900
  TIMELINE_REGISTRY = staticRegistry([
903
901
  "color",
@@ -16899,6 +16897,21 @@ function parseBoxesAndLines(content) {
16899
16897
  }
16900
16898
  continue;
16901
16899
  }
16900
+ if (!contentStarted) {
16901
+ const metricMatch = trimmed.match(/^box-metric\s+(.+)$/i);
16902
+ if (metricMatch) {
16903
+ const { label, colorName } = peelTrailingColorName(
16904
+ metricMatch[1].trim()
16905
+ );
16906
+ result.boxMetric = label;
16907
+ if (colorName !== void 0) result.boxMetricColor = colorName;
16908
+ continue;
16909
+ }
16910
+ if (/^show-values$/i.test(trimmed)) {
16911
+ result.showValues = true;
16912
+ continue;
16913
+ }
16914
+ }
16902
16915
  if (!contentStarted) {
16903
16916
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
16904
16917
  if (optMatch) {
@@ -17277,6 +17290,19 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
17277
17290
  description = [metadata["description"]];
17278
17291
  delete metadata["description"];
17279
17292
  }
17293
+ let value;
17294
+ if (metadata["value"] !== void 0) {
17295
+ const raw = metadata["value"];
17296
+ const num = Number(raw);
17297
+ if (Number.isFinite(num)) {
17298
+ value = num;
17299
+ } else {
17300
+ diagnostics.push(
17301
+ makeDgmoError(lineNum, `value must be a number (got "${raw}")`, "error")
17302
+ );
17303
+ }
17304
+ delete metadata["value"];
17305
+ }
17280
17306
  if (split.alias) {
17281
17307
  nameAliasMap?.set(normalizeName(split.alias), label);
17282
17308
  }
@@ -17285,7 +17311,8 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
17285
17311
  label,
17286
17312
  lineNumber: lineNum,
17287
17313
  metadata,
17288
- ...description !== void 0 && { description }
17314
+ ...description !== void 0 && { description },
17315
+ ...value !== void 0 && { value }
17289
17316
  };
17290
17317
  }
17291
17318
  function splitTargetAndMeta(rest, metaAliasMap) {
@@ -26405,7 +26432,18 @@ function fitLabelToHeader(label, nodeWidth, maxLines) {
26405
26432
  const truncated = label.length > maxChars ? label.slice(0, maxChars - 1) + "\u2026" : label;
26406
26433
  return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
26407
26434
  }
26408
- function nodeColors(node, tagGroups, activeGroupName, palette, isDark, solid) {
26435
+ function nodeColors(node, tagGroups, activeGroupName, palette, isDark, value, solid) {
26436
+ const neutralFill = mix(palette.bg, palette.text, isDark ? 90 : 95);
26437
+ if (value.active) {
26438
+ const fill3 = node.value !== void 0 ? value.fillForValue(node.value) : neutralFill;
26439
+ const stroke3 = value.hue;
26440
+ const text2 = contrastText(
26441
+ fill3,
26442
+ palette.textOnFillLight,
26443
+ palette.textOnFillDark
26444
+ );
26445
+ return { fill: fill3, stroke: stroke3, text: text2 };
26446
+ }
26409
26447
  const tagColor = resolveTagColor(
26410
26448
  node.metadata,
26411
26449
  [...tagGroups],
@@ -26514,25 +26552,65 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26514
26552
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26515
26553
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26516
26554
  const sTitleY = sctx.structural(TITLE_Y);
26555
+ const nodeValues = parsed.nodes.filter((n) => n.value !== void 0).map((n) => n.value);
26556
+ const hasRamp = nodeValues.length > 0;
26557
+ const allNonNegative = hasRamp && nodeValues.every((v) => v >= 0);
26558
+ const rampMin = allNonNegative ? 0 : Math.min(...nodeValues);
26559
+ const rampMax = Math.max(...nodeValues);
26560
+ const rampHue = resolveColor(parsed.boxMetricColor ?? "", palette) ?? palette.primary;
26561
+ const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
26562
+ const fillForValue = (v) => {
26563
+ const t = rampMax > rampMin ? (v - rampMin) / (rampMax - rampMin) : 1;
26564
+ const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
26565
+ return mix(rampHue, rampBase, pct);
26566
+ };
26567
+ const VALUE_NAME = hasRamp ? parsed.boxMetric?.trim() || "Value" : null;
26568
+ const matchColorGroup = (v) => {
26569
+ const lv = v.trim().toLowerCase();
26570
+ if (lv === "" || lv === "none") return null;
26571
+ const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
26572
+ if (tg) return tg.name;
26573
+ if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
26574
+ return v;
26575
+ };
26576
+ const override = activeTagGroup;
26577
+ let activeGroup;
26578
+ if (override !== void 0) {
26579
+ activeGroup = override === null ? null : matchColorGroup(override);
26580
+ } else if (parsed.options["active-tag"] !== void 0) {
26581
+ activeGroup = matchColorGroup(parsed.options["active-tag"]);
26582
+ } else {
26583
+ activeGroup = VALUE_NAME ?? (parsed.tagGroups.length > 0 ? parsed.tagGroups[0].name : null);
26584
+ }
26585
+ const activeIsValue = VALUE_NAME !== null && activeGroup === VALUE_NAME;
26586
+ const valueGroup = VALUE_NAME !== null ? {
26587
+ name: VALUE_NAME,
26588
+ entries: [],
26589
+ gradient: {
26590
+ min: rampMin,
26591
+ max: rampMax,
26592
+ hue: rampHue,
26593
+ base: rampBase
26594
+ }
26595
+ } : null;
26596
+ const legendGroups = [
26597
+ ...valueGroup ? [valueGroup] : [],
26598
+ ...parsed.tagGroups
26599
+ ];
26517
26600
  const reserveHasDescriptions = parsed.nodes.some(
26518
26601
  (n) => n.description && n.description.length > 0
26519
26602
  );
26520
- const willRenderLegend = parsed.tagGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26603
+ const willRenderLegend = legendGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26521
26604
  const sLegendHeight = willRenderLegend ? sctx.structural(
26522
26605
  getMaxLegendReservedHeight(
26523
26606
  {
26524
- groups: parsed.tagGroups,
26607
+ groups: legendGroups,
26525
26608
  position: { placement: "top-center", titleRelation: "below-title" },
26526
26609
  mode: exportMode ? "export" : "preview"
26527
26610
  },
26528
26611
  width
26529
26612
  )
26530
26613
  ) : 0;
26531
- const activeGroup = resolveActiveTagGroup(
26532
- parsed.tagGroups,
26533
- parsed.options["active-tag"],
26534
- activeTagGroup
26535
- );
26536
26614
  const hidden = hiddenTagValues ?? parsed.initialHiddenTagValues;
26537
26615
  const nodeMap = /* @__PURE__ */ new Map();
26538
26616
  for (const node of parsed.nodes) nodeMap.set(node.label, node);
@@ -26543,7 +26621,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26543
26621
  const hasAnyDescriptions = parsed.nodes.some(
26544
26622
  (n) => n.description && n.description.length > 0
26545
26623
  );
26546
- const needsLegend = parsed.tagGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26624
+ const needsLegend = legendGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26547
26625
  const legendH = needsLegend ? sLegendHeight + 8 : 0;
26548
26626
  const groupLabelsSet = new Set(layout.groups.map((g) => g.label));
26549
26627
  let labelZoneExtension = 0;
@@ -26749,12 +26827,16 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26749
26827
  activeGroup,
26750
26828
  palette,
26751
26829
  isDark,
26830
+ { active: activeIsValue, hue: rampHue, fillForValue },
26752
26831
  parsed.options["solid-fill"] === "on"
26753
26832
  );
26754
26833
  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);
26755
26834
  for (const [key, val] of Object.entries(node.metadata)) {
26756
26835
  nodeG.attr(`data-tag-${key.toLowerCase()}`, val.toLowerCase());
26757
26836
  }
26837
+ if (node.value !== void 0) {
26838
+ nodeG.attr("data-value", node.value);
26839
+ }
26758
26840
  if (onClickItem) {
26759
26841
  nodeG.on("click", (event) => {
26760
26842
  const target = event.target;
@@ -26826,6 +26908,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26826
26908
  const tooltipText = fullText.length > 200 ? fullText.slice(0, 199) + "\u2026" : fullText;
26827
26909
  nodeG.append("title").text(tooltipText);
26828
26910
  }
26911
+ } else if (parsed.showValues && node.value !== void 0) {
26912
+ const valueLabel = parsed.boxMetric ? `${parsed.boxMetric}: ${node.value}` : String(node.value);
26913
+ const headerH = ln.height / 2;
26914
+ const sepY = -ln.height / 2 + headerH;
26915
+ const fitted = fitLabelToHeader(node.label, ln.width, 2);
26916
+ const labelLineH = fitted.fontSize * 1.3;
26917
+ const labelTotalH = fitted.lines.length * labelLineH;
26918
+ const headerCenterY = -ln.height / 2 + headerH / 2;
26919
+ for (let li = 0; li < fitted.lines.length; li++) {
26920
+ nodeG.append("text").attr("x", 0).attr(
26921
+ "y",
26922
+ headerCenterY - labelTotalH / 2 + labelLineH / 2 + li * labelLineH
26923
+ ).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]);
26924
+ }
26925
+ 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);
26926
+ 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);
26829
26927
  } else {
26830
26928
  const maxLabelLines = Math.max(
26831
26929
  2,
@@ -26838,11 +26936,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26838
26936
  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]);
26839
26937
  }
26840
26938
  }
26939
+ if (parsed.showValues && node.value !== void 0 && desc && desc.length > 0 && !hideDescriptions) {
26940
+ const valueText = String(node.value);
26941
+ const padX = 6;
26942
+ const padY = 5;
26943
+ const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
26944
+ const bh = VALUE_FONT_SIZE + 4;
26945
+ const bx = Math.max(-ln.width / 2 + 4, ln.width / 2 - bw - 4);
26946
+ const by = -ln.height / 2 + 4;
26947
+ 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);
26948
+ 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);
26949
+ }
26841
26950
  }
26842
26951
  const hasDescriptions = parsed.nodes.some(
26843
26952
  (n) => n.description && n.description.length > 0
26844
26953
  );
26845
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26954
+ const hasLegend = legendGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26846
26955
  if (hasLegend) {
26847
26956
  let controlsGroup;
26848
26957
  if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
@@ -26860,7 +26969,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26860
26969
  };
26861
26970
  }
26862
26971
  const legendConfig = {
26863
- groups: parsed.tagGroups,
26972
+ groups: legendGroups,
26864
26973
  position: { placement: "top-center", titleRelation: "below-title" },
26865
26974
  mode: exportMode ? "export" : "preview",
26866
26975
  // Keep inactive sibling tag groups visible as collapsed pills so the user
@@ -26915,7 +27024,7 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
26915
27024
  }
26916
27025
  });
26917
27026
  }
26918
- 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;
27027
+ 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;
26919
27028
  var init_renderer6 = __esm({
26920
27029
  "src/boxes-and-lines/renderer.ts"() {
26921
27030
  "use strict";
@@ -26926,12 +27035,13 @@ var init_renderer6 = __esm({
26926
27035
  init_legend_layout();
26927
27036
  init_title_constants();
26928
27037
  init_color_utils();
27038
+ init_colors();
26929
27039
  init_tag_groups();
26930
27040
  init_inline_markdown();
26931
27041
  init_wrapped_desc();
26932
27042
  init_scaling();
26933
27043
  DIAGRAM_PADDING6 = 20;
26934
- NODE_FONT_SIZE = 13;
27044
+ NODE_FONT_SIZE = 11;
26935
27045
  MIN_NODE_FONT_SIZE = 9;
26936
27046
  EDGE_LABEL_FONT_SIZE4 = 11;
26937
27047
  EDGE_STROKE_WIDTH5 = 1.5;
@@ -26948,6 +27058,8 @@ var init_renderer6 = __esm({
26948
27058
  GROUP_RX = 8;
26949
27059
  GROUP_LABEL_FONT_SIZE = 14;
26950
27060
  GROUP_LABEL_ZONE = 32;
27061
+ RAMP_FLOOR = 15;
27062
+ VALUE_FONT_SIZE = 11;
26951
27063
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
26952
27064
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
26953
27065
  }
@@ -46666,7 +46778,11 @@ function resolveMap(parsed, data) {
46666
46778
  if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46667
46779
  }
46668
46780
  const containerUnion = unionExtent(containerBoxes, points);
46669
- if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46781
+ if (containerUnion)
46782
+ extent2 = pad(
46783
+ clampContainerToCluster(containerUnion, points),
46784
+ PAD_FRACTION
46785
+ );
46670
46786
  }
46671
46787
  if (isPoiOnly) {
46672
46788
  const cx = (extent2[0][0] + extent2[1][0]) / 2;
@@ -46747,6 +46863,22 @@ function mostCommonCountry(regions, poiCountries) {
46747
46863
  }
46748
46864
  return best;
46749
46865
  }
46866
+ function clampContainerToCluster(container, points) {
46867
+ const poi = unionExtent([], points);
46868
+ if (!poi) return container;
46869
+ let [[west, south], [east, north]] = container;
46870
+ const [[pWest, pSouth], [pEast, pNorth]] = poi;
46871
+ south = Math.max(south, pSouth - CONTAINER_OVERSHOOT_DEG);
46872
+ north = Math.min(north, pNorth + CONTAINER_OVERSHOOT_DEG);
46873
+ if (east <= 180 && pEast <= 180) {
46874
+ west = Math.max(west, pWest - CONTAINER_OVERSHOOT_DEG);
46875
+ east = Math.min(east, pEast + CONTAINER_OVERSHOOT_DEG);
46876
+ }
46877
+ return [
46878
+ [west, south],
46879
+ [east, north]
46880
+ ];
46881
+ }
46750
46882
  function pad(e, frac) {
46751
46883
  const dLon = (e[1][0] - e[0][0]) * frac || 1;
46752
46884
  const dLat = (e[1][1] - e[0][1]) * frac || 1;
@@ -46759,7 +46891,7 @@ function firstError(diags) {
46759
46891
  const e = diags.find((d) => d.severity === "error");
46760
46892
  return e ? formatDgmoError(e) : null;
46761
46893
  }
46762
- 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;
46894
+ 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;
46763
46895
  var init_resolver2 = __esm({
46764
46896
  "src/map/resolver.ts"() {
46765
46897
  "use strict";
@@ -46772,6 +46904,7 @@ var init_resolver2 = __esm({
46772
46904
  WORLD_LAT_SOUTH = -58;
46773
46905
  WORLD_LAT_NORTH = 78;
46774
46906
  POI_ZOOM_FLOOR_DEG = 7;
46907
+ CONTAINER_OVERSHOOT_DEG = 8;
46775
46908
  US_NATIONAL_LON_SPAN = 48;
46776
46909
  REGION_ALIASES = {
46777
46910
  // Common everyday names → the Natural-Earth display name actually shipped.
@@ -46850,6 +46983,55 @@ var init_resolver2 = __esm({
46850
46983
  }
46851
46984
  });
46852
46985
 
46986
+ // src/map/legend-band.ts
46987
+ function mapLegendGroups(legend) {
46988
+ const ramp = legend.ramp;
46989
+ const scoreGroup = ramp ? {
46990
+ name: ramp.metric?.trim() || "Value",
46991
+ entries: [],
46992
+ gradient: {
46993
+ min: ramp.min,
46994
+ max: ramp.max,
46995
+ hue: ramp.hue,
46996
+ base: ramp.base
46997
+ }
46998
+ } : null;
46999
+ const tagGroups = legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
47000
+ return [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
47001
+ }
47002
+ function mapLegendConfig(groups, mode) {
47003
+ return {
47004
+ groups,
47005
+ position: { placement: "top-center", titleRelation: "below-title" },
47006
+ mode,
47007
+ showEmptyGroups: false,
47008
+ showInactivePills: true
47009
+ };
47010
+ }
47011
+ function mapLegendTop(hasTitle, hasSubtitle) {
47012
+ return (hasTitle ? TITLE_Y + TITLE_FONT_SIZE : 0) + (hasSubtitle ? TITLE_FONT_SIZE : 0) + LEGEND_TOP_GAP2;
47013
+ }
47014
+ function mapLegendBand(legend, opts) {
47015
+ if (!legend) return 0;
47016
+ const groups = mapLegendGroups(legend);
47017
+ if (groups.length === 0) return 0;
47018
+ const config = mapLegendConfig(groups, opts.mode);
47019
+ const state = { activeGroup: legend.activeGroup };
47020
+ const { height } = computeLegendLayout(config, state, opts.width);
47021
+ if (height <= 0) return 0;
47022
+ return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
47023
+ }
47024
+ var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
47025
+ var init_legend_band = __esm({
47026
+ "src/map/legend-band.ts"() {
47027
+ "use strict";
47028
+ init_legend_layout();
47029
+ init_title_constants();
47030
+ LEGEND_TOP_GAP2 = 8;
47031
+ LEGEND_BOTTOM_GAP2 = 10;
47032
+ }
47033
+ });
47034
+
46853
47035
  // src/map/colorize.ts
46854
47036
  function assignColors(isos, adjacency) {
46855
47037
  const sorted = [...isos].sort();
@@ -47279,6 +47461,38 @@ function parsePathRings(d) {
47279
47461
  if (cur.length) rings.push(cur);
47280
47462
  return rings;
47281
47463
  }
47464
+ function dropAntimeridianWrapSlivers(d, width, height) {
47465
+ const rings = parsePathRings(d);
47466
+ if (rings.length <= 1) return d;
47467
+ const eps = 0.75;
47468
+ const minArea = 3e-3 * width * height;
47469
+ const ringArea = (r) => {
47470
+ let s = 0;
47471
+ for (let i = 0; i < r.length; i++) {
47472
+ const a = r[i];
47473
+ const b = r[(i + 1) % r.length];
47474
+ s += a[0] * b[1] - b[0] * a[1];
47475
+ }
47476
+ return Math.abs(s) / 2;
47477
+ };
47478
+ const areas = rings.map(ringArea);
47479
+ const maxArea = Math.max(...areas);
47480
+ 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;
47481
+ let dropped = false;
47482
+ const kept = rings.filter((r, idx) => {
47483
+ if (areas[idx] >= maxArea || areas[idx] >= minArea) return true;
47484
+ const touches = r.some((p, i) => onVEdge(p, r[(i + 1) % r.length]));
47485
+ if (touches) {
47486
+ dropped = true;
47487
+ return false;
47488
+ }
47489
+ return true;
47490
+ });
47491
+ if (!dropped) return d;
47492
+ return kept.map(
47493
+ (r) => r.map((p, i) => (i ? "L" : "M") + p[0] + "," + p[1]).join("") + "Z"
47494
+ ).join("");
47495
+ }
47282
47496
  function layoutMap(resolved, data, size, opts) {
47283
47497
  const { palette, isDark } = opts;
47284
47498
  const { width, height } = size;
@@ -47362,7 +47576,7 @@ function layoutMap(resolved, data, size, opts) {
47362
47576
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
47363
47577
  const fillForValue = (s) => {
47364
47578
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
47365
- const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
47579
+ const pct = RAMP_FLOOR2 + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR2);
47366
47580
  return mix(rampHue, rampBase, pct);
47367
47581
  };
47368
47582
  const tagFill = (tags, groupName) => {
@@ -47398,12 +47612,43 @@ function layoutMap(resolved, data, size, opts) {
47398
47612
  return tagFill(r.tags, activeGroup) ?? neutralFill;
47399
47613
  };
47400
47614
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
47615
+ let legend = null;
47616
+ if (!resolved.directives.noLegend) {
47617
+ const legendTagGroups = resolved.tagGroups.map((g) => ({
47618
+ name: g.name,
47619
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
47620
+ }));
47621
+ if (legendTagGroups.length > 0 || hasRamp) {
47622
+ legend = {
47623
+ tagGroups: legendTagGroups,
47624
+ activeGroup,
47625
+ ...hasRamp && {
47626
+ ramp: {
47627
+ ...resolved.directives.regionMetric !== void 0 && {
47628
+ metric: resolved.directives.regionMetric
47629
+ },
47630
+ min: rampMin,
47631
+ max: rampMax,
47632
+ hue: rampHue,
47633
+ base: rampBase
47634
+ }
47635
+ }
47636
+ };
47637
+ }
47638
+ }
47401
47639
  const TITLE_GAP2 = 16;
47402
47640
  let topPad = FIT_PAD;
47403
47641
  if (resolved.title && resolved.pois.length > 0) {
47404
47642
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
47405
47643
  topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
47406
47644
  }
47645
+ const legendBand = mapLegendBand(legend, {
47646
+ width,
47647
+ mode: opts.legendMode ?? "preview",
47648
+ hasTitle: Boolean(resolved.title),
47649
+ hasSubtitle: Boolean(resolved.subtitle)
47650
+ });
47651
+ if (legendBand > topPad) topPad = legendBand;
47407
47652
  const fitBox = [
47408
47653
  [FIT_PAD, topPad],
47409
47654
  [
@@ -47421,10 +47666,11 @@ function layoutMap(resolved, data, size, opts) {
47421
47666
  const by0 = cb[0][1];
47422
47667
  const cw = cb[1][0] - bx0;
47423
47668
  const ch = cb[1][1] - by0;
47424
- const ox = fitBox[0][0];
47425
- const oy = fitBox[0][1];
47426
- const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
47427
- const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47669
+ const topReserve = resolved.title && resolved.pois.length > 0 || legendBand > 0 ? topPad : 0;
47670
+ const ox = 0;
47671
+ const oy = topReserve;
47672
+ const sx = cw > 0 ? width / cw : 1;
47673
+ const sy = ch > 0 ? (height - topReserve) / ch : 1;
47428
47674
  stretchParams = { sx, sy, ox, oy, bx0, by0 };
47429
47675
  const stretch = (x, y) => [
47430
47676
  ox + (x - bx0) * sx,
@@ -47697,7 +47943,8 @@ function layoutMap(resolved, data, size, opts) {
47697
47943
  const r = regionById.get(iso);
47698
47944
  const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
47699
47945
  if (!viewF) continue;
47700
- const d = path(viewF) ?? "";
47946
+ const raw = path(viewF) ?? "";
47947
+ const d = fitIsGlobal ? dropAntimeridianWrapSlivers(raw, width, height) : raw;
47701
47948
  if (!d) continue;
47702
47949
  const isThisLayer = r?.layer === layerKind;
47703
47950
  const isForeign = layerKind === "country" && usContext && iso !== "US";
@@ -47714,6 +47961,9 @@ function layoutMap(resolved, data, size, opts) {
47714
47961
  } else {
47715
47962
  label = f.properties?.name;
47716
47963
  }
47964
+ const labelAnchor = WORLD_LABEL_ANCHORS[iso];
47965
+ const c = labelAnchor ? project(labelAnchor[0], labelAnchor[1]) : path.centroid(viewF);
47966
+ const hasCentroid = c != null && Number.isFinite(c[0]) && Number.isFinite(c[1]);
47717
47967
  regions.push({
47718
47968
  id: iso,
47719
47969
  d,
@@ -47722,6 +47972,7 @@ function layoutMap(resolved, data, size, opts) {
47722
47972
  lineNumber,
47723
47973
  layer,
47724
47974
  ...label !== void 0 && { label },
47975
+ ...hasCentroid && { labelX: c[0], labelY: c[1] },
47725
47976
  ...isThisLayer && r.value !== void 0 && { value: r.value },
47726
47977
  ...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
47727
47978
  });
@@ -48162,10 +48413,6 @@ function layoutMap(resolved, data, size, opts) {
48162
48413
  lineNumber
48163
48414
  });
48164
48415
  };
48165
- const WORLD_LABEL_ANCHORS = {
48166
- US: [-98.5, 39.5]
48167
- // CONUS geographic centre (near Lebanon, Kansas)
48168
- };
48169
48416
  const REGION_LABEL_GAP = 2;
48170
48417
  const regionLabelRect = (cx, cy, text) => {
48171
48418
  const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
@@ -48483,30 +48730,6 @@ function layoutMap(resolved, data, size, opts) {
48483
48730
  });
48484
48731
  labels.push(...contextLabels);
48485
48732
  }
48486
- let legend = null;
48487
- if (!resolved.directives.noLegend) {
48488
- const tagGroups = resolved.tagGroups.map((g) => ({
48489
- name: g.name,
48490
- entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
48491
- }));
48492
- if (tagGroups.length > 0 || hasRamp) {
48493
- legend = {
48494
- tagGroups,
48495
- activeGroup,
48496
- ...hasRamp && {
48497
- ramp: {
48498
- ...resolved.directives.regionMetric !== void 0 && {
48499
- metric: resolved.directives.regionMetric
48500
- },
48501
- min: rampMin,
48502
- max: rampMax,
48503
- hue: rampHue,
48504
- base: rampBase
48505
- }
48506
- }
48507
- };
48508
- }
48509
- }
48510
48733
  return {
48511
48734
  width,
48512
48735
  height,
@@ -48531,7 +48754,7 @@ function layoutMap(resolved, data, size, opts) {
48531
48754
  diagnostics: []
48532
48755
  };
48533
48756
  }
48534
- 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;
48757
+ 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;
48535
48758
  var init_layout15 = __esm({
48536
48759
  "src/map/layout.ts"() {
48537
48760
  "use strict";
@@ -48544,15 +48767,20 @@ var init_layout15 = __esm({
48544
48767
  init_label_layout();
48545
48768
  init_legend_constants();
48546
48769
  init_title_constants();
48770
+ init_legend_band();
48547
48771
  init_context_labels();
48548
48772
  FIT_PAD = 24;
48549
- RAMP_FLOOR = 15;
48773
+ RAMP_FLOOR2 = 15;
48550
48774
  R_DEFAULT = 6;
48551
48775
  R_MIN = 4;
48552
48776
  R_MAX = 22;
48553
48777
  W_MIN = 1.25;
48554
48778
  W_MAX = 8;
48555
48779
  FONT2 = 11;
48780
+ WORLD_LABEL_ANCHORS = {
48781
+ US: [-98.5, 39.5]
48782
+ // CONUS geographic centre (near Lebanon, Kansas)
48783
+ };
48556
48784
  MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48557
48785
  MAX_COLUMN_ROWS = 7;
48558
48786
  REGION_LABEL_HALO_RATIO = 4.5;
@@ -48566,9 +48794,9 @@ var init_layout15 = __esm({
48566
48794
  COMPACT_WIDTH_PX = 480;
48567
48795
  RELIEF_MIN_AREA = 12;
48568
48796
  RELIEF_MIN_DIM = 2;
48569
- RELIEF_HATCH_SPACING = 2;
48570
- RELIEF_HATCH_WIDTH = 0.15;
48571
- RELIEF_HATCH_STRENGTH = 32;
48797
+ RELIEF_HATCH_SPACING = 1.5;
48798
+ RELIEF_HATCH_WIDTH = 0.2;
48799
+ RELIEF_HATCH_STRENGTH = 26;
48572
48800
  COASTLINE_RING_COUNT = 5;
48573
48801
  COASTLINE_D0 = 16e-4;
48574
48802
  COASTLINE_STEP = 28e-4;
@@ -48646,7 +48874,47 @@ function ringToPath(ring) {
48646
48874
  d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48647
48875
  return d + "Z";
48648
48876
  }
48649
- function coastlineOuterRings(regions, minExtent) {
48877
+ function polylineToPath(pts) {
48878
+ let d = "";
48879
+ for (let i = 0; i < pts.length; i++)
48880
+ d += (i ? "L" : "M") + pts[i][0] + "," + pts[i][1];
48881
+ return d;
48882
+ }
48883
+ function ringToCoastPaths(ring, frame) {
48884
+ if (!frame) return [ringToPath(ring)];
48885
+ const n = ring.length;
48886
+ const eps = 0.75;
48887
+ const onL = (x) => Math.abs(x) <= eps;
48888
+ const onR = (x) => Math.abs(x - frame.w) <= eps;
48889
+ const onT = (y) => Math.abs(y) <= eps;
48890
+ const onB = (y) => Math.abs(y - frame.h) <= eps;
48891
+ 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]);
48892
+ let firstBreak = -1;
48893
+ for (let i = 0; i < n; i++)
48894
+ if (isFrameEdge(ring[i], ring[(i + 1) % n])) {
48895
+ firstBreak = i;
48896
+ break;
48897
+ }
48898
+ if (firstBreak === -1) return [ringToPath(ring)];
48899
+ const paths = [];
48900
+ let cur = [];
48901
+ const start = (firstBreak + 1) % n;
48902
+ for (let k = 0; k < n; k++) {
48903
+ const i = (start + k) % n;
48904
+ const a = ring[i];
48905
+ const b = ring[(i + 1) % n];
48906
+ if (isFrameEdge(a, b)) {
48907
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
48908
+ cur = [];
48909
+ continue;
48910
+ }
48911
+ if (cur.length === 0) cur.push(a);
48912
+ cur.push(b);
48913
+ }
48914
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
48915
+ return paths;
48916
+ }
48917
+ function coastlineOuterRings(regions, minExtent, frame) {
48650
48918
  const paths = [];
48651
48919
  for (const r of regions) {
48652
48920
  const rings = parsePathRings(r.d);
@@ -48669,7 +48937,7 @@ function coastlineOuterRings(regions, minExtent) {
48669
48937
  for (let j = 0; j < rings.length; j++)
48670
48938
  if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48671
48939
  if (depth % 2 === 1) continue;
48672
- paths.push(ringToPath(ring));
48940
+ paths.push(...ringToCoastPaths(ring, frame));
48673
48941
  }
48674
48942
  }
48675
48943
  return paths;
@@ -48699,6 +48967,9 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48699
48967
  // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48700
48968
  // keeps the global stretch-fill.
48701
48969
  preferContain: exportDims?.preferContain ?? false,
48970
+ // Reserve the legend band for the mode actually drawn below (export shows
48971
+ // only the active group; preview keeps the inactive pills).
48972
+ legendMode: exportDims ? "export" : "preview",
48702
48973
  ...activeGroupOverride !== void 0 && {
48703
48974
  activeGroup: activeGroupOverride
48704
48975
  }
@@ -48713,6 +48984,10 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48713
48984
  const drawRegion = (g, r, strokeWidth) => {
48714
48985
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
48715
48986
  if (r.label) p.attr("data-region-name", r.label);
48987
+ if (r.id && r.id !== "lake") p.attr("data-iso", r.id);
48988
+ if (r.labelX !== void 0 && r.labelY !== void 0) {
48989
+ p.attr("data-label-x", r.labelX).attr("data-label-y", r.labelY);
48990
+ }
48716
48991
  if (r.layer !== "base") {
48717
48992
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
48718
48993
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -48742,7 +49017,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48742
49017
  const landClip = defs.append("clipPath").attr("id", landClipId);
48743
49018
  for (const r of layout.regions)
48744
49019
  if (r.id !== "lake") landClip.append("path").attr("d", r.d);
48745
- 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");
49020
+ 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");
48746
49021
  for (let y = h.spacing; y < height; y += h.spacing) {
48747
49022
  gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
48748
49023
  }
@@ -48763,10 +49038,16 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48763
49038
  mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
48764
49039
  }
48765
49040
  }
48766
- const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
49041
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").style("pointer-events", "none").attr("mask", `url(#${maskId})`);
48767
49042
  appendWaterLines(
48768
49043
  gWater,
48769
- coastlineOuterRings(layout.regions, cs.minExtent),
49044
+ // Pass the canvas frame so edges collinear with it (the antimeridian on a
49045
+ // world map, regional clipExtent cuts) don't get ringed as fake coast —
49046
+ // land runs cleanly to the render-area edge.
49047
+ coastlineOuterRings(layout.regions, cs.minExtent, {
49048
+ w: width,
49049
+ h: height
49050
+ }),
48770
49051
  cs,
48771
49052
  layout.background
48772
49053
  );
@@ -48780,7 +49061,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48780
49061
  gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48781
49062
  }
48782
49063
  if (layout.rivers.length) {
48783
- const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
49064
+ const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none").style("pointer-events", "none");
48784
49065
  for (const r of layout.rivers) {
48785
49066
  gRivers.append("path").attr("d", r.d).attr("stroke", r.color).attr("stroke-width", r.width).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
48786
49067
  }
@@ -48821,7 +49102,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48821
49102
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48822
49103
  clip.append("path").attr("d", d);
48823
49104
  }
48824
- 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})`);
49105
+ 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})`);
48825
49106
  appendWaterLines(
48826
49107
  gInsetWater,
48827
49108
  coastlineOuterRings(layout.insetRegions, cs.minExtent),
@@ -48980,30 +49261,12 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48980
49261
  if (layout.legend) {
48981
49262
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
48982
49263
  const legendG = svg.append("g").attr("class", "dgmo-map-legend").attr("transform", `translate(0, ${legendY})`);
48983
- const ramp = layout.legend.ramp;
48984
- const scoreGroup = ramp ? {
48985
- name: ramp.metric?.trim() || "Value",
48986
- entries: [],
48987
- gradient: {
48988
- min: ramp.min,
48989
- max: ramp.max,
48990
- hue: ramp.hue,
48991
- base: ramp.base
48992
- }
48993
- } : null;
48994
- const tagGroups = layout.legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
48995
- const groups = [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
49264
+ const groups = mapLegendGroups(layout.legend);
48996
49265
  if (groups.length > 0) {
48997
- const config = {
49266
+ const config = mapLegendConfig(
48998
49267
  groups,
48999
- position: { placement: "top-center", titleRelation: "below-title" },
49000
- mode: exportDims ? "export" : "preview",
49001
- showEmptyGroups: false,
49002
- // Keep inactive siblings visible as pills so the user can click to flip
49003
- // the active colouring dimension (preview only — export shows just the
49004
- // active group).
49005
- showInactivePills: true
49006
- };
49268
+ exportDims ? "export" : "preview"
49269
+ );
49007
49270
  const state = { activeGroup: layout.legend.activeGroup };
49008
49271
  renderLegendD3(legendG, config, state, palette, isDark, void 0, width);
49009
49272
  }
@@ -49048,6 +49311,7 @@ var init_renderer16 = __esm({
49048
49311
  init_title_constants();
49049
49312
  init_color_utils();
49050
49313
  init_legend_d3();
49314
+ init_legend_band();
49051
49315
  init_layout15();
49052
49316
  LABEL_FONT = 11;
49053
49317
  }
@@ -49068,9 +49332,10 @@ function mapContentAspect(resolved, data, ref = REF) {
49068
49332
  const aspect = w / h;
49069
49333
  return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49070
49334
  }
49071
- function mapExportDimensions(resolved, data, baseWidth = 1200) {
49072
- const raw = mapContentAspect(resolved, data);
49073
- const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49335
+ function mapExportDimensions(resolved, data, baseWidth = 1200, aspectOverride) {
49336
+ const useOverride = aspectOverride !== void 0 && Number.isFinite(aspectOverride) && aspectOverride > 0;
49337
+ const raw = useOverride ? aspectOverride : mapContentAspect(resolved, data);
49338
+ const clamped = useOverride ? raw : Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49074
49339
  const width = baseWidth;
49075
49340
  let height = Math.round(width / clamped);
49076
49341
  let chromeReserve = 0;
@@ -49083,7 +49348,7 @@ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49083
49348
  height = Math.round(chromeReserve + MIN_MAP_BAND);
49084
49349
  floored = true;
49085
49350
  }
49086
- const preferContain = clamped !== raw || floored;
49351
+ const preferContain = useOverride ? floored : clamped !== raw || floored;
49087
49352
  return { width, height, preferContain };
49088
49353
  }
49089
49354
  var import_d3_geo3, FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
@@ -54790,7 +55055,6 @@ function renderTimelineTagLegendOverlay(container, parsed, palette, isDark, setu
54790
55055
  function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, setup, hovers, onClickItem, _exportDims, _swimlaneTagGroup, _activeTagGroup, _onTagStateChange, _viewMode) {
54791
55056
  const {
54792
55057
  width,
54793
- height,
54794
55058
  tooltip,
54795
55059
  solid,
54796
55060
  textColor,
@@ -54839,10 +55103,11 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
54839
55103
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
54840
55104
  const eraLabelY = eraReserve ? -(topScaleH + markerReserve + ERA_ROW_H / 2) : 0;
54841
55105
  const innerWidth = width - margin.left - margin.right;
54842
- const innerHeight = height - margin.top - margin.bottom;
54843
- const rowH = Math.min(ctx.structural(28), innerHeight / sorted.length);
55106
+ const rowH = ctx.structural(28);
55107
+ const innerHeight = rowH * sorted.length;
55108
+ const usedHeight = margin.top + innerHeight + margin.bottom;
54844
55109
  const xScale = d3Scale2.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
54845
- 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);
55110
+ 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);
54846
55111
  if (ctx.isBelowFloor) {
54847
55112
  svg.attr("width", "100%");
54848
55113
  }
@@ -57363,7 +57628,12 @@ async function renderForExport(content, theme, palette, viewState, options) {
57363
57628
  }
57364
57629
  }
57365
57630
  const mapResolved = resolveMap2(mapParsed, mapData);
57366
- const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57631
+ const dims2 = mapExportDimensions2(
57632
+ mapResolved,
57633
+ mapData,
57634
+ EXPORT_WIDTH,
57635
+ options?.mapAspect
57636
+ );
57367
57637
  const container2 = createExportContainer(dims2.width, dims2.height);
57368
57638
  renderMapForExport2(
57369
57639
  container2,
@@ -58156,8 +58426,10 @@ __export(index_exports, {
58156
58426
  decodeDiagramUrl: () => decodeDiagramUrl2,
58157
58427
  encodeDiagramUrl: () => encodeDiagramUrl2,
58158
58428
  formatDgmoError: () => formatDgmoError,
58429
+ getEmbedSvgViewBox: () => getEmbedSvgViewBox,
58159
58430
  getMinDimensions: () => getMinDimensions,
58160
58431
  getPalette: () => getPalette,
58432
+ normalizeSvgForEmbed: () => normalizeSvgForEmbed,
58161
58433
  palettes: () => palettes,
58162
58434
  render: () => render2,
58163
58435
  themes: () => themes,
@@ -58529,6 +58801,134 @@ function extractInfraCounts(content) {
58529
58801
  return { nodes: parsed.nodes.length };
58530
58802
  }
58531
58803
 
58804
+ // src/utils/svg-embed.ts
58805
+ function normalizeSvgForEmbed(input) {
58806
+ let svg = input;
58807
+ const rootMatch = svg.match(/<svg[^>]*>/);
58808
+ const rootTag = rootMatch?.[0] ?? "";
58809
+ if (rootTag && !rootTag.includes("viewBox")) {
58810
+ const wh = rootTag.match(/width="(\d+)"[^>]*height="(\d+)"/);
58811
+ if (wh) {
58812
+ svg = svg.replace(/<svg/, `<svg viewBox="0 0 ${wh[1]} ${wh[2]}"`);
58813
+ }
58814
+ }
58815
+ const tight = computeBBox(svg);
58816
+ if (tight && tight.width > 0 && tight.height > 0) {
58817
+ const pad2 = 16;
58818
+ const vb = `${tight.x - pad2} ${tight.y - pad2} ${tight.width + pad2 * 2} ${tight.height + pad2 * 2}`;
58819
+ svg = svg.replace(/(<svg[^>]*?)viewBox="[^"]*"/, `$1viewBox="${vb}"`);
58820
+ }
58821
+ svg = svg.replace(/(<svg[^>]*?) width="[^"]*"/g, "$1");
58822
+ svg = svg.replace(/(<svg[^>]*?) height="[^"]*"/g, "$1");
58823
+ svg = svg.replace(/(<svg[^>]*?style="[^"]*?)background:[^;"]*;?\s*/g, "$1");
58824
+ svg = svg.replace(/<svg\s{2,}/g, "<svg ");
58825
+ return svg;
58826
+ }
58827
+ function getEmbedSvgViewBox(svg) {
58828
+ const tight = computeBBox(svg);
58829
+ if (!tight || tight.width <= 0 || tight.height <= 0) return null;
58830
+ const pad2 = 16;
58831
+ return {
58832
+ x: tight.x - pad2,
58833
+ y: tight.y - pad2,
58834
+ width: tight.width + pad2 * 2,
58835
+ height: tight.height + pad2 * 2
58836
+ };
58837
+ }
58838
+ function computeBBox(svg) {
58839
+ const xs = [];
58840
+ const ys = [];
58841
+ function push(x, y) {
58842
+ if (Number.isFinite(x) && Number.isFinite(y)) {
58843
+ xs.push(x);
58844
+ ys.push(y);
58845
+ }
58846
+ }
58847
+ function attr(tag, name) {
58848
+ const m = tag.match(new RegExp(`\\b${name}="([^"]*)"`));
58849
+ if (!m) return null;
58850
+ const n = parseFloat(m[1]);
58851
+ return Number.isFinite(n) ? n : null;
58852
+ }
58853
+ for (const m of svg.matchAll(/<rect\b[^>]*?\/?>/g)) {
58854
+ const tag = m[0];
58855
+ const x = attr(tag, "x");
58856
+ const y = attr(tag, "y");
58857
+ const w = attr(tag, "width");
58858
+ const h = attr(tag, "height");
58859
+ if (x !== null && y !== null && w !== null && h !== null) {
58860
+ push(x, y);
58861
+ push(x + w, y + h);
58862
+ }
58863
+ }
58864
+ for (const m of svg.matchAll(/<line\b[^>]*?\/?>/g)) {
58865
+ const tag = m[0];
58866
+ const x1 = attr(tag, "x1");
58867
+ const y1 = attr(tag, "y1");
58868
+ const x2 = attr(tag, "x2");
58869
+ const y2 = attr(tag, "y2");
58870
+ if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
58871
+ push(x1, y1);
58872
+ push(x2, y2);
58873
+ }
58874
+ }
58875
+ for (const m of svg.matchAll(/<circle\b[^>]*?\/?>/g)) {
58876
+ const tag = m[0];
58877
+ const cx = attr(tag, "cx");
58878
+ const cy = attr(tag, "cy");
58879
+ const r = attr(tag, "r");
58880
+ if (cx !== null && cy !== null && r !== null) {
58881
+ push(cx - r, cy - r);
58882
+ push(cx + r, cy + r);
58883
+ }
58884
+ }
58885
+ for (const m of svg.matchAll(/<ellipse\b[^>]*?\/?>/g)) {
58886
+ const tag = m[0];
58887
+ const cx = attr(tag, "cx");
58888
+ const cy = attr(tag, "cy");
58889
+ const rx = attr(tag, "rx");
58890
+ const ry = attr(tag, "ry");
58891
+ if (cx !== null && cy !== null && rx !== null && ry !== null) {
58892
+ push(cx - rx, cy - ry);
58893
+ push(cx + rx, cy + ry);
58894
+ }
58895
+ }
58896
+ for (const m of svg.matchAll(/<text\b([^>]*?)>([\s\S]*?)<\/text>/g)) {
58897
+ const tag = `<text${m[1]}>`;
58898
+ const text = m[2].replace(/<[^>]+>/g, "");
58899
+ const x = attr(tag, "x");
58900
+ const y = attr(tag, "y");
58901
+ if (x !== null && y !== null) {
58902
+ const w = text.length * 7;
58903
+ push(x - w / 2, y - 14);
58904
+ push(x + w / 2, y + 4);
58905
+ }
58906
+ }
58907
+ for (const m of svg.matchAll(/<path\b[^>]*?\bd="([^"]+)"/g)) {
58908
+ const d = m[1];
58909
+ const nums = d.match(/-?\d+(?:\.\d+)?/g);
58910
+ if (!nums) continue;
58911
+ for (let i = 0; i + 1 < nums.length; i += 2) {
58912
+ push(parseFloat(nums[i]), parseFloat(nums[i + 1]));
58913
+ }
58914
+ }
58915
+ for (const m of svg.matchAll(
58916
+ /<(?:polygon|polyline)\b[^>]*?\bpoints="([^"]+)"/g
58917
+ )) {
58918
+ const nums = m[1].match(/-?\d+(?:\.\d+)?/g);
58919
+ if (!nums) continue;
58920
+ for (let i = 0; i + 1 < nums.length; i += 2) {
58921
+ push(parseFloat(nums[i]), parseFloat(nums[i + 1]));
58922
+ }
58923
+ }
58924
+ if (xs.length === 0 || ys.length === 0) return null;
58925
+ const minX = Math.min(...xs);
58926
+ const maxX = Math.max(...xs);
58927
+ const minY = Math.min(...ys);
58928
+ const maxY = Math.max(...ys);
58929
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
58930
+ }
58931
+
58532
58932
  // src/map/completion.ts
58533
58933
  var fold2 = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
58534
58934
  var groupThousands = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -58653,8 +59053,10 @@ function decodeDiagramUrl2(url) {
58653
59053
  decodeDiagramUrl,
58654
59054
  encodeDiagramUrl,
58655
59055
  formatDgmoError,
59056
+ getEmbedSvgViewBox,
58656
59057
  getMinDimensions,
58657
59058
  getPalette,
59059
+ normalizeSvgForEmbed,
58658
59060
  palettes,
58659
59061
  render,
58660
59062
  themes,