diff --git a/plugins/fluencyloop/site/diagram-renderer.js b/plugins/fluencyloop/site/diagram-renderer.js
index c2db0d9..a0459cf 100644
--- a/plugins/fluencyloop/site/diagram-renderer.js
+++ b/plugins/fluencyloop/site/diagram-renderer.js
@@ -96,14 +96,19 @@ const CARD_H = 104;
// that is merely shorter looks like an unfinished white panel, even when its SVG is technically
// valid.
const HEIGHT = 528;
+// Keep a small, stable reading key inside the fixed iframe. Layouts use the remaining canvas so
+// the key never becomes an afterthought below a cropped or scrollable diagram.
+const KEY_H = 52;
+const CONTENT_BOTTOM = HEIGHT - KEY_H;
function card(node, box, shared = false) {
node.box = box;
const klass = shared ? 'card shared' : 'card';
+ const compact = box.w < 180 ? ' compact' : '';
const nameY = box.y + box.h / 2 - 8;
return `
- ${escapeHtml(node.label)}
- ${escapeHtml(node.detail)}`;
+ ${escapeHtml(node.label)}
+ ${escapeHtml(node.detail)}`;
}
function edgeLabel(edge, x, y) {
@@ -113,6 +118,17 @@ function edgeLabel(edge, x, y) {
${escapeHtml(edge.label)}`;
}
+function readingKey() {
+ const ruleY = HEIGHT - KEY_H + 6;
+ const baseline = HEIGHT - 16;
+ return `
+ READING KEY
+
+ arrow: directed relationship
+
+ accent border: focal boundary`;
+}
+
function linearLayout() {
const n = graph.nodes.length;
const width = Math.min(CARD_W, Math.floor((WIDTH - 96 - (n - 1) * 24) / n));
@@ -141,6 +157,9 @@ function mergeLayout() {
|| graph.nodes.some((node) => node !== hub && outgoing.get(node.id).length !== 1)) {
fail('merge layout needs one directed path from every participant into --hub.');
}
+ if (graph.edges.some((edge) => !edge.label)) {
+ fail('merge layout requires --edge-label after every --edge so each converging relationship is clear.');
+ }
const depth = new Map([[hub.id, 0]]);
const resolving = new Set();
const resolveDepth = (node) => {
@@ -157,6 +176,10 @@ function mergeLayout() {
graph.nodes.forEach(resolveDepth);
const maxDepth = Math.max(...depth.values());
if (maxDepth > 3) fail('merge layout supports paths up to four cards deep; use a concise overview.');
+ // Four columns still need visible lanes for arrows and labels. Compact only as much as this
+ // bounded topology needs, rather than letting full-width cards consume every inter-card gap.
+ const mergeCardW = Math.min(CARD_W, Math.floor((WIDTH - 96 - maxDepth * 64) / (maxDepth + 1)));
+ if (mergeCardW < 150) fail('merge layout cannot give this graph readable lanes; use a concise overview.');
const leaves = graph.nodes.filter((node) => !incoming.get(node.id).length);
const y = new Map();
// Leave a deliberate title band above and a quiet breathing band below the graph. The embedded
@@ -170,10 +193,10 @@ function mergeLayout() {
return value;
};
graph.nodes.forEach(resolveY);
- const colGap = (WIDTH - 96 - CARD_W) / maxDepth;
+ const colGap = (WIDTH - 96 - mergeCardW) / maxDepth;
graph.nodes.forEach((node) => {
const centerY = resolveY(node);
- node.box = { x: Math.round(48 + (maxDepth - depth.get(node.id)) * colGap), y: centerY - CARD_H / 2, w: CARD_W, h: CARD_H };
+ node.box = { x: Math.round(48 + (maxDepth - depth.get(node.id)) * colGap), y: centerY - CARD_H / 2, w: mergeCardW, h: CARD_H };
});
const inputPorts = new Map();
graph.nodes.forEach((node) => {
@@ -208,9 +231,9 @@ function hubLayout() {
// Every leaf gets its own horizontal port. No elbows means no crossing, shared attachment
// point, or marker that can render inside a non-endpoint card.
const LEAF_H = 80;
- hub.box = { x: 380, y: 96, w: CARD_W, h: 380 };
+ hub.box = { x: 380, y: 88, w: CARD_W, h: 360 };
const spaced = (items, x) => items.forEach((node, index) => {
- const center = Math.round(136 + index * (300 / Math.max(1, items.length - 1)));
+ const center = Math.round(132 + index * (268 / Math.max(1, items.length - 1)));
node.box = { x, y: center - LEAF_H / 2, w: CARD_W, h: LEAF_H };
});
spaced(left, 48); spaced(right, 712);
@@ -248,7 +271,7 @@ function layeredLayout() {
const groups = Array.from({ length: maxRank + 1 }, () => []);
graph.nodes.forEach((node) => groups[ranks.get(node.id)].push(node));
groups.forEach((group, rank) => group.forEach((node, index) => {
- const y = Math.round(126 + index * ((HEIGHT - 126 - CARD_H) / Math.max(1, group.length - 1)));
+ const y = Math.round(126 + index * ((CONTENT_BOTTOM - 126 - CARD_H) / Math.max(1, group.length - 1)));
const x = Math.round(48 + rank * ((WIDTH - 96 - CARD_W) / Math.max(1, maxRank)));
node.box = { x, y, w: CARD_W, h: CARD_H };
}));
@@ -284,8 +307,8 @@ const html = `
-