Skip to content

fix(flowchart): respect per-subgraph direction keyword in Dagre layout#7672

Open
sjackson0109 wants to merge 22 commits into
mermaid-js:developfrom
sjackson0109:fix/4648-directions
Open

fix(flowchart): respect per-subgraph direction keyword in Dagre layout#7672
sjackson0109 wants to merge 22 commits into
mermaid-js:developfrom
sjackson0109:fix/4648-directions

Conversation

@sjackson0109
Copy link
Copy Markdown
Contributor

@sjackson0109 sjackson0109 commented Apr 26, 2026

Summary

This PR fixes a long-standing bug (#4648, #6785) where subgraphs with an explicit direction keyword were silently ignored when those subgraphs had external connections.

What changed

packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.js

The cluster-extraction predicate was changed from:

if (!externalConnections && ...)

to:

if (clusterData?.explicitDir && ...)

The old check (!externalConnections) meant that any subgraph with an edge to/from a node outside its boundary would never get its own cluster sub-layout — so the user's direction keyword was silently discarded. The new check uses the explicitDir flag (set only when the user wrote an explicit direction X keyword) as the trigger, which is the correct and intentional signal from the user.

packages/mermaid/src/diagrams/flowchart/flowDb.ts

  • addSubGraph now captures hasExplicitDir = rawDir !== undefined before any inherited-direction fallback, ensuring the flag is true only when the user wrote an explicit direction keyword.
  • getData maps explicitDir: subGraph.hasExplicitDir and normalises direction TD → TB, mirroring the existing normalisation in setDirection for the top-level graph.

packages/mermaid/src/diagrams/flowchart/types.ts

Added hasExplicitDir: boolean to the FlowSubGraph interface to carry the parse-time flag through to layout.

packages/mermaid/src/rendering-util/types.ts

Added explicitDir?: boolean to the BaseNode interface so the flag is visible to mermaid-graphlib.js.

Behavior change

Scenario Old behavior New behavior
Subgraph with explicit direction, no external edges Separate cluster sub-layout Separate cluster sub-layout ✅ (unchanged)
Subgraph with explicit direction, has external edges Inherits parent rankdir Separate cluster sub-layout ✅ (fixed)
Subgraph with no explicit direction Separate cluster sub-layout, defaults to opposite of parent rankdir Inherits parent rankdir (no separate cluster)

⚠️ The third row is a visible default-behavior change. Subgraphs without an explicit direction keyword previously received a separate cluster graph (defaulting to the opposite rankdir of the parent). They now simply inherit the parent direction. The full e2e Argos run will show the visual impact of this change.

Tests

  • Two new regression tests added to cypress/integration/rendering/flowchart/flowchart-v2.spec.js:

  • Manual visual test page added at cypress/platform/regression/issue-4648.html

  • Unit tests in mermaid-graphlib.spec.js: all 19 tests pass ✅ including 4 new GLB-DIR tests that specifically exercise the explicitDir → separate-cluster code path.

Fixes #4648
See also #6785

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 26, 2026

🦋 Changeset detected

Latest commit: 8eccac1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
mermaid Patch

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

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 26, 2026

Deploy Preview for mermaid-js ready!

Name Link
🔨 Latest commit 8eccac1
🔍 Latest deploy log https://app.netlify.com/projects/mermaid-js/deploys/6a00e2dafc6ca300082fcd61
😎 Deploy Preview https://deploy-preview-7672--mermaid-js.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions Bot added the Type: Bug / Error Something isn't working or is incorrect label Apr 26, 2026
@sjackson0109
Copy link
Copy Markdown
Contributor Author

#4648 should be fixed

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 26, 2026

Open in StackBlitz

@mermaid-js/examples

npm i https://pkg.pr.new/@mermaid-js/examples@7672

mermaid

npm i https://pkg.pr.new/mermaid@7672

@mermaid-js/layout-elk

npm i https://pkg.pr.new/@mermaid-js/layout-elk@7672

@mermaid-js/layout-tidy-tree

npm i https://pkg.pr.new/@mermaid-js/layout-tidy-tree@7672

@mermaid-js/mermaid-zenuml

npm i https://pkg.pr.new/@mermaid-js/mermaid-zenuml@7672

@mermaid-js/parser

npm i https://pkg.pr.new/@mermaid-js/parser@7672

@mermaid-js/tiny

npm i https://pkg.pr.new/@mermaid-js/tiny@7672

commit: 8eccac1

@sjackson0109
Copy link
Copy Markdown
Contributor Author

The unit tests are failing with errors such as:

Error: TypeError: Cannot read properties of undefined (reading 'isDirected')
 ❯ Module.write node_modules/.pnpm/dagre-d3-es@7.0.14/node_modules/dagre-d3-es/src/graphlib/json.js:64:19
 ❯ packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.spec.js:341:44

and

Error: TypeError: Cannot read properties of undefined (reading 'node')
 ❯ packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.spec.js:366:29

and

Error: TypeError: Cannot read properties of undefined (reading 'node')
 ❯ packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.spec.js:396:27

These failures are expected, for now.

This PR depends on a new release of Dagre JS, specifically the changes in this PR I submitted here: dagrejs/dagre#511

That update provides the layout engine with the additional attributes required for these tests to pass.

Copy link
Copy Markdown
Collaborator

@knsv-bot knsv-bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[sisyphus-bot]

Thanks for taking on #4648 @sjackson0109 — this is one of the longest-standing layout bugs in the project, and the angle you've gone for (gate on explicit direction rather than on !externalConnections) is genuinely more principled than the existing logic. That said, before this can land I think it needs more guardrails and a couple of fixes, given how broadly mermaid-graphlib.js reaches.

What's promising

  • 🎉 The core conceptual change in mermaid-graphlib.js:327-330 — switching the cluster-extraction predicate from !externalConnections to clusterData?.dir — is the right shape of fix. The old check was the actual root cause of the "external edge into a subgraph node leaks direction" behavior reported in #4648 and #6785. Treating "user explicitly said direction X" as the trigger for a separate sub-layout is much more intentional.
  • 🎉 The TDTB normalisation in flowDb.ts:703-706 (inside addSubGraph) plugs a real gap — setDirection() already normalises this for the top-level direction (flowDb.ts:451-453), but subgraph directions previously didn't, which is exactly the "direction TD doesn't work, you have to use direction TB" complaint in the issue thread. Mirroring the existing normalisation here is the right call.

Things to address

🔴 [blocking] No automated tests for a shared-layout change

packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.js is used by every Dagre-based diagram type (flowchart, class, state, etc.). CLAUDE.md is explicit on this:

rendering-util/ is shared by all diagram types. Changes here can regress any diagram. Always run full e2e tests (pnpm e2e) after modifying shared rendering code — unit tests alone are insufficient.

The existing Cypress suite already has extensive subgraph-direction coverage in cypress/integration/rendering/flowchart/flowchart-v2.spec.js (around lines 696, 814, 1014+ — multiple test cases mixing top-level and subgraph directions, nested subgraphs, etc.). Two things needed:

  1. Run those existing tests against the change and post the Argos diff — that's the only way to know whether the new condition regresses existing flowchart layouts. Subgraphs without an explicit direction keyword no longer get a separate cluster graph under the new logic (old code: separate graph if !externalConnections; new code: separate graph only if clusterData?.dir), so default-direction subgraphs may shift visually.
  2. Add a new test in that same file specifically targeting #4648's two patterns: (a) external edge into a subgraph internal node where the subgraph has its own direction, (b) sibling subgraphs with different directions connected via subgraph-to-subgraph edges. A side-by-side that locks in the fixed behavior would also serve as the long-term regression guard the issue thread has been asking for.

🔴 [blocking] The manual test in test/4648.html won't parse

The mermaid source in the <div class="mermaid"> block has no newlines between top-level statements:

flowchart TD subgraph Group1 direction TB A1 --> A2 A2 --> A3 end subgraph Group2 direction LR
B1 --> B2 B2 --> B3 end subgraph Group3 direction LR C1 --> C2 C2 --> C3 end %% External
connections between subgraphs A3 --- B1 B3 --- C1 %% Wrapper workaround subgraph Wrapper
direction LR subgraph Inner D1 --> D2 D2 --> D3 end end

Mermaid's flowchart parser uses newlines/semicolons as statement separators — flowchart TD subgraph Group1 direction TB A1 --> A2 on a single line is a parse error, not a rendered diagram. Looks like the file was reflowed by an editor at some point. Each statement (flowchart TD, subgraph X, direction Y, each edge, each end) needs its own line. There's also a B1 B3 token sequence in A3 --- B1 B3 --- C1 that won't parse even with newlines — looks like that should be two separate edges (A3 --- B1 and B3 --- C1).

Worth opening this file in the dev server (pnpm dev) and confirming it actually renders before relying on it as the visual proof.

🟡 [important] Test file location

test/ isn't an established directory in this repo — manual test pages live in cypress/platform/ (e.g., cypress/platform/subgraph.html already exists for similar purposes), and automated visual tests live in cypress/integration/rendering/. The new test/4648.html won't be picked up by any tooling and will likely confuse future contributors. Suggest:

  • Move the manual page to cypress/platform/4648.html (or merge it into the existing subgraph.html), and
  • Add the automated coverage as a Cypress test in flowchart-v2.spec.js.

🟡 [important] Missing changeset

This is a user-visible layout-behavior change, so it needs a .changeset/*.md entry. pnpm changesetpatch for mermaid with a fix: description should do it.

🟡 [important] PR description is contradictory

The title says "fix(flowchart): improve subgraph direction handling" and the body says "This PR does not attempt to fix Dagre, but provides a clear test and documentation of the issue." The diff clearly does attempt a fix — both mermaid-graphlib.js and flowDb.ts change runtime layout behavior. Worth rewriting the description to reflect what's actually being changed (which condition, which code path, and what the expected behavior shift is) — that helps future maintainers reading git blame.

Behavioral question worth flagging in the description

Worth being explicit about how the new logic handles subgraphs without an explicit direction keyword:

  • Old behavior: !externalConnections && hasChildren → create a separate cluster graph, defaulting to the opposite of the parent's rankdir. So a subgraph in a TB flowchart (with no direction) would lay out internally as LR by default.
  • New behavior: only clusterData?.dir && hasChildren triggers a separate cluster graph. Default-direction subgraphs now inherit the parent's rankdir.

That's a defensible direction (no pun intended), but it's a visible default-behavior change that could shift many existing flowcharts. The full e2e Argos run is the only way to know how broad the impact is — and if the change is intentional, the changeset should call it out so users know what's coming.

Security

No XSS or injection concerns — the layout-algorithm changes don't touch DOM sinks or user-facing string interpolation, and the new manual HTML page is a standard mermaid-init page (not loaded by the published library).

Summary

Severity Count
🔴 blocking 2
🟡 important 3
🟢 nit 0
💡 suggestion 0
🎉 praise 2

The conceptual fix is the right shape — please don't take the volume of feedback as discouragement. The combination of "shared layout code with broad blast radius" + "no automated tests" + "broken manual repro" is what's pushing this to changes-requested rather than just comments. With the Cypress coverage in place and an Argos diff to look at, this could land cleanly. Happy to help if any of the above is unclear.

@sjackson0109 sjackson0109 changed the title fix(flowchart): improve subgraph direction handling and add comprehensive visual test (fixes #4648) fix(flowchart): respect subgraph direction handling Apr 27, 2026
@sjackson0109 sjackson0109 changed the title fix(flowchart): respect subgraph direction handling fix(flowchart): respect per-subgraph direction keyword in Dagre layout May 1, 2026
@argos-ci
Copy link
Copy Markdown

argos-ci Bot commented May 1, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ⚠️ Changes detected (Review) 13 changed, 2 added May 10, 2026, 8:10 PM

@sjackson0109 sjackson0109 requested a review from knsv-bot May 1, 2026 10:48
…ates

- Patch dagre-d3-es@7.0.14 via pnpm patch to include applyClusterDirections
  (runs after main dagre layout) and preserve rankdir on cluster nodes in
  buildLayoutGraph.  This is a forward-port of the same fix in the dagre fork
  (sjackson0109/dagre@feature/per-cluster-direction, dagrejs/dagre#511).

- Fix mermaid-graphlib.js extractor predicate:
  * Restore the original !externalConnections branch (was accidentally
    dropped in commit 415af35, causing GLB test regressions where all
    normal clusters without external connections stopped being extracted)
  * Keep the new clusterData?.dir branch as the FIRST check, so subgraphs
    with an explicit direction keyword are ALWAYS given their own sub-graph,
    even when they have external connections (this is the actual mermaid-js#4648 fix)
  * Normalize TD→TB in both branches when setting rankdir on clusterGraph
    (dagre's makeSpaceForEdgeLabels uses 'TB', not 'TD')

- Remove premature TD→TB normalization from flowDb.ts addSubGraph — it was
  changing the parsed dir value before the parser tests could check it,
  breaking subgraph.spec.js line 325.  Direction normalization now happens
  inside mermaid-graphlib.js when rankdir is set on the cluster sub-graph.
…tor branches

Both Branch 1 (explicit direction) and Branch 2 (no external connections)
were using copy(child, graph, clusterGraph, child) per-child loops, which
is a no-op for leaf nodes and incorrectly handles nested sub-clusters.

The original code used copy(node, graph, clusterGraph, node) — a single
call with the cluster node as the root — which correctly:
 - Extracts all direct leaf children into the clusterGraph
 - Recursively handles nested sub-clusters with proper parent relationships
 - Removes extracted nodes from the outer graph

This fixes all 11 mermaid-graphlib unit tests (GLB4-GLB77) that were
regressed by the earlier change, and enables test 2050 (issue mermaid-js#4648)
to render correctly in e2e tests.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 2, 2026

Codecov Report

❌ Patch coverage is 0% with 75 lines in your changes missing coverage. Please review.
✅ Project coverage is 3.28%. Comparing base (f50fb4f) to head (8eccac1).

Files with missing lines Patch % Lines
...g-util/layout-algorithms/dagre/mermaid-graphlib.js 0.00% 67 Missing ⚠️
packages/mermaid/src/diagrams/flowchart/flowDb.ts 0.00% 7 Missing ⚠️
packages/mermaid/src/diagrams/state/dataFetcher.ts 0.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           develop   #7672      +/-   ##
==========================================
- Coverage     3.29%   3.28%   -0.01%     
==========================================
  Files          561     561              
  Lines        58576   58642      +66     
  Branches       873     873              
==========================================
  Hits          1928    1928              
- Misses       56648   56714      +66     
Flag Coverage Δ
unit 3.28% <0.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
packages/mermaid/src/diagrams/flowchart/types.ts 100.00% <ø> (ø)
packages/mermaid/src/diagrams/state/stateDb.ts 0.00% <ø> (ø)
packages/mermaid/src/rendering-util/types.ts 100.00% <ø> (ø)
packages/mermaid/src/diagrams/state/dataFetcher.ts 0.35% <0.00%> (-0.01%) ⬇️
packages/mermaid/src/diagrams/flowchart/flowDb.ts 0.00% <0.00%> (ø)
...g-util/layout-algorithms/dagre/mermaid-graphlib.js 0.00% <0.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

sjackson0109 and others added 5 commits May 2, 2026 03:06
…actor (Branch 1 coverage)

Adds 3 new unit tests (GLB-DIR1, GLB-DIR2, GLB-DIR3) covering the new
clusterData?.dir branch introduced by fix mermaid-js#4648:
- GLB-DIR1: cluster with explicit dir + external connections becomes clusterNode
- GLB-DIR2: cluster with explicit dir + no external connections becomes clusterNode
- GLB-DIR3: sibling clusters with different explicit dirs and inter-cluster edge

These tests exercise the ~47 lines of Branch 1 that were flagged by Codecov
as having 0% patch coverage.
…an nodes

When copy() extracts a cluster whose leaf nodes have edges that cross the
cluster boundary, edgeInCluster's OR-logic was copying those edges into the
subgraph and auto-creating the external endpoint as a zero-width orphan node.
recursiveRender then called insertNode(nodes, undefined) and crashed, leaving
window.rendered=false and failing Cypress tests for:
  issue-4648: sibling subgraphs with different directions and external connections
  issue-4648: nested subgraph with external connection
  2050: handling of different rendering direction in subgraphs

Fix: check whether BOTH endpoints are strictly inside rootId's cluster.
  - Strictly internal edge: copy to newGraph as before.
  - Cross-boundary edge: rebind in the outer graph as rootId to externalNode,
    so the cluster connection is preserved after removeNode() drops the leaf.
Every compound state node was receiving dir:'TB' (the DEFAULT_NESTED_DOC_DIR
default) even when the user wrote no 'direction' keyword inside that state.
That truthy dir value caused mermaid-graphlib Branch 1
(clusterData?.dir && hasChildren) to trigger for ALL compound states,
including those with external connections, changing their layout relative
to the original !externalConnections-based extraction rule.

Fix: only set newNode.dir when parsedItem.doc actually contains a 'dir'
statement (i.e. the user explicitly wrote 'direction X' inside the state
body), leaving dir as undefined otherwise.

This restores correct rendering for:
  State-diagram-v2-A-compound-state-should-be-able-to-link-to-itself
  State-diagram-v2-handle-transition-from-one-state-in-a-composite-state-to-a-composite-state

Compound states WITH explicit direction still go through Branch 1.
…nch 1

The Branch 1 predicate  clusterData?.dir  was accidentally catching ALL
compound state nodes, because state diagram dataFetcher always sets
dir:'TB' (DEFAULT_NESTED_DOC_DIR) even when the user wrote no 'direction'
keyword.  This caused 36 state diagram layouts to regress.

A previous attempted fix set dir:undefined for non-explicit state nodes,
which broke 36 OTHER diagrams because Branch 2 also relies on clusterData.dir
for its sub-layout direction arithmetic.

Proper fix: add an explicitDir:boolean field, true only when the user
explicitly wrote a 'direction X' keyword in the subgraph/state body.
Branch 1 now checks explicitDir instead of dir, so:

  flowchart subgraph with  direction LR :  explicitDir:true  -> Branch 1
  flowchart subgraph without direction:    explicitDir:false -> Branch 2/else
  state compound state with  direction LR: explicitDir:true  -> Branch 1
  state compound state without direction:  explicitDir:false -> Branch 2/else

Branch 2's direction override (clusterData?.dir) is untouched and keeps
working exactly as before for both flowchart and state diagram.
@sjackson0109
Copy link
Copy Markdown
Contributor Author

sjackson0109 commented May 2, 2026

The ci items of concern now are the argos-ci tests.

Category Count Caused by this PR
Unchanged diagrams 2794 NO IMPACT
Added (new tests) 2 Intended diagrams created > our new test screenshots (expeced)
Changed: architecture flaky test 1 ❌ NO NEW IMPACT > pre-existing situation 0% stability for 2 months on develop itself

@sjackson0109
Copy link
Copy Markdown
Contributor Author

sjackson0109 commented May 2, 2026

Any chance one of you can review and consider approving?
@ashishjain0512 @pbrolin47 @knsv @lee-treehouse
I would appreciate a review when you have a moment. This fixes a 2-year-old issue (#4648) where subgraph direction keywords were silently discarded whenever a subgraph had external connections. Argos shows 2794 unchanged + 2 new test screenshots + 1 pre-existing flaky architecture test (0% stability on develop itself for 2 months, unrelated to this PR).

Copy link
Copy Markdown
Collaborator

@ashishjain0512 ashishjain0512 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[sisyphus-bot]

Round 2 — and first, thank you for the substantial follow-through on round 1, plus apologies for how long this has been sitting in the queue. You asked for a review on May 2 and the queue has been slow; that's on us, not on you. The work in this revision is genuinely good. 🎉 [praise]

A few things still need attention before this can land — most importantly two test/cleanup issues, plus one architectural call that probably needs a maintainer rather than me. Round 1 → round 2 already cleared the previous blockers; one more pass should put this over the line.

What's working well

  • 🎉 The explicitDir refinement is better than what I asked for in round 1. Preserving the original !externalConnections path for default-direction subgraphs (so they keep inheriting the parent rankdir) and only diverging when the user wrote a direction keyword is the right call — it cleanly addresses the round-1 concern about "default-direction subgraphs may shift visually" without giving up the fix.
  • 🎉 Real test coverage added. Two Cypress visual cases in flowchart-v2.spec.js:351-92 covering #4648's two patterns (sibling subgraphs with different directions, and nested with external edges), plus the manual page at the conventional location (cypress/platform/regression/issue-4648.html). That's exactly what round 1 asked for.
  • 🎉 State-diagram parity. Threading explicitDir through dataFetcher.ts:274-276 and stateDb.ts:151 so compound states behave consistently is more thorough than #4648 strictly required, and avoids leaving state in an inconsistent place. Nice touch.
  • 🎉 Changeset is well-written and explicitly calls out the behavior shift — readers tracing the changelog will understand both the fix and the intent.

Things to address

🔴 [blocking] The new unit tests don't actually exercise Branch 1

This one's subtle and worth verifying yourself. I ran the new spec tests with verbose output:

$ pnpm vitest run packages/mermaid/src/rendering-util/layout-algorithms/dagre/mermaid-graphlib.spec.js -t "GLB-DIR"
...
WARN: Cluster without external connections, without a parent and with children C1 0   ← GLB-DIR1
WARN: Cluster without external connections, without a parent and with children C1 0   ← GLB-DIR2
WARN: Cluster without external connections, without a parent and with children B1 0   ← GLB-DIR3
WARN: Cluster without external connections, without a parent and with children B2 0
✓ all 3 pass

All three tests are landing in the old Branch 2 (!externalConnections && hasChildren), not the new Branch 1 (explicitDir && hasChildren). The reason: the test setup uses g.setNode('C1', { data: 3, dir: 'LR' })dir is set but explicitDir is not, and clusterData is just the node object (mermaid-graphlib.js:219: clusterDb.set(id, { id: ..., clusterData: graph.node(id) })). So clusterData?.explicitDir is undefined, Branch 1 is skipped, and the cluster gets extracted by the unchanged Branch 2 logic.

In other words, deleting Branch 1 entirely would not break these tests — they'd still pass. The new code path is unit-untested.

Fix: set explicitDir: true on each cluster node in the test setup, e.g.:

g.setNode('C1', { data: 3, dir: 'LR', explicitDir: true });

Worth confirming with a quick TDD sanity check: temporarily comment out the new Branch 1, re-run the tests, and they should fail. If they still pass, the test still isn't exercising the new branch.

(The Cypress visual tests do exercise Branch 1 transitively — they go through flowDb.ts which sets explicitDir: !!subGraph.dir. But for a shared rendering-util change of this scope, unit-level coverage of the new branch is the right safety net.)

🔴 [blocking] Old test/4648.html is still in the diff

Round 1 flagged this one as 🔴 because the mermaid source was on a single line and wouldn't parse. You did the right thing creating the properly-formatted page at cypress/platform/regression/issue-4648.html, but the broken file is still being added at test/4648.html (line 674 of the diff). The test/ directory isn't an established location, the source still doesn't parse, and keeping it around will confuse future contributors who find it via grep. Please delete it.

🟡 [important] The 143-line dagre-d3-es@7.0.14.patch warrants a maintainer call

This is the part I'd most like a human to weigh in on, so I'm flagging it but not insisting on a particular outcome:

  • Magnitude: The existing patches/roughjs.patch is 4 lines of type-export tweak. This new patch is 143 lines of new layout logic (applyClusterDirections, getAllDescendants, sub-graph construction, position translation). That's a different kind of decision.
  • Two parallel cluster-layout systems: mermaid-graphlib.js's extractor already builds a per-cluster clusterGraph with its own rankdir (your new Branch 1 lines 314-356 do exactly that). The applyClusterDirections function in the patch does conceptually similar work after the main layout pass. It's not obvious to me whether both layers are necessary, or whether one alone would suffice — what specifically breaks if the dagre-d3-es patch is dropped?
  • Maintenance: every upstream dagre-d3-es version bump now requires patch reconciliation. Worth either upstreaming (PR to dagre-d3-es) or having a clear "remove once X" target.

If the patch is genuinely needed, that's fine — but the architectural call ("are we comfortable taking on a 143-line third-party patch with parallel logic to existing code?") is bigger than I can resolve. @ashishjain0512 / @knsv / @aloisklink — worth your eyes.

🟡 [important] Cross-boundary edge rebinding in copy() lacks a unit test

mermaid-graphlib.js:282-307 introduces new logic that rebinds edges crossing the cluster boundary so they terminate at rootId in the outer graph (preventing the orphan-node crash you describe in the comment). The reasoning is sound, but it's a behavior change for any cluster hitting the copy() path. The Cypress tests cover it transitively via #4648 patterns, but a direct unit test that sets up a cross-boundary edge and asserts:

  • the inner edge does NOT appear in clusterGraph
  • the outer graph has a rebound edge from rootId to the external node

…would lock in the behavior and protect against future regressions. Could be added to the new GLB-DIR* describe block.

🟡 [important] Changeset description points to the wrong file

The changeset says:

Also normalises direction TD to direction TB inside addSubGraph

But the diff puts the TD→TB normalization in mermaid-graphlib.js:322-325 (new Branch 1) and :372-373 (existing Branch 2), not in flowDb.ts addSubGraph (whose only change is the explicitDir flag). Round 1's review praised an addSubGraph normalization, so I'm guessing it lived there in an earlier revision and got moved. Either:

  • (a) move the normalization back into addSubGraph (single source of truth, matches the existing setDirection normalization at flowDb.ts:451-453), or
  • (b) update the changeset to point to mermaid-graphlib.

(a) is probably cleaner — normalize once at parse-time rather than at every layout-extractor branch.

🟡 [important] Argos visual diff is the real safety net

This is a CI/maintainer thing rather than something you need to act on, but worth saying: with mermaid-graphlib.js + the dagre-d3-es patch both touched, the e2e Argos run on this PR is the most informative thing for landing it. If Argos comes back clean across flowchart/class/ER/state, the architectural concerns above mostly evaporate. If Argos shows shifts, those become the conversation.

Security

No XSS or injection concerns — the changes are entirely layout-positioning logic. No DOM sinks, no user-string interpolation, no DOMPurify config touched. The new manual HTML page is a standard mermaid-init demo (not loaded by the published library).

Summary

Severity Count
🔴 blocking 2
🟡 important 4
🟢 nit 0
💡 suggestion 0
🎉 praise 4

Verdict: REQUEST_CHANGES.

This is round 2 of automated review. Per project policy, if a third round is still requesting changes, the protocol is to escalate to a human reviewer rather than keep iterating with me — so the architectural call on the dagre-d3-es patch may end up landing there regardless. If you want to split this PR, dropping the patch and shipping just the explicitDir + tests + cleanup as a smaller fix would also be a sensible path forward — the patch can come as a follow-up once it's discussed.

Either way: thanks again for the patience and the depth of work here. Once the test-bug is fixed, test/4648.html is removed, and there's a path on the dagre-d3-es patch, this should land. 🙏

- Add explicitDir: true to GLB-DIR1/2/3 cluster node setups so tests
  actually exercise Branch 1 (clusterData.explicitDir check) rather than
  the unchanged Branch 2 path. Deleting Branch 1 would have left the
  previous tests green -- that loophole is now closed.

- Add GLB-DIR4 unit test: cross-boundary edge from internal node to
  external node must NOT be copied into the clusterGraph; the outer graph
  must instead carry a rebound edge from the cluster root (C2) to the
  external node. Directly locks in the copy() rebinding logic.

- Delete test/4648.html (broken: diagram source on one line, non-standard
  location). The correctly-formatted manual repro is already at
  cypress/platform/regression/issue-4648.html.

- Move TD->TB normalisation from mermaid-graphlib.js (two sites) into
  flowDb.ts addSubGraph -- parse-time single source of truth, mirrors
  setDirection() and matches the changeset description.

- Update changeset to reference clusterData?.explicitDir (actual check)
  rather than clusterData?.dir.
@sjackson0109
Copy link
Copy Markdown
Contributor Author

@ashishjain0512 any chance of another review?

@pbrolin47
Copy link
Copy Markdown
Collaborator

Hi @sjackson0109,
Thanks for the work you spend to resolve the problem with direction keyword for subgraphs.
I understand you want @ashishjain0512 to do an additional review, but in some cases we try to share the tasks of reviewing PR:s.

[sisyphus-bot]


What's Working Well

🎉 [praise] The explicitDir boolean is an elegant semantic solution. The root of the state diagram regressions in earlier commits was that checking clusterData?.dir was ambiguous — state compound
nodes always have a default dir. Using a dedicated boolean that is only true when the user explicitly wrote direction X is the right abstraction.

🎉 [praise] The cross-boundary edge rebinding in copy() is well-diagnosed and well-reasoned. The OR-logic bug in edgeInCluster causing orphan crash was subtle — tracing it through the crash
(insertNode(nodes, undefined)) and fixing it correctly (rebind in outer graph vs. auto-create in inner graph) shows solid debugging work.

🎉 [praise] The state diagram fix in dataFetcher.ts (parsedItem.doc.some((s) => s.stmt === 'dir')) is precisely targeted. Rather than a broad change, it pinpoints exactly which state diagram nodes
have user-intent direction vs. default direction.

🎉 [praise] The GLB-DIR4 test (cross-boundary edge not copied into clusterGraph) is especially valuable — it directly tests the copy() bug that was triggering the Cypress crash.


Things to Address

🟡 [important] patches/dagre-d3-es@7.0.14.patch — The 143-line vendor patch adds applyClusterDirections to dagre's layout pipeline, but this function never fires in Mermaid's rendering pipeline.

applyClusterDirections triggers when an individual graphlib node has node.rankdir set. In Mermaid's pipeline, rankdir is only ever set on graph metadata (graph.setGraph({ rankdir: dir })), never on
individual node data objects. Looking at getData() in flowDb.ts (line 1120: dir: subGraph.dir) and dataFetcher.ts (line 1179: newNode.dir = getDir(...)), and at index.js's render() which spreads
node data with { ...node } — no code path ever sets node.rankdir as a node property. So the dagre patch's guard if (node.rankdir) { nodeAttrs.rankdir = node.rankdir; } in buildLayoutGraph and the
entire applyClusterDirections function are dead code.

The actual fix is Branch 1 in mermaid-graphlib.js — it correctly extracts clusters with explicit direction into their own subgraph regardless of external connections, and the recursive extractor +
recursiveRender pipeline lays them out with the correct rankdir. That path works without the dagre patch.

The patch as-is:

  • Ships 143 lines of dead code into every Mermaid deployment
  • Pins behavior to dagre-d3-es@7.0.14 specifically (harder to upgrade)
  • Makes future dagre-d3-es upgrades require re-applying and verifying the patch
  • Obscures the actual fix

It would be great to verify the fix still works without the patch (remove patches/dagre-d3-es@7.0.14.patch, the package.json entry, and pnpm-lock.yaml changes, then rerun the Cypress tests). If the
tests pass, the patch can be dropped entirely.

🟡 [important] flowDb.ts:1226 — explicitDir: !!subGraph.dir has a false-positive case with flowchart.inheritDir: true. When inheritDir is enabled, addSubGraph() (lines 703–708) sets dir to the
parent direction via the nullish coalescing fallback — even when the user wrote no direction keyword:

dir = dir ?? (flowchartConfig.inheritDir
? (this.getDirection() ?? ...)
: undefined);

If inheritDir: true and dir is undefined (no explicit keyword), dir gets the parent direction. Then explicitDir: !!subGraph.dir becomes true, triggering Branch 1 for all subgraphs — including
externally-connected ones that previously used the else path. This is a behavioral change for inheritDir: true users that isn't documented.

A more robust fix would track the flag before the inheritDir override:

let dir = result.dir;
const hasExplicitDir = dir !== undefined; // capture BEFORE inheritDir override
if (dir === 'TD') { dir = 'TB'; }
dir = dir ?? (flowchartConfig.inheritDir ? this.getDirection() : undefined);
// ...
// In getData():
explicitDir: hasExplicitDir,


Security

No XSS or injection issues identified. The dir/explicitDir values are bounded by JISON grammar literals (TB, LR, RL, BT, TD) and flow only through dagre's numeric layout calculation — never into SVG
text or DOM attributes. DOMPurify pipeline is unchanged.


Tests

Good coverage:

  • 4 new unit tests (GLB-DIR1 through GLB-DIR4) covering Branch 1 with explicit direction + external connections, explicit direction + no external connections, sibling clusters with different
    directions, and cross-boundary edge rebinding
  • 2 new Cypress E2E tests covering the sibling-subgraph and nested-subgraph scenarios from issue Direction in subgraphs inconsistent #4648

One note: snapshot baselines for the new Cypress tests will be generated on first CI run. This is the expected workflow.


Copilot AI and others added 6 commits May 7, 2026 16:36
…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>
…e-consumption point

Agent-Logs-Url: https://github.com/sjackson0109/mermaid/sessions/ed8547c3-2e55-4088-bbec-113a7976d0aa

Co-authored-by: sjackson0109 <38080190+sjackson0109@users.noreply.github.com>
Agent-Logs-Url: https://github.com/sjackson0109/mermaid/sessions/ed8547c3-2e55-4088-bbec-113a7976d0aa

Co-authored-by: sjackson0109 <38080190+sjackson0109@users.noreply.github.com>
…-handling

fix(flowchart): preserve raw subgraph direction value; normalize TD→TB only for dagre
@sjackson0109
Copy link
Copy Markdown
Contributor Author

CI status — unit-test failure is pre-existing, not introduced by this PR

The unit-test job is failing due to pre-existing test failures that exist identically on upstream/develop HEAD (8b52e5378):

Failing test file Failures Root cause
packages/parser/tests/treeView.test.ts 19 incomplete treeView parser feature (merged in #7527)
packages/mermaid/src/diagrams/treeView/parser.spec.ts 27 same
packages/mermaid/src/diagrams/cynefin/cynefin.integration.spec.ts 15 @mermaid-js/parser missing DomainBlock/Transition/Cynefin exports (merged in #7535)
packages/mermaid/src/diagrams/wardley/wardleyParser.spec.ts 7 pre-existing
packages/mermaid/src/diagrams/architecture/svgDraw.spec.ts 1 canvas unavailable in jsdom (pre-existing)
packages/mermaid/src/mermaidAPI.spec.ts 1 same canvas issue
2 others 2 canvas issue

Verified locally against upstream/develop: both branches produce 8 failed | 129 passed, 72 individual test failures — identical counts, identical test names.

This PR introduces zero new test failures. All flowchart and graphlib tests pass:

  • mermaid-graphlib.spec.js19/19 passed ✅ (including 4 new GLB-DIR tests)
  • All flowchart diagram tests → 971 passed, 3 skipped

@knsv
Copy link
Copy Markdown
Collaborator

knsv commented May 8, 2026

I could rerun the unit test, and then it worked. GitHub had a network glitch.

@knsv
Copy link
Copy Markdown
Collaborator

knsv commented May 8, 2026

Thanks for tackling this one, @sjackson0109#4648 has been open since 2023 and the diagnosis here is exactly right. The cluster-extraction predicate using !externalConnections was the wrong signal; pivoting to explicitDir cleanly separates the fix from the legacy default-behavior path. Really nice work, and the writeup in the PR description, (especially the behavior-change matrix) made this a much faster read.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Bug / Error Something isn't working or is incorrect

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Direction in subgraphs inconsistent

6 participants