@diagrammo/dgmo 0.23.0 → 0.25.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.
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 Diagrammo
3
+ Copyright (c) 2026 Demian Neidetcher
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/dist/advanced.cjs CHANGED
@@ -840,7 +840,7 @@ function withTagAliases(base, aliases) {
840
840
  function isReservedKey(registry, key) {
841
841
  return registry.keys.has(key) || registry.tagAliases.has(key);
842
842
  }
843
- var SEQUENCE_REGISTRY, INFRA_REGISTRY, MAP_REGISTRY, ORG_REGISTRY, C4_REGISTRY, ER_REGISTRY, CLASS_REGISTRY, KANBAN_REGISTRY, SITEMAP_REGISTRY, GANTT_REGISTRY, PERT_REGISTRY, BOXES_AND_LINES_REGISTRY, TIMELINE_REGISTRY, MINDMAP_REGISTRY, TECH_RADAR_REGISTRY, CYCLE_REGISTRY, JOURNEY_MAP_REGISTRY, PYRAMID_REGISTRY, RING_REGISTRY, RACI_REGISTRY, WIREFRAME_REGISTRY;
843
+ var SEQUENCE_REGISTRY, INFRA_REGISTRY, MAP_REGISTRY, ORG_REGISTRY, C4_REGISTRY, ER_REGISTRY, KANBAN_REGISTRY, SITEMAP_REGISTRY, GANTT_REGISTRY, PERT_REGISTRY, BOXES_AND_LINES_REGISTRY, TIMELINE_REGISTRY, MINDMAP_REGISTRY, TECH_RADAR_REGISTRY, CYCLE_REGISTRY, JOURNEY_MAP_REGISTRY, PYRAMID_REGISTRY, RING_REGISTRY, RACI_REGISTRY;
844
844
  var init_reserved_key_registry = __esm({
845
845
  "src/utils/reserved-key-registry.ts"() {
846
846
  "use strict";
@@ -884,10 +884,6 @@ var init_reserved_key_registry = __esm({
884
884
  "description",
885
885
  "domain"
886
886
  ]);
887
- CLASS_REGISTRY = staticRegistry([
888
- "color",
889
- "description"
890
- ]);
891
887
  KANBAN_REGISTRY = staticRegistry([
892
888
  "color",
893
889
  "description",
@@ -963,7 +959,6 @@ var init_reserved_key_registry = __esm({
963
959
  "color",
964
960
  "description"
965
961
  ]);
966
- WIREFRAME_REGISTRY = staticRegistry([]);
967
962
  }
968
963
  });
969
964
 
@@ -4236,6 +4231,9 @@ var init_legend_layout = __esm({
4236
4231
  });
4237
4232
 
4238
4233
  // src/utils/legend-d3.ts
4234
+ function centerText(sel) {
4235
+ return sel.attr("dy", LEGEND_TEXT_DY);
4236
+ }
4239
4237
  function renderLegendD3(container, config, state, palette, isDark, callbacks, containerWidth) {
4240
4238
  const width = containerWidth ?? parseFloat(container.attr("width") || "800");
4241
4239
  let currentState = { ...state };
@@ -4318,21 +4316,21 @@ function renderCapsule(parent, capsule, palette, groupBg, pillBorder, _isDark, c
4318
4316
  const pill = capsule.pill;
4319
4317
  g.append("rect").attr("x", pill.x).attr("y", pill.y).attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", palette.bg);
4320
4318
  g.append("rect").attr("x", pill.x).attr("y", pill.y).attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", "none").attr("stroke", pillBorder).attr("stroke-width", 0.75);
4321
- g.append("text").attr("x", pill.x + pill.width / 2).attr("y", LEGEND_HEIGHT / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.text).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(capsule.groupName);
4319
+ g.append("text").attr("x", pill.x + pill.width / 2).attr("y", LEGEND_HEIGHT / 2).attr("text-anchor", "middle").call(centerText).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.text).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(capsule.groupName);
4322
4320
  if (capsule.gradient) {
4323
4321
  const gr = capsule.gradient;
4324
4322
  const gradId = `dgmo-legend-ramp-${capsule.groupName.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
4325
4323
  const def = g.append("defs").append("linearGradient").attr("id", gradId);
4326
4324
  def.append("stop").attr("offset", "0%").attr("stop-color", mix(gr.hue, gr.base, 15));
4327
4325
  def.append("stop").attr("offset", "100%").attr("stop-color", gr.hue);
4328
- g.append("text").attr("x", gr.minX).attr("y", gr.textY).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(gr.minText);
4326
+ g.append("text").attr("x", gr.minX).attr("y", gr.textY).call(centerText).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(gr.minText);
4329
4327
  g.append("rect").attr("class", "dgmo-legend-gradient-ramp").attr("data-ramp-min", gr.min).attr("data-ramp-max", gr.max).attr("x", gr.rampX).attr("y", gr.rampY).attr("width", gr.rampW).attr("height", gr.rampH).attr("rx", 2).attr("fill", `url(#${gradId})`);
4330
- g.append("text").attr("x", gr.maxX).attr("y", gr.textY).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(gr.maxText);
4328
+ g.append("text").attr("x", gr.maxX).attr("y", gr.textY).call(centerText).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(gr.maxText);
4331
4329
  }
4332
4330
  for (const entry of capsule.entries) {
4333
4331
  const entryG = g.append("g").attr("data-legend-entry", entry.value.toLowerCase()).attr("data-series-name", entry.value).style("cursor", "pointer");
4334
4332
  entryG.append("circle").attr("cx", entry.dotCx).attr("cy", entry.dotCy).attr("r", LEGEND_DOT_R).attr("fill", entry.color);
4335
- entryG.append("text").attr("x", entry.textX).attr("y", entry.textY).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(entry.displayValue ?? entry.value);
4333
+ entryG.append("text").attr("x", entry.textX).attr("y", entry.textY).call(centerText).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("font-family", FONT_FAMILY).text(entry.displayValue ?? entry.value);
4336
4334
  if (callbacks?.onEntryHover) {
4337
4335
  const groupName = capsule.groupName;
4338
4336
  const entryValue = entry.value;
@@ -4352,7 +4350,7 @@ function renderCapsule(parent, capsule, palette, groupBg, pillBorder, _isDark, c
4352
4350
  function renderPill(parent, pill, palette, groupBg, callbacks) {
4353
4351
  const g = parent.append("g").attr("transform", `translate(${pill.x},${pill.y})`).attr("data-legend-group", pill.groupName.toLowerCase()).style("cursor", "pointer");
4354
4352
  g.append("rect").attr("width", pill.width).attr("height", pill.height).attr("rx", pill.height / 2).attr("fill", groupBg);
4355
- g.append("text").attr("x", pill.width / 2).attr("y", pill.height / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(pill.groupName);
4353
+ g.append("text").attr("x", pill.width / 2).attr("y", pill.height / 2).attr("text-anchor", "middle").call(centerText).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(pill.groupName);
4356
4354
  if (callbacks?.onGroupToggle) {
4357
4355
  const cb = callbacks.onGroupToggle;
4358
4356
  const name = pill.groupName;
@@ -4375,7 +4373,7 @@ function renderControl(parent, ctrl, palette, _groupBg, pillBorder, _isDark, con
4375
4373
  textX = 8 + 14 + LEGEND_ENTRY_DOT_GAP + measureLegendText(ctrl.label, LEGEND_PILL_FONT_SIZE) / 2;
4376
4374
  }
4377
4375
  if (ctrl.label) {
4378
- g.append("text").attr("x", textX).attr("y", ctrl.height / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(ctrl.label);
4376
+ g.append("text").attr("x", textX).attr("y", ctrl.height / 2).attr("text-anchor", "middle").call(centerText).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", 500).attr("fill", palette.textMuted).attr("pointer-events", "none").attr("font-family", FONT_FAMILY).text(ctrl.label);
4379
4377
  }
4380
4378
  if (ctrl.children) {
4381
4379
  let cx = ctrl.width + 4;
@@ -4385,7 +4383,7 @@ function renderControl(parent, ctrl, palette, _groupBg, pillBorder, _isDark, con
4385
4383
  "fill",
4386
4384
  child.isActive ? palette.primary ?? palette.text : "none"
4387
4385
  ).attr("stroke", pillBorder).attr("stroke-width", 0.75);
4388
- childG.append("text").attr("x", child.width / 2).attr("y", ctrl.height / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", child.isActive ? palette.bg : palette.textMuted).attr("font-family", FONT_FAMILY).text(child.label);
4386
+ childG.append("text").attr("x", child.width / 2).attr("y", ctrl.height / 2).attr("text-anchor", "middle").call(centerText).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", child.isActive ? palette.bg : palette.textMuted).attr("font-family", FONT_FAMILY).text(child.label);
4389
4387
  const configCtrl2 = configControls?.find((c) => c.id === ctrl.id);
4390
4388
  const configChild = configCtrl2?.children?.find((c) => c.id === child.id);
4391
4389
  if (configChild?.onClick) {
@@ -4439,7 +4437,7 @@ function renderControlsGroup(parent, layout, palette, groupBg, pillBorder, callb
4439
4437
  } else {
4440
4438
  entryG.append("circle").attr("cx", tl.dotCx).attr("cy", tl.dotCy).attr("r", LEGEND_TOGGLE_DOT_R).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1);
4441
4439
  }
4442
- entryG.append("text").attr("x", tl.textX).attr("y", tl.textY).attr("dominant-baseline", "central").attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("opacity", tl.active ? 1 : LEGEND_TOGGLE_OFF_OPACITY).attr("font-family", FONT_FAMILY).text(tl.label);
4440
+ entryG.append("text").attr("x", tl.textX).attr("y", tl.textY).call(centerText).attr("font-size", LEGEND_ENTRY_FONT_SIZE).attr("fill", palette.textMuted).attr("opacity", tl.active ? 1 : LEGEND_TOGGLE_OFF_OPACITY).attr("font-family", FONT_FAMILY).text(tl.label);
4443
4441
  if (callbacks?.onControlsToggle && toggle) {
4444
4442
  const cb = callbacks.onControlsToggle;
4445
4443
  const id = tl.id;
@@ -4452,6 +4450,7 @@ function renderControlsGroup(parent, layout, palette, groupBg, pillBorder, callb
4452
4450
  }
4453
4451
  }
4454
4452
  }
4453
+ var LEGEND_TEXT_DY;
4455
4454
  var init_legend_d3 = __esm({
4456
4455
  "src/utils/legend-d3.ts"() {
4457
4456
  "use strict";
@@ -4459,6 +4458,7 @@ var init_legend_d3 = __esm({
4459
4458
  init_legend_layout();
4460
4459
  init_color_utils();
4461
4460
  init_fonts();
4461
+ LEGEND_TEXT_DY = "0.32em";
4462
4462
  }
4463
4463
  });
4464
4464
 
@@ -4644,7 +4644,6 @@ var init_name_normalize = __esm({
4644
4644
  var parser_exports = {};
4645
4645
  __export(parser_exports, {
4646
4646
  isSequenceBlock: () => isSequenceBlock,
4647
- isSequenceMessage: () => isSequenceMessage,
4648
4647
  isSequenceNote: () => isSequenceNote,
4649
4648
  isSequenceSection: () => isSequenceSection,
4650
4649
  looksLikeSequence: () => looksLikeSequence,
@@ -4660,9 +4659,6 @@ function isHardRemovedToken(remainder) {
4660
4659
  }
4661
4660
  return { removed: false };
4662
4661
  }
4663
- function isSequenceMessage(el) {
4664
- return el.kind === "message";
4665
- }
4666
4662
  function isSequenceBlock(el) {
4667
4663
  return el.kind === "block";
4668
4664
  }
@@ -26631,7 +26627,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26631
26627
  const VALUE_NAME = hasRamp ? parsed.boxMetric?.trim() || "Value" : null;
26632
26628
  const matchColorGroup = (v) => {
26633
26629
  const lv = v.trim().toLowerCase();
26634
- if (lv === "none") return null;
26630
+ if (lv === "" || lv === "none") return null;
26635
26631
  const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
26636
26632
  if (tg) return tg.name;
26637
26633
  if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
@@ -26972,6 +26968,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26972
26968
  const tooltipText = fullText.length > 200 ? fullText.slice(0, 199) + "\u2026" : fullText;
26973
26969
  nodeG.append("title").text(tooltipText);
26974
26970
  }
26971
+ } else if (parsed.showValues && node.value !== void 0) {
26972
+ const valueLabel = parsed.boxMetric ? `${parsed.boxMetric}: ${node.value}` : String(node.value);
26973
+ const headerH = ln.height / 2;
26974
+ const sepY = -ln.height / 2 + headerH;
26975
+ const fitted = fitLabelToHeader(node.label, ln.width, 2);
26976
+ const labelLineH = fitted.fontSize * 1.3;
26977
+ const labelTotalH = fitted.lines.length * labelLineH;
26978
+ const headerCenterY = -ln.height / 2 + headerH / 2;
26979
+ for (let li = 0; li < fitted.lines.length; li++) {
26980
+ nodeG.append("text").attr("x", 0).attr(
26981
+ "y",
26982
+ headerCenterY - labelTotalH / 2 + labelLineH / 2 + li * labelLineH
26983
+ ).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]);
26984
+ }
26985
+ 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);
26986
+ 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);
26975
26987
  } else {
26976
26988
  const maxLabelLines = Math.max(
26977
26989
  2,
@@ -26984,21 +26996,16 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26984
26996
  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]);
26985
26997
  }
26986
26998
  }
26987
- if (parsed.showValues && node.value !== void 0) {
26999
+ if (parsed.showValues && node.value !== void 0 && desc && desc.length > 0 && !hideDescriptions) {
26988
27000
  const valueText = String(node.value);
26989
- const descShown = !!(desc && desc.length > 0 && !hideDescriptions);
26990
- if (descShown) {
26991
- const padX = 6;
26992
- const padY = 5;
26993
- const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
26994
- const bh = VALUE_FONT_SIZE + 4;
26995
- const bx = ln.width / 2 - bw - 4;
26996
- const by = -ln.height / 2 + 4;
26997
- 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);
26998
- 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);
26999
- } else {
27000
- nodeG.append("text").attr("class", "bl-node-value").attr("x", 0).attr("y", ln.height / 2 - VALUE_FONT_SIZE).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", VALUE_FONT_SIZE).attr("font-weight", "600").attr("fill", colors.text).attr("opacity", 0.8).text(valueText);
27001
- }
27001
+ const padX = 6;
27002
+ const padY = 5;
27003
+ const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
27004
+ const bh = VALUE_FONT_SIZE + 4;
27005
+ const bx = Math.max(-ln.width / 2 + 4, ln.width / 2 - bw - 4);
27006
+ const by = -ln.height / 2 + 4;
27007
+ 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);
27008
+ 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);
27002
27009
  }
27003
27010
  }
27004
27011
  const hasDescriptions = parsed.nodes.some(
@@ -27094,7 +27101,7 @@ var init_renderer6 = __esm({
27094
27101
  init_wrapped_desc();
27095
27102
  init_scaling();
27096
27103
  DIAGRAM_PADDING6 = 20;
27097
- NODE_FONT_SIZE = 13;
27104
+ NODE_FONT_SIZE = 11;
27098
27105
  MIN_NODE_FONT_SIZE = 9;
27099
27106
  EDGE_LABEL_FONT_SIZE4 = 11;
27100
27107
  EDGE_STROKE_WIDTH5 = 1.5;
@@ -47054,7 +47061,11 @@ function resolveMap(parsed, data) {
47054
47061
  if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
47055
47062
  }
47056
47063
  const containerUnion = unionExtent(containerBoxes, points);
47057
- if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
47064
+ if (containerUnion)
47065
+ extent2 = pad(
47066
+ clampContainerToCluster(containerUnion, points),
47067
+ PAD_FRACTION
47068
+ );
47058
47069
  }
47059
47070
  if (isPoiOnly) {
47060
47071
  const cx = (extent2[0][0] + extent2[1][0]) / 2;
@@ -47135,6 +47146,22 @@ function mostCommonCountry(regions, poiCountries) {
47135
47146
  }
47136
47147
  return best;
47137
47148
  }
47149
+ function clampContainerToCluster(container, points) {
47150
+ const poi = unionExtent([], points);
47151
+ if (!poi) return container;
47152
+ let [[west, south], [east, north]] = container;
47153
+ const [[pWest, pSouth], [pEast, pNorth]] = poi;
47154
+ south = Math.max(south, pSouth - CONTAINER_OVERSHOOT_DEG);
47155
+ north = Math.min(north, pNorth + CONTAINER_OVERSHOOT_DEG);
47156
+ if (east <= 180 && pEast <= 180) {
47157
+ west = Math.max(west, pWest - CONTAINER_OVERSHOOT_DEG);
47158
+ east = Math.min(east, pEast + CONTAINER_OVERSHOOT_DEG);
47159
+ }
47160
+ return [
47161
+ [west, south],
47162
+ [east, north]
47163
+ ];
47164
+ }
47138
47165
  function pad(e, frac) {
47139
47166
  const dLon = (e[1][0] - e[0][0]) * frac || 1;
47140
47167
  const dLat = (e[1][1] - e[0][1]) * frac || 1;
@@ -47147,7 +47174,7 @@ function firstError(diags) {
47147
47174
  const e = diags.find((d) => d.severity === "error");
47148
47175
  return e ? formatDgmoError(e) : null;
47149
47176
  }
47150
- 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;
47177
+ 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;
47151
47178
  var init_resolver2 = __esm({
47152
47179
  "src/map/resolver.ts"() {
47153
47180
  "use strict";
@@ -47160,6 +47187,7 @@ var init_resolver2 = __esm({
47160
47187
  WORLD_LAT_SOUTH = -58;
47161
47188
  WORLD_LAT_NORTH = 78;
47162
47189
  POI_ZOOM_FLOOR_DEG = 7;
47190
+ CONTAINER_OVERSHOOT_DEG = 8;
47163
47191
  US_NATIONAL_LON_SPAN = 48;
47164
47192
  REGION_ALIASES = {
47165
47193
  // Common everyday names → the Natural-Earth display name actually shipped.
@@ -47238,6 +47266,55 @@ var init_resolver2 = __esm({
47238
47266
  }
47239
47267
  });
47240
47268
 
47269
+ // src/map/legend-band.ts
47270
+ function mapLegendGroups(legend) {
47271
+ const ramp = legend.ramp;
47272
+ const scoreGroup = ramp ? {
47273
+ name: ramp.metric?.trim() || "Value",
47274
+ entries: [],
47275
+ gradient: {
47276
+ min: ramp.min,
47277
+ max: ramp.max,
47278
+ hue: ramp.hue,
47279
+ base: ramp.base
47280
+ }
47281
+ } : null;
47282
+ const tagGroups = legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
47283
+ return [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
47284
+ }
47285
+ function mapLegendConfig(groups, mode) {
47286
+ return {
47287
+ groups,
47288
+ position: { placement: "top-center", titleRelation: "below-title" },
47289
+ mode,
47290
+ showEmptyGroups: false,
47291
+ showInactivePills: true
47292
+ };
47293
+ }
47294
+ function mapLegendTop(hasTitle, hasSubtitle) {
47295
+ return (hasTitle ? TITLE_Y + TITLE_FONT_SIZE : 0) + (hasSubtitle ? TITLE_FONT_SIZE : 0) + LEGEND_TOP_GAP2;
47296
+ }
47297
+ function mapLegendBand(legend, opts) {
47298
+ if (!legend) return 0;
47299
+ const groups = mapLegendGroups(legend);
47300
+ if (groups.length === 0) return 0;
47301
+ const config = mapLegendConfig(groups, opts.mode);
47302
+ const state = { activeGroup: legend.activeGroup };
47303
+ const { height } = computeLegendLayout(config, state, opts.width);
47304
+ if (height <= 0) return 0;
47305
+ return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
47306
+ }
47307
+ var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
47308
+ var init_legend_band = __esm({
47309
+ "src/map/legend-band.ts"() {
47310
+ "use strict";
47311
+ init_legend_layout();
47312
+ init_title_constants();
47313
+ LEGEND_TOP_GAP2 = 8;
47314
+ LEGEND_BOTTOM_GAP2 = 10;
47315
+ }
47316
+ });
47317
+
47241
47318
  // src/map/colorize.ts
47242
47319
  function assignColors(isos, adjacency) {
47243
47320
  const sorted = [...isos].sort();
@@ -47818,12 +47895,43 @@ function layoutMap(resolved, data, size, opts) {
47818
47895
  return tagFill(r.tags, activeGroup) ?? neutralFill;
47819
47896
  };
47820
47897
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
47898
+ let legend = null;
47899
+ if (!resolved.directives.noLegend) {
47900
+ const legendTagGroups = resolved.tagGroups.map((g) => ({
47901
+ name: g.name,
47902
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
47903
+ }));
47904
+ if (legendTagGroups.length > 0 || hasRamp) {
47905
+ legend = {
47906
+ tagGroups: legendTagGroups,
47907
+ activeGroup,
47908
+ ...hasRamp && {
47909
+ ramp: {
47910
+ ...resolved.directives.regionMetric !== void 0 && {
47911
+ metric: resolved.directives.regionMetric
47912
+ },
47913
+ min: rampMin,
47914
+ max: rampMax,
47915
+ hue: rampHue,
47916
+ base: rampBase
47917
+ }
47918
+ }
47919
+ };
47920
+ }
47921
+ }
47821
47922
  const TITLE_GAP2 = 16;
47822
47923
  let topPad = FIT_PAD;
47823
47924
  if (resolved.title && resolved.pois.length > 0) {
47824
47925
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
47825
47926
  topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
47826
47927
  }
47928
+ const legendBand = mapLegendBand(legend, {
47929
+ width,
47930
+ mode: opts.legendMode ?? "preview",
47931
+ hasTitle: Boolean(resolved.title),
47932
+ hasSubtitle: Boolean(resolved.subtitle)
47933
+ });
47934
+ if (legendBand > topPad) topPad = legendBand;
47827
47935
  const fitBox = [
47828
47936
  [FIT_PAD, topPad],
47829
47937
  [
@@ -47841,7 +47949,7 @@ function layoutMap(resolved, data, size, opts) {
47841
47949
  const by0 = cb[0][1];
47842
47950
  const cw = cb[1][0] - bx0;
47843
47951
  const ch = cb[1][1] - by0;
47844
- const topReserve = resolved.title && resolved.pois.length > 0 ? topPad : 0;
47952
+ const topReserve = resolved.title && resolved.pois.length > 0 || legendBand > 0 ? topPad : 0;
47845
47953
  const ox = 0;
47846
47954
  const oy = topReserve;
47847
47955
  const sx = cw > 0 ? width / cw : 1;
@@ -48905,30 +49013,6 @@ function layoutMap(resolved, data, size, opts) {
48905
49013
  });
48906
49014
  labels.push(...contextLabels);
48907
49015
  }
48908
- let legend = null;
48909
- if (!resolved.directives.noLegend) {
48910
- const tagGroups = resolved.tagGroups.map((g) => ({
48911
- name: g.name,
48912
- entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
48913
- }));
48914
- if (tagGroups.length > 0 || hasRamp) {
48915
- legend = {
48916
- tagGroups,
48917
- activeGroup,
48918
- ...hasRamp && {
48919
- ramp: {
48920
- ...resolved.directives.regionMetric !== void 0 && {
48921
- metric: resolved.directives.regionMetric
48922
- },
48923
- min: rampMin,
48924
- max: rampMax,
48925
- hue: rampHue,
48926
- base: rampBase
48927
- }
48928
- }
48929
- };
48930
- }
48931
- }
48932
49016
  return {
48933
49017
  width,
48934
49018
  height,
@@ -48966,6 +49050,7 @@ var init_layout15 = __esm({
48966
49050
  init_label_layout();
48967
49051
  init_legend_constants();
48968
49052
  init_title_constants();
49053
+ init_legend_band();
48969
49054
  init_context_labels();
48970
49055
  FIT_PAD = 24;
48971
49056
  RAMP_FLOOR2 = 15;
@@ -49165,6 +49250,9 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49165
49250
  // stretch-distorting. The in-app preview pane passes no exportDims → unset →
49166
49251
  // keeps the global stretch-fill.
49167
49252
  preferContain: exportDims?.preferContain ?? false,
49253
+ // Reserve the legend band for the mode actually drawn below (export shows
49254
+ // only the active group; preview keeps the inactive pills).
49255
+ legendMode: exportDims ? "export" : "preview",
49168
49256
  ...activeGroupOverride !== void 0 && {
49169
49257
  activeGroup: activeGroupOverride
49170
49258
  }
@@ -49456,30 +49544,12 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49456
49544
  if (layout.legend) {
49457
49545
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
49458
49546
  const legendG = svg.append("g").attr("class", "dgmo-map-legend").attr("transform", `translate(0, ${legendY})`);
49459
- const ramp = layout.legend.ramp;
49460
- const scoreGroup = ramp ? {
49461
- name: ramp.metric?.trim() || "Value",
49462
- entries: [],
49463
- gradient: {
49464
- min: ramp.min,
49465
- max: ramp.max,
49466
- hue: ramp.hue,
49467
- base: ramp.base
49468
- }
49469
- } : null;
49470
- const tagGroups = layout.legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
49471
- const groups = [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
49547
+ const groups = mapLegendGroups(layout.legend);
49472
49548
  if (groups.length > 0) {
49473
- const config = {
49549
+ const config = mapLegendConfig(
49474
49550
  groups,
49475
- position: { placement: "top-center", titleRelation: "below-title" },
49476
- mode: exportDims ? "export" : "preview",
49477
- showEmptyGroups: false,
49478
- // Keep inactive siblings visible as pills so the user can click to flip
49479
- // the active colouring dimension (preview only — export shows just the
49480
- // active group).
49481
- showInactivePills: true
49482
- };
49551
+ exportDims ? "export" : "preview"
49552
+ );
49483
49553
  const state = { activeGroup: layout.legend.activeGroup };
49484
49554
  renderLegendD3(legendG, config, state, palette, isDark, void 0, width);
49485
49555
  }
@@ -49524,6 +49594,7 @@ var init_renderer16 = __esm({
49524
49594
  init_title_constants();
49525
49595
  init_color_utils();
49526
49596
  init_legend_d3();
49597
+ init_legend_band();
49527
49598
  init_layout15();
49528
49599
  LABEL_FONT = 11;
49529
49600
  }
@@ -49544,9 +49615,10 @@ function mapContentAspect(resolved, data, ref = REF) {
49544
49615
  const aspect = w / h;
49545
49616
  return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49546
49617
  }
49547
- function mapExportDimensions(resolved, data, baseWidth = 1200) {
49548
- const raw = mapContentAspect(resolved, data);
49549
- const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49618
+ function mapExportDimensions(resolved, data, baseWidth = 1200, aspectOverride) {
49619
+ const useOverride = aspectOverride !== void 0 && Number.isFinite(aspectOverride) && aspectOverride > 0;
49620
+ const raw = useOverride ? aspectOverride : mapContentAspect(resolved, data);
49621
+ const clamped = useOverride ? raw : Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49550
49622
  const width = baseWidth;
49551
49623
  let height = Math.round(width / clamped);
49552
49624
  let chromeReserve = 0;
@@ -49559,7 +49631,7 @@ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49559
49631
  height = Math.round(chromeReserve + MIN_MAP_BAND);
49560
49632
  floored = true;
49561
49633
  }
49562
- const preferContain = clamped !== raw || floored;
49634
+ const preferContain = useOverride ? floored : clamped !== raw || floored;
49563
49635
  return { width, height, preferContain };
49564
49636
  }
49565
49637
  var import_d3_geo3, FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
@@ -55266,7 +55338,6 @@ function renderTimelineTagLegendOverlay(container, parsed, palette, isDark, setu
55266
55338
  function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, setup, hovers, onClickItem, _exportDims, _swimlaneTagGroup, _activeTagGroup, _onTagStateChange, _viewMode) {
55267
55339
  const {
55268
55340
  width,
55269
- height,
55270
55341
  tooltip,
55271
55342
  solid,
55272
55343
  textColor,
@@ -55315,8 +55386,7 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
55315
55386
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
55316
55387
  const eraLabelY = eraReserve ? -(topScaleH + markerReserve + ERA_ROW_H / 2) : 0;
55317
55388
  const innerWidth = width - margin.left - margin.right;
55318
- const availInnerHeight = height - margin.top - margin.bottom;
55319
- const rowH = Math.min(ctx.structural(28), availInnerHeight / sorted.length);
55389
+ const rowH = ctx.structural(28);
55320
55390
  const innerHeight = rowH * sorted.length;
55321
55391
  const usedHeight = margin.top + innerHeight + margin.bottom;
55322
55392
  const xScale = d3Scale2.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
@@ -57897,7 +57967,12 @@ async function renderForExport(content, theme, palette, viewState, options) {
57897
57967
  }
57898
57968
  }
57899
57969
  const mapResolved = resolveMap2(mapParsed, mapData);
57900
- const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57970
+ const dims2 = mapExportDimensions2(
57971
+ mapResolved,
57972
+ mapData,
57973
+ EXPORT_WIDTH,
57974
+ options?.mapAspect
57975
+ );
57901
57976
  const container2 = createExportContainer(dims2.width, dims2.height);
57902
57977
  renderMapForExport2(
57903
57978
  container2,
@@ -236,6 +236,28 @@ interface ParsedMap {
236
236
  readonly diagnostics: readonly DgmoError[];
237
237
  readonly error: string | null;
238
238
  }
239
+ /** Legend descriptor for a rendered map (a layout-stage output, re-exported from
240
+ * `layout.ts`). It lives here so the `legend-band` helper can consume it without
241
+ * importing `layout` — `layout` already value-imports `mapLegendBand`, so the
242
+ * reverse type import would form a layout↔legend-band cycle. */
243
+ interface MapLayoutLegend {
244
+ readonly tagGroups: ReadonlyArray<{
245
+ name: string;
246
+ entries: ReadonlyArray<{
247
+ value: string;
248
+ color: string;
249
+ }>;
250
+ }>;
251
+ readonly activeGroup: string | null;
252
+ readonly ramp?: {
253
+ metric?: string;
254
+ min: number;
255
+ max: number;
256
+ hue: string;
257
+ /** Low end of the ramp gradient (the land colour the fills blend from). */
258
+ base: string;
259
+ };
260
+ }
239
261
 
240
262
  /** A TopoJSON topology (world-coarse/world-detail keyed by ISO 3166-1 alpha-2;
241
263
  * us-states keyed by ISO 3166-2). Geometry feature `id` is the ISO code;
@@ -1568,6 +1590,7 @@ declare function renderForExport(content: string, theme: 'light' | 'dark' | 'tra
1568
1590
  tagGroup?: string;
1569
1591
  exportMode?: boolean;
1570
1592
  mapData?: MapData;
1593
+ mapAspect?: number;
1571
1594
  }): Promise<string>;
1572
1595
 
1573
1596
  /**
@@ -4839,24 +4862,7 @@ interface PlacedLabel {
4839
4862
  readonly clusterMember?: string;
4840
4863
  readonly lineNumber: number;
4841
4864
  }
4842
- interface MapLayoutLegend {
4843
- readonly tagGroups: ReadonlyArray<{
4844
- name: string;
4845
- entries: ReadonlyArray<{
4846
- value: string;
4847
- color: string;
4848
- }>;
4849
- }>;
4850
- readonly activeGroup: string | null;
4851
- readonly ramp?: {
4852
- metric?: string;
4853
- min: number;
4854
- max: number;
4855
- hue: string;
4856
- /** Low end of the ramp gradient (the land colour the fills blend from). */
4857
- base: string;
4858
- };
4859
- }
4865
+
4860
4866
  /** A drawn river centerline — an open stroked path (no fill). */
4861
4867
  interface MapLayoutRiver {
4862
4868
  readonly d: string;
@@ -4955,6 +4961,10 @@ interface LayoutOptions {
4955
4961
  * canvas away from the content aspect, so the off-aspect canvas doesn't
4956
4962
  * re-distort. The in-app preview pane leaves this unset (keeps stretch-fill). */
4957
4963
  readonly preferContain?: boolean;
4964
+ /** Which legend variant gets drawn — `'export'` shows only the active group,
4965
+ * `'preview'` keeps inactive pills. Used to size the reserved legend band so
4966
+ * the projected land starts below the legend. Defaults to `'preview'`. */
4967
+ readonly legendMode?: LegendMode;
4958
4968
  }
4959
4969
  interface Size {
4960
4970
  readonly width: number;
@@ -5007,7 +5017,13 @@ interface MapExportDimensions {
5007
5017
  readonly height: number;
5008
5018
  readonly preferContain: boolean;
5009
5019
  }
5010
- declare function mapExportDimensions(resolved: ResolvedMap, data: MapData, baseWidth?: number): MapExportDimensions;
5020
+ declare function mapExportDimensions(resolved: ResolvedMap, data: MapData, baseWidth?: number,
5021
+ /** WYSIWYG override (app export): the live preview pane's displayed aspect
5022
+ * (width / height). When provided, the canvas adopts it verbatim and
5023
+ * stretch-fills (no clamp, no contain) so the PNG matches exactly what's on
5024
+ * screen. Omitted by every headless consumer (CLI / MCP / SSG / Obsidian),
5025
+ * which keep the intrinsic-aspect sizing below. */
5026
+ aspectOverride?: number): MapExportDimensions;
5011
5027
 
5012
5028
  /** Nearest gazetteer city to a point: the real haversine distance, plus the
5013
5029
  * canonical name + ISO + (US-only) subdivision for token shaping. `lon`/`lat`