mermaid
Version:
Markdown-ish syntax for generating flowcharts, mindmaps, sequence diagrams, class diagrams, gantt charts, git graphs and more.
702 lines (700 loc) • 23.8 kB
JavaScript
import {
adjustClustersAndEdges,
clusterDb,
createCommonLayoutRenderer,
createLayoutElementGroups,
findNonClusterChild,
insertMeasuredNode,
sortNodesByHierarchy
} from "./chunk-DSNCTWBM.mjs";
import {
insertCluster
} from "./chunk-JQ64N6SF.mjs";
import {
insertEdge,
insertEdgeLabel,
positionEdgeLabel
} from "./chunk-R7TYR2AO.mjs";
import {
getSubGraphTitleMargins,
positionNode,
setNodeElem
} from "./chunk-OBVCFTLP.mjs";
import {
updateNodeBounds
} from "./chunk-TEH6E4GO.mjs";
import "./chunk-P2QGCYS3.mjs";
import "./chunk-GMAD6QVW.mjs";
import "./chunk-PWAF6VOD.mjs";
import "./chunk-75Z2AOVW.mjs";
import {
getConfig2 as getConfig
} from "./chunk-DU6HZSFF.mjs";
import {
log
} from "./chunk-X3CZISLH.mjs";
import {
__name
} from "./chunk-Y2CYZVJY.mjs";
// src/rendering-util/layout-algorithms/dagre/index.js
import { layout as dagreLayout } from "dagre-d3-es/src/dagre/index.js";
import * as graphlib from "dagre-d3-es/src/graphlib/index.js";
var clamp = /* @__PURE__ */ __name((value, min, max) => Math.max(min, Math.min(max, value)), "clamp");
var getDefaultSelfLoopSide = /* @__PURE__ */ __name((rankdir = "TB") => {
switch (rankdir) {
case "BT":
return "bottom";
case "LR":
return "right";
case "RL":
return "left";
case "TB":
default:
return "top";
}
}, "getDefaultSelfLoopSide");
var shouldMergeSelfLoopSegments = /* @__PURE__ */ __name((diagramType) => diagramType === "flowchart" || diagramType === "flowchart-v2" || diagramType === "stateDiagram" || diagramType === "er" || diagramType === "classDiagram", "shouldMergeSelfLoopSegments");
var DAGRE_NODE_LAYOUT_PROPERTIES = [
"x",
"y",
"width",
"height",
"labelBBox",
"intersect",
"calcIntersect",
"diff",
"clusterNode"
];
var getSelfLoopSide = /* @__PURE__ */ __name((graph, node, segments, originalNodeId, rankdir) => {
const layoutHints = [];
const dummyNodeIds = /* @__PURE__ */ new Set();
segments.forEach(({ start, end }) => {
if (start !== originalNodeId) {
dummyNodeIds.add(start);
}
if (end !== originalNodeId) {
dummyNodeIds.add(end);
}
});
dummyNodeIds.forEach((id) => {
const dummyNode = graph.node(id);
if (typeof dummyNode?.x === "number" && typeof dummyNode?.y === "number") {
layoutHints.push(dummyNode);
}
});
if (layoutHints.length === 0) {
segments.forEach(({ edge }) => {
(edge.points ?? []).forEach((point) => {
if (typeof point?.x === "number" && typeof point?.y === "number") {
layoutHints.push(point);
}
});
});
}
if (layoutHints.length === 0) {
return getDefaultSelfLoopSide(rankdir);
}
const center = layoutHints.reduce(
(acc, point) => ({
x: acc.x + point.x / layoutHints.length,
y: acc.y + point.y / layoutHints.length
}),
{ x: 0, y: 0 }
);
const dx = center.x - node.x;
const dy = center.y - node.y;
if (Math.abs(dx) > Math.abs(dy)) {
return dx > 0 ? "right" : "left";
}
if (Math.abs(dy) > 0) {
return dy > 0 ? "bottom" : "top";
}
return getDefaultSelfLoopSide(rankdir);
}, "getSelfLoopSide");
var getSelfLoopPoints = /* @__PURE__ */ __name((node, side = "top", yOffset = 0, labelWidth = 0) => {
const x = node.x;
const y = node.y - yOffset;
const halfWidth = node.width / 2;
const halfHeight = node.height / 2;
const maxSpan = Math.max(36, Math.min(100, node.width * 0.8));
const span = clamp(Math.max(labelWidth, node.width * 0.35), 36, maxSpan);
const depth = clamp(Math.min(node.width, node.height) * 0.45, 24, 48);
switch (side) {
case "bottom": {
const bottom = y + halfHeight;
return [
{ x: x - span / 2, y: bottom },
{ x: x - span / 2, y: bottom + depth },
{ x: x + span / 2, y: bottom + depth },
{ x: x + span / 2, y: bottom }
];
}
case "right": {
const right = x + halfWidth;
return [
{ x: right, y: y - span / 2 },
{ x: right + depth, y: y - span / 2 },
{ x: right + depth, y: y + span / 2 },
{ x: right, y: y + span / 2 }
];
}
case "left": {
const left = x - halfWidth;
return [
{ x: left, y: y - span / 2 },
{ x: left - depth, y: y - span / 2 },
{ x: left - depth, y: y + span / 2 },
{ x: left, y: y + span / 2 }
];
}
case "top":
default: {
const top = y - halfHeight;
return [
{ x: x - span / 2, y: top },
{ x: x - span / 2, y: top - depth },
{ x: x + span / 2, y: top - depth },
{ x: x + span / 2, y: top }
];
}
}
}, "getSelfLoopPoints");
var getSelfLoopLabelPosition = /* @__PURE__ */ __name((node, points, side = "top", yOffset = 0, label = {}) => {
const gap = 4;
const x = node.x;
const y = node.y - yOffset;
const labelWidth = label.width ?? 0;
const labelHeight = label.height ?? 0;
switch (side) {
case "bottom":
return { x, y: Math.max(...points.map((point) => point.y)) + labelHeight / 2 + gap };
case "right":
return { x: Math.max(...points.map((point) => point.x)) + labelWidth / 2 + gap, y };
case "left":
return { x: Math.min(...points.map((point) => point.x)) - labelWidth / 2 - gap, y };
case "top":
default:
return { x, y: Math.min(...points.map((point) => point.y)) - labelHeight / 2 - gap };
}
}, "getSelfLoopLabelPosition");
var getEdgesToRender = /* @__PURE__ */ __name((graph, yOffset = 0, { mergeSelfLoops = true } = {}) => {
const selfLoopEdgeGroups = /* @__PURE__ */ new Map();
const edgesToRender = [];
const rankdir = graph.graph()?.rankdir;
graph.edges().forEach((e) => {
const edge = graph.edge(e);
if (mergeSelfLoops && edge.selfLoop) {
const key = edge.selfLoop.id;
if (!selfLoopEdgeGroups.has(key)) {
selfLoopEdgeGroups.set(key, []);
}
selfLoopEdgeGroups.get(key).push({ edge, start: e.v, end: e.w });
} else {
edgesToRender.push({ edge, start: e.v, end: e.w });
}
});
selfLoopEdgeGroups.forEach((segments) => {
if (segments.length !== 3) {
segments.forEach((segment) => edgesToRender.push(segment));
return;
}
segments.sort((a, b) => a.edge.selfLoop.order - b.edge.selfLoop.order);
const [firstSegment, middleSegment, lastSegment] = segments;
const originalEdge = firstSegment.edge.originalEdge ?? middleSegment.edge.originalEdge ?? lastSegment.edge.originalEdge ?? middleSegment.edge;
const node = graph.node(originalEdge.start);
if (!node) {
segments.forEach((segment) => edgesToRender.push(segment));
return;
}
const label = {
width: middleSegment.edge.width,
height: middleSegment.edge.height
};
const side = getSelfLoopSide(graph, node, segments, originalEdge.start, rankdir);
const points = getSelfLoopPoints(node, side, yOffset, label.width ?? 0);
const labelPosition = getSelfLoopLabelPosition(node, points, side, yOffset, label);
const mergedEdge = {
...middleSegment.edge,
...originalEdge,
id: originalEdge.id,
points,
start: originalEdge.start,
end: originalEdge.end,
x: labelPosition.x,
y: labelPosition.y,
width: label.width,
height: label.height,
labelStyle: middleSegment.edge.labelStyle,
fromCluster: firstSegment.edge.fromCluster ?? middleSegment.edge.fromCluster ?? lastSegment.edge.fromCluster,
toCluster: firstSegment.edge.toCluster ?? middleSegment.edge.toCluster ?? lastSegment.edge.toCluster
};
delete mergedEdge.selfLoop;
delete mergedEdge.originalEdge;
edgesToRender.push({ edge: mergedEdge, start: mergedEdge.start, end: mergedEdge.end });
});
return edgesToRender;
}, "getEdgesToRender");
var measureDagreGraph = /* @__PURE__ */ __name(async ({
element: _elem,
graph,
diagramType,
id,
parentCluster,
siteConfig
}) => {
const dir = graph.graph().rankdir;
log.trace("Dir in recursive render - dir:", dir);
const {
clusters,
edgePaths,
edgeLabels,
nodes,
rootGroups: elem
} = createLayoutElementGroups(_elem, {
edgePathsClass: "edgePaths"
});
if (!graph.nodes()) {
log.info("No nodes found for", graph);
} else {
log.info("Recursive render XXX", graph.nodes());
}
if (graph.edges().length > 0) {
log.info("Recursive edges", graph.edge(graph.edges()[0]));
}
const mergeSelfLoops = shouldMergeSelfLoopSegments(diagramType);
await Promise.all(
graph.nodes().map(async function(v) {
const node = graph.node(v);
if (parentCluster !== void 0) {
const data = JSON.parse(JSON.stringify(parentCluster.clusterData));
log.trace(
"Setting data for parent cluster XXX\n Node.id = ",
v,
"\n data=",
data.height,
"\nParent cluster",
parentCluster.height
);
graph.setNode(parentCluster.id, data);
if (!graph.parent(v)) {
log.trace("Setting parent", v, parentCluster.id);
graph.setParent(v, parentCluster.id, data);
}
}
log.info("(Insert) Node XXX" + v + ": " + JSON.stringify(graph.node(v)));
if (node?.clusterNode) {
log.info("Cluster identified XBX", v, node.width, graph.node(v));
const { ranksep, nodesep } = graph.graph();
node.graph.setGraph({
...node.graph.graph(),
ranksep: ranksep + 25,
nodesep
});
const o = await renderDagreSubgraph({
element: nodes,
graph: node.graph,
diagramType,
id,
parentCluster: graph.node(v),
siteConfig
});
const newEl = o.elem;
updateNodeBounds(node, newEl);
node.diff = o.diff || 0;
log.info(
"New compound node after recursive render XAX",
v,
"width",
// node,
node.width,
"height",
node.height
// node.x,
// node.y
);
setNodeElem(newEl, node);
} else {
if (graph.children(v).length > 0) {
log.trace(
"Cluster - the non recursive path XBX",
v,
node.id,
node,
node.width,
"Graph:",
graph
);
log.trace(findNonClusterChild(node.id, graph));
clusterDb.set(node.id, { id: findNonClusterChild(node.id, graph), node });
} else {
log.trace("Node - the non recursive path XAX", v, nodes, graph.node(v), dir);
await insertMeasuredNode(nodes, graph.node(v), { config: siteConfig, dir });
}
}
})
);
const processEdges = /* @__PURE__ */ __name(async () => {
const edgePromises = graph.edges().map(async function(e) {
const edge = graph.edge(e.v, e.w, e.name);
log.info("Edge " + e.v + " -> " + e.w + ": " + JSON.stringify(e));
log.info("Edge " + e.v + " -> " + e.w + ": ", e, " ", JSON.stringify(graph.edge(e)));
log.info(
"Fix",
clusterDb,
"ids:",
e.v,
e.w,
"Translating: ",
clusterDb.get(e.v),
clusterDb.get(e.w)
);
if (mergeSelfLoops && edge.selfLoop) {
if (edge.selfLoop.order !== 1) {
return;
}
const labelEdge = {
...edge.originalEdge,
...edge,
id: edge.selfLoop.id,
startLabelLeft: edge.originalEdge?.startLabelLeft ?? edge.startLabelLeft,
startLabelRight: edge.originalEdge?.startLabelRight ?? edge.startLabelRight,
endLabelLeft: edge.originalEdge?.endLabelLeft ?? edge.endLabelLeft,
endLabelRight: edge.originalEdge?.endLabelRight ?? edge.endLabelRight
};
await insertEdgeLabel(edgeLabels, labelEdge);
edge.width = labelEdge.width;
edge.height = labelEdge.height;
edge.labelStyle = labelEdge.labelStyle;
return;
}
await insertEdgeLabel(edgeLabels, edge);
});
await Promise.all(edgePromises);
}, "processEdges");
await processEdges();
const { subGraphTitleTotalMargin } = getSubGraphTitleMargins(siteConfig);
return {
elem,
graph,
groups: { clusters, edgePaths, edgeLabels, nodes, rootGroups: elem },
diagramType,
id,
mergeSelfLoops,
subGraphTitleTotalMargin
};
}, "measureDagreGraph");
var runDagreGraphLayout = /* @__PURE__ */ __name((graph) => {
log.info("############################################# XXX");
log.info("### Layout ### XXX");
log.info("############################################# XXX");
if (false) {
profiler.begin("layoutCore");
}
dagreLayout(graph);
if (false) {
profiler.end();
}
}, "runDagreGraphLayout");
var normalizeDagreNode = /* @__PURE__ */ __name((graph, nodeId, subGraphTitleTotalMargin) => {
const node = graph.node(nodeId);
if (!node) {
return void 0;
}
const normalizedNode = { ...node };
if (node?.clusterNode) {
normalizedNode.y = (node.y ?? 0) + subGraphTitleTotalMargin;
} else if (graph.children(nodeId).length > 0) {
normalizedNode.height = (node.height ?? 0) + subGraphTitleTotalMargin;
} else {
normalizedNode.y = (node.y ?? 0) + subGraphTitleTotalMargin / 2;
}
return normalizedNode;
}, "normalizeDagreNode");
var applyDagreNodeLayout = /* @__PURE__ */ __name((targetNode, dagreNode) => {
DAGRE_NODE_LAYOUT_PROPERTIES.forEach((property) => {
if (dagreNode[property] !== void 0) {
targetNode[property] = dagreNode[property];
}
});
}, "applyDagreNodeLayout");
var normalizeDagreEdge = /* @__PURE__ */ __name((edge, start, end, edgeOffsetY) => ({
...edge,
start: edge.start ?? start,
end: edge.end ?? end,
points: (edge.points ?? []).map((point) => ({
...point,
y: typeof point.y === "number" ? point.y + edgeOffsetY : point.y
}))
}), "normalizeDagreEdge");
var applyDagreLayoutResult = /* @__PURE__ */ __name((data4Layout, measuredLayout) => {
const { graph, mergeSelfLoops, subGraphTitleTotalMargin = 0 } = measuredLayout;
const nodeById = new Map(data4Layout.nodes.map((node) => [node.id, node]));
sortNodesByHierarchy(graph).forEach((nodeId) => {
const dagreNode = normalizeDagreNode(graph, nodeId, subGraphTitleTotalMargin);
if (!dagreNode) {
return;
}
applyDagreNodeLayout(graph.node(nodeId), dagreNode);
const targetNode = nodeById.get(nodeId);
if (targetNode) {
applyDagreNodeLayout(targetNode, dagreNode);
}
});
const edgeOffsetY = subGraphTitleTotalMargin / 2;
data4Layout.edges = getEdgesToRender(graph, edgeOffsetY, { mergeSelfLoops }).map(
({ edge, start, end }) => normalizeDagreEdge(edge, start, end, edgeOffsetY)
);
return data4Layout;
}, "applyDagreLayoutResult");
var paintDagreLayoutCore = /* @__PURE__ */ __name(async ({
elem,
graph,
groups: { clusters, edgePaths },
diagramType,
id,
mergeSelfLoops,
subGraphTitleTotalMargin
}) => {
let diff = 0;
await Promise.all(
sortNodesByHierarchy(graph).map(async function(v) {
const node = graph.node(v);
log.info(
"Position XBX => " + v + ": (" + node.x,
"," + node.y,
") width: ",
node.width,
" height: ",
node.height
);
if (node?.clusterNode) {
node.y += subGraphTitleTotalMargin;
log.info(
"A tainted cluster node XBX1",
v,
node.id,
node.width,
node.height,
node.x,
node.y,
graph.parent(v)
);
clusterDb.get(node.id).node = node;
positionNode(node);
} else {
if (graph.children(v).length > 0) {
log.info(
"A pure cluster node XBX1",
v,
node.id,
node.x,
node.y,
node.width,
node.height,
graph.parent(v)
);
node.height += subGraphTitleTotalMargin;
graph.node(node.parentId);
const halfPadding = node?.padding / 2 || 0;
const labelHeight = node?.labelBBox?.height || 0;
const offsetY = labelHeight - halfPadding || 0;
log.debug("OffsetY", offsetY, "labelHeight", labelHeight, "halfPadding", halfPadding);
await insertCluster(clusters, node);
clusterDb.get(node.id).node = node;
} else {
const parent = graph.node(node.parentId);
node.y += subGraphTitleTotalMargin / 2;
log.info(
"A regular node XBX1 - using the padding",
node.id,
"parent",
node.parentId,
node.width,
node.height,
node.x,
node.y,
"offsetY",
node.offsetY,
"parent",
parent,
parent?.offsetY,
node
);
positionNode(node);
}
}
})
);
const edgeOffsetY = subGraphTitleTotalMargin / 2;
const edgesToRender = getEdgesToRender(graph, edgeOffsetY, { mergeSelfLoops });
edgesToRender.forEach(function({ edge, start, end }) {
log.info("Edge " + start + " -> " + end + ": " + JSON.stringify(edge), edge);
edge.points.forEach((point) => point.y += edgeOffsetY);
const startNode = graph.node(start);
const endNode = graph.node(end);
const paths = insertEdge(edgePaths, edge, clusterDb, diagramType, startNode, endNode, id);
positionEdgeLabel(edge, paths);
});
graph.nodes().forEach(function(v) {
const n = graph.node(v);
log.info(v, n.type, n.diff);
if (n.isGroup) {
diff = n.diff;
}
});
log.warn("Returning from recursive render XAX", elem, diff);
return { elem, diff };
}, "paintDagreLayoutCore");
var renderDagreSubgraph = /* @__PURE__ */ __name(async (options) => {
const measuredLayout = await measureDagreGraph(options);
runDagreGraphLayout(measuredLayout.graph);
return await paintDagreLayoutCore(measuredLayout);
}, "renderDagreSubgraph");
var prepareLayoutForDagre = /* @__PURE__ */ __name((data4Layout) => {
const graph = new graphlib.Graph({
multigraph: true,
compound: true
}).setGraph({
rankdir: data4Layout.direction,
// Precedence: an explicit top-level config override (only present when a
// user sets it - it has no schema default) > the value the diagram's own
// renderer put on data4Layout (usually derived from that diagram's config)
// > the flowchart config as a generic fallback. The flowchart keys have
// schema defaults and therefore always exist, so they must come last or
// they silently shadow every diagram's own spacing (see #7932).
nodesep: data4Layout.config?.nodeSpacing || data4Layout.nodeSpacing || data4Layout.config?.flowchart?.nodeSpacing,
ranksep: data4Layout.config?.rankSpacing || data4Layout.rankSpacing || data4Layout.config?.flowchart?.rankSpacing,
marginx: 8,
marginy: 8
}).setDefaultEdgeLabel(function() {
return {};
});
data4Layout.nodes.forEach((node) => {
graph.setNode(node.id, { ...node });
if (node.parentId) {
graph.setParent(node.id, node.parentId);
}
});
log.debug("Edges:", data4Layout.edges);
data4Layout.edges.forEach((edge) => {
if (edge.start === edge.end) {
const nodeId = edge.start;
const specialId1 = nodeId + "---" + nodeId + "---1";
const specialId2 = nodeId + "---" + nodeId + "---2";
const node = graph.node(nodeId);
graph.setNode(specialId1, {
domId: specialId1,
id: specialId1,
parentId: node.parentId,
labelStyle: "",
label: "",
padding: 0,
shape: "labelRect",
// shape: 'rect',
style: "",
width: 10,
height: 10
});
graph.setParent(specialId1, node.parentId);
graph.setNode(specialId2, {
domId: specialId2,
id: specialId2,
parentId: node.parentId,
labelStyle: "",
padding: 0,
// shape: 'rect',
shape: "labelRect",
label: "",
style: "",
width: 10,
height: 10
});
graph.setParent(specialId2, node.parentId);
const originalEdge = structuredClone(edge);
const edge1 = structuredClone(edge);
const edgeMid = structuredClone(edge);
const edge2 = structuredClone(edge);
edge1.originalEdge = originalEdge;
edge1.selfLoop = { id: originalEdge.id, order: 0 };
edgeMid.originalEdge = originalEdge;
edgeMid.selfLoop = { id: originalEdge.id, order: 1 };
edge2.originalEdge = originalEdge;
edge2.selfLoop = { id: originalEdge.id, order: 2 };
edge1.label = "";
edge1.arrowTypeEnd = "none";
edge1.endLabelLeft = "";
edge1.endLabelRight = "";
edge1.startLabelLeft = "";
edge1.id = nodeId + "-cyclic-special-1";
edgeMid.startLabelRight = "";
edgeMid.startLabelLeft = "";
edgeMid.endLabelLeft = "";
edgeMid.endLabelRight = "";
edgeMid.arrowTypeStart = "none";
edgeMid.arrowTypeEnd = "none";
edgeMid.id = nodeId + "-cyclic-special-mid";
edge2.label = "";
edge2.startLabelRight = "";
edge2.startLabelLeft = "";
edge2.arrowTypeStart = "none";
if (node.isGroup) {
edge1.fromCluster = nodeId;
edge2.toCluster = nodeId;
}
edge2.id = nodeId + "-cyclic-special-2";
edge2.arrowTypeStart = "none";
graph.setEdge(nodeId, specialId1, edge1, nodeId + "-cyclic-special-0");
graph.setEdge(specialId1, specialId2, edgeMid, nodeId + "-cyclic-special-1");
graph.setEdge(specialId2, nodeId, edge2, nodeId + "-cyclic-special-2");
} else {
graph.setEdge(edge.start, edge.end, { ...edge }, edge.id);
}
});
adjustClustersAndEdges(graph);
return { graph };
}, "prepareLayoutForDagre");
var measureDagreLayout = /* @__PURE__ */ __name(async (data4Layout, { element, preparedLayout }) => {
const prepared = preparedLayout ?? prepareLayoutForDagre(data4Layout);
const siteConfig = getConfig();
const measuredLayout = await measureDagreGraph({
element,
graph: prepared.graph,
diagramType: data4Layout.type,
id: data4Layout.diagramId,
parentCluster: void 0,
siteConfig
});
prepared.measuredLayout = measuredLayout;
return measuredLayout;
}, "measureDagreLayout");
var runDagreLayoutCore = /* @__PURE__ */ __name((data4Layout, context) => {
const measuredLayout = context.preparedLayout?.measuredLayout;
if (!measuredLayout) {
throw new Error("runDagreLayoutCore requires measureDagreLayout to run first");
}
runDagreGraphLayout(measuredLayout.graph);
applyDagreLayoutResult(data4Layout, measuredLayout);
return measuredLayout;
}, "runDagreLayoutCore");
var getDagrePaintNodes = /* @__PURE__ */ __name((_data4Layout, { measure }) => sortNodesByHierarchy(measure.graph).map((nodeId) => measure.graph.node(nodeId)).filter(Boolean), "getDagrePaintNodes");
var getDagreEdgeNode = /* @__PURE__ */ __name((nodeId, _edge, { measure }) => nodeId ? measure.graph.node(nodeId) : void 0, "getDagreEdgeNode");
var render = createCommonLayoutRenderer({
prepareLayout: prepareLayoutForDagre,
measureLayout: measureDagreLayout,
runLayoutCore: runDagreLayoutCore,
paintOptions: {
clusterDb,
getNodes: getDagrePaintNodes,
getEdgeNode: getDagreEdgeNode,
skipNode: /* @__PURE__ */ __name((node, { measure }) => !measure.graph.hasNode(node.id), "skipNode"),
isCluster: /* @__PURE__ */ __name((node, { measure }) => measure.graph.hasNode(node.id) && (measure.graph.children(node.id) ?? []).length > 0, "isCluster")
}
});
export {
applyDagreLayoutResult,
getEdgesToRender,
measureDagreLayout,
prepareLayoutForDagre,
render,
runDagreLayoutCore
};