Skip to content

fix: avoid Sankey nodes overlapping skipped-depth links#7471

Merged
PavelVanecek merged 3 commits into
recharts:mainfrom
pupuking723:fix/sankey-node-overlap
Jun 24, 2026
Merged

fix: avoid Sankey nodes overlapping skipped-depth links#7471
PavelVanecek merged 3 commits into
recharts:mainfrom
pupuking723:fix/sankey-node-overlap

Conversation

@pupuking723

@pupuking723 pupuking723 commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Description

Fixes a Sankey layout case where a high-value link can skip over an intermediate depth and visually cover smaller nodes in that depth.

This keeps the existing node sizing and nodePadding behavior unchanged. After the normal Sankey relaxation and collision passes complete, the layout now checks links that span across intermediate depth columns. If an intermediate node is fully contained inside one of those skipped-depth link bands, that link is treated as a fixed local obstacle and the affected depth is adjusted once.

Related Issue

Fixes #5559.

Motivation and Context

The workaround discussed in the issue is to increase nodePadding, but that does not scale well when a Sankey chart has many nodes. The root problem is that normal node collision handling only considers nodes in the same depth, while a link that skips a depth can still occupy visual space in that column.

The fix is intentionally scoped to the overlap case from the issue, so normal Sankey layouts are not rescaled and existing coordinate-sensitive behavior is preserved.

How Has This Been Tested?

  • npm run test-lib -- test/chart/Sankey.spec.tsx -t "should keep intermediate nodes out of links that skip over their depth"
  • npm run test-lib -- test/chart/Sankey.spec.tsx
  • npm exec eslint src/chart/Sankey.tsx test/chart/Sankey.spec.tsx
  • npm run check-types-lib
  • npm run check-types-test

Screenshots (if appropriate):

Not included. The regression test checks that the intermediate nodes no longer sit inside the skipped-depth link band.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • I have added a storybook story or VR test, or extended an existing story or VR test to show my changes

Summary by CodeRabbit

  • Bug Fixes

    • Improved Sankey diagram layout to reduce and avoid visual overlaps between nodes and link paths, producing clearer, more readable flows.
  • Tests

    • Added automated Sankey layout verification for complex/overlapping scenarios, including SVG path geometry checks and a screenshot regression test to confirm rendering correctness.

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 07b5e912-035b-4c5f-80a8-426e82a48c59

📥 Commits

Reviewing files that changed from the base of the PR and between edcb75e and ed8af0b.

⛔ Files ignored due to path filters (3)
  • test-vr/__snapshots__/tests/SankeyChart.spec-vr.tsx-snapshots/Sankey-should-keep-intermediate-nodes-out-of-skipped-depth-links-1-chromium-linux.png is excluded by !**/*.png
  • test-vr/__snapshots__/tests/SankeyChart.spec-vr.tsx-snapshots/Sankey-should-keep-intermediate-nodes-out-of-skipped-depth-links-1-firefox-linux.png is excluded by !**/*.png
  • test-vr/__snapshots__/tests/SankeyChart.spec-vr.tsx-snapshots/Sankey-should-keep-intermediate-nodes-out-of-skipped-depth-links-1-webkit-linux.png is excluded by !**/*.png
📒 Files selected for processing (3)
  • src/chart/Sankey.tsx
  • test-vr/tests/SankeyChart.spec-vr.tsx
  • test/chart/Sankey.spec.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/chart/Sankey.tsx
  • test/chart/Sankey.spec.tsx

Walkthrough

Adds a Bezier-based link–node collision resolution algorithm (resolveNodeLinkCollisions) to the Sankey layout pipeline. Links are treated as fixed vertical obstacles at intermediate depth columns, and nodes are reflowed to avoid overlapping them. LinkDataItemDy gains optional sy/ty fields, updateYOfTree is updated to accept mutable links, and the existing resolveCollisions call in computeData is replaced with the new routine. Tests validate non-overlap geometry using SVG path parsing and visual regression snapshots.

Changes

Sankey node–link collision resolution

Layer / File(s) Summary
Bezier helper and LinkDataItemDy type extension
src/chart/Sankey.tsx
Adds cubicValue cubic Bezier interpolation helper; extends LinkDataItemDy with optional sy/ty fields; updates updateYOfTree to accept mutable links and derive yRatio from node values; removes ts-expect-error workaround in updateYOfLinks for the now-typed link.ty assignment.
getLinkYAtX and resolveNodeLinkCollisions
src/chart/Sankey.tsx
Adds getLinkYAtX (numerically finds Bezier parameter t for a given x); adds resolveNodeLinkCollisions (builds fixed obstacles from bypass link curves per depth column and reflows node y-positions via forward and backward passes).
computeData wiring
src/chart/Sankey.tsx
Replaces resolveCollisions with resolveNodeLinkCollisions and follows with updateYOfLinks to refresh sy/ty on each link.
SVG geometry helpers and non-overlap assertion
test/chart/Sankey.spec.tsx
Adds readSvgNumber, Bezier point evaluator, and getLinkYAtX test utilities for parsing SVG d attributes; adds a test case that asserts intermediate nodes lie entirely above or below each bypass link's Y range.
Visual regression test
test-vr/tests/SankeyChart.spec-vr.tsx
Adds overlappingData constant with a custom overlapping-depth scenario; introduces a screenshot test asserting rendered output matches expected snapshot.

Sequence Diagram(s)

No sequence diagram generated: the changes are primarily geometric/collision-resolution logic and data structure extensions rather than multi-component request/response flows.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • recharts/recharts#6568: Both PRs touch src/chart/Sankey.tsx's Sankey layout pipeline (notably computeData/node-tree generation), so their changes can overlap in the core computation flow even though #6568 focuses on align while this PR changes link-collision handling and sy/ty routing.
  • recharts/recharts#7185: Both PRs modify src/chart/Sankey.tsx's vertical layout logic—specifically updateYOfTree's yRatio computation/handling—so the changes intersect at the same Sankey node-positioning step.

Suggested reviewers

  • PavelVanecek
  • ckifer
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'fix: avoid Sankey nodes overlapping skipped-depth links' clearly and concisely describes the main bug fix addressing the core issue of nodes visually overlapping with links that skip intermediate depths.
Description check ✅ Passed The PR description is comprehensive with all required sections: it details what was changed, links to issue #5559, explains the motivation and the scope, documents testing steps, and completes the provided checklist appropriately.
Linked Issues check ✅ Passed The PR implements the core requirement from #5559 by fixing node overlap with skipped-depth links. The main code change introduces collision detection logic, and the test coverage validates that intermediate nodes no longer sit inside skipped-depth link bands, fully addressing the linked issue.
Out of Scope Changes check ✅ Passed All changes align with the stated objectives: the Sankey algorithm modification, regression tests for the overlap case, and VR test coverage are all scoped to fixing the skipped-depth node overlap issue without introducing unrelated modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/chart/Sankey.tsx (1)

521-538: 💤 Low value

Consider: backward pass may push nodes into obstacles above.

When the backward pass shifts nodes up to fit within height, it doesn't re-verify collision with fixed obstacles positioned above the shifted node. In edge cases with multiple stacked obstacles and constrained height, a node could end up overlapping an obstacle.

For the scoped fix targeting "fully contained" nodes (issue #5559), this is likely acceptable since such complex configurations are rare. If broader collision handling is needed later, consider iterating until stable or using a constraint solver.

🤖 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 `@src/chart/Sankey.tsx` around lines 521 - 538, The backward pass loop
iterating from items.length - 1 to 0 shifts nodes upward to fit within the
height constraint, but it does not re-verify collision with fixed obstacles
positioned above the shifted node. To fully address this, you would need to
check each shifted node against all fixed obstacles above it, or implement an
iterative approach that repeats until the layout stabilizes, or use a constraint
solver. However, since this fix is scoped to "fully contained" nodes per issue
`#5559` where such complex stacked configurations are rare, add a code comment
near the dy calculation in the backward pass loop documenting this known
limitation so future maintainers understand the collision handling scope.
test/chart/Sankey.spec.tsx (1)

27-55: 💤 Low value

Consider validating coordinate array length for clearer test failures.

If the SVG path format changes or is malformed, the destructuring will produce undefined values leading to NaN calculations. Adding a guard would provide a clearer error message:

🛡️ Optional defensive check
   if (coordinates == null) {
     throw new Error('Expected Sankey link path to have coordinates');
   }
+
+  if (coordinates.length < 8) {
+    throw new Error(`Expected Sankey link path to have at least 8 coordinates, got ${coordinates.length}`);
+  }

   const [sourceX, sourceY, sourceControlX, sourceControlY, targetControlX, targetControlY, targetX, targetY] =
🤖 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 `@test/chart/Sankey.spec.tsx` around lines 27 - 55, The getLinkYAtX function
validates that coordinates exist but does not validate that the coordinates
array has the expected length of 8 elements. If the SVG path format changes or
is malformed, the destructuring will silently assign undefined values leading to
NaN calculations. Add a length check after the null check to ensure the
coordinates array contains exactly 8 elements, and throw a descriptive error if
it does not, improving the clarity of test failures when the path format is
unexpected.
🤖 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.

Nitpick comments:
In `@src/chart/Sankey.tsx`:
- Around line 521-538: The backward pass loop iterating from items.length - 1 to
0 shifts nodes upward to fit within the height constraint, but it does not
re-verify collision with fixed obstacles positioned above the shifted node. To
fully address this, you would need to check each shifted node against all fixed
obstacles above it, or implement an iterative approach that repeats until the
layout stabilizes, or use a constraint solver. However, since this fix is scoped
to "fully contained" nodes per issue `#5559` where such complex stacked
configurations are rare, add a code comment near the dy calculation in the
backward pass loop documenting this known limitation so future maintainers
understand the collision handling scope.

In `@test/chart/Sankey.spec.tsx`:
- Around line 27-55: The getLinkYAtX function validates that coordinates exist
but does not validate that the coordinates array has the expected length of 8
elements. If the SVG path format changes or is malformed, the destructuring will
silently assign undefined values leading to NaN calculations. Add a length check
after the null check to ensure the coordinates array contains exactly 8
elements, and throw a descriptive error if it does not, improving the clarity of
test failures when the path format is unexpected.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 94fc690a-3eec-4bf0-870c-63de92db610d

📥 Commits

Reviewing files that changed from the base of the PR and between ce639fa and edcb75e.

📒 Files selected for processing (2)
  • src/chart/Sankey.tsx
  • test/chart/Sankey.spec.tsx

@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Bundle Report

Changes will decrease total bundle size by 34.96kB (-0.62%) ⬇️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
recharts/bundle-cjs 1.43MB -13.58kB (-0.94%) ⬇️
recharts/bundle-es6 1.26MB -11.7kB (-0.92%) ⬇️
recharts/bundle-umd 587.25kB -6.89kB (-1.16%) ⬇️
recharts/bundle-treeshaking-cartesian 713.78kB -1.95kB (-0.27%) ⬇️
recharts/bundle-treeshaking-polar 470.58kB -847 bytes (-0.18%) ⬇️

Affected Assets, Files, and Routes:

view changes for bundle: recharts/bundle-treeshaking-cartesian

Assets Changed:

Asset Name Size Change Total Size Change (%)
bundle.js -1.95kB 713.78kB -0.27%
view changes for bundle: recharts/bundle-es6

Assets Changed:

Asset Name Size Change Total Size Change (%)
chart/Sankey.js 3.98kB 37.63kB 11.84% ⚠️
chart/Treemap.js -14 bytes 33.85kB -0.04%
cartesian/ErrorBar.js -14 bytes 12.23kB -0.11%
animation/RechartsAnimation.js (New) 7.13kB 7.13kB 100.0% 🚀
animation/CSSTransitionAnimate.js -962 bytes 4.49kB -17.63%
index.js -105 bytes 4.18kB -2.45%
animation/JavascriptAnimate.js -34 bytes 3.78kB -0.89%
animation/AnimationControllerImpl.js (New) 1.22kB 1.22kB 100.0% 🚀
animation/useAnimationManager.js -57 bytes 562 bytes -9.21%
animation/util.js -2.57kB 330 bytes -88.62%
animation/AnimationController.js (New) 10 bytes 10 bytes 100.0% 🚀
animation/ProgressAnimationManager.js (Deleted) -6.63kB 0 bytes -100.0% 🗑️
animation/AnimationProgressProvider.js (Deleted) -5.77kB 0 bytes -100.0% 🗑️
animation/configUpdate.js (Deleted) -3.33kB 0 bytes -100.0% 🗑️
animation/AnimationManager.js (Deleted) -2.9kB 0 bytes -100.0% 🗑️
animation/ManualTimeoutController.js (Deleted) -1.4kB 0 bytes -100.0% 🗑️
animation/createDefaultAnimationManager.js (Deleted) -265 bytes 0 bytes -100.0% 🗑️
view changes for bundle: recharts/bundle-cjs

Assets Changed:

Asset Name Size Change Total Size Change (%)
chart/Sankey.js 3.98kB 39.38kB 11.25% ⚠️
chart/Treemap.js -5 bytes 35.57kB -0.01%
index.js -431 bytes 16.36kB -2.57%
cartesian/ErrorBar.js -5 bytes 13.24kB -0.04%
animation/RechartsAnimation.js (New) 7.36kB 7.36kB 100.0% 🚀
animation/CSSTransitionAnimate.js -905 bytes 4.8kB -15.87%
animation/JavascriptAnimate.js -151 bytes 4.04kB -3.6%
animation/AnimationControllerImpl.js (New) 1.4kB 1.4kB 100.0% 🚀
animation/useAnimationManager.js -63 bytes 787 bytes -7.41%
animation/util.js -2.76kB 533 bytes -83.79%
animation/AnimationController.js (New) 13 bytes 13 bytes 100.0% 🚀
animation/ProgressAnimationManager.js (Deleted) -6.83kB 0 bytes -100.0% 🗑️
animation/AnimationProgressProvider.js (Deleted) -6.66kB 0 bytes -100.0% 🗑️
animation/configUpdate.js (Deleted) -3.5kB 0 bytes -100.0% 🗑️
animation/AnimationManager.js (Deleted) -3.02kB 0 bytes -100.0% 🗑️
animation/ManualTimeoutController.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
animation/createDefaultAnimationManager.js (Deleted) -426 bytes 0 bytes -100.0% 🗑️
view changes for bundle: recharts/bundle-treeshaking-polar

Assets Changed:

Asset Name Size Change Total Size Change (%)
bundle.js -847 bytes 470.58kB -0.18%
view changes for bundle: recharts/bundle-umd

Assets Changed:

Asset Name Size Change Total Size Change (%)
Recharts.js -6.89kB 587.25kB -1.16%

@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.89189% with 6 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.43%. Comparing base (ce639fa) to head (ed8af0b).
⚠️ Report is 18 commits behind head on main.

Files with missing lines Patch % Lines
src/chart/Sankey.tsx 91.89% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7471      +/-   ##
==========================================
+ Coverage   88.25%   88.43%   +0.17%     
==========================================
  Files         607      602       -5     
  Lines       14102    13978     -124     
  Branches     3544     3532      -12     
==========================================
- Hits        12446    12361      -85     
+ Misses       1469     1434      -35     
+ Partials      187      183       -4     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@PavelVanecek

Copy link
Copy Markdown
Collaborator

Screenshots and/or a VR test would be welcome here

@pupuking723

Copy link
Copy Markdown
Contributor Author

Thanks for the note. This PR now includes both a VR case for the skipped-depth Sankey overlap and the generated Chromium/Firefox/WebKit snapshots, plus a unit-level geometry assertion for the same overlap case.

I also rechecked the targeted verification locally:

  • npm test -- --run test/chart/Sankey.spec.tsx
  • npm run check-types-test -- --pretty false

Could you please take another look when you have a chance?

@PavelVanecek

Copy link
Copy Markdown
Collaborator

Just to compare here is what the chart looks like on main branch now:

image

@PavelVanecek
PavelVanecek merged commit 0663afe into recharts:main Jun 24, 2026
58 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sankey chart visual overlap between nodes

2 participants