@diagrammo/dgmo 0.5.3 → 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.js CHANGED
@@ -6861,7 +6861,10 @@ var init_types2 = __esm({
6861
6861
  "buffer",
6862
6862
  "drain-rate",
6863
6863
  "retention-hours",
6864
- "partitions"
6864
+ "partitions",
6865
+ "slo-availability",
6866
+ "slo-p90-latency-ms",
6867
+ "slo-warning-margin"
6865
6868
  ]);
6866
6869
  EDGE_ONLY_KEYS = /* @__PURE__ */ new Set(["rps"]);
6867
6870
  }
@@ -6997,6 +7000,18 @@ function parseInfra(content) {
6997
7000
  result.options["default-uptime"] = trimmed.replace(/^default-uptime\s*:\s*/i, "").trim();
6998
7001
  continue;
6999
7002
  }
7003
+ if (/^slo-availability\s*:/i.test(trimmed)) {
7004
+ result.options["slo-availability"] = trimmed.replace(/^slo-availability\s*:\s*/i, "").trim();
7005
+ continue;
7006
+ }
7007
+ if (/^slo-p90-latency-ms\s*:/i.test(trimmed)) {
7008
+ result.options["slo-p90-latency-ms"] = trimmed.replace(/^slo-p90-latency-ms\s*:\s*/i, "").trim();
7009
+ continue;
7010
+ }
7011
+ if (/^slo-warning-margin\s*:/i.test(trimmed)) {
7012
+ result.options["slo-warning-margin"] = trimmed.replace(/^slo-warning-margin\s*:\s*/i, "").trim();
7013
+ continue;
7014
+ }
7000
7015
  if (/^scenario\s*:/i.test(trimmed)) {
7001
7016
  finishCurrentNode();
7002
7017
  finishCurrentTagGroup();
@@ -7141,12 +7156,19 @@ function parseInfra(content) {
7141
7156
  if (simpleConn) {
7142
7157
  const targetName = simpleConn[1].trim();
7143
7158
  const splitStr = simpleConn[2];
7159
+ const fanoutStr = simpleConn[3];
7144
7160
  const split = splitStr ? parseFloat(splitStr) : null;
7161
+ const fanoutRaw = fanoutStr ? parseInt(fanoutStr, 10) : null;
7162
+ if (fanoutRaw !== null && fanoutRaw < 1) {
7163
+ warn(lineNumber, `Fan-out multiplier must be at least 1 (got x${fanoutRaw}). Ignoring.`);
7164
+ }
7165
+ const fanout = fanoutRaw !== null && fanoutRaw >= 1 ? fanoutRaw : null;
7145
7166
  result.edges.push({
7146
7167
  sourceId: currentNode.id,
7147
7168
  targetId: nodeId2(targetName),
7148
7169
  label: "",
7149
7170
  split,
7171
+ fanout,
7150
7172
  lineNumber
7151
7173
  });
7152
7174
  continue;
@@ -7156,7 +7178,13 @@ function parseInfra(content) {
7156
7178
  const label = connMatch[1]?.trim() || "";
7157
7179
  const targetName = connMatch[2].trim();
7158
7180
  const splitStr = connMatch[3];
7181
+ const fanoutStr = connMatch[4];
7159
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;
7160
7188
  let targetId;
7161
7189
  const targetGroupMatch = targetName.match(GROUP_RE);
7162
7190
  if (targetGroupMatch) {
@@ -7169,14 +7197,20 @@ function parseInfra(content) {
7169
7197
  targetId,
7170
7198
  label,
7171
7199
  split,
7200
+ fanout,
7172
7201
  lineNumber
7173
7202
  });
7174
7203
  continue;
7175
7204
  }
7205
+ if (/^description\s*:\s*$/i.test(trimmed)) continue;
7176
7206
  const propMatch = trimmed.match(PROPERTY_RE);
7177
7207
  if (propMatch) {
7178
7208
  const key = propMatch[1].toLowerCase();
7179
7209
  const rawVal = propMatch[2].trim();
7210
+ if (key === "description" && currentNode) {
7211
+ if (!currentNode.isEdge) currentNode.description = rawVal;
7212
+ continue;
7213
+ }
7180
7214
  if (!INFRA_BEHAVIOR_KEYS.has(key) && !EDGE_ONLY_KEYS.has(key)) {
7181
7215
  const allKeys = [...INFRA_BEHAVIOR_KEYS, ...EDGE_ONLY_KEYS];
7182
7216
  let msg = `Unknown property '${key}'.`;
@@ -7278,12 +7312,12 @@ var init_parser9 = __esm({
7278
7312
  init_diagnostics();
7279
7313
  init_parsing();
7280
7314
  init_types2();
7281
- CONNECTION_RE = /^-(?:([^-].*?))?->\s+(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*$/;
7282
- SIMPLE_CONNECTION_RE = /^->\s+(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*$/;
7315
+ CONNECTION_RE = /^-(?:([^-].*?))?->\s+(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*(?:x(\d+))?\s*$/;
7316
+ SIMPLE_CONNECTION_RE = /^->\s+(.+?)(?:(?:\s*\|\s*|\s+)split\s*:?\s*(\d+)%)?\s*(?:x(\d+))?\s*$/;
7283
7317
  GROUP_RE = /^\[([^\]]+)\]$/;
7284
7318
  TAG_GROUP_RE = /^tag\s*:\s*(\w[\w\s]*?)(?:\s+alias\s+(\w+))?\s*$/;
7285
7319
  TAG_VALUE_RE = /^(\w[\w\s]*?)(?:\(([^)]+)\))?(\s+default)?\s*$/;
7286
- COMPONENT_RE = /^([a-zA-Z_][\w]*)(.*)$/;
7320
+ COMPONENT_RE = /^([a-zA-Z_][\w-]*)(.*)$/;
7287
7321
  PIPE_META_RE = /[|,]\s*(\w+)\s*:\s*([^|,]+)/g;
7288
7322
  PROPERTY_RE = /^([\w-]+)\s*:\s*(.+)$/;
7289
7323
  PERCENT_RE = /^([\d.]+)%$/;
@@ -7518,7 +7552,7 @@ function centerHeavyChildren(node) {
7518
7552
  }
7519
7553
  node.children = result;
7520
7554
  }
7521
- function computeLegendGroups(tagGroups, _showEyeIcons, usedValuesByGroup) {
7555
+ function computeLegendGroups(tagGroups, showEyeIcons, usedValuesByGroup) {
7522
7556
  const groups = [];
7523
7557
  for (const group of tagGroups) {
7524
7558
  if (group.entries.length === 0) continue;
@@ -7531,7 +7565,8 @@ function computeLegendGroups(tagGroups, _showEyeIcons, usedValuesByGroup) {
7531
7565
  for (const entry of visibleEntries) {
7532
7566
  entriesWidth += LEGEND_DOT_R * 2 + LEGEND_ENTRY_DOT_GAP + entry.value.length * LEGEND_ENTRY_FONT_W + LEGEND_ENTRY_TRAIL;
7533
7567
  }
7534
- const capsuleWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + entriesWidth;
7568
+ const eyeSpace = showEyeIcons ? LEGEND_EYE_SIZE + LEGEND_EYE_GAP : 0;
7569
+ const capsuleWidth = LEGEND_CAPSULE_PAD * 2 + pillWidth + 4 + eyeSpace + entriesWidth;
7535
7570
  groups.push({
7536
7571
  name: group.name,
7537
7572
  alias: group.alias,
@@ -8164,7 +8199,7 @@ function layoutOrg(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, expan
8164
8199
  height: finalHeight
8165
8200
  };
8166
8201
  }
8167
- var 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;
8202
+ var 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;
8168
8203
  var init_layout = __esm({
8169
8204
  "src/org/layout.ts"() {
8170
8205
  "use strict";
@@ -8194,6 +8229,8 @@ var init_layout = __esm({
8194
8229
  LEGEND_ENTRY_DOT_GAP = 4;
8195
8230
  LEGEND_ENTRY_TRAIL = 8;
8196
8231
  LEGEND_GROUP_GAP = 12;
8232
+ LEGEND_EYE_SIZE = 14;
8233
+ LEGEND_EYE_GAP = 6;
8197
8234
  }
8198
8235
  });
8199
8236
 
@@ -8296,22 +8333,27 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
8296
8333
  const layoutLegendShift = LEGEND_HEIGHT2 + LEGEND_GROUP_GAP2;
8297
8334
  const fixedLegend = !exportDims && hasLegend && !legendOnly;
8298
8335
  const legendReserve = fixedLegend ? LEGEND_HEIGHT2 + LEGEND_FIXED_GAP : 0;
8336
+ const fixedTitle = !exportDims && !!parsed.title;
8337
+ const titleReserve = fixedTitle ? TITLE_HEIGHT : 0;
8299
8338
  const diagramW = layout.width;
8300
- let diagramH = layout.height + titleOffset;
8339
+ let diagramH = layout.height + (fixedTitle ? 0 : titleOffset);
8301
8340
  if (fixedLegend) {
8302
8341
  diagramH -= layoutLegendShift;
8303
8342
  }
8304
- const availH = height - DIAGRAM_PADDING * 2 - legendReserve;
8343
+ const availH = height - DIAGRAM_PADDING * 2 - legendReserve - titleReserve;
8305
8344
  const scaleX = (width - DIAGRAM_PADDING * 2) / diagramW;
8306
8345
  const scaleY = availH / diagramH;
8307
8346
  const scale = Math.min(MAX_SCALE, scaleX, scaleY);
8308
8347
  const scaledW = diagramW * scale;
8309
8348
  const offsetX = (width - scaledW) / 2;
8310
- const offsetY = legendPosition === "top" && fixedLegend ? DIAGRAM_PADDING + legendReserve : DIAGRAM_PADDING;
8349
+ const offsetY = legendPosition === "top" && fixedLegend ? DIAGRAM_PADDING + legendReserve + titleReserve : DIAGRAM_PADDING + titleReserve;
8311
8350
  const svg = d3Selection.select(container).append("svg").attr("width", width).attr("height", height).style("font-family", FONT_FAMILY);
8312
8351
  const mainG = svg.append("g").attr("transform", `translate(${offsetX}, ${offsetY}) scale(${scale})`);
8313
8352
  if (parsed.title) {
8314
- 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(
8353
+ const titleParent = fixedTitle ? svg : mainG;
8354
+ const titleX = fixedTitle ? width / 2 : diagramW / 2;
8355
+ const titleY = fixedTitle ? DIAGRAM_PADDING + TITLE_FONT_SIZE : TITLE_FONT_SIZE;
8356
+ 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(
8315
8357
  "cursor",
8316
8358
  onClickItem && parsed.titleLineNumber ? "pointer" : "default"
8317
8359
  ).text(parsed.title);
@@ -8326,7 +8368,7 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
8326
8368
  }
8327
8369
  }
8328
8370
  }
8329
- const contentG = mainG.append("g").attr("transform", `translate(0, ${titleOffset})`);
8371
+ const contentG = mainG.append("g").attr("transform", `translate(0, ${fixedTitle ? 0 : titleOffset})`);
8330
8372
  const displayNames = /* @__PURE__ */ new Map();
8331
8373
  for (const group of parsed.tagGroups) {
8332
8374
  displayNames.set(group.name.toLowerCase(), group.name);
@@ -8454,7 +8496,7 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
8454
8496
  }
8455
8497
  const legendParent = fixedLegend ? svg.append("g").attr("class", "org-legend-fixed").attr(
8456
8498
  "transform",
8457
- legendPosition === "bottom" ? `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT2})` : `translate(0, ${DIAGRAM_PADDING})`
8499
+ legendPosition === "bottom" ? `translate(0, ${height - DIAGRAM_PADDING - LEGEND_HEIGHT2})` : `translate(0, ${DIAGRAM_PADDING + titleReserve})`
8458
8500
  ) : contentG;
8459
8501
  for (const group of visibleGroups) {
8460
8502
  const isActive = legendOnly || activeTagGroup != null && group.name.toLowerCase() === activeTagGroup.toLowerCase();
@@ -8475,8 +8517,19 @@ function renderOrg(container, parsed, layout, palette, isDark, onClickItem, expo
8475
8517
  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);
8476
8518
  }
8477
8519
  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);
8520
+ if (isActive && fixedLegend) {
8521
+ const groupKey = group.name.toLowerCase();
8522
+ const isHidden = hiddenAttributes?.has(groupKey) ?? false;
8523
+ const eyeX = pillXOff + pillWidth + LEGEND_EYE_GAP2;
8524
+ const eyeY = (LEGEND_HEIGHT2 - LEGEND_EYE_SIZE2) / 2;
8525
+ const hitPad = 6;
8526
+ 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);
8527
+ 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");
8528
+ 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");
8529
+ }
8478
8530
  if (isActive) {
8479
- let entryX = pillXOff + pillWidth + 4;
8531
+ const eyeShift = fixedLegend ? LEGEND_EYE_SIZE2 + LEGEND_EYE_GAP2 : 0;
8532
+ let entryX = pillXOff + pillWidth + 4 + eyeShift;
8480
8533
  for (const entry of group.entries) {
8481
8534
  const entryG = gEl.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
8482
8535
  entryG.append("circle").attr("cx", entryX + LEGEND_DOT_R2).attr("cy", LEGEND_HEIGHT2 / 2).attr("r", LEGEND_DOT_R2).attr("fill", entry.color);
@@ -8522,7 +8575,7 @@ function renderOrgForExport(content, theme, palette) {
8522
8575
  document.body.removeChild(container);
8523
8576
  }
8524
8577
  }
8525
- var 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;
8578
+ var 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;
8526
8579
  var init_renderer = __esm({
8527
8580
  "src/org/renderer.ts"() {
8528
8581
  "use strict";
@@ -8560,7 +8613,11 @@ var init_renderer = __esm({
8560
8613
  LEGEND_ENTRY_DOT_GAP2 = 4;
8561
8614
  LEGEND_ENTRY_TRAIL2 = 8;
8562
8615
  LEGEND_GROUP_GAP2 = 12;
8616
+ LEGEND_EYE_SIZE2 = 14;
8617
+ LEGEND_EYE_GAP2 = 6;
8563
8618
  LEGEND_FIXED_GAP = 8;
8619
+ 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";
8620
+ 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";
8564
8621
  }
8565
8622
  });
8566
8623
 
@@ -8610,7 +8667,7 @@ function computeLegendGroups2(tagGroups, usedValuesByGroup) {
8610
8667
  for (const entry of visibleEntries) {
8611
8668
  entriesWidth += LEGEND_DOT_R3 * 2 + LEGEND_ENTRY_DOT_GAP3 + entry.value.length * LEGEND_ENTRY_FONT_W3 + LEGEND_ENTRY_TRAIL3;
8612
8669
  }
8613
- const eyeSpace = LEGEND_EYE_SIZE + LEGEND_EYE_GAP;
8670
+ const eyeSpace = LEGEND_EYE_SIZE3 + LEGEND_EYE_GAP3;
8614
8671
  const capsuleWidth = LEGEND_CAPSULE_PAD3 * 2 + pillWidth + 4 + eyeSpace + entriesWidth;
8615
8672
  groups.push({
8616
8673
  name: group.name,
@@ -9012,7 +9069,7 @@ function layoutSitemap(parsed, hiddenCounts, activeTagGroup, hiddenAttributes, e
9012
9069
  height: totalHeight
9013
9070
  };
9014
9071
  }
9015
- var 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;
9072
+ var 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;
9016
9073
  var init_layout2 = __esm({
9017
9074
  "src/sitemap/layout.ts"() {
9018
9075
  "use strict";
@@ -9039,8 +9096,8 @@ var init_layout2 = __esm({
9039
9096
  LEGEND_ENTRY_DOT_GAP3 = 4;
9040
9097
  LEGEND_ENTRY_TRAIL3 = 8;
9041
9098
  LEGEND_GROUP_GAP3 = 12;
9042
- LEGEND_EYE_SIZE = 14;
9043
- LEGEND_EYE_GAP = 6;
9099
+ LEGEND_EYE_SIZE3 = 14;
9100
+ LEGEND_EYE_GAP3 = 6;
9044
9101
  OVERLAP_GAP = 20;
9045
9102
  }
9046
9103
  });
@@ -9394,15 +9451,15 @@ function renderLegend(parent, legendGroups, palette, isDark, activeTagGroup, fix
9394
9451
  if (isActive && fixedWidth != null) {
9395
9452
  const groupKey = group.name.toLowerCase();
9396
9453
  const isHidden = hiddenAttributes?.has(groupKey) ?? false;
9397
- const eyeX = pillXOff + pillW + LEGEND_EYE_GAP2;
9398
- const eyeY = (LEGEND_HEIGHT4 - LEGEND_EYE_SIZE2) / 2;
9454
+ const eyeX = pillXOff + pillW + LEGEND_EYE_GAP4;
9455
+ const eyeY = (LEGEND_HEIGHT4 - LEGEND_EYE_SIZE4) / 2;
9399
9456
  const hitPad = 6;
9400
9457
  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);
9401
- 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");
9402
- 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");
9458
+ 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");
9459
+ 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");
9403
9460
  }
9404
9461
  if (isActive) {
9405
- const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE2 + LEGEND_EYE_GAP2 : 0;
9462
+ const eyeShift = fixedWidth != null ? LEGEND_EYE_SIZE4 + LEGEND_EYE_GAP4 : 0;
9406
9463
  let entryX = pillXOff + pillW + 4 + eyeShift;
9407
9464
  for (const entry of group.entries) {
9408
9465
  const entryG = legendG.append("g").attr("data-legend-entry", entry.value.toLowerCase()).style("cursor", "pointer");
@@ -9455,7 +9512,7 @@ async function renderSitemapForExport(content, theme, palette) {
9455
9512
  const brandColor = theme === "transparent" ? "#888" : effectivePalette.textMuted;
9456
9513
  return injectBranding2(svgHtml, brandColor);
9457
9514
  }
9458
- var 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;
9515
+ var 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;
9459
9516
  var init_renderer2 = __esm({
9460
9517
  "src/sitemap/renderer.ts"() {
9461
9518
  "use strict";
@@ -9494,11 +9551,11 @@ var init_renderer2 = __esm({
9494
9551
  LEGEND_ENTRY_DOT_GAP4 = 4;
9495
9552
  LEGEND_ENTRY_TRAIL4 = 8;
9496
9553
  LEGEND_GROUP_GAP4 = 12;
9497
- LEGEND_EYE_SIZE2 = 14;
9498
- LEGEND_EYE_GAP2 = 6;
9554
+ LEGEND_EYE_SIZE4 = 14;
9555
+ LEGEND_EYE_GAP4 = 6;
9499
9556
  lineGenerator = d3Shape.line().x((d) => d.x).y((d) => d.y).curve(d3Shape.curveBasis);
9500
- 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";
9501
- 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";
9557
+ 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";
9558
+ 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";
9502
9559
  }
9503
9560
  });
9504
9561
 
@@ -10734,7 +10791,8 @@ var init_renderer5 = __esm({
10734
10791
  // src/initiative-status/layout.ts
10735
10792
  var layout_exports5 = {};
10736
10793
  __export(layout_exports5, {
10737
- layoutInitiativeStatus: () => layoutInitiativeStatus
10794
+ layoutInitiativeStatus: () => layoutInitiativeStatus,
10795
+ rollUpStatus: () => rollUpStatus
10738
10796
  });
10739
10797
  import dagre4 from "@dagrejs/dagre";
10740
10798
  function rollUpStatus(members) {
@@ -10749,14 +10807,28 @@ function rollUpStatus(members) {
10749
10807
  }
10750
10808
  return worst;
10751
10809
  }
10752
- function layoutInitiativeStatus(parsed) {
10753
- if (parsed.nodes.length === 0) {
10810
+ function layoutInitiativeStatus(parsed, collapseResult) {
10811
+ if (parsed.nodes.length === 0 && (!collapseResult || collapseResult.collapsedGroupStatuses.size === 0)) {
10754
10812
  return { nodes: [], edges: [], groups: [], width: 0, height: 0 };
10755
10813
  }
10756
- const hasGroups = parsed.groups.length > 0;
10814
+ const originalGroups = collapseResult?.originalGroups ?? parsed.groups;
10815
+ const collapsedGroupStatuses = collapseResult?.collapsedGroupStatuses ?? /* @__PURE__ */ new Map();
10816
+ const collapsedGroupLabels = new Set(
10817
+ originalGroups.map((g2) => g2.label).filter((l) => !parsed.groups.some((g2) => g2.label === l))
10818
+ );
10819
+ const hasGroups = parsed.groups.length > 0 || collapsedGroupLabels.size > 0;
10757
10820
  const g = new dagre4.graphlib.Graph({ multigraph: true, compound: hasGroups });
10758
10821
  g.setGraph({ rankdir: "LR", nodesep: NODESEP, ranksep: RANKSEP });
10759
10822
  g.setDefaultEdgeLabel(() => ({}));
10823
+ for (const group of originalGroups) {
10824
+ if (collapsedGroupLabels.has(group.label)) {
10825
+ const collapsedW = Math.max(
10826
+ NODE_WIDTH,
10827
+ Math.ceil(group.label.length * CHAR_WIDTH_RATIO * NODE_FONT_SIZE) + NODE_TEXT_PADDING * 2
10828
+ );
10829
+ g.setNode(group.label, { label: group.label, width: collapsedW, height: NODE_HEIGHT });
10830
+ }
10831
+ }
10760
10832
  for (const group of parsed.groups) {
10761
10833
  g.setNode(`__group_${group.label}`, { label: group.label, clusterLabelPos: "top" });
10762
10834
  }
@@ -10787,38 +10859,130 @@ function layoutInitiativeStatus(parsed) {
10787
10859
  height: pos.height
10788
10860
  };
10789
10861
  });
10790
- const nodeMap = new Map(layoutNodes.map((n) => [n.label, n]));
10791
- const allNodeX = layoutNodes.map((n) => n.x);
10792
- const layoutEdges = parsed.edges.map((edge, i) => {
10793
- const src = nodeMap.get(edge.source);
10794
- const tgt = nodeMap.get(edge.target);
10862
+ const posMap = new Map(layoutNodes.map((n) => [n.label, n]));
10863
+ for (const label of collapsedGroupLabels) {
10864
+ const pos = g.node(label);
10865
+ if (pos) posMap.set(label, { x: pos.x, y: pos.y, width: pos.width, height: pos.height });
10866
+ }
10867
+ const allNodeX = [...posMap.values()].map((n) => n.x);
10868
+ const avgNodeY = layoutNodes.length > 0 ? layoutNodes.reduce((s, n) => s + n.y, 0) / layoutNodes.length : 0;
10869
+ const avgNodeX = layoutNodes.length > 0 ? layoutNodes.reduce((s, n) => s + n.x, 0) / layoutNodes.length : 0;
10870
+ const edgeYOffsets = new Array(parsed.edges.length).fill(0);
10871
+ const edgeParallelCounts = new Array(parsed.edges.length).fill(1);
10872
+ const parallelGroups = /* @__PURE__ */ new Map();
10873
+ for (let i = 0; i < parsed.edges.length; i++) {
10874
+ const edge = parsed.edges[i];
10875
+ const key = `${edge.source}\0${edge.target}`;
10876
+ parallelGroups.set(key, parallelGroups.get(key) ?? []);
10877
+ parallelGroups.get(key).push(i);
10878
+ }
10879
+ for (const group of parallelGroups.values()) {
10880
+ const capped = group.slice(0, MAX_PARALLEL_EDGES);
10881
+ for (const idx of group.slice(MAX_PARALLEL_EDGES)) {
10882
+ edgeParallelCounts[idx] = 0;
10883
+ }
10884
+ if (capped.length < 2) continue;
10885
+ const effectiveSpacing = Math.min(PARALLEL_SPACING, (NODE_HEIGHT - PARALLEL_EDGE_MARGIN) / (capped.length - 1));
10886
+ for (let j = 0; j < capped.length; j++) {
10887
+ edgeYOffsets[capped[j]] = (j - (capped.length - 1) / 2) * effectiveSpacing;
10888
+ edgeParallelCounts[capped[j]] = capped.length;
10889
+ }
10890
+ }
10891
+ const layoutEdges = [];
10892
+ for (let i = 0; i < parsed.edges.length; i++) {
10893
+ const edge = parsed.edges[i];
10894
+ const src = posMap.get(edge.source);
10895
+ const tgt = posMap.get(edge.target);
10896
+ if (edgeParallelCounts[i] === 0) continue;
10897
+ if (!src || !tgt) continue;
10898
+ const yOffset = edgeYOffsets[i];
10899
+ const parallelCount = edgeParallelCounts[i];
10795
10900
  const exitX = src.x + src.width / 2;
10796
10901
  const enterX = tgt.x - tgt.width / 2;
10797
10902
  const dagreEdge = g.edge(edge.source, edge.target, `e${i}`);
10798
10903
  const dagrePoints = dagreEdge?.points ?? [];
10799
10904
  const hasIntermediateRank = allNodeX.some((x) => x > src.x + 20 && x < tgt.x - 20);
10800
10905
  const step = Math.min((enterX - exitX) * 0.15, 20);
10801
- const fixedDagrePoints = dagrePoints.length >= 2 ? [
10802
- { x: exitX, y: src.y },
10803
- ...dagrePoints.slice(1, -1),
10804
- { x: enterX, y: tgt.y }
10805
- ] : dagrePoints;
10806
- const points = tgt.x > src.x && !hasIntermediateRank ? [
10807
- { x: exitX, y: src.y },
10808
- { x: exitX + step, y: src.y },
10809
- { x: enterX - step, y: tgt.y },
10810
- { x: enterX, y: tgt.y }
10811
- ] : fixedDagrePoints;
10812
- return {
10906
+ const isBackEdge = tgt.x < src.x - 5;
10907
+ const isYDisplaced = !isBackEdge && Math.abs(tgt.y - src.y) > NODESEP;
10908
+ let points;
10909
+ if (isBackEdge) {
10910
+ const routeAbove = Math.min(src.y, tgt.y) > avgNodeY;
10911
+ const srcHalfH = src.height / 2;
10912
+ const tgtHalfH = tgt.height / 2;
10913
+ const rawMidX = (src.x + tgt.x) / 2;
10914
+ const spreadDir = avgNodeX < rawMidX ? 1 : -1;
10915
+ const unclamped = Math.abs(src.x - tgt.x) < NODE_WIDTH ? rawMidX + spreadDir * BACK_EDGE_MIN_SPREAD : rawMidX;
10916
+ const midX = Math.min(src.x, Math.max(tgt.x, unclamped));
10917
+ if (routeAbove) {
10918
+ const arcY = Math.min(src.y - srcHalfH, tgt.y - tgtHalfH) - BACK_EDGE_MARGIN;
10919
+ points = [
10920
+ { x: src.x, y: src.y - srcHalfH },
10921
+ { x: midX, y: arcY },
10922
+ { x: tgt.x, y: tgt.y - tgtHalfH }
10923
+ ];
10924
+ } else {
10925
+ const arcY = Math.max(src.y + srcHalfH, tgt.y + tgtHalfH) + BACK_EDGE_MARGIN;
10926
+ points = [
10927
+ { x: src.x, y: src.y + srcHalfH },
10928
+ { x: midX, y: arcY },
10929
+ { x: tgt.x, y: tgt.y + tgtHalfH }
10930
+ ];
10931
+ }
10932
+ } else if (isYDisplaced) {
10933
+ const exitY = tgt.y > src.y + NODESEP ? src.y + src.height / 2 : src.y - src.height / 2;
10934
+ const midX = Math.max(src.x + 1, (src.x + enterX) / 2);
10935
+ const midY = (exitY + tgt.y) / 2;
10936
+ points = [
10937
+ { x: src.x, y: exitY },
10938
+ { x: midX, y: midY },
10939
+ { x: enterX, y: tgt.y }
10940
+ ];
10941
+ } else if (tgt.x > src.x && !hasIntermediateRank) {
10942
+ points = [
10943
+ { x: exitX, y: src.y },
10944
+ // exits node center — stays pinned
10945
+ { x: exitX + step, y: src.y + yOffset },
10946
+ // fans out
10947
+ { x: enterX - step, y: tgt.y + yOffset },
10948
+ // still fanned
10949
+ { x: enterX, y: tgt.y }
10950
+ // enters node center — stays pinned
10951
+ ];
10952
+ } else {
10953
+ points = dagrePoints.length >= 2 ? [
10954
+ { x: exitX, y: src.y + yOffset },
10955
+ ...dagrePoints.slice(1, -1),
10956
+ { x: enterX, y: tgt.y + yOffset }
10957
+ ] : dagrePoints;
10958
+ }
10959
+ layoutEdges.push({
10813
10960
  source: edge.source,
10814
10961
  target: edge.target,
10815
10962
  label: edge.label,
10816
10963
  status: edge.status,
10817
10964
  lineNumber: edge.lineNumber,
10818
- points
10819
- };
10820
- });
10965
+ points,
10966
+ parallelCount
10967
+ });
10968
+ }
10821
10969
  const layoutGroups = [];
10970
+ for (const group of originalGroups) {
10971
+ if (collapsedGroupLabels.has(group.label)) {
10972
+ const pos = g.node(group.label);
10973
+ if (!pos) continue;
10974
+ layoutGroups.push({
10975
+ label: group.label,
10976
+ status: collapsedGroupStatuses.get(group.label) ?? null,
10977
+ x: pos.x - pos.width / 2,
10978
+ y: pos.y - pos.height / 2,
10979
+ width: pos.width,
10980
+ height: pos.height,
10981
+ lineNumber: group.lineNumber,
10982
+ collapsed: true
10983
+ });
10984
+ }
10985
+ }
10822
10986
  if (parsed.groups.length > 0) {
10823
10987
  const nMap = new Map(layoutNodes.map((n) => [n.label, n]));
10824
10988
  for (const group of parsed.groups) {
@@ -10842,7 +11006,8 @@ function layoutInitiativeStatus(parsed) {
10842
11006
  y: minY - GROUP_PADDING,
10843
11007
  width: maxX - minX + GROUP_PADDING * 2,
10844
11008
  height: maxY - minY + GROUP_PADDING * 2,
10845
- lineNumber: group.lineNumber
11009
+ lineNumber: group.lineNumber,
11010
+ collapsed: false
10846
11011
  });
10847
11012
  }
10848
11013
  }
@@ -10868,7 +11033,7 @@ function layoutInitiativeStatus(parsed) {
10868
11033
  totalHeight += 40;
10869
11034
  return { nodes: layoutNodes, edges: layoutEdges, groups: layoutGroups, width: totalWidth, height: totalHeight };
10870
11035
  }
10871
- var STATUS_PRIORITY, PHI, NODE_HEIGHT, NODE_WIDTH, GROUP_PADDING, NODESEP, RANKSEP;
11036
+ var 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;
10872
11037
  var init_layout5 = __esm({
10873
11038
  "src/initiative-status/layout.ts"() {
10874
11039
  "use strict";
@@ -10879,6 +11044,14 @@ var init_layout5 = __esm({
10879
11044
  GROUP_PADDING = 20;
10880
11045
  NODESEP = 80;
10881
11046
  RANKSEP = 160;
11047
+ PARALLEL_SPACING = 16;
11048
+ PARALLEL_EDGE_MARGIN = 12;
11049
+ MAX_PARALLEL_EDGES = 5;
11050
+ BACK_EDGE_MARGIN = 40;
11051
+ BACK_EDGE_MIN_SPREAD = Math.round(NODE_WIDTH * 0.75);
11052
+ CHAR_WIDTH_RATIO = 0.6;
11053
+ NODE_FONT_SIZE = 13;
11054
+ NODE_TEXT_PADDING = 12;
10882
11055
  }
10883
11056
  });
10884
11057
 
@@ -10936,10 +11109,10 @@ function splitCamelCase(word) {
10936
11109
  return parts.length > 1 ? parts : [word];
10937
11110
  }
10938
11111
  function fitTextToNode(label, nodeWidth, nodeHeight) {
10939
- const maxTextWidth = nodeWidth - NODE_TEXT_PADDING * 2;
11112
+ const maxTextWidth = nodeWidth - NODE_TEXT_PADDING2 * 2;
10940
11113
  const lineHeight = 1.3;
10941
- for (let fontSize = NODE_FONT_SIZE; fontSize >= MIN_NODE_FONT_SIZE; fontSize--) {
10942
- const charWidth2 = fontSize * CHAR_WIDTH_RATIO;
11114
+ for (let fontSize = NODE_FONT_SIZE2; fontSize >= MIN_NODE_FONT_SIZE; fontSize--) {
11115
+ const charWidth2 = fontSize * CHAR_WIDTH_RATIO2;
10943
11116
  const maxCharsPerLine = Math.floor(maxTextWidth / charWidth2);
10944
11117
  const maxLines = Math.floor((nodeHeight - 8) / (fontSize * lineHeight));
10945
11118
  if (maxCharsPerLine < 2 || maxLines < 1) continue;
@@ -11000,8 +11173,8 @@ function fitTextToNode(label, nodeWidth, nodeHeight) {
11000
11173
  return { lines: hardLines, fontSize };
11001
11174
  }
11002
11175
  }
11003
- const charWidth = MIN_NODE_FONT_SIZE * CHAR_WIDTH_RATIO;
11004
- const maxChars = Math.floor((nodeWidth - NODE_TEXT_PADDING * 2) / charWidth);
11176
+ const charWidth = MIN_NODE_FONT_SIZE * CHAR_WIDTH_RATIO2;
11177
+ const maxChars = Math.floor((nodeWidth - NODE_TEXT_PADDING2 * 2) / charWidth);
11005
11178
  const truncated = label.length > maxChars ? label.slice(0, maxChars - 1) + "\u2026" : label;
11006
11179
  return { lines: [truncated], fontSize: MIN_NODE_FONT_SIZE };
11007
11180
  }
@@ -11224,21 +11397,27 @@ function renderInitiativeStatus(container, parsed, layout, palette, isDark, onCl
11224
11397
  const labelMap = /* @__PURE__ */ new Map();
11225
11398
  for (const lp of labelPlacements) labelMap.set(lp.edgeIdx, lp);
11226
11399
  for (const group of layout.groups) {
11227
- if (group.width === 0 && group.height === 0) continue;
11228
- const gx = group.x - GROUP_EXTRA_PADDING;
11229
- const gy = group.y - GROUP_EXTRA_PADDING - GROUP_LABEL_FONT_SIZE - 4;
11230
- const gw = group.width + GROUP_EXTRA_PADDING * 2;
11231
- const gh = group.height + GROUP_EXTRA_PADDING * 2 + GROUP_LABEL_FONT_SIZE + 4;
11232
- const groupStatusColor = group.status ? statusColor(group.status, palette, isDark) : palette.textMuted;
11233
- const fillColor = mix(groupStatusColor, isDark ? palette.surface : palette.bg, 15);
11234
- const strokeColor = mix(groupStatusColor, palette.textMuted, 50);
11235
- const groupG = contentG.append("g").attr("class", "is-group").attr("data-line-number", String(group.lineNumber));
11236
- 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);
11237
- 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);
11238
- if (onClickItem) {
11239
- groupG.style("cursor", "pointer").on("click", () => {
11240
- onClickItem(group.lineNumber);
11241
- });
11400
+ if (group.collapsed) {
11401
+ const fillCol = nodeFill4(group.status, palette, isDark);
11402
+ const strokeCol = nodeStroke4(group.status, palette, isDark);
11403
+ const textCol = nodeTextColor(group.status, palette, isDark);
11404
+ const clipId = `clip-group-${group.lineNumber}`;
11405
+ 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");
11406
+ 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);
11407
+ 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);
11408
+ 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");
11409
+ 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);
11410
+ } else {
11411
+ if (group.width === 0 && group.height === 0) continue;
11412
+ const gx = group.x - GROUP_EXTRA_PADDING;
11413
+ const gy = group.y - GROUP_EXTRA_PADDING - GROUP_LABEL_FONT_SIZE - 4;
11414
+ const gw = group.width + GROUP_EXTRA_PADDING * 2;
11415
+ const gh = group.height + GROUP_EXTRA_PADDING * 2 + GROUP_LABEL_FONT_SIZE + 4;
11416
+ const fillColor = isDark ? palette.surface : palette.bg;
11417
+ const strokeColor = palette.textMuted;
11418
+ 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");
11419
+ 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);
11420
+ 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);
11242
11421
  }
11243
11422
  }
11244
11423
  for (let ei = 0; ei < layout.edges.length; ei++) {
@@ -11249,7 +11428,7 @@ function renderInitiativeStatus(container, parsed, layout, palette, isDark, onCl
11249
11428
  const edgeG = contentG.append("g").attr("class", "is-edge-group").attr("data-line-number", String(edge.lineNumber));
11250
11429
  const pathD = lineGenerator4(edge.points);
11251
11430
  if (pathD) {
11252
- edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", "transparent").attr("stroke-width", 16);
11431
+ edgeG.append("path").attr("d", pathD).attr("fill", "none").attr("stroke", "transparent").attr("stroke-width", Math.max(6, Math.round(16 / (edge.parallelCount ?? 1))));
11253
11432
  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");
11254
11433
  }
11255
11434
  const lp = labelMap.get(ei);
@@ -11329,7 +11508,7 @@ function renderInitiativeStatusForExport(content, theme, palette) {
11329
11508
  document.body.removeChild(container);
11330
11509
  }
11331
11510
  }
11332
- var 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;
11511
+ var 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;
11333
11512
  var init_renderer6 = __esm({
11334
11513
  "src/initiative-status/renderer.ts"() {
11335
11514
  "use strict";
@@ -11339,19 +11518,20 @@ var init_renderer6 = __esm({
11339
11518
  init_layout5();
11340
11519
  DIAGRAM_PADDING6 = 20;
11341
11520
  MAX_SCALE5 = 3;
11342
- NODE_FONT_SIZE = 13;
11521
+ NODE_FONT_SIZE2 = 13;
11343
11522
  MIN_NODE_FONT_SIZE = 9;
11344
11523
  EDGE_LABEL_FONT_SIZE4 = 11;
11345
11524
  EDGE_STROKE_WIDTH5 = 2;
11346
11525
  NODE_STROKE_WIDTH5 = 2;
11347
11526
  NODE_RX = 8;
11348
- ARROWHEAD_W2 = 10;
11349
- ARROWHEAD_H2 = 7;
11350
- CHAR_WIDTH_RATIO = 0.6;
11351
- NODE_TEXT_PADDING = 12;
11527
+ ARROWHEAD_W2 = 5;
11528
+ ARROWHEAD_H2 = 4;
11529
+ CHAR_WIDTH_RATIO2 = 0.6;
11530
+ NODE_TEXT_PADDING2 = 12;
11352
11531
  SERVICE_RX = 10;
11353
11532
  GROUP_EXTRA_PADDING = 8;
11354
11533
  GROUP_LABEL_FONT_SIZE = 11;
11534
+ COLLAPSE_BAR_HEIGHT3 = 6;
11355
11535
  lineGenerator4 = d3Shape4.line().x((d) => d.x).y((d) => d.y).curve(d3Shape4.curveMonotoneX);
11356
11536
  }
11357
11537
  });
@@ -14129,7 +14309,7 @@ function renderFlowchart(container, graph, layout, palette, isDark, onClickItem,
14129
14309
  });
14130
14310
  }
14131
14311
  renderNodeShape2(nodeG, node, palette, isDark, endTerminalIds, colorOff);
14132
- 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);
14312
+ 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);
14133
14313
  }
14134
14314
  }
14135
14315
  function renderFlowchartForExport(content, theme, palette) {
@@ -14167,7 +14347,7 @@ function renderFlowchartForExport(content, theme, palette) {
14167
14347
  document.body.removeChild(container);
14168
14348
  }
14169
14349
  }
14170
- var 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;
14350
+ var 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;
14171
14351
  var init_flowchart_renderer = __esm({
14172
14352
  "src/graph/flowchart-renderer.ts"() {
14173
14353
  "use strict";
@@ -14177,7 +14357,7 @@ var init_flowchart_renderer = __esm({
14177
14357
  init_layout7();
14178
14358
  DIAGRAM_PADDING8 = 20;
14179
14359
  MAX_SCALE7 = 3;
14180
- NODE_FONT_SIZE2 = 13;
14360
+ NODE_FONT_SIZE3 = 13;
14181
14361
  EDGE_LABEL_FONT_SIZE6 = 11;
14182
14362
  EDGE_STROKE_WIDTH7 = 1.5;
14183
14363
  NODE_STROKE_WIDTH7 = 1.5;
@@ -14365,23 +14545,26 @@ function collapseGroups(parsed, collapsedIds, defaultLatencyMs = 0, defaultUptim
14365
14545
  if (!collapsedIds.has(group.id)) continue;
14366
14546
  const children = groupChildren.get(group.id) ?? [];
14367
14547
  if (children.length === 0) continue;
14368
- let totalLatency = 0;
14369
14548
  let minEffectiveCapacity = Infinity;
14370
14549
  let hasMaxRps = false;
14371
14550
  let composedUptime = 1;
14372
14551
  const behaviorProps = [];
14373
14552
  const perChildCapacities = [];
14553
+ const childIdSet = new Set(children.map((c) => c.id));
14554
+ const childLatencies = /* @__PURE__ */ new Map();
14374
14555
  for (const child of children) {
14375
14556
  const latencyProp = child.properties.find((p) => p.key === "latency-ms");
14376
14557
  const childIsServerless = child.properties.some((p) => p.key === "concurrency");
14558
+ let childLat;
14377
14559
  if (childIsServerless) {
14378
14560
  const durationProp = child.properties.find((p) => p.key === "duration-ms");
14379
- totalLatency += durationProp ? typeof durationProp.value === "number" ? durationProp.value : parseFloat(String(durationProp.value)) || 100 : 100;
14561
+ childLat = durationProp ? typeof durationProp.value === "number" ? durationProp.value : parseFloat(String(durationProp.value)) || 100 : 100;
14380
14562
  } else if (latencyProp) {
14381
- totalLatency += typeof latencyProp.value === "number" ? latencyProp.value : parseFloat(String(latencyProp.value)) || 0;
14563
+ childLat = typeof latencyProp.value === "number" ? latencyProp.value : parseFloat(String(latencyProp.value)) || 0;
14382
14564
  } else {
14383
- totalLatency += defaultLatencyMs;
14565
+ childLat = defaultLatencyMs;
14384
14566
  }
14567
+ childLatencies.set(child.id, childLat);
14385
14568
  const maxRps = child.properties.find((p) => p.key === "max-rps");
14386
14569
  if (maxRps) {
14387
14570
  hasMaxRps = true;
@@ -14412,6 +14595,59 @@ function collapseGroups(parsed, collapsedIds, defaultLatencyMs = 0, defaultUptim
14412
14595
  }
14413
14596
  }
14414
14597
  }
14598
+ const entryIds = /* @__PURE__ */ new Set();
14599
+ const exitIds = /* @__PURE__ */ new Set();
14600
+ for (const edge of inboundEdges) {
14601
+ if (childIdSet.has(edge.targetId)) entryIds.add(edge.targetId);
14602
+ }
14603
+ for (const edge of outboundEdges) {
14604
+ if (childIdSet.has(edge.sourceId)) exitIds.add(edge.sourceId);
14605
+ }
14606
+ for (const edge of crossGroupEdges) {
14607
+ if (childIdSet.has(edge.sourceId)) exitIds.add(edge.sourceId);
14608
+ if (childIdSet.has(edge.targetId)) entryIds.add(edge.targetId);
14609
+ }
14610
+ const fwdAdj = /* @__PURE__ */ new Map();
14611
+ for (const edge of internalEdges) {
14612
+ if (!childIdSet.has(edge.sourceId) || !childIdSet.has(edge.targetId)) continue;
14613
+ const list = fwdAdj.get(edge.sourceId) ?? [];
14614
+ list.push(edge.targetId);
14615
+ fwdAdj.set(edge.sourceId, list);
14616
+ }
14617
+ const topoOrder = [];
14618
+ const tsVisited = /* @__PURE__ */ new Set();
14619
+ const dfsTopoSort = (id) => {
14620
+ if (tsVisited.has(id)) return;
14621
+ tsVisited.add(id);
14622
+ for (const next of fwdAdj.get(id) ?? []) dfsTopoSort(next);
14623
+ topoOrder.unshift(id);
14624
+ };
14625
+ for (const child of children) dfsTopoSort(child.id);
14626
+ const dist = /* @__PURE__ */ new Map();
14627
+ for (const child of children) {
14628
+ dist.set(child.id, entryIds.has(child.id) ? childLatencies.get(child.id) ?? 0 : -Infinity);
14629
+ }
14630
+ for (const nodeId3 of topoOrder) {
14631
+ const curDist = dist.get(nodeId3) ?? -Infinity;
14632
+ if (curDist === -Infinity) continue;
14633
+ for (const nextId of fwdAdj.get(nodeId3) ?? []) {
14634
+ const newDist = curDist + (childLatencies.get(nextId) ?? 0);
14635
+ if (newDist > (dist.get(nextId) ?? -Infinity)) dist.set(nextId, newDist);
14636
+ }
14637
+ }
14638
+ let totalLatency = 0;
14639
+ if (exitIds.size > 0) {
14640
+ for (const id of exitIds) {
14641
+ const d = dist.get(id);
14642
+ if (d !== void 0 && d > -Infinity && d > totalLatency) totalLatency = d;
14643
+ }
14644
+ } else if (entryIds.size > 0) {
14645
+ for (const [, d] of dist) {
14646
+ if (d > 0 && d > totalLatency) totalLatency = d;
14647
+ }
14648
+ } else {
14649
+ for (const [, lat] of childLatencies) totalLatency += lat;
14650
+ }
14415
14651
  const props = [];
14416
14652
  if (totalLatency > 0) props.push({ key: "latency-ms", value: totalLatency, lineNumber: group.lineNumber });
14417
14653
  const groupInstances = typeof group.instances === "number" ? group.instances : 1;
@@ -14644,8 +14880,10 @@ function computeInfra(parsed, params = {}) {
14644
14880
  const resolved = resolveSplits(outbound, diagnostics);
14645
14881
  for (const { edge, split } of resolved) {
14646
14882
  const edgeRps = outboundRps * (split / 100);
14883
+ const fanout = edge.fanout != null && edge.fanout >= 1 ? edge.fanout : 1;
14884
+ const fanoutedRps = edgeRps * fanout;
14647
14885
  const edgeKey = `${edge.sourceId}->${edge.targetId}`;
14648
- computedEdgeRps.set(edgeKey, edgeRps);
14886
+ computedEdgeRps.set(edgeKey, fanoutedRps);
14649
14887
  let targetIds;
14650
14888
  const groupChildren = groupChildMap.get(edge.targetId);
14651
14889
  if (groupChildren && groupChildren.length > 0) {
@@ -14654,7 +14892,7 @@ function computeInfra(parsed, params = {}) {
14654
14892
  targetIds = [edge.targetId];
14655
14893
  }
14656
14894
  for (const targetId of targetIds) {
14657
- const perTarget = edgeRps / targetIds.length;
14895
+ const perTarget = fanoutedRps / targetIds.length;
14658
14896
  const existing = computedRps.get(targetId) ?? 0;
14659
14897
  computedRps.set(targetId, existing + perTarget);
14660
14898
  const prevLatency = computedLatency.get(targetId) ?? 0;
@@ -14750,6 +14988,7 @@ function computeInfra(parsed, params = {}) {
14750
14988
  const resolved = resolveSplits(outbound, []);
14751
14989
  const paths = [];
14752
14990
  for (const { edge, split } of resolved) {
14991
+ const fanout = edge.fanout != null && edge.fanout >= 1 ? edge.fanout : 1;
14753
14992
  const groupChildren = groupChildMap.get(edge.targetId);
14754
14993
  const targetIds = groupChildren && groupChildren.length > 0 ? groupChildren : [edge.targetId];
14755
14994
  for (const targetId of targetIds) {
@@ -14760,20 +14999,20 @@ function computeInfra(parsed, params = {}) {
14760
14999
  latency: nodeLatency + cp.latency,
14761
15000
  uptime: nodeUptimeFrac * cp.uptime,
14762
15001
  availability: nodeAvail * cp.availability,
14763
- weight: cp.weight * (split / 100) / targetIds.length * 0.95
15002
+ weight: cp.weight * (split / 100) / targetIds.length * fanout * 0.95
14764
15003
  });
14765
15004
  paths.push({
14766
15005
  latency: coldLatency + cp.latency,
14767
15006
  uptime: nodeUptimeFrac * cp.uptime,
14768
15007
  availability: nodeAvail * cp.availability,
14769
- weight: cp.weight * (split / 100) / targetIds.length * 0.05
15008
+ weight: cp.weight * (split / 100) / targetIds.length * fanout * 0.05
14770
15009
  });
14771
15010
  } else {
14772
15011
  paths.push({
14773
15012
  latency: nodeLatency + cp.latency,
14774
15013
  uptime: nodeUptimeFrac * cp.uptime,
14775
15014
  availability: nodeAvail * cp.availability,
14776
- weight: cp.weight * (split / 100) / targetIds.length
15015
+ weight: cp.weight * (split / 100) / targetIds.length * fanout
14777
15016
  });
14778
15017
  }
14779
15018
  }
@@ -14928,6 +15167,7 @@ function computeInfra(parsed, params = {}) {
14928
15167
  queueMetrics,
14929
15168
  properties: node.properties,
14930
15169
  tags: node.tags,
15170
+ description: node.description,
14931
15171
  lineNumber: node.lineNumber
14932
15172
  };
14933
15173
  });
@@ -14952,6 +15192,7 @@ function computeInfra(parsed, params = {}) {
14952
15192
  label: edge.label,
14953
15193
  computedRps: rps,
14954
15194
  split: resolvedSplit,
15195
+ fanout: edge.fanout,
14955
15196
  lineNumber: edge.lineNumber
14956
15197
  };
14957
15198
  });
@@ -14978,7 +15219,8 @@ var init_compute = __esm({
14978
15219
  // src/infra/layout.ts
14979
15220
  var layout_exports8 = {};
14980
15221
  __export(layout_exports8, {
14981
- layoutInfra: () => layoutInfra
15222
+ layoutInfra: () => layoutInfra,
15223
+ separateGroups: () => separateGroups
14982
15224
  });
14983
15225
  import dagre7 from "@dagrejs/dagre";
14984
15226
  function countDisplayProps(node, expanded, options) {
@@ -15045,7 +15287,7 @@ function computeNodeWidth2(node, expanded, options) {
15045
15287
  if (expanded) {
15046
15288
  allKeys.push("p50", "p90", "p99");
15047
15289
  } else {
15048
- allKeys.push("p99");
15290
+ allKeys.push("p90");
15049
15291
  }
15050
15292
  }
15051
15293
  if (node.computedUptime < 1) {
@@ -15087,13 +15329,21 @@ function computeNodeWidth2(node, expanded, options) {
15087
15329
  }
15088
15330
  if (computedRows > 0) {
15089
15331
  const perc = node.computedLatencyPercentiles;
15090
- const msValues = expanded ? [perc.p50, perc.p90, perc.p99] : [perc.p99];
15332
+ const msValues = expanded ? [perc.p50, perc.p90, perc.p99] : [perc.p90];
15091
15333
  for (const ms of msValues) {
15092
15334
  if (ms > 0) {
15093
15335
  const valLen = formatMs(ms).length;
15094
15336
  maxRowWidth = Math.max(maxRowWidth, (maxKeyLen + 2 + valLen) * META_CHAR_WIDTH3);
15095
15337
  }
15096
15338
  }
15339
+ if (perc.p90 > 0) {
15340
+ const rawThreshold = node.properties.find((p) => p.key === "slo-p90-latency-ms")?.value ?? options?.["slo-p90-latency-ms"];
15341
+ const threshold = rawThreshold != null ? parseFloat(String(rawThreshold)) : NaN;
15342
+ if (!isNaN(threshold) && threshold > 0) {
15343
+ const combinedVal = `${formatMs(perc.p90)} / ${formatMs(threshold)}`;
15344
+ maxRowWidth = Math.max(maxRowWidth, (maxKeyLen + 2 + combinedVal.length) * META_CHAR_WIDTH3);
15345
+ }
15346
+ }
15097
15347
  if (node.computedUptime < 1) {
15098
15348
  const valLen = formatUptime(node.computedUptime).length;
15099
15349
  maxRowWidth = Math.max(maxRowWidth, (maxKeyLen + 2 + valLen) * META_CHAR_WIDTH3);
@@ -15106,21 +15356,26 @@ function computeNodeWidth2(node, expanded, options) {
15106
15356
  maxRowWidth = Math.max(maxRowWidth, "CB: OPEN".length * META_CHAR_WIDTH3 + 8);
15107
15357
  }
15108
15358
  }
15109
- return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20);
15359
+ const DESC_MAX_CHARS2 = 120;
15360
+ const descText = expanded && node.description && !node.isEdge ? node.description : "";
15361
+ const descTruncated = descText.length > DESC_MAX_CHARS2 ? descText.slice(0, DESC_MAX_CHARS2 - 1) + "\u2026" : descText;
15362
+ const descWidth = descTruncated.length > 0 ? descTruncated.length * META_CHAR_WIDTH3 + PADDING_X3 : 0;
15363
+ return Math.max(MIN_NODE_WIDTH2, labelWidth, maxRowWidth + 20, descWidth);
15110
15364
  }
15111
15365
  function computeNodeHeight2(node, expanded, options) {
15112
15366
  const propCount = countDisplayProps(node, expanded, options);
15113
15367
  const computedCount = countComputedRows(node, expanded);
15114
15368
  const hasRps = node.computedRps > 0;
15115
- if (propCount === 0 && computedCount === 0 && !hasRps) return NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM;
15116
- let h = NODE_HEADER_HEIGHT + NODE_SEPARATOR_GAP;
15369
+ const descH = expanded && node.description && !node.isEdge ? META_LINE_HEIGHT7 : 0;
15370
+ if (propCount === 0 && computedCount === 0 && !hasRps) return NODE_HEADER_HEIGHT + descH + NODE_PAD_BOTTOM;
15371
+ let h = NODE_HEADER_HEIGHT + descH + NODE_SEPARATOR_GAP;
15117
15372
  const computedSectionCount = (hasRps ? 1 : 0) + computedCount;
15118
15373
  h += computedSectionCount * META_LINE_HEIGHT7;
15119
15374
  if (computedSectionCount > 0 && propCount > 0) h += NODE_SEPARATOR_GAP;
15120
15375
  h += propCount * META_LINE_HEIGHT7;
15121
15376
  if (hasRoles(node)) h += ROLE_DOT_ROW;
15122
15377
  h += NODE_PAD_BOTTOM;
15123
- if (node.id.startsWith("[")) h += COLLAPSE_BAR_HEIGHT3;
15378
+ if (node.id.startsWith("[")) h += COLLAPSE_BAR_HEIGHT4;
15124
15379
  return h;
15125
15380
  }
15126
15381
  function formatRps(rps) {
@@ -15147,7 +15402,36 @@ function formatUptime(fraction) {
15147
15402
  if (pct >= 99) return `${pct.toFixed(1)}%`;
15148
15403
  return `${pct.toFixed(1)}%`;
15149
15404
  }
15150
- function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15405
+ function separateGroups(groups, nodes, isLR, maxIterations = 20) {
15406
+ for (let iter = 0; iter < maxIterations; iter++) {
15407
+ let anyOverlap = false;
15408
+ for (let i = 0; i < groups.length; i++) {
15409
+ for (let j = i + 1; j < groups.length; j++) {
15410
+ const ga = groups[i];
15411
+ const gb = groups[j];
15412
+ 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);
15413
+ if (primaryOverlap <= 0) continue;
15414
+ 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);
15415
+ if (crossOverlap <= 0) continue;
15416
+ anyOverlap = true;
15417
+ const shift = primaryOverlap + GROUP_GAP;
15418
+ const aCenter = isLR ? ga.y + ga.height / 2 : ga.x + ga.width / 2;
15419
+ const bCenter = isLR ? gb.y + gb.height / 2 : gb.x + gb.width / 2;
15420
+ const groupToShift = aCenter <= bCenter ? gb : ga;
15421
+ if (isLR) groupToShift.y += shift;
15422
+ else groupToShift.x += shift;
15423
+ for (const node of nodes) {
15424
+ if (node.groupId === groupToShift.id) {
15425
+ if (isLR) node.y += shift;
15426
+ else node.x += shift;
15427
+ }
15428
+ }
15429
+ }
15430
+ }
15431
+ if (!anyOverlap) break;
15432
+ }
15433
+ }
15434
+ function layoutInfra(computed, expandedNodeIds, collapsedNodes) {
15151
15435
  if (computed.nodes.length === 0) {
15152
15436
  return { nodes: [], edges: [], groups: [], options: {}, width: 0, height: 0 };
15153
15437
  }
@@ -15169,7 +15453,7 @@ function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15169
15453
  const heightMap = /* @__PURE__ */ new Map();
15170
15454
  for (const node of computed.nodes) {
15171
15455
  const isNodeCollapsed = collapsedNodes?.has(node.id) ?? false;
15172
- const expanded = !isNodeCollapsed && node.id === selectedNodeId;
15456
+ const expanded = !isNodeCollapsed && (expandedNodeIds?.has(node.id) ?? false);
15173
15457
  const width = computeNodeWidth2(node, expanded, computed.options);
15174
15458
  const height = isNodeCollapsed ? NODE_HEADER_HEIGHT + NODE_PAD_BOTTOM : computeNodeHeight2(node, expanded, computed.options);
15175
15459
  widthMap.set(node.id, width);
@@ -15230,6 +15514,7 @@ function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15230
15514
  queueMetrics: node.queueMetrics,
15231
15515
  properties: node.properties,
15232
15516
  tags: node.tags,
15517
+ description: node.description,
15233
15518
  lineNumber: node.lineNumber
15234
15519
  };
15235
15520
  });
@@ -15244,6 +15529,7 @@ function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15244
15529
  label: edge.label,
15245
15530
  computedRps: edge.computedRps,
15246
15531
  split: edge.split,
15532
+ fanout: edge.fanout,
15247
15533
  points: edgeData?.points ?? [],
15248
15534
  lineNumber: edge.lineNumber
15249
15535
  });
@@ -15255,6 +15541,7 @@ function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15255
15541
  label: edge.label,
15256
15542
  computedRps: edge.computedRps,
15257
15543
  split: edge.split,
15544
+ fanout: edge.fanout,
15258
15545
  points: edgeData?.points ?? [],
15259
15546
  lineNumber: edge.lineNumber
15260
15547
  });
@@ -15296,6 +15583,7 @@ function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15296
15583
  lineNumber: group.lineNumber
15297
15584
  };
15298
15585
  });
15586
+ separateGroups(layoutGroups, layoutNodes, isLR);
15299
15587
  let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
15300
15588
  for (const node of layoutNodes) {
15301
15589
  const left = node.x - node.width / 2;
@@ -15357,7 +15645,7 @@ function layoutInfra(computed, selectedNodeId, collapsedNodes) {
15357
15645
  height: totalHeight
15358
15646
  };
15359
15647
  }
15360
- var 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;
15648
+ var 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;
15361
15649
  var init_layout8 = __esm({
15362
15650
  "src/infra/layout.ts"() {
15363
15651
  "use strict";
@@ -15367,7 +15655,7 @@ var init_layout8 = __esm({
15367
15655
  NODE_SEPARATOR_GAP = 4;
15368
15656
  NODE_PAD_BOTTOM = 10;
15369
15657
  ROLE_DOT_ROW = 12;
15370
- COLLAPSE_BAR_HEIGHT3 = 6;
15658
+ COLLAPSE_BAR_HEIGHT4 = 6;
15371
15659
  CHAR_WIDTH6 = 7;
15372
15660
  META_CHAR_WIDTH3 = 6;
15373
15661
  PADDING_X3 = 24;
@@ -15395,11 +15683,11 @@ var init_layout8 = __esm({
15395
15683
  DISPLAY_NAMES = {
15396
15684
  "cache-hit": "cache hit",
15397
15685
  "firewall-block": "fw block",
15398
- "ratelimit-rps": "rate limit",
15686
+ "ratelimit-rps": "rate limit RPS",
15399
15687
  "latency-ms": "latency",
15400
15688
  "uptime": "uptime",
15401
15689
  "instances": "instances",
15402
- "max-rps": "capacity",
15690
+ "max-rps": "max RPS",
15403
15691
  "cb-error-threshold": "CB error",
15404
15692
  "cb-latency-threshold-ms": "CB latency",
15405
15693
  "concurrency": "concurrency",
@@ -15410,6 +15698,7 @@ var init_layout8 = __esm({
15410
15698
  "retention-hours": "retention",
15411
15699
  "partitions": "partitions"
15412
15700
  };
15701
+ GROUP_GAP = 24;
15413
15702
  }
15414
15703
  });
15415
15704
 
@@ -15424,6 +15713,13 @@ function inferRoles(properties) {
15424
15713
  }
15425
15714
  return roles;
15426
15715
  }
15716
+ function collectFanoutSourceIds(edges) {
15717
+ const ids = /* @__PURE__ */ new Set();
15718
+ for (const e of edges) {
15719
+ if (e.fanout != null) ids.add(e.sourceId);
15720
+ }
15721
+ return ids;
15722
+ }
15427
15723
  function collectDiagramRoles(allProperties) {
15428
15724
  const seen = /* @__PURE__ */ new Set();
15429
15725
  const roles = [];
@@ -15437,7 +15733,7 @@ function collectDiagramRoles(allProperties) {
15437
15733
  }
15438
15734
  return roles;
15439
15735
  }
15440
- var ROLE_RULES;
15736
+ var ROLE_RULES, FANOUT_ROLE;
15441
15737
  var init_roles = __esm({
15442
15738
  "src/infra/roles.ts"() {
15443
15739
  "use strict";
@@ -15450,6 +15746,7 @@ var init_roles = __esm({
15450
15746
  { keys: ["concurrency"], role: { name: "Serverless", color: "#06b6d4" } },
15451
15747
  { keys: ["buffer"], role: { name: "Queue", color: "#8b5cf6" } }
15452
15748
  ];
15749
+ FANOUT_ROLE = { name: "Fan-Out", color: "#f97316" };
15453
15750
  }
15454
15751
  });
15455
15752
 
@@ -15462,6 +15759,20 @@ __export(renderer_exports8, {
15462
15759
  });
15463
15760
  import * as d3Selection9 from "d3-selection";
15464
15761
  import * as d3Shape7 from "d3-shape";
15762
+ function resolveNodeSlo(node, diagramOptions) {
15763
+ const nodeProp = (key) => node.properties.find((p) => p.key === key);
15764
+ const availRaw = nodeProp("slo-availability")?.value ?? diagramOptions["slo-availability"];
15765
+ const latencyRaw = nodeProp("slo-p90-latency-ms")?.value ?? diagramOptions["slo-p90-latency-ms"];
15766
+ const marginRaw = nodeProp("slo-warning-margin")?.value ?? diagramOptions["slo-warning-margin"];
15767
+ const availParsed = availRaw != null ? parseFloat(String(availRaw).replace("%", "")) / 100 : NaN;
15768
+ const availThreshold = !isNaN(availParsed) ? availParsed : null;
15769
+ const latencyParsed = latencyRaw != null ? parseFloat(String(latencyRaw)) : NaN;
15770
+ const latencyP90 = !isNaN(latencyParsed) ? latencyParsed : null;
15771
+ const marginParsed = marginRaw != null ? parseFloat(String(marginRaw).replace("%", "")) / 100 : NaN;
15772
+ const warningMargin = !isNaN(marginParsed) ? marginParsed : 0.05;
15773
+ if (availThreshold == null && latencyP90 == null) return null;
15774
+ return { availThreshold, latencyP90, warningMargin };
15775
+ }
15465
15776
  function nodeBorderPoint(node, target) {
15466
15777
  const hw = node.width / 2;
15467
15778
  const hh = node.height / 2;
@@ -15498,7 +15809,17 @@ function isWarning(node) {
15498
15809
  const cap = Number(maxRps.value) * node.computedInstances;
15499
15810
  return cap > 0 && node.computedRps / cap > 0.7;
15500
15811
  }
15501
- function getComputedRows(node, expanded) {
15812
+ function truncateDesc(text) {
15813
+ if (text.length <= DESC_MAX_CHARS) return text;
15814
+ return text.slice(0, DESC_MAX_CHARS - 1) + "\u2026";
15815
+ }
15816
+ function sloLatencyColor(p90, slo) {
15817
+ const t = slo.latencyP90 ?? 0;
15818
+ if (t === 0) return COLOR_HEALTHY;
15819
+ const m = slo.warningMargin;
15820
+ return p90 > t ? COLOR_OVERLOADED : p90 > t * (1 - m) ? COLOR_WARNING : COLOR_HEALTHY;
15821
+ }
15822
+ function getComputedRows(node, expanded, slo) {
15502
15823
  const rows = [];
15503
15824
  if (node.computedConcurrentInvocations > 0) {
15504
15825
  const concurrency = getNodeNumProp(node, "concurrency", 0);
@@ -15512,9 +15833,23 @@ function getComputedRows(node, expanded) {
15512
15833
  if (p.p50 > 0 || p.p90 > 0 || p.p99 > 0) {
15513
15834
  if (expanded) {
15514
15835
  rows.push({ key: "p50", value: formatMsShort(p.p50) });
15515
- rows.push({ key: "p90", value: formatMsShort(p.p90) });
15836
+ if (slo?.latencyP90 != null) {
15837
+ const color = sloLatencyColor(p.p90, slo);
15838
+ const p90Value = color !== COLOR_HEALTHY ? `${formatMsShort(p.p90)} / ${formatMsShort(slo.latencyP90)}` : formatMsShort(p.p90);
15839
+ rows.push({ key: "p90", value: p90Value, color, inverted: color !== COLOR_HEALTHY });
15840
+ } else {
15841
+ rows.push({ key: "p90", value: formatMsShort(p.p90) });
15842
+ }
15843
+ rows.push({ key: "p99", value: formatMsShort(p.p99) });
15844
+ } else if (p.p90 > 0) {
15845
+ if (slo?.latencyP90 != null) {
15846
+ const color = sloLatencyColor(p.p90, slo);
15847
+ const p90Value = color !== COLOR_HEALTHY ? `${formatMsShort(p.p90)} / ${formatMsShort(slo.latencyP90)}` : formatMsShort(p.p90);
15848
+ rows.push({ key: "p90", value: p90Value, color, inverted: color !== COLOR_HEALTHY });
15849
+ } else {
15850
+ rows.push({ key: "p90", value: formatMsShort(p.p90) });
15851
+ }
15516
15852
  }
15517
- rows.push({ key: "p99", value: formatMsShort(p.p99) });
15518
15853
  }
15519
15854
  if (node.computedUptime < 1) {
15520
15855
  const declaredUptime = node.properties.find((p2) => p2.key === "uptime");
@@ -15525,8 +15860,17 @@ function getComputedRows(node, expanded) {
15525
15860
  }
15526
15861
  }
15527
15862
  if (node.computedAvailability < 1) {
15528
- const color = node.computedAvailability < 0.95 ? COLOR_OVERLOADED : node.computedAvailability < 0.99 ? COLOR_WARNING : void 0;
15529
- rows.push({ key: "availability", value: formatUptimeShort(node.computedAvailability), color, inverted: color != null });
15863
+ let color;
15864
+ if (slo?.availThreshold != null) {
15865
+ const t = slo.availThreshold;
15866
+ const m = slo.warningMargin;
15867
+ if (node.computedAvailability < t) color = COLOR_OVERLOADED;
15868
+ else if (node.computedAvailability < Math.min(1, t + m)) color = COLOR_WARNING;
15869
+ else color = COLOR_HEALTHY;
15870
+ } else {
15871
+ color = node.computedAvailability < 0.95 ? COLOR_OVERLOADED : node.computedAvailability < 0.99 ? COLOR_WARNING : void 0;
15872
+ }
15873
+ rows.push({ key: "availability", value: formatUptimeShort(node.computedAvailability), color, inverted: color != null && color !== COLOR_HEALTHY });
15530
15874
  }
15531
15875
  if (node.computedCbState === "open") {
15532
15876
  rows.push({ key: "CB", value: "OPEN", color: COLOR_OVERLOADED, inverted: true });
@@ -15614,7 +15958,7 @@ function formatRpsShort2(rps) {
15614
15958
  if (rps >= 1e3) return `${(rps / 1e3).toFixed(1)}k`;
15615
15959
  return `${Math.round(rps)}`;
15616
15960
  }
15617
- function worstNodeSeverity(node) {
15961
+ function worstNodeSeverity(node, slo) {
15618
15962
  let worst = "normal";
15619
15963
  const upgrade = (s) => {
15620
15964
  if (s === "overloaded") worst = "overloaded";
@@ -15625,8 +15969,22 @@ function worstNodeSeverity(node) {
15625
15969
  if (node.overloaded) upgrade("overloaded");
15626
15970
  if (node.rateLimited) upgrade("warning");
15627
15971
  if (isWarning(node)) upgrade("warning");
15628
- if (node.computedAvailability < 0.95) upgrade("overloaded");
15629
- else if (node.computedAvailability < 0.99) upgrade("warning");
15972
+ if (slo?.availThreshold != null) {
15973
+ const t = slo.availThreshold;
15974
+ const m = slo.warningMargin;
15975
+ if (node.computedAvailability < t) upgrade("overloaded");
15976
+ else if (node.computedAvailability < Math.min(1, t + m)) upgrade("warning");
15977
+ } else {
15978
+ if (node.computedAvailability < 0.95) upgrade("overloaded");
15979
+ else if (node.computedAvailability < 0.99) upgrade("warning");
15980
+ }
15981
+ if (slo?.latencyP90 != null) {
15982
+ const t = slo.latencyP90;
15983
+ const m = slo.warningMargin;
15984
+ const p90 = node.computedLatencyPercentiles.p90;
15985
+ if (p90 > t) upgrade("overloaded");
15986
+ else if (p90 > t * (1 - m)) upgrade("warning");
15987
+ }
15630
15988
  if (node.computedCbState === "open") upgrade("overloaded");
15631
15989
  if (node.queueMetrics && node.queueMetrics.fillRate > 0) {
15632
15990
  if (node.queueMetrics.timeToOverflow < 60) upgrade("overloaded");
@@ -15644,10 +16002,14 @@ function worstNodeSeverity(node) {
15644
16002
  else if (preRl > nodeRateLimit * 0.8) upgrade("warning");
15645
16003
  }
15646
16004
  }
16005
+ if (worst === "normal" && slo != null) {
16006
+ const availGreen = slo.availThreshold == null || node.computedAvailability >= Math.min(1, slo.availThreshold + slo.warningMargin);
16007
+ const latencyGreen = slo.latencyP90 == null || node.computedLatencyPercentiles.p90 <= slo.latencyP90 * (1 - slo.warningMargin);
16008
+ if (availGreen && latencyGreen) return "healthy";
16009
+ }
15647
16010
  return worst;
15648
16011
  }
15649
- function nodeColor(node, palette, isDark) {
15650
- const severity = worstNodeSeverity(node);
16012
+ function nodeColor(_node, palette, isDark, severity) {
15651
16013
  if (severity === "overloaded") {
15652
16014
  return {
15653
16015
  fill: mix(palette.bg, COLOR_OVERLOADED, isDark ? 80 : 92),
@@ -15662,6 +16024,13 @@ function nodeColor(node, palette, isDark) {
15662
16024
  textFill: palette.text
15663
16025
  };
15664
16026
  }
16027
+ if (severity === "healthy") {
16028
+ return {
16029
+ fill: mix(palette.bg, COLOR_HEALTHY, isDark ? 85 : 93),
16030
+ stroke: COLOR_HEALTHY,
16031
+ textFill: palette.text
16032
+ };
16033
+ }
15665
16034
  return {
15666
16035
  fill: isDark ? mix(palette.bg, palette.text, 90) : mix(palette.bg, palette.text, 95),
15667
16036
  stroke: isDark ? mix(palette.text, palette.bg, 60) : mix(palette.text, palette.bg, 40),
@@ -15755,10 +16124,12 @@ function resolveActiveTagStroke(node, activeGroup, tagGroups, palette) {
15755
16124
  if (!tv?.color) return null;
15756
16125
  return resolveColor(tv.color, palette);
15757
16126
  }
15758
- function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activeGroup, diagramOptions, collapsedNodes, tagGroups) {
16127
+ function renderNodes(svg, nodes, palette, isDark, animate, expandedNodeIds, activeGroup, diagramOptions, collapsedNodes, tagGroups, fanoutSourceIds, scaledGroupIds) {
15759
16128
  const mutedColor = palette.textMuted;
15760
16129
  for (const node of nodes) {
15761
- let { fill: fill2, stroke: stroke2, textFill } = nodeColor(node, palette, isDark);
16130
+ const slo = !node.isEdge && diagramOptions ? resolveNodeSlo(node, diagramOptions) : null;
16131
+ const severity = worstNodeSeverity(node, slo);
16132
+ let { fill: fill2, stroke: stroke2, textFill } = nodeColor(node, palette, isDark, severity);
15762
16133
  if (activeGroup && tagGroups && !node.isEdge) {
15763
16134
  const tagStroke = resolveActiveTagStroke(node, activeGroup, tagGroups, palette);
15764
16135
  if (tagStroke) {
@@ -15770,7 +16141,6 @@ function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activ
15770
16141
  if (animate && node.isEdge) {
15771
16142
  cls += " infra-node-edge-throb";
15772
16143
  } else if (animate && !node.isEdge) {
15773
- const severity = worstNodeSeverity(node);
15774
16144
  if (node.computedCbState === "open") cls += " infra-node-cb-open";
15775
16145
  else if (severity === "overloaded") cls += " infra-node-overload";
15776
16146
  else if (severity === "warning") cls += " infra-node-warning";
@@ -15787,26 +16157,36 @@ function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activ
15787
16157
  for (const role of roles) {
15788
16158
  g.attr(`data-role-${role.name.toLowerCase().replace(/\s+/g, "-")}`, "true");
15789
16159
  }
16160
+ if (fanoutSourceIds?.has(node.id)) {
16161
+ g.attr("data-role-fan-out", "true");
16162
+ }
15790
16163
  }
15791
16164
  const x = node.x - node.width / 2;
15792
16165
  const y = node.y - node.height / 2;
15793
16166
  const isCollapsedGroup = node.id.startsWith("[");
15794
- const strokeWidth = worstNodeSeverity(node) !== "normal" ? OVERLOAD_STROKE_WIDTH : NODE_STROKE_WIDTH8;
16167
+ const strokeWidth = severity !== "normal" ? OVERLOAD_STROKE_WIDTH : NODE_STROKE_WIDTH8;
15795
16168
  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);
15796
- const headerCenterY = y + NODE_HEADER_HEIGHT2 / 2 + NODE_FONT_SIZE3 * 0.35;
15797
- 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);
16169
+ const headerCenterY = y + NODE_HEADER_HEIGHT2 / 2 + NODE_FONT_SIZE4 * 0.35;
16170
+ 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);
15798
16171
  const isNodeCollapsed = collapsedNodes?.has(node.id) ?? false;
15799
16172
  if (isNodeCollapsed) {
15800
16173
  const chevronY = y + node.height - 6;
15801
16174
  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");
15802
16175
  }
15803
16176
  if (!isNodeCollapsed) {
15804
- const expanded = node.id === selectedNodeId;
16177
+ const expanded = expandedNodeIds?.has(node.id) ?? false;
16178
+ const descH = expanded && node.description && !node.isEdge ? META_LINE_HEIGHT8 : 0;
16179
+ if (descH > 0 && node.description) {
16180
+ const descTruncated = truncateDesc(node.description);
16181
+ const isTruncated = descTruncated !== node.description;
16182
+ 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);
16183
+ if (isTruncated) textEl.append("title").text(node.description);
16184
+ }
15805
16185
  const displayProps = !node.isEdge && expanded ? getDisplayProps(node, expanded, diagramOptions) : [];
15806
- const computedRows = getComputedRows(node, expanded);
16186
+ const computedRows = getComputedRows(node, expanded, slo);
15807
16187
  const hasContent = displayProps.length > 0 || computedRows.length > 0 || node.computedRps > 0;
15808
16188
  if (hasContent) {
15809
- const sepY = y + NODE_HEADER_HEIGHT2;
16189
+ const sepY = y + NODE_HEADER_HEIGHT2 + descH;
15810
16190
  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);
15811
16191
  const computedSection = [];
15812
16192
  const declaredSection = [];
@@ -15902,14 +16282,15 @@ function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activ
15902
16282
  rowIdx++;
15903
16283
  }
15904
16284
  }
15905
- if (!node.isEdge && node.computedConcurrentInvocations === 0 && node.computedInstances > 1) {
16285
+ const inScaledGroup = node.groupId != null && (scaledGroupIds?.has(node.groupId) ?? false);
16286
+ if (!node.isEdge && node.computedConcurrentInvocations === 0 && node.computedInstances > 1 && !inScaledGroup) {
15906
16287
  const badgeText = `${node.computedInstances}x`;
15907
16288
  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);
15908
16289
  }
15909
16290
  const showDots = activeGroup != null && activeGroup.toLowerCase() === "capabilities";
15910
16291
  const roles = showDots && !node.isEdge ? inferRoles(node.properties) : [];
15911
16292
  if (roles.length > 0) {
15912
- const dotY = isCollapsedGroup ? y + node.height - COLLAPSE_BAR_HEIGHT4 - NODE_PAD_BOTTOM2 / 2 : y + node.height - NODE_PAD_BOTTOM2 / 2;
16293
+ const dotY = isCollapsedGroup ? y + node.height - COLLAPSE_BAR_HEIGHT5 - NODE_PAD_BOTTOM2 / 2 : y + node.height - NODE_PAD_BOTTOM2 / 2;
15913
16294
  const totalDotsWidth = roles.length * (ROLE_DOT_RADIUS * 2 + 2) - 2;
15914
16295
  const startX = node.x - totalDotsWidth / 2 + ROLE_DOT_RADIUS;
15915
16296
  for (let i = 0; i < roles.length; i++) {
@@ -15919,7 +16300,7 @@ function renderNodes(svg, nodes, palette, isDark, animate, selectedNodeId, activ
15919
16300
  if (isCollapsedGroup) {
15920
16301
  const clipId = `clip-${node.id.replace(/[[\]\s]/g, "")}`;
15921
16302
  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);
15922
- 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");
16303
+ 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");
15923
16304
  }
15924
16305
  }
15925
16306
  }
@@ -15988,9 +16369,12 @@ function renderRejectParticles(svg, nodes) {
15988
16369
  }
15989
16370
  }
15990
16371
  }
15991
- function computeInfraLegendGroups(nodes, tagGroups, palette) {
16372
+ function computeInfraLegendGroups(nodes, tagGroups, palette, edges) {
15992
16373
  const groups = [];
15993
16374
  const roles = collectDiagramRoles(nodes.filter((n) => !n.isEdge).map((n) => n.properties));
16375
+ if (edges && collectFanoutSourceIds(edges).size > 0) {
16376
+ roles.push(FANOUT_ROLE);
16377
+ }
15994
16378
  if (roles.length > 0) {
15995
16379
  const entries = roles.map((r) => ({
15996
16380
  value: r.name,
@@ -16045,7 +16429,7 @@ function computePlaybackWidth(playback) {
16045
16429
  let entriesW = 8;
16046
16430
  entriesW += LEGEND_PILL_FONT_SIZE5 * 0.8 + 6;
16047
16431
  for (const s of playback.speedOptions) {
16048
- entriesW += `${s}x`.length * LEGEND_ENTRY_FONT_W7 + 6;
16432
+ entriesW += `${s}x`.length * LEGEND_ENTRY_FONT_W7 + SPEED_BADGE_H_PAD * 2 + SPEED_BADGE_GAP;
16049
16433
  }
16050
16434
  return LEGEND_CAPSULE_PAD7 * 2 + pillWidth + entriesW;
16051
16435
  }
@@ -16116,16 +16500,21 @@ function renderLegend3(rootSvg, legendGroups, totalWidth, legendY, palette, isDa
16116
16500
  for (const s of playback.speedOptions) {
16117
16501
  const label = `${s}x`;
16118
16502
  const isActive = playback.speed === s;
16119
- 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);
16120
- entryX += label.length * LEGEND_ENTRY_FONT_W7 + 6;
16503
+ const slotW = label.length * LEGEND_ENTRY_FONT_W7 + SPEED_BADGE_H_PAD * 2;
16504
+ const badgeH = LEGEND_ENTRY_FONT_SIZE6 + SPEED_BADGE_V_PAD * 2;
16505
+ const badgeY = (LEGEND_HEIGHT8 - badgeH) / 2;
16506
+ const speedG = pbG.append("g").attr("data-playback-action", "set-speed").attr("data-playback-value", String(s)).style("cursor", "pointer");
16507
+ 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");
16508
+ 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);
16509
+ entryX += slotW + SPEED_BADGE_GAP;
16121
16510
  }
16122
16511
  }
16123
16512
  cursorX += fullW + LEGEND_GROUP_GAP5;
16124
16513
  }
16125
16514
  }
16126
- function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, selectedNodeId, exportMode, collapsedNodes) {
16515
+ function renderInfra(container, layout, palette, isDark, title, titleLineNumber, tagGroups, activeGroup, animate, playback, expandedNodeIds, exportMode, collapsedNodes) {
16127
16516
  d3Selection9.select(container).selectAll(":not([data-d3-tooltip])").remove();
16128
- const legendGroups = computeInfraLegendGroups(layout.nodes, tagGroups ?? [], palette);
16517
+ const legendGroups = computeInfraLegendGroups(layout.nodes, tagGroups ?? [], palette, layout.edges);
16129
16518
  const hasLegend = legendGroups.length > 0 || !!playback;
16130
16519
  const fixedLegend = !exportMode && hasLegend;
16131
16520
  const legendOffset = hasLegend && !fixedLegend ? LEGEND_HEIGHT8 : 0;
@@ -16182,7 +16571,14 @@ function renderInfra(container, layout, palette, isDark, title, titleLineNumber,
16182
16571
  }
16183
16572
  renderGroups(svg, layout.groups, palette, isDark);
16184
16573
  renderEdgePaths(svg, layout.edges, layout.nodes, palette, isDark, shouldAnimate);
16185
- renderNodes(svg, layout.nodes, palette, isDark, shouldAnimate, selectedNodeId, activeGroup, layout.options, collapsedNodes, tagGroups ?? []);
16574
+ const fanoutSourceIds = collectFanoutSourceIds(layout.edges);
16575
+ const scaledGroupIds = new Set(
16576
+ layout.groups.filter((g) => {
16577
+ const gi = typeof g.instances === "number" ? g.instances : typeof g.instances === "string" ? parseInt(String(g.instances), 10) || 0 : 0;
16578
+ return gi > 1;
16579
+ }).map((g) => g.id)
16580
+ );
16581
+ renderNodes(svg, layout.nodes, palette, isDark, shouldAnimate, expandedNodeIds, activeGroup, layout.options, collapsedNodes, tagGroups ?? [], fanoutSourceIds, scaledGroupIds);
16186
16582
  if (shouldAnimate) {
16187
16583
  renderRejectParticles(svg, layout.nodes);
16188
16584
  }
@@ -16204,7 +16600,7 @@ function parseAndLayoutInfra(content) {
16204
16600
  const layout = layoutInfra(computed);
16205
16601
  return { parsed, computed, layout };
16206
16602
  }
16207
- var 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;
16603
+ var 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;
16208
16604
  var init_renderer8 = __esm({
16209
16605
  "src/infra/renderer.ts"() {
16210
16606
  "use strict";
@@ -16215,7 +16611,7 @@ var init_renderer8 = __esm({
16215
16611
  init_parser9();
16216
16612
  init_compute();
16217
16613
  init_layout8();
16218
- NODE_FONT_SIZE3 = 13;
16614
+ NODE_FONT_SIZE4 = 13;
16219
16615
  META_FONT_SIZE4 = 10;
16220
16616
  META_LINE_HEIGHT8 = 14;
16221
16617
  EDGE_LABEL_FONT_SIZE7 = 11;
@@ -16228,7 +16624,7 @@ var init_renderer8 = __esm({
16228
16624
  NODE_HEADER_HEIGHT2 = 28;
16229
16625
  NODE_SEPARATOR_GAP2 = 4;
16230
16626
  NODE_PAD_BOTTOM2 = 10;
16231
- COLLAPSE_BAR_HEIGHT4 = 6;
16627
+ COLLAPSE_BAR_HEIGHT5 = 6;
16232
16628
  COLLAPSE_BAR_INSET2 = 0;
16233
16629
  LEGEND_HEIGHT8 = 28;
16234
16630
  LEGEND_PILL_PAD7 = 16;
@@ -16242,6 +16638,10 @@ var init_renderer8 = __esm({
16242
16638
  LEGEND_ENTRY_TRAIL7 = 8;
16243
16639
  LEGEND_GROUP_GAP5 = 12;
16244
16640
  LEGEND_FIXED_GAP3 = 16;
16641
+ SPEED_BADGE_H_PAD = 5;
16642
+ SPEED_BADGE_V_PAD = 3;
16643
+ SPEED_BADGE_GAP = 6;
16644
+ COLOR_HEALTHY = "#22c55e";
16245
16645
  COLOR_WARNING = "#eab308";
16246
16646
  COLOR_OVERLOADED = "#ef4444";
16247
16647
  FLOW_SPEED_MIN = 2.5;
@@ -16260,22 +16660,23 @@ var init_renderer8 = __esm({
16260
16660
  lineGenerator7 = d3Shape7.line().x((d) => d.x).y((d) => d.y).curve(d3Shape7.curveBasis);
16261
16661
  PROP_DISPLAY = {
16262
16662
  "cache-hit": "cache hit",
16263
- "firewall-block": "fw block",
16264
- "ratelimit-rps": "rate limit",
16663
+ "firewall-block": "firewall block",
16664
+ "ratelimit-rps": "rate limit RPS",
16265
16665
  "latency-ms": "latency",
16266
16666
  "uptime": "uptime",
16267
16667
  "instances": "instances",
16268
- "max-rps": "capacity",
16269
- "cb-error-threshold": "CB error",
16270
- "cb-latency-threshold-ms": "CB latency",
16668
+ "max-rps": "max RPS",
16669
+ "cb-error-threshold": "CB error threshold",
16670
+ "cb-latency-threshold-ms": "CB latency threshold",
16271
16671
  "concurrency": "concurrency",
16272
16672
  "duration-ms": "duration",
16273
16673
  "cold-start-ms": "cold start",
16274
16674
  "buffer": "buffer",
16275
- "drain-rate": "drain",
16675
+ "drain-rate": "drain rate",
16276
16676
  "retention-hours": "retention",
16277
16677
  "partitions": "partitions"
16278
16678
  };
16679
+ DESC_MAX_CHARS = 120;
16279
16680
  RPS_FORMAT_KEYS = /* @__PURE__ */ new Set(["max-rps", "ratelimit-rps"]);
16280
16681
  MS_FORMAT_KEYS = /* @__PURE__ */ new Set(["latency-ms", "cb-latency-threshold-ms", "duration-ms", "cold-start-ms"]);
16281
16682
  PCT_FORMAT_KEYS = /* @__PURE__ */ new Set(["cache-hit", "firewall-block", "uptime", "cb-error-threshold"]);
@@ -16466,7 +16867,7 @@ function renderState(container, graph, layout, palette, isDark, onClickItem, exp
16466
16867
  const w = node.width;
16467
16868
  const h = node.height;
16468
16869
  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);
16469
- 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);
16870
+ 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);
16470
16871
  }
16471
16872
  }
16472
16873
  }
@@ -16505,7 +16906,7 @@ function renderStateForExport(content, theme, palette) {
16505
16906
  document.body.removeChild(container);
16506
16907
  }
16507
16908
  }
16508
- var 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;
16909
+ var 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;
16509
16910
  var init_state_renderer = __esm({
16510
16911
  "src/graph/state-renderer.ts"() {
16511
16912
  "use strict";
@@ -16515,7 +16916,7 @@ var init_state_renderer = __esm({
16515
16916
  init_layout7();
16516
16917
  DIAGRAM_PADDING9 = 20;
16517
16918
  MAX_SCALE8 = 3;
16518
- NODE_FONT_SIZE4 = 13;
16919
+ NODE_FONT_SIZE5 = 13;
16519
16920
  EDGE_LABEL_FONT_SIZE8 = 11;
16520
16921
  GROUP_LABEL_FONT_SIZE3 = 11;
16521
16922
  EDGE_STROKE_WIDTH9 = 1.5;
@@ -18127,7 +18528,6 @@ function parseD3(content, palette) {
18127
18528
  timelineSwimlanes: false,
18128
18529
  vennSets: [],
18129
18530
  vennOverlaps: [],
18130
- vennShowValues: false,
18131
18531
  quadrantLabels: {
18132
18532
  topRight: null,
18133
18533
  topLeft: null,
@@ -18149,6 +18549,9 @@ function parseD3(content, palette) {
18149
18549
  result.error = formatDgmoError(diag);
18150
18550
  return result;
18151
18551
  };
18552
+ const warn = (line10, message) => {
18553
+ result.diagnostics.push(makeDgmoError(line10, message, "warning"));
18554
+ };
18152
18555
  if (!content || !content.trim()) {
18153
18556
  return fail(0, "Empty content");
18154
18557
  }
@@ -18344,25 +18747,38 @@ function parseD3(content, palette) {
18344
18747
  }
18345
18748
  }
18346
18749
  if (result.type === "venn") {
18347
- const overlapMatch = line10.match(
18348
- /^(.+?&.+?)\s*:\s*(\d+(?:\.\d+)?)\s*(?:"([^"]*)")?\s*$/
18349
- );
18350
- if (overlapMatch) {
18351
- const sets = overlapMatch[1].split("&").map((s) => s.trim()).filter(Boolean).sort();
18352
- const size = parseFloat(overlapMatch[2]);
18353
- const label = overlapMatch[3] ?? null;
18354
- result.vennOverlaps.push({ sets, size, label, lineNumber });
18355
- continue;
18750
+ if (/\+/.test(line10)) {
18751
+ const colonIdx = line10.indexOf(":");
18752
+ let setsPart;
18753
+ let label;
18754
+ if (colonIdx >= 0) {
18755
+ setsPart = line10.substring(0, colonIdx).trim();
18756
+ label = line10.substring(colonIdx + 1).trim() || null;
18757
+ } else {
18758
+ setsPart = line10.trim();
18759
+ label = null;
18760
+ }
18761
+ const rawSets = setsPart.split("+").map((s) => s.trim()).filter(Boolean);
18762
+ if (rawSets.length >= 2) {
18763
+ result.vennOverlaps.push({ sets: rawSets, label, lineNumber });
18764
+ continue;
18765
+ }
18356
18766
  }
18357
- const setMatch = line10.match(
18358
- /^(.+?)(?:\(([^)]+)\))?\s*:\s*(\d+(?:\.\d+)?)\s*(?:"([^"]*)")?\s*$/
18359
- );
18360
- if (setMatch) {
18361
- const name = setMatch[1].trim();
18362
- const color = setMatch[2] ? resolveColor(setMatch[2].trim(), palette) : null;
18363
- const size = parseFloat(setMatch[3]);
18364
- const label = setMatch[4] ?? null;
18365
- result.vennSets.push({ name, size, color, label, lineNumber });
18767
+ const setDeclMatch = line10.match(/^([^(:]+?)(?:\(([^)]+)\))?(?:\s+alias\s+(\S+))?\s*$/i);
18768
+ if (setDeclMatch) {
18769
+ const name = setDeclMatch[1].trim();
18770
+ const colorName = setDeclMatch[2]?.trim() ?? null;
18771
+ let color = null;
18772
+ if (colorName) {
18773
+ const resolved = resolveColor(colorName, palette);
18774
+ if (resolved.startsWith("#")) {
18775
+ color = resolved;
18776
+ } else {
18777
+ warn(lineNumber, `Unknown color "${colorName}" on set "${name}". Using auto-assigned color.`);
18778
+ }
18779
+ }
18780
+ const alias = setDeclMatch[3]?.trim() ?? null;
18781
+ result.vennSets.push({ name, alias, color, lineNumber });
18366
18782
  continue;
18367
18783
  }
18368
18784
  }
@@ -18502,15 +18918,6 @@ function parseD3(content, palette) {
18502
18918
  }
18503
18919
  continue;
18504
18920
  }
18505
- if (key === "values") {
18506
- const v = line10.substring(colonIndex + 1).trim().toLowerCase();
18507
- if (v === "off") {
18508
- result.vennShowValues = false;
18509
- } else if (v === "on") {
18510
- result.vennShowValues = true;
18511
- }
18512
- continue;
18513
- }
18514
18921
  if (key === "rotate") {
18515
18922
  const v = line10.substring(colonIndex + 1).trim().toLowerCase();
18516
18923
  if (v === "none" || v === "mixed" || v === "angled") {
@@ -18588,9 +18995,6 @@ function parseD3(content, palette) {
18588
18995
  if (result.type === "sequence") {
18589
18996
  return result;
18590
18997
  }
18591
- const warn = (line10, message) => {
18592
- result.diagnostics.push(makeDgmoError(line10, message, "warning"));
18593
- };
18594
18998
  if (result.type === "wordcloud") {
18595
18999
  if (result.words.length === 0 && freeformLines.length > 0) {
18596
19000
  result.words = tokenizeFreeformText(freeformLines.join(" "));
@@ -18662,29 +19066,34 @@ function parseD3(content, palette) {
18662
19066
  }
18663
19067
  if (result.type === "venn") {
18664
19068
  if (result.vennSets.length < 2) {
18665
- return fail(1, 'At least 2 sets are required. Add sets as "Name: size" (e.g., "Math: 100")');
19069
+ return fail(1, 'At least 2 sets are required. Add set names (e.g., "Apples", "Oranges")');
18666
19070
  }
18667
19071
  if (result.vennSets.length > 3) {
18668
- return fail(1, "At most 3 sets are supported. Remove extra sets.");
19072
+ return fail(1, "Venn diagrams support 2\u20133 sets");
18669
19073
  }
18670
- const setMap = new Map(result.vennSets.map((s) => [s.name, s.size]));
19074
+ const setNameLower = new Map(
19075
+ result.vennSets.map((s) => [s.name.toLowerCase(), s.name])
19076
+ );
19077
+ const aliasLower = /* @__PURE__ */ new Map();
19078
+ for (const s of result.vennSets) {
19079
+ if (s.alias) aliasLower.set(s.alias.toLowerCase(), s.name);
19080
+ }
19081
+ const resolveSetRef = (ref) => setNameLower.get(ref.toLowerCase()) ?? aliasLower.get(ref.toLowerCase()) ?? null;
18671
19082
  const validOverlaps = [];
18672
19083
  for (const ov of result.vennOverlaps) {
19084
+ const resolvedSets = [];
18673
19085
  let valid = true;
18674
- for (const setName of ov.sets) {
18675
- if (!setMap.has(setName)) {
18676
- result.diagnostics.push(makeDgmoError(ov.lineNumber, `Overlap references unknown set "${setName}". Define it first as "${setName}: <size>"`));
19086
+ for (const ref of ov.sets) {
19087
+ const resolved = resolveSetRef(ref);
19088
+ if (!resolved) {
19089
+ result.diagnostics.push(makeDgmoError(ov.lineNumber, `Intersection references unknown set or alias "${ref}"`));
18677
19090
  if (!result.error) result.error = formatDgmoError(result.diagnostics[result.diagnostics.length - 1]);
18678
19091
  valid = false;
18679
19092
  break;
18680
19093
  }
19094
+ resolvedSets.push(resolved);
18681
19095
  }
18682
- if (!valid) continue;
18683
- const minSetSize = Math.min(...ov.sets.map((s) => setMap.get(s)));
18684
- if (ov.size > minSetSize) {
18685
- warn(ov.lineNumber, `Overlap size ${ov.size} exceeds smallest constituent set size ${minSetSize}`);
18686
- }
18687
- validOverlaps.push(ov);
19096
+ if (valid) validOverlaps.push({ ...ov, sets: resolvedSets.sort() });
18688
19097
  }
18689
19098
  result.vennOverlaps = validOverlaps;
18690
19099
  return result;
@@ -19838,7 +20247,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19838
20247
  return;
19839
20248
  }
19840
20249
  const BAR_H = 22;
19841
- const GROUP_GAP = 12;
20250
+ const GROUP_GAP2 = 12;
19842
20251
  const useGroupedHorizontal = tagLanes != null || timelineSort === "group" && timelineGroups.length > 0;
19843
20252
  if (useGroupedHorizontal) {
19844
20253
  let lanes;
@@ -19871,7 +20280,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19871
20280
  };
19872
20281
  const innerWidth = width - margin.left - margin.right;
19873
20282
  const innerHeight = height - margin.top - margin.bottom;
19874
- const totalGaps = (lanes.length - 1) * GROUP_GAP;
20283
+ const totalGaps = (lanes.length - 1) * GROUP_GAP2;
19875
20284
  const rowH = Math.min(28, (innerHeight - totalGaps) / totalEventRows);
19876
20285
  const xScale = d3Scale.scaleLinear().domain([minDate - datePadding, maxDate + datePadding]).range([0, innerWidth]);
19877
20286
  const svg = d3Selection12.select(container).append("svg").attr("width", width).attr("height", height).style("background", bgColor);
@@ -19921,8 +20330,8 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
19921
20330
  lanes.forEach((lane, idx) => {
19922
20331
  const laneSpan = lane.events.length * rowH;
19923
20332
  const fillColor = idx % 2 === 0 ? textColor : "transparent";
19924
- 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);
19925
- swimY += laneSpan + GROUP_GAP;
20333
+ 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);
20334
+ swimY += laneSpan + GROUP_GAP2;
19926
20335
  });
19927
20336
  }
19928
20337
  for (const lane of lanes) {
@@ -20003,7 +20412,7 @@ function renderTimeline(container, parsed, palette, isDark, onClickItem, exportD
20003
20412
  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);
20004
20413
  }
20005
20414
  });
20006
- curY += laneSpan + GROUP_GAP;
20415
+ curY += laneSpan + GROUP_GAP2;
20007
20416
  }
20008
20417
  } else {
20009
20418
  const sorted = timelineEvents.slice().sort((a, b) => parseTimelineDate(a.date) - parseTimelineDate(b.date));
@@ -20406,49 +20815,6 @@ function renderWordCloudAsync(container, parsed, palette, _isDark, exportDims) {
20406
20815
  }).start();
20407
20816
  });
20408
20817
  }
20409
- function radiusFromArea(area) {
20410
- return Math.sqrt(area / Math.PI);
20411
- }
20412
- function circleOverlapArea(r1, r2, d) {
20413
- if (d >= r1 + r2) return 0;
20414
- if (d + Math.min(r1, r2) <= Math.max(r1, r2)) {
20415
- return Math.PI * Math.min(r1, r2) ** 2;
20416
- }
20417
- const part1 = r1 * r1 * Math.acos((d * d + r1 * r1 - r2 * r2) / (2 * d * r1));
20418
- const part2 = r2 * r2 * Math.acos((d * d + r2 * r2 - r1 * r1) / (2 * d * r2));
20419
- const part3 = 0.5 * Math.sqrt((-d + r1 + r2) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2));
20420
- return part1 + part2 - part3;
20421
- }
20422
- function distanceForOverlap(r1, r2, targetArea) {
20423
- if (targetArea <= 0) return r1 + r2;
20424
- const minR = Math.min(r1, r2);
20425
- if (targetArea >= Math.PI * minR * minR) return Math.abs(r1 - r2);
20426
- let lo = Math.abs(r1 - r2);
20427
- let hi = r1 + r2;
20428
- for (let i = 0; i < 64; i++) {
20429
- const mid = (lo + hi) / 2;
20430
- if (circleOverlapArea(r1, r2, mid) > targetArea) {
20431
- lo = mid;
20432
- } else {
20433
- hi = mid;
20434
- }
20435
- }
20436
- return (lo + hi) / 2;
20437
- }
20438
- function thirdCirclePosition(ax, ay, dAC, bx, by, dBC) {
20439
- const dx = bx - ax;
20440
- const dy = by - ay;
20441
- const dAB = Math.sqrt(dx * dx + dy * dy);
20442
- if (dAB === 0) return { x: ax + dAC, y: ay };
20443
- const cosA = (dAB * dAB + dAC * dAC - dBC * dBC) / (2 * dAB * dAC);
20444
- const sinA = Math.sqrt(Math.max(0, 1 - cosA * cosA));
20445
- const ux = dx / dAB;
20446
- const uy = dy / dAB;
20447
- return {
20448
- x: ax + dAC * (cosA * ux - sinA * uy),
20449
- y: ay + dAC * (cosA * uy + sinA * ux)
20450
- };
20451
- }
20452
20818
  function fitCirclesToContainerAsymmetric(circles, w, h, mLeft, mRight, mTop, mBottom) {
20453
20819
  if (circles.length === 0) return [];
20454
20820
  let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
@@ -20523,54 +20889,28 @@ function regionCentroid(circles, inside) {
20523
20889
  return { x: sx / count, y: sy / count };
20524
20890
  }
20525
20891
  function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims) {
20526
- const { vennSets, vennOverlaps, vennShowValues, title } = parsed;
20892
+ const { vennSets, vennOverlaps, title } = parsed;
20527
20893
  if (vennSets.length < 2) return;
20528
20894
  const init2 = initD3Chart(container, palette, exportDims);
20529
20895
  if (!init2) return;
20530
20896
  const { svg, width, height, textColor, colors } = init2;
20531
20897
  const titleHeight = title ? 40 : 0;
20532
- const radii = vennSets.map((s) => radiusFromArea(s.size));
20533
- const overlapMap = /* @__PURE__ */ new Map();
20534
- for (const ov of vennOverlaps) {
20535
- overlapMap.set(ov.sets.join("&"), ov.size);
20536
- }
20537
- let rawCircles;
20538
20898
  const n = vennSets.length;
20899
+ const BASE_R = 100;
20900
+ const OVERLAP_DISTANCE = BASE_R * 1.4;
20901
+ let rawCircles;
20539
20902
  if (n === 2) {
20540
- const d = distanceForOverlap(
20541
- radii[0],
20542
- radii[1],
20543
- overlapMap.get([vennSets[0].name, vennSets[1].name].sort().join("&")) ?? 0
20544
- );
20545
20903
  rawCircles = [
20546
- { x: -d / 2, y: 0, r: radii[0] },
20547
- { x: d / 2, y: 0, r: radii[1] }
20904
+ { x: -OVERLAP_DISTANCE / 2, y: 0, r: BASE_R },
20905
+ { x: OVERLAP_DISTANCE / 2, y: 0, r: BASE_R }
20548
20906
  ];
20549
20907
  } else {
20550
- const names = vennSets.map((s) => s.name);
20551
- const pairKey = (i, j) => [names[i], names[j]].sort().join("&");
20552
- const dAB = distanceForOverlap(
20553
- radii[0],
20554
- radii[1],
20555
- overlapMap.get(pairKey(0, 1)) ?? 0
20556
- );
20557
- const dAC = distanceForOverlap(
20558
- radii[0],
20559
- radii[2],
20560
- overlapMap.get(pairKey(0, 2)) ?? 0
20561
- );
20562
- const dBC = distanceForOverlap(
20563
- radii[1],
20564
- radii[2],
20565
- overlapMap.get(pairKey(1, 2)) ?? 0
20566
- );
20567
- const ax = -dAB / 2;
20568
- const bx = dAB / 2;
20569
- const cPos = thirdCirclePosition(ax, 0, dAC, bx, 0, dBC);
20908
+ const s = OVERLAP_DISTANCE;
20909
+ const h = Math.sqrt(3) / 2 * s;
20570
20910
  rawCircles = [
20571
- { x: ax, y: 0, r: radii[0] },
20572
- { x: bx, y: 0, r: radii[1] },
20573
- { x: cPos.x, y: cPos.y, r: radii[2] }
20911
+ { x: -s / 2, y: h / 3, r: BASE_R },
20912
+ { x: s / 2, y: h / 3, r: BASE_R },
20913
+ { x: 0, y: -(2 * h) / 3, r: BASE_R }
20574
20914
  ];
20575
20915
  }
20576
20916
  const setColors = vennSets.map(
@@ -20583,8 +20923,7 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
20583
20923
  const edgePad = 8;
20584
20924
  const labelTextPad = 4;
20585
20925
  for (let i = 0; i < n; i++) {
20586
- const displayName = vennSets[i].label ?? vennSets[i].name;
20587
- const estimatedWidth = displayName.length * 8.5 + stubLen + edgePad + labelTextPad;
20926
+ const estimatedWidth = vennSets[i].name.length * 8.5 + stubLen + edgePad + labelTextPad;
20588
20927
  const dx = rawCircles[i].x - clusterCx;
20589
20928
  const dy = rawCircles[i].y - clusterCy;
20590
20929
  if (Math.abs(dx) >= Math.abs(dy)) {
@@ -20606,25 +20945,71 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
20606
20945
  marginTop,
20607
20946
  marginBottom
20608
20947
  ).map((c) => ({ ...c, y: c.y + titleHeight }));
20609
- const tooltip = createTooltip(container, palette, isDark);
20948
+ const scaledR = circles[0].r;
20949
+ svg.append("style").text("circle:focus, circle:focus-visible { outline: none !important; }");
20610
20950
  renderChartTitle(svg, title, parsed.titleLineNumber, width, textColor, onClickItem);
20611
20951
  const circleEls = [];
20612
20952
  const circleGroup = svg.append("g");
20613
20953
  circles.forEach((c, i) => {
20614
- 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");
20954
+ 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");
20615
20955
  circleEls.push(el);
20616
20956
  });
20957
+ const defs = svg.append("defs");
20958
+ circles.forEach((c, i) => {
20959
+ defs.append("clipPath").attr("id", `vcp-${i}`).append("circle").attr("cx", c.x).attr("cy", c.y).attr("r", c.r);
20960
+ });
20961
+ const regionIdxSets = circles.map((_, i) => [i]);
20962
+ if (n === 2) {
20963
+ regionIdxSets.push([0, 1]);
20964
+ } else {
20965
+ regionIdxSets.push([0, 1], [0, 2], [1, 2], [0, 1, 2]);
20966
+ }
20967
+ const overlayGroup = svg.append("g").style("pointer-events", "none");
20968
+ const overlayEls = /* @__PURE__ */ new Map();
20969
+ for (const idxs of regionIdxSets) {
20970
+ const key = idxs.join("-");
20971
+ const excluded = Array.from({ length: n }, (_, j) => j).filter((j) => !idxs.includes(j));
20972
+ let clipId = `vcp-${idxs[0]}`;
20973
+ for (let k = 1; k < idxs.length; k++) {
20974
+ const nestedId = `vcp-n-${idxs.slice(0, k + 1).join("-")}`;
20975
+ const ci = idxs[k];
20976
+ 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})`);
20977
+ clipId = nestedId;
20978
+ }
20979
+ let regionLineNumber = null;
20980
+ if (idxs.length === 1) {
20981
+ regionLineNumber = vennSets[idxs[0]].lineNumber;
20982
+ } else {
20983
+ const sortedNames = idxs.map((i) => vennSets[i].name).sort();
20984
+ const ov = vennOverlaps.find(
20985
+ (o) => o.sets.length === sortedNames.length && o.sets.every((s, k) => s === sortedNames[k])
20986
+ );
20987
+ regionLineNumber = ov?.lineNumber ?? null;
20988
+ }
20989
+ 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})`);
20990
+ if (regionLineNumber != null) {
20991
+ el.attr("data-line-number", String(regionLineNumber));
20992
+ }
20993
+ if (excluded.length > 0) {
20994
+ const maskId = `vvm-${key}`;
20995
+ const mask = defs.append("mask").attr("id", maskId);
20996
+ mask.append("rect").attr("x", 0).attr("y", 0).attr("width", width).attr("height", height).attr("fill", "white");
20997
+ for (const j of excluded) {
20998
+ mask.append("circle").attr("cx", circles[j].x).attr("cy", circles[j].y).attr("r", circles[j].r).attr("fill", "black");
20999
+ }
21000
+ el.attr("mask", `url(#${maskId})`);
21001
+ }
21002
+ overlayEls.set(key, el);
21003
+ }
21004
+ const showRegionOverlay = (idxs) => {
21005
+ const key = [...idxs].sort((a, b) => a - b).join("-");
21006
+ overlayEls.forEach((el, k) => el.attr("fill-opacity", k === key ? 0.3 : 0));
21007
+ };
21008
+ const hideAllOverlays = () => {
21009
+ overlayEls.forEach((el) => el.attr("fill-opacity", 0));
21010
+ };
20617
21011
  const gcx = circles.reduce((s, c) => s + c.x, 0) / n;
20618
21012
  const gcy = circles.reduce((s, c) => s + c.y, 0) / n;
20619
- function rayCircleExit(ox, oy, dx, dy, c) {
20620
- const lx = ox - c.x;
20621
- const ly = oy - c.y;
20622
- const b = lx * dx + ly * dy;
20623
- const det = b * b - (lx * lx + ly * ly - c.r * c.r);
20624
- if (det < 0) return 0;
20625
- return -b + Math.sqrt(det);
20626
- }
20627
- const labelGroup = svg.append("g").style("pointer-events", "none");
20628
21013
  function exclusiveHSpan(px, py, ci) {
20629
21014
  const dy = py - circles[ci].y;
20630
21015
  const halfChord = Math.sqrt(Math.max(0, circles[ci].r * circles[ci].r - dy * dy));
@@ -20647,8 +21032,9 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
20647
21032
  const MIN_FONT = 10;
20648
21033
  const MAX_FONT = 22;
20649
21034
  const INTERNAL_PAD = 12;
21035
+ const labelGroup = svg.append("g").style("pointer-events", "none");
20650
21036
  circles.forEach((c, i) => {
20651
- const text = vennSets[i].label ?? vennSets[i].name;
21037
+ const text = vennSets[i].name;
20652
21038
  const inside = circles.map((_, j) => j === i);
20653
21039
  const centroid = regionCentroid(circles, inside);
20654
21040
  const availW = exclusiveHSpan(centroid.x, centroid.y, i);
@@ -20683,11 +21069,8 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
20683
21069
  let textX = stubEndX + (isRight ? labelTextPad : -labelTextPad);
20684
21070
  const textY = stubEndY;
20685
21071
  const estW = text.length * 8.5;
20686
- if (isRight) {
20687
- textX = Math.min(textX, width - estW - 4);
20688
- } else {
20689
- textX = Math.max(textX, estW + 4);
20690
- }
21072
+ if (isRight) textX = Math.min(textX, width - estW - 4);
21073
+ else textX = Math.max(textX, estW + 4);
20691
21074
  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);
20692
21075
  }
20693
21076
  });
@@ -20715,40 +21098,57 @@ function renderVenn(container, parsed, palette, isDark, onClickItem, exportDims)
20715
21098
  return Math.max(0, right - left);
20716
21099
  }
20717
21100
  for (const ov of vennOverlaps) {
21101
+ if (!ov.label) continue;
20718
21102
  const idxs = ov.sets.map((s) => vennSets.findIndex((vs) => vs.name === s));
20719
21103
  if (idxs.some((idx) => idx < 0)) continue;
20720
- if (!ov.label) continue;
20721
21104
  const inside = circles.map((_, j) => idxs.includes(j));
20722
21105
  const centroid = regionCentroid(circles, inside);
20723
- const text = ov.label;
20724
21106
  const availW = overlapHSpan(centroid.y, idxs);
20725
21107
  const fitFont = Math.min(MAX_FONT, Math.max(
20726
21108
  MIN_FONT,
20727
- (availW - INTERNAL_PAD * 2) / (text.length * CH_RATIO)
21109
+ (availW - INTERNAL_PAD * 2) / (ov.label.length * CH_RATIO)
20728
21110
  ));
20729
- 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);
21111
+ 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);
20730
21112
  }
20731
21113
  const hoverGroup = svg.append("g");
20732
21114
  circles.forEach((c, i) => {
20733
- const tipName = vennSets[i].label ? `${vennSets[i].label} (${vennSets[i].name})` : vennSets[i].name;
20734
- const tipHtml = `<strong>${tipName}</strong><br>Size: ${vennSets[i].size}`;
20735
- 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) => {
20736
- circleEls.forEach((el, ci) => {
20737
- el.attr("fill-opacity", ci === i ? 0.5 : 0.1);
20738
- });
20739
- showTooltip(tooltip, tipHtml, event);
20740
- }).on("mousemove", (event) => {
20741
- showTooltip(tooltip, tipHtml, event);
21115
+ 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", () => {
21116
+ showRegionOverlay([i]);
20742
21117
  }).on("mouseleave", () => {
20743
- circleEls.forEach((el) => {
20744
- el.attr("fill-opacity", 0.35);
20745
- });
20746
- hideTooltip(tooltip);
20747
- }).on("click", () => {
20748
- if (onClickItem && vennSets[i].lineNumber)
20749
- onClickItem(vennSets[i].lineNumber);
21118
+ hideAllOverlays();
21119
+ }).on("click", function() {
21120
+ this.blur?.();
21121
+ if (onClickItem && vennSets[i].lineNumber) onClickItem(vennSets[i].lineNumber);
20750
21122
  });
20751
21123
  });
21124
+ const overlayR = scaledR * 0.35;
21125
+ const subsets = [];
21126
+ if (n === 2) {
21127
+ subsets.push({ idxs: [0, 1], sets: [vennSets[0].name, vennSets[1].name].sort() });
21128
+ } else {
21129
+ for (let a = 0; a < n; a++) {
21130
+ for (let b = a + 1; b < n; b++) {
21131
+ subsets.push({ idxs: [a, b], sets: [vennSets[a].name, vennSets[b].name].sort() });
21132
+ }
21133
+ }
21134
+ subsets.push({ idxs: [0, 1, 2], sets: [vennSets[0].name, vennSets[1].name, vennSets[2].name].sort() });
21135
+ }
21136
+ for (const subset of subsets) {
21137
+ const { idxs, sets } = subset;
21138
+ const inside = circles.map((_, j) => idxs.includes(j));
21139
+ const centroid = regionCentroid(circles, inside);
21140
+ const declaredOv = vennOverlaps.find(
21141
+ (ov) => ov.sets.length === sets.length && ov.sets.every((s, k) => s === sets[k])
21142
+ );
21143
+ 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", () => {
21144
+ showRegionOverlay(idxs);
21145
+ }).on("mouseleave", () => {
21146
+ hideAllOverlays();
21147
+ }).on("click", function() {
21148
+ this.blur?.();
21149
+ if (onClickItem && declaredOv) onClickItem(declaredOv.lineNumber);
21150
+ });
21151
+ }
20752
21152
  }
20753
21153
  function renderQuadrant(container, parsed, palette, isDark, onClickItem, exportDims) {
20754
21154
  const {
@@ -20856,8 +21256,8 @@ function renderQuadrant(container, parsed, palette, isDark, onClickItem, exportD
20856
21256
  const LABEL_MAX_FONT = 48;
20857
21257
  const LABEL_MIN_FONT = 14;
20858
21258
  const LABEL_PAD = 40;
20859
- const CHAR_WIDTH_RATIO2 = 0.6;
20860
- const estTextWidth = (text, fontSize) => text.length * fontSize * CHAR_WIDTH_RATIO2;
21259
+ const CHAR_WIDTH_RATIO3 = 0.6;
21260
+ const estTextWidth = (text, fontSize) => text.length * fontSize * CHAR_WIDTH_RATIO3;
20861
21261
  const quadrantLabelLayout = (text, qw2, qh2) => {
20862
21262
  const availW = qw2 - LABEL_PAD;
20863
21263
  const availH = qh2 - LABEL_PAD;
@@ -21680,6 +22080,43 @@ init_renderer7();
21680
22080
  init_parser7();
21681
22081
  init_layout5();
21682
22082
  init_renderer6();
22083
+
22084
+ // src/initiative-status/collapse.ts
22085
+ init_layout5();
22086
+ function collapseInitiativeStatus(parsed, collapsedGroups) {
22087
+ const originalGroups = parsed.groups;
22088
+ if (collapsedGroups.size === 0) {
22089
+ return { parsed, collapsedGroupStatuses: /* @__PURE__ */ new Map(), originalGroups };
22090
+ }
22091
+ const nodeToGroup = /* @__PURE__ */ new Map();
22092
+ const collapsedGroupStatuses = /* @__PURE__ */ new Map();
22093
+ for (const group of parsed.groups) {
22094
+ if (!collapsedGroups.has(group.label)) continue;
22095
+ const children = group.nodeLabels.map((l) => parsed.nodes.find((n) => n.label === l)).filter((n) => n !== void 0);
22096
+ for (const node of children) nodeToGroup.set(node.label, group.label);
22097
+ collapsedGroupStatuses.set(group.label, rollUpStatus(children));
22098
+ }
22099
+ const nodes = parsed.nodes.filter((n) => !nodeToGroup.has(n.label));
22100
+ const edgeKeys = /* @__PURE__ */ new Set();
22101
+ const edges = [];
22102
+ for (const edge of parsed.edges) {
22103
+ const src = nodeToGroup.get(edge.source) ?? edge.source;
22104
+ const tgt = nodeToGroup.get(edge.target) ?? edge.target;
22105
+ if (src === tgt) continue;
22106
+ const key = `${src}|${tgt}|${edge.label ?? ""}`;
22107
+ if (edgeKeys.has(key)) continue;
22108
+ edgeKeys.add(key);
22109
+ edges.push({ ...edge, source: src, target: tgt });
22110
+ }
22111
+ const groups = parsed.groups.filter((g) => !collapsedGroups.has(g.label));
22112
+ return {
22113
+ parsed: { ...parsed, nodes, edges, groups },
22114
+ collapsedGroupStatuses,
22115
+ originalGroups
22116
+ };
22117
+ }
22118
+
22119
+ // src/index.ts
21683
22120
  init_parser8();
21684
22121
  init_layout2();
21685
22122
  init_renderer2();
@@ -21937,7 +22374,7 @@ async function resolveFile(content, filePath, readFileFn, diagnostics, ancestorC
21937
22374
  continue;
21938
22375
  }
21939
22376
  if (isTagBlockHeading(trimmed)) continue;
21940
- if (lines[i] !== trimmed) continue;
22377
+ if (/^\s/.test(lines[i])) continue;
21941
22378
  const tagsMatch = trimmed.match(TAGS_RE);
21942
22379
  if (tagsMatch) {
21943
22380
  tagsDirective = tagsMatch[1].trim();
@@ -22179,6 +22616,12 @@ function encodeDiagramUrl(dsl, options) {
22179
22616
  if (options?.viewState?.swimlaneTagGroup) {
22180
22617
  hash += `&swim=${encodeURIComponent(options.viewState.swimlaneTagGroup)}`;
22181
22618
  }
22619
+ if (options?.viewState?.palette && options.viewState.palette !== "nord") {
22620
+ hash += `&pal=${encodeURIComponent(options.viewState.palette)}`;
22621
+ }
22622
+ if (options?.viewState?.theme && options.viewState.theme !== "dark") {
22623
+ hash += `&th=${encodeURIComponent(options.viewState.theme)}`;
22624
+ }
22182
22625
  return { url: `${baseUrl}?${hash}#${hash}` };
22183
22626
  }
22184
22627
  function decodeDiagramUrl(hash) {
@@ -22205,6 +22648,8 @@ function decodeDiagramUrl(hash) {
22205
22648
  if (key === "swim" && val) {
22206
22649
  viewState.swimlaneTagGroup = val;
22207
22650
  }
22651
+ if (key === "pal" && val) viewState.palette = val;
22652
+ if (key === "th" && (val === "light" || val === "dark")) viewState.theme = val;
22208
22653
  }
22209
22654
  if (payload.startsWith("dgmo=")) {
22210
22655
  payload = payload.slice(5);
@@ -22237,6 +22682,7 @@ export {
22237
22682
  buildRenderSequence,
22238
22683
  buildThemeCSS,
22239
22684
  catppuccinPalette,
22685
+ collapseInitiativeStatus,
22240
22686
  collapseOrgTree,
22241
22687
  collapseSitemapTree,
22242
22688
  collectDiagramRoles,