@diagrammo/dgmo 0.23.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.
package/dist/internal.cjs CHANGED
@@ -26631,7 +26631,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26631
26631
  const VALUE_NAME = hasRamp ? parsed.boxMetric?.trim() || "Value" : null;
26632
26632
  const matchColorGroup = (v) => {
26633
26633
  const lv = v.trim().toLowerCase();
26634
- if (lv === "none") return null;
26634
+ if (lv === "" || lv === "none") return null;
26635
26635
  const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
26636
26636
  if (tg) return tg.name;
26637
26637
  if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
@@ -26972,6 +26972,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26972
26972
  const tooltipText = fullText.length > 200 ? fullText.slice(0, 199) + "\u2026" : fullText;
26973
26973
  nodeG.append("title").text(tooltipText);
26974
26974
  }
26975
+ } else if (parsed.showValues && node.value !== void 0) {
26976
+ const valueLabel = parsed.boxMetric ? `${parsed.boxMetric}: ${node.value}` : String(node.value);
26977
+ const headerH = ln.height / 2;
26978
+ const sepY = -ln.height / 2 + headerH;
26979
+ const fitted = fitLabelToHeader(node.label, ln.width, 2);
26980
+ const labelLineH = fitted.fontSize * 1.3;
26981
+ const labelTotalH = fitted.lines.length * labelLineH;
26982
+ const headerCenterY = -ln.height / 2 + headerH / 2;
26983
+ for (let li = 0; li < fitted.lines.length; li++) {
26984
+ nodeG.append("text").attr("x", 0).attr(
26985
+ "y",
26986
+ headerCenterY - labelTotalH / 2 + labelLineH / 2 + li * labelLineH
26987
+ ).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]);
26988
+ }
26989
+ 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);
26990
+ 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
26991
  } else {
26976
26992
  const maxLabelLines = Math.max(
26977
26993
  2,
@@ -26984,21 +27000,16 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26984
27000
  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
27001
  }
26986
27002
  }
26987
- if (parsed.showValues && node.value !== void 0) {
27003
+ if (parsed.showValues && node.value !== void 0 && desc && desc.length > 0 && !hideDescriptions) {
26988
27004
  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
- }
27005
+ const padX = 6;
27006
+ const padY = 5;
27007
+ const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
27008
+ const bh = VALUE_FONT_SIZE + 4;
27009
+ const bx = Math.max(-ln.width / 2 + 4, ln.width / 2 - bw - 4);
27010
+ const by = -ln.height / 2 + 4;
27011
+ 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);
27012
+ 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
27013
  }
27003
27014
  }
27004
27015
  const hasDescriptions = parsed.nodes.some(
@@ -27094,7 +27105,7 @@ var init_renderer6 = __esm({
27094
27105
  init_wrapped_desc();
27095
27106
  init_scaling();
27096
27107
  DIAGRAM_PADDING6 = 20;
27097
- NODE_FONT_SIZE = 13;
27108
+ NODE_FONT_SIZE = 11;
27098
27109
  MIN_NODE_FONT_SIZE = 9;
27099
27110
  EDGE_LABEL_FONT_SIZE4 = 11;
27100
27111
  EDGE_STROKE_WIDTH5 = 1.5;
@@ -47054,7 +47065,11 @@ function resolveMap(parsed, data) {
47054
47065
  if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
47055
47066
  }
47056
47067
  const containerUnion = unionExtent(containerBoxes, points);
47057
- if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
47068
+ if (containerUnion)
47069
+ extent2 = pad(
47070
+ clampContainerToCluster(containerUnion, points),
47071
+ PAD_FRACTION
47072
+ );
47058
47073
  }
47059
47074
  if (isPoiOnly) {
47060
47075
  const cx = (extent2[0][0] + extent2[1][0]) / 2;
@@ -47135,6 +47150,22 @@ function mostCommonCountry(regions, poiCountries) {
47135
47150
  }
47136
47151
  return best;
47137
47152
  }
47153
+ function clampContainerToCluster(container, points) {
47154
+ const poi = unionExtent([], points);
47155
+ if (!poi) return container;
47156
+ let [[west, south], [east, north]] = container;
47157
+ const [[pWest, pSouth], [pEast, pNorth]] = poi;
47158
+ south = Math.max(south, pSouth - CONTAINER_OVERSHOOT_DEG);
47159
+ north = Math.min(north, pNorth + CONTAINER_OVERSHOOT_DEG);
47160
+ if (east <= 180 && pEast <= 180) {
47161
+ west = Math.max(west, pWest - CONTAINER_OVERSHOOT_DEG);
47162
+ east = Math.min(east, pEast + CONTAINER_OVERSHOOT_DEG);
47163
+ }
47164
+ return [
47165
+ [west, south],
47166
+ [east, north]
47167
+ ];
47168
+ }
47138
47169
  function pad(e, frac) {
47139
47170
  const dLon = (e[1][0] - e[0][0]) * frac || 1;
47140
47171
  const dLat = (e[1][1] - e[0][1]) * frac || 1;
@@ -47147,7 +47178,7 @@ function firstError(diags) {
47147
47178
  const e = diags.find((d) => d.severity === "error");
47148
47179
  return e ? formatDgmoError(e) : null;
47149
47180
  }
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;
47181
+ 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
47182
  var init_resolver2 = __esm({
47152
47183
  "src/map/resolver.ts"() {
47153
47184
  "use strict";
@@ -47160,6 +47191,7 @@ var init_resolver2 = __esm({
47160
47191
  WORLD_LAT_SOUTH = -58;
47161
47192
  WORLD_LAT_NORTH = 78;
47162
47193
  POI_ZOOM_FLOOR_DEG = 7;
47194
+ CONTAINER_OVERSHOOT_DEG = 8;
47163
47195
  US_NATIONAL_LON_SPAN = 48;
47164
47196
  REGION_ALIASES = {
47165
47197
  // Common everyday names → the Natural-Earth display name actually shipped.
@@ -47238,6 +47270,55 @@ var init_resolver2 = __esm({
47238
47270
  }
47239
47271
  });
47240
47272
 
47273
+ // src/map/legend-band.ts
47274
+ function mapLegendGroups(legend) {
47275
+ const ramp = legend.ramp;
47276
+ const scoreGroup = ramp ? {
47277
+ name: ramp.metric?.trim() || "Value",
47278
+ entries: [],
47279
+ gradient: {
47280
+ min: ramp.min,
47281
+ max: ramp.max,
47282
+ hue: ramp.hue,
47283
+ base: ramp.base
47284
+ }
47285
+ } : null;
47286
+ const tagGroups = legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
47287
+ return [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
47288
+ }
47289
+ function mapLegendConfig(groups, mode) {
47290
+ return {
47291
+ groups,
47292
+ position: { placement: "top-center", titleRelation: "below-title" },
47293
+ mode,
47294
+ showEmptyGroups: false,
47295
+ showInactivePills: true
47296
+ };
47297
+ }
47298
+ function mapLegendTop(hasTitle, hasSubtitle) {
47299
+ return (hasTitle ? TITLE_Y + TITLE_FONT_SIZE : 0) + (hasSubtitle ? TITLE_FONT_SIZE : 0) + LEGEND_TOP_GAP2;
47300
+ }
47301
+ function mapLegendBand(legend, opts) {
47302
+ if (!legend) return 0;
47303
+ const groups = mapLegendGroups(legend);
47304
+ if (groups.length === 0) return 0;
47305
+ const config = mapLegendConfig(groups, opts.mode);
47306
+ const state = { activeGroup: legend.activeGroup };
47307
+ const { height } = computeLegendLayout(config, state, opts.width);
47308
+ if (height <= 0) return 0;
47309
+ return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
47310
+ }
47311
+ var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
47312
+ var init_legend_band = __esm({
47313
+ "src/map/legend-band.ts"() {
47314
+ "use strict";
47315
+ init_legend_layout();
47316
+ init_title_constants();
47317
+ LEGEND_TOP_GAP2 = 8;
47318
+ LEGEND_BOTTOM_GAP2 = 10;
47319
+ }
47320
+ });
47321
+
47241
47322
  // src/map/colorize.ts
47242
47323
  function assignColors(isos, adjacency) {
47243
47324
  const sorted = [...isos].sort();
@@ -47818,12 +47899,43 @@ function layoutMap(resolved, data, size, opts) {
47818
47899
  return tagFill(r.tags, activeGroup) ?? neutralFill;
47819
47900
  };
47820
47901
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
47902
+ let legend = null;
47903
+ if (!resolved.directives.noLegend) {
47904
+ const legendTagGroups = resolved.tagGroups.map((g) => ({
47905
+ name: g.name,
47906
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
47907
+ }));
47908
+ if (legendTagGroups.length > 0 || hasRamp) {
47909
+ legend = {
47910
+ tagGroups: legendTagGroups,
47911
+ activeGroup,
47912
+ ...hasRamp && {
47913
+ ramp: {
47914
+ ...resolved.directives.regionMetric !== void 0 && {
47915
+ metric: resolved.directives.regionMetric
47916
+ },
47917
+ min: rampMin,
47918
+ max: rampMax,
47919
+ hue: rampHue,
47920
+ base: rampBase
47921
+ }
47922
+ }
47923
+ };
47924
+ }
47925
+ }
47821
47926
  const TITLE_GAP2 = 16;
47822
47927
  let topPad = FIT_PAD;
47823
47928
  if (resolved.title && resolved.pois.length > 0) {
47824
47929
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
47825
47930
  topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
47826
47931
  }
47932
+ const legendBand = mapLegendBand(legend, {
47933
+ width,
47934
+ mode: opts.legendMode ?? "preview",
47935
+ hasTitle: Boolean(resolved.title),
47936
+ hasSubtitle: Boolean(resolved.subtitle)
47937
+ });
47938
+ if (legendBand > topPad) topPad = legendBand;
47827
47939
  const fitBox = [
47828
47940
  [FIT_PAD, topPad],
47829
47941
  [
@@ -47841,7 +47953,7 @@ function layoutMap(resolved, data, size, opts) {
47841
47953
  const by0 = cb[0][1];
47842
47954
  const cw = cb[1][0] - bx0;
47843
47955
  const ch = cb[1][1] - by0;
47844
- const topReserve = resolved.title && resolved.pois.length > 0 ? topPad : 0;
47956
+ const topReserve = resolved.title && resolved.pois.length > 0 || legendBand > 0 ? topPad : 0;
47845
47957
  const ox = 0;
47846
47958
  const oy = topReserve;
47847
47959
  const sx = cw > 0 ? width / cw : 1;
@@ -48905,30 +49017,6 @@ function layoutMap(resolved, data, size, opts) {
48905
49017
  });
48906
49018
  labels.push(...contextLabels);
48907
49019
  }
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
49020
  return {
48933
49021
  width,
48934
49022
  height,
@@ -48966,6 +49054,7 @@ var init_layout15 = __esm({
48966
49054
  init_label_layout();
48967
49055
  init_legend_constants();
48968
49056
  init_title_constants();
49057
+ init_legend_band();
48969
49058
  init_context_labels();
48970
49059
  FIT_PAD = 24;
48971
49060
  RAMP_FLOOR2 = 15;
@@ -49165,6 +49254,9 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49165
49254
  // stretch-distorting. The in-app preview pane passes no exportDims → unset →
49166
49255
  // keeps the global stretch-fill.
49167
49256
  preferContain: exportDims?.preferContain ?? false,
49257
+ // Reserve the legend band for the mode actually drawn below (export shows
49258
+ // only the active group; preview keeps the inactive pills).
49259
+ legendMode: exportDims ? "export" : "preview",
49168
49260
  ...activeGroupOverride !== void 0 && {
49169
49261
  activeGroup: activeGroupOverride
49170
49262
  }
@@ -49456,30 +49548,12 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49456
49548
  if (layout.legend) {
49457
49549
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
49458
49550
  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];
49551
+ const groups = mapLegendGroups(layout.legend);
49472
49552
  if (groups.length > 0) {
49473
- const config = {
49553
+ const config = mapLegendConfig(
49474
49554
  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
- };
49555
+ exportDims ? "export" : "preview"
49556
+ );
49483
49557
  const state = { activeGroup: layout.legend.activeGroup };
49484
49558
  renderLegendD3(legendG, config, state, palette, isDark, void 0, width);
49485
49559
  }
@@ -49524,6 +49598,7 @@ var init_renderer16 = __esm({
49524
49598
  init_title_constants();
49525
49599
  init_color_utils();
49526
49600
  init_legend_d3();
49601
+ init_legend_band();
49527
49602
  init_layout15();
49528
49603
  LABEL_FONT = 11;
49529
49604
  }
@@ -49544,9 +49619,10 @@ function mapContentAspect(resolved, data, ref = REF) {
49544
49619
  const aspect = w / h;
49545
49620
  return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49546
49621
  }
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));
49622
+ function mapExportDimensions(resolved, data, baseWidth = 1200, aspectOverride) {
49623
+ const useOverride = aspectOverride !== void 0 && Number.isFinite(aspectOverride) && aspectOverride > 0;
49624
+ const raw = useOverride ? aspectOverride : mapContentAspect(resolved, data);
49625
+ const clamped = useOverride ? raw : Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49550
49626
  const width = baseWidth;
49551
49627
  let height = Math.round(width / clamped);
49552
49628
  let chromeReserve = 0;
@@ -49559,7 +49635,7 @@ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49559
49635
  height = Math.round(chromeReserve + MIN_MAP_BAND);
49560
49636
  floored = true;
49561
49637
  }
49562
- const preferContain = clamped !== raw || floored;
49638
+ const preferContain = useOverride ? floored : clamped !== raw || floored;
49563
49639
  return { width, height, preferContain };
49564
49640
  }
49565
49641
  var import_d3_geo3, FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
@@ -55266,7 +55342,6 @@ function renderTimelineTagLegendOverlay(container, parsed, palette, isDark, setu
55266
55342
  function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, setup, hovers, onClickItem, _exportDims, _swimlaneTagGroup, _activeTagGroup, _onTagStateChange, _viewMode) {
55267
55343
  const {
55268
55344
  width,
55269
- height,
55270
55345
  tooltip,
55271
55346
  solid,
55272
55347
  textColor,
@@ -55315,8 +55390,7 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
55315
55390
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
55316
55391
  const eraLabelY = eraReserve ? -(topScaleH + markerReserve + ERA_ROW_H / 2) : 0;
55317
55392
  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);
55393
+ const rowH = ctx.structural(28);
55320
55394
  const innerHeight = rowH * sorted.length;
55321
55395
  const usedHeight = margin.top + innerHeight + margin.bottom;
55322
55396
  const xScale = d3Scale2.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
@@ -57897,7 +57971,12 @@ async function renderForExport(content, theme, palette, viewState, options) {
57897
57971
  }
57898
57972
  }
57899
57973
  const mapResolved = resolveMap2(mapParsed, mapData);
57900
- const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57974
+ const dims2 = mapExportDimensions2(
57975
+ mapResolved,
57976
+ mapData,
57977
+ EXPORT_WIDTH,
57978
+ options?.mapAspect
57979
+ );
57901
57980
  const container2 = createExportContainer(dims2.width, dims2.height);
57902
57981
  renderMapForExport2(
57903
57982
  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`
@@ -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`