@diagrammo/dgmo 0.5.2 → 0.5.4

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
@@ -6883,7 +6883,10 @@ var init_types2 = __esm({
6883
6883
  "buffer",
6884
6884
  "drain-rate",
6885
6885
  "retention-hours",
6886
- "partitions"
6886
+ "partitions",
6887
+ "slo-availability",
6888
+ "slo-p90-latency-ms",
6889
+ "slo-warning-margin"
6887
6890
  ]);
6888
6891
  EDGE_ONLY_KEYS = /* @__PURE__ */ new Set(["rps"]);
6889
6892
  }
@@ -7019,6 +7022,18 @@ function parseInfra(content) {
7019
7022
  result.options["default-uptime"] = trimmed.replace(/^default-uptime\s*:\s*/i, "").trim();
7020
7023
  continue;
7021
7024
  }
7025
+ if (/^slo-availability\s*:/i.test(trimmed)) {
7026
+ result.options["slo-availability"] = trimmed.replace(/^slo-availability\s*:\s*/i, "").trim();
7027
+ continue;
7028
+ }
7029
+ if (/^slo-p90-latency-ms\s*:/i.test(trimmed)) {
7030
+ result.options["slo-p90-latency-ms"] = trimmed.replace(/^slo-p90-latency-ms\s*:\s*/i, "").trim();
7031
+ continue;
7032
+ }
7033
+ if (/^slo-warning-margin\s*:/i.test(trimmed)) {
7034
+ result.options["slo-warning-margin"] = trimmed.replace(/^slo-warning-margin\s*:\s*/i, "").trim();
7035
+ continue;
7036
+ }
7022
7037
  if (/^scenario\s*:/i.test(trimmed)) {
7023
7038
  finishCurrentNode();
7024
7039
  finishCurrentTagGroup();
@@ -7163,12 +7178,19 @@ function parseInfra(content) {
7163
7178
  if (simpleConn) {
7164
7179
  const targetName = simpleConn[1].trim();
7165
7180
  const splitStr = simpleConn[2];
7181
+ const fanoutStr = simpleConn[3];
7166
7182
  const split = splitStr ? parseFloat(splitStr) : null;
7183
+ const fanoutRaw = fanoutStr ? parseInt(fanoutStr, 10) : null;
7184
+ if (fanoutRaw !== null && fanoutRaw < 1) {
7185
+ warn(lineNumber, `Fan-out multiplier must be at least 1 (got x${fanoutRaw}). Ignoring.`);
7186
+ }
7187
+ const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
7167
7188
  result.edges.push({
7168
7189
  sourceId: currentNode.id,
7169
7190
  targetId: nodeId2(targetName),
7170
7191
  label: "",
7171
7192
  split,
7193
+ fanout,
7172
7194
  lineNumber
7173
7195
  });
7174
7196
  continue;
@@ -7178,7 +7200,13 @@ function parseInfra(content) {
7178
7200
  const label = connMatch[1]?.trim() || "";
7179
7201
  const targetName = connMatch[2].trim();
7180
7202
  const splitStr = connMatch[3];
7203
+ const fanoutStr = connMatch[4];
7181
7204
  const split = splitStr ? parseFloat(splitStr) : null;
7205
+ const fanoutRaw = fanoutStr ? parseInt(fanoutStr, 10) : null;
7206
+ if (fanoutRaw !== null && fanoutRaw < 1) {
7207
+ warn(lineNumber, `Fan-out multiplier must be at least 1 (got x${fanoutRaw}). Ignoring.`);
7208
+ }
7209
+ const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
7182
7210
  let targetId;
7183
7211
  const targetGroupMatch = targetName.match(GROUP_RE);
7184
7212
  if (targetGroupMatch) {
@@ -7191,14 +7219,20 @@ function parseInfra(content) {
7191
7219
  targetId,
7192
7220
  label,
7193
7221
  split,
7222
+ fanout,
7194
7223
  lineNumber
7195
7224
  });
7196
7225
  continue;
7197
7226
  }
7227
+ if (/^description\s*:\s*$/i.test(trimmed)) continue;
7198
7228
  const propMatch = trimmed.match(PROPERTY_RE);
7199
7229
  if (propMatch) {
7200
7230
  const key = propMatch[1].toLowerCase();
7201
7231
  const rawVal = propMatch[2].trim();
7232
+ if (key === "description" && currentNode) {
7233
+ if (!currentNode.isEdge) currentNode.description = rawVal;
7234
+ continue;
7235
+ }
7202
7236
  if (!INFRA_BEHAVIOR_KEYS.has(key) && !EDGE_ONLY_KEYS.has(key)) {
7203
7237
  const allKeys = [...INFRA_BEHAVIOR_KEYS, ...EDGE_ONLY_KEYS];
7204
7238
  let msg = `Unknown property '${key}'.`;
@@ -7300,12 +7334,12 @@ var init_parser9 = __esm({
7300
7334
  init_diagnostics();
7301
7335
  init_parsing();
7302
7336
  init_types2();
7303
- CONNECTION_RE = /^-(?:([^-].*?))?->\s+(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*$/;
7304
- SIMPLE_CONNECTION_RE = /^->\s+(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*$/;
7337
+ CONNECTION_RE = /^-(?:([^-].*?))?->\s+(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*(?:x(\d+))?\s*$/;
7338
+ SIMPLE_CONNECTION_RE = /^->\s+(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*(?:x(\d+))?\s*$/;
7305
7339
  GROUP_RE = /^\[([^\]]+)\]$/;
7306
7340
  TAG_GROUP_RE = /^tag\s*:\s*(\w[\w\s]*?)(?:\s+alias\s+(\w+))?\s*$/;
7307
7341
  TAG_VALUE_RE = /^(\w[\w\s]*?)(?:\(([^)]+)\))?(\s+default)?\s*$/;
7308
- COMPONENT_RE = /^([a-zA-Z_][\w]*)(.*)$/;
7342
+ COMPONENT_RE = /^([a-zA-Z_][\w-]*)(.*)$/;
7309
7343
  PIPE_META_RE = /[|,]\s*(\w+)\s*:\s*([^|,]+)/g;
7310
7344
  PROPERTY_RE = /^([\w-]+)\s*:\s*(.+)$/;
7311
7345
  PERCENT_RE = /^([\d.]+)%$/;
@@ -7539,7 +7573,7 @@ function centerHeavyChildren(node) {
7539
7573
  }
7540
7574
  node.children = result;
7541
7575
  }
7542
- function computeLegendGroups(tagGroups, _showEyeIcons, usedValuesByGroup) {
7576
+ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
7543
7577
  const groups = [];
7544
7578
  for (const group of tagGroups) {
7545
7579
  if (group.entries.length === 0) continue;
@@ -7552,7 +7586,8 @@ function computeLegendGroups(tagGroups, _showEyeIcons, usedValuesByGroup) {
7552
7586
  for (const entry of visibleEntries) {
7553
7587
  entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
7554
7588
  }
7555
- const capsuleWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + entriesWidth;
7589
+ const eyeSpace = showEyeIcons ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
7590
+ const capsuleWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + eyeSpace + entriesWidth;
7556
7591
  groups.push({
7557
7592
  name: group.name,
7558
7593
  alias: group.alias,
@@ -8185,7 +8220,7 @@ function layoutOrg(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expan
8185
8220
  height: finalHeight
8186
8221
  };
8187
8222
  }
8188
- var import_d3_hierarchy, CHAR_WIDTH, META_LINE_HEIGHT, HEADER_HEIGHT, SEPARATOR_GAP, CARD_H_PAD, CARD_V_PAD, MIN_CARD_WIDTH, H_GAP, V_GAP, MARGIN, CONTAINER_PAD_X, CONTAINER_PAD_BOTTOM, CONTAINER_LABEL_HEIGHT, CONTAINER_META_LINE_HEIGHT, STACK_V_GAP, LEGEND_GAP, LEGEND_HEIGHT, LEGEND_PILL_PAD, LEGEND_PILL_FONT_W, LEGEND_CAPSULE_PAD, LEGEND_DOT_R, LEGEND_ENTRY_FONT_W, LEGEND_ENTRY_DOT_GAP, LEGEND_ENTRY_TRAIL, LEGEND_GROUP_GAP;
8223
+ var import_d3_hierarchy, CHAR_WIDTH, META_LINE_HEIGHT, HEADER_HEIGHT, SEPARATOR_GAP, CARD_H_PAD, CARD_V_PAD, MIN_CARD_WIDTH, H_GAP, V_GAP, MARGIN, CONTAINER_PAD_X, CONTAINER_PAD_BOTTOM, CONTAINER_LABEL_HEIGHT, CONTAINER_META_LINE_HEIGHT, STACK_V_GAP, LEGEND_GAP, LEGEND_HEIGHT, LEGEND_PILL_PAD, LEGEND_PILL_FONT_W, LEGEND_CAPSULE_PAD, LEGEND_DOT_R, LEGEND_ENTRY_FONT_W, LEGEND_ENTRY_DOT_GAP, LEGEND_ENTRY_TRAIL, LEGEND_GROUP_GAP, LEGEND_EYE_SIZE, LEGEND_EYE_GAP;
8189
8224
  var init_layout = __esm({
8190
8225
  "src/org/layout.ts"() {
8191
8226
  "use strict";
@@ -8216,6 +8251,8 @@ var init_layout = __esm({
8216
8251
  LEGEND_ENTRY_DOT_GAP = 4;
8217
8252
  LEGEND_ENTRY_TRAIL = 8;
8218
8253
  LEGEND_GROUP_GAP = 12;
8254
+ LEGEND_EYE_SIZE = 14;
8255
+ LEGEND_EYE_GAP = 6;
8219
8256
  }
8220
8257
  });
8221
8258
 
@@ -8317,22 +8354,27 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
8317
8354
  const layoutLegendShift = LEGEND_HEIGHT2 + LEGEND_GROUP_GAP2;
8318
8355
  const fixedLegend = !exportDims && hasLegend && !legendOnly;
8319
8356
  const legendReserve = fixedLegend ? LEGEND_HEIGHT2 + LEGEND_FIXED_GAP : 0;
8357
+ const fixedTitle = !exportDims && !!parsed.title;
8358
+ const titleReserve = fixedTitle ? TITLE_HEIGHT : 0;
8320
8359
  const diagramW = layout.width;
8321
- let diagramH = layout.height + titleOffset;
8360
+ let diagramH = layout.height + (fixedTitle ? 0 : titleOffset);
8322
8361
  if (fixedLegend) {
8323
8362
  diagramH -= layoutLegendShift;
8324
8363
  }
8325
- const availH = height - DIAGRAM_PADDING * 2 - legendReserve;
8364
+ const availH = height - DIAGRAM_PADDING * 2 - legendReserve - titleReserve;
8326
8365
  const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
8327
8366
  const scaleY = availH / diagramH;
8328
8367
  const scale = Math.min(MAX_SCALE, scaleX, scaleY);
8329
8368
  const scaledW = diagramW * scale;
8330
8369
  const offsetX = (width - scaledW) / 2;
8331
- const offsetY = legendPosition === "top" && fixedLegend ? DIAGRAM_PADDING + legendReserve : DIAGRAM_PADDING;
8370
+ const offsetY = legendPosition === "top" && fixedLegend ? DIAGRAM_PADDING + legendReserve + titleReserve : DIAGRAM_PADDING + titleReserve;
8332
8371
  const svg = d3Selection.select(container).append("svg").attr("width", width).attr("height", height).style("font-family", FONT_FAMILY);
8333
8372
  const mainG = svg.append("g").attr("transform", `translate(${offsetX}, ${offsetY}) scale(${scale})`);
8334
8373
  if (parsed.title) {
8335
- const titleEl = mainG.append("text").attr("x", diagramW / 2).attr("y", TITLE_FONT_SIZE).attr("text-anchor", "middle").attr("fill", palette.text).attr("font-size", TITLE_FONT_SIZE).attr("font-weight", "bold").attr("class", "org-title chart-title").style(
8374
+ const titleParent = fixedTitle ? svg : mainG;
8375
+ const titleX = fixedTitle ? width / 2 : diagramW / 2;
8376
+ const titleY = fixedTitle ? DIAGRAM_PADDING + TITLE_FONT_SIZE : TITLE_FONT_SIZE;
8377
+ const titleEl = titleParent.append("text").attr("x", titleX).attr("y", titleY).attr("text-anchor", "middle").attr("fill", palette.text).attr("font-size", TITLE_FONT_SIZE).attr("font-weight", "bold").attr("class", "org-title chart-title").style(
8336
8378
  "cursor",
8337
8379
  onClickItem && parsed.titleLineNumber ? "pointer" : "default"
8338
8380
  ).text(parsed.title);
@@ -8347,7 +8389,7 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
8347
8389
  }
8348
8390
  }
8349
8391
  }
8350
- const contentG = mainG.append("g").attr("transform", `translate(0, ${titleOffset})`);
8392
+ const contentG = mainG.append("g").attr("transform", `translate(0, ${fixedTitle ? 0 : titleOffset})`);
8351
8393
  const displayNames = /* @__PURE__ */ new Map();
8352
8394
  for (const group of parsed.tagGroups) {
8353
8395
  displayNames.set(group.name.toLowerCase(), group.name);
@@ -8475,7 +8517,7 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
8475
8517
  }
8476
8518
  const legendParent = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr(
8477
8519
  "transform",
8478
- legendPosition === "bottom" ? `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT2})` : `translate(0, ${DIAGRAM_PADDING})`
8520
+ legendPosition === "bottom" ? `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT2})` : `translate(0, ${DIAGRAM_PADDING + titleReserve})`
8479
8521
  ) : contentG;
8480
8522
  for (const group of visibleGroups) {
8481
8523
  const isActive = legendOnly || activeTagGroup != null && group.name.toLowerCase() === activeTagGroup.toLowerCase();
@@ -8496,8 +8538,19 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
8496
8538
  gEl.append("rect").attr("x", pillXOff).attr("y", pillYOff).attr("width", pillWidth).attr("height", pillH).attr("rx", pillH / 2).attr("fill", "none").attr("stroke", mix(palette.textMuted, palette.bg, 50)).attr("stroke-width", 0.75);
8497
8539
  }
8498
8540
  gEl.append("text").attr("x", pillXOff + pillWidth / 2).attr("y", LEGEND_HEIGHT2 / 2 + LEGEND_PILL_FONT_SIZE / 2 - 2).attr("font-size", LEGEND_PILL_FONT_SIZE).attr("font-weight", "500").attr("fill", isActive ? palette.text : palette.textMuted).attr("text-anchor", "middle").text(pillLabel);
8541
+ if (isActive && fixedLegend) {
8542
+ const groupKey = group.name.toLowerCase();
8543
+ const isHidden = hiddenAttributes?.has(groupKey) ?? false;
8544
+ const eyeX = pillXOff + pillWidth + LEGEND_EYE_GAP2;
8545
+ const eyeY = (LEGEND_HEIGHT2 - LEGEND_EYE_SIZE2) / 2;
8546
+ const hitPad = 6;
8547
+ const eyeG = gEl.append("g").attr("class", "org-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
8548
+ eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE2 + hitPad * 2).attr("height", LEGEND_EYE_SIZE2 + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
8549
+ eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
8550
+ }
8499
8551
  if (isActive) {
8500
- let entryX = pillXOff + pillWidth + 4;
8552
+ const eyeShift = fixedLegend ? LEGEND_EYE_SIZE2 + LEGEND_EYE_GAP2 : 0;
8553
+ let entryX = pillXOff + pillWidth + 4 + eyeShift;
8501
8554
  for (const entry of group.entries) {
8502
8555
  const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
8503
8556
  entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R2).attr("cy", LEGEND_HEIGHT2 / 2).attr("r", LEGEND_DOT_R2).attr("fill", entry.color);
@@ -8543,7 +8596,7 @@ function renderOrgForExport(content, theme, palette) {
8543
8596
  document.body.removeChild(container);
8544
8597
  }
8545
8598
  }
8546
- var d3Selection, DIAGRAM_PADDING, MAX_SCALE, TITLE_HEIGHT, TITLE_FONT_SIZE, LABEL_FONT_SIZE, META_FONT_SIZE, META_LINE_HEIGHT2, HEADER_HEIGHT2, SEPARATOR_GAP2, EDGE_STROKE_WIDTH, NODE_STROKE_WIDTH, CARD_RADIUS, CONTAINER_RADIUS, CONTAINER_LABEL_FONT_SIZE, CONTAINER_META_FONT_SIZE, CONTAINER_META_LINE_HEIGHT2, CONTAINER_HEADER_HEIGHT, COLLAPSE_BAR_HEIGHT, COLLAPSE_BAR_INSET, LEGEND_HEIGHT2, LEGEND_PILL_PAD2, LEGEND_PILL_FONT_SIZE, LEGEND_PILL_FONT_W2, LEGEND_CAPSULE_PAD2, LEGEND_DOT_R2, LEGEND_ENTRY_FONT_SIZE, LEGEND_ENTRY_FONT_W2, LEGEND_ENTRY_DOT_GAP2, LEGEND_ENTRY_TRAIL2, LEGEND_GROUP_GAP2, LEGEND_FIXED_GAP;
8599
+ var d3Selection, DIAGRAM_PADDING, MAX_SCALE, TITLE_HEIGHT, TITLE_FONT_SIZE, LABEL_FONT_SIZE, META_FONT_SIZE, META_LINE_HEIGHT2, HEADER_HEIGHT2, SEPARATOR_GAP2, EDGE_STROKE_WIDTH, NODE_STROKE_WIDTH, CARD_RADIUS, CONTAINER_RADIUS, CONTAINER_LABEL_FONT_SIZE, CONTAINER_META_FONT_SIZE, CONTAINER_META_LINE_HEIGHT2, CONTAINER_HEADER_HEIGHT, COLLAPSE_BAR_HEIGHT, COLLAPSE_BAR_INSET, LEGEND_HEIGHT2, LEGEND_PILL_PAD2, LEGEND_PILL_FONT_SIZE, LEGEND_PILL_FONT_W2, LEGEND_CAPSULE_PAD2, LEGEND_DOT_R2, LEGEND_ENTRY_FONT_SIZE, LEGEND_ENTRY_FONT_W2, LEGEND_ENTRY_DOT_GAP2, LEGEND_ENTRY_TRAIL2, LEGEND_GROUP_GAP2, LEGEND_EYE_SIZE2, LEGEND_EYE_GAP2, LEGEND_FIXED_GAP, EYE_OPEN_PATH, EYE_CLOSED_PATH;
8547
8600
  var init_renderer = __esm({
8548
8601
  "src/org/renderer.ts"() {
8549
8602
  "use strict";
@@ -8582,7 +8635,11 @@ var init_renderer = __esm({
8582
8635
  LEGEND_ENTRY_DOT_GAP2 = 4;
8583
8636
  LEGEND_ENTRY_TRAIL2 = 8;
8584
8637
  LEGEND_GROUP_GAP2 = 12;
8638
+ LEGEND_EYE_SIZE2 = 14;
8639
+ LEGEND_EYE_GAP2 = 6;
8585
8640
  LEGEND_FIXED_GAP = 8;
8641
+ EYE_OPEN_PATH = "M1 7s2.5-5 6-5 6 5 6 5-2.5 5-6 5-6-5-6-5z M7 9.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z";
8642
+ EYE_CLOSED_PATH = "M2.5 2.5l9 9 M1.5 7s2.2-4 5.5-4c1.2 0 2.2.5 3 1.1 M12.5 7s-2.2 4-5.5 4c-1.2 0-2.2-.5-3-1.1";
8586
8643
  }
8587
8644
  });
8588
8645
 
@@ -8631,7 +8688,7 @@ function computeLegendGroups2(tagGroups, usedValuesByGroup) {
8631
8688
  for (const entry of visibleEntries) {
8632
8689
  entriesWidth += LEGEND_DOT_R3 * 2 + LEGEND_ENTRY_DOT_GAP3 + entry.value.length * LEGEND_ENTRY_FONT_W3 + LEGEND_ENTRY_TRAIL3;
8633
8690
  }
8634
- const eyeSpace = LEGEND_EYE_SIZE + LEGEND_EYE_GAP;
8691
+ const eyeSpace = LEGEND_EYE_SIZE3 + LEGEND_EYE_GAP3;
8635
8692
  const capsuleWidth = LEGEND_CAPSULE_PAD3 * 2 + pillWidth + 4 + eyeSpace + entriesWidth;
8636
8693
  groups.push({
8637
8694
  name: group.name,
@@ -9033,7 +9090,7 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
9033
9090
  height: totalHeight
9034
9091
  };
9035
9092
  }
9036
- var import_dagre, CHAR_WIDTH2, META_LINE_HEIGHT3, HEADER_HEIGHT3, SEPARATOR_GAP3, CARD_H_PAD2, CARD_V_PAD2, MIN_CARD_WIDTH2, MARGIN2, CONTAINER_PAD_X2, CONTAINER_PAD_TOP, CONTAINER_PAD_BOTTOM2, CONTAINER_LABEL_HEIGHT2, CONTAINER_META_LINE_HEIGHT3, LEGEND_HEIGHT3, LEGEND_PILL_PAD3, LEGEND_PILL_FONT_W3, LEGEND_CAPSULE_PAD3, LEGEND_DOT_R3, LEGEND_ENTRY_FONT_W3, LEGEND_ENTRY_DOT_GAP3, LEGEND_ENTRY_TRAIL3, LEGEND_GROUP_GAP3, LEGEND_EYE_SIZE, LEGEND_EYE_GAP, OVERLAP_GAP;
9093
+ var import_dagre, CHAR_WIDTH2, META_LINE_HEIGHT3, HEADER_HEIGHT3, SEPARATOR_GAP3, CARD_H_PAD2, CARD_V_PAD2, MIN_CARD_WIDTH2, MARGIN2, CONTAINER_PAD_X2, CONTAINER_PAD_TOP, CONTAINER_PAD_BOTTOM2, CONTAINER_LABEL_HEIGHT2, CONTAINER_META_LINE_HEIGHT3, LEGEND_HEIGHT3, LEGEND_PILL_PAD3, LEGEND_PILL_FONT_W3, LEGEND_CAPSULE_PAD3, LEGEND_DOT_R3, LEGEND_ENTRY_FONT_W3, LEGEND_ENTRY_DOT_GAP3, LEGEND_ENTRY_TRAIL3, LEGEND_GROUP_GAP3, LEGEND_EYE_SIZE3, LEGEND_EYE_GAP3, OVERLAP_GAP;
9037
9094
  var init_layout2 = __esm({
9038
9095
  "src/sitemap/layout.ts"() {
9039
9096
  "use strict";
@@ -9061,8 +9118,8 @@ var init_layout2 = __esm({
9061
9118
  LEGEND_ENTRY_DOT_GAP3 = 4;
9062
9119
  LEGEND_ENTRY_TRAIL3 = 8;
9063
9120
  LEGEND_GROUP_GAP3 = 12;
9064
- LEGEND_EYE_SIZE = 14;
9065
- LEGEND_EYE_GAP = 6;
9121
+ LEGEND_EYE_SIZE3 = 14;
9122
+ LEGEND_EYE_GAP3 = 6;
9066
9123
  OVERLAP_GAP = 20;
9067
9124
  }
9068
9125
  });
@@ -9414,15 +9471,15 @@ function renderLegend(parent, legendGroups, palette, isDark, activeTagGroup, fix
9414
9471
  if (isActive && fixedWidth != null) {
9415
9472
  const groupKey = group.name.toLowerCase();
9416
9473
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
9417
- const eyeX = pillXOff + pillW + LEGEND_EYE_GAP2;
9418
- const eyeY = (LEGEND_HEIGHT4 - LEGEND_EYE_SIZE2) / 2;
9474
+ const eyeX = pillXOff + pillW + LEGEND_EYE_GAP4;
9475
+ const eyeY = (LEGEND_HEIGHT4 - LEGEND_EYE_SIZE4) / 2;
9419
9476
  const hitPad = 6;
9420
9477
  const eyeG = legendG.append("g").attr("class", "sitemap-legend-eye").attr("data-legend-visibility", groupKey).style("cursor", "pointer").attr("opacity", isHidden ? 0.4 : 0.7);
9421
- eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE2 + hitPad * 2).attr("height", LEGEND_EYE_SIZE2 + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
9422
- eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH : EYE_OPEN_PATH).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
9478
+ eyeG.append("rect").attr("x", eyeX - hitPad).attr("y", eyeY - hitPad).attr("width", LEGEND_EYE_SIZE4 + hitPad * 2).attr("height", LEGEND_EYE_SIZE4 + hitPad * 2).attr("fill", "transparent").attr("pointer-events", "all");
9479
+ eyeG.append("path").attr("d", isHidden ? EYE_CLOSED_PATH2 : EYE_OPEN_PATH2).attr("transform", `translate(${eyeX}, ${eyeY})`).attr("fill", "none").attr("stroke", palette.textMuted).attr("stroke-width", 1.2).attr("stroke-linecap", "round").attr("stroke-linejoin", "round");
9423
9480
  }
9424
9481
  if (isActive) {
9425
- const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE2 + LEGEND_EYE_GAP2 : 0;
9482
+ const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE4 + LEGEND_EYE_GAP4 : 0;
9426
9483
  let entryX = pillXOff + pillW + 4 + eyeShift;
9427
9484
  for (const entry of group.entries) {
9428
9485
  const entryG = legendG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
@@ -9475,7 +9532,7 @@ async function renderSitemapForExport(content, theme, palette) {
9475
9532
  const brandColor = theme === "transparent" ? "#888" : effectivePalette.textMuted;
9476
9533
  return injectBranding2(svgHtml, brandColor);
9477
9534
  }
9478
- var d3Selection2, d3Shape, DIAGRAM_PADDING2, MAX_SCALE2, TITLE_HEIGHT2, TITLE_FONT_SIZE2, LABEL_FONT_SIZE2, META_FONT_SIZE2, META_LINE_HEIGHT4, HEADER_HEIGHT4, SEPARATOR_GAP4, EDGE_STROKE_WIDTH2, NODE_STROKE_WIDTH2, CARD_RADIUS2, CONTAINER_RADIUS2, CONTAINER_LABEL_FONT_SIZE2, CONTAINER_META_FONT_SIZE2, CONTAINER_META_LINE_HEIGHT4, CONTAINER_HEADER_HEIGHT2, ARROWHEAD_W, ARROWHEAD_H, EDGE_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT2, LEGEND_HEIGHT4, LEGEND_FIXED_GAP2, LEGEND_PILL_PAD4, LEGEND_PILL_FONT_SIZE2, LEGEND_PILL_FONT_W4, LEGEND_CAPSULE_PAD4, LEGEND_DOT_R4, LEGEND_ENTRY_FONT_SIZE2, LEGEND_ENTRY_FONT_W4, LEGEND_ENTRY_DOT_GAP4, LEGEND_ENTRY_TRAIL4, LEGEND_GROUP_GAP4, LEGEND_EYE_SIZE2, LEGEND_EYE_GAP2, lineGenerator, EYE_OPEN_PATH, EYE_CLOSED_PATH;
9535
+ var d3Selection2, d3Shape, DIAGRAM_PADDING2, MAX_SCALE2, TITLE_HEIGHT2, TITLE_FONT_SIZE2, LABEL_FONT_SIZE2, META_FONT_SIZE2, META_LINE_HEIGHT4, HEADER_HEIGHT4, SEPARATOR_GAP4, EDGE_STROKE_WIDTH2, NODE_STROKE_WIDTH2, CARD_RADIUS2, CONTAINER_RADIUS2, CONTAINER_LABEL_FONT_SIZE2, CONTAINER_META_FONT_SIZE2, CONTAINER_META_LINE_HEIGHT4, CONTAINER_HEADER_HEIGHT2, ARROWHEAD_W, ARROWHEAD_H, EDGE_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT2, LEGEND_HEIGHT4, LEGEND_FIXED_GAP2, LEGEND_PILL_PAD4, LEGEND_PILL_FONT_SIZE2, LEGEND_PILL_FONT_W4, LEGEND_CAPSULE_PAD4, LEGEND_DOT_R4, LEGEND_ENTRY_FONT_SIZE2, LEGEND_ENTRY_FONT_W4, LEGEND_ENTRY_DOT_GAP4, LEGEND_ENTRY_TRAIL4, LEGEND_GROUP_GAP4, LEGEND_EYE_SIZE4, LEGEND_EYE_GAP4, lineGenerator, EYE_OPEN_PATH2, EYE_CLOSED_PATH2;
9479
9536
  var init_renderer2 = __esm({
9480
9537
  "src/sitemap/renderer.ts"() {
9481
9538
  "use strict";
@@ -9516,11 +9573,11 @@ var init_renderer2 = __esm({
9516
9573
  LEGEND_ENTRY_DOT_GAP4 = 4;
9517
9574
  LEGEND_ENTRY_TRAIL4 = 8;
9518
9575
  LEGEND_GROUP_GAP4 = 12;
9519
- LEGEND_EYE_SIZE2 = 14;
9520
- LEGEND_EYE_GAP2 = 6;
9576
+ LEGEND_EYE_SIZE4 = 14;
9577
+ LEGEND_EYE_GAP4 = 6;
9521
9578
  lineGenerator = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveBasis);
9522
- EYE_OPEN_PATH = "M1 7s2.5-5 6-5 6 5 6 5-2.5 5-6 5-6-5-6-5z M7 9.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z";
9523
- EYE_CLOSED_PATH = "M2.5 2.5l9 9 M1.5 7s2.2-4 5.5-4c1.2 0 2.2.5 3 1.1 M12.5 7s-2.2 4-5.5 4c-1.2 0-2.2-.5-3-1.1";
9579
+ EYE_OPEN_PATH2 = "M1 7s2.5-5 6-5 6 5 6 5-2.5 5-6 5-6-5-6-5z M7 9.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z";
9580
+ EYE_CLOSED_PATH2 = "M2.5 2.5l9 9 M1.5 7s2.2-4 5.5-4c1.2 0 2.2.5 3 1.1 M12.5 7s-2.2 4-5.5 4c-1.2 0-2.2-.5-3-1.1";
9524
9581
  }
9525
9582
  });
9526
9583
 
@@ -10756,7 +10813,8 @@ var init_renderer5 = __esm({
10756
10813
  // src/initiative-status/layout.ts
10757
10814
  var layout_exports5 = {};
10758
10815
  __export(layout_exports5, {
10759
- layoutInitiativeStatus: () => layoutInitiativeStatus
10816
+ layoutInitiativeStatus: () => layoutInitiativeStatus,
10817
+ rollUpStatus: () => rollUpStatus
10760
10818
  });
10761
10819
  function rollUpStatus(members) {
10762
10820
  let worst = null;
@@ -10770,14 +10828,28 @@ function rollUpStatus(members) {
10770
10828
  }
10771
10829
  return worst;
10772
10830
  }
10773
- function layoutInitiativeStatus(parsed) {
10774
- if (parsed.nodes.length === 0) {
10831
+ function layoutInitiativeStatus(parsed, collapseResult) {
10832
+ if (parsed.nodes.length === 0 && (!collapseResult || collapseResult.collapsedGroupStatuses.size === 0)) {
10775
10833
  return { nodes: [], edges: [], groups: [], width: 0, height: 0 };
10776
10834
  }
10777
- const hasGroups = parsed.groups.length > 0;
10835
+ const originalGroups = collapseResult?.originalGroups ?? parsed.groups;
10836
+ const collapsedGroupStatuses = collapseResult?.collapsedGroupStatuses ?? /* @__PURE__ */ new Map();
10837
+ const collapsedGroupLabels = new Set(
10838
+ originalGroups.map((g2) => g2.label).filter((l) => !parsed.groups.some((g2) => g2.label === l))
10839
+ );
10840
+ const hasGroups = parsed.groups.length > 0 || collapsedGroupLabels.size > 0;
10778
10841
  const g = new import_dagre4.default.graphlib.Graph({ multigraph: true, compound: hasGroups });
10779
10842
  g.setGraph({ rankdir: "LR", nodesep: NODESEP, ranksep: RANKSEP });
10780
10843
  g.setDefaultEdgeLabel(() => ({}));
10844
+ for (const group of originalGroups) {
10845
+ if (collapsedGroupLabels.has(group.label)) {
10846
+ const collapsedW = Math.max(
10847
+ NODE_WIDTH,
10848
+ Math.ceil(group.label.length * CHAR_WIDTH_RATIO * NODE_FONT_SIZE) + NODE_TEXT_PADDING * 2
10849
+ );
10850
+ g.setNode(group.label, { label: group.label, width: collapsedW, height: NODE_HEIGHT });
10851
+ }
10852
+ }
10781
10853
  for (const group of parsed.groups) {
10782
10854
  g.setNode(`__group_${group.label}`, { label: group.label, clusterLabelPos: "top" });
10783
10855
  }
@@ -10808,38 +10880,130 @@ function layoutInitiativeStatus(parsed) {
10808
10880
  height: pos.height
10809
10881
  };
10810
10882
  });
10811
- const nodeMap = new Map(layoutNodes.map((n) => [n.label, n]));
10812
- const allNodeX = layoutNodes.map((n) => n.x);
10813
- const layoutEdges = parsed.edges.map((edge, i) => {
10814
- const src = nodeMap.get(edge.source);
10815
- const tgt = nodeMap.get(edge.target);
10883
+ const posMap = new Map(layoutNodes.map((n) => [n.label, n]));
10884
+ for (const label of collapsedGroupLabels) {
10885
+ const pos = g.node(label);
10886
+ if (pos) posMap.set(label, { x: pos.x, y: pos.y, width: pos.width, height: pos.height });
10887
+ }
10888
+ const allNodeX = [...posMap.values()].map((n) => n.x);
10889
+ const avgNodeY = layoutNodes.length > 0 ? layoutNodes.reduce((s, n) => s + n.y, 0) / layoutNodes.length : 0;
10890
+ const avgNodeX = layoutNodes.length > 0 ? layoutNodes.reduce((s, n) => s + n.x, 0) / layoutNodes.length : 0;
10891
+ const edgeYOffsets = new Array(parsed.edges.length).fill(0);
10892
+ const edgeParallelCounts = new Array(parsed.edges.length).fill(1);
10893
+ const parallelGroups = /* @__PURE__ */ new Map();
10894
+ for (let i = 0; i < parsed.edges.length; i++) {
10895
+ const edge = parsed.edges[i];
10896
+ const key = `${edge.source}\0${edge.target}`;
10897
+ parallelGroups.set(key, parallelGroups.get(key) ?? []);
10898
+ parallelGroups.get(key).push(i);
10899
+ }
10900
+ for (const group of parallelGroups.values()) {
10901
+ const capped = group.slice(0, MAX_PARALLEL_EDGES);
10902
+ for (const idx of group.slice(MAX_PARALLEL_EDGES)) {
10903
+ edgeParallelCounts[idx] = 0;
10904
+ }
10905
+ if (capped.length < 2) continue;
10906
+ const effectiveSpacing = Math.min(PARALLEL_SPACING, (NODE_HEIGHT - PARALLEL_EDGE_MARGIN) / (capped.length - 1));
10907
+ for (let j = 0; j < capped.length; j++) {
10908
+ edgeYOffsets[capped[j]] = (j - (capped.length - 1) / 2) * effectiveSpacing;
10909
+ edgeParallelCounts[capped[j]] = capped.length;
10910
+ }
10911
+ }
10912
+ const layoutEdges = [];
10913
+ for (let i = 0; i < parsed.edges.length; i++) {
10914
+ const edge = parsed.edges[i];
10915
+ const src = posMap.get(edge.source);
10916
+ const tgt = posMap.get(edge.target);
10917
+ if (edgeParallelCounts[i] === 0) continue;
10918
+ if (!src || !tgt) continue;
10919
+ const yOffset = edgeYOffsets[i];
10920
+ const parallelCount = edgeParallelCounts[i];
10816
10921
  const exitX = src.x + src.width / 2;
10817
10922
  const enterX = tgt.x - tgt.width / 2;
10818
10923
  const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
10819
10924
  const dagrePoints = dagreEdge?.points ?? [];
10820
10925
  const hasIntermediateRank = allNodeX.some((x) => x > src.x + 20 && x < tgt.x - 20);
10821
10926
  const step = Math.min((enterX - exitX) * 0.15, 20);
10822
- const fixedDagrePoints = dagrePoints.length >= 2 ? [
10823
- { x: exitX, y: src.y },
10824
- ...dagrePoints.slice(1, -1),
10825
- { x: enterX, y: tgt.y }
10826
- ] : dagrePoints;
10827
- const points = tgt.x > src.x && !hasIntermediateRank ? [
10828
- { x: exitX, y: src.y },
10829
- { x: exitX + step, y: src.y },
10830
- { x: enterX - step, y: tgt.y },
10831
- { x: enterX, y: tgt.y }
10832
- ] : fixedDagrePoints;
10833
- return {
10927
+ const isBackEdge = tgt.x < src.x - 5;
10928
+ const isYDisplaced = !isBackEdge && Math.abs(tgt.y - src.y) > NODESEP;
10929
+ let points;
10930
+ if (isBackEdge) {
10931
+ const routeAbove = Math.min(src.y, tgt.y) > avgNodeY;
10932
+ const srcHalfH = src.height / 2;
10933
+ const tgtHalfH = tgt.height / 2;
10934
+ const rawMidX = (src.x + tgt.x) / 2;
10935
+ const spreadDir = avgNodeX < rawMidX ? 1 : -1;
10936
+ const unclamped = Math.abs(src.x - tgt.x) < NODE_WIDTH ? rawMidX + spreadDir * BACK_EDGE_MIN_SPREAD : rawMidX;
10937
+ const midX = Math.min(src.x, Math.max(tgt.x, unclamped));
10938
+ if (routeAbove) {
10939
+ const arcY = Math.min(src.y - srcHalfH, tgt.y - tgtHalfH) - BACK_EDGE_MARGIN;
10940
+ points = [
10941
+ { x: src.x, y: src.y - srcHalfH },
10942
+ { x: midX, y: arcY },
10943
+ { x: tgt.x, y: tgt.y - tgtHalfH }
10944
+ ];
10945
+ } else {
10946
+ const arcY = Math.max(src.y + srcHalfH, tgt.y + tgtHalfH) + BACK_EDGE_MARGIN;
10947
+ points = [
10948
+ { x: src.x, y: src.y + srcHalfH },
10949
+ { x: midX, y: arcY },
10950
+ { x: tgt.x, y: tgt.y + tgtHalfH }
10951
+ ];
10952
+ }
10953
+ } else if (isYDisplaced) {
10954
+ const exitY = tgt.y > src.y + NODESEP ? src.y + src.height / 2 : src.y - src.height / 2;
10955
+ const midX = Math.max(src.x + 1, (src.x + enterX) / 2);
10956
+ const midY = (exitY + tgt.y) / 2;
10957
+ points = [
10958
+ { x: src.x, y: exitY },
10959
+ { x: midX, y: midY },
10960
+ { x: enterX, y: tgt.y }
10961
+ ];
10962
+ } else if (tgt.x > src.x && !hasIntermediateRank) {
10963
+ points = [
10964
+ { x: exitX, y: src.y },
10965
+ // exits node center — stays pinned
10966
+ { x: exitX + step, y: src.y + yOffset },
10967
+ // fans out
10968
+ { x: enterX - step, y: tgt.y + yOffset },
10969
+ // still fanned
10970
+ { x: enterX, y: tgt.y }
10971
+ // enters node center — stays pinned
10972
+ ];
10973
+ } else {
10974
+ points = dagrePoints.length >= 2 ? [
10975
+ { x: exitX, y: src.y + yOffset },
10976
+ ...dagrePoints.slice(1, -1),
10977
+ { x: enterX, y: tgt.y + yOffset }
10978
+ ] : dagrePoints;
10979
+ }
10980
+ layoutEdges.push({
10834
10981
  source: edge.source,
10835
10982
  target: edge.target,
10836
10983
  label: edge.label,
10837
10984
  status: edge.status,
10838
10985
  lineNumber: edge.lineNumber,
10839
- points
10840
- };
10841
- });
10986
+ points,
10987
+ parallelCount
10988
+ });
10989
+ }
10842
10990
  const layoutGroups = [];
10991
+ for (const group of originalGroups) {
10992
+ if (collapsedGroupLabels.has(group.label)) {
10993
+ const pos = g.node(group.label);
10994
+ if (!pos) continue;
10995
+ layoutGroups.push({
10996
+ label: group.label,
10997
+ status: collapsedGroupStatuses.get(group.label) ?? null,
10998
+ x: pos.x - pos.width / 2,
10999
+ y: pos.y - pos.height / 2,
11000
+ width: pos.width,
11001
+ height: pos.height,
11002
+ lineNumber: group.lineNumber,
11003
+ collapsed: true
11004
+ });
11005
+ }
11006
+ }
10843
11007
  if (parsed.groups.length > 0) {
10844
11008
  const nMap = new Map(layoutNodes.map((n) => [n.label, n]));
10845
11009
  for (const group of parsed.groups) {
@@ -10863,7 +11027,8 @@ function layoutInitiativeStatus(parsed) {
10863
11027
  y: minY - GROUP_PADDING,
10864
11028
  width: maxX - minX + GROUP_PADDING * 2,
10865
11029
  height: maxY - minY + GROUP_PADDING * 2,
10866
- lineNumber: group.lineNumber
11030
+ lineNumber: group.lineNumber,
11031
+ collapsed: false
10867
11032
  });
10868
11033
  }
10869
11034
  }
@@ -10889,7 +11054,7 @@ function layoutInitiativeStatus(parsed) {
10889
11054
  totalHeight += 40;
10890
11055
  return { nodes: layoutNodes, edges: layoutEdges, groups: layoutGroups, width: totalWidth, height: totalHeight };
10891
11056
  }
10892
- var import_dagre4, STATUS_PRIORITY, PHI, NODE_HEIGHT, NODE_WIDTH, GROUP_PADDING, NODESEP, RANKSEP;
11057
+ var import_dagre4, STATUS_PRIORITY, PHI, NODE_HEIGHT, NODE_WIDTH, GROUP_PADDING, NODESEP, RANKSEP, PARALLEL_SPACING, PARALLEL_EDGE_MARGIN, MAX_PARALLEL_EDGES, BACK_EDGE_MARGIN, BACK_EDGE_MIN_SPREAD, CHAR_WIDTH_RATIO, NODE_FONT_SIZE, NODE_TEXT_PADDING;
10893
11058
  var init_layout5 = __esm({
10894
11059
  "src/initiative-status/layout.ts"() {
10895
11060
  "use strict";
@@ -10901,6 +11066,14 @@ var init_layout5 = __esm({
10901
11066
  GROUP_PADDING = 20;
10902
11067
  NODESEP = 80;
10903
11068
  RANKSEP = 160;
11069
+ PARALLEL_SPACING = 16;
11070
+ PARALLEL_EDGE_MARGIN = 12;
11071
+ MAX_PARALLEL_EDGES = 5;
11072
+ BACK_EDGE_MARGIN = 40;
11073
+ BACK_EDGE_MIN_SPREAD = Math.round(NODE_WIDTH * 0.75);
11074
+ CHAR_WIDTH_RATIO = 0.6;
11075
+ NODE_FONT_SIZE = 13;
11076
+ NODE_TEXT_PADDING = 12;
10904
11077
  }
10905
11078
  });
10906
11079
 
@@ -10956,10 +11129,10 @@ function splitCamelCase(word) {
10956
11129
  return parts.length > 1 ? parts : [word];
10957
11130
  }
10958
11131
  function fitTextToNode(label, nodeWidth, nodeHeight) {
10959
- const maxTextWidth = nodeWidth - NODE_TEXT_PADDING * 2;
11132
+ const maxTextWidth = nodeWidth - NODE_TEXT_PADDING2 * 2;
10960
11133
  const lineHeight = 1.3;
10961
- for (let fontSize = NODE_FONT_SIZE; fontSize >= MIN_NODE_FONT_SIZE; fontSize--) {
10962
- const charWidth2 = fontSize * CHAR_WIDTH_RATIO;
11134
+ for (let fontSize = NODE_FONT_SIZE2; fontSize >= MIN_NODE_FONT_SIZE; fontSize--) {
11135
+ const charWidth2 = fontSize * CHAR_WIDTH_RATIO2;
10963
11136
  const maxCharsPerLine = Math.floor(maxTextWidth / charWidth2);
10964
11137
  const maxLines = Math.floor((nodeHeight - 8) / (fontSize * lineHeight));
10965
11138
  if (maxCharsPerLine < 2 || maxLines < 1) continue;
@@ -11020,8 +11193,8 @@ function fitTextToNode(label, nodeWidth, nodeHeight) {
11020
11193
  return { lines: hardLines, fontSize };
11021
11194
  }
11022
11195
  }
11023
- const charWidth = MIN_NODE_FONT_SIZE * CHAR_WIDTH_RATIO;
11024
- const maxChars = Math.floor((nodeWidth - NODE_TEXT_PADDING * 2) / charWidth);
11196
+ const charWidth = MIN_NODE_FONT_SIZE * CHAR_WIDTH_RATIO2;
11197
+ const maxChars = Math.floor((nodeWidth - NODE_TEXT_PADDING2 * 2) / charWidth);
11025
11198
  const truncated = label.length > maxChars ? label.slice(0, maxChars - 1) + "\u2026" : label;
11026
11199
  return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
11027
11200
  }
@@ -11244,21 +11417,27 @@ function renderInitiativeStatus(container, parsed, layout, palette, isDark, onCl
11244
11417
  const labelMap = /* @__PURE__ */ new Map();
11245
11418
  for (const lp of labelPlacements) labelMap.set(lp.edgeIdx, lp);
11246
11419
  for (const group of layout.groups) {
11247
- if (group.width === 0 && group.height === 0) continue;
11248
- const gx = group.x - GROUP_EXTRA_PADDING;
11249
- const gy = group.y - GROUP_EXTRA_PADDING - GROUP_LABEL_FONT_SIZE - 4;
11250
- const gw = group.width + GROUP_EXTRA_PADDING * 2;
11251
- const gh = group.height + GROUP_EXTRA_PADDING * 2 + GROUP_LABEL_FONT_SIZE + 4;
11252
- const groupStatusColor = group.status ? statusColor(group.status, palette, isDark) : palette.textMuted;
11253
- const fillColor = mix(groupStatusColor, isDark ? palette.surface : palette.bg, 15);
11254
- const strokeColor = mix(groupStatusColor, palette.textMuted, 50);
11255
- const groupG = contentG.append("g").attr("class", "is-group").attr("data-line-number", String(group.lineNumber));
11256
- groupG.append("rect").attr("x", gx).attr("y", gy).attr("width", gw).attr("height", gh).attr("rx", 6).attr("fill", fillColor).attr("stroke", strokeColor).attr("stroke-width", 1).attr("stroke-opacity", 0.5);
11257
- groupG.append("text").attr("x", gx + 8).attr("y", gy + GROUP_LABEL_FONT_SIZE + 4).attr("fill", strokeColor).attr("font-size", GROUP_LABEL_FONT_SIZE).attr("font-weight", "bold").attr("opacity", 0.7).attr("class", "is-group-label").text(group.label);
11258
- if (onClickItem) {
11259
- groupG.style("cursor", "pointer").on("click", () => {
11260
- onClickItem(group.lineNumber);
11261
- });
11420
+ if (group.collapsed) {
11421
+ const fillCol = nodeFill4(group.status, palette, isDark);
11422
+ const strokeCol = nodeStroke4(group.status, palette, isDark);
11423
+ const textCol = nodeTextColor(group.status, palette, isDark);
11424
+ const clipId = `clip-group-${group.lineNumber}`;
11425
+ const groupG = contentG.append("g").attr("class", "is-group is-group-collapsed").attr("data-line-number", String(group.lineNumber)).attr("data-group-toggle", group.label).style("cursor", "pointer");
11426
+ groupG.append("clipPath").attr("id", clipId).append("rect").attr("x", group.x).attr("y", group.y).attr("width", group.width).attr("height", group.height).attr("rx", NODE_RX);
11427
+ groupG.append("rect").attr("x", group.x).attr("y", group.y).attr("width", group.width).attr("height", group.height).attr("rx", NODE_RX).attr("fill", fillCol).attr("stroke", strokeCol).attr("stroke-width", NODE_STROKE_WIDTH5);
11428
+ groupG.append("rect").attr("x", group.x).attr("y", group.y + group.height - COLLAPSE_BAR_HEIGHT3).attr("width", group.width).attr("height", COLLAPSE_BAR_HEIGHT3).attr("fill", strokeCol).attr("clip-path", `url(#${clipId})`).attr("class", "is-collapse-bar");
11429
+ groupG.append("text").attr("x", group.x + group.width / 2).attr("y", group.y + group.height / 2 - COLLAPSE_BAR_HEIGHT3 / 2).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("fill", textCol).attr("font-size", NODE_FONT_SIZE2).attr("font-weight", "bold").attr("font-family", FONT_FAMILY).text(group.label);
11430
+ } else {
11431
+ if (group.width === 0 && group.height === 0) continue;
11432
+ const gx = group.x - GROUP_EXTRA_PADDING;
11433
+ const gy = group.y - GROUP_EXTRA_PADDING - GROUP_LABEL_FONT_SIZE - 4;
11434
+ const gw = group.width + GROUP_EXTRA_PADDING * 2;
11435
+ const gh = group.height + GROUP_EXTRA_PADDING * 2 + GROUP_LABEL_FONT_SIZE + 4;
11436
+ const fillColor = isDark ? palette.surface : palette.bg;
11437
+ const strokeColor = palette.textMuted;
11438
+ const groupG = contentG.append("g").attr("class", "is-group").attr("data-line-number", String(group.lineNumber)).attr("data-group-toggle", group.label).style("cursor", "pointer");
11439
+ groupG.append("rect").attr("x", gx).attr("y", gy).attr("width", gw).attr("height", gh).attr("rx", 6).attr("fill", fillColor).attr("stroke", strokeColor).attr("stroke-opacity", 0.5);
11440
+ groupG.append("text").attr("x", gx + 8).attr("y", gy + GROUP_LABEL_FONT_SIZE + 4).attr("fill", strokeColor).attr("font-size", GROUP_LABEL_FONT_SIZE).attr("font-weight", "bold").attr("opacity", 0.7).attr("class", "is-group-label").text(group.label);
11262
11441
  }
11263
11442
  }
11264
11443
  for (let ei = 0; ei < layout.edges.length; ei++) {
@@ -11269,7 +11448,7 @@ function renderInitiativeStatus(container, parsed, layout, palette, isDark, onCl
11269
11448
  const edgeG = contentG.append("g").attr("class", "is-edge-group").attr("data-line-number", String(edge.lineNumber));
11270
11449
  const pathD = lineGenerator4(edge.points);
11271
11450
  if (pathD) {
11272
- edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", "transparent").attr("stroke-width", 16);
11451
+ edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", "transparent").attr("stroke-width", Math.max(6, Math.round(16 / (edge.parallelCount ?? 1))));
11273
11452
  edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", edgeColor2).attr("stroke-width", EDGE_STROKE_WIDTH5).attr("marker-end", `url(#${markerId})`).attr("class", "is-edge");
11274
11453
  }
11275
11454
  const lp = labelMap.get(ei);
@@ -11349,7 +11528,7 @@ function renderInitiativeStatusForExport(content, theme, palette) {
11349
11528
  document.body.removeChild(container);
11350
11529
  }
11351
11530
  }
11352
- var d3Selection6, d3Shape4, DIAGRAM_PADDING6, MAX_SCALE5, NODE_FONT_SIZE, MIN_NODE_FONT_SIZE, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, ARROWHEAD_W2, ARROWHEAD_H2, CHAR_WIDTH_RATIO, NODE_TEXT_PADDING, SERVICE_RX, GROUP_EXTRA_PADDING, GROUP_LABEL_FONT_SIZE, lineGenerator4;
11531
+ var d3Selection6, d3Shape4, DIAGRAM_PADDING6, MAX_SCALE5, NODE_FONT_SIZE2, MIN_NODE_FONT_SIZE, EDGE_LABEL_FONT_SIZE4, EDGE_STROKE_WIDTH5, NODE_STROKE_WIDTH5, NODE_RX, ARROWHEAD_W2, ARROWHEAD_H2, CHAR_WIDTH_RATIO2, NODE_TEXT_PADDING2, SERVICE_RX, GROUP_EXTRA_PADDING, GROUP_LABEL_FONT_SIZE, COLLAPSE_BAR_HEIGHT3, lineGenerator4;
11353
11532
  var init_renderer6 = __esm({
11354
11533
  "src/initiative-status/renderer.ts"() {
11355
11534
  "use strict";
@@ -11361,19 +11540,20 @@ var init_renderer6 = __esm({
11361
11540
  init_layout5();
11362
11541
  DIAGRAM_PADDING6 = 20;
11363
11542
  MAX_SCALE5 = 3;
11364
- NODE_FONT_SIZE = 13;
11543
+ NODE_FONT_SIZE2 = 13;
11365
11544
  MIN_NODE_FONT_SIZE = 9;
11366
11545
  EDGE_LABEL_FONT_SIZE4 = 11;
11367
11546
  EDGE_STROKE_WIDTH5 = 2;
11368
11547
  NODE_STROKE_WIDTH5 = 2;
11369
11548
  NODE_RX = 8;
11370
- ARROWHEAD_W2 = 10;
11371
- ARROWHEAD_H2 = 7;
11372
- CHAR_WIDTH_RATIO = 0.6;
11373
- NODE_TEXT_PADDING = 12;
11549
+ ARROWHEAD_W2 = 5;
11550
+ ARROWHEAD_H2 = 4;
11551
+ CHAR_WIDTH_RATIO2 = 0.6;
11552
+ NODE_TEXT_PADDING2 = 12;
11374
11553
  SERVICE_RX = 10;
11375
11554
  GROUP_EXTRA_PADDING = 8;
11376
11555
  GROUP_LABEL_FONT_SIZE = 11;
11556
+ COLLAPSE_BAR_HEIGHT3 = 6;
11377
11557
  lineGenerator4 = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveMonotoneX);
11378
11558
  }
11379
11559
  });
@@ -14149,7 +14329,7 @@ function renderFlowchart(container, graph, layout, palette, isDark, onClickItem,
14149
14329
  });
14150
14330
  }
14151
14331
  renderNodeShape2(nodeG, node, palette, isDark, endTerminalIds, colorOff);
14152
- nodeG.append("text").attr("x", 0).attr("y", 0).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("fill", palette.text).attr("font-size", NODE_FONT_SIZE2).text(node.label);
14332
+ nodeG.append("text").attr("x", 0).attr("y", 0).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("fill", palette.text).attr("font-size", NODE_FONT_SIZE3).text(node.label);
14153
14333
  }
14154
14334
  }
14155
14335
  function renderFlowchartForExport(content, theme, palette) {
@@ -14187,7 +14367,7 @@ function renderFlowchartForExport(content, theme, palette) {
14187
14367
  document.body.removeChild(container);
14188
14368
  }
14189
14369
  }
14190
- var d3Selection8, d3Shape6, DIAGRAM_PADDING8, MAX_SCALE7, NODE_FONT_SIZE2, EDGE_LABEL_FONT_SIZE6, EDGE_STROKE_WIDTH7, NODE_STROKE_WIDTH7, ARROWHEAD_W3, ARROWHEAD_H3, IO_SKEW, SUBROUTINE_INSET, DOC_WAVE_HEIGHT, lineGenerator6;
14370
+ var d3Selection8, d3Shape6, DIAGRAM_PADDING8, MAX_SCALE7, NODE_FONT_SIZE3, EDGE_LABEL_FONT_SIZE6, EDGE_STROKE_WIDTH7, NODE_STROKE_WIDTH7, ARROWHEAD_W3, ARROWHEAD_H3, IO_SKEW, SUBROUTINE_INSET, DOC_WAVE_HEIGHT, lineGenerator6;
14191
14371
  var init_flowchart_renderer = __esm({
14192
14372
  "src/graph/flowchart-renderer.ts"() {
14193
14373
  "use strict";
@@ -14199,7 +14379,7 @@ var init_flowchart_renderer = __esm({
14199
14379
  init_layout7();
14200
14380
  DIAGRAM_PADDING8 = 20;
14201
14381
  MAX_SCALE7 = 3;
14202
- NODE_FONT_SIZE2 = 13;
14382
+ NODE_FONT_SIZE3 = 13;
14203
14383
  EDGE_LABEL_FONT_SIZE6 = 11;
14204
14384
  EDGE_STROKE_WIDTH7 = 1.5;
14205
14385
  NODE_STROKE_WIDTH7 = 1.5;
@@ -14387,23 +14567,26 @@ function collapseGroups(parsed, collapsedIds, defaultLatencyMs = 0, defaultUptim
14387
14567
  if (!collapsedIds.has(group.id)) continue;
14388
14568
  const children = groupChildren.get(group.id) ?? [];
14389
14569
  if (children.length === 0) continue;
14390
- let totalLatency = 0;
14391
14570
  let minEffectiveCapacity = Infinity;
14392
14571
  let hasMaxRps = false;
14393
14572
  let composedUptime = 1;
14394
14573
  const behaviorProps = [];
14395
14574
  const perChildCapacities = [];
14575
+ const childIdSet = new Set(children.map((c) => c.id));
14576
+ const childLatencies = /* @__PURE__ */ new Map();
14396
14577
  for (const child of children) {
14397
14578
  const latencyProp = child.properties.find((p) => p.key === "latency-ms");
14398
14579
  const childIsServerless = child.properties.some((p) => p.key === "concurrency");
14580
+ let childLat;
14399
14581
  if (childIsServerless) {
14400
14582
  const durationProp = child.properties.find((p) => p.key === "duration-ms");
14401
- totalLatency += durationProp ? typeof durationProp.value === "number" ? durationProp.value : parseFloat(String(durationProp.value)) || 100 : 100;
14583
+ childLat = durationProp ? typeof durationProp.value === "number" ? durationProp.value : parseFloat(String(durationProp.value)) || 100 : 100;
14402
14584
  } else if (latencyProp) {
14403
- totalLatency += typeof latencyProp.value === "number" ? latencyProp.value : parseFloat(String(latencyProp.value)) || 0;
14585
+ childLat = typeof latencyProp.value === "number" ? latencyProp.value : parseFloat(String(latencyProp.value)) || 0;
14404
14586
  } else {
14405
- totalLatency += defaultLatencyMs;
14587
+ childLat = defaultLatencyMs;
14406
14588
  }
14589
+ childLatencies.set(child.id, childLat);
14407
14590
  const maxRps = child.properties.find((p) => p.key === "max-rps");
14408
14591
  if (maxRps) {
14409
14592
  hasMaxRps = true;
@@ -14434,6 +14617,59 @@ function collapseGroups(parsed, collapsedIds, defaultLatencyMs = 0, defaultUptim
14434
14617
  }
14435
14618
  }
14436
14619
  }
14620
+ const entryIds = /* @__PURE__ */ new Set();
14621
+ const exitIds = /* @__PURE__ */ new Set();
14622
+ for (const edge of inboundEdges) {
14623
+ if (childIdSet.has(edge.targetId)) entryIds.add(edge.targetId);
14624
+ }
14625
+ for (const edge of outboundEdges) {
14626
+ if (childIdSet.has(edge.sourceId)) exitIds.add(edge.sourceId);
14627
+ }
14628
+ for (const edge of crossGroupEdges) {
14629
+ if (childIdSet.has(edge.sourceId)) exitIds.add(edge.sourceId);
14630
+ if (childIdSet.has(edge.targetId)) entryIds.add(edge.targetId);
14631
+ }
14632
+ const fwdAdj = /* @__PURE__ */ new Map();
14633
+ for (const edge of internalEdges) {
14634
+ if (!childIdSet.has(edge.sourceId) || !childIdSet.has(edge.targetId)) continue;
14635
+ const list = fwdAdj.get(edge.sourceId) ?? [];
14636
+ list.push(edge.targetId);
14637
+ fwdAdj.set(edge.sourceId, list);
14638
+ }
14639
+ const topoOrder = [];
14640
+ const tsVisited = /* @__PURE__ */ new Set();
14641
+ const dfsTopoSort = (id) => {
14642
+ if (tsVisited.has(id)) return;
14643
+ tsVisited.add(id);
14644
+ for (const next of fwdAdj.get(id) ?? []) dfsTopoSort(next);
14645
+ topoOrder.unshift(id);
14646
+ };
14647
+ for (const child of children) dfsTopoSort(child.id);
14648
+ const dist = /* @__PURE__ */ new Map();
14649
+ for (const child of children) {
14650
+ dist.set(child.id, entryIds.has(child.id) ? childLatencies.get(child.id) ?? 0 : -Infinity);
14651
+ }
14652
+ for (const nodeId3 of topoOrder) {
14653
+ const curDist = dist.get(nodeId3) ?? -Infinity;
14654
+ if (curDist === -Infinity) continue;
14655
+ for (const nextId of fwdAdj.get(nodeId3) ?? []) {
14656
+ const newDist = curDist + (childLatencies.get(nextId) ?? 0);
14657
+ if (newDist > (dist.get(nextId) ?? -Infinity)) dist.set(nextId, newDist);
14658
+ }
14659
+ }
14660
+ let totalLatency = 0;
14661
+ if (exitIds.size > 0) {
14662
+ for (const id of exitIds) {
14663
+ const d = dist.get(id);
14664
+ if (d !== void 0 && d > -Infinity && d > totalLatency) totalLatency = d;
14665
+ }
14666
+ } else if (entryIds.size > 0) {
14667
+ for (const [, d] of dist) {
14668
+ if (d > 0 && d > totalLatency) totalLatency = d;
14669
+ }
14670
+ } else {
14671
+ for (const [, lat] of childLatencies) totalLatency += lat;
14672
+ }
14437
14673
  const props = [];
14438
14674
  if (totalLatency > 0) props.push({ key: "latency-ms", value: totalLatency, lineNumber: group.lineNumber });
14439
14675
  const groupInstances = typeof group.instances === "number" ? group.instances : 1;
@@ -14666,8 +14902,10 @@ function computeInfra(parsed, params = {}) {
14666
14902
  const resolved = resolveSplits(outbound, diagnostics);
14667
14903
  for (const { edge, split } of resolved) {
14668
14904
  const edgeRps = outboundRps * (split / 100);
14905
+ const fanout = edge.fanout != null && edge.fanout >= 1 ? edge.fanout : 1;
14906
+ const fanoutedRps = edgeRps * fanout;
14669
14907
  const edgeKey = `${edge.sourceId}->${edge.targetId}`;
14670
- computedEdgeRps.set(edgeKey, edgeRps);
14908
+ computedEdgeRps.set(edgeKey, fanoutedRps);
14671
14909
  let targetIds;
14672
14910
  const groupChildren = groupChildMap.get(edge.targetId);
14673
14911
  if (groupChildren && groupChildren.length > 0) {
@@ -14676,7 +14914,7 @@ function computeInfra(parsed, params = {}) {
14676
14914
  targetIds = [edge.targetId];
14677
14915
  }
14678
14916
  for (const targetId of targetIds) {
14679
- const perTarget = edgeRps / targetIds.length;
14917
+ const perTarget = fanoutedRps / targetIds.length;
14680
14918
  const existing = computedRps.get(targetId) ?? 0;
14681
14919
  computedRps.set(targetId, existing + perTarget);
14682
14920
  const prevLatency = computedLatency.get(targetId) ?? 0;
@@ -14772,6 +15010,7 @@ function computeInfra(parsed, params = {}) {
14772
15010
  const resolved = resolveSplits(outbound, []);
14773
15011
  const paths = [];
14774
15012
  for (const { edge, split } of resolved) {
15013
+ const fanout = edge.fanout != null && edge.fanout >= 1 ? edge.fanout : 1;
14775
15014
  const groupChildren = groupChildMap.get(edge.targetId);
14776
15015
  const targetIds = groupChildren && groupChildren.length > 0 ? groupChildren : [edge.targetId];
14777
15016
  for (const targetId of targetIds) {
@@ -14782,20 +15021,20 @@ function computeInfra(parsed, params = {}) {
14782
15021
  latency: nodeLatency + cp.latency,
14783
15022
  uptime: nodeUptimeFrac * cp.uptime,
14784
15023
  availability: nodeAvail * cp.availability,
14785
- weight: cp.weight * (split / 100) / targetIds.length * 0.95
15024
+ weight: cp.weight * (split / 100) / targetIds.length * fanout * 0.95
14786
15025
  });
14787
15026
  paths.push({
14788
15027
  latency: coldLatency + cp.latency,
14789
15028
  uptime: nodeUptimeFrac * cp.uptime,
14790
15029
  availability: nodeAvail * cp.availability,
14791
- weight: cp.weight * (split / 100) / targetIds.length * 0.05
15030
+ weight: cp.weight * (split / 100) / targetIds.length * fanout * 0.05
14792
15031
  });
14793
15032
  } else {
14794
15033
  paths.push({
14795
15034
  latency: nodeLatency + cp.latency,
14796
15035
  uptime: nodeUptimeFrac * cp.uptime,
14797
15036
  availability: nodeAvail * cp.availability,
14798
- weight: cp.weight * (split / 100) / targetIds.length
15037
+ weight: cp.weight * (split / 100) / targetIds.length * fanout
14799
15038
  });
14800
15039
  }
14801
15040
  }
@@ -14950,6 +15189,7 @@ function computeInfra(parsed, params = {}) {
14950
15189
  queueMetrics,
14951
15190
  properties: node.properties,
14952
15191
  tags: node.tags,
15192
+ description: node.description,
14953
15193
  lineNumber: node.lineNumber
14954
15194
  };
14955
15195
  });
@@ -14974,6 +15214,7 @@ function computeInfra(parsed, params = {}) {
14974
15214
  label: edge.label,
14975
15215
  computedRps: rps,
14976
15216
  split: resolvedSplit,
15217
+ fanout: edge.fanout,
14977
15218
  lineNumber: edge.lineNumber
14978
15219
  };
14979
15220
  });
@@ -15000,7 +15241,8 @@ var init_compute = __esm({
15000
15241
  // src/infra/layout.ts
15001
15242
  var layout_exports8 = {};
15002
15243
  __export(layout_exports8, {
15003
- layoutInfra: () => layoutInfra
15244
+ layoutInfra: () => layoutInfra,
15245
+ separateGroups: () => separateGroups
15004
15246
  });
15005
15247
  function countDisplayProps(node, expanded, options) {
15006
15248
  if (!expanded) return 0;
@@ -15066,7 +15308,7 @@ function computeNodeWidth2(node, expanded, options) {
15066
15308
  if (expanded) {
15067
15309
  allKeys.push("p50", "p90", "p99");
15068
15310
  } else {
15069
- allKeys.push("p99");
15311
+ allKeys.push("p90");
15070
15312
  }
15071
15313
  }
15072
15314
  if (node.computedUptime < 1) {
@@ -15108,13 +15350,21 @@ function computeNodeWidth2(node, expanded, options) {
15108
15350
  }
15109
15351
  if (computedRows > 0) {
15110
15352
  const perc = node.computedLatencyPercentiles;
15111
- const msValues = expanded ? [perc.p50, perc.p90, perc.p99] : [perc.p99];
15353
+ const msValues = expanded ? [perc.p50, perc.p90, perc.p99] : [perc.p90];
15112
15354
  for (const ms of msValues) {
15113
15355
  if (ms > 0) {
15114
15356
  const valLen = formatMs(ms).length;
15115
15357
  maxRowWidth = Math.max(maxRowWidth, (maxKeyLen + 2 + valLen) * META_CHAR_WIDTH3);
15116
15358
  }
15117
15359
  }
15360
+ if (perc.p90 > 0) {
15361
+ const rawThreshold = node.properties.find((p) => p.key === "slo-p90-latency-ms")?.value ?? options?.["slo-p90-latency-ms"];
15362
+ const threshold = rawThreshold != null ? parseFloat(String(rawThreshold)) : NaN;
15363
+ if (!isNaN(threshold) && threshold > 0) {
15364
+ const combinedVal = `${formatMs(perc.p90)} / ${formatMs(threshold)}`;
15365
+ maxRowWidth = Math.max(maxRowWidth, (maxKeyLen + 2 + combinedVal.length) * META_CHAR_WIDTH3);
15366
+ }
15367
+ }
15118
15368
  if (node.computedUptime < 1) {
15119
15369
  const valLen = formatUptime(node.computedUptime).length;
15120
15370
  maxRowWidth = Math.max(maxRowWidth, (maxKeyLen + 2 + valLen) * META_CHAR_WIDTH3);
@@ -15127,21 +15377,26 @@ function computeNodeWidth2(node, expanded, options) {
15127
15377
  maxRowWidth = Math.max(maxRowWidth, "CB: OPEN".length * META_CHAR_WIDTH3 + 8);
15128
15378
  }
15129
15379
  }
15130
- return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20);
15380
+ const DESC_MAX_CHARS2 = 120;
15381
+ const descText = expanded && node.description && !node.isEdge ? node.description : "";
15382
+ const descTruncated = descText.length > DESC_MAX_CHARS2 ? descText.slice(0, DESC_MAX_CHARS2 - 1) + "\u2026" : descText;
15383
+ const descWidth = descTruncated.length > 0 ? descTruncated.length * META_CHAR_WIDTH3 + PADDING_X3 : 0;
15384
+ return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20, descWidth);
15131
15385
  }
15132
15386
  function computeNodeHeight2(node, expanded, options) {
15133
15387
  const propCount = countDisplayProps(node, expanded, options);
15134
15388
  const computedCount = countComputedRows(node, expanded);
15135
15389
  const hasRps = node.computedRps > 0;
15136
- if (propCount === 0 && computedCount === 0 && !hasRps) return NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM;
15137
- let h = NODE_HEADER_HEIGHT + NODE_SEPARATOR_GAP;
15390
+ const descH = expanded && node.description && !node.isEdge ? META_LINE_HEIGHT7 : 0;
15391
+ if (propCount === 0 && computedCount === 0 && !hasRps) return NODE_HEADER_HEIGHT + descH + NODE_PAD_BOTTOM;
15392
+ let h = NODE_HEADER_HEIGHT + descH + NODE_SEPARATOR_GAP;
15138
15393
  const computedSectionCount = (hasRps ? 1 : 0) + computedCount;
15139
15394
  h += computedSectionCount * META_LINE_HEIGHT7;
15140
15395
  if (computedSectionCount > 0 && propCount > 0) h += NODE_SEPARATOR_GAP;
15141
15396
  h += propCount * META_LINE_HEIGHT7;
15142
15397
  if (hasRoles(node)) h += ROLE_DOT_ROW;
15143
15398
  h += NODE_PAD_BOTTOM;
15144
- if (node.id.startsWith("[")) h += COLLAPSE_BAR_HEIGHT3;
15399
+ if (node.id.startsWith("[")) h += COLLAPSE_BAR_HEIGHT4;
15145
15400
  return h;
15146
15401
  }
15147
15402
  function formatRps(rps) {
@@ -15168,7 +15423,36 @@ function formatUptime(fraction) {
15168
15423
  if (pct >= 99) return `${pct.toFixed(1)}%`;
15169
15424
  return `${pct.toFixed(1)}%`;
15170
15425
  }
15171
- function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15426
+ function separateGroups(groups, nodes, isLR, maxIterations = 20) {
15427
+ for (let iter = 0; iter < maxIterations; iter++) {
15428
+ let anyOverlap = false;
15429
+ for (let i = 0; i < groups.length; i++) {
15430
+ for (let j = i + 1; j < groups.length; j++) {
15431
+ const ga = groups[i];
15432
+ const gb = groups[j];
15433
+ const primaryOverlap = isLR ? Math.min(ga.y + ga.height, gb.y + gb.height) - Math.max(ga.y, gb.y) : Math.min(ga.x + ga.width, gb.x + gb.width) - Math.max(ga.x, gb.x);
15434
+ if (primaryOverlap <= 0) continue;
15435
+ const crossOverlap = isLR ? Math.min(ga.x + ga.width, gb.x + gb.width) - Math.max(ga.x, gb.x) : Math.min(ga.y + ga.height, gb.y + gb.height) - Math.max(ga.y, gb.y);
15436
+ if (crossOverlap <= 0) continue;
15437
+ anyOverlap = true;
15438
+ const shift = primaryOverlap + GROUP_GAP;
15439
+ const aCenter = isLR ? ga.y + ga.height / 2 : ga.x + ga.width / 2;
15440
+ const bCenter = isLR ? gb.y + gb.height / 2 : gb.x + gb.width / 2;
15441
+ const groupToShift = aCenter <= bCenter ? gb : ga;
15442
+ if (isLR) groupToShift.y += shift;
15443
+ else groupToShift.x += shift;
15444
+ for (const node of nodes) {
15445
+ if (node.groupId === groupToShift.id) {
15446
+ if (isLR) node.y += shift;
15447
+ else node.x += shift;
15448
+ }
15449
+ }
15450
+ }
15451
+ }
15452
+ if (!anyOverlap) break;
15453
+ }
15454
+ }
15455
+ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15172
15456
  if (computed.nodes.length === 0) {
15173
15457
  return { nodes: [], edges: [], groups: [], options: {}, width: 0, height: 0 };
15174
15458
  }
@@ -15190,7 +15474,7 @@ function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15190
15474
  const heightMap = /* @__PURE__ */ new Map();
15191
15475
  for (const node of computed.nodes) {
15192
15476
  const isNodeCollapsed = collapsedNodes?.has(node.id) ?? false;
15193
- const expanded = !isNodeCollapsed && node.id === selectedNodeId;
15477
+ const expanded = !isNodeCollapsed && (expandedNodeIds?.has(node.id) ?? false);
15194
15478
  const width = computeNodeWidth2(node, expanded, computed.options);
15195
15479
  const height = isNodeCollapsed ? NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM : computeNodeHeight2(node, expanded, computed.options);
15196
15480
  widthMap.set(node.id, width);
@@ -15251,6 +15535,7 @@ function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15251
15535
  queueMetrics: node.queueMetrics,
15252
15536
  properties: node.properties,
15253
15537
  tags: node.tags,
15538
+ description: node.description,
15254
15539
  lineNumber: node.lineNumber
15255
15540
  };
15256
15541
  });
@@ -15265,6 +15550,7 @@ function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15265
15550
  label: edge.label,
15266
15551
  computedRps: edge.computedRps,
15267
15552
  split: edge.split,
15553
+ fanout: edge.fanout,
15268
15554
  points: edgeData?.points ?? [],
15269
15555
  lineNumber: edge.lineNumber
15270
15556
  });
@@ -15276,6 +15562,7 @@ function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15276
15562
  label: edge.label,
15277
15563
  computedRps: edge.computedRps,
15278
15564
  split: edge.split,
15565
+ fanout: edge.fanout,
15279
15566
  points: edgeData?.points ?? [],
15280
15567
  lineNumber: edge.lineNumber
15281
15568
  });
@@ -15317,6 +15604,7 @@ function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15317
15604
  lineNumber: group.lineNumber
15318
15605
  };
15319
15606
  });
15607
+ separateGroups(layoutGroups, layoutNodes, isLR);
15320
15608
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
15321
15609
  for (const node of layoutNodes) {
15322
15610
  const left = node.x - node.width / 2;
@@ -15378,7 +15666,7 @@ function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15378
15666
  height: totalHeight
15379
15667
  };
15380
15668
  }
15381
- var import_dagre7, MIN_NODE_WIDTH2, NODE_HEADER_HEIGHT, META_LINE_HEIGHT7, NODE_SEPARATOR_GAP, NODE_PAD_BOTTOM, ROLE_DOT_ROW, COLLAPSE_BAR_HEIGHT3, CHAR_WIDTH6, META_CHAR_WIDTH3, PADDING_X3, GROUP_PADDING3, GROUP_HEADER_HEIGHT, EDGE_MARGIN, DISPLAY_KEYS, DISPLAY_NAMES;
15669
+ var import_dagre7, MIN_NODE_WIDTH2, NODE_HEADER_HEIGHT, META_LINE_HEIGHT7, NODE_SEPARATOR_GAP, NODE_PAD_BOTTOM, ROLE_DOT_ROW, COLLAPSE_BAR_HEIGHT4, CHAR_WIDTH6, META_CHAR_WIDTH3, PADDING_X3, GROUP_PADDING3, GROUP_HEADER_HEIGHT, EDGE_MARGIN, DISPLAY_KEYS, DISPLAY_NAMES, GROUP_GAP;
15382
15670
  var init_layout8 = __esm({
15383
15671
  "src/infra/layout.ts"() {
15384
15672
  "use strict";
@@ -15389,7 +15677,7 @@ var init_layout8 = __esm({
15389
15677
  NODE_SEPARATOR_GAP = 4;
15390
15678
  NODE_PAD_BOTTOM = 10;
15391
15679
  ROLE_DOT_ROW = 12;
15392
- COLLAPSE_BAR_HEIGHT3 = 6;
15680
+ COLLAPSE_BAR_HEIGHT4 = 6;
15393
15681
  CHAR_WIDTH6 = 7;
15394
15682
  META_CHAR_WIDTH3 = 6;
15395
15683
  PADDING_X3 = 24;
@@ -15417,11 +15705,11 @@ var init_layout8 = __esm({
15417
15705
  DISPLAY_NAMES = {
15418
15706
  "cache-hit": "cache hit",
15419
15707
  "firewall-block": "fw block",
15420
- "ratelimit-rps": "rate limit",
15708
+ "ratelimit-rps": "rate limit RPS",
15421
15709
  "latency-ms": "latency",
15422
15710
  "uptime": "uptime",
15423
15711
  "instances": "instances",
15424
- "max-rps": "capacity",
15712
+ "max-rps": "max RPS",
15425
15713
  "cb-error-threshold": "CB error",
15426
15714
  "cb-latency-threshold-ms": "CB latency",
15427
15715
  "concurrency": "concurrency",
@@ -15432,6 +15720,7 @@ var init_layout8 = __esm({
15432
15720
  "retention-hours": "retention",
15433
15721
  "partitions": "partitions"
15434
15722
  };
15723
+ GROUP_GAP = 24;
15435
15724
  }
15436
15725
  });
15437
15726
 
@@ -15446,6 +15735,13 @@ function inferRoles(properties) {
15446
15735
  }
15447
15736
  return roles;
15448
15737
  }
15738
+ function collectFanoutSourceIds(edges) {
15739
+ const ids = /* @__PURE__ */ new Set();
15740
+ for (const e of edges) {
15741
+ if (e.fanout != null) ids.add(e.sourceId);
15742
+ }
15743
+ return ids;
15744
+ }
15449
15745
  function collectDiagramRoles(allProperties) {
15450
15746
  const seen = /* @__PURE__ */ new Set();
15451
15747
  const roles = [];
@@ -15459,7 +15755,7 @@ function collectDiagramRoles(allProperties) {
15459
15755
  }
15460
15756
  return roles;
15461
15757
  }
15462
- var ROLE_RULES;
15758
+ var ROLE_RULES, FANOUT_ROLE;
15463
15759
  var init_roles = __esm({
15464
15760
  "src/infra/roles.ts"() {
15465
15761
  "use strict";
@@ -15472,6 +15768,7 @@ var init_roles = __esm({
15472
15768
  { keys: ["concurrency"], role: { name: "Serverless", color: "#06b6d4" } },
15473
15769
  { keys: ["buffer"], role: { name: "Queue", color: "#8b5cf6" } }
15474
15770
  ];
15771
+ FANOUT_ROLE = { name: "Fan-Out", color: "#f97316" };
15475
15772
  }
15476
15773
  });
15477
15774
 
@@ -15482,6 +15779,20 @@ __export(renderer_exports8, {
15482
15779
  parseAndLayoutInfra: () => parseAndLayoutInfra,
15483
15780
  renderInfra: () => renderInfra
15484
15781
  });
15782
+ function resolveNodeSlo(node, diagramOptions) {
15783
+ const nodeProp = (key) => node.properties.find((p) => p.key === key);
15784
+ const availRaw = nodeProp("slo-availability")?.value ?? diagramOptions["slo-availability"];
15785
+ const latencyRaw = nodeProp("slo-p90-latency-ms")?.value ?? diagramOptions["slo-p90-latency-ms"];
15786
+ const marginRaw = nodeProp("slo-warning-margin")?.value ?? diagramOptions["slo-warning-margin"];
15787
+ const availParsed = availRaw != null ? parseFloat(String(availRaw).replace("%", "")) / 100 : NaN;
15788
+ const availThreshold = !isNaN(availParsed) ? availParsed : null;
15789
+ const latencyParsed = latencyRaw != null ? parseFloat(String(latencyRaw)) : NaN;
15790
+ const latencyP90 = !isNaN(latencyParsed) ? latencyParsed : null;
15791
+ const marginParsed = marginRaw != null ? parseFloat(String(marginRaw).replace("%", "")) / 100 : NaN;
15792
+ const warningMargin = !isNaN(marginParsed) ? marginParsed : 0.05;
15793
+ if (availThreshold == null && latencyP90 == null) return null;
15794
+ return { availThreshold, latencyP90, warningMargin };
15795
+ }
15485
15796
  function nodeBorderPoint(node, target) {
15486
15797
  const hw = node.width / 2;
15487
15798
  const hh = node.height / 2;
@@ -15518,7 +15829,17 @@ function isWarning(node) {
15518
15829
  const cap = Number(maxRps.value) * node.computedInstances;
15519
15830
  return cap > 0 && node.computedRps / cap > 0.7;
15520
15831
  }
15521
- function getComputedRows(node, expanded) {
15832
+ function truncateDesc(text) {
15833
+ if (text.length <= DESC_MAX_CHARS) return text;
15834
+ return text.slice(0, DESC_MAX_CHARS - 1) + "\u2026";
15835
+ }
15836
+ function sloLatencyColor(p90, slo) {
15837
+ const t = slo.latencyP90 ?? 0;
15838
+ if (t === 0) return COLOR_HEALTHY;
15839
+ const m = slo.warningMargin;
15840
+ return p90 > t ? COLOR_OVERLOADED : p90 > t * (1 - m) ? COLOR_WARNING : COLOR_HEALTHY;
15841
+ }
15842
+ function getComputedRows(node, expanded, slo) {
15522
15843
  const rows = [];
15523
15844
  if (node.computedConcurrentInvocations > 0) {
15524
15845
  const concurrency = getNodeNumProp(node, "concurrency", 0);
@@ -15532,9 +15853,23 @@ function getComputedRows(node, expanded) {
15532
15853
  if (p.p50 > 0 || p.p90 > 0 || p.p99 > 0) {
15533
15854
  if (expanded) {
15534
15855
  rows.push({ key: "p50", value: formatMsShort(p.p50) });
15535
- rows.push({ key: "p90", value: formatMsShort(p.p90) });
15856
+ if (slo?.latencyP90 != null) {
15857
+ const color = sloLatencyColor(p.p90, slo);
15858
+ const p90Value = color !== COLOR_HEALTHY ? `${formatMsShort(p.p90)} / ${formatMsShort(slo.latencyP90)}` : formatMsShort(p.p90);
15859
+ rows.push({ key: "p90", value: p90Value, color, inverted: color !== COLOR_HEALTHY });
15860
+ } else {
15861
+ rows.push({ key: "p90", value: formatMsShort(p.p90) });
15862
+ }
15863
+ rows.push({ key: "p99", value: formatMsShort(p.p99) });
15864
+ } else if (p.p90 > 0) {
15865
+ if (slo?.latencyP90 != null) {
15866
+ const color = sloLatencyColor(p.p90, slo);
15867
+ const p90Value = color !== COLOR_HEALTHY ? `${formatMsShort(p.p90)} / ${formatMsShort(slo.latencyP90)}` : formatMsShort(p.p90);
15868
+ rows.push({ key: "p90", value: p90Value, color, inverted: color !== COLOR_HEALTHY });
15869
+ } else {
15870
+ rows.push({ key: "p90", value: formatMsShort(p.p90) });
15871
+ }
15536
15872
  }
15537
- rows.push({ key: "p99", value: formatMsShort(p.p99) });
15538
15873
  }
15539
15874
  if (node.computedUptime < 1) {
15540
15875
  const declaredUptime = node.properties.find((p2) => p2.key === "uptime");
@@ -15545,8 +15880,17 @@ function getComputedRows(node, expanded) {
15545
15880
  }
15546
15881
  }
15547
15882
  if (node.computedAvailability < 1) {
15548
- const color = node.computedAvailability < 0.95 ? COLOR_OVERLOADED : node.computedAvailability < 0.99 ? COLOR_WARNING : void 0;
15549
- rows.push({ key: "availability", value: formatUptimeShort(node.computedAvailability), color, inverted: color != null });
15883
+ let color;
15884
+ if (slo?.availThreshold != null) {
15885
+ const t = slo.availThreshold;
15886
+ const m = slo.warningMargin;
15887
+ if (node.computedAvailability < t) color = COLOR_OVERLOADED;
15888
+ else if (node.computedAvailability < Math.min(1, t + m)) color = COLOR_WARNING;
15889
+ else color = COLOR_HEALTHY;
15890
+ } else {
15891
+ color = node.computedAvailability < 0.95 ? COLOR_OVERLOADED : node.computedAvailability < 0.99 ? COLOR_WARNING : void 0;
15892
+ }
15893
+ rows.push({ key: "availability", value: formatUptimeShort(node.computedAvailability), color, inverted: color != null && color !== COLOR_HEALTHY });
15550
15894
  }
15551
15895
  if (node.computedCbState === "open") {
15552
15896
  rows.push({ key: "CB", value: "OPEN", color: COLOR_OVERLOADED, inverted: true });
@@ -15634,7 +15978,7 @@ function formatRpsShort2(rps) {
15634
15978
  if (rps >= 1e3) return `${(rps / 1e3).toFixed(1)}k`;
15635
15979
  return `${Math.round(rps)}`;
15636
15980
  }
15637
- function worstNodeSeverity(node) {
15981
+ function worstNodeSeverity(node, slo) {
15638
15982
  let worst = "normal";
15639
15983
  const upgrade = (s) => {
15640
15984
  if (s === "overloaded") worst = "overloaded";
@@ -15645,8 +15989,22 @@ function worstNodeSeverity(node) {
15645
15989
  if (node.overloaded) upgrade("overloaded");
15646
15990
  if (node.rateLimited) upgrade("warning");
15647
15991
  if (isWarning(node)) upgrade("warning");
15648
- if (node.computedAvailability < 0.95) upgrade("overloaded");
15649
- else if (node.computedAvailability < 0.99) upgrade("warning");
15992
+ if (slo?.availThreshold != null) {
15993
+ const t = slo.availThreshold;
15994
+ const m = slo.warningMargin;
15995
+ if (node.computedAvailability < t) upgrade("overloaded");
15996
+ else if (node.computedAvailability < Math.min(1, t + m)) upgrade("warning");
15997
+ } else {
15998
+ if (node.computedAvailability < 0.95) upgrade("overloaded");
15999
+ else if (node.computedAvailability < 0.99) upgrade("warning");
16000
+ }
16001
+ if (slo?.latencyP90 != null) {
16002
+ const t = slo.latencyP90;
16003
+ const m = slo.warningMargin;
16004
+ const p90 = node.computedLatencyPercentiles.p90;
16005
+ if (p90 > t) upgrade("overloaded");
16006
+ else if (p90 > t * (1 - m)) upgrade("warning");
16007
+ }
15650
16008
  if (node.computedCbState === "open") upgrade("overloaded");
15651
16009
  if (node.queueMetrics && node.queueMetrics.fillRate > 0) {
15652
16010
  if (node.queueMetrics.timeToOverflow < 60) upgrade("overloaded");
@@ -15664,10 +16022,14 @@ function worstNodeSeverity(node) {
15664
16022
  else if (preRl > nodeRateLimit * 0.8) upgrade("warning");
15665
16023
  }
15666
16024
  }
16025
+ if (worst === "normal" && slo != null) {
16026
+ const availGreen = slo.availThreshold == null || node.computedAvailability >= Math.min(1, slo.availThreshold + slo.warningMargin);
16027
+ const latencyGreen = slo.latencyP90 == null || node.computedLatencyPercentiles.p90 <= slo.latencyP90 * (1 - slo.warningMargin);
16028
+ if (availGreen && latencyGreen) return "healthy";
16029
+ }
15667
16030
  return worst;
15668
16031
  }
15669
- function nodeColor(node, palette, isDark) {
15670
- const severity = worstNodeSeverity(node);
16032
+ function nodeColor(_node, palette, isDark, severity) {
15671
16033
  if (severity === "overloaded") {
15672
16034
  return {
15673
16035
  fill: mix(palette.bg, COLOR_OVERLOADED, isDark ? 80 : 92),
@@ -15682,6 +16044,13 @@ function nodeColor(node, palette, isDark) {
15682
16044
  textFill: palette.text
15683
16045
  };
15684
16046
  }
16047
+ if (severity === "healthy") {
16048
+ return {
16049
+ fill: mix(palette.bg, COLOR_HEALTHY, isDark ? 85 : 93),
16050
+ stroke: COLOR_HEALTHY,
16051
+ textFill: palette.text
16052
+ };
16053
+ }
15685
16054
  return {
15686
16055
  fill: isDark ? mix(palette.bg, palette.text, 90) : mix(palette.bg, palette.text, 95),
15687
16056
  stroke: isDark ? mix(palette.text, palette.bg, 60) : mix(palette.text, palette.bg, 40),
@@ -15775,10 +16144,12 @@ function resolveActiveTagStroke(node, activeGroup, tagGroups, palette) {
15775
16144
  if (!tv?.color) return null;
15776
16145
  return resolveColor(tv.color, palette);
15777
16146
  }
15778
- function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activeGroup, diagramOptions, collapsedNodes, tagGroups) {
16147
+ function renderNodes(svg, nodes, palette, isDark, animate, expandedNodeIds, activeGroup, diagramOptions, collapsedNodes, tagGroups, fanoutSourceIds, scaledGroupIds) {
15779
16148
  const mutedColor = palette.textMuted;
15780
16149
  for (const node of nodes) {
15781
- let { fill: fill2, stroke: stroke2, textFill } = nodeColor(node, palette, isDark);
16150
+ const slo = !node.isEdge && diagramOptions ? resolveNodeSlo(node, diagramOptions) : null;
16151
+ const severity = worstNodeSeverity(node, slo);
16152
+ let { fill: fill2, stroke: stroke2, textFill } = nodeColor(node, palette, isDark, severity);
15782
16153
  if (activeGroup && tagGroups && !node.isEdge) {
15783
16154
  const tagStroke = resolveActiveTagStroke(node, activeGroup, tagGroups, palette);
15784
16155
  if (tagStroke) {
@@ -15790,7 +16161,6 @@ function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activ
15790
16161
  if (animate && node.isEdge) {
15791
16162
  cls += " infra-node-edge-throb";
15792
16163
  } else if (animate && !node.isEdge) {
15793
- const severity = worstNodeSeverity(node);
15794
16164
  if (node.computedCbState === "open") cls += " infra-node-cb-open";
15795
16165
  else if (severity === "overloaded") cls += " infra-node-overload";
15796
16166
  else if (severity === "warning") cls += " infra-node-warning";
@@ -15807,26 +16177,36 @@ function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activ
15807
16177
  for (const role of roles) {
15808
16178
  g.attr(`data-role-${role.name.toLowerCase().replace(/\s+/g, "-")}`, "true");
15809
16179
  }
16180
+ if (fanoutSourceIds?.has(node.id)) {
16181
+ g.attr("data-role-fan-out", "true");
16182
+ }
15810
16183
  }
15811
16184
  const x = node.x - node.width / 2;
15812
16185
  const y = node.y - node.height / 2;
15813
16186
  const isCollapsedGroup = node.id.startsWith("[");
15814
- const strokeWidth = worstNodeSeverity(node) !== "normal" ? OVERLOAD_STROKE_WIDTH : NODE_STROKE_WIDTH8;
16187
+ const strokeWidth = severity !== "normal" ? OVERLOAD_STROKE_WIDTH : NODE_STROKE_WIDTH8;
15815
16188
  g.append("rect").attr("x", x).attr("y", y).attr("width", node.width).attr("height", node.height).attr("rx", NODE_BORDER_RADIUS).attr("fill", fill2).attr("stroke", stroke2).attr("stroke-width", strokeWidth);
15816
- const headerCenterY = y + NODE_HEADER_HEIGHT2 / 2 + NODE_FONT_SIZE3 * 0.35;
15817
- g.append("text").attr("x", node.x).attr("y", headerCenterY).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", NODE_FONT_SIZE3).attr("font-weight", "600").attr("fill", textFill).text(node.label);
16189
+ const headerCenterY = y + NODE_HEADER_HEIGHT2 / 2 + NODE_FONT_SIZE4 * 0.35;
16190
+ g.append("text").attr("x", node.x).attr("y", headerCenterY).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", NODE_FONT_SIZE4).attr("font-weight", "600").attr("fill", textFill).text(node.label);
15818
16191
  const isNodeCollapsed = collapsedNodes?.has(node.id) ?? false;
15819
16192
  if (isNodeCollapsed) {
15820
16193
  const chevronY = y + node.height - 6;
15821
16194
  g.append("text").attr("x", node.x).attr("y", chevronY).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", 8).attr("fill", textFill).attr("opacity", 0.5).text("\u25BC");
15822
16195
  }
15823
16196
  if (!isNodeCollapsed) {
15824
- const expanded = node.id === selectedNodeId;
16197
+ const expanded = expandedNodeIds?.has(node.id) ?? false;
16198
+ const descH = expanded && node.description && !node.isEdge ? META_LINE_HEIGHT8 : 0;
16199
+ if (descH > 0 && node.description) {
16200
+ const descTruncated = truncateDesc(node.description);
16201
+ const isTruncated = descTruncated !== node.description;
16202
+ const textEl = g.append("text").attr("x", node.x).attr("y", y + NODE_HEADER_HEIGHT2 + META_LINE_HEIGHT8 / 2 + META_FONT_SIZE4 * 0.35).attr("text-anchor", "middle").attr("font-family", FONT_FAMILY).attr("font-size", META_FONT_SIZE4).attr("fill", mutedColor).text(descTruncated);
16203
+ if (isTruncated) textEl.append("title").text(node.description);
16204
+ }
15825
16205
  const displayProps = !node.isEdge && expanded ? getDisplayProps(node, expanded, diagramOptions) : [];
15826
- const computedRows = getComputedRows(node, expanded);
16206
+ const computedRows = getComputedRows(node, expanded, slo);
15827
16207
  const hasContent = displayProps.length > 0 || computedRows.length > 0 || node.computedRps > 0;
15828
16208
  if (hasContent) {
15829
- const sepY = y + NODE_HEADER_HEIGHT2;
16209
+ const sepY = y + NODE_HEADER_HEIGHT2 + descH;
15830
16210
  g.append("line").attr("x1", x).attr("y1", sepY).attr("x2", x + node.width).attr("y2", sepY).attr("stroke", stroke2).attr("stroke-opacity", 0.3).attr("stroke-width", 1);
15831
16211
  const computedSection = [];
15832
16212
  const declaredSection = [];
@@ -15922,14 +16302,15 @@ function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activ
15922
16302
  rowIdx++;
15923
16303
  }
15924
16304
  }
15925
- if (!node.isEdge && node.computedConcurrentInvocations === 0 && node.computedInstances > 1) {
16305
+ const inScaledGroup = node.groupId != null && (scaledGroupIds?.has(node.groupId) ?? false);
16306
+ if (!node.isEdge && node.computedConcurrentInvocations === 0 && node.computedInstances > 1 && !inScaledGroup) {
15926
16307
  const badgeText = `${node.computedInstances}x`;
15927
16308
  g.append("text").attr("x", x + node.width - 6).attr("y", y + NODE_HEADER_HEIGHT2 / 2 + META_FONT_SIZE4 * 0.35).attr("text-anchor", "end").attr("font-family", FONT_FAMILY).attr("font-size", META_FONT_SIZE4).attr("fill", mutedColor).attr("data-instance-node", node.id).style("cursor", "pointer").text(badgeText);
15928
16309
  }
15929
16310
  const showDots = activeGroup != null && activeGroup.toLowerCase() === "capabilities";
15930
16311
  const roles = showDots && !node.isEdge ? inferRoles(node.properties) : [];
15931
16312
  if (roles.length > 0) {
15932
- const dotY = isCollapsedGroup ? y + node.height - COLLAPSE_BAR_HEIGHT4 - NODE_PAD_BOTTOM2 / 2 : y + node.height - NODE_PAD_BOTTOM2 / 2;
16313
+ const dotY = isCollapsedGroup ? y + node.height - COLLAPSE_BAR_HEIGHT5 - NODE_PAD_BOTTOM2 / 2 : y + node.height - NODE_PAD_BOTTOM2 / 2;
15933
16314
  const totalDotsWidth = roles.length * (ROLE_DOT_RADIUS * 2 + 2) - 2;
15934
16315
  const startX = node.x - totalDotsWidth / 2 + ROLE_DOT_RADIUS;
15935
16316
  for (let i = 0; i < roles.length; i++) {
@@ -15939,7 +16320,7 @@ function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activ
15939
16320
  if (isCollapsedGroup) {
15940
16321
  const clipId = `clip-${node.id.replace(/[[\]\s]/g, "")}`;
15941
16322
  g.append("clipPath").attr("id", clipId).append("rect").attr("x", x).attr("y", y).attr("width", node.width).attr("height", node.height).attr("rx", NODE_BORDER_RADIUS);
15942
- g.append("rect").attr("x", x + COLLAPSE_BAR_INSET2).attr("y", y + node.height - COLLAPSE_BAR_HEIGHT4).attr("width", node.width - COLLAPSE_BAR_INSET2 * 2).attr("height", COLLAPSE_BAR_HEIGHT4).attr("fill", stroke2).attr("clip-path", `url(#${clipId})`).attr("class", "infra-collapse-bar");
16323
+ g.append("rect").attr("x", x + COLLAPSE_BAR_INSET2).attr("y", y + node.height - COLLAPSE_BAR_HEIGHT5).attr("width", node.width - COLLAPSE_BAR_INSET2 * 2).attr("height", COLLAPSE_BAR_HEIGHT5).attr("fill", stroke2).attr("clip-path", `url(#${clipId})`).attr("class", "infra-collapse-bar");
15943
16324
  }
15944
16325
  }
15945
16326
  }
@@ -16008,9 +16389,12 @@ function renderRejectParticles(svg, nodes) {
16008
16389
  }
16009
16390
  }
16010
16391
  }
16011
- function computeInfraLegendGroups(nodes, tagGroups, palette) {
16392
+ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
16012
16393
  const groups = [];
16013
16394
  const roles = collectDiagramRoles(nodes.filter((n) => !n.isEdge).map((n) => n.properties));
16395
+ if (edges && collectFanoutSourceIds(edges).size > 0) {
16396
+ roles.push(FANOUT_ROLE);
16397
+ }
16014
16398
  if (roles.length > 0) {
16015
16399
  const entries = roles.map((r) => ({
16016
16400
  value: r.name,
@@ -16065,7 +16449,7 @@ function computePlaybackWidth(playback) {
16065
16449
  let entriesW = 8;
16066
16450
  entriesW += LEGEND_PILL_FONT_SIZE5 * 0.8 + 6;
16067
16451
  for (const s of playback.speedOptions) {
16068
- entriesW += `${s}x`.length * LEGEND_ENTRY_FONT_W7 + 6;
16452
+ entriesW += `${s}x`.length * LEGEND_ENTRY_FONT_W7 + SPEED_BADGE_H_PAD * 2 + SPEED_BADGE_GAP;
16069
16453
  }
16070
16454
  return LEGEND_CAPSULE_PAD7 * 2 + pillWidth + entriesW;
16071
16455
  }
@@ -16136,16 +16520,21 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
16136
16520
  for (const s of playback.speedOptions) {
16137
16521
  const label = `${s}x`;
16138
16522
  const isActive = playback.speed === s;
16139
- pbG.append("text").attr("x", entryX).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_ENTRY_FONT_SIZE6).attr("font-weight", isActive ? "600" : "400").attr("fill", isActive ? palette.primary : palette.textMuted).attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer").text(label);
16140
- entryX += label.length * LEGEND_ENTRY_FONT_W7 + 6;
16523
+ const slotW = label.length * LEGEND_ENTRY_FONT_W7 + SPEED_BADGE_H_PAD * 2;
16524
+ const badgeH = LEGEND_ENTRY_FONT_SIZE6 + SPEED_BADGE_V_PAD * 2;
16525
+ const badgeY = (LEGEND_HEIGHT8 - badgeH) / 2;
16526
+ const speedG = pbG.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
16527
+ speedG.append("rect").attr("x", entryX).attr("y", badgeY).attr("width", slotW).attr("height", badgeH).attr("rx", badgeH / 2).attr("fill", isActive ? palette.primary : "transparent");
16528
+ speedG.append("text").attr("x", entryX + slotW / 2).attr("y", entryY).attr("font-family", FONT_FAMILY).attr("font-size", LEGEND_ENTRY_FONT_SIZE6).attr("font-weight", isActive ? "600" : "400").attr("fill", isActive ? palette.bg : palette.textMuted).attr("text-anchor", "middle").text(label);
16529
+ entryX += slotW + SPEED_BADGE_GAP;
16141
16530
  }
16142
16531
  }
16143
16532
  cursorX += fullW + LEGEND_GROUP_GAP5;
16144
16533
  }
16145
16534
  }
16146
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, selectedNodeId, exportMode, collapsedNodes) {
16535
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
16147
16536
  d3Selection9.select(container).selectAll(":not([data-d3-tooltip])").remove();
16148
- const legendGroups = computeInfraLegendGroups(layout.nodes, tagGroups ?? [], palette);
16537
+ const legendGroups = computeInfraLegendGroups(layout.nodes, tagGroups ?? [], palette, layout.edges);
16149
16538
  const hasLegend = legendGroups.length > 0 || !!playback;
16150
16539
  const fixedLegend = !exportMode && hasLegend;
16151
16540
  const legendOffset = hasLegend && !fixedLegend ? LEGEND_HEIGHT8 : 0;
@@ -16202,7 +16591,14 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
16202
16591
  }
16203
16592
  renderGroups(svg, layout.groups, palette, isDark);
16204
16593
  renderEdgePaths(svg, layout.edges, layout.nodes, palette, isDark, shouldAnimate);
16205
- renderNodes(svg, layout.nodes, palette, isDark, shouldAnimate, selectedNodeId, activeGroup, layout.options, collapsedNodes, tagGroups ?? []);
16594
+ const fanoutSourceIds = collectFanoutSourceIds(layout.edges);
16595
+ const scaledGroupIds = new Set(
16596
+ layout.groups.filter((g) => {
16597
+ const gi = typeof g.instances === "number" ? g.instances : typeof g.instances === "string" ? parseInt(String(g.instances), 10) || 0 : 0;
16598
+ return gi > 1;
16599
+ }).map((g) => g.id)
16600
+ );
16601
+ renderNodes(svg, layout.nodes, palette, isDark, shouldAnimate, expandedNodeIds, activeGroup, layout.options, collapsedNodes, tagGroups ?? [], fanoutSourceIds, scaledGroupIds);
16206
16602
  if (shouldAnimate) {
16207
16603
  renderRejectParticles(svg, layout.nodes);
16208
16604
  }
@@ -16224,7 +16620,7 @@ function parseAndLayoutInfra(content) {
16224
16620
  const layout = layoutInfra(computed);
16225
16621
  return { parsed, computed, layout };
16226
16622
  }
16227
- var d3Selection9, d3Shape7, NODE_FONT_SIZE3, META_FONT_SIZE4, META_LINE_HEIGHT8, EDGE_LABEL_FONT_SIZE7, GROUP_LABEL_FONT_SIZE2, NODE_BORDER_RADIUS, EDGE_STROKE_WIDTH8, NODE_STROKE_WIDTH8, OVERLOAD_STROKE_WIDTH, ROLE_DOT_RADIUS, NODE_HEADER_HEIGHT2, NODE_SEPARATOR_GAP2, NODE_PAD_BOTTOM2, COLLAPSE_BAR_HEIGHT4, COLLAPSE_BAR_INSET2, LEGEND_HEIGHT8, LEGEND_PILL_PAD7, LEGEND_PILL_FONT_SIZE5, LEGEND_PILL_FONT_W7, LEGEND_CAPSULE_PAD7, LEGEND_DOT_R8, LEGEND_ENTRY_FONT_SIZE6, LEGEND_ENTRY_FONT_W7, LEGEND_ENTRY_DOT_GAP7, LEGEND_ENTRY_TRAIL7, LEGEND_GROUP_GAP5, LEGEND_FIXED_GAP3, COLOR_WARNING, COLOR_OVERLOADED, FLOW_SPEED_MIN, FLOW_SPEED_MAX, PARTICLE_R, PARTICLE_COUNT_MIN, PARTICLE_COUNT_MAX, NODE_PULSE_SPEED, NODE_PULSE_OVERLOAD, REJECT_PARTICLE_R, REJECT_DROP_DISTANCE, REJECT_DURATION_MIN, REJECT_DURATION_MAX, REJECT_COUNT_MIN, REJECT_COUNT_MAX, lineGenerator7, PROP_DISPLAY, RPS_FORMAT_KEYS, MS_FORMAT_KEYS, PCT_FORMAT_KEYS;
16623
+ var d3Selection9, d3Shape7, NODE_FONT_SIZE4, META_FONT_SIZE4, META_LINE_HEIGHT8, EDGE_LABEL_FONT_SIZE7, GROUP_LABEL_FONT_SIZE2, NODE_BORDER_RADIUS, EDGE_STROKE_WIDTH8, NODE_STROKE_WIDTH8, OVERLOAD_STROKE_WIDTH, ROLE_DOT_RADIUS, NODE_HEADER_HEIGHT2, NODE_SEPARATOR_GAP2, NODE_PAD_BOTTOM2, COLLAPSE_BAR_HEIGHT5, COLLAPSE_BAR_INSET2, LEGEND_HEIGHT8, LEGEND_PILL_PAD7, LEGEND_PILL_FONT_SIZE5, LEGEND_PILL_FONT_W7, LEGEND_CAPSULE_PAD7, LEGEND_DOT_R8, LEGEND_ENTRY_FONT_SIZE6, LEGEND_ENTRY_FONT_W7, LEGEND_ENTRY_DOT_GAP7, LEGEND_ENTRY_TRAIL7, LEGEND_GROUP_GAP5, LEGEND_FIXED_GAP3, SPEED_BADGE_H_PAD, SPEED_BADGE_V_PAD, SPEED_BADGE_GAP, COLOR_HEALTHY, COLOR_WARNING, COLOR_OVERLOADED, FLOW_SPEED_MIN, FLOW_SPEED_MAX, PARTICLE_R, PARTICLE_COUNT_MIN, PARTICLE_COUNT_MAX, NODE_PULSE_SPEED, NODE_PULSE_OVERLOAD, REJECT_PARTICLE_R, REJECT_DROP_DISTANCE, REJECT_DURATION_MIN, REJECT_DURATION_MAX, REJECT_COUNT_MIN, REJECT_COUNT_MAX, lineGenerator7, PROP_DISPLAY, DESC_MAX_CHARS, RPS_FORMAT_KEYS, MS_FORMAT_KEYS, PCT_FORMAT_KEYS;
16228
16624
  var init_renderer8 = __esm({
16229
16625
  "src/infra/renderer.ts"() {
16230
16626
  "use strict";
@@ -16237,7 +16633,7 @@ var init_renderer8 = __esm({
16237
16633
  init_parser9();
16238
16634
  init_compute();
16239
16635
  init_layout8();
16240
- NODE_FONT_SIZE3 = 13;
16636
+ NODE_FONT_SIZE4 = 13;
16241
16637
  META_FONT_SIZE4 = 10;
16242
16638
  META_LINE_HEIGHT8 = 14;
16243
16639
  EDGE_LABEL_FONT_SIZE7 = 11;
@@ -16250,7 +16646,7 @@ var init_renderer8 = __esm({
16250
16646
  NODE_HEADER_HEIGHT2 = 28;
16251
16647
  NODE_SEPARATOR_GAP2 = 4;
16252
16648
  NODE_PAD_BOTTOM2 = 10;
16253
- COLLAPSE_BAR_HEIGHT4 = 6;
16649
+ COLLAPSE_BAR_HEIGHT5 = 6;
16254
16650
  COLLAPSE_BAR_INSET2 = 0;
16255
16651
  LEGEND_HEIGHT8 = 28;
16256
16652
  LEGEND_PILL_PAD7 = 16;
@@ -16264,6 +16660,10 @@ var init_renderer8 = __esm({
16264
16660
  LEGEND_ENTRY_TRAIL7 = 8;
16265
16661
  LEGEND_GROUP_GAP5 = 12;
16266
16662
  LEGEND_FIXED_GAP3 = 16;
16663
+ SPEED_BADGE_H_PAD = 5;
16664
+ SPEED_BADGE_V_PAD = 3;
16665
+ SPEED_BADGE_GAP = 6;
16666
+ COLOR_HEALTHY = "#22c55e";
16267
16667
  COLOR_WARNING = "#eab308";
16268
16668
  COLOR_OVERLOADED = "#ef4444";
16269
16669
  FLOW_SPEED_MIN = 2.5;
@@ -16282,22 +16682,23 @@ var init_renderer8 = __esm({
16282
16682
  lineGenerator7 = d3Shape7.line().x((d) => d.x).y((d) => d.y).curve(d3Shape7.curveBasis);
16283
16683
  PROP_DISPLAY = {
16284
16684
  "cache-hit": "cache hit",
16285
- "firewall-block": "fw block",
16286
- "ratelimit-rps": "rate limit",
16685
+ "firewall-block": "firewall block",
16686
+ "ratelimit-rps": "rate limit RPS",
16287
16687
  "latency-ms": "latency",
16288
16688
  "uptime": "uptime",
16289
16689
  "instances": "instances",
16290
- "max-rps": "capacity",
16291
- "cb-error-threshold": "CB error",
16292
- "cb-latency-threshold-ms": "CB latency",
16690
+ "max-rps": "max RPS",
16691
+ "cb-error-threshold": "CB error threshold",
16692
+ "cb-latency-threshold-ms": "CB latency threshold",
16293
16693
  "concurrency": "concurrency",
16294
16694
  "duration-ms": "duration",
16295
16695
  "cold-start-ms": "cold start",
16296
16696
  "buffer": "buffer",
16297
- "drain-rate": "drain",
16697
+ "drain-rate": "drain rate",
16298
16698
  "retention-hours": "retention",
16299
16699
  "partitions": "partitions"
16300
16700
  };
16701
+ DESC_MAX_CHARS = 120;
16301
16702
  RPS_FORMAT_KEYS = /* @__PURE__ */ new Set(["max-rps", "ratelimit-rps"]);
16302
16703
  MS_FORMAT_KEYS = /* @__PURE__ */ new Set(["latency-ms", "cb-latency-threshold-ms", "duration-ms", "cold-start-ms"]);
16303
16704
  PCT_FORMAT_KEYS = /* @__PURE__ */ new Set(["cache-hit", "firewall-block", "uptime", "cb-error-threshold"]);
@@ -16486,7 +16887,7 @@ function renderState(container, graph, layout, palette, isDark, onClickItem, exp
16486
16887
  const w = node.width;
16487
16888
  const h = node.height;
16488
16889
  nodeG.append("rect").attr("x", -w / 2).attr("y", -h / 2).attr("width", w).attr("height", h).attr("rx", STATE_CORNER_RADIUS).attr("ry", STATE_CORNER_RADIUS).attr("fill", stateFill(palette, isDark, node.color, colorOff)).attr("stroke", stateStroke(palette, node.color, colorOff)).attr("stroke-width", NODE_STROKE_WIDTH9);
16489
- nodeG.append("text").attr("x", 0).attr("y", 0).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("fill", palette.text).attr("font-size", NODE_FONT_SIZE4).text(node.label);
16890
+ nodeG.append("text").attr("x", 0).attr("y", 0).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("fill", palette.text).attr("font-size", NODE_FONT_SIZE5).text(node.label);
16490
16891
  }
16491
16892
  }
16492
16893
  }
@@ -16525,7 +16926,7 @@ function renderStateForExport(content, theme, palette) {
16525
16926
  document.body.removeChild(container);
16526
16927
  }
16527
16928
  }
16528
- var d3Selection10, d3Shape8, DIAGRAM_PADDING9, MAX_SCALE8, NODE_FONT_SIZE4, EDGE_LABEL_FONT_SIZE8, GROUP_LABEL_FONT_SIZE3, EDGE_STROKE_WIDTH9, NODE_STROKE_WIDTH9, ARROWHEAD_W4, ARROWHEAD_H4, PSEUDOSTATE_RADIUS, STATE_CORNER_RADIUS, GROUP_EXTRA_PADDING2, lineGenerator8;
16929
+ var d3Selection10, d3Shape8, DIAGRAM_PADDING9, MAX_SCALE8, NODE_FONT_SIZE5, EDGE_LABEL_FONT_SIZE8, GROUP_LABEL_FONT_SIZE3, EDGE_STROKE_WIDTH9, NODE_STROKE_WIDTH9, ARROWHEAD_W4, ARROWHEAD_H4, PSEUDOSTATE_RADIUS, STATE_CORNER_RADIUS, GROUP_EXTRA_PADDING2, lineGenerator8;
16529
16930
  var init_state_renderer = __esm({
16530
16931
  "src/graph/state-renderer.ts"() {
16531
16932
  "use strict";
@@ -16537,7 +16938,7 @@ var init_state_renderer = __esm({
16537
16938
  init_layout7();
16538
16939
  DIAGRAM_PADDING9 = 20;
16539
16940
  MAX_SCALE8 = 3;
16540
- NODE_FONT_SIZE4 = 13;
16941
+ NODE_FONT_SIZE5 = 13;
16541
16942
  EDGE_LABEL_FONT_SIZE8 = 11;
16542
16943
  GROUP_LABEL_FONT_SIZE3 = 11;
16543
16944
  EDGE_STROKE_WIDTH9 = 1.5;
@@ -18144,7 +18545,6 @@ function parseD3(content, palette) {
18144
18545
  timelineSwimlanes: false,
18145
18546
  vennSets: [],
18146
18547
  vennOverlaps: [],
18147
- vennShowValues: false,
18148
18548
  quadrantLabels: {
18149
18549
  topRight: null,
18150
18550
  topLeft: null,
@@ -18166,6 +18566,9 @@ function parseD3(content, palette) {
18166
18566
  result.error = formatDgmoError(diag);
18167
18567
  return result;
18168
18568
  };
18569
+ const warn = (line10, message) => {
18570
+ result.diagnostics.push(makeDgmoError(line10, message, "warning"));
18571
+ };
18169
18572
  if (!content || !content.trim()) {
18170
18573
  return fail(0, "Empty content");
18171
18574
  }
@@ -18361,25 +18764,38 @@ function parseD3(content, palette) {
18361
18764
  }
18362
18765
  }
18363
18766
  if (result.type === "venn") {
18364
- const overlapMatch = line10.match(
18365
- /^(.+?&.+?)\s*:\s*(\d+(?:\.\d+)?)\s*(?:"([^"]*)")?\s*$/
18366
- );
18367
- if (overlapMatch) {
18368
- const sets = overlapMatch[1].split("&").map((s) => s.trim()).filter(Boolean).sort();
18369
- const size = parseFloat(overlapMatch[2]);
18370
- const label = overlapMatch[3] ?? null;
18371
- result.vennOverlaps.push({ sets, size, label, lineNumber });
18372
- continue;
18767
+ if (/\+/.test(line10)) {
18768
+ const colonIdx = line10.indexOf(":");
18769
+ let setsPart;
18770
+ let label;
18771
+ if (colonIdx >= 0) {
18772
+ setsPart = line10.substring(0, colonIdx).trim();
18773
+ label = line10.substring(colonIdx + 1).trim() || null;
18774
+ } else {
18775
+ setsPart = line10.trim();
18776
+ label = null;
18777
+ }
18778
+ const rawSets = setsPart.split("+").map((s) => s.trim()).filter(Boolean);
18779
+ if (rawSets.length >= 2) {
18780
+ result.vennOverlaps.push({ sets: rawSets, label, lineNumber });
18781
+ continue;
18782
+ }
18373
18783
  }
18374
- const setMatch = line10.match(
18375
- /^(.+?)(?:\(([^)]+)\))?\s*:\s*(\d+(?:\.\d+)?)\s*(?:"([^"]*)")?\s*$/
18376
- );
18377
- if (setMatch) {
18378
- const name = setMatch[1].trim();
18379
- const color = setMatch[2] ? resolveColor(setMatch[2].trim(), palette) : null;
18380
- const size = parseFloat(setMatch[3]);
18381
- const label = setMatch[4] ?? null;
18382
- result.vennSets.push({ name, size, color, label, lineNumber });
18784
+ const setDeclMatch = line10.match(/^([^(:]+?)(?:\(([^)]+)\))?(?:\s+alias\s+(\S+))?\s*$/i);
18785
+ if (setDeclMatch) {
18786
+ const name = setDeclMatch[1].trim();
18787
+ const colorName = setDeclMatch[2]?.trim() ?? null;
18788
+ let color = null;
18789
+ if (colorName) {
18790
+ const resolved = resolveColor(colorName, palette);
18791
+ if (resolved.startsWith("#")) {
18792
+ color = resolved;
18793
+ } else {
18794
+ warn(lineNumber, `Unknown color "${colorName}" on set "${name}". Using auto-assigned color.`);
18795
+ }
18796
+ }
18797
+ const alias = setDeclMatch[3]?.trim() ?? null;
18798
+ result.vennSets.push({ name, alias, color, lineNumber });
18383
18799
  continue;
18384
18800
  }
18385
18801
  }
@@ -18519,15 +18935,6 @@ function parseD3(content, palette) {
18519
18935
  }
18520
18936
  continue;
18521
18937
  }
18522
- if (key === "values") {
18523
- const v = line10.substring(colonIndex + 1).trim().toLowerCase();
18524
- if (v === "off") {
18525
- result.vennShowValues = false;
18526
- } else if (v === "on") {
18527
- result.vennShowValues = true;
18528
- }
18529
- continue;
18530
- }
18531
18938
  if (key === "rotate") {
18532
18939
  const v = line10.substring(colonIndex + 1).trim().toLowerCase();
18533
18940
  if (v === "none" || v === "mixed" || v === "angled") {
@@ -18605,9 +19012,6 @@ function parseD3(content, palette) {
18605
19012
  if (result.type === "sequence") {
18606
19013
  return result;
18607
19014
  }
18608
- const warn = (line10, message) => {
18609
- result.diagnostics.push(makeDgmoError(line10, message, "warning"));
18610
- };
18611
19015
  if (result.type === "wordcloud") {
18612
19016
  if (result.words.length === 0 && freeformLines.length > 0) {
18613
19017
  result.words = tokenizeFreeformText(freeformLines.join(" "));
@@ -18679,29 +19083,34 @@ function parseD3(content, palette) {
18679
19083
  }
18680
19084
  if (result.type === "venn") {
18681
19085
  if (result.vennSets.length < 2) {
18682
- return fail(1, 'At least 2 sets are required. Add sets as "Name: size" (e.g., "Math: 100")');
19086
+ return fail(1, 'At least 2 sets are required. Add set names (e.g., "Apples", "Oranges")');
18683
19087
  }
18684
19088
  if (result.vennSets.length > 3) {
18685
- return fail(1, "At most 3 sets are supported. Remove extra sets.");
19089
+ return fail(1, "Venn diagrams support 2\u20133 sets");
19090
+ }
19091
+ const setNameLower = new Map(
19092
+ result.vennSets.map((s) => [s.name.toLowerCase(), s.name])
19093
+ );
19094
+ const aliasLower = /* @__PURE__ */ new Map();
19095
+ for (const s of result.vennSets) {
19096
+ if (s.alias) aliasLower.set(s.alias.toLowerCase(), s.name);
18686
19097
  }
18687
- const setMap = new Map(result.vennSets.map((s) => [s.name, s.size]));
19098
+ const resolveSetRef = (ref) => setNameLower.get(ref.toLowerCase()) ?? aliasLower.get(ref.toLowerCase()) ?? null;
18688
19099
  const validOverlaps = [];
18689
19100
  for (const ov of result.vennOverlaps) {
19101
+ const resolvedSets = [];
18690
19102
  let valid = true;
18691
- for (const setName of ov.sets) {
18692
- if (!setMap.has(setName)) {
18693
- result.diagnostics.push(makeDgmoError(ov.lineNumber, `Overlap references unknown set "${setName}". Define it first as "${setName}: <size>"`));
19103
+ for (const ref of ov.sets) {
19104
+ const resolved = resolveSetRef(ref);
19105
+ if (!resolved) {
19106
+ result.diagnostics.push(makeDgmoError(ov.lineNumber, `Intersection references unknown set or alias "${ref}"`));
18694
19107
  if (!result.error) result.error = formatDgmoError(result.diagnostics[result.diagnostics.length - 1]);
18695
19108
  valid = false;
18696
19109
  break;
18697
19110
  }
19111
+ resolvedSets.push(resolved);
18698
19112
  }
18699
- if (!valid) continue;
18700
- const minSetSize = Math.min(...ov.sets.map((s) => setMap.get(s)));
18701
- if (ov.size > minSetSize) {
18702
- warn(ov.lineNumber, `Overlap size ${ov.size} exceeds smallest constituent set size ${minSetSize}`);
18703
- }
18704
- validOverlaps.push(ov);
19113
+ if (valid) validOverlaps.push({ ...ov, sets: resolvedSets.sort() });
18705
19114
  }
18706
19115
  result.vennOverlaps = validOverlaps;
18707
19116
  return result;
@@ -19855,7 +20264,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19855
20264
  return;
19856
20265
  }
19857
20266
  const BAR_H = 22;
19858
- const GROUP_GAP = 12;
20267
+ const GROUP_GAP2 = 12;
19859
20268
  const useGroupedHorizontal = tagLanes != null || timelineSort === "group" && timelineGroups.length > 0;
19860
20269
  if (useGroupedHorizontal) {
19861
20270
  let lanes;
@@ -19888,7 +20297,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19888
20297
  };
19889
20298
  const innerWidth = width - margin.left - margin.right;
19890
20299
  const innerHeight = height - margin.top - margin.bottom;
19891
- const totalGaps = (lanes.length - 1) * GROUP_GAP;
20300
+ const totalGaps = (lanes.length - 1) * GROUP_GAP2;
19892
20301
  const rowH = Math.min(28, (innerHeight - totalGaps) / totalEventRows);
19893
20302
  const xScale = d3Scale.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
19894
20303
  const svg = d3Selection12.select(container).append("svg").attr("width", width).attr("height", height).style("background", bgColor);
@@ -19938,8 +20347,8 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19938
20347
  lanes.forEach((lane, idx) => {
19939
20348
  const laneSpan = lane.events.length * rowH;
19940
20349
  const fillColor = idx % 2 === 0 ? textColor : "transparent";
19941
- g.append("rect").attr("class", "tl-swimlane").attr("data-group", lane.name).attr("x", -margin.left).attr("y", swimY).attr("width", innerWidth + margin.left).attr("height", laneSpan + (idx < lanes.length - 1 ? GROUP_GAP : 0)).attr("fill", fillColor).attr("opacity", 0.06);
19942
- swimY += laneSpan + GROUP_GAP;
20350
+ g.append("rect").attr("class", "tl-swimlane").attr("data-group", lane.name).attr("x", -margin.left).attr("y", swimY).attr("width", innerWidth + margin.left).attr("height", laneSpan + (idx < lanes.length - 1 ? GROUP_GAP2 : 0)).attr("fill", fillColor).attr("opacity", 0.06);
20351
+ swimY += laneSpan + GROUP_GAP2;
19943
20352
  });
19944
20353
  }
19945
20354
  for (const lane of lanes) {
@@ -20020,7 +20429,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20020
20429
  evG.append("text").attr("x", flipLeft ? x - 10 : x + 10).attr("y", y).attr("dy", "0.35em").attr("text-anchor", flipLeft ? "end" : "start").attr("fill", textColor).attr("font-size", "12px").text(ev.label);
20021
20430
  }
20022
20431
  });
20023
- curY += laneSpan + GROUP_GAP;
20432
+ curY += laneSpan + GROUP_GAP2;
20024
20433
  }
20025
20434
  } else {
20026
20435
  const sorted = timelineEvents.slice().sort((a, b) => parseTimelineDate(a.date) - parseTimelineDate(b.date));
@@ -20203,18 +20612,19 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20203
20612
  );
20204
20613
  }, drawLegend2 = function() {
20205
20614
  mainSvg.selectAll(".tl-tag-legend-group").remove();
20615
+ const effectiveColorKey = (currentActiveGroup ?? currentSwimlaneGroup)?.toLowerCase() ?? null;
20206
20616
  const visibleGroups = viewMode ? legendGroups.filter(
20207
- (lg) => currentActiveGroup != null && lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase()
20617
+ (lg) => effectiveColorKey != null && lg.group.name.toLowerCase() === effectiveColorKey
20208
20618
  ) : legendGroups;
20209
20619
  if (visibleGroups.length === 0) return;
20210
20620
  const totalW = visibleGroups.reduce((s, lg) => {
20211
- const isActive = currentActiveGroup != null && lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase();
20621
+ const isActive = viewMode || currentActiveGroup != null && lg.group.name.toLowerCase() === currentActiveGroup.toLowerCase();
20212
20622
  return s + (isActive ? lg.expandedWidth : lg.minifiedWidth);
20213
20623
  }, 0) + (visibleGroups.length - 1) * LG_GROUP_GAP;
20214
20624
  let cx = (width - totalW) / 2;
20215
20625
  for (const lg of visibleGroups) {
20216
20626
  const groupKey = lg.group.name.toLowerCase();
20217
- const isActive = currentActiveGroup != null && currentActiveGroup.toLowerCase() === groupKey;
20627
+ const isActive = viewMode || currentActiveGroup != null && currentActiveGroup.toLowerCase() === groupKey;
20218
20628
  const isSwimActive = currentSwimlaneGroup != null && currentSwimlaneGroup.toLowerCase() === groupKey;
20219
20629
  const pillLabel = lg.group.name;
20220
20630
  const pillWidth = pillLabel.length * LG_PILL_FONT_W + LG_PILL_PAD;
@@ -20422,49 +20832,6 @@ function renderWordCloudAsync(container, parsed, palette, _isDark, exportDims) {
20422
20832
  }).start();
20423
20833
  });
20424
20834
  }
20425
- function radiusFromArea(area) {
20426
- return Math.sqrt(area / Math.PI);
20427
- }
20428
- function circleOverlapArea(r1, r2, d) {
20429
- if (d >= r1 + r2) return 0;
20430
- if (d + Math.min(r1, r2) <= Math.max(r1, r2)) {
20431
- return Math.PI * Math.min(r1, r2) ** 2;
20432
- }
20433
- const part1 = r1 * r1 * Math.acos((d * d + r1 * r1 - r2 * r2) / (2 * d * r1));
20434
- const part2 = r2 * r2 * Math.acos((d * d + r2 * r2 - r1 * r1) / (2 * d * r2));
20435
- const part3 = 0.5 * Math.sqrt((-d + r1 + r2) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2));
20436
- return part1 + part2 - part3;
20437
- }
20438
- function distanceForOverlap(r1, r2, targetArea) {
20439
- if (targetArea <= 0) return r1 + r2;
20440
- const minR = Math.min(r1, r2);
20441
- if (targetArea >= Math.PI * minR * minR) return Math.abs(r1 - r2);
20442
- let lo = Math.abs(r1 - r2);
20443
- let hi = r1 + r2;
20444
- for (let i = 0; i < 64; i++) {
20445
- const mid = (lo + hi) / 2;
20446
- if (circleOverlapArea(r1, r2, mid) > targetArea) {
20447
- lo = mid;
20448
- } else {
20449
- hi = mid;
20450
- }
20451
- }
20452
- return (lo + hi) / 2;
20453
- }
20454
- function thirdCirclePosition(ax, ay, dAC, bx, by, dBC) {
20455
- const dx = bx - ax;
20456
- const dy = by - ay;
20457
- const dAB = Math.sqrt(dx * dx + dy * dy);
20458
- if (dAB === 0) return { x: ax + dAC, y: ay };
20459
- const cosA = (dAB * dAB + dAC * dAC - dBC * dBC) / (2 * dAB * dAC);
20460
- const sinA = Math.sqrt(Math.max(0, 1 - cosA * cosA));
20461
- const ux = dx / dAB;
20462
- const uy = dy / dAB;
20463
- return {
20464
- x: ax + dAC * (cosA * ux - sinA * uy),
20465
- y: ay + dAC * (cosA * uy + sinA * ux)
20466
- };
20467
- }
20468
20835
  function fitCirclesToContainerAsymmetric(circles, w, h, mLeft, mRight, mTop, mBottom) {
20469
20836
  if (circles.length === 0) return [];
20470
20837
  let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
@@ -20539,54 +20906,28 @@ function regionCentroid(circles, inside) {
20539
20906
  return { x: sx / count, y: sy / count };
20540
20907
  }
20541
20908
  function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims) {
20542
- const { vennSets, vennOverlaps, vennShowValues, title } = parsed;
20909
+ const { vennSets, vennOverlaps, title } = parsed;
20543
20910
  if (vennSets.length < 2) return;
20544
20911
  const init2 = initD3Chart(container, palette, exportDims);
20545
20912
  if (!init2) return;
20546
20913
  const { svg, width, height, textColor, colors } = init2;
20547
20914
  const titleHeight = title ? 40 : 0;
20548
- const radii = vennSets.map((s) => radiusFromArea(s.size));
20549
- const overlapMap = /* @__PURE__ */ new Map();
20550
- for (const ov of vennOverlaps) {
20551
- overlapMap.set(ov.sets.join("&"), ov.size);
20552
- }
20553
- let rawCircles;
20554
20915
  const n = vennSets.length;
20916
+ const BASE_R = 100;
20917
+ const OVERLAP_DISTANCE = BASE_R * 1.4;
20918
+ let rawCircles;
20555
20919
  if (n === 2) {
20556
- const d = distanceForOverlap(
20557
- radii[0],
20558
- radii[1],
20559
- overlapMap.get([vennSets[0].name, vennSets[1].name].sort().join("&")) ?? 0
20560
- );
20561
20920
  rawCircles = [
20562
- { x: -d / 2, y: 0, r: radii[0] },
20563
- { x: d / 2, y: 0, r: radii[1] }
20921
+ { x: -OVERLAP_DISTANCE / 2, y: 0, r: BASE_R },
20922
+ { x: OVERLAP_DISTANCE / 2, y: 0, r: BASE_R }
20564
20923
  ];
20565
20924
  } else {
20566
- const names = vennSets.map((s) => s.name);
20567
- const pairKey = (i, j) => [names[i], names[j]].sort().join("&");
20568
- const dAB = distanceForOverlap(
20569
- radii[0],
20570
- radii[1],
20571
- overlapMap.get(pairKey(0, 1)) ?? 0
20572
- );
20573
- const dAC = distanceForOverlap(
20574
- radii[0],
20575
- radii[2],
20576
- overlapMap.get(pairKey(0, 2)) ?? 0
20577
- );
20578
- const dBC = distanceForOverlap(
20579
- radii[1],
20580
- radii[2],
20581
- overlapMap.get(pairKey(1, 2)) ?? 0
20582
- );
20583
- const ax = -dAB / 2;
20584
- const bx = dAB / 2;
20585
- const cPos = thirdCirclePosition(ax, 0, dAC, bx, 0, dBC);
20925
+ const s = OVERLAP_DISTANCE;
20926
+ const h = Math.sqrt(3) / 2 * s;
20586
20927
  rawCircles = [
20587
- { x: ax, y: 0, r: radii[0] },
20588
- { x: bx, y: 0, r: radii[1] },
20589
- { x: cPos.x, y: cPos.y, r: radii[2] }
20928
+ { x: -s / 2, y: h / 3, r: BASE_R },
20929
+ { x: s / 2, y: h / 3, r: BASE_R },
20930
+ { x: 0, y: -(2 * h) / 3, r: BASE_R }
20590
20931
  ];
20591
20932
  }
20592
20933
  const setColors = vennSets.map(
@@ -20599,8 +20940,7 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
20599
20940
  const edgePad = 8;
20600
20941
  const labelTextPad = 4;
20601
20942
  for (let i = 0; i < n; i++) {
20602
- const displayName = vennSets[i].label ?? vennSets[i].name;
20603
- const estimatedWidth = displayName.length * 8.5 + stubLen + edgePad + labelTextPad;
20943
+ const estimatedWidth = vennSets[i].name.length * 8.5 + stubLen + edgePad + labelTextPad;
20604
20944
  const dx = rawCircles[i].x - clusterCx;
20605
20945
  const dy = rawCircles[i].y - clusterCy;
20606
20946
  if (Math.abs(dx) >= Math.abs(dy)) {
@@ -20622,25 +20962,71 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
20622
20962
  marginTop,
20623
20963
  marginBottom
20624
20964
  ).map((c) => ({ ...c, y: c.y + titleHeight }));
20625
- const tooltip = createTooltip(container, palette, isDark);
20965
+ const scaledR = circles[0].r;
20966
+ svg.append("style").text("circle:focus, circle:focus-visible { outline: none !important; }");
20626
20967
  renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
20627
20968
  const circleEls = [];
20628
20969
  const circleGroup = svg.append("g");
20629
20970
  circles.forEach((c, i) => {
20630
- const el = circleGroup.append("circle").attr("cx", c.x).attr("cy", c.y).attr("r", c.r).attr("fill", setColors[i]).attr("fill-opacity", 0.35).attr("stroke", setColors[i]).attr("stroke-width", 2).style("pointer-events", "none");
20971
+ const el = circleGroup.append("circle").attr("cx", c.x).attr("cy", c.y).attr("r", c.r).attr("fill", setColors[i]).attr("fill-opacity", 0.35).attr("stroke", setColors[i]).attr("stroke-width", 2).attr("class", "venn-fill-circle").attr("data-line-number", String(vennSets[i].lineNumber)).style("pointer-events", "none");
20631
20972
  circleEls.push(el);
20632
20973
  });
20974
+ const defs = svg.append("defs");
20975
+ circles.forEach((c, i) => {
20976
+ defs.append("clipPath").attr("id", `vcp-${i}`).append("circle").attr("cx", c.x).attr("cy", c.y).attr("r", c.r);
20977
+ });
20978
+ const regionIdxSets = circles.map((_, i) => [i]);
20979
+ if (n === 2) {
20980
+ regionIdxSets.push([0, 1]);
20981
+ } else {
20982
+ regionIdxSets.push([0, 1], [0, 2], [1, 2], [0, 1, 2]);
20983
+ }
20984
+ const overlayGroup = svg.append("g").style("pointer-events", "none");
20985
+ const overlayEls = /* @__PURE__ */ new Map();
20986
+ for (const idxs of regionIdxSets) {
20987
+ const key = idxs.join("-");
20988
+ const excluded = Array.from({ length: n }, (_, j) => j).filter((j) => !idxs.includes(j));
20989
+ let clipId = `vcp-${idxs[0]}`;
20990
+ for (let k = 1; k < idxs.length; k++) {
20991
+ const nestedId = `vcp-n-${idxs.slice(0, k + 1).join("-")}`;
20992
+ const ci = idxs[k];
20993
+ defs.append("clipPath").attr("id", nestedId).append("circle").attr("cx", circles[ci].x).attr("cy", circles[ci].y).attr("r", circles[ci].r).attr("clip-path", `url(#${clipId})`);
20994
+ clipId = nestedId;
20995
+ }
20996
+ let regionLineNumber = null;
20997
+ if (idxs.length === 1) {
20998
+ regionLineNumber = vennSets[idxs[0]].lineNumber;
20999
+ } else {
21000
+ const sortedNames = idxs.map((i) => vennSets[i].name).sort();
21001
+ const ov = vennOverlaps.find(
21002
+ (o) => o.sets.length === sortedNames.length && o.sets.every((s, k) => s === sortedNames[k])
21003
+ );
21004
+ regionLineNumber = ov?.lineNumber ?? null;
21005
+ }
21006
+ const el = overlayGroup.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "white").attr("fill-opacity", 0).attr("class", "venn-region-overlay").attr("clip-path", `url(#${clipId})`);
21007
+ if (regionLineNumber != null) {
21008
+ el.attr("data-line-number", String(regionLineNumber));
21009
+ }
21010
+ if (excluded.length > 0) {
21011
+ const maskId = `vvm-${key}`;
21012
+ const mask = defs.append("mask").attr("id", maskId);
21013
+ mask.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "white");
21014
+ for (const j of excluded) {
21015
+ mask.append("circle").attr("cx", circles[j].x).attr("cy", circles[j].y).attr("r", circles[j].r).attr("fill", "black");
21016
+ }
21017
+ el.attr("mask", `url(#${maskId})`);
21018
+ }
21019
+ overlayEls.set(key, el);
21020
+ }
21021
+ const showRegionOverlay = (idxs) => {
21022
+ const key = [...idxs].sort((a, b) => a - b).join("-");
21023
+ overlayEls.forEach((el, k) => el.attr("fill-opacity", k === key ? 0.3 : 0));
21024
+ };
21025
+ const hideAllOverlays = () => {
21026
+ overlayEls.forEach((el) => el.attr("fill-opacity", 0));
21027
+ };
20633
21028
  const gcx = circles.reduce((s, c) => s + c.x, 0) / n;
20634
21029
  const gcy = circles.reduce((s, c) => s + c.y, 0) / n;
20635
- function rayCircleExit(ox, oy, dx, dy, c) {
20636
- const lx = ox - c.x;
20637
- const ly = oy - c.y;
20638
- const b = lx * dx + ly * dy;
20639
- const det = b * b - (lx * lx + ly * ly - c.r * c.r);
20640
- if (det < 0) return 0;
20641
- return -b + Math.sqrt(det);
20642
- }
20643
- const labelGroup = svg.append("g").style("pointer-events", "none");
20644
21030
  function exclusiveHSpan(px, py, ci) {
20645
21031
  const dy = py - circles[ci].y;
20646
21032
  const halfChord = Math.sqrt(Math.max(0, circles[ci].r * circles[ci].r - dy * dy));
@@ -20663,8 +21049,9 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
20663
21049
  const MIN_FONT = 10;
20664
21050
  const MAX_FONT = 22;
20665
21051
  const INTERNAL_PAD = 12;
21052
+ const labelGroup = svg.append("g").style("pointer-events", "none");
20666
21053
  circles.forEach((c, i) => {
20667
- const text = vennSets[i].label ?? vennSets[i].name;
21054
+ const text = vennSets[i].name;
20668
21055
  const inside = circles.map((_, j) => j === i);
20669
21056
  const centroid = regionCentroid(circles, inside);
20670
21057
  const availW = exclusiveHSpan(centroid.x, centroid.y, i);
@@ -20699,11 +21086,8 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
20699
21086
  let textX = stubEndX + (isRight ? labelTextPad : -labelTextPad);
20700
21087
  const textY = stubEndY;
20701
21088
  const estW = text.length * 8.5;
20702
- if (isRight) {
20703
- textX = Math.min(textX, width - estW - 4);
20704
- } else {
20705
- textX = Math.max(textX, estW + 4);
20706
- }
21089
+ if (isRight) textX = Math.min(textX, width - estW - 4);
21090
+ else textX = Math.max(textX, estW + 4);
20707
21091
  labelGroup.append("text").attr("x", textX).attr("y", Math.max(14, Math.min(height - 4, textY))).attr("text-anchor", textAnchor).attr("dominant-baseline", "central").attr("fill", textColor).attr("font-size", "14px").attr("font-weight", "bold").text(text);
20708
21092
  }
20709
21093
  });
@@ -20731,40 +21115,57 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
20731
21115
  return Math.max(0, right - left);
20732
21116
  }
20733
21117
  for (const ov of vennOverlaps) {
21118
+ if (!ov.label) continue;
20734
21119
  const idxs = ov.sets.map((s) => vennSets.findIndex((vs) => vs.name === s));
20735
21120
  if (idxs.some((idx) => idx < 0)) continue;
20736
- if (!ov.label) continue;
20737
21121
  const inside = circles.map((_, j) => idxs.includes(j));
20738
21122
  const centroid = regionCentroid(circles, inside);
20739
- const text = ov.label;
20740
21123
  const availW = overlapHSpan(centroid.y, idxs);
20741
21124
  const fitFont = Math.min(MAX_FONT, Math.max(
20742
21125
  MIN_FONT,
20743
- (availW - INTERNAL_PAD * 2) / (text.length * CH_RATIO)
21126
+ (availW - INTERNAL_PAD * 2) / (ov.label.length * CH_RATIO)
20744
21127
  ));
20745
- labelGroup.append("text").attr("x", centroid.x).attr("y", centroid.y).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("fill", textColor).attr("font-size", `${Math.round(fitFont)}px`).attr("font-weight", "600").text(text);
21128
+ labelGroup.append("text").attr("x", centroid.x).attr("y", centroid.y).attr("text-anchor", "middle").attr("dominant-baseline", "central").attr("fill", textColor).attr("font-size", `${Math.round(fitFont)}px`).attr("font-weight", "600").text(ov.label);
20746
21129
  }
20747
21130
  const hoverGroup = svg.append("g");
20748
21131
  circles.forEach((c, i) => {
20749
- const tipName = vennSets[i].label ? `${vennSets[i].label} (${vennSets[i].name})` : vennSets[i].name;
20750
- const tipHtml = `<strong>${tipName}</strong><br>Size: ${vennSets[i].size}`;
20751
- hoverGroup.append("circle").attr("cx", c.x).attr("cy", c.y).attr("r", c.r).attr("fill", "transparent").attr("data-line-number", String(vennSets[i].lineNumber)).style("cursor", onClickItem ? "pointer" : "default").on("mouseenter", (event) => {
20752
- circleEls.forEach((el, ci) => {
20753
- el.attr("fill-opacity", ci === i ? 0.5 : 0.1);
20754
- });
20755
- showTooltip(tooltip, tipHtml, event);
20756
- }).on("mousemove", (event) => {
20757
- showTooltip(tooltip, tipHtml, event);
21132
+ hoverGroup.append("circle").attr("cx", c.x).attr("cy", c.y).attr("r", c.r).attr("fill", "transparent").attr("stroke", "none").attr("class", "venn-hit-target").attr("data-line-number", String(vennSets[i].lineNumber)).style("cursor", onClickItem ? "pointer" : "default").style("outline", "none").on("mouseenter", () => {
21133
+ showRegionOverlay([i]);
20758
21134
  }).on("mouseleave", () => {
20759
- circleEls.forEach((el) => {
20760
- el.attr("fill-opacity", 0.35);
20761
- });
20762
- hideTooltip(tooltip);
20763
- }).on("click", () => {
20764
- if (onClickItem && vennSets[i].lineNumber)
20765
- onClickItem(vennSets[i].lineNumber);
21135
+ hideAllOverlays();
21136
+ }).on("click", function() {
21137
+ this.blur?.();
21138
+ if (onClickItem && vennSets[i].lineNumber) onClickItem(vennSets[i].lineNumber);
20766
21139
  });
20767
21140
  });
21141
+ const overlayR = scaledR * 0.35;
21142
+ const subsets = [];
21143
+ if (n === 2) {
21144
+ subsets.push({ idxs: [0, 1], sets: [vennSets[0].name, vennSets[1].name].sort() });
21145
+ } else {
21146
+ for (let a = 0; a < n; a++) {
21147
+ for (let b = a + 1; b < n; b++) {
21148
+ subsets.push({ idxs: [a, b], sets: [vennSets[a].name, vennSets[b].name].sort() });
21149
+ }
21150
+ }
21151
+ subsets.push({ idxs: [0, 1, 2], sets: [vennSets[0].name, vennSets[1].name, vennSets[2].name].sort() });
21152
+ }
21153
+ for (const subset of subsets) {
21154
+ const { idxs, sets } = subset;
21155
+ const inside = circles.map((_, j) => idxs.includes(j));
21156
+ const centroid = regionCentroid(circles, inside);
21157
+ const declaredOv = vennOverlaps.find(
21158
+ (ov) => ov.sets.length === sets.length && ov.sets.every((s, k) => s === sets[k])
21159
+ );
21160
+ hoverGroup.append("circle").attr("cx", centroid.x).attr("cy", centroid.y).attr("r", overlayR).attr("fill", "transparent").attr("stroke", "none").attr("class", "venn-hit-target").attr("data-line-number", declaredOv ? String(declaredOv.lineNumber) : "").style("cursor", onClickItem && declaredOv ? "pointer" : "default").style("outline", "none").on("mouseenter", () => {
21161
+ showRegionOverlay(idxs);
21162
+ }).on("mouseleave", () => {
21163
+ hideAllOverlays();
21164
+ }).on("click", function() {
21165
+ this.blur?.();
21166
+ if (onClickItem && declaredOv) onClickItem(declaredOv.lineNumber);
21167
+ });
21168
+ }
20768
21169
  }
20769
21170
  function renderQuadrant(container, parsed, palette, isDark, onClickItem, exportDims) {
20770
21171
  const {
@@ -20872,8 +21273,8 @@ function renderQuadrant(container, parsed, palette, isDark, onClickItem, exportD
20872
21273
  const LABEL_MAX_FONT = 48;
20873
21274
  const LABEL_MIN_FONT = 14;
20874
21275
  const LABEL_PAD = 40;
20875
- const CHAR_WIDTH_RATIO2 = 0.6;
20876
- const estTextWidth = (text, fontSize) => text.length * fontSize * CHAR_WIDTH_RATIO2;
21276
+ const CHAR_WIDTH_RATIO3 = 0.6;
21277
+ const estTextWidth = (text, fontSize) => text.length * fontSize * CHAR_WIDTH_RATIO3;
20877
21278
  const quadrantLabelLayout = (text, qw2, qh2) => {
20878
21279
  const availW = qw2 - LABEL_PAD;
20879
21280
  const availH = qh2 - LABEL_PAD;
@@ -21495,6 +21896,7 @@ __export(index_exports, {
21495
21896
  buildRenderSequence: () => buildRenderSequence,
21496
21897
  buildThemeCSS: () => buildThemeCSS,
21497
21898
  catppuccinPalette: () => catppuccinPalette,
21899
+ collapseInitiativeStatus: () => collapseInitiativeStatus,
21498
21900
  collapseOrgTree: () => collapseOrgTree,
21499
21901
  collapseSitemapTree: () => collapseSitemapTree,
21500
21902
  collectDiagramRoles: () => collectDiagramRoles,
@@ -21843,6 +22245,43 @@ init_renderer7();
21843
22245
  init_parser7();
21844
22246
  init_layout5();
21845
22247
  init_renderer6();
22248
+
22249
+ // src/initiative-status/collapse.ts
22250
+ init_layout5();
22251
+ function collapseInitiativeStatus(parsed, collapsedGroups) {
22252
+ const originalGroups = parsed.groups;
22253
+ if (collapsedGroups.size === 0) {
22254
+ return { parsed, collapsedGroupStatuses: /* @__PURE__ */ new Map(), originalGroups };
22255
+ }
22256
+ const nodeToGroup = /* @__PURE__ */ new Map();
22257
+ const collapsedGroupStatuses = /* @__PURE__ */ new Map();
22258
+ for (const group of parsed.groups) {
22259
+ if (!collapsedGroups.has(group.label)) continue;
22260
+ const children = group.nodeLabels.map((l) => parsed.nodes.find((n) => n.label === l)).filter((n) => n !== void 0);
22261
+ for (const node of children) nodeToGroup.set(node.label, group.label);
22262
+ collapsedGroupStatuses.set(group.label, rollUpStatus(children));
22263
+ }
22264
+ const nodes = parsed.nodes.filter((n) => !nodeToGroup.has(n.label));
22265
+ const edgeKeys = /* @__PURE__ */ new Set();
22266
+ const edges = [];
22267
+ for (const edge of parsed.edges) {
22268
+ const src = nodeToGroup.get(edge.source) ?? edge.source;
22269
+ const tgt = nodeToGroup.get(edge.target) ?? edge.target;
22270
+ if (src === tgt) continue;
22271
+ const key = `${src}|${tgt}|${edge.label ?? ""}`;
22272
+ if (edgeKeys.has(key)) continue;
22273
+ edgeKeys.add(key);
22274
+ edges.push({ ...edge, source: src, target: tgt });
22275
+ }
22276
+ const groups = parsed.groups.filter((g) => !collapsedGroups.has(g.label));
22277
+ return {
22278
+ parsed: { ...parsed, nodes, edges, groups },
22279
+ collapsedGroupStatuses,
22280
+ originalGroups
22281
+ };
22282
+ }
22283
+
22284
+ // src/index.ts
21846
22285
  init_parser8();
21847
22286
  init_layout2();
21848
22287
  init_renderer2();
@@ -22100,7 +22539,7 @@ async function resolveFile(content, filePath, readFileFn, diagnostics, ancestorC
22100
22539
  continue;
22101
22540
  }
22102
22541
  if (isTagBlockHeading(trimmed)) continue;
22103
- if (lines[i] !== trimmed) continue;
22542
+ if (/^\s/.test(lines[i])) continue;
22104
22543
  const tagsMatch = trimmed.match(TAGS_RE);
22105
22544
  if (tagsMatch) {
22106
22545
  tagsDirective = tagsMatch[1].trim();
@@ -22339,6 +22778,12 @@ function encodeDiagramUrl(dsl, options) {
22339
22778
  if (options?.viewState?.swimlaneTagGroup) {
22340
22779
  hash += `&swim=${encodeURIComponent(options.viewState.swimlaneTagGroup)}`;
22341
22780
  }
22781
+ if (options?.viewState?.palette && options.viewState.palette !== "nord") {
22782
+ hash += `&pal=${encodeURIComponent(options.viewState.palette)}`;
22783
+ }
22784
+ if (options?.viewState?.theme && options.viewState.theme !== "dark") {
22785
+ hash += `&th=${encodeURIComponent(options.viewState.theme)}`;
22786
+ }
22342
22787
  return { url: `${baseUrl}?${hash}#${hash}` };
22343
22788
  }
22344
22789
  function decodeDiagramUrl(hash) {
@@ -22365,6 +22810,8 @@ function decodeDiagramUrl(hash) {
22365
22810
  if (key === "swim" && val) {
22366
22811
  viewState.swimlaneTagGroup = val;
22367
22812
  }
22813
+ if (key === "pal" && val) viewState.palette = val;
22814
+ if (key === "th" && (val === "light" || val === "dark")) viewState.theme = val;
22368
22815
  }
22369
22816
  if (payload.startsWith("dgmo=")) {
22370
22817
  payload = payload.slice(5);
@@ -22398,6 +22845,7 @@ init_branding();
22398
22845
  buildRenderSequence,
22399
22846
  buildThemeCSS,
22400
22847
  catppuccinPalette,
22848
+ collapseInitiativeStatus,
22401
22849
  collapseOrgTree,
22402
22850
  collapseSitemapTree,
22403
22851
  collectDiagramRoles,