@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/index.cjs CHANGED
@@ -26567,7 +26567,7 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26567
26567
  const VALUE_NAME = hasRamp ? parsed.boxMetric?.trim() || "Value" : null;
26568
26568
  const matchColorGroup = (v) => {
26569
26569
  const lv = v.trim().toLowerCase();
26570
- if (lv === "none") return null;
26570
+ if (lv === "" || lv === "none") return null;
26571
26571
  const tg = parsed.tagGroups.find((g) => g.name.toLowerCase() === lv);
26572
26572
  if (tg) return tg.name;
26573
26573
  if (lv === VALUE_NAME?.toLowerCase()) return VALUE_NAME;
@@ -26908,6 +26908,22 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26908
26908
  const tooltipText = fullText.length > 200 ? fullText.slice(0, 199) + "\u2026" : fullText;
26909
26909
  nodeG.append("title").text(tooltipText);
26910
26910
  }
26911
+ } else if (parsed.showValues && node.value !== void 0) {
26912
+ const valueLabel = parsed.boxMetric ? `${parsed.boxMetric}: ${node.value}` : String(node.value);
26913
+ const headerH = ln.height / 2;
26914
+ const sepY = -ln.height / 2 + headerH;
26915
+ const fitted = fitLabelToHeader(node.label, ln.width, 2);
26916
+ const labelLineH = fitted.fontSize * 1.3;
26917
+ const labelTotalH = fitted.lines.length * labelLineH;
26918
+ const headerCenterY = -ln.height / 2 + headerH / 2;
26919
+ for (let li = 0; li < fitted.lines.length; li++) {
26920
+ nodeG.append("text").attr("x", 0).attr(
26921
+ "y",
26922
+ headerCenterY - labelTotalH / 2 + labelLineH / 2 + li * labelLineH
26923
+ ).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", fitted.fontSize).attr("font-weight", "600").attr("fill", colors.text).text(fitted.lines[li]);
26924
+ }
26925
+ nodeG.append("line").attr("x1", -ln.width / 2).attr("y1", sepY).attr("x2", ln.width / 2).attr("y2", sepY).attr("stroke", colors.stroke).attr("stroke-opacity", 0.3).attr("stroke-width", 1);
26926
+ nodeG.append("text").attr("class", "bl-node-value").attr("x", 0).attr("y", (sepY + ln.height / 2) / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", VALUE_FONT_SIZE).attr("fill", colors.text).attr("opacity", 0.85).text(valueLabel);
26911
26927
  } else {
26912
26928
  const maxLabelLines = Math.max(
26913
26929
  2,
@@ -26920,21 +26936,16 @@ function renderBoxesAndLines(container, parsed, layout, palette, isDark, options
26920
26936
  nodeG.append("text").attr("x", 0).attr("y", -totalH / 2 + lineH / 2 + li * lineH).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("font-size", fitted.fontSize).attr("font-weight", "600").attr("fill", colors.text).text(fitted.lines[li]);
26921
26937
  }
26922
26938
  }
26923
- if (parsed.showValues && node.value !== void 0) {
26939
+ if (parsed.showValues && node.value !== void 0 && desc && desc.length > 0 && !hideDescriptions) {
26924
26940
  const valueText = String(node.value);
26925
- const descShown = !!(desc && desc.length > 0 && !hideDescriptions);
26926
- if (descShown) {
26927
- const padX = 6;
26928
- const padY = 5;
26929
- const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
26930
- const bh = VALUE_FONT_SIZE + 4;
26931
- const bx = ln.width / 2 - bw - 4;
26932
- const by = -ln.height / 2 + 4;
26933
- 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);
26934
- 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);
26935
- } else {
26936
- 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);
26937
- }
26941
+ const padX = 6;
26942
+ const padY = 5;
26943
+ const bw = valueText.length * VALUE_FONT_SIZE * CHAR_WIDTH_RATIO2 + 8;
26944
+ const bh = VALUE_FONT_SIZE + 4;
26945
+ const bx = Math.max(-ln.width / 2 + 4, ln.width / 2 - bw - 4);
26946
+ const by = -ln.height / 2 + 4;
26947
+ nodeG.append("rect").attr("x", bx).attr("y", by).attr("width", bw).attr("height", bh).attr("rx", 3).attr("fill", palette.bg).attr("opacity", 0.85);
26948
+ nodeG.append("text").attr("class", "bl-node-value").attr("x", bx + bw - padX).attr("y", by + padY).attr("text-anchor", "end").attr("dominant-baseline", "central").attr("font-size", VALUE_FONT_SIZE).attr("font-weight", "600").attr("fill", palette.textMuted).text(valueText);
26938
26949
  }
26939
26950
  }
26940
26951
  const hasDescriptions = parsed.nodes.some(
@@ -27030,7 +27041,7 @@ var init_renderer6 = __esm({
27030
27041
  init_wrapped_desc();
27031
27042
  init_scaling();
27032
27043
  DIAGRAM_PADDING6 = 20;
27033
- NODE_FONT_SIZE = 13;
27044
+ NODE_FONT_SIZE = 11;
27034
27045
  MIN_NODE_FONT_SIZE = 9;
27035
27046
  EDGE_LABEL_FONT_SIZE4 = 11;
27036
27047
  EDGE_STROKE_WIDTH5 = 1.5;
@@ -46767,7 +46778,11 @@ function resolveMap(parsed, data) {
46767
46778
  if (bb && !isWholeSphere(bb)) containerBoxes.push(bb);
46768
46779
  }
46769
46780
  const containerUnion = unionExtent(containerBoxes, points);
46770
- if (containerUnion) extent2 = pad(containerUnion, PAD_FRACTION);
46781
+ if (containerUnion)
46782
+ extent2 = pad(
46783
+ clampContainerToCluster(containerUnion, points),
46784
+ PAD_FRACTION
46785
+ );
46771
46786
  }
46772
46787
  if (isPoiOnly) {
46773
46788
  const cx = (extent2[0][0] + extent2[1][0]) / 2;
@@ -46848,6 +46863,22 @@ function mostCommonCountry(regions, poiCountries) {
46848
46863
  }
46849
46864
  return best;
46850
46865
  }
46866
+ function clampContainerToCluster(container, points) {
46867
+ const poi = unionExtent([], points);
46868
+ if (!poi) return container;
46869
+ let [[west, south], [east, north]] = container;
46870
+ const [[pWest, pSouth], [pEast, pNorth]] = poi;
46871
+ south = Math.max(south, pSouth - CONTAINER_OVERSHOOT_DEG);
46872
+ north = Math.min(north, pNorth + CONTAINER_OVERSHOOT_DEG);
46873
+ if (east <= 180 && pEast <= 180) {
46874
+ west = Math.max(west, pWest - CONTAINER_OVERSHOOT_DEG);
46875
+ east = Math.min(east, pEast + CONTAINER_OVERSHOOT_DEG);
46876
+ }
46877
+ return [
46878
+ [west, south],
46879
+ [east, north]
46880
+ ];
46881
+ }
46851
46882
  function pad(e, frac) {
46852
46883
  const dLon = (e[1][0] - e[0][0]) * frac || 1;
46853
46884
  const dLat = (e[1][1] - e[0][1]) * frac || 1;
@@ -46860,7 +46891,7 @@ function firstError(diags) {
46860
46891
  const e = diags.find((d) => d.severity === "error");
46861
46892
  return e ? formatDgmoError(e) : null;
46862
46893
  }
46863
- var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, REGION_PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, POI_ZOOM_FLOOR_DEG, US_NATIONAL_LON_SPAN, REGION_ALIASES, US_STATE_POSTAL;
46894
+ var WORLD_SPAN, MERCATOR_MAX_LAT, PAD_FRACTION, REGION_PAD_FRACTION, WORLD_LAT_SOUTH, WORLD_LAT_NORTH, POI_ZOOM_FLOOR_DEG, CONTAINER_OVERSHOOT_DEG, US_NATIONAL_LON_SPAN, REGION_ALIASES, US_STATE_POSTAL;
46864
46895
  var init_resolver2 = __esm({
46865
46896
  "src/map/resolver.ts"() {
46866
46897
  "use strict";
@@ -46873,6 +46904,7 @@ var init_resolver2 = __esm({
46873
46904
  WORLD_LAT_SOUTH = -58;
46874
46905
  WORLD_LAT_NORTH = 78;
46875
46906
  POI_ZOOM_FLOOR_DEG = 7;
46907
+ CONTAINER_OVERSHOOT_DEG = 8;
46876
46908
  US_NATIONAL_LON_SPAN = 48;
46877
46909
  REGION_ALIASES = {
46878
46910
  // Common everyday names → the Natural-Earth display name actually shipped.
@@ -46951,6 +46983,55 @@ var init_resolver2 = __esm({
46951
46983
  }
46952
46984
  });
46953
46985
 
46986
+ // src/map/legend-band.ts
46987
+ function mapLegendGroups(legend) {
46988
+ const ramp = legend.ramp;
46989
+ const scoreGroup = ramp ? {
46990
+ name: ramp.metric?.trim() || "Value",
46991
+ entries: [],
46992
+ gradient: {
46993
+ min: ramp.min,
46994
+ max: ramp.max,
46995
+ hue: ramp.hue,
46996
+ base: ramp.base
46997
+ }
46998
+ } : null;
46999
+ const tagGroups = legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
47000
+ return [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
47001
+ }
47002
+ function mapLegendConfig(groups, mode) {
47003
+ return {
47004
+ groups,
47005
+ position: { placement: "top-center", titleRelation: "below-title" },
47006
+ mode,
47007
+ showEmptyGroups: false,
47008
+ showInactivePills: true
47009
+ };
47010
+ }
47011
+ function mapLegendTop(hasTitle, hasSubtitle) {
47012
+ return (hasTitle ? TITLE_Y + TITLE_FONT_SIZE : 0) + (hasSubtitle ? TITLE_FONT_SIZE : 0) + LEGEND_TOP_GAP2;
47013
+ }
47014
+ function mapLegendBand(legend, opts) {
47015
+ if (!legend) return 0;
47016
+ const groups = mapLegendGroups(legend);
47017
+ if (groups.length === 0) return 0;
47018
+ const config = mapLegendConfig(groups, opts.mode);
47019
+ const state = { activeGroup: legend.activeGroup };
47020
+ const { height } = computeLegendLayout(config, state, opts.width);
47021
+ if (height <= 0) return 0;
47022
+ return mapLegendTop(opts.hasTitle, opts.hasSubtitle) + height + LEGEND_BOTTOM_GAP2;
47023
+ }
47024
+ var LEGEND_TOP_GAP2, LEGEND_BOTTOM_GAP2;
47025
+ var init_legend_band = __esm({
47026
+ "src/map/legend-band.ts"() {
47027
+ "use strict";
47028
+ init_legend_layout();
47029
+ init_title_constants();
47030
+ LEGEND_TOP_GAP2 = 8;
47031
+ LEGEND_BOTTOM_GAP2 = 10;
47032
+ }
47033
+ });
47034
+
46954
47035
  // src/map/colorize.ts
46955
47036
  function assignColors(isos, adjacency) {
46956
47037
  const sorted = [...isos].sort();
@@ -47531,12 +47612,43 @@ function layoutMap(resolved, data, size, opts) {
47531
47612
  return tagFill(r.tags, activeGroup) ?? neutralFill;
47532
47613
  };
47533
47614
  const regionById = new Map(resolved.regions.map((r) => [r.iso, r]));
47615
+ let legend = null;
47616
+ if (!resolved.directives.noLegend) {
47617
+ const legendTagGroups = resolved.tagGroups.map((g) => ({
47618
+ name: g.name,
47619
+ entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
47620
+ }));
47621
+ if (legendTagGroups.length > 0 || hasRamp) {
47622
+ legend = {
47623
+ tagGroups: legendTagGroups,
47624
+ activeGroup,
47625
+ ...hasRamp && {
47626
+ ramp: {
47627
+ ...resolved.directives.regionMetric !== void 0 && {
47628
+ metric: resolved.directives.regionMetric
47629
+ },
47630
+ min: rampMin,
47631
+ max: rampMax,
47632
+ hue: rampHue,
47633
+ base: rampBase
47634
+ }
47635
+ }
47636
+ };
47637
+ }
47638
+ }
47534
47639
  const TITLE_GAP2 = 16;
47535
47640
  let topPad = FIT_PAD;
47536
47641
  if (resolved.title && resolved.pois.length > 0) {
47537
47642
  const bannerBottom = (resolved.subtitle ? TITLE_Y + TITLE_FONT_SIZE : TITLE_Y) + TITLE_FONT_SIZE / 2;
47538
47643
  topPad = Math.max(FIT_PAD, bannerBottom + TITLE_GAP2);
47539
47644
  }
47645
+ const legendBand = mapLegendBand(legend, {
47646
+ width,
47647
+ mode: opts.legendMode ?? "preview",
47648
+ hasTitle: Boolean(resolved.title),
47649
+ hasSubtitle: Boolean(resolved.subtitle)
47650
+ });
47651
+ if (legendBand > topPad) topPad = legendBand;
47540
47652
  const fitBox = [
47541
47653
  [FIT_PAD, topPad],
47542
47654
  [
@@ -47554,7 +47666,7 @@ function layoutMap(resolved, data, size, opts) {
47554
47666
  const by0 = cb[0][1];
47555
47667
  const cw = cb[1][0] - bx0;
47556
47668
  const ch = cb[1][1] - by0;
47557
- const topReserve = resolved.title && resolved.pois.length > 0 ? topPad : 0;
47669
+ const topReserve = resolved.title && resolved.pois.length > 0 || legendBand > 0 ? topPad : 0;
47558
47670
  const ox = 0;
47559
47671
  const oy = topReserve;
47560
47672
  const sx = cw > 0 ? width / cw : 1;
@@ -48618,30 +48730,6 @@ function layoutMap(resolved, data, size, opts) {
48618
48730
  });
48619
48731
  labels.push(...contextLabels);
48620
48732
  }
48621
- let legend = null;
48622
- if (!resolved.directives.noLegend) {
48623
- const tagGroups = resolved.tagGroups.map((g) => ({
48624
- name: g.name,
48625
- entries: g.entries.map((e) => ({ value: e.value, color: e.color }))
48626
- }));
48627
- if (tagGroups.length > 0 || hasRamp) {
48628
- legend = {
48629
- tagGroups,
48630
- activeGroup,
48631
- ...hasRamp && {
48632
- ramp: {
48633
- ...resolved.directives.regionMetric !== void 0 && {
48634
- metric: resolved.directives.regionMetric
48635
- },
48636
- min: rampMin,
48637
- max: rampMax,
48638
- hue: rampHue,
48639
- base: rampBase
48640
- }
48641
- }
48642
- };
48643
- }
48644
- }
48645
48733
  return {
48646
48734
  width,
48647
48735
  height,
@@ -48679,6 +48767,7 @@ var init_layout15 = __esm({
48679
48767
  init_label_layout();
48680
48768
  init_legend_constants();
48681
48769
  init_title_constants();
48770
+ init_legend_band();
48682
48771
  init_context_labels();
48683
48772
  FIT_PAD = 24;
48684
48773
  RAMP_FLOOR2 = 15;
@@ -48878,6 +48967,9 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
48878
48967
  // stretch-distorting. The in-app preview pane passes no exportDims → unset →
48879
48968
  // keeps the global stretch-fill.
48880
48969
  preferContain: exportDims?.preferContain ?? false,
48970
+ // Reserve the legend band for the mode actually drawn below (export shows
48971
+ // only the active group; preview keeps the inactive pills).
48972
+ legendMode: exportDims ? "export" : "preview",
48881
48973
  ...activeGroupOverride !== void 0 && {
48882
48974
  activeGroup: activeGroupOverride
48883
48975
  }
@@ -49169,30 +49261,12 @@ function renderMap(container, resolved, data, palette, isDark, onClickItem, expo
49169
49261
  if (layout.legend) {
49170
49262
  const legendY = (layout.title ? TITLE_Y + TITLE_FONT_SIZE : 0) + (layout.subtitle ? TITLE_FONT_SIZE : 0) + 8;
49171
49263
  const legendG = svg.append("g").attr("class", "dgmo-map-legend").attr("transform", `translate(0, ${legendY})`);
49172
- const ramp = layout.legend.ramp;
49173
- const scoreGroup = ramp ? {
49174
- name: ramp.metric?.trim() || "Value",
49175
- entries: [],
49176
- gradient: {
49177
- min: ramp.min,
49178
- max: ramp.max,
49179
- hue: ramp.hue,
49180
- base: ramp.base
49181
- }
49182
- } : null;
49183
- const tagGroups = layout.legend.tagGroups.filter((g) => g.entries.length > 0).map((g) => ({ name: g.name, entries: [...g.entries] }));
49184
- const groups = [...scoreGroup ? [scoreGroup] : [], ...tagGroups];
49264
+ const groups = mapLegendGroups(layout.legend);
49185
49265
  if (groups.length > 0) {
49186
- const config = {
49266
+ const config = mapLegendConfig(
49187
49267
  groups,
49188
- position: { placement: "top-center", titleRelation: "below-title" },
49189
- mode: exportDims ? "export" : "preview",
49190
- showEmptyGroups: false,
49191
- // Keep inactive siblings visible as pills so the user can click to flip
49192
- // the active colouring dimension (preview only — export shows just the
49193
- // active group).
49194
- showInactivePills: true
49195
- };
49268
+ exportDims ? "export" : "preview"
49269
+ );
49196
49270
  const state = { activeGroup: layout.legend.activeGroup };
49197
49271
  renderLegendD3(legendG, config, state, palette, isDark, void 0, width);
49198
49272
  }
@@ -49237,6 +49311,7 @@ var init_renderer16 = __esm({
49237
49311
  init_title_constants();
49238
49312
  init_color_utils();
49239
49313
  init_legend_d3();
49314
+ init_legend_band();
49240
49315
  init_layout15();
49241
49316
  LABEL_FONT = 11;
49242
49317
  }
@@ -49257,9 +49332,10 @@ function mapContentAspect(resolved, data, ref = REF) {
49257
49332
  const aspect = w / h;
49258
49333
  return Number.isFinite(aspect) && aspect > 0 ? aspect : FALLBACK_ASPECT;
49259
49334
  }
49260
- function mapExportDimensions(resolved, data, baseWidth = 1200) {
49261
- const raw = mapContentAspect(resolved, data);
49262
- const clamped = Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49335
+ function mapExportDimensions(resolved, data, baseWidth = 1200, aspectOverride) {
49336
+ const useOverride = aspectOverride !== void 0 && Number.isFinite(aspectOverride) && aspectOverride > 0;
49337
+ const raw = useOverride ? aspectOverride : mapContentAspect(resolved, data);
49338
+ const clamped = useOverride ? raw : Math.max(ASPECT_MIN, Math.min(ASPECT_MAX, raw));
49263
49339
  const width = baseWidth;
49264
49340
  let height = Math.round(width / clamped);
49265
49341
  let chromeReserve = 0;
@@ -49272,7 +49348,7 @@ function mapExportDimensions(resolved, data, baseWidth = 1200) {
49272
49348
  height = Math.round(chromeReserve + MIN_MAP_BAND);
49273
49349
  floored = true;
49274
49350
  }
49275
- const preferContain = clamped !== raw || floored;
49351
+ const preferContain = useOverride ? floored : clamped !== raw || floored;
49276
49352
  return { width, height, preferContain };
49277
49353
  }
49278
49354
  var import_d3_geo3, FIT_PAD2, TITLE_GAP, ASPECT_MAX, ASPECT_MIN, MIN_MAP_BAND, FALLBACK_ASPECT, REF;
@@ -54979,7 +55055,6 @@ function renderTimelineTagLegendOverlay(container, parsed, palette, isDark, setu
54979
55055
  function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, setup, hovers, onClickItem, _exportDims, _swimlaneTagGroup, _activeTagGroup, _onTagStateChange, _viewMode) {
54980
55056
  const {
54981
55057
  width,
54982
- height,
54983
55058
  tooltip,
54984
55059
  solid,
54985
55060
  textColor,
@@ -55028,8 +55103,7 @@ function renderTimelineHorizontalTimeSort(container, parsed, palette, isDark, se
55028
55103
  const markerLabelY = markerReserve ? -(topScaleH + MARKER_ROW_H / 2) : 0;
55029
55104
  const eraLabelY = eraReserve ? -(topScaleH + markerReserve + ERA_ROW_H / 2) : 0;
55030
55105
  const innerWidth = width - margin.left - margin.right;
55031
- const availInnerHeight = height - margin.top - margin.bottom;
55032
- const rowH = Math.min(ctx.structural(28), availInnerHeight / sorted.length);
55106
+ const rowH = ctx.structural(28);
55033
55107
  const innerHeight = rowH * sorted.length;
55034
55108
  const usedHeight = margin.top + innerHeight + margin.bottom;
55035
55109
  const xScale = d3Scale2.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
@@ -57554,7 +57628,12 @@ async function renderForExport(content, theme, palette, viewState, options) {
57554
57628
  }
57555
57629
  }
57556
57630
  const mapResolved = resolveMap2(mapParsed, mapData);
57557
- const dims2 = mapExportDimensions2(mapResolved, mapData, EXPORT_WIDTH);
57631
+ const dims2 = mapExportDimensions2(
57632
+ mapResolved,
57633
+ mapData,
57634
+ EXPORT_WIDTH,
57635
+ options?.mapAspect
57636
+ );
57558
57637
  const container2 = createExportContainer(dims2.width, dims2.height);
57559
57638
  renderMapForExport2(
57560
57639
  container2,
@@ -58347,8 +58426,10 @@ __export(index_exports, {
58347
58426
  decodeDiagramUrl: () => decodeDiagramUrl2,
58348
58427
  encodeDiagramUrl: () => encodeDiagramUrl2,
58349
58428
  formatDgmoError: () => formatDgmoError,
58429
+ getEmbedSvgViewBox: () => getEmbedSvgViewBox,
58350
58430
  getMinDimensions: () => getMinDimensions,
58351
58431
  getPalette: () => getPalette,
58432
+ normalizeSvgForEmbed: () => normalizeSvgForEmbed,
58352
58433
  palettes: () => palettes,
58353
58434
  render: () => render2,
58354
58435
  themes: () => themes,
@@ -58720,6 +58801,134 @@ function extractInfraCounts(content) {
58720
58801
  return { nodes: parsed.nodes.length };
58721
58802
  }
58722
58803
 
58804
+ // src/utils/svg-embed.ts
58805
+ function normalizeSvgForEmbed(input) {
58806
+ let svg = input;
58807
+ const rootMatch = svg.match(/<svg[^>]*>/);
58808
+ const rootTag = rootMatch?.[0] ?? "";
58809
+ if (rootTag && !rootTag.includes("viewBox")) {
58810
+ const wh = rootTag.match(/width="(\d+)"[^>]*height="(\d+)"/);
58811
+ if (wh) {
58812
+ svg = svg.replace(/<svg/, `<svg viewBox="0 0 ${wh[1]} ${wh[2]}"`);
58813
+ }
58814
+ }
58815
+ const tight = computeBBox(svg);
58816
+ if (tight && tight.width > 0 && tight.height > 0) {
58817
+ const pad2 = 16;
58818
+ const vb = `${tight.x - pad2} ${tight.y - pad2} ${tight.width + pad2 * 2} ${tight.height + pad2 * 2}`;
58819
+ svg = svg.replace(/(<svg[^>]*?)viewBox="[^"]*"/, `$1viewBox="${vb}"`);
58820
+ }
58821
+ svg = svg.replace(/(<svg[^>]*?) width="[^"]*"/g, "$1");
58822
+ svg = svg.replace(/(<svg[^>]*?) height="[^"]*"/g, "$1");
58823
+ svg = svg.replace(/(<svg[^>]*?style="[^"]*?)background:[^;"]*;?\s*/g, "$1");
58824
+ svg = svg.replace(/<svg\s{2,}/g, "<svg ");
58825
+ return svg;
58826
+ }
58827
+ function getEmbedSvgViewBox(svg) {
58828
+ const tight = computeBBox(svg);
58829
+ if (!tight || tight.width <= 0 || tight.height <= 0) return null;
58830
+ const pad2 = 16;
58831
+ return {
58832
+ x: tight.x - pad2,
58833
+ y: tight.y - pad2,
58834
+ width: tight.width + pad2 * 2,
58835
+ height: tight.height + pad2 * 2
58836
+ };
58837
+ }
58838
+ function computeBBox(svg) {
58839
+ const xs = [];
58840
+ const ys = [];
58841
+ function push(x, y) {
58842
+ if (Number.isFinite(x) && Number.isFinite(y)) {
58843
+ xs.push(x);
58844
+ ys.push(y);
58845
+ }
58846
+ }
58847
+ function attr(tag, name) {
58848
+ const m = tag.match(new RegExp(`\\b${name}="([^"]*)"`));
58849
+ if (!m) return null;
58850
+ const n = parseFloat(m[1]);
58851
+ return Number.isFinite(n) ? n : null;
58852
+ }
58853
+ for (const m of svg.matchAll(/<rect\b[^>]*?\/?>/g)) {
58854
+ const tag = m[0];
58855
+ const x = attr(tag, "x");
58856
+ const y = attr(tag, "y");
58857
+ const w = attr(tag, "width");
58858
+ const h = attr(tag, "height");
58859
+ if (x !== null && y !== null && w !== null && h !== null) {
58860
+ push(x, y);
58861
+ push(x + w, y + h);
58862
+ }
58863
+ }
58864
+ for (const m of svg.matchAll(/<line\b[^>]*?\/?>/g)) {
58865
+ const tag = m[0];
58866
+ const x1 = attr(tag, "x1");
58867
+ const y1 = attr(tag, "y1");
58868
+ const x2 = attr(tag, "x2");
58869
+ const y2 = attr(tag, "y2");
58870
+ if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
58871
+ push(x1, y1);
58872
+ push(x2, y2);
58873
+ }
58874
+ }
58875
+ for (const m of svg.matchAll(/<circle\b[^>]*?\/?>/g)) {
58876
+ const tag = m[0];
58877
+ const cx = attr(tag, "cx");
58878
+ const cy = attr(tag, "cy");
58879
+ const r = attr(tag, "r");
58880
+ if (cx !== null && cy !== null && r !== null) {
58881
+ push(cx - r, cy - r);
58882
+ push(cx + r, cy + r);
58883
+ }
58884
+ }
58885
+ for (const m of svg.matchAll(/<ellipse\b[^>]*?\/?>/g)) {
58886
+ const tag = m[0];
58887
+ const cx = attr(tag, "cx");
58888
+ const cy = attr(tag, "cy");
58889
+ const rx = attr(tag, "rx");
58890
+ const ry = attr(tag, "ry");
58891
+ if (cx !== null && cy !== null && rx !== null && ry !== null) {
58892
+ push(cx - rx, cy - ry);
58893
+ push(cx + rx, cy + ry);
58894
+ }
58895
+ }
58896
+ for (const m of svg.matchAll(/<text\b([^>]*?)>([\s\S]*?)<\/text>/g)) {
58897
+ const tag = `<text${m[1]}>`;
58898
+ const text = m[2].replace(/<[^>]+>/g, "");
58899
+ const x = attr(tag, "x");
58900
+ const y = attr(tag, "y");
58901
+ if (x !== null && y !== null) {
58902
+ const w = text.length * 7;
58903
+ push(x - w / 2, y - 14);
58904
+ push(x + w / 2, y + 4);
58905
+ }
58906
+ }
58907
+ for (const m of svg.matchAll(/<path\b[^>]*?\bd="([^"]+)"/g)) {
58908
+ const d = m[1];
58909
+ const nums = d.match(/-?\d+(?:\.\d+)?/g);
58910
+ if (!nums) continue;
58911
+ for (let i = 0; i + 1 < nums.length; i += 2) {
58912
+ push(parseFloat(nums[i]), parseFloat(nums[i + 1]));
58913
+ }
58914
+ }
58915
+ for (const m of svg.matchAll(
58916
+ /<(?:polygon|polyline)\b[^>]*?\bpoints="([^"]+)"/g
58917
+ )) {
58918
+ const nums = m[1].match(/-?\d+(?:\.\d+)?/g);
58919
+ if (!nums) continue;
58920
+ for (let i = 0; i + 1 < nums.length; i += 2) {
58921
+ push(parseFloat(nums[i]), parseFloat(nums[i + 1]));
58922
+ }
58923
+ }
58924
+ if (xs.length === 0 || ys.length === 0) return null;
58925
+ const minX = Math.min(...xs);
58926
+ const maxX = Math.max(...xs);
58927
+ const minY = Math.min(...ys);
58928
+ const maxY = Math.max(...ys);
58929
+ return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
58930
+ }
58931
+
58723
58932
  // src/map/completion.ts
58724
58933
  var fold2 = (s) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase().trim();
58725
58934
  var groupThousands = (n) => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
@@ -58844,8 +59053,10 @@ function decodeDiagramUrl2(url) {
58844
59053
  decodeDiagramUrl,
58845
59054
  encodeDiagramUrl,
58846
59055
  formatDgmoError,
59056
+ getEmbedSvgViewBox,
58847
59057
  getMinDimensions,
58848
59058
  getPalette,
59059
+ normalizeSvgForEmbed,
58849
59060
  palettes,
58850
59061
  render,
58851
59062
  themes,
package/dist/index.d.cts CHANGED
@@ -182,6 +182,42 @@ declare function getMinDimensions(content: string): {
182
182
  height: number;
183
183
  };
184
184
 
185
+ /**
186
+ * Make an SVG produced by `@diagrammo/dgmo`'s static `render()` suitable for
187
+ * responsive inline embedding in any host (Obsidian, remark/markdown, web
188
+ * pages):
189
+ *
190
+ * - dgmo renders diagrams inside a fixed export canvas (e.g.
191
+ * `viewBox="0 0 1200 800"`), with content often occupying only a fraction
192
+ * of it. We compute a tight content bounding box from element coordinates
193
+ * and set the root `viewBox` to bbox+padding, so the diagram's intrinsic
194
+ * aspect ratio matches its CONTENT — no dead space above/below or beside it.
195
+ * - Ensure the root `<svg>` has a `viewBox` so it scales responsively.
196
+ * - Strip fixed `width="N"` / `height="N"` so CSS (e.g. `width:100%;
197
+ * height:auto`, or an aspect-ratio derived from the tight viewBox) controls
198
+ * sizing.
199
+ * - Remove any inline `background:` from the root style so the page
200
+ * background shows through.
201
+ *
202
+ * This is intentionally a string transform, not a DOM `getBBox()` step: dgmo
203
+ * can dual-render light/dark SVGs where one is hidden by color-mode CSS, and
204
+ * `getBBox()` returns 0 for the hidden copy. Parsing coordinates from the
205
+ * markup measures both copies reliably and works server-side (Node).
206
+ */
207
+ declare function normalizeSvgForEmbed(input: string): string;
208
+ /**
209
+ * Parse the content bounding box of a normalized embed SVG, if one can be
210
+ * derived. Returns `null` when no usable coordinates are found (e.g. an empty
211
+ * diagram). Useful for hosts that want to set an explicit `aspect-ratio` from
212
+ * the tight viewBox.
213
+ */
214
+ declare function getEmbedSvgViewBox(svg: string): {
215
+ x: number;
216
+ y: number;
217
+ width: number;
218
+ height: number;
219
+ } | null;
220
+
185
221
  /**
186
222
  * A gazetteer city entry: `[lat, lon, iso, pop, name, sub?]`.
187
223
  * - `lat`/`lon` — rounded to 3 decimals.
@@ -336,4 +372,4 @@ interface DecodedDiagramUrl {
336
372
  */
337
373
  declare function decodeDiagramUrl(url: string): DecodedDiagramUrl | null;
338
374
 
339
- export { type CompactViewState, type DecodedDiagramUrl, type DgmoError, type DgmoSeverity, type EncodeDiagramUrlOptions, type Gazetteer, type GazetteerEntry, type MapCompletionOptions, type MapPlaceCompletion, type MapRegionCompletion, type PaletteColors, type PaletteConfig, type RegionName, type RegionNames, type RenderOptions, type RenderResult, type Theme, completeMapPlaces, completeMapRegions, decodeDiagramUrl, encodeDiagramUrl, formatDgmoError, getMinDimensions, getPalette, palettes, render, themes, parseDgmo as validate };
375
+ export { type CompactViewState, type DecodedDiagramUrl, type DgmoError, type DgmoSeverity, type EncodeDiagramUrlOptions, type Gazetteer, type GazetteerEntry, type MapCompletionOptions, type MapPlaceCompletion, type MapRegionCompletion, type PaletteColors, type PaletteConfig, type RegionName, type RegionNames, type RenderOptions, type RenderResult, type Theme, completeMapPlaces, completeMapRegions, decodeDiagramUrl, encodeDiagramUrl, formatDgmoError, getEmbedSvgViewBox, getMinDimensions, getPalette, normalizeSvgForEmbed, palettes, render, themes, parseDgmo as validate };
package/dist/index.d.ts CHANGED
@@ -182,6 +182,42 @@ declare function getMinDimensions(content: string): {
182
182
  height: number;
183
183
  };
184
184
 
185
+ /**
186
+ * Make an SVG produced by `@diagrammo/dgmo`'s static `render()` suitable for
187
+ * responsive inline embedding in any host (Obsidian, remark/markdown, web
188
+ * pages):
189
+ *
190
+ * - dgmo renders diagrams inside a fixed export canvas (e.g.
191
+ * `viewBox="0 0 1200 800"`), with content often occupying only a fraction
192
+ * of it. We compute a tight content bounding box from element coordinates
193
+ * and set the root `viewBox` to bbox+padding, so the diagram's intrinsic
194
+ * aspect ratio matches its CONTENT — no dead space above/below or beside it.
195
+ * - Ensure the root `<svg>` has a `viewBox` so it scales responsively.
196
+ * - Strip fixed `width="N"` / `height="N"` so CSS (e.g. `width:100%;
197
+ * height:auto`, or an aspect-ratio derived from the tight viewBox) controls
198
+ * sizing.
199
+ * - Remove any inline `background:` from the root style so the page
200
+ * background shows through.
201
+ *
202
+ * This is intentionally a string transform, not a DOM `getBBox()` step: dgmo
203
+ * can dual-render light/dark SVGs where one is hidden by color-mode CSS, and
204
+ * `getBBox()` returns 0 for the hidden copy. Parsing coordinates from the
205
+ * markup measures both copies reliably and works server-side (Node).
206
+ */
207
+ declare function normalizeSvgForEmbed(input: string): string;
208
+ /**
209
+ * Parse the content bounding box of a normalized embed SVG, if one can be
210
+ * derived. Returns `null` when no usable coordinates are found (e.g. an empty
211
+ * diagram). Useful for hosts that want to set an explicit `aspect-ratio` from
212
+ * the tight viewBox.
213
+ */
214
+ declare function getEmbedSvgViewBox(svg: string): {
215
+ x: number;
216
+ y: number;
217
+ width: number;
218
+ height: number;
219
+ } | null;
220
+
185
221
  /**
186
222
  * A gazetteer city entry: `[lat, lon, iso, pop, name, sub?]`.
187
223
  * - `lat`/`lon` — rounded to 3 decimals.
@@ -336,4 +372,4 @@ interface DecodedDiagramUrl {
336
372
  */
337
373
  declare function decodeDiagramUrl(url: string): DecodedDiagramUrl | null;
338
374
 
339
- export { type CompactViewState, type DecodedDiagramUrl, type DgmoError, type DgmoSeverity, type EncodeDiagramUrlOptions, type Gazetteer, type GazetteerEntry, type MapCompletionOptions, type MapPlaceCompletion, type MapRegionCompletion, type PaletteColors, type PaletteConfig, type RegionName, type RegionNames, type RenderOptions, type RenderResult, type Theme, completeMapPlaces, completeMapRegions, decodeDiagramUrl, encodeDiagramUrl, formatDgmoError, getMinDimensions, getPalette, palettes, render, themes, parseDgmo as validate };
375
+ export { type CompactViewState, type DecodedDiagramUrl, type DgmoError, type DgmoSeverity, type EncodeDiagramUrlOptions, type Gazetteer, type GazetteerEntry, type MapCompletionOptions, type MapPlaceCompletion, type MapRegionCompletion, type PaletteColors, type PaletteConfig, type RegionName, type RegionNames, type RenderOptions, type RenderResult, type Theme, completeMapPlaces, completeMapRegions, decodeDiagramUrl, encodeDiagramUrl, formatDgmoError, getEmbedSvgViewBox, getMinDimensions, getPalette, normalizeSvgForEmbed, palettes, render, themes, parseDgmo as validate };