fix: remove dead dagre patch and fix explicitDir false-positive with inheritDir

Agent-Logs-Url: https://github.com/sjackson0109/mermaid/sessions/6e0b59e7-9b3a-4532-bdc3-7d7893b2c0da

Co-authored-by: sjackson0109 <38080190+sjackson0109@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-05-07 16:36:19 +00:00
committed by GitHub
parent fc6ce97dd7
commit c9ce128d39
5 changed files with 10 additions and 151 deletions
+1 -2
View File
@@ -147,8 +147,7 @@
},
"pnpm": {
"patchedDependencies": {
"roughjs": "patches/roughjs.patch",
"dagre-d3-es@7.0.14": "patches/dagre-d3-es@7.0.14.patch"
"roughjs": "patches/roughjs.patch"
},
"onlyBuiltDependencies": [
"canvas",
@@ -700,6 +700,10 @@ You have to call mermaid.initialize.`
const result = uniq(list.flat());
const nodeList = result.nodeList;
let dir = result.dir;
// Capture whether the user explicitly wrote a direction keyword BEFORE any
// TD→TB normalisation or inheritDir override, so that explicitDir is true
// only for user-authored direction statements.
const hasExplicitDir = dir !== undefined;
// Normalize TD ("top-down" alias) to the canonical TB ("top-bottom") that dagre expects,
// mirroring the same normalisation in setDirection() for the top-level graph direction.
if (dir === 'TD') {
@@ -729,6 +733,7 @@ You have to call mermaid.initialize.`
title: title.trim(),
classes: [],
dir,
hasExplicitDir,
labelType: this.sanitizeNodeLabelType(_title?.type),
};
@@ -1123,7 +1128,7 @@ You have to call mermaid.initialize.`
cssClasses: subGraph.classes.join(' '),
shape: 'rect',
dir: subGraph.dir,
explicitDir: !!subGraph.dir, // true only when the user wrote an explicit 'direction X' keyword
explicitDir: subGraph.hasExplicitDir, // true only when the user wrote an explicit 'direction X' keyword
isGroup: true,
look: config.look,
});
@@ -78,6 +78,7 @@ export interface FlowClass {
export interface FlowSubGraph {
classes: string[];
dir?: string;
hasExplicitDir: boolean;
id: string;
labelType: string;
nodes: string[];
-143
View File
@@ -1,143 +0,0 @@
diff --git a/src/dagre/layout.js b/src/dagre/layout.js
index ee10670c600a9ac6ac6ebfa575c84a3c7e3a7621..0283ef7424ece094ffd231179537b37902b1600a 100644
--- a/src/dagre/layout.js
+++ b/src/dagre/layout.js
@@ -18,6 +18,8 @@ function layout(g, opts) {
time('layout', () => {
var layoutGraph = time(' buildLayoutGraph', () => buildLayoutGraph(g));
time(' runLayout', () => runLayout(layoutGraph, time));
+ // After main layout, re-layout any clusters with their own rankdir
+ time(' applyClusterDirections', () => applyClusterDirections(layoutGraph, time));
time(' updateInputGraph', () => updateInputGraph(g, layoutGraph));
});
}
@@ -121,7 +123,12 @@ function buildLayoutGraph(inputGraph) {
_.forEach(inputGraph.nodes(), function (v) {
var node = canonicalize(inputGraph.node(v));
- g.setNode(v, _.defaults(selectNumberAttrs(node, nodeNumAttrs), nodeDefaults));
+ var nodeAttrs = _.defaults(selectNumberAttrs(node, nodeNumAttrs), nodeDefaults);
+ // Preserve per-cluster rankdir so applyClusterDirections can use it
+ if (node.rankdir) {
+ nodeAttrs.rankdir = node.rankdir;
+ }
+ g.setNode(v, nodeAttrs);
g.setParent(v, inputGraph.parent(v));
});
@@ -393,6 +400,115 @@ function positionSelfEdges(g) {
});
}
+/*
+ * Returns all descendant node IDs of a given cluster node (all depths).
+ */
+function getAllDescendants(g, v) {
+ var result = [];
+ function collect(node) {
+ g.children(node).forEach(function (child) {
+ result.push(child);
+ if (g.children(child).length) {
+ collect(child);
+ }
+ });
+ }
+ collect(v);
+ return result;
+}
+
+/*
+ * After the main layout has assigned positions to all nodes, re-layout any
+ * clusters (compound nodes) that carry a `rankdir` different from the
+ * top-level graph. Each such cluster's descendants are laid out in a fresh
+ * sub-graph with the cluster's own direction, then the resulting positions are
+ * translated so the sub-layout is centred at the cluster's main-graph position.
+ */
+function applyClusterDirections(g, time) {
+ var graphRankdir = ((g.graph().rankdir) || 'TB').toUpperCase();
+
+ g.nodes().forEach(function (v) {
+ if (!g.children(v).length) return;
+ var node = g.node(v);
+ if (!node || !node.rankdir) return;
+ var clusterRankdir = node.rankdir.toUpperCase();
+ if (clusterRankdir === graphRankdir) return;
+
+ var descendants = getAllDescendants(g, v);
+ if (!descendants.length) return;
+
+ // Build a fresh sub-graph — avoid filterNodes which mutates nesting state
+ var subGraph = new Graph({ multigraph: true, compound: true });
+ subGraph.setGraph({
+ rankdir: node.rankdir,
+ ranksep: g.graph().ranksep != null ? g.graph().ranksep : 50,
+ edgesep: g.graph().edgesep != null ? g.graph().edgesep : 20,
+ nodesep: g.graph().nodesep != null ? g.graph().nodesep : 50,
+ });
+ subGraph.setDefaultEdgeLabel(function () { return {}; });
+
+ descendants.forEach(function (u) {
+ var nl = g.node(u);
+ subGraph.setNode(u, { width: (nl && nl.width != null) ? nl.width : 50,
+ height: (nl && nl.height != null) ? nl.height : 50 });
+ var parent = g.parent(u);
+ if (parent && parent !== v && descendants.indexOf(parent) !== -1) {
+ subGraph.setParent(u, parent);
+ }
+ });
+
+ g.edges().forEach(function (e) {
+ if (descendants.indexOf(e.v) !== -1 && descendants.indexOf(e.w) !== -1) {
+ var el = g.edge(e);
+ subGraph.setEdge(e.v, e.w, {
+ weight: (el && el.weight != null) ? el.weight : 1,
+ minlen: (el && el.minlen != null) ? el.minlen : 1,
+ width: (el && el.width != null) ? el.width : 0,
+ height: (el && el.height != null) ? el.height : 0,
+ labeloffset: (el && el.labeloffset != null) ? el.labeloffset : 10,
+ labelpos: (el && el.labelpos) ? el.labelpos : 'r',
+ });
+ }
+ });
+
+ // Layout the sub-graph with the cluster's own direction
+ runLayout(subGraph, time);
+
+ // Compute the sub-graph bounding box
+ var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
+ descendants.forEach(function (u) {
+ var n = subGraph.node(u);
+ if (n && n.x != null && n.y != null) {
+ var hw = (n.width || 0) / 2;
+ var hh = (n.height || 0) / 2;
+ minX = Math.min(minX, n.x - hw);
+ minY = Math.min(minY, n.y - hh);
+ maxX = Math.max(maxX, n.x + hw);
+ maxY = Math.max(maxY, n.y + hh);
+ }
+ });
+
+ // Translate sub-layout to be centred at the cluster's main-graph position
+ var clusterNode = g.node(v);
+ if (!clusterNode) return;
+ var offsetX = (clusterNode.x || 0) - (maxX + minX) / 2;
+ var offsetY = (clusterNode.y || 0) - (maxY + minY) / 2;
+
+ descendants.forEach(function (u) {
+ var subNode = subGraph.node(u);
+ var mainNode = g.node(u);
+ if (mainNode && subNode && subNode.x != null && subNode.y != null) {
+ mainNode.x = subNode.x + offsetX;
+ mainNode.y = subNode.y + offsetY;
+ }
+ });
+
+ // Update cluster bounding box to match sub-layout
+ clusterNode.width = Math.max(clusterNode.width || 0, maxX - minX);
+ clusterNode.height = Math.max(clusterNode.height || 0, maxY - minY);
+ });
+}
+
function selectNumberAttrs(obj, attrs) {
return _.mapValues(_.pick(obj, attrs), Number);
}
+2 -5
View File
@@ -5,9 +5,6 @@ settings:
excludeLinksFromLockfile: false
patchedDependencies:
dagre-d3-es@7.0.14:
hash: 9da670a68ec3d17e16b620a8bf8f9ea88df6e8122e728e68caec8eb0cdd9b9d0
path: patches/dagre-d3-es@7.0.14.patch
roughjs:
hash: 3543d47108cb41b68ec6a671c0e1f9d0cfe2ce524fea5b0992511ae84c3c6b64
path: patches/roughjs.patch
@@ -267,7 +264,7 @@ importers:
version: 0.12.3
dagre-d3-es:
specifier: 7.0.14
version: 7.0.14(patch_hash=9da670a68ec3d17e16b620a8bf8f9ea88df6e8122e728e68caec8eb0cdd9b9d0)
version: 7.0.14
dayjs:
specifier: ^1.11.19
version: 1.11.19
@@ -15687,7 +15684,7 @@ snapshots:
d3-transition: 3.0.1(d3-selection@3.0.0)
d3-zoom: 3.0.0
dagre-d3-es@7.0.14(patch_hash=9da670a68ec3d17e16b620a8bf8f9ea88df6e8122e728e68caec8eb0cdd9b9d0):
dagre-d3-es@7.0.14:
dependencies:
d3: 7.9.0
lodash-es: 4.18.1