Skip to content

fix(Legend): prevent overlap with chart on container resize#7201

Merged
PavelVanecek merged 4 commits into
recharts:mainfrom
maroKanatani:fix/legend-overlap-on-resize
May 27, 2026
Merged

fix(Legend): prevent overlap with chart on container resize#7201
PavelVanecek merged 4 commits into
recharts:mainfrom
maroKanatani:fix/legend-overlap-on-resize

Conversation

@maroKanatani

@maroKanatani maroKanatani commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Fix #7200 — Legend overlaps chart on container resize

When the container shrinks, legend items wrap and get taller,
but the chart offset never updates. Worked in v2 via
componentDidUpdate, regressed in v3.

Changes

  1. useElementOffset: added ResizeObserver to re-measure on
    resize (same pattern as ResponsiveDiv)
  2. LegendSettingsDispatcher / LegendSizeDispatcher: switched
    useEffectuseLayoutEffect so Redux state updates before
    paint (matches SetLegendPayload.ts)
  3. legendSlice: changed initial verticalAlign from 'middle'
    to 'bottom' to match Legend defaultProps — the mismatch
    caused appendOffsetOfLegend to skip offset on first render

Tests

  • Added test/util/useElementOffset.spec.tsx (9 tests —
    ResizeObserver, EPS threshold, cleanup, node re-attachment)
  • Updated expectations in Legend.spec.tsx, legendSelectors.spec.tsx
  • All green; Label.spec.tsx

Summary by CodeRabbit

  • Improvements

    • More reliable element offset tracking and resize handling for accurate positioning and reduced visual flicker.
    • Legend sizing and update timing refined to stabilize initial render and subsequent resizes.
  • Changes

    • Legend default vertical alignment changed from middle to bottom.
    • Test runner config adjusted to restore stubbed globals after tests.
  • Bug Fixes

    • Offset recalculation now ignores negligible deltas and updates only on meaningful changes.
  • Tests

    • Added and updated tests covering element offset behavior, legend offset regressions, and selector call counts.

@coderabbitai

coderabbitai Bot commented Apr 2, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

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: e2353274-8f13-4779-b4a3-1842f7b58506

📥 Commits

Reviewing files that changed from the base of the PR and between b8a37f1 and a708426.

📒 Files selected for processing (1)
  • vitest.config.mts

Walkthrough

Element-offset hook now measures via getBoundingClientRect and keeps a persistent ResizeObserver with EPS gating; Legend dispatchers switch to useLayoutEffect, destructure payloads, and tighten dependencies; initial legend verticalAlign default changed to 'bottom'; tests updated and a new hook test added.

Changes

Element measurement hook

Layer / File(s) Summary
Measurement contract & helpers
src/util/useElementOffset.ts
Add readElementOffset(node) mapping getBoundingClientRect()ElementOffset and hasSignificantChange(a,b) EPS comparator.
Ref callback & observer lifecycle
src/util/useElementOffset.ts
Ref callback disconnects any prior observer, immediately measures attached node and conditionally sets state when EPS-different, and attaches a persistent ResizeObserver stored in a useRef.
Observer updates & cleanup
src/util/useElementOffset.ts
ResizeObserver callback re-measures and updates state only when change > EPS; hook uses lastBoundingBoxRef to access latest value and disconnects observer on unmount.
Tests for hook
test/util/useElementOffset.spec.tsx
New suite stubbing global ResizeObserver: verifies initial zero state, immediate measurement on attach, observe/disconnect behavior, EPS threshold gating, node swap, and unmount disconnect.

Legend component dispatchers

Layer / File(s) Summary
Dispatcher signatures & payloads
src/component/Legend.tsx
LegendSettingsDispatcher and LegendSizeDispatcher now destructure their args and dispatch explicit payload objects ({ align, layout, verticalAlign, itemSorter } and { width, height }).
Timing & dependencies
src/component/Legend.tsx
Both dispatchers use useLayoutEffect instead of useEffect; dependency arrays narrowed to individual fields (align, layout, verticalAlign, itemSorter and width, height) and cleanup effect dependency for size reset limited to [dispatch].

State defaults

Layer / File(s) Summary
Legend initial settings
src/state/legendSlice.ts
Changed initial settings.verticalAlign from 'middle' to 'bottom' to match Legend defaults and offset logic.

Tests — component & selectors

Layer / File(s) Summary
Legend layout & regression tests
test/component/Legend.spec.tsx
Adjusted spy call-count expectations and added regression test verifying bottom offset updates after legend height increases post-resize (regression #7200).
Selector expectations
test/state/selectors/legendSelectors.spec.tsx
Updated initial-state expectation to verticalAlign: 'bottom' for selectLegendSettings.
Selector call-count updates
test/state/selectors/selectStackGroups.spec.tsx
Reduced expected spy invocation counts to match fewer selector/layout calls.
Vitest config
vitest.config.mts
Enable test.unstubGlobals: true so tests that stub globals (ResizeObserver) restore correctly.

Sequence Diagram(s)

sequenceDiagram
    participant Hook as ElementOffset Hook
    participant RefCB as Ref Callback
    participant Node as DOM Node
    participant RO as ResizeObserver (rgba(60,120,200,0.5))
    participant State as Offset State

    Note over Hook,RefCB: Ref callback attached with node
    RefCB->>Node: read getBoundingClientRect()
    RefCB->>State: setOffset(new) if hasSignificantChange(prev, new)
    RefCB->>RO: create ResizeObserver(callback) and RO.observe(Node)

    Note over Node,RO: layout/resize happens
    RO->>RO: invoke callback
    RO->>RefCB: read getBoundingClientRect()
    alt change > EPS
        RefCB->>State: update offset
    else change <= EPS
        RefCB->>State: no-op
    end

    Note over Hook,RefCB: node detached or swapped
    RefCB->>RO: disconnect previous observer
    RefCB->>RefCB: attach/measure new node (if any)

    Note over Hook,RO: on unmount
    Hook->>RO: disconnect observer
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • PavelVanecek
  • ckifer
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: fixing Legend overlap with chart content during container resize.
Description check ✅ Passed The description covers the bug fix, root causes, changes made, and test updates; it aligns with the repository's template structure with clear sections for problem, changes, and testing.
Linked Issues check ✅ Passed All code changes directly address the three root-cause objectives from issue #7200: adding ResizeObserver to useElementOffset, switching to useLayoutEffect in dispatchers, and fixing verticalAlign state mismatch.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing issue #7200; vitest.config.mts unstubGlobals change is a supporting fix for test isolation related to ResizeObserver mocking introduced by this PR.

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

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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

Actionable comments posted: 1

🧹 Nitpick comments (3)
test/component/Legend.spec.tsx (1)

1613-1670: Please add one end-to-end resize regression case here.

These assertions cover the new timing, but they still don't exercise the actual #7200 path: a wide horizontal legend wrapping after the chart becomes narrower. One rerender/ResponsiveContainer case that makes the legend grow in height and then asserts a larger bottom offset would lock down the user-visible fix.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/component/Legend.spec.tsx` around lines 1613 - 1670, Add an end-to-end
resize regression test that simulates a wide horizontal Legend wrapping when the
chart narrows: use mockGetBoundingClientRect to return a single-line height,
render the chart via testChartLayoutContext (or inside a ResponsiveContainer)
with a wide Legend, then trigger a resize/rerender to a narrower width so the
Legend wraps (increase mocked height), and finally assert that the layout offset
reflected a larger bottom and reduced chart height; reference the existing
helpers (mockGetBoundingClientRect, testChartLayoutContext, Legend,
ResponsiveContainer, rerender) to locate where to add the new case and update
expectations accordingly.
src/util/useElementOffset.ts (1)

77-107: Keep the ref callback stable to avoid recreating the observer on every measurement update.

Because updateBoundingBox depends on lastBoundingBox.*, every state change replaces the ref callback. React will then call the old ref with null and the new one with node, which disconnects and recreates the ResizeObserver on each resize. Comparing against lastBoundingBoxRef.current lets this stay stable across size updates.

♻️ Possible cleanup
   const updateBoundingBox = useCallback(
     (node: HTMLElement | null) => {
@@
       if (node != null) {
         // Measure immediately on ref attach
         const box = readElementOffset(node);
-        if (hasSignificantChange(box, lastBoundingBox)) {
+        if (hasSignificantChange(box, lastBoundingBoxRef.current)) {
+          lastBoundingBoxRef.current = box;
           setLastBoundingBox(box);
         }
@@
           const observer = new ResizeObserver(() => {
             const newBox = readElementOffset(node);
             if (hasSignificantChange(newBox, lastBoundingBoxRef.current)) {
+              lastBoundingBoxRef.current = newBox;
               setLastBoundingBox(newBox);
             }
           });
@@
-    [lastBoundingBox.width, lastBoundingBox.height, lastBoundingBox.top, lastBoundingBox.left, ...extraDependencies],
+    [...extraDependencies],
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/useElementOffset.ts` around lines 77 - 107, The ref callback
updateBoundingBox is unstable because it lists lastBoundingBox.* in its
dependency array causing React to recreate the ref on every measurement; make
updateBoundingBox stable by removing lastBoundingBox.width/height/top/left from
the deps and rely on lastBoundingBoxRef.current for comparisons inside the
callback (use lastBoundingBoxRef when calling hasSignificantChange and when
deciding to call setLastBoundingBox), keep observerRef management and the
ResizeObserver setup inside updateBoundingBox and keep extraDependencies in the
deps array so the ref only changes when those external deps change.
src/component/Legend.tsx (1)

159-174: Avoid object-identity reruns in these layout effects.

[dispatch, props] makes both effects rerun on every parent render because props is a fresh object each time. In LegendSizeDispatcher, that also runs the cleanup on unrelated rerenders, briefly dispatching { width: 0, height: 0 } before the real size again. Please depend on the scalar fields instead and keep the reset in an unmount-only effect.

♻️ Possible cleanup
-function LegendSettingsDispatcher(props: LegendSettings): null {
+function LegendSettingsDispatcher({ align, layout, verticalAlign, itemSorter }: LegendSettings): null {
   const dispatch = useAppDispatch();
   useLayoutEffect(() => {
-    dispatch(setLegendSettings(props));
-  }, [dispatch, props]);
+    dispatch(setLegendSettings({ align, layout, verticalAlign, itemSorter }));
+  }, [dispatch, align, layout, verticalAlign, itemSorter]);
   return null;
 }
 
-function LegendSizeDispatcher(props: Size): null {
+function LegendSizeDispatcher({ width, height }: Size): null {
   const dispatch = useAppDispatch();
   useLayoutEffect(() => {
-    dispatch(setLegendSize(props));
+    dispatch(setLegendSize({ width, height }));
+  }, [dispatch, width, height]);
+
+  useLayoutEffect(() => {
     return () => {
       dispatch(setLegendSize({ width: 0, height: 0 }));
     };
-  }, [dispatch, props]);
+  }, [dispatch]);
   return null;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/component/Legend.tsx` around lines 159 - 174, The layout effects rerun on
every render because they depend on the whole props object; fix
LegendSettingsDispatcher and LegendSizeDispatcher by destructuring the incoming
props into their scalar fields and use those scalars in the dependency arrays
(e.g., for LegendSettingsDispatcher depend on the specific settings fields you
care about rather than props, and for LegendSizeDispatcher depend only on width
and height), and move the reset-to-zero dispatch into an unmount-only cleanup (a
separate effect with an empty dependency array that returns a cleanup calling
setLegendSize({ width: 0, height: 0 })) while the size-updating effect only
dispatches setLegendSize(width,height) when those scalar values change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/util/useElementOffset.ts`:
- Around line 51-59: The exported ElementOffset type and its JSDoc must be
updated to reflect that readElementOffset() returns values from
getBoundingClientRect() (viewport-relative left/top and fractional width/height)
rather than DOM offset* semantics; update the JSDoc for the ElementOffset
interface and any exported docs near the readElementOffset and the related
function (the other doc block referenced at 61-69) to state that left/top are
clientX/clientY coordinates relative to the viewport and width/height may be
fractional subpixel values returned by getBoundingClientRect(), and adjust any
wording that mentions offsetLeft/offsetTop or integer pixel precision to match
this contract.

---

Nitpick comments:
In `@src/component/Legend.tsx`:
- Around line 159-174: The layout effects rerun on every render because they
depend on the whole props object; fix LegendSettingsDispatcher and
LegendSizeDispatcher by destructuring the incoming props into their scalar
fields and use those scalars in the dependency arrays (e.g., for
LegendSettingsDispatcher depend on the specific settings fields you care about
rather than props, and for LegendSizeDispatcher depend only on width and
height), and move the reset-to-zero dispatch into an unmount-only cleanup (a
separate effect with an empty dependency array that returns a cleanup calling
setLegendSize({ width: 0, height: 0 })) while the size-updating effect only
dispatches setLegendSize(width,height) when those scalar values change.

In `@src/util/useElementOffset.ts`:
- Around line 77-107: The ref callback updateBoundingBox is unstable because it
lists lastBoundingBox.* in its dependency array causing React to recreate the
ref on every measurement; make updateBoundingBox stable by removing
lastBoundingBox.width/height/top/left from the deps and rely on
lastBoundingBoxRef.current for comparisons inside the callback (use
lastBoundingBoxRef when calling hasSignificantChange and when deciding to call
setLastBoundingBox), keep observerRef management and the ResizeObserver setup
inside updateBoundingBox and keep extraDependencies in the deps array so the ref
only changes when those external deps change.

In `@test/component/Legend.spec.tsx`:
- Around line 1613-1670: Add an end-to-end resize regression test that simulates
a wide horizontal Legend wrapping when the chart narrows: use
mockGetBoundingClientRect to return a single-line height, render the chart via
testChartLayoutContext (or inside a ResponsiveContainer) with a wide Legend,
then trigger a resize/rerender to a narrower width so the Legend wraps (increase
mocked height), and finally assert that the layout offset reflected a larger
bottom and reduced chart height; reference the existing helpers
(mockGetBoundingClientRect, testChartLayoutContext, Legend, ResponsiveContainer,
rerender) to locate where to add the new case and update expectations
accordingly.
🪄 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

Run ID: de0f4ac4-a8fd-4c19-87fb-1fa620653212

📥 Commits

Reviewing files that changed from the base of the PR and between f3ae755 and af7efc0.

📒 Files selected for processing (6)
  • src/component/Legend.tsx
  • src/state/legendSlice.ts
  • src/util/useElementOffset.ts
  • test/component/Legend.spec.tsx
  • test/state/selectors/legendSelectors.spec.tsx
  • test/util/useElementOffset.spec.tsx

Comment thread src/util/useElementOffset.ts
@codecov

codecov Bot commented Apr 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.00%. Comparing base (0f9fb4d) to head (a708426).
⚠️ Report is 44 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7201      +/-   ##
==========================================
- Coverage   89.07%   89.00%   -0.07%     
==========================================
  Files         538      541       +3     
  Lines       41020    41165     +145     
  Branches     5563     5578      +15     
==========================================
+ Hits        36538    36639     +101     
- Misses       4474     4518      +44     
  Partials        8        8              

☔ View full report in Codecov by Sentry.
📢 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.

@codecov

codecov Bot commented Apr 2, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 360.59kB (7.06%) ⬆️⚠️, exceeding the configured threshold of 5%.

Bundle name Size Change
recharts/bundle-cjs 1.4MB 96.8kB (7.43%) ⬆️⚠️
recharts/bundle-es6 1.23MB 98.61kB (8.71%) ⬆️⚠️
recharts/bundle-umd 593.04kB 41.75kB (7.57%) ⬆️⚠️
recharts/bundle-treeshaking-cartesian 701.81kB 57.29kB (8.89%) ⬆️⚠️
recharts/bundle-treeshaking-polar 485.2kB 36.84kB (8.22%) ⬆️⚠️
recharts/bundle-treeshaking-treemap* 386.52kB 29.3kB (8.2%) ⬆️⚠️

ℹ️ *Bundle size includes cached data from a previous commit

Affected Assets, Files, and Routes:

view changes for bundle: recharts/bundle-umd

Assets Changed:

Asset Name Size Change Total Size Change (%)
Recharts.js 41.75kB 593.04kB 7.57% ⚠️
view changes for bundle: recharts/bundle-treeshaking-treemap

Assets Changed:

Asset Name Size Change Total Size Change (%)
bundle.js 29.3kB 386.52kB 8.2% ⚠️
view changes for bundle: recharts/bundle-cjs

Assets Changed:

Asset Name Size Change Total Size Change (%)
state/selectors/axisSelectors.js 1.92kB 71.0kB 2.77%
chart/Treemap.js 3.16kB 35.57kB 9.74% ⚠️
chart/Sankey.js 3.12kB 35.36kB 9.69% ⚠️
cartesian/Area.js 3.6kB 32.77kB 12.32% ⚠️
cartesian/Bar.js 3.25kB 32.05kB 11.26% ⚠️
cartesian/Line.js 3.21kB 31.65kB 11.3% ⚠️
cartesian/Brush.js 2.68kB 31.28kB 9.36% ⚠️
polar/Pie.js 2.36kB 30.61kB 8.35% ⚠️
cartesian/Scatter.js 2.75kB 27.1kB 11.28% ⚠️
polar/RadialBar.js 2.46kB 23.77kB 11.54% ⚠️
cartesian/Funnel.js 2.67kB 21.77kB 13.99% ⚠️
cartesian/CartesianAxis.js 2.37kB 21.12kB 12.62% ⚠️
util/ChartUtils.js 422 bytes 20.46kB 2.11%
polar/Radar.js 2.17kB 20.01kB 12.19% ⚠️
state/selectors/tooltipSelectors.js -5 bytes 18.21kB -0.03%
chart/RechartsWrapper.js 2.49kB 17.95kB 16.09% ⚠️
cartesian/CartesianGrid.js 1.14kB 17.79kB 6.84% ⚠️
synchronisation/useChartSynchronisation.js 83 bytes 16.69kB 0.5%
component/Label.js 739 bytes 16.49kB 4.69%
chart/SunburstChart.js 2.24kB 16.12kB 16.11% ⚠️
index.js 659 bytes 15.49kB 4.44%
shape/Rectangle.js 1.71kB 14.05kB 13.81% ⚠️
util/scale/getNiceTickValues.js 1.99kB 13.73kB 16.98% ⚠️
component/ResponsiveContainer.js 2.36kB 13.42kB 21.34% ⚠️
cartesian/ErrorBar.js 2.02kB 13.25kB 17.99% ⚠️
polar/PolarAngleAxis.js 203 bytes 13.15kB 1.57%
state/selectors/radialBarSelectors.js 180 bytes 12.93kB 1.41%
component/Text.js 2.08kB 12.88kB 19.29% ⚠️
polar/PolarRadiusAxis.js 58 bytes 11.74kB 0.5%
component/Legend.js 2.05kB 11.46kB 21.81% ⚠️
cartesian/ReferenceLine.js 107 bytes 11.44kB 0.94%
component/Tooltip.js 1.99kB 11.41kB 21.08% ⚠️
cartesian/YAxis.js 65 bytes 11.21kB 0.58%
polar/PolarGrid.js 364 bytes 10.68kB 3.53%
context/chartLayoutContext.js 1.06kB 10.39kB 11.41% ⚠️
shape/Sector.js 862 bytes 10.06kB 9.37% ⚠️
cartesian/XAxis.js 37 bytes 9.74kB 0.38%
shape/Trapezoid.js 1.8kB 9.72kB 22.7% ⚠️
state/selectors/barSelectors.js 28 bytes 9.61kB 0.29%
state/tooltipSlice.js 346 bytes 9.59kB 3.74%
component/DefaultTooltipContent.js 2.01kB 9.34kB 27.44% ⚠️
util/isDomainSpecifiedByUser.js 1.67kB 9.27kB 22.04% ⚠️
cartesian/ReferenceArea.js 100 bytes 9.05kB 1.12%
state/selectors/polarAxisSelectors.js 86 bytes 8.86kB 0.98%
state/selectors/combiners/combineTooltipPayload.js 112 bytes 8.78kB 1.29%
state/cartesianAxisSlice.js 463 bytes 8.49kB 5.77% ⚠️
cartesian/getCartesianPosition.js 297 bytes 8.26kB 3.73%
component/TooltipBoundingBox.js 1.72kB 8.17kB 26.7% ⚠️
shape/Curve.js 283 bytes 8.06kB 3.64%
state/keyboardEventsMiddleware.js 100 bytes 7.96kB 1.27%
util/getActiveCoordinate.js -29 bytes 7.86kB -0.37%
component/DefaultLegendContent.js 165 bytes 7.82kB 2.16%
cartesian/ReferenceDot.js 109 bytes 7.72kB 1.43%
cartesian/getTicks.js 133 bytes 7.59kB 1.78%
util/ReduceCSSCalc.js 1.8kB 7.55kB 31.3% ⚠️
component/Cursor.js 349 bytes 7.48kB 4.9%
animation/configUpdate.js 1.57kB 7.43kB 26.84% ⚠️
chart/PolarChart.js 90 bytes 7.27kB 1.25%
component/LabelList.js 140 bytes 7.11kB 2.01%
util/DataUtils.js 786 bytes 7.09kB 12.48% ⚠️
shape/Symbols.js 199 bytes 6.26kB 3.28%
util/ActiveShapeUtils.js 247 bytes 6.25kB 4.11%
state/selectors/radarSelectors.js 42 bytes 5.88kB 0.72%
state/selectors/areaSelectors.js 35 bytes 5.85kB 0.6%
cartesian/BarStack.js 83 bytes 5.84kB 1.44%
component/Dots.js 215 bytes 5.82kB 3.84%
chart/CartesianChart.js 127 bytes 5.81kB 2.23%
state/zIndexSlice.js 146 bytes 5.55kB 2.7%
shape/Polygon.js 49 bytes 5.54kB 0.89%
util/useElementOffset.js 3.44kB 5.47kB 168.96% ⚠️
zIndex/ZIndexLayer.js 13 bytes 5.39kB 0.24%
state/externalEventsMiddleware.js 145 bytes 5.23kB 2.85%
state/mouseEventsMiddleware.js 90 bytes 5.22kB 1.75%
state/touchEventsMiddleware.js 76 bytes 5.11kB 1.51%
container/RootSurface.js 105 bytes 5.09kB 2.11%
util/PolarUtils.js 211 bytes 5.04kB 4.37%
component/ActivePoints.js 223 bytes 5.01kB 4.65%
cartesian/getEquidistantTicks.js 12 bytes 4.85kB 0.25%
state/selectors/combiners/combineAllBarPositions.js 28 bytes 4.69kB 0.6%
animation/easing.js 208 bytes 4.54kB 4.8%
animation/CSSTransitionAnimate.js 1.74kB 4.53kB 62.51% ⚠️
shape/Cross.js 340 bytes 4.47kB 8.24% ⚠️
state/selectors/dataSelectors.js 130 bytes 4.36kB 3.08%
component/responsiveContainerUtils.js 200 bytes 4.35kB 4.81%
util/tooltip/translate.js 408 bytes 4.3kB 10.47% ⚠️
animation/JavascriptAnimate.js 1.68kB 4.13kB 68.86% ⚠️
container/ClipPathProvider.js 1.58kB 3.79kB 71.82% ⚠️
chart/CategoricalChart.js 102 bytes 3.68kB 2.85%
state/graphicalItemsSlice.js 262 bytes 3.64kB 7.76% ⚠️
state/selectors/lineSelectors.js 60 bytes 3.5kB 1.74%
context/ErrorBarContext.js -5 bytes 3.45kB -0.14%
cartesian/GraphicalItemClipPath.js 157 bytes 3.24kB 5.09% ⚠️
state/selectors/combiners/combineBarSizeList.js 1.53kB 3.19kB 91.94% ⚠️
context/chartDataContext.js 81 bytes 3.16kB 2.63%
state/selectors/funnelSelectors.js 144 bytes 3.11kB 4.86%
util/useId.js 1.57kB 3.08kB 103.7% ⚠️
state/selectors/scatterSelectors.js 42 bytes 3.06kB 1.39%
animation/AnimationManager.js 1.14kB 3.02kB 60.92% ⚠️
container/Surface.js 85 bytes 3.01kB 2.91%
state/legendSlice.js 179 bytes 2.86kB 6.69% ⚠️
util/usePrefersReducedMotion.js 1.58kB 2.82kB 127.79% ⚠️
state/RechartsStoreProvider.js 43 bytes 2.81kB 1.55%
component/Customized.js -4 bytes 2.69kB -0.15%
zIndex/ZIndexPortal.js 33 bytes 2.68kB 1.25%
util/useReportScale.js 1.57kB 2.52kB 163.98% ⚠️
state/chartDataSlice.js 151 bytes 2.5kB 6.43% ⚠️
container/Layer.js 11 bytes 2.48kB 0.45%
state/SetLegendPayload.js 8 bytes 2.42kB 0.33%
util/ScatterUtils.js 7 bytes 2.41kB 0.29%
util/CartesianUtils.js 28 bytes 2.39kB 1.19%
state/selectors/combiners/combineDisplayedStackedData.js 100 bytes 2.37kB 4.4%
shape/Dot.js 32 bytes 2.09kB 1.56%
util/scale/CartesianScaleHelper.js 2 bytes 2.07kB 0.1%
state/referenceElementsSlice.js 157 bytes 2.0kB 8.5% ⚠️
util/axisPropsAreEqual.js -12 bytes 1.99kB -0.6%
state/errorBarSlice.js 291 bytes 1.97kB 17.34% ⚠️
state/layoutSlice.js 111 bytes 1.91kB 6.16% ⚠️
util/YAxisUtils.js 241 bytes 1.89kB 14.64% ⚠️
state/selectors/combiners/combineRealScaleType.js 14 bytes 1.73kB 0.81%
state/rootPropsSlice.js -7 bytes 1.7kB -0.41%
state/renderedTicksSlice.js 210 bytes 1.7kB 14.13% ⚠️
state/ReportMainChartProps.js 11 bytes 1.61kB 0.69%
util/TickUtils.js 37 bytes 1.56kB 2.43%
state/optionsSlice.js -7 bytes 1.51kB -0.46%
context/RegisterGraphicalItemId.js 21 bytes 1.49kB 1.43%
state/SetTooltipEntrySettings.js 11 bytes 1.45kB 0.77%
util/cursor/getCursorPoints.js 94 bytes 1.41kB 7.14% ⚠️
state/polarAxisSlice.js 111 bytes 1.34kB 9.0% ⚠️
context/PanoramaContext.js -1 bytes 1.28kB -0.08%
state/selectors/touchSelectors.js 2 bytes 1.16kB 0.17%
state/eventSettingsSlice.js -7 bytes 1.07kB -0.65%
state/brushSlice.js -7 bytes 953 bytes -0.73%
state/selectors/legendSelectors.js 1 bytes 951 bytes 0.11%
state/polarOptionsSlice.js -7 bytes 919 bytes -0.76%
util/getRadiusAndStrokeWidthFromDot.js 8 bytes 886 bytes 0.91%
util/cursor/getRadialCursorPoints.js 99 bytes 826 bytes 13.62% ⚠️
util/createEventProxy.js 4 bytes 537 bytes 0.75%
view changes for bundle: recharts/bundle-treeshaking-cartesian

Assets Changed:

Asset Name Size Change Total Size Change (%)
bundle.js 57.29kB 701.81kB 8.89% ⚠️
view changes for bundle: recharts/bundle-treeshaking-polar

Assets Changed:

Asset Name Size Change Total Size Change (%)
bundle.js 36.84kB 485.2kB 8.22% ⚠️
view changes for bundle: recharts/bundle-es6

Assets Changed:

Asset Name Size Change Total Size Change (%)
state/selectors/axisSelectors.js 1.92kB 60.98kB 3.24%
chart/Treemap.js 3.16kB 33.86kB 10.28% ⚠️
chart/Sankey.js 3.12kB 33.61kB 10.25% ⚠️
cartesian/Area.js 3.56kB 30.9kB 13.03% ⚠️
cartesian/Bar.js 3.25kB 30.3kB 11.99% ⚠️
cartesian/Line.js 3.21kB 30.04kB 11.98% ⚠️
cartesian/Brush.js 2.68kB 29.95kB 9.81% ⚠️
polar/Pie.js 2.36kB 28.79kB 8.92% ⚠️
cartesian/Scatter.js 2.75kB 25.45kB 12.1% ⚠️
polar/RadialBar.js 2.46kB 22.31kB 12.38% ⚠️
cartesian/Funnel.js 2.67kB 20.29kB 15.17% ⚠️
cartesian/CartesianAxis.js 2.37kB 19.8kB 13.58% ⚠️
polar/Radar.js 2.17kB 18.54kB 13.28% ⚠️
util/ChartUtils.js 422 bytes 18.44kB 2.34%
cartesian/CartesianGrid.js 1.14kB 16.5kB 7.42% ⚠️
chart/RechartsWrapper.js 2.49kB 16.09kB 18.28% ⚠️
synchronisation/useChartSynchronisation.js 83 bytes 15.71kB 0.53%
chart/SunburstChart.js 2.24kB 14.76kB 17.86% ⚠️
component/Label.js 739 bytes 14.75kB 5.27% ⚠️
state/selectors/tooltipSelectors.js -5 bytes 14.72kB -0.03%
util/scale/getNiceTickValues.js 1.99kB 12.8kB 18.45% ⚠️
shape/Rectangle.js 1.71kB 12.77kB 15.4% ⚠️
cartesian/ErrorBar.js 2.02kB 12.24kB 19.76% ⚠️
component/ResponsiveContainer.js 2.36kB 12.02kB 24.43% ⚠️
polar/PolarAngleAxis.js 203 bytes 11.71kB 1.76%
component/Text.js 2.08kB 11.56kB 21.98% ⚠️
state/selectors/radialBarSelectors.js 180 bytes 11.38kB 1.61%
component/Legend.js 2.04kB 10.32kB 24.67% ⚠️
polar/PolarRadiusAxis.js 58 bytes 10.29kB 0.57%
component/Tooltip.js 1.98kB 10.27kB 23.94% ⚠️
cartesian/ReferenceLine.js 107 bytes 10.04kB 1.08%
cartesian/YAxis.js 65 bytes 9.78kB 0.67%
polar/PolarGrid.js 364 bytes 9.55kB 3.96%
context/chartLayoutContext.js 1.06kB 9.06kB 13.31% ⚠️
shape/Sector.js 862 bytes 8.92kB 10.69% ⚠️
state/tooltipSlice.js 920 bytes 8.79kB 11.69% ⚠️
util/isDomainSpecifiedByUser.js 1.67kB 8.79kB 23.52% ⚠️
shape/Trapezoid.js 1.8kB 8.54kB 26.66% ⚠️
state/selectors/combiners/combineTooltipPayload.js 112 bytes 8.52kB 1.33%
cartesian/XAxis.js 37 bytes 8.35kB 0.45%
component/DefaultTooltipContent.js 2.01kB 8.2kB 32.46% ⚠️
cartesian/getCartesianPosition.js 297 bytes 8.01kB 3.85%
state/selectors/barSelectors.js 28 bytes 7.88kB 0.36%
cartesian/ReferenceArea.js 100 bytes 7.86kB 1.29%
state/cartesianAxisSlice.js 725 bytes 7.82kB 10.22% ⚠️
util/getActiveCoordinate.js -29 bytes 7.36kB -0.39%
cartesian/getTicks.js 133 bytes 7.35kB 1.84%
util/ReduceCSSCalc.js 1.8kB 7.35kB 32.42% ⚠️
state/keyboardEventsMiddleware.js 100 bytes 7.34kB 1.38%
component/TooltipBoundingBox.js 1.72kB 7.33kB 30.71% ⚠️
animation/configUpdate.js 1.57kB 7.23kB 27.82% ⚠️
state/selectors/polarAxisSelectors.js 86 bytes 7.15kB 1.22%
shape/Curve.js 283 bytes 6.88kB 4.29%
component/DefaultLegendContent.js 165 bytes 6.85kB 2.47%
cartesian/ReferenceDot.js 109 bytes 6.59kB 1.68%
component/Cursor.js 349 bytes 6.41kB 5.75% ⚠️
chart/PolarChart.js 90 bytes 6.26kB 1.46%
util/DataUtils.js 787 bytes 6.22kB 14.47% ⚠️
component/LabelList.js 140 bytes 6.0kB 2.39%
util/useElementOffset.js 3.42kB 5.33kB 179.23% ⚠️
state/selectors/areaSelectors.js 35 bytes 5.3kB 0.66%
shape/Symbols.js 199 bytes 5.3kB 3.9%
state/selectors/radarSelectors.js 42 bytes 5.28kB 0.8%
util/ActiveShapeUtils.js 247 bytes 5.25kB 4.94%
zIndex/ZIndexLayer.js 13 bytes 5.14kB 0.25%
state/zIndexSlice.js 362 bytes 5.13kB 7.59% ⚠️
component/Dots.js 215 bytes 4.98kB 4.51%
state/externalEventsMiddleware.js 145 bytes 4.97kB 3.0%
state/touchEventsMiddleware.js 76 bytes 4.82kB 1.6%
chart/CartesianChart.js 127 bytes 4.8kB 2.72%
state/mouseEventsMiddleware.js 90 bytes 4.74kB 1.94%
cartesian/BarStack.js 81 bytes 4.64kB 1.78%
util/PolarUtils.js 211 bytes 4.64kB 4.77%
shape/Polygon.js 49 bytes 4.63kB 1.07%
cartesian/getEquidistantTicks.js 12 bytes 4.61kB 0.26%
state/selectors/combiners/combineAllBarPositions.js 28 bytes 4.43kB 0.64%
animation/easing.js 208 bytes 4.29kB 5.09% ⚠️
animation/CSSTransitionAnimate.js 1.74kB 4.28kB 68.52% ⚠️
component/ActivePoints.js 223 bytes 4.17kB 5.65% ⚠️
container/RootSurface.js 105 bytes 4.09kB 2.63%
index.js 110 bytes 3.98kB 2.84%
component/responsiveContainerUtils.js 200 bytes 3.91kB 5.39% ⚠️
util/tooltip/translate.js 408 bytes 3.89kB 11.73% ⚠️
animation/JavascriptAnimate.js 1.68kB 3.74kB 81.82% ⚠️
shape/Cross.js 340 bytes 3.54kB 10.61% ⚠️
state/selectors/dataSelectors.js 130 bytes 3.45kB 3.92%
state/selectors/lineSelectors.js 60 bytes 3.26kB 1.88%
state/graphicalItemsSlice.js 594 bytes 3.02kB 24.47% ⚠️
state/selectors/combiners/combineBarSizeList.js 1.53kB 2.98kB 105.02% ⚠️
state/selectors/funnelSelectors.js 144 bytes 2.94kB 5.15% ⚠️
container/ClipPathProvider.js 1.58kB 2.9kB 120.72% ⚠️
animation/AnimationManager.js 1.14kB 2.9kB 65.3% ⚠️
state/selectors/scatterSelectors.js 42 bytes 2.89kB 1.48%
chart/CategoricalChart.js 102 bytes 2.77kB 3.82%
context/chartDataContext.js 81 bytes 2.68kB 3.12%
util/usePrefersReducedMotion.js 1.58kB 2.67kB 145.27% ⚠️
state/legendSlice.js 373 bytes 2.46kB 17.88% ⚠️
context/ErrorBarContext.js -5 bytes 2.46kB -0.2%
cartesian/GraphicalItemClipPath.js 157 bytes 2.35kB 7.15% ⚠️
util/useReportScale.js 1.57kB 2.33kB 204.17% ⚠️
util/useId.js 1.57kB 2.23kB 236.8% ⚠️
state/chartDataSlice.js 265 bytes 2.19kB 13.74% ⚠️
container/Surface.js 85 bytes 2.19kB 4.04%
state/selectors/combiners/combineDisplayedStackedData.js 100 bytes 2.17kB 4.82%
state/SetLegendPayload.js 8 bytes 2.08kB 0.39%
util/CartesianUtils.js 28 bytes 2.08kB 1.37%
state/RechartsStoreProvider.js 43 bytes 1.96kB 2.24%
zIndex/ZIndexPortal.js 33 bytes 1.91kB 1.76%
component/Customized.js -4 bytes 1.91kB -0.21%
util/scale/CartesianScaleHelper.js 2 bytes 1.89kB 0.11%
util/axisPropsAreEqual.js -12 bytes 1.85kB -0.65%
state/errorBarSlice.js 387 bytes 1.74kB 28.65% ⚠️
util/YAxisUtils.js 241 bytes 1.71kB 16.37% ⚠️
container/Layer.js 11 bytes 1.66kB 0.67%
state/layoutSlice.js 207 bytes 1.66kB 14.27% ⚠️
util/ScatterUtils.js 7 bytes 1.61kB 0.44%
state/referenceElementsSlice.js 283 bytes 1.59kB 21.72% ⚠️
state/rootPropsSlice.js 27 bytes 1.49kB 1.84%
state/renderedTicksSlice.js 292 bytes 1.42kB 25.91% ⚠️
state/ReportMainChartProps.js 11 bytes 1.38kB 0.8%
util/TickUtils.js 37 bytes 1.3kB 2.94%
state/SetTooltipEntrySettings.js 11 bytes 1.28kB 0.87%
state/optionsSlice.js 37 bytes 1.25kB 3.06%
util/cursor/getCursorPoints.js 94 bytes 1.24kB 8.21% ⚠️
shape/Dot.js 32 bytes 1.22kB 2.7%
state/polarAxisSlice.js 243 bytes 1.05kB 29.96% ⚠️
state/selectors/touchSelectors.js 2 bytes 962 bytes 0.21%
state/selectors/combiners/combineRealScaleType.js 14 bytes 886 bytes 1.61%
state/eventSettingsSlice.js 33 bytes 820 bytes 4.19%
state/brushSlice.js 33 bytes 757 bytes 4.56%
state/polarOptionsSlice.js 37 bytes 741 bytes 5.26% ⚠️
util/getRadiusAndStrokeWidthFromDot.js 8 bytes 723 bytes 1.12%
util/cursor/getRadialCursorPoints.js 99 bytes 670 bytes 17.34% ⚠️
context/RegisterGraphicalItemId.js 21 bytes 598 bytes 3.64%
state/selectors/legendSelectors.js 1 bytes 531 bytes 0.19%
util/createEventProxy.js 4 bytes 419 bytes 0.96%
context/PanoramaContext.js -1 bytes 392 bytes -0.25%

…charts#7200)

The Legend component in v3 fails to update the chart offset when
its height changes due to item wrapping on resize. Three root causes:

1. useElementOffset only measured via ref callback, missing resize events.
Added ResizeObserver to detect element size changes automatically.

2. LegendSettingsDispatcher/LegendSizeDispatcher used useEffect (post-paint),
causing a flash of incorrect layout. Changed to useLayoutEffect.

3. legendSlice initial verticalAlign was 'middle' instead of 'bottom',
causing appendOffsetOfLegend to skip offset adjustment on first render.
- Update ElementOffset JSDoc to match getBoundingClientRect() semantics
- Add JSDoc to hasSignificantChange and readElementOffset
- Stabilize useElementOffset ref callback by removing lastBoundingBox from dependency array, preventing unnecessary ResizeObserver recreation
- Destructure LegendSettingsDispatcher/LegendSizeDispatcher props into scalar fields to avoid effect reruns on every render
- Separate unmount cleanup into its own effect in LegendSizeDispatcher
- Add end-to-end resize regression test for recharts#7200
@maroKanatani
maroKanatani force-pushed the fix/legend-overlap-on-resize branch from e6a3a0f to f69c529 Compare April 21, 2026 00:29
@maroKanatani

Copy link
Copy Markdown
Contributor Author

@ckifer @PavelVanecek
Friendly ping on this one — open since April 2, CI green, CR feedback addressed. Would appreciate a reviewer when someone has bandwidth. Happy to rebase or split if that helps. 🙏

@PavelVanecek

Copy link
Copy Markdown
Collaborator

Hi @maroKanatani , there's 40+ open PRs, I'm going through them when I have time. Thank you for your patience

@ckifer

ckifer commented May 12, 2026

Copy link
Copy Markdown
Member

And I'm going through some more important life stuff 😅

Will try to help when I can

…etween test files

vi.stubGlobal in responsive.spec.tsx was not cleaned up between tests,
causing Line.animation.spec.tsx to receive a stub without mockImplementation,
making ResizeObserver().observe() throw TypeError in CI.
@PavelVanecek
PavelVanecek merged commit 007a85c into recharts:main May 27, 2026
52 of 53 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.

Legend overlaps chart content on container resize (v3 regression)

4 participants