@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.js CHANGED
@@ -893,9 +893,7 @@ var init_reserved_key_registry = __esm({
893
893
  BOXES_AND_LINES_REGISTRY = staticRegistry([
894
894
  "color",
895
895
  "description",
896
- "width",
897
- "split",
898
- "fanout"
896
+ "value"
899
897
  ]);
900
898
  TIMELINE_REGISTRY = staticRegistry([
901
899
  "color",
@@ -16915,6 +16913,21 @@ function parseBoxesAndLines(content) {
16915
16913
  }
16916
16914
  continue;
16917
16915
  }
16916
+ if (!contentStarted) {
16917
+ const metricMatch = trimmed.match(/^box-metric\s+(.+)$/i);
16918
+ if (metricMatch) {
16919
+ const { label, colorName } = peelTrailingColorName(
16920
+ metricMatch[1].trim()
16921
+ );
16922
+ result.boxMetric = label;
16923
+ if (colorName !== void 0) result.boxMetricColor = colorName;
16924
+ continue;
16925
+ }
16926
+ if (/^show-values$/i.test(trimmed)) {
16927
+ result.showValues = true;
16928
+ continue;
16929
+ }
16930
+ }
16918
16931
  if (!contentStarted) {
16919
16932
  const optMatch = trimmed.match(OPTION_NOCOLON_RE);
16920
16933
  if (optMatch) {
@@ -17293,6 +17306,19 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
17293
17306
  description = [metadata["description"]];
17294
17307
  delete metadata["description"];
17295
17308
  }
17309
+ let value;
17310
+ if (metadata["value"] !== void 0) {
17311
+ const raw = metadata["value"];
17312
+ const num = Number(raw);
17313
+ if (Number.isFinite(num)) {
17314
+ value = num;
17315
+ } else {
17316
+ diagnostics.push(
17317
+ makeDgmoError(lineNum, `value must be a number (got "${raw}")`, "error")
17318
+ );
17319
+ }
17320
+ delete metadata["value"];
17321
+ }
17296
17322
  if (split.alias) {
17297
17323
  nameAliasMap?.set(normalizeName(split.alias), label);
17298
17324
  }
@@ -17301,7 +17327,8 @@ function parseNodeLine(trimmed, lineNum, metaAliasMap, diagnostics, nameAliasMap
17301
17327
  label,
17302
17328
  lineNumber: lineNum,
17303
17329
  metadata,
17304
- ...description !== void 0 && { description }
17330
+ ...description !== void 0 && { description },
17331
+ ...value !== void 0 && { value }
17305
17332
  };
17306
17333
  }
17307
17334
  function splitTargetAndMeta(rest, metaAliasMap) {
@@ -26423,7 +26450,18 @@ function fitLabelToHeader(label, nodeWidth, maxLines) {
26423
26450
  const truncated = label.length > maxChars ? label.slice(0, maxChars - 1) + "\u2026" : label;
26424
26451
  return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
26425
26452
  }
26426
- function nodeColors(node, tagGroups, activeGroupName, palette, isDark, solid) {
26453
+ function nodeColors(node, tagGroups, activeGroupName, palette, isDark, value, solid) {
26454
+ const neutralFill = mix(palette.bg, palette.text, isDark ? 90 : 95);
26455
+ if (value.active) {
26456
+ const fill3 = node.value !== void 0 ? value.fillForValue(node.value) : neutralFill;
26457
+ const stroke3 = value.hue;
26458
+ const text2 = contrastText(
26459
+ fill3,
26460
+ palette.textOnFillLight,
26461
+ palette.textOnFillDark
26462
+ );
26463
+ return { fill: fill3, stroke: stroke3, text: text2 };
26464
+ }
26427
26465
  const tagColor = resolveTagColor(
26428
26466
  node.metadata,
26429
26467
  [...tagGroups],
@@ -26532,25 +26570,65 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26532
26570
  const sGroupLabelZone = sctx.structural(GROUP_LABEL_ZONE);
26533
26571
  const sTitleFontSize = sctx.text(TITLE_FONT_SIZE);
26534
26572
  const sTitleY = sctx.structural(TITLE_Y);
26573
+ const nodeValues = parsed.nodes.filter((n) => n.value !== void 0).map((n) => n.value);
26574
+ const hasRamp = nodeValues.length > 0;
26575
+ const allNonNegative = hasRamp && nodeValues.every((v) => v >= 0);
26576
+ const rampMin = allNonNegative ? 0 : Math.min(...nodeValues);
26577
+ const rampMax = Math.max(...nodeValues);
26578
+ const rampHue = resolveColor(parsed.boxMetricColor ?? "", palette) ?? palette.primary;
26579
+ const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
26580
+ const fillForValue = (v) => {
26581
+ const t = rampMax > rampMin ? (v - rampMin) / (rampMax - rampMin) : 1;
26582
+ const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
26583
+ return mix(rampHue, rampBase, pct);
26584
+ };
26585
+ const VALUE_NAME = hasRamp ? parsed.boxMetric?.trim() || "Value" : null;
26586
+ const matchColorGroup = (v) => {
26587
+ const lv = v.trim().toLowerCase();
26588
+ if (lv === "" || lv === "none") return null;
26589
+ const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
26590
+ if (tg) return tg.name;
26591
+ if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
26592
+ return v;
26593
+ };
26594
+ const override = activeTagGroup;
26595
+ let activeGroup;
26596
+ if (override !== void 0) {
26597
+ activeGroup = override === null ? null : matchColorGroup(override);
26598
+ } else if (parsed.options["active-tag"] !== void 0) {
26599
+ activeGroup = matchColorGroup(parsed.options["active-tag"]);
26600
+ } else {
26601
+ activeGroup = VALUE_NAME ?? (parsed.tagGroups.length > 0 ? parsed.tagGroups[0].name : null);
26602
+ }
26603
+ const activeIsValue = VALUE_NAME !== null && activeGroup === VALUE_NAME;
26604
+ const valueGroup = VALUE_NAME !== null ? {
26605
+ name: VALUE_NAME,
26606
+ entries: [],
26607
+ gradient: {
26608
+ min: rampMin,
26609
+ max: rampMax,
26610
+ hue: rampHue,
26611
+ base: rampBase
26612
+ }
26613
+ } : null;
26614
+ const legendGroups = [
26615
+ ...valueGroup ? [valueGroup] : [],
26616
+ ...parsed.tagGroups
26617
+ ];
26535
26618
  const reserveHasDescriptions = parsed.nodes.some(
26536
26619
  (n) => n.description && n.description.length > 0
26537
26620
  );
26538
- const willRenderLegend = parsed.tagGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26621
+ const willRenderLegend = legendGroups.length > 0 || reserveHasDescriptions && controlsHost !== "app";
26539
26622
  const sLegendHeight = willRenderLegend ? sctx.structural(
26540
26623
  getMaxLegendReservedHeight(
26541
26624
  {
26542
- groups: parsed.tagGroups,
26625
+ groups: legendGroups,
26543
26626
  position: { placement: "top-center", titleRelation: "below-title" },
26544
26627
  mode: exportMode ? "export" : "preview"
26545
26628
  },
26546
26629
  width
26547
26630
  )
26548
26631
  ) : 0;
26549
- const activeGroup = resolveActiveTagGroup(
26550
- parsed.tagGroups,
26551
- parsed.options["active-tag"],
26552
- activeTagGroup
26553
- );
26554
26632
  const hidden = hiddenTagValues ?? parsed.initialHiddenTagValues;
26555
26633
  const nodeMap = /* @__PURE__ */ new Map();
26556
26634
  for (const node of parsed.nodes) nodeMap.set(node.label, node);
@@ -26561,7 +26639,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26561
26639
  const hasAnyDescriptions = parsed.nodes.some(
26562
26640
  (n) => n.description && n.description.length > 0
26563
26641
  );
26564
- const needsLegend = parsed.tagGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26642
+ const needsLegend = legendGroups.length > 0 || hasAnyDescriptions && onToggleDescriptions;
26565
26643
  const legendH = needsLegend ? sLegendHeight + 8 : 0;
26566
26644
  const groupLabelsSet = new Set(layout.groups.map((g) => g.label));
26567
26645
  let labelZoneExtension = 0;
@@ -26767,12 +26845,16 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26767
26845
  activeGroup,
26768
26846
  palette,
26769
26847
  isDark,
26848
+ { active: activeIsValue, hue: rampHue, fillForValue },
26770
26849
  parsed.options["solid-fill"] === "on"
26771
26850
  );
26772
26851
  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);
26773
26852
  for (const [key, val] of Object.entries(node.metadata)) {
26774
26853
  nodeG.attr(`data-tag-${key.toLowerCase()}`, val.toLowerCase());
26775
26854
  }
26855
+ if (node.value !== void 0) {
26856
+ nodeG.attr("data-value", node.value);
26857
+ }
26776
26858
  if (onClickItem) {
26777
26859
  nodeG.on("click", (event) => {
26778
26860
  const target = event.target;
@@ -26844,6 +26926,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26844
26926
  const tooltipText = fullText.length > 200 ? fullText.slice(0, 199) + "\u2026" : fullText;
26845
26927
  nodeG.append("title").text(tooltipText);
26846
26928
  }
26929
+ } else if (parsed.showValues && node.value !== void 0) {
26930
+ const valueLabel = parsed.boxMetric ? `${parsed.boxMetric}: ${node.value}` : String(node.value);
26931
+ const headerH = ln.height / 2;
26932
+ const sepY = -ln.height / 2 + headerH;
26933
+ const fitted = fitLabelToHeader(node.label, ln.width, 2);
26934
+ const labelLineH = fitted.fontSize * 1.3;
26935
+ const labelTotalH = fitted.lines.length * labelLineH;
26936
+ const headerCenterY = -ln.height / 2 + headerH / 2;
26937
+ for (let li = 0; li < fitted.lines.length; li++) {
26938
+ nodeG.append("text").attr("x", 0).attr(
26939
+ "y",
26940
+ headerCenterY - labelTotalH / 2 + labelLineH / 2 + li * labelLineH
26941
+ ).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]);
26942
+ }
26943
+ 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);
26944
+ 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);
26847
26945
  } else {
26848
26946
  const maxLabelLines = Math.max(
26849
26947
  2,
@@ -26856,11 +26954,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26856
26954
  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]);
26857
26955
  }
26858
26956
  }
26957
+ if (parsed.showValues && node.value !== void 0 && desc && desc.length > 0 && !hideDescriptions) {
26958
+ const valueText = String(node.value);
26959
+ const padX = 6;
26960
+ const padY = 5;
26961
+ const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
26962
+ const bh = VALUE_FONT_SIZE + 4;
26963
+ const bx = Math.max(-ln.width / 2 + 4, ln.width / 2 - bw - 4);
26964
+ const by = -ln.height / 2 + 4;
26965
+ 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);
26966
+ 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);
26967
+ }
26859
26968
  }
26860
26969
  const hasDescriptions = parsed.nodes.some(
26861
26970
  (n) => n.description && n.description.length > 0
26862
26971
  );
26863
- const hasLegend = parsed.tagGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26972
+ const hasLegend = legendGroups.length > 0 || hasDescriptions && controlsHost !== "app";
26864
26973
  if (hasLegend) {
26865
26974
  let controlsGroup;
26866
26975
  if (hasDescriptions && (onToggleDescriptions || controlsHost === "app")) {
@@ -26878,7 +26987,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26878
26987
  };
26879
26988
  }
26880
26989
  const legendConfig = {
26881
- groups: parsed.tagGroups,
26990
+ groups: legendGroups,
26882
26991
  position: { placement: "top-center", titleRelation: "below-title" },
26883
26992
  mode: exportMode ? "export" : "preview",
26884
26993
  // Keep inactive sibling tag groups visible as collapsed pills so the user
@@ -26933,7 +27042,7 @@ function renderBoxesAndLinesForExport(container, parsed, layout, palette, isDark
26933
27042
  }
26934
27043
  });
26935
27044
  }
26936
- 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;
27045
+ 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;
26937
27046
  var init_renderer6 = __esm({
26938
27047
  "src/boxes-and-lines/renderer.ts"() {
26939
27048
  "use strict";
@@ -26942,12 +27051,13 @@ var init_renderer6 = __esm({
26942
27051
  init_legend_layout();
26943
27052
  init_title_constants();
26944
27053
  init_color_utils();
27054
+ init_colors();
26945
27055
  init_tag_groups();
26946
27056
  init_inline_markdown();
26947
27057
  init_wrapped_desc();
26948
27058
  init_scaling();
26949
27059
  DIAGRAM_PADDING6 = 20;
26950
- NODE_FONT_SIZE = 13;
27060
+ NODE_FONT_SIZE = 11;
26951
27061
  MIN_NODE_FONT_SIZE = 9;
26952
27062
  EDGE_LABEL_FONT_SIZE4 = 11;
26953
27063
  EDGE_STROKE_WIDTH5 = 1.5;
@@ -26964,6 +27074,8 @@ var init_renderer6 = __esm({
26964
27074
  GROUP_RX = 8;
26965
27075
  GROUP_LABEL_FONT_SIZE = 14;
26966
27076
  GROUP_LABEL_ZONE = 32;
27077
+ RAMP_FLOOR = 15;
27078
+ VALUE_FONT_SIZE = 11;
26967
27079
  lineGeneratorLR = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
26968
27080
  lineGeneratorTB = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveBasis);
26969
27081
  }
@@ -46682,7 +46794,11 @@ function resolveMap(parsed, data) {
46682
46794
  if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46683
46795
  }
46684
46796
  const containerUnion = unionExtent(containerBoxes, points);
46685
- if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46797
+ if (containerUnion)
46798
+ extent2 = pad(
46799
+ clampContainerToCluster(containerUnion, points),
46800
+ PAD_FRACTION
46801
+ );
46686
46802
  }
46687
46803
  if (isPoiOnly) {
46688
46804
  const cx = (extent2[0][0] + extent2[1][0]) / 2;
@@ -46763,6 +46879,22 @@ function mostCommonCountry(regions, poiCountries) {
46763
46879
  }
46764
46880
  return best;
46765
46881
  }
46882
+ function clampContainerToCluster(container, points) {
46883
+ const poi = unionExtent([], points);
46884
+ if (!poi) return container;
46885
+ let [[west, south], [east, north]] = container;
46886
+ const [[pWest, pSouth], [pEast, pNorth]] = poi;
46887
+ south = Math.max(south, pSouth - CONTAINER_OVERSHOOT_DEG);
46888
+ north = Math.min(north, pNorth + CONTAINER_OVERSHOOT_DEG);
46889
+ if (east <= 180 && pEast <= 180) {
46890
+ west = Math.max(west, pWest - CONTAINER_OVERSHOOT_DEG);
46891
+ east = Math.min(east, pEast + CONTAINER_OVERSHOOT_DEG);
46892
+ }
46893
+ return [
46894
+ [west, south],
46895
+ [east, north]
46896
+ ];
46897
+ }
46766
46898
  function pad(e, frac) {
46767
46899
  const dLon = (e[1][0] - e[0][0]) * frac || 1;
46768
46900
  const dLat = (e[1][1] - e[0][1]) * frac || 1;
@@ -46775,7 +46907,7 @@ function firstError(diags) {
46775
46907
  const e = diags.find((d) => d.severity === "error");
46776
46908
  return e ? formatDgmoError(e) : null;
46777
46909
  }
46778
- 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;
46910
+ 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;
46779
46911
  var init_resolver2 = __esm({
46780
46912
  "src/map/resolver.ts"() {
46781
46913
  "use strict";
@@ -46788,6 +46920,7 @@ var init_resolver2 = __esm({
46788
46920
  WORLD_LAT_SOUTH = -58;
46789
46921
  WORLD_LAT_NORTH = 78;
46790
46922
  POI_ZOOM_FLOOR_DEG = 7;
46923
+ CONTAINER_OVERSHOOT_DEG = 8;
46791
46924
  US_NATIONAL_LON_SPAN = 48;
46792
46925
  REGION_ALIASES = {
46793
46926
  // Common everyday names → the Natural-Earth display name actually shipped.
@@ -46866,6 +46999,55 @@ var init_resolver2 = __esm({
46866
46999
  }
46867
47000
  });
46868
47001
 
47002
+ // src/map/legend-band.ts
47003
+ function mapLegendGroups(legend) {
47004
+ const ramp = legend.ramp;
47005
+ const scoreGroup = ramp ? {
47006
+ name: ramp.metric?.trim() || "Value",
47007
+ entries: [],
47008
+ gradient: {
47009
+ min: ramp.min,
47010
+ max: ramp.max,
47011
+ hue: ramp.hue,
47012
+ base: ramp.base
47013
+ }
47014
+ } : null;
47015
+ const tagGroups = legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
47016
+ return [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
47017
+ }
47018
+ function mapLegendConfig(groups, mode) {
47019
+ return {
47020
+ groups,
47021
+ position: { placement: "top-center", titleRelation: "below-title" },
47022
+ mode,
47023
+ showEmptyGroups: false,
47024
+ showInactivePills: true
47025
+ };
47026
+ }
47027
+ function mapLegendTop(hasTitle, hasSubtitle) {
47028
+ return (hasTitle ? TITLE_Y + TITLE_FONT_SIZE : 0) + (hasSubtitle ? TITLE_FONT_SIZE : 0) + LEGEND_TOP_GAP2;
47029
+ }
47030
+ function mapLegendBand(legend, opts) {
47031
+ if (!legend) return 0;
47032
+ const groups = mapLegendGroups(legend);
47033
+ if (groups.length === 0) return 0;
47034
+ const config = mapLegendConfig(groups, opts.mode);
47035
+ const state = { activeGroup: legend.activeGroup };
47036
+ const { height } = computeLegendLayout(config, state, opts.width);
47037
+ if (height <= 0) return 0;
47038
+ return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
47039
+ }
47040
+ var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
47041
+ var init_legend_band = __esm({
47042
+ "src/map/legend-band.ts"() {
47043
+ "use strict";
47044
+ init_legend_layout();
47045
+ init_title_constants();
47046
+ LEGEND_TOP_GAP2 = 8;
47047
+ LEGEND_BOTTOM_GAP2 = 10;
47048
+ }
47049
+ });
47050
+
46869
47051
  // src/map/colorize.ts
46870
47052
  function assignColors(isos, adjacency) {
46871
47053
  const sorted = [...isos].sort();
@@ -47306,6 +47488,38 @@ function parsePathRings(d) {
47306
47488
  if (cur.length) rings.push(cur);
47307
47489
  return rings;
47308
47490
  }
47491
+ function dropAntimeridianWrapSlivers(d, width, height) {
47492
+ const rings = parsePathRings(d);
47493
+ if (rings.length <= 1) return d;
47494
+ const eps = 0.75;
47495
+ const minArea = 3e-3 * width * height;
47496
+ const ringArea = (r) => {
47497
+ let s = 0;
47498
+ for (let i = 0; i < r.length; i++) {
47499
+ const a = r[i];
47500
+ const b = r[(i + 1) % r.length];
47501
+ s += a[0] * b[1] - b[0] * a[1];
47502
+ }
47503
+ return Math.abs(s) / 2;
47504
+ };
47505
+ const areas = rings.map(ringArea);
47506
+ const maxArea = Math.max(...areas);
47507
+ 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;
47508
+ let dropped = false;
47509
+ const kept = rings.filter((r, idx) => {
47510
+ if (areas[idx] >= maxArea || areas[idx] >= minArea) return true;
47511
+ const touches = r.some((p, i) => onVEdge(p, r[(i + 1) % r.length]));
47512
+ if (touches) {
47513
+ dropped = true;
47514
+ return false;
47515
+ }
47516
+ return true;
47517
+ });
47518
+ if (!dropped) return d;
47519
+ return kept.map(
47520
+ (r) => r.map((p, i) => (i ? "L" : "M") + p[0] + "," + p[1]).join("") + "Z"
47521
+ ).join("");
47522
+ }
47309
47523
  function layoutMap(resolved, data, size, opts) {
47310
47524
  const { palette, isDark } = opts;
47311
47525
  const { width, height } = size;
@@ -47389,7 +47603,7 @@ function layoutMap(resolved, data, size, opts) {
47389
47603
  const rampBase = isDark ? mix(palette.surface, palette.text, 28) : palette.bg;
47390
47604
  const fillForValue = (s) => {
47391
47605
  const t = rampMax > rampMin ? (s - rampMin) / (rampMax - rampMin) : 1;
47392
- const pct = RAMP_FLOOR + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR);
47606
+ const pct = RAMP_FLOOR2 + Math.max(0, Math.min(1, t)) * (100 - RAMP_FLOOR2);
47393
47607
  return mix(rampHue, rampBase, pct);
47394
47608
  };
47395
47609
  const tagFill = (tags, groupName) => {
@@ -47425,12 +47639,43 @@ function layoutMap(resolved, data, size, opts) {
47425
47639
  return tagFill(r.tags, activeGroup) ?? neutralFill;
47426
47640
  };
47427
47641
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
47642
+ let legend = null;
47643
+ if (!resolved.directives.noLegend) {
47644
+ const legendTagGroups = resolved.tagGroups.map((g) => ({
47645
+ name: g.name,
47646
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
47647
+ }));
47648
+ if (legendTagGroups.length > 0 || hasRamp) {
47649
+ legend = {
47650
+ tagGroups: legendTagGroups,
47651
+ activeGroup,
47652
+ ...hasRamp && {
47653
+ ramp: {
47654
+ ...resolved.directives.regionMetric !== void 0 && {
47655
+ metric: resolved.directives.regionMetric
47656
+ },
47657
+ min: rampMin,
47658
+ max: rampMax,
47659
+ hue: rampHue,
47660
+ base: rampBase
47661
+ }
47662
+ }
47663
+ };
47664
+ }
47665
+ }
47428
47666
  const TITLE_GAP2 = 16;
47429
47667
  let topPad = FIT_PAD;
47430
47668
  if (resolved.title && resolved.pois.length > 0) {
47431
47669
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
47432
47670
  topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
47433
47671
  }
47672
+ const legendBand = mapLegendBand(legend, {
47673
+ width,
47674
+ mode: opts.legendMode ?? "preview",
47675
+ hasTitle: Boolean(resolved.title),
47676
+ hasSubtitle: Boolean(resolved.subtitle)
47677
+ });
47678
+ if (legendBand > topPad) topPad = legendBand;
47434
47679
  const fitBox = [
47435
47680
  [FIT_PAD, topPad],
47436
47681
  [
@@ -47448,10 +47693,11 @@ function layoutMap(resolved, data, size, opts) {
47448
47693
  const by0 = cb[0][1];
47449
47694
  const cw = cb[1][0] - bx0;
47450
47695
  const ch = cb[1][1] - by0;
47451
- const ox = fitBox[0][0];
47452
- const oy = fitBox[0][1];
47453
- const sx = cw > 0 ? (fitBox[1][0] - ox) / cw : 1;
47454
- const sy = ch > 0 ? (fitBox[1][1] - oy) / ch : 1;
47696
+ const topReserve = resolved.title && resolved.pois.length > 0 || legendBand > 0 ? topPad : 0;
47697
+ const ox = 0;
47698
+ const oy = topReserve;
47699
+ const sx = cw > 0 ? width / cw : 1;
47700
+ const sy = ch > 0 ? (height - topReserve) / ch : 1;
47455
47701
  stretchParams = { sx, sy, ox, oy, bx0, by0 };
47456
47702
  const stretch = (x, y) => [
47457
47703
  ox + (x - bx0) * sx,
@@ -47724,7 +47970,8 @@ function layoutMap(resolved, data, size, opts) {
47724
47970
  const r = regionById.get(iso);
47725
47971
  const viewF = shouldCull ? cullFeatureToView(f) : dropFrameFillers(f);
47726
47972
  if (!viewF) continue;
47727
- const d = path(viewF) ?? "";
47973
+ const raw = path(viewF) ?? "";
47974
+ const d = fitIsGlobal ? dropAntimeridianWrapSlivers(raw, width, height) : raw;
47728
47975
  if (!d) continue;
47729
47976
  const isThisLayer = r?.layer === layerKind;
47730
47977
  const isForeign = layerKind === "country" && usContext && iso !== "US";
@@ -47741,6 +47988,9 @@ function layoutMap(resolved, data, size, opts) {
47741
47988
  } else {
47742
47989
  label = f.properties?.name;
47743
47990
  }
47991
+ const labelAnchor = WORLD_LABEL_ANCHORS[iso];
47992
+ const c = labelAnchor ? project(labelAnchor[0], labelAnchor[1]) : path.centroid(viewF);
47993
+ const hasCentroid = c != null && Number.isFinite(c[0]) && Number.isFinite(c[1]);
47744
47994
  regions.push({
47745
47995
  id: iso,
47746
47996
  d,
@@ -47749,6 +47999,7 @@ function layoutMap(resolved, data, size, opts) {
47749
47999
  lineNumber,
47750
48000
  layer,
47751
48001
  ...label !== void 0 && { label },
48002
+ ...hasCentroid && { labelX: c[0], labelY: c[1] },
47752
48003
  ...isThisLayer && r.value !== void 0 && { value: r.value },
47753
48004
  ...isThisLayer && Object.keys(r.tags).length > 0 && { tags: r.tags }
47754
48005
  });
@@ -48189,10 +48440,6 @@ function layoutMap(resolved, data, size, opts) {
48189
48440
  lineNumber
48190
48441
  });
48191
48442
  };
48192
- const WORLD_LABEL_ANCHORS = {
48193
- US: [-98.5, 39.5]
48194
- // CONUS geographic centre (near Lebanon, Kansas)
48195
- };
48196
48443
  const REGION_LABEL_GAP = 2;
48197
48444
  const regionLabelRect = (cx, cy, text) => {
48198
48445
  const w = measureLegendText(text, FONT2) + 2 * REGION_LABEL_GAP;
@@ -48510,30 +48757,6 @@ function layoutMap(resolved, data, size, opts) {
48510
48757
  });
48511
48758
  labels.push(...contextLabels);
48512
48759
  }
48513
- let legend = null;
48514
- if (!resolved.directives.noLegend) {
48515
- const tagGroups = resolved.tagGroups.map((g) => ({
48516
- name: g.name,
48517
- entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
48518
- }));
48519
- if (tagGroups.length > 0 || hasRamp) {
48520
- legend = {
48521
- tagGroups,
48522
- activeGroup,
48523
- ...hasRamp && {
48524
- ramp: {
48525
- ...resolved.directives.regionMetric !== void 0 && {
48526
- metric: resolved.directives.regionMetric
48527
- },
48528
- min: rampMin,
48529
- max: rampMax,
48530
- hue: rampHue,
48531
- base: rampBase
48532
- }
48533
- }
48534
- };
48535
- }
48536
- }
48537
48760
  return {
48538
48761
  width,
48539
48762
  height,
@@ -48558,7 +48781,7 @@ function layoutMap(resolved, data, size, opts) {
48558
48781
  diagnostics: []
48559
48782
  };
48560
48783
  }
48561
- 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;
48784
+ 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;
48562
48785
  var init_layout15 = __esm({
48563
48786
  "src/map/layout.ts"() {
48564
48787
  "use strict";
@@ -48569,15 +48792,20 @@ var init_layout15 = __esm({
48569
48792
  init_label_layout();
48570
48793
  init_legend_constants();
48571
48794
  init_title_constants();
48795
+ init_legend_band();
48572
48796
  init_context_labels();
48573
48797
  FIT_PAD = 24;
48574
- RAMP_FLOOR = 15;
48798
+ RAMP_FLOOR2 = 15;
48575
48799
  R_DEFAULT = 6;
48576
48800
  R_MIN = 4;
48577
48801
  R_MAX = 22;
48578
48802
  W_MIN = 1.25;
48579
48803
  W_MAX = 8;
48580
48804
  FONT2 = 11;
48805
+ WORLD_LABEL_ANCHORS = {
48806
+ US: [-98.5, 39.5]
48807
+ // CONUS geographic centre (near Lebanon, Kansas)
48808
+ };
48581
48809
  MAX_CLUSTER_EXTENT_FACTOR = 0.18;
48582
48810
  MAX_COLUMN_ROWS = 7;
48583
48811
  REGION_LABEL_HALO_RATIO = 4.5;
@@ -48591,9 +48819,9 @@ var init_layout15 = __esm({
48591
48819
  COMPACT_WIDTH_PX = 480;
48592
48820
  RELIEF_MIN_AREA = 12;
48593
48821
  RELIEF_MIN_DIM = 2;
48594
- RELIEF_HATCH_SPACING = 2;
48595
- RELIEF_HATCH_WIDTH = 0.15;
48596
- RELIEF_HATCH_STRENGTH = 32;
48822
+ RELIEF_HATCH_SPACING = 1.5;
48823
+ RELIEF_HATCH_WIDTH = 0.2;
48824
+ RELIEF_HATCH_STRENGTH = 26;
48597
48825
  COASTLINE_RING_COUNT = 5;
48598
48826
  COASTLINE_D0 = 16e-4;
48599
48827
  COASTLINE_STEP = 28e-4;
@@ -48672,7 +48900,47 @@ function ringToPath(ring) {
48672
48900
  d += (i ? "L" : "M") + ring[i][0] + "," + ring[i][1];
48673
48901
  return d + "Z";
48674
48902
  }
48675
- function coastlineOuterRings(regions, minExtent) {
48903
+ function polylineToPath(pts) {
48904
+ let d = "";
48905
+ for (let i = 0; i < pts.length; i++)
48906
+ d += (i ? "L" : "M") + pts[i][0] + "," + pts[i][1];
48907
+ return d;
48908
+ }
48909
+ function ringToCoastPaths(ring, frame) {
48910
+ if (!frame) return [ringToPath(ring)];
48911
+ const n = ring.length;
48912
+ const eps = 0.75;
48913
+ const onL = (x) => Math.abs(x) <= eps;
48914
+ const onR = (x) => Math.abs(x - frame.w) <= eps;
48915
+ const onT = (y) => Math.abs(y) <= eps;
48916
+ const onB = (y) => Math.abs(y - frame.h) <= eps;
48917
+ 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]);
48918
+ let firstBreak = -1;
48919
+ for (let i = 0; i < n; i++)
48920
+ if (isFrameEdge(ring[i], ring[(i + 1) % n])) {
48921
+ firstBreak = i;
48922
+ break;
48923
+ }
48924
+ if (firstBreak === -1) return [ringToPath(ring)];
48925
+ const paths = [];
48926
+ let cur = [];
48927
+ const start = (firstBreak + 1) % n;
48928
+ for (let k = 0; k < n; k++) {
48929
+ const i = (start + k) % n;
48930
+ const a = ring[i];
48931
+ const b = ring[(i + 1) % n];
48932
+ if (isFrameEdge(a, b)) {
48933
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
48934
+ cur = [];
48935
+ continue;
48936
+ }
48937
+ if (cur.length === 0) cur.push(a);
48938
+ cur.push(b);
48939
+ }
48940
+ if (cur.length >= 2) paths.push(polylineToPath(cur));
48941
+ return paths;
48942
+ }
48943
+ function coastlineOuterRings(regions, minExtent, frame) {
48676
48944
  const paths = [];
48677
48945
  for (const r of regions) {
48678
48946
  const rings = parsePathRings(r.d);
@@ -48695,7 +48963,7 @@ function coastlineOuterRings(regions, minExtent) {
48695
48963
  for (let j = 0; j < rings.length; j++)
48696
48964
  if (j !== i && pointInRing2(fx, fy, rings[j])) depth++;
48697
48965
  if (depth % 2 === 1) continue;
48698
- paths.push(ringToPath(ring));
48966
+ paths.push(...ringToCoastPaths(ring, frame));
48699
48967
  }
48700
48968
  }
48701
48969
  return paths;
@@ -48725,6 +48993,9 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48725
48993
  // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48726
48994
  // keeps the global stretch-fill.
48727
48995
  preferContain: exportDims?.preferContain ?? false,
48996
+ // Reserve the legend band for the mode actually drawn below (export shows
48997
+ // only the active group; preview keeps the inactive pills).
48998
+ legendMode: exportDims ? "export" : "preview",
48728
48999
  ...activeGroupOverride !== void 0 && {
48729
49000
  activeGroup: activeGroupOverride
48730
49001
  }
@@ -48739,6 +49010,10 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48739
49010
  const drawRegion = (g, r, strokeWidth) => {
48740
49011
  const p = g.append("path").attr("d", r.d).attr("fill", r.fill).attr("stroke", r.stroke).attr("stroke-width", strokeWidth);
48741
49012
  if (r.label) p.attr("data-region-name", r.label);
49013
+ if (r.id && r.id !== "lake") p.attr("data-iso", r.id);
49014
+ if (r.labelX !== void 0 && r.labelY !== void 0) {
49015
+ p.attr("data-label-x", r.labelX).attr("data-label-y", r.labelY);
49016
+ }
48742
49017
  if (r.layer !== "base") {
48743
49018
  p.classed("dgmo-map-region", true).attr("data-region", r.id);
48744
49019
  if (r.value !== void 0) p.attr("data-value", r.value);
@@ -48768,7 +49043,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48768
49043
  const landClip = defs.append("clipPath").attr("id", landClipId);
48769
49044
  for (const r of layout.regions)
48770
49045
  if (r.id !== "lake") landClip.append("path").attr("d", r.d);
48771
- 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");
49046
+ 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");
48772
49047
  for (let y = h.spacing; y < height; y += h.spacing) {
48773
49048
  gRelief.append("line").attr("x1", 0).attr("y1", y).attr("x2", width).attr("y2", y);
48774
49049
  }
@@ -48789,10 +49064,16 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48789
49064
  mask.append("path").attr("d", d).attr("fill", "black").attr("stroke", "black").attr("stroke-width", 2 * reach).attr("stroke-linejoin", "round");
48790
49065
  }
48791
49066
  }
48792
- const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").attr("mask", `url(#${maskId})`);
49067
+ const gWater = svg.append("g").attr("class", "dgmo-map-water-lines").attr("fill", "none").style("pointer-events", "none").attr("mask", `url(#${maskId})`);
48793
49068
  appendWaterLines(
48794
49069
  gWater,
48795
- coastlineOuterRings(layout.regions, cs.minExtent),
49070
+ // Pass the canvas frame so edges collinear with it (the antimeridian on a
49071
+ // world map, regional clipExtent cuts) don't get ringed as fake coast —
49072
+ // land runs cleanly to the render-area edge.
49073
+ coastlineOuterRings(layout.regions, cs.minExtent, {
49074
+ w: width,
49075
+ h: height
49076
+ }),
48796
49077
  cs,
48797
49078
  layout.background
48798
49079
  );
@@ -48806,7 +49087,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48806
49087
  gWater.append("path").attr("d", ds.join(" ")).attr("stroke", stroke2).attr("stroke-width", 0.5).attr("stroke-linejoin", "round");
48807
49088
  }
48808
49089
  if (layout.rivers.length) {
48809
- const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none");
49090
+ const gRivers = svg.append("g").attr("class", "dgmo-map-rivers").attr("fill", "none").style("pointer-events", "none");
48810
49091
  for (const r of layout.rivers) {
48811
49092
  gRivers.append("path").attr("d", r.d).attr("stroke", r.color).attr("stroke-width", r.width).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
48812
49093
  }
@@ -48847,7 +49128,7 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48847
49128
  const d = box.points.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join("") + "Z";
48848
49129
  clip.append("path").attr("d", d);
48849
49130
  }
48850
- 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})`);
49131
+ 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})`);
48851
49132
  appendWaterLines(
48852
49133
  gInsetWater,
48853
49134
  coastlineOuterRings(layout.insetRegions, cs.minExtent),
@@ -49006,30 +49287,12 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49006
49287
  if (layout.legend) {
49007
49288
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
49008
49289
  const legendG = svg.append("g").attr("class", "dgmo-map-legend").attr("transform", `translate(0, ${legendY})`);
49009
- const ramp = layout.legend.ramp;
49010
- const scoreGroup = ramp ? {
49011
- name: ramp.metric?.trim() || "Value",
49012
- entries: [],
49013
- gradient: {
49014
- min: ramp.min,
49015
- max: ramp.max,
49016
- hue: ramp.hue,
49017
- base: ramp.base
49018
- }
49019
- } : null;
49020
- const tagGroups = layout.legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
49021
- const groups = [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
49290
+ const groups = mapLegendGroups(layout.legend);
49022
49291
  if (groups.length > 0) {
49023
- const config = {
49292
+ const config = mapLegendConfig(
49024
49293
  groups,
49025
- position: { placement: "top-center", titleRelation: "below-title" },
49026
- mode: exportDims ? "export" : "preview",
49027
- showEmptyGroups: false,
49028
- // Keep inactive siblings visible as pills so the user can click to flip
49029
- // the active colouring dimension (preview only — export shows just the
49030
- // active group).
49031
- showInactivePills: true
49032
- };
49294
+ exportDims ? "export" : "preview"
49295
+ );
49033
49296
  const state = { activeGroup: layout.legend.activeGroup };
49034
49297
  renderLegendD3(legendG, config, state, palette, isDark, void 0, width);
49035
49298
  }
@@ -49073,6 +49336,7 @@ var init_renderer16 = __esm({
49073
49336
  init_title_constants();
49074
49337
  init_color_utils();
49075
49338
  init_legend_d3();
49339
+ init_legend_band();
49076
49340
  init_layout15();
49077
49341
  LABEL_FONT = 11;
49078
49342
  }
@@ -49094,9 +49358,10 @@ function mapContentAspect(resolved, data, ref = REF) {
49094
49358
  const aspect = w / h;
49095
49359
  return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49096
49360
  }
49097
- function mapExportDimensions(resolved, data, baseWidth = 1200) {
49098
- const raw = mapContentAspect(resolved, data);
49099
- const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49361
+ function mapExportDimensions(resolved, data, baseWidth = 1200, aspectOverride) {
49362
+ const useOverride = aspectOverride !== void 0 && Number.isFinite(aspectOverride) && aspectOverride > 0;
49363
+ const raw = useOverride ? aspectOverride : mapContentAspect(resolved, data);
49364
+ const clamped = useOverride ? raw : Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49100
49365
  const width = baseWidth;
49101
49366
  let height = Math.round(width / clamped);
49102
49367
  let chromeReserve = 0;
@@ -49109,7 +49374,7 @@ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49109
49374
  height = Math.round(chromeReserve + MIN_MAP_BAND);
49110
49375
  floored = true;
49111
49376
  }
49112
- const preferContain = clamped !== raw || floored;
49377
+ const preferContain = useOverride ? floored : clamped !== raw || floored;
49113
49378
  return { width, height, preferContain };
49114
49379
  }
49115
49380
  var FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
@@ -54819,7 +55084,6 @@ function renderTimelineTagLegendOverlay(container, parsed, palette, isDark, setu
54819
55084
  function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, setup, hovers, onClickItem, _exportDims, _swimlaneTagGroup, _activeTagGroup, _onTagStateChange, _viewMode) {
54820
55085
  const {
54821
55086
  width,
54822
- height,
54823
55087
  tooltip,
54824
55088
  solid,
54825
55089
  textColor,
@@ -54868,10 +55132,11 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
54868
55132
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
54869
55133
  const eraLabelY = eraReserve ? -(topScaleH + markerReserve + ERA_ROW_H / 2) : 0;
54870
55134
  const innerWidth = width - margin.left - margin.right;
54871
- const innerHeight = height - margin.top - margin.bottom;
54872
- const rowH = Math.min(ctx.structural(28), innerHeight / sorted.length);
55135
+ const rowH = ctx.structural(28);
55136
+ const innerHeight = rowH * sorted.length;
55137
+ const usedHeight = margin.top + innerHeight + margin.bottom;
54873
55138
  const xScale = d3Scale2.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
54874
- 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);
55139
+ 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);
54875
55140
  if (ctx.isBelowFloor) {
54876
55141
  svg.attr("width", "100%");
54877
55142
  }
@@ -57392,7 +57657,12 @@ async function renderForExport(content, theme, palette, viewState, options) {
57392
57657
  }
57393
57658
  }
57394
57659
  const mapResolved = resolveMap2(mapParsed, mapData);
57395
- const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57660
+ const dims2 = mapExportDimensions2(
57661
+ mapResolved,
57662
+ mapData,
57663
+ EXPORT_WIDTH,
57664
+ options?.mapAspect
57665
+ );
57396
57666
  const container2 = createExportContainer(dims2.width, dims2.height);
57397
57667
  renderMapForExport2(
57398
57668
  container2,
@@ -58536,6 +58806,134 @@ function extractInfraCounts(content) {
58536
58806
  return { nodes: parsed.nodes.length };
58537
58807
  }
58538
58808
 
58809
+ // src/utils/svg-embed.ts
58810
+ function normalizeSvgForEmbed(input) {
58811
+ let svg = input;
58812
+ const rootMatch = svg.match(/<svg[^>]*>/);
58813
+ const rootTag = rootMatch?.[0] ?? "";
58814
+ if (rootTag && !rootTag.includes("viewBox")) {
58815
+ const wh = rootTag.match(/width="(\d+)"[^>]*height="(\d+)"/);
58816
+ if (wh) {
58817
+ svg = svg.replace(/<svg/, `<svg viewBox="0 0 ${wh[1]} ${wh[2]}"`);
58818
+ }
58819
+ }
58820
+ const tight = computeBBox(svg);
58821
+ if (tight && tight.width > 0 && tight.height > 0) {
58822
+ const pad2 = 16;
58823
+ const vb = `${tight.x - pad2} ${tight.y - pad2} ${tight.width + pad2 * 2} ${tight.height + pad2 * 2}`;
58824
+ svg = svg.replace(/(<svg[^>]*?)viewBox="[^"]*"/, `$1viewBox="${vb}"`);
58825
+ }
58826
+ svg = svg.replace(/(<svg[^>]*?) width="[^"]*"/g, "$1");
58827
+ svg = svg.replace(/(<svg[^>]*?) height="[^"]*"/g, "$1");
58828
+ svg = svg.replace(/(<svg[^>]*?style="[^"]*?)background:[^;"]*;?\s*/g, "$1");
58829
+ svg = svg.replace(/<svg\s{2,}/g, "<svg ");
58830
+ return svg;
58831
+ }
58832
+ function getEmbedSvgViewBox(svg) {
58833
+ const tight = computeBBox(svg);
58834
+ if (!tight || tight.width <= 0 || tight.height <= 0) return null;
58835
+ const pad2 = 16;
58836
+ return {
58837
+ x: tight.x - pad2,
58838
+ y: tight.y - pad2,
58839
+ width: tight.width + pad2 * 2,
58840
+ height: tight.height + pad2 * 2
58841
+ };
58842
+ }
58843
+ function computeBBox(svg) {
58844
+ const xs = [];
58845
+ const ys = [];
58846
+ function push(x, y) {
58847
+ if (Number.isFinite(x) && Number.isFinite(y)) {
58848
+ xs.push(x);
58849
+ ys.push(y);
58850
+ }
58851
+ }
58852
+ function attr(tag, name) {
58853
+ const m = tag.match(new RegExp(`\\b${name}="([^"]*)"`));
58854
+ if (!m) return null;
58855
+ const n = parseFloat(m[1]);
58856
+ return Number.isFinite(n) ? n : null;
58857
+ }
58858
+ for (const m of svg.matchAll(/<rect\b[^>]*?\/?>/g)) {
58859
+ const tag = m[0];
58860
+ const x = attr(tag, "x");
58861
+ const y = attr(tag, "y");
58862
+ const w = attr(tag, "width");
58863
+ const h = attr(tag, "height");
58864
+ if (x !== null && y !== null && w !== null && h !== null) {
58865
+ push(x, y);
58866
+ push(x + w, y + h);
58867
+ }
58868
+ }
58869
+ for (const m of svg.matchAll(/<line\b[^>]*?\/?>/g)) {
58870
+ const tag = m[0];
58871
+ const x1 = attr(tag, "x1");
58872
+ const y1 = attr(tag, "y1");
58873
+ const x2 = attr(tag, "x2");
58874
+ const y2 = attr(tag, "y2");
58875
+ if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
58876
+ push(x1, y1);
58877
+ push(x2, y2);
58878
+ }
58879
+ }
58880
+ for (const m of svg.matchAll(/<circle\b[^>]*?\/?>/g)) {
58881
+ const tag = m[0];
58882
+ const cx = attr(tag, "cx");
58883
+ const cy = attr(tag, "cy");
58884
+ const r = attr(tag, "r");
58885
+ if (cx !== null && cy !== null && r !== null) {
58886
+ push(cx - r, cy - r);
58887
+ push(cx + r, cy + r);
58888
+ }
58889
+ }
58890
+ for (const m of svg.matchAll(/<ellipse\b[^>]*?\/?>/g)) {
58891
+ const tag = m[0];
58892
+ const cx = attr(tag, "cx");
58893
+ const cy = attr(tag, "cy");
58894
+ const rx = attr(tag, "rx");
58895
+ const ry = attr(tag, "ry");
58896
+ if (cx !== null && cy !== null && rx !== null && ry !== null) {
58897
+ push(cx - rx, cy - ry);
58898
+ push(cx + rx, cy + ry);
58899
+ }
58900
+ }
58901
+ for (const m of svg.matchAll(/<text\b([^>]*?)>([\s\S]*?)<\/text>/g)) {
58902
+ const tag = `<text${m[1]}>`;
58903
+ const text = m[2].replace(/<[^>]+>/g, "");
58904
+ const x = attr(tag, "x");
58905
+ const y = attr(tag, "y");
58906
+ if (x !== null && y !== null) {
58907
+ const w = text.length * 7;
58908
+ push(x - w / 2, y - 14);
58909
+ push(x + w / 2, y + 4);
58910
+ }
58911
+ }
58912
+ for (const m of svg.matchAll(/<path\b[^>]*?\bd="([^"]+)"/g)) {
58913
+ const d = m[1];
58914
+ const nums = d.match(/-?\d+(?:\.\d+)?/g);
58915
+ if (!nums) continue;
58916
+ for (let i = 0; i + 1 < nums.length; i += 2) {
58917
+ push(parseFloat(nums[i]), parseFloat(nums[i + 1]));
58918
+ }
58919
+ }
58920
+ for (const m of svg.matchAll(
58921
+ /<(?:polygon|polyline)\b[^>]*?\bpoints="([^"]+)"/g
58922
+ )) {
58923
+ const nums = m[1].match(/-?\d+(?:\.\d+)?/g);
58924
+ if (!nums) continue;
58925
+ for (let i = 0; i + 1 < nums.length; i += 2) {
58926
+ push(parseFloat(nums[i]), parseFloat(nums[i + 1]));
58927
+ }
58928
+ }
58929
+ if (xs.length === 0 || ys.length === 0) return null;
58930
+ const minX = Math.min(...xs);
58931
+ const maxX = Math.max(...xs);
58932
+ const minY = Math.min(...ys);
58933
+ const maxY = Math.max(...ys);
58934
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
58935
+ }
58936
+
58539
58937
  // src/map/completion.ts
58540
58938
  var fold2 = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
58541
58939
  var groupThousands = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -58659,8 +59057,10 @@ export {
58659
59057
  decodeDiagramUrl2 as decodeDiagramUrl,
58660
59058
  encodeDiagramUrl2 as encodeDiagramUrl,
58661
59059
  formatDgmoError,
59060
+ getEmbedSvgViewBox,
58662
59061
  getMinDimensions,
58663
59062
  getPalette,
59063
+ normalizeSvgForEmbed,
58664
59064
  palettes,
58665
59065
  render2 as render,
58666
59066
  themes,