fix(diagram): balance architecture group layouts
This commit is contained in:
1 parent
940c4d8cde
commit
43e5675d0a
2 files changed
+77
-2
No files matched your search
@@ -286,6 +286,35 @@ describe("diagram auto layout", () => {
|
||||
expect(new Set(boundaries.map((node) => node.y)).size).toBeGreaterThan(1);
|
||||
});
|
||||
|
||||
test("uses two rows of architecture groups when a single column wastes horizontal space", () => {
|
||||
const nodes = [];
|
||||
const edges = [];
|
||||
let previousId;
|
||||
for (let groupIndex = 0; groupIndex < 4; groupIndex += 1) {
|
||||
const boundaryId = `group-${groupIndex}`;
|
||||
nodes.push({ id: boundaryId, label: boundaryId, type: "boundary" });
|
||||
for (let nodeIndex = 0; nodeIndex < 2; nodeIndex += 1) {
|
||||
const id = `${boundaryId}-node-${nodeIndex}`;
|
||||
nodes.push({ id, label: id, type: "service", parentId: boundaryId });
|
||||
if (previousId) edges.push({ source: previousId, target: id });
|
||||
previousId = id;
|
||||
}
|
||||
}
|
||||
const document = compileDiagramIr({ kind: "architecture", nodes, edges });
|
||||
const boundaries = document.nodes.filter((node) => node.shape === "boundary");
|
||||
const width = Math.max(...boundaries.map((node) => node.x + node.width))
|
||||
- Math.min(...boundaries.map((node) => node.x));
|
||||
expect(width).toBeGreaterThan(900);
|
||||
expect(width).toBeLessThanOrEqual(1480);
|
||||
expect(new Set(boundaries.map((node) => node.y)).size).toBe(2);
|
||||
const relayout = computeDiagramLayoutResult(document);
|
||||
const relayoutWidth = Math.max(...boundaries.map((node) => relayout.nodes[node.id].x + relayout.nodes[node.id].width))
|
||||
- Math.min(...boundaries.map((node) => relayout.nodes[node.id].x));
|
||||
expect(relayoutWidth).toBeGreaterThan(900);
|
||||
const vertical = computeDiagramLayoutResult(document, { direction: "top-to-bottom" });
|
||||
expect(vertical.nodes["group-1"].y).toBeGreaterThan(vertical.nodes["group-0"].y);
|
||||
});
|
||||
|
||||
test("returns a complete strategy result for every diagram kind", () => {
|
||||
for (const kind of ["mind-map", "flowchart", "architecture"]) {
|
||||
const document = createDefaultDiagramDocument(kind);
|
||||
|
||||
@@ -115,6 +115,7 @@ const FLOWCHART_DETACHED_GAP = 56;
|
||||
const FLOWCHART_DETACHED_ROW_GAP = 24;
|
||||
const FLOWCHART_DETACHED_ROW_WIDTH = 960;
|
||||
const ARCHITECTURE_LAYOUT_ROW_WIDTH = 1480;
|
||||
const ARCHITECTURE_LAYOUT_TARGET_RATIO = 1.35;
|
||||
const ARCHITECTURE_GROUP_HORIZONTAL_GAP = 72;
|
||||
const ARCHITECTURE_GROUP_VERTICAL_GAP = 88;
|
||||
const ARCHITECTURE_GROUP_PAD_X = 36;
|
||||
@@ -519,6 +520,52 @@ const layoutArchitectureGraph = (
|
||||
return width(vertical) < width(positions) ? vertical : positions;
|
||||
};
|
||||
|
||||
const layoutArchitectureMetaGraph = (
|
||||
nodes: Array<{ id: string; width: number; height: number }>,
|
||||
edges: Array<{ source: string; target: string }>,
|
||||
direction: "left-to-right" | "top-to-bottom",
|
||||
margin: { x: number; y: number },
|
||||
): DiagramLayoutPositions => {
|
||||
const vertical = layoutDagreGraph(nodes, edges, "top-to-bottom", ARCHITECTURE_META_SPACING, margin);
|
||||
if (direction === "top-to-bottom" || nodes.length === 0) return vertical;
|
||||
const horizontal = layoutDagreGraph(nodes, edges, "left-to-right", ARCHITECTURE_META_SPACING, margin);
|
||||
const size = (positions: DiagramLayoutPositions) => ({
|
||||
width: Math.max(...nodes.map((node) => positions[node.id].x + node.width))
|
||||
- Math.min(...nodes.map((node) => positions[node.id].x)),
|
||||
height: Math.max(...nodes.map((node) => positions[node.id].y + node.height))
|
||||
- Math.min(...nodes.map((node) => positions[node.id].y)),
|
||||
});
|
||||
if (size(horizontal).width <= ARCHITECTURE_LAYOUT_ROW_WIDTH) return horizontal;
|
||||
if (nodes.length < 3) return vertical;
|
||||
|
||||
const wrapped: DiagramLayoutPositions = {};
|
||||
let x = margin.x;
|
||||
let y = margin.y;
|
||||
let rowHeight = 0;
|
||||
for (const node of [...nodes].sort((left, right) => (
|
||||
horizontal[left.id].x - horizontal[right.id].x
|
||||
|| horizontal[left.id].y - horizontal[right.id].y
|
||||
|| left.id.localeCompare(right.id)
|
||||
))) {
|
||||
if (x > margin.x && x + node.width > margin.x + ARCHITECTURE_LAYOUT_ROW_WIDTH) {
|
||||
x = margin.x;
|
||||
y += rowHeight + ARCHITECTURE_GROUP_VERTICAL_GAP;
|
||||
rowHeight = 0;
|
||||
}
|
||||
wrapped[node.id] = { x, y };
|
||||
x += node.width + ARCHITECTURE_GROUP_HORIZONTAL_GAP;
|
||||
rowHeight = Math.max(rowHeight, node.height);
|
||||
}
|
||||
const fitCost = (positions: DiagramLayoutPositions) => {
|
||||
const bounds = size(positions);
|
||||
const targetHeight = ARCHITECTURE_LAYOUT_ROW_WIDTH / ARCHITECTURE_LAYOUT_TARGET_RATIO;
|
||||
const overflow = Math.max(bounds.width / ARCHITECTURE_LAYOUT_ROW_WIDTH, bounds.height / targetHeight);
|
||||
const aspectError = Math.abs(Math.log((bounds.width / bounds.height) / ARCHITECTURE_LAYOUT_TARGET_RATIO));
|
||||
return overflow + aspectError * 0.2;
|
||||
};
|
||||
return fitCost(wrapped) < fitCost(vertical) ? wrapped : vertical;
|
||||
};
|
||||
|
||||
const wrapArchitectureGroups = (
|
||||
document: DiagramDocument,
|
||||
positions: DiagramLayoutPositions,
|
||||
@@ -869,11 +916,10 @@ const computeArchitectureLayout = (document: DiagramDocument, options: DiagramLa
|
||||
}
|
||||
|
||||
const metaMargin = { x: ARCHITECTURE_ORIGIN, y: ARCHITECTURE_ORIGIN };
|
||||
const metaPositions = layoutArchitectureGraph(
|
||||
const metaPositions = layoutArchitectureMetaGraph(
|
||||
metaNodes,
|
||||
metaEdges,
|
||||
direction,
|
||||
ARCHITECTURE_META_SPACING,
|
||||
metaMargin,
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user