Repository navigation
fix(layout): reserve each cluster's measured label height above its children - #7859
filipsajdak wants to merge 3 commits into
Conversation
🦋 Changeset detectedLatest commit: 64dd1ea The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for mermaid-js ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
@mermaid-js/examples
mermaid
@mermaid-js/layout-elk
@mermaid-js/layout-tidy-tree
@mermaid-js/mermaid-zenuml
@mermaid-js/parser
@mermaid-js/tiny
commit: |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #7859 +/- ##
===========================================
+ Coverage 80.73% 81.14% +0.40%
===========================================
Files 619 621 +2
Lines 84970 85858 +888
Branches 18596 16364 -2232
===========================================
+ Hits 68601 69666 +1065
+ Misses 15350 15175 -175
+ Partials 1019 1017 -2
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
1121f41 to
35679e3
Compare
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
35679e3 to
2ab2b9a
Compare
Aligns the shared layout change with mermaid-js#7859: keep only the cluster label-height reservation (the c4-beta overlap fix); revert the edge-label centering that shifted edge labels for any subGraphTitleMargin>0 diagram (incl. flowcharts). c4-beta relationship-label centering can return later scoped to the c4-beta diagram type. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2ab2b9a to
9160c2a
Compare
Aligns the shared layout change with mermaid-js#7859: keep only the cluster label-height reservation (the c4-beta overlap fix); revert the edge-label centering that shifted edge labels for any subGraphTitleMargin>0 diagram (incl. flowcharts). c4-beta relationship-label centering can return later scoped to the c4-beta diagram type. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9160c2a to
e7cd3b6
Compare
Aligns the shared layout change with mermaid-js#7859: keep only the cluster label-height reservation (the c4-beta overlap fix); revert the edge-label centering that shifted edge labels for any subGraphTitleMargin>0 diagram (incl. flowcharts). c4-beta relationship-label centering can return later scoped to the c4-beta diagram type. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR updates dagre normalization to reserve measured cluster header space, clear cumulative ancestor space, and preserve zero-margin behavior. It adds unit tests, nested flowchart snapshots, and a patch changeset. ChangesCluster Header Spacing Fix
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant applyDagreLayoutResult
participant clusterReserveHelpers
participant normalizedDagreNodes
applyDagreLayoutResult->>clusterReserveHelpers: calculate measured header and ancestor reserves
clusterReserveHelpers->>normalizedDagreNodes: provide nested reserve values
applyDagreLayoutResult->>normalizedDagreNodes: shift nodes and resize clusters
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.spec.js (1)
242-292: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSolid test, but the
clusterNode(recursively-rendered) branch is untested.This test thoroughly validates the top-level-cluster and leaf branches of
normalizeDagreNode, but doesn't cover thenode.clusterNodebranch (Line 505 in index.js), which is also touched by this PR (newancestorReserveaddition to its shift). Consider adding a case with a node carryingclusterNode: trueto lock in that behavior too.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.spec.js` around lines 242 - 292, The current test covers top-level clusters and leaves, but it does not exercise the normalizeDagreNode branch for recursively rendered cluster nodes (the node.clusterNode path). Extend the existing dagre layout spec by adding a case with a node marked clusterNode: true, and assert its y/height shift reflects the new ancestorReserve behavior inside normalizeDagreNode so that this branch is locked in too.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.js`:
- Around line 822-828: The `layoutTitleMargin` override in the Dagre layout
config is using a truthy check, which drops an explicit `subGraphTitleMargin: 0`
set in `data4Layout.config.flowchart`. Update the logic in the `dagre` layout
path so the diagram-level value wins whenever it is provided, including zero, by
checking for presence rather than truthiness before assigning into
`siteConfig.flowchart.subGraphTitleMargin`.
---
Nitpick comments:
In `@packages/mermaid/src/rendering-util/layout-algorithms/dagre/index.spec.js`:
- Around line 242-292: The current test covers top-level clusters and leaves,
but it does not exercise the normalizeDagreNode branch for recursively rendered
cluster nodes (the node.clusterNode path). Extend the existing dagre layout spec
by adding a case with a node marked clusterNode: true, and assert its y/height
shift reflects the new ancestorReserve behavior inside normalizeDagreNode so
that this branch is locked in too.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: f5e87b46-f2b4-4b2d-b420-d5c380817481
📒 Files selected for processing (3)
.changeset/dagre-cluster-label-reserve.mdpackages/mermaid/src/rendering-util/layout-algorithms/dagre/index.jspackages/mermaid/src/rendering-util/layout-algorithms/dagre/index.spec.js
|
Re the coverage nitpick (recursively-rendered |
|
Hi @knsv @sidharthv96 - friendly nudge on this one. It's fully green (including the argos visual-diff check) and cleanly mergeable, with no open threads. Whenever you have a moment, a review would be very welcome. The full staged plan for this C4 modernization effort - what's landed, what's open, and the recommended merge order - is kept up to date in #7849, so that's the best single place to see how this PR fits into the whole picture. This one's actually the top of that recommended order. Thanks for your time! |
|
Gentle follow-up on this one (no rush) - it's still fully green including |
|
@aloisklink @sidharthv96 - a gentle nudge on this one, as it has been quiet since 13 July. It is currently the only one of my open PRs that is fully green, including Argos, with no open review threads and no conflicts. Three commits, one diagram-agnostic layout fix:
Flowchart output is unchanged - the edge-label centering that would have altered it was deliberately dropped so existing snapshots stay byte-identical. Not urgent in itself, but the C4 layout migration depends on it, so it is worth landing before that slice arrives. Happy to split or rescope if that makes it easier to review. |
948e559 to
36c4b63
Compare
|
Rebased onto current The two The tree is byte-identical to the previous head - the series was rebuilt from that exact tree rather than replayed, and Still 3 files, +125/-4, and still the same behaviour: flowchart output is unchanged, since the edge-label centering that would have moved existing diagrams was deliberately left out. |
|
Still the cheapest merge in the C4 stack: fully green including Worth restating that the fix itself is diagram-agnostic despite arriving from the C4 work: dagre reserves height for a cluster label, and edge labels are centred. The C4 layout slice depends on it, but nothing in it is C4-specific. Full stack status: #7849 (comment) |
Aligns the shared layout change with mermaid-js#7859: keep only the cluster label-height reservation (the c4-beta overlap fix); revert the edge-label centering that shifted edge labels for any subGraphTitleMargin>0 diagram (incl. flowcharts). c4-beta relationship-label centering can return later scoped to the c4-beta diagram type. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
knsv-bot
left a comment
There was a problem hiding this comment.
[sisyphus-bot]
Thanks for this, @filipsajdak — and apologies it's been sitting since 16 June. That's far too long for a change in rendering-util/, where the review burden is highest and the cost of going stale is real.
I went further than usual on verification here, because this is the single most regression-prone file in the repo. The headline: your central safety claim is true and I verified it independently — at the default margin the output is byte-for-byte identical. Everything below is about things around that claim, not the claim itself.
What's working well
🎉 [praise] The default-margin inertness is real, not asserted. I ran applyDagreLayoutResult over the same fixture on develop and on this branch:
| case | develop | this PR |
|---|---|---|
| margin 0 (default) | cluster {y:100,h:180} leaf {y:120} |
identical |
| margin 15, label 30 | cluster {y:100,h:195} leaf {y:127.5} |
cluster {y:122.5,h:210} leaf {y:157.5} |
Gating the whole behaviour behind subGraphTitleTotalMargin > 0 was the right call and it holds exactly as described.
🎉 [praise] deepestClusterReserve taking Math.max across sibling sub-clusters rather than summing them is the correct graph-theoretic insight — siblings share a rank, so only the heaviest single ancestor-to-leaf chain stacks vertically. The comment at dagre/index.js:44-48 explains precisely that, and it's the kind of reasoning that's usually left implicit and then broken six months later.
🎉 [praise] The comments explain why rather than what, which is rare and very welcome in layout code. dagre/index.js:80-83 ("Growing downward and moving the centre by half the growth keeps the cluster's top edge in place") is exactly the sentence a future reader needs.
🎉 [praise] The unit test is DDLT-style — DOM-decoupled, pre-captured labelBBox sizes, arithmetic derived step by step in the comments (index.spec.js:160-168). That matches the methodology in rendering-util/CLAUDE.md. I ran the full dagre suite on your branch: 70/70 pass.
🎉 [praise] The commit message is unusually honest about scope — explicitly recording that edge-label centering "was deliberately left out so current snapshots stay byte-identical." That's the right instinct, and it's what let me resolve the discrepancy below quickly.
Things to address
🔴 [blocking] — AI attribution trailers in the commit message
Commit 36c4b63 carries:
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ftYK49bbNLySKsY1Pg1Rg
and the PR description ends with 🤖 Generated with Claude Code.
CLAUDE.md is explicit on this: "Never tag commits as authored by Claude or any AI tool. Do NOT add Co-Authored-By: Claude …, 'Generated with Claude Code', 🤖 emojis, or any similar attribution to commit messages, PR descriptions, or PR titles."
Flagging it as blocking only because commit trailers are permanent once merged — the PR body can be edited any time, but the trailer needs a git commit --amend and force-push before this lands. No judgement on how the work was produced; it's purely the attribution metadata.
🟡 [important] — Nothing demonstrates the fix actually changes anything
This is my main technical concern, and it's a question more than an accusation.
The existing e2e suite already covers your target scenario: flowchart-v2.spec.js:1027 renders nested subgraphs (subgraph TOP containing B1 and B2) with subGraphTitleMargin: {top: 10, bottom: 5}, and there are five more like it plus appli.spec.js:32. Argos on this branch (build 7674, correct head SHA 36c4b63, so not stale) reports no visual change on any of them.
That's consistent with my probe results: the clusterNode branch is unchanged for a top-level cluster (ancestorReserve is 0), and only the graph.children(nodeId).length > 0 branch moves. Which suggests real flowchart subgraphs are recursively extracted into their own sub-graphs, so within each sub-graph the ancestor chain is empty and cumulativeAncestorReserve always returns 0.
If that's right, then the new nested-reserve logic isn't reachable from any diagram in the repo today, and the unit test hand-builds a graph shape the real pipeline may never produce. That's not necessarily wrong — I understand this is groundwork for the c4-beta deployment diagram (#7843/#7849) — but it means ~66 lines of geometry logic are landing in the highest-risk shared file with no end-to-end evidence they do what they claim.
Could you either add an imgSnapshotTest that visibly demonstrates the overlap being fixed, or confirm which pipeline actually hits the children branch? If the answer is "only c4 deployment, and that renderer isn't merged yet," it would be worth saying so explicitly in the PR body — and it's a reasonable argument for landing this together with #7843 rather than ahead of it. CLAUDE.md does ask for e2e coverage on renderer changes, and right now the visible behaviour is unverified in either direction.
🟡 [important] — Superlinear cost at the default margin (dagre/index.js:462-490)
Both helpers run unconditionally. cumulativeAncestorReserve re-walks the full ancestor chain for every node, and deepestClusterReserve re-walks the subtree for every cluster — the > 0 gate is inside getClusterTitleReserve, which returns 0 only after the traversal has happened.
Measured on applyDagreLayoutResult with a chain of nested clusters, at subGraphTitleTotalMargin: 0:
| nesting depth | develop | this PR |
|---|---|---|
| 500 | 3 ms | 24 ms |
| 1000 | 10 ms | 73 ms |
| 2000 | 44 ms | 485 ms |
To be fair about the real-world impact: this is depth-driven, and realistic diagrams nest maybe five or ten levels, where the overhead is negligible. I'm not claiming a practical DoS. But the changeset's promise is that default-margin diagrams are unaffected, and that's currently true of the output but not of the cost — and the fix is one line in each helper:
if (subGraphTitleTotalMargin <= 0) return 0;That restores "default margin is free" exactly as advertised. (An independent check also hit RangeError: Maximum call stack size exceeded at depth ~5000 where develop still completes — same one-line fix avoids it for the common case, or an iterative post-order walk removes the stack dependence entirely.)
🟡 [important] — Title and description promise edge-label centering that isn't here
The PR title is "reserve cluster label height and center edge labels" and the summary's fourth bullet describes centering edge labels on the rendered path. The diff contains no edge-label code at all — grep for edgeLabel|centerEdge over the diff returns zero matches — and your own commit message confirms it was deliberately dropped.
The body also says "Commits (review by commit)" and lists two, where there's now one. Since the PR title becomes the squashed commit subject, merging as-is would put a claim in the changelog for work that didn't ship. Just needs a title/body refresh.
🟡 [important] — The subGraphTitleMargin override hunk (dagre/index.js:825-835)
Three things tangled together here:
-
The comment describes the wrong shape. "
!== undefinedso an explicit diagram-level 0 opts out of a global margin" — butsubGraphTitleMarginis an object (config.schema.yaml:2274-2287,{top, bottom}, default{top: 0, bottom: 0}), not a number. A diagram passing a number like10would hit(10)?.top ?? 0insubGraphTitleMargins.ts:12-13and silently produce a total margin of 0 — the opposite of what was asked for. Sincedagre/index.jsisn't type-checked, nothing catches that. -
The
!== undefinedguard doesn't gate anything.LayoutData.configis a required, fully-resolvedMermaidConfig(rendering-util/types.ts:213-219), populated fromgetConfig()(e.g.flowDb.ts:200), soflowchart.subGraphTitleMarginis always present via the schema default. The branch is taken on essentially every render, copying the value over itself. -
The paint phase wouldn't honour it anyway.
rendering-elements/clusters.jsandedges.jscallgetConfig()directly rather than reading the overridden local, so ifdata4Layout.configever did diverge — which is the whole point of the override — titles and edge labels would be placed with the old margin while nodes used the new one.
Net: this hunk is currently a no-op with a misleading comment. Worth either dropping it until #7843 needs it, or threading the effective margin through to the painters so it's coherent when it does.
(One thing I checked and want to record as a non-issue: siteConfig.flowchart = {...} does not leak into global config. config.ts:140-142 returns assignWithDepth({}, currentConfig), a fresh copy per call. No state bleed between renders.)
🟢 [nit] — dagre/index.js:470
while (parentId) — graph.parent() returns undefined for a root, so !== undefined matches the contract more precisely and avoids truncating the walk on a falsy (empty-string) node id. Geometry-only even if it were reachable, so purely tidiness.
Security
No XSS or injection issues. The changed code is numeric geometry throughout — it reads node.labelBBox.height, graph.parent() and graph.children(), and writes only .y/.height. It touches no label text, href, innerHTML, .html(), foreignObject, or <style>, and DAGRE_NODE_LAYOUT_PROPERTIES (index.js:44-58) is an unchanged fixed allowlist. Hostile subGraphTitleMargin values (Infinity, NaN, negative, string, __proto__) produce bad geometry only — no crash, no prototype pollution (sanitizeDirective.ts:54-64 strips __proto__-style keys from %%{init}%% before they reach config). Parent-cycle recursion is impossible: graphlib's setParent throws on cycle creation, so the parent relation is always a forest.
CI
Green on the correct head SHA, including all 8 e2e shards, unit tests, lint and Argos. The three Netlify checks show skipping, not failing.
None of the above touches the core idea, which is sound and carefully reasoned. The blocking item is a one-command fix, and the important ones are mostly about making the PR's story match what the code does. Happy to re-review quickly once the trailer is off — let's get this across the finish line. 🚀
Review self-check
- Severity tally: 1 🔴 / 4 🟡 / 1 🟢 / 0 💡 / 5 🎉
- Verdict: REQUEST_CHANGES (1 🔴 present)
- Not a draft; no prior sisyphus-bot review rounds (escalation count 0)
- No inline comments used — all findings carry
file:linereferences in the body
|
Thank you - this is the most useful review I have had on this stack, and independently re-deriving the default-margin claim rather than taking my word for it is exactly the scrutiny this file deserves. All six points are addressed. Two of them I have to push back on with evidence, and one turned out to be a worse problem than described. 🔴 AI attribution - removedThe trailer is off the commit and the PR body. Amended and force-pushed. One observation offered only because it may be useful to you, not as an argument: the rule is not discoverable from a clone. 🟡 "Nothing demonstrates the fix actually changes anything" - fixed, and the diagnosis is half rightYour observation is right and your explanation is not, and the difference matters because it tells us which fixture to write. Extraction is not gated on nesting - it is gated on external connections ( } else if (!clusterDb.get(node).externalConnections && graph.children(node)?.length > 0) {
Why Argos was still silent: the fixture you cite has no boundary-crossing edge. Every edge in it is either inside one cluster ( So the missing ingredient was an edge into a nested subgraph. Added, at two margins so the effect is visible by comparison:
I have also added two unit tests: one for the 🟡 Superlinear cost at the default margin - fixedCorrect, and thank you for measuring it. The if (subGraphTitleTotalMargin <= 0) {
return 0;
}That also removes the deep-recursion 🟡 The
|
36c4b63 to
7e69b65
Compare
Aligns the shared layout change with mermaid-js#7859: keep only the cluster label-height reservation (the c4-beta overlap fix); revert the edge-label centering that shifted edge labels for any subGraphTitleMargin>0 diagram (incl. flowcharts). c4-beta relationship-label centering can return later scoped to the c4-beta diagram type.
|
Gentle nudge on this one - no rush intended, and happy to leave it sitting if the queue is full. Two things that may not be obvious from the checks:
It is 1 commit touching 4 files, and it is independent of the C4 series apart from being a prerequisite for the layout slice (#8042) and the c4-beta deployment visuals. If the shape of it is wrong I would rather hear that than have it wait - I am happy to split or close it. |
|
Following my nudge a few hours ago: the Cypress-to-Playwright migration landed in between and made this conflict, so please do not review the diff as it stood. It is ported and mergeable again. The two nested-subgraph cases were plain snapshot tests differing only in The explanation of why that particular diagram shape exercises the nested case moved into the fixtures, since there is no spec body to hold it any more. Nothing about the fix itself changed. |
|
Gentle nudge - no rush. The requested changes were addressed on 2026-08-20 and every review thread here is resolved, so this one is only waiting on the |
|
Could a maintainer dismiss or refresh the GitHub reports
Branch is Entirely my own oversight that I have been nudging this for three weeks without noticing it was formally blocked - I was checking |
2468ab6 to
c040e0f
Compare
… title fixtures in their own folder
A subgraph title is drawn above its children, and the space for it was reserved one level up only. A title in a nested subgraph therefore overlapped the children of the subgraph it sat in, and the deeper the nesting the worse it got. Every ancestor's reserve now accumulates, so a title always has its own band whatever the depth, with e2e coverage at two margin sizes. Edges move with their endpoints: each route point and label shifts by the reserve of the clusters around the ends it connects, and a merged self-loop, whose points are already placed, is not shifted a second time. The overlap was most visible in C4 diagrams, where every boundary is a cluster.
…lder The two nested-subgraph title fixtures now live in e2e/diagrams/flowchart/nested-subgraph-title/, so they form their own Argos sheet instead of joining diagrams/flowchart. e2e/sheet-order.json pins 139 of the 146 flowchart fixtures on develop. The unpinned rest is laid out alphabetically after them, and these two names sort before three of those fixtures, so every tile after them moved one cell and flowchart-012 and flowchart-013 were reported as changed although nothing in them re-rendered.
77258aa to
64dd1ea
Compare
|
The knsv-bot review from 2026-08-10 still shows changes requested, but every item in it is addressed on the current head (
Today's push also moved the two fixtures into their own folder, so they get their own Argos sheet instead of shifting the tiles of |
The title reserve reads each cluster's label height, but dagre only measured a cluster label while painting it, after layout. Every reserve therefore fell back to the flat margin, so a title taller than `subGraphTitleMargin` still overlapped the cluster's first row. `measureDagreGraph` now measures each cluster title before layout, the same way `insertCluster` paints it (plain labels unwrapped, markdown wrapped), and only when a title margin is set. A recursively rendered cluster measures its own title too, and its painter moves children, edges and nested clusters down by however much a title overflows the margin. When every title fits, the output is unchanged.
Problem
A cluster reserved a fixed margin for its title. When the label wrapped to more than one line, the label overlapped the nodes inside the cluster, and nested clusters overlapped at every level because each node only cleared its immediate parent's header.
Change
insertClusterpaints them: plain labels unwrapped, markdown labels wrapped.Active only when
flowchart.subGraphTitleMargingives a non-zero total. At the default margin the output is byte-identical and costs no extra traversal or measurement: every helper returns before walking the graph, and titles are not measured. With a margin set, a title that fits inside it also leaves the output unchanged. Only titles taller than the margin move anything.Expected Argos changes
Only dagre fixtures that set
subGraphTitleMarginsmaller than a title's rendered height: their cluster content moves down to clear the title.What this does not do
Edge-label centering was deliberately left out. It would have moved existing diagrams, and keeping current snapshots byte-identical was worth more. The PR title previously advertised it; that was wrong and is now corrected.
Reachability, and why the existing tests could not show this
Worth stating explicitly, because it is not obvious from the diff.
A subgraph is extracted into its own sub-graph unless an edge crosses its boundary (
mermaid-graphlib.js:343-347, gated onexternalConnections, set only when an edge has exactly one endpoint inside the cluster).normalizeDagreNoderuns on the top-level graph, so the nested-reserve path needs a cluster that survived extraction.Every existing test under "Subgraph title margins" uses edges that are either inside a single cluster or between cluster nodes (
A --> TOP,B1 --> B2). Cluster-to-cluster edges mark neither side, becauseisDescendant(id, id)is false - so all those clusters are extracted, and they reach only the sub-graph path described in the last bullet above, not the nested reserve.The new rendering test therefore uses an edge into a node inside a nested subgraph:
A --> i1marks bothB1andTOPas externally connected, so both stay compound and the nested reserve applies. It is rendered at two margins so the effect is visible by comparison.Two rendering unit tests (one extracted cluster, one top-level cluster, each with a title taller than the margin) check that the first child clears the measured title. Both fail without the pre-layout measurement. Unit tests additionally cover the
node.clusterNodebranch (shifted by its ancestors' reserve, not resized, since it already contains its own header) and pin the default-margin inertness so it is guarded by CI rather than asserted in prose.Consumer
The nested case is what C4 deployment diagrams hit constantly (#7843, tracked in #7849), which is why this exists. It is a plain dagre fix and stands on its own, but I have no objection to landing it alongside #7843 if reviewing them together is easier.
Note that per-diagram layout config for
subGraphTitleMarginis not part of this PR. An earlier revision threaded it through the layout only, which left cluster titles and edge labels placed with the global value because the painters readgetConfig()directly. That belongs with #7843, its only consumer, done coherently.Summary
subGraphTitleMarginis zero.mermaidpackage.