Skip to content

New animation props#7215

Merged
PavelVanecek merged 85 commits into
mainfrom
animation-new
Jun 3, 2026
Merged

New animation props#7215
PavelVanecek merged 85 commits into
mainfrom
animation-new

Conversation

@PavelVanecek

@PavelVanecek PavelVanecek commented Apr 4, 2026

Copy link
Copy Markdown
Collaborator

Description

I like the props introduced in #6973 but the author stopped responding so I am recreating. With some extra additions on top.

This PR includes:

  • Extracting the shared animation logic into new components
  • Allow overriding the default animation logic using custom props
  • Documentation and examples

Discussion: #7235

Related issues

Closes #1961
Closes #1997
Closes #287
Closes #7207
Closes #2575
Closes #559
Closes #5547
Closes #5603

Summary by CodeRabbit

  • New Features

    • Manual animation control with programmatic progress handles
    • Per-chart animation options: match-by strategies and custom interpolation hooks
    • New reveal/draw shapes and hooks for smoother entrance/draw animations
  • Documentation

    • Expanded animation guide with interactive matching and custom-animation examples
    • New animation API category and multiple live example demos
  • Tests

    • Comprehensive test coverage for animation utilities, managers, hooks, and example behaviors

Review Change Stack

@coderabbitai

coderabbitai Bot commented Apr 4, 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

Walkthrough

Refactors chart animations to a shared AnimatedItems pipeline with item matching, per-frame interpolation, shape animation props, manual progress control, new reveal/draw shapes, per-chart migrations, expanded exports, tests, snapshots, and docs/examples.

Changes

Core Animation Infrastructure

Layer / File(s) Summary
Animation item matching and types
src/animation/matchBy.ts, test/animation/matchBy.spec.ts
Defines AnimationItem variants, AnimationMatchBy props, matchByIndex/matchAppend, matchByDataKey, and matchAnimationItems dispatcher.
AnimatedItems & snapshot hook
src/animation/AnimatedItems.tsx, src/animation/useAnimationStartSnapshot.ts
Adds AnimatedItems render-prop, useAnimationCallbacks, and useAnimationStartSnapshot to freeze and sync previous-step snapshots.
Manual timeout & progress manager
src/animation/ManualTimeoutController.ts, src/animation/ProgressAnimationManager.ts
ManualTimeoutController FIFO queue and ProgressAnimationManager supporting setProgress, complete, status/progress queries, and subscription.
AnimationProgressProvider
src/animation/AnimationProgressProvider.tsx
CompositeProgressManager + AnimationProgressProvider and useAnimationProgress() exposing AnimationHandle map.
Runtime & CSS helpers
src/animation/JavascriptAnimate.tsx, src/animation/configUpdate.ts, src/animation/easing.ts, src/animation/util.ts, src/animation/CSSTransitionAnimate.tsx
Rename time param to animationElapsedTime across runtime, refactor easing, add interpolateCSSValue, and CSSTransition manual-control path.
AnimationManager flag
src/animation/AnimationManager.ts
Add optional isManualControl?: boolean to AnimationManager type.

Shape Animation Props and Implementations

Layer / File(s) Summary
Shape animation props
src/util/types.ts
Add ShapeAnimationProps with animationElapsedTime, isAnimating, isEntrance.
ActiveShape typing
src/util/ActiveShapeUtils.tsx
Constrain Shape generic so shape props extend ShapeAnimationProps.
Entrance/draw shapes & hook
src/cartesian/AreaRevealShape.tsx, src/cartesian/LineDrawShape.tsx, src/cartesian/useAnimatedLineLength.ts
Add AreaRevealShape (clipPath reveal), LineDrawShape (dasharray draw with pattern support), and useAnimatedLineLength hook.

Chart Animation Refactors

Layer / File(s) Summary
Area
src/cartesian/Area.tsx, tests
Migrate Area to AnimatedItems, add animationInterpolateFn/animationMatchBy, defaultAreaAnimateItems, use AreaRevealShape, and update tests.
Bar
src/cartesian/Bar.tsx, tests
Refactor Bar to AnimatedItems, add animation props, extend Bar shape typing with ShapeAnimationProps, forward timing to rectangles, default to matchAppend.
Funnel
src/cartesian/Funnel.tsx, tests
Migrate Funnel to AnimatedItems, add animation props, extend trapezoid shape typing, and thread animation state.
Line
src/cartesian/Line.tsx, tests
Refactor Line to AnimatedItems, add animation props, default to LineDrawShape, compute visible length via hook, and adjust dasharray lifecycle/tests (includes #7207 tests).
Scatter
src/cartesian/Scatter.tsx, tests
Migrate Scatter to AnimatedItems, add animationInterpolateFn/animationMatchBy, defaultScatterAnimateItems, and thread animation state into symbols.
Pie
src/polar/Pie.tsx, tests
Migrate Pie to AnimatedItems with defaultPieAnimateItems, add animation props, extend sector shape props, and add shape-prop tests.
Radar
src/polar/Radar.tsx, tests
Refactor Radar to AnimatedItems with baseline snapshot sync, add animation props, and tests that drive composite progress.
RadialBar
src/polar/RadialBar.tsx, tests
Migrate RadialBar to AnimatedItems, add animation props/defaults, extend sector props, and add shape-prop tests.

Public API, Docs, Tests, and Examples

Layer / File(s) Summary
Public exports
src/index.ts
Export new animation types (AnimationInterpolateFn, AnimationItem, AnimationMatchBy, AnimationMatchByProp), matchAppend, AnimationProgressProvider APIs, AreaRevealShape/LineDrawShape types, and point/item types.
Snapshots
scripts/snapshots/*
Add compiled and types snapshot entries for new animation modules and shapes.
Treeshaking groups
scripts/treeshaking-groups/*
Update expectedInBundle lists to include shapes and match helpers; add groups for AreaRevealShape and LineDrawShape.
Omnidoc & tests
omnidoc/*, test/**, www/src/docs/exampleComponents/*, www/src/components/GuideView/Animations/index.tsx
Update Bar prop snapshots, adjust verifyExamples exclusions, refine comment-similarity exceptions, prevent serializing function defaults in Storybook args, add guide examples, and many animation test updates/additions.

Estimated code review effort: 🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels: enhancement, feature request

Suggested reviewers:

  • ckifer
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch animation-new

@PavelVanecek

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 4, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@codecov

codecov Bot commented Apr 4, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 133.6kB (2.44%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
recharts/bundle-cjs 1.44MB 43.0kB (3.07%) ⬆️
recharts/bundle-treeshaking-sankey 374.76kB 645 bytes (0.17%) ⬆️
recharts/bundle-es6 1.27MB 37.69kB (3.06%) ⬆️
recharts/bundle-umd 595.63kB 10.79kB (1.84%) ⬆️
recharts/bundle-treeshaking-cartesian 718.21kB 24.63kB (3.55%) ⬆️
recharts/bundle-treeshaking-polar 473.91kB 14.47kB (3.15%) ⬆️
recharts/bundle-treeshaking-treemap 388.92kB 2.38kB (0.61%) ⬆️

Affected Assets, Files, and Routes:

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

Assets Changed:

Asset Name Size Change Total Size Change (%)
bundle.js 24.63kB 718.21kB 3.55%
view changes for bundle: recharts/bundle-cjs

Assets Changed:

Asset Name Size Change Total Size Change (%)
chart/Sankey.js 38 bytes 35.4kB 0.11%
cartesian/Bar.js 1.24kB 33.37kB 3.85%
polar/Pie.js -1.35kB 29.41kB -4.38%
cartesian/Area.js -5.49kB 27.28kB -16.76%
cartesian/Scatter.js -1.11kB 25.97kB -4.11%
cartesian/Line.js -8.02kB 23.74kB -25.24%
polar/RadialBar.js -1.09kB 22.75kB -4.58%
cartesian/Funnel.js 697 bytes 22.51kB 3.19%
polar/Radar.js -1.08kB 18.93kB -5.42%
index.js 1.3kB 16.79kB 8.39% ⚠️
shape/Rectangle.js 114 bytes 14.16kB 0.81%
util/types.js 222 bytes 9.92kB 2.29%
shape/Trapezoid.js 133 bytes 9.85kB 1.37%
animation/matchBy.js (New) 9.01kB 9.01kB 100.0% 🚀
cartesian/AreaRevealShape.js (New) 8.72kB 8.72kB 100.0% 🚀
cartesian/LineDrawShape.js (New) 8.09kB 8.09kB 100.0% 🚀
animation/configUpdate.js 57 bytes 7.49kB 0.77%
util/DataUtils.js 131 bytes 7.22kB 1.85%
animation/AnimatedItems.js (New) 7.17kB 7.17kB 100.0% 🚀
animation/ProgressAnimationManager.js (New) 6.83kB 6.83kB 100.0% 🚀
animation/AnimationProgressProvider.js (New) 6.66kB 6.66kB 100.0% 🚀
animation/CSSTransitionAnimate.js 1.18kB 5.7kB 25.98% ⚠️
animation/easing.js 247 bytes 4.79kB 5.44% ⚠️
animation/useAnimationStartSnapshot.js (New) 4.58kB 4.58kB 100.0% 🚀
animation/JavascriptAnimate.js 57 bytes 4.18kB 1.38%
animation/util.js 692 bytes 3.29kB 26.65% ⚠️
cartesian/useAnimatedLineLength.js (New) 2.42kB 2.42kB 100.0% 🚀
animation/ManualTimeoutController.js (New) 1.58kB 1.58kB 100.0% 🚀
view changes for bundle: recharts/bundle-es6

Assets Changed:

Asset Name Size Change Total Size Change (%)
chart/Sankey.js 38 bytes 33.65kB 0.11%
cartesian/Bar.js 1.25kB 31.63kB 4.11%
polar/Pie.js -1.34kB 27.6kB -4.62%
cartesian/Area.js -5.28kB 25.63kB -17.07%
cartesian/Scatter.js -1.08kB 24.36kB -4.23%
cartesian/Line.js -7.96kB 22.18kB -26.42%
polar/RadialBar.js -1.08kB 21.31kB -4.82%
cartesian/Funnel.js 713 bytes 21.05kB 3.51%
polar/Radar.js -1.1kB 17.44kB -5.92%
shape/Rectangle.js 114 bytes 12.89kB 0.89%
util/types.js 222 bytes 9.51kB 2.39%
animation/matchBy.js (New) 8.75kB 8.75kB 100.0% 🚀
shape/Trapezoid.js 133 bytes 8.68kB 1.56%
cartesian/AreaRevealShape.js (New) 7.7kB 7.7kB 100.0% 🚀
cartesian/LineDrawShape.js (New) 7.3kB 7.3kB 100.0% 🚀
animation/configUpdate.js 57 bytes 7.28kB 0.79%
animation/ProgressAnimationManager.js (New) 6.63kB 6.63kB 100.0% 🚀
util/DataUtils.js 131 bytes 6.36kB 2.1%
animation/AnimatedItems.js (New) 6.27kB 6.27kB 100.0% 🚀
animation/AnimationProgressProvider.js (New) 5.77kB 5.77kB 100.0% 🚀
animation/CSSTransitionAnimate.js 1.17kB 5.46kB 27.42% ⚠️
animation/easing.js 247 bytes 4.54kB 5.75% ⚠️
animation/useAnimationStartSnapshot.js (New) 4.41kB 4.41kB 100.0% 🚀
index.js 308 bytes 4.29kB 7.74% ⚠️
animation/JavascriptAnimate.js 57 bytes 3.8kB 1.52%
animation/util.js 625 bytes 2.9kB 27.46% ⚠️
cartesian/useAnimatedLineLength.js (New) 2.23kB 2.23kB 100.0% 🚀
animation/ManualTimeoutController.js (New) 1.4kB 1.4kB 100.0% 🚀
view changes for bundle: recharts/bundle-umd

Assets Changed:

Asset Name Size Change Total Size Change (%)
Recharts.js 10.79kB 595.63kB 1.84%
view changes for bundle: recharts/bundle-treeshaking-polar

Assets Changed:

Asset Name Size Change Total Size Change (%)
bundle.js 14.47kB 473.91kB 3.15%
view changes for bundle: recharts/bundle-treeshaking-sankey

Assets Changed:

Asset Name Size Change Total Size Change (%)
bundle.js 645 bytes 374.76kB 0.17%
view changes for bundle: recharts/bundle-treeshaking-treemap

Assets Changed:

Asset Name Size Change Total Size Change (%)
bundle.js 2.38kB 388.92kB 0.61%

@codecov

codecov Bot commented Apr 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 68.14070% with 317 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.49%. Comparing base (7a23854) to head (be92e25).

Files with missing lines Patch % Lines
...mponents/BarChart/AnimatedBarTimeSeriesExample.tsx 25.97% 47 Missing and 10 partials ⚠️
...Components/ScatterChart/CustomAnimationExample.tsx 20.00% 44 Missing and 12 partials ⚠️
src/animation/AnimationProgressProvider.tsx 3.12% 29 Missing and 2 partials ⚠️
...GuideView/Animations/MatchingStrategiesExample.tsx 6.66% 24 Missing and 4 partials ⚠️
...Components/LineChart/AnimatedTimeSeriesExample.tsx 34.14% 26 Missing and 1 partial ⚠️
...onents/AreaChart/RangeAreaChartCustomAnimation.tsx 57.14% 20 Missing and 1 partial ⚠️
...ents/RadarChart/RangeRadarChartCustomAnimation.tsx 41.66% 20 Missing and 1 partial ⚠️
...reaChart/AreaChartCustomAnimationExample/index.tsx 57.14% 18 Missing ⚠️
src/animation/CSSTransitionAnimate.tsx 15.78% 14 Missing and 2 partials ⚠️
...ts/LineChart/LineChartCustomShapeExample/index.tsx 44.00% 14 Missing ⚠️
... and 8 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7215      +/-   ##
==========================================
- Coverage   89.01%   87.49%   -1.53%     
==========================================
  Files         579      597      +18     
  Lines       13562    14183     +621     
  Branches     3426     3560     +134     
==========================================
+ Hits        12072    12409     +337     
- Misses       1328     1580     +252     
- Partials      162      194      +32     

☔ 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.

@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: 7

🧹 Nitpick comments (4)
omnidoc/verifyExamples.spec.ts (1)

193-200: Consider keeping runtime animation APIs out of the examples-exemption list.

Exempting matchByIndex, matchByDataKey, AnimationProgressProvider, and useAnimationProgress weakens coverage checks for the new user-facing API surface. Prefer exempting type-only exports, but keep runtime exports enforced.

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

In `@omnidoc/verifyExamples.spec.ts` around lines 193 - 200, The
examples-exemption list currently includes runtime APIs (matchByIndex,
matchByDataKey, AnimationProgressProvider, useAnimationProgress) which should
remain covered by runtime checks; update the list to remove those runtime
symbols and only exempt type-only exports (e.g., AnimationInterpolateFn,
AnimationMatchBy, AnimationHandle, AnimationStatus) so that runtime animation
APIs continue to be enforced by the verification test. Locate the array
containing the exempted names and delete the runtime entries (matchByIndex,
matchByDataKey, AnimationProgressProvider, useAnimationProgress), leaving only
the type-only export names in the exemption list.
src/animation/CSSTransitionAnimate.tsx (1)

87-97: JS animation loop continues past t=1.

The timingUpdate function schedules itself recursively via tc.setTimeout(timingUpdate) on every frame, even after t reaches 1. While the animation manager's start() will eventually trigger cleanup after duration, the interpolation loop will keep running and re-setting style to the final value unnecessarily.

Consider adding a check to stop scheduling once t >= 1:

♻️ Proposed optimization
         const timingUpdate = (now: number) => {
           if (!beginTime) {
             beginTime = now;
           }
           const t = Math.min(1, Math.max(0, (now - beginTime) / duration));
           setStyle(interpolateCSSValue(from, to, t));
-          stopJSAnimation.current = tc.setTimeout(timingUpdate);
+          if (t < 1) {
+            stopJSAnimation.current = tc.setTimeout(timingUpdate);
+          }
         };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/animation/CSSTransitionAnimate.tsx` around lines 87 - 97, In
onAnimationActive, timingUpdate currently reschedules itself unconditionally via
tc.setTimeout even after t reaches 1, causing unnecessary work; modify
timingUpdate (and related stopJSAnimation handling) to check computed t and when
t >= 1 stop scheduling further frames (clear or avoid setting
stopJSAnimation.current) and ensure the final setStyle(interpolateCSSValue(from,
to, 1)) is applied once before stopping so the loop does not run past
completion; update references to beginTime, stopJSAnimation, tc.setTimeout,
setStyle, interpolateCSSValue and duration accordingly.
src/cartesian/Bar.tsx (1)

819-820: Consider memoizing the interpolation function.

The defaultBarAnimateItems(layout) is called on every render, creating a new function reference. While AnimatedItems may handle this internally, memoizing could prevent unnecessary animation restarts if the component uses reference equality checks.

♻️ Optional: memoize the interpolator
 function RectanglesWithAnimation({
   props,
   previousRectanglesRef,
 }: {
   props: InternalProps;
   previousRectanglesRef: MutableRefObject<null | ReadonlyArray<BarRectangleItem>>;
 }) {
   const { data, layout, isAnimationActive, animationBegin, animationDuration, animationEasing } = props;
-  const animationInterpolateFn = props.animationInterpolateFn ?? defaultBarAnimateItems(layout);
+  const defaultInterpolator = React.useMemo(() => defaultBarAnimateItems(layout), [layout]);
+  const animationInterpolateFn = props.animationInterpolateFn ?? defaultInterpolator;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cartesian/Bar.tsx` around lines 819 - 820, The interpolator is recreated
every render because animationInterpolateFn is set to
props.animationInterpolateFn ?? defaultBarAnimateItems(layout); change this to
memoize the fallback: compute defaultBarAnimateItems(layout) inside a useMemo
(or equivalent) keyed on layout (and any layout-derived values) and then use
props.animationInterpolateFn ?? memoizedInterpolator; update the assignment of
animationInterpolateFn in the Bar component to reference that memoized value so
reference equality is stable for AnimatedItems and prevents unnecessary
animation restarts.
www/src/components/GuideView/Animations/CustomAnimationExample.tsx (1)

29-31: Potential edge case: division safety in staggered animation.

When nextItems.length is 0, count will be 0 and index / count results in NaN. While t === 1 early return at line 26 handles the final state, if called with empty nextItems before completion, the function may produce unexpected results.

Consider adding a guard:

 const staggeredCrossfade: AnimationInterpolateFn<ScatterPointItem> = (prevItems, nextItems, t) => {
   if (t === 1) return nextItems;
+  if (nextItems.length === 0) return nextItems;
   const count = nextItems.length;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@www/src/components/GuideView/Animations/CustomAnimationExample.tsx` around
lines 29 - 31, The mapping that computes staggered animation values can divide
by zero when count is 0; in the function in CustomAnimationExample.tsx that uses
index/count to compute delay and progress, add a guard for count === 0 (or count
<= 0) before computing delay — e.g., if count === 0 simply return the original
entry (or set a sensible default delay/progress) to avoid NaN, and ensure you
still return a valid opacity value; update the code that calculates delay,
progress, and the returned { ...entry, opacity } to use this guard (references:
index, count, delay, progress).
🤖 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/animation/AnimatedItems.tsx`:
- Around line 43-47: The public interpolator type AnimationInterpolateFn
currently types prevItems as ReadonlyArray<T> | null which disallows empty
slots; change that parameter to ReadonlyArray<T | null> | null so predecessors
may contain holes (or use T | undefined if your codebase prefers undefined), and
make the same change to the other interpolator type declared later (the second
occurrence around lines 129-135) so any custom interpolator used with
animationMatchBy can safely handle missing entries.

In `@src/animation/CSSTransitionAnimate.tsx`:
- Around line 100-108: The cleanup calls onAnimationEnd directly while the
animationManager also receives onAnimationEnd via animationManager.start,
risking double invocation; create a safe wrapper (e.g., safeOnAnimationEnd) that
guards with an internal "called" flag and use that wrapper both when passing to
animationManager.start and in the cleanup instead of calling onAnimationEnd
directly (or remove the direct call and only call animationManager.stop with the
wrapper if needed); reference animationManager.start, stopJSAnimation.current,
onAnimationEnd and replace usages with safeOnAnimationEnd to ensure
onAnimationEnd is only invoked once.

In `@src/animation/ManualTimeoutController.ts`:
- Around line 13-17: The current setTimeout implementation pushes an entry with
only { callback } and the returned cancel function filters out all entries with
the same callback, which removes multiple scheduled timeouts; change it to
create a distinct entry object (e.g., const entry = { callback, ... } or assign
a unique id) and push that entry into this.timeouts, then have the returned
CancelableTimeout remove only that specific entry by reference or id (e.g.,
this.timeouts = this.timeouts.filter(t => t !== entry || t.id !== entry.id)).
Apply the same fix to the analogous removal logic in the other method around
lines 20-24 so cancellations remove only the single scheduled item.

In `@src/animation/matchBy.ts`:
- Around line 43-49: The matchByDataKey function currently uses unsafe `as`
casts and its declared match return type doesn't reflect that unmatched items
yield undefined; update the AnimationMatchBy return type to allow undefined
(e.g. include | undefined in the match result array type) and remove all `as`
assertions in this module (notably the cast on item.payload in matchByDataKey
and the assertions referenced at lines 86–88) — after the runtime guard
(item.payload == null || typeof item.payload !== 'object') payload is already
narrowed to object so pass it directly to getValueByDataKey(item.payload,
dataKey) and return value or undefined, and ensure any callers/types are
adjusted to accept ReadonlyArray<T | undefined>.

In `@src/cartesian/Area.tsx`:
- Around line 625-663: The defaultAreaAnimateItems and interpolateBaseLine
functions are using Math.floor(index * prevPointsDiffFactor) which breaks
key-based (animationMatchBy) alignment; change the lookup to use the direct
index when points were already aligned (i.e., when prevPointsDiffFactor === 1)
and only apply the stretched-index math when prevPointsDiffFactor !== 1. Update
defaultAreaAnimateItems (replace prevPointIndex calculation) and
interpolateBaseLine (replace prevPointIndex calculation) to compute
prevPointIndex = index when prevPointsDiffFactor === 1, otherwise prevPointIndex
= Math.floor(index * prevPointsDiffFactor), so keyed AnimatedItems read
prevItems[index] and stacked/range baselines still use the stretch logic.

In `@src/cartesian/Line.tsx`:
- Around line 585-608: The defaultLineAnimateItems function currently uses
prevPointsDiffFactor to compute prevPointIndex which breaks correct key-based
matching done by animationMatchBy; change the lookup to use prevItems[index]
when prevItems are already matched by key (i.e., when animationMatchBy is in
effect) and only use the stretched-index computation (Math.floor(index *
prevPointsDiffFactor)) for the match-by-index code path; update the analogous
logic in the other occurrences noted (around the blocks identified at lines
653-655 and 711-712) so inserted/removed points interpolate from the true
matched previous item when keys are used and fall back to the stretched lookup
only for index-based matching.

In `@src/cartesian/Scatter.tsx`:
- Around line 625-627: The animationInput currently passes the entire props
object to AnimatedItems (animationInput={props}), causing unnecessary animation
restarts; update Scatter to pass a stable, minimal input such as the computed
points array or a memoized subset of props that only changes when geometry
changes (e.g., use the existing points variable or create a useMemo that derives
[points, dataKey, cx, cy] and pass that to animationInput), and ensure
AnimatedItems still receives animationIdPrefix="recharts-scatter-" so only
genuine geometry updates trigger transitions.

---

Nitpick comments:
In `@omnidoc/verifyExamples.spec.ts`:
- Around line 193-200: The examples-exemption list currently includes runtime
APIs (matchByIndex, matchByDataKey, AnimationProgressProvider,
useAnimationProgress) which should remain covered by runtime checks; update the
list to remove those runtime symbols and only exempt type-only exports (e.g.,
AnimationInterpolateFn, AnimationMatchBy, AnimationHandle, AnimationStatus) so
that runtime animation APIs continue to be enforced by the verification test.
Locate the array containing the exempted names and delete the runtime entries
(matchByIndex, matchByDataKey, AnimationProgressProvider, useAnimationProgress),
leaving only the type-only export names in the exemption list.

In `@src/animation/CSSTransitionAnimate.tsx`:
- Around line 87-97: In onAnimationActive, timingUpdate currently reschedules
itself unconditionally via tc.setTimeout even after t reaches 1, causing
unnecessary work; modify timingUpdate (and related stopJSAnimation handling) to
check computed t and when t >= 1 stop scheduling further frames (clear or avoid
setting stopJSAnimation.current) and ensure the final
setStyle(interpolateCSSValue(from, to, 1)) is applied once before stopping so
the loop does not run past completion; update references to beginTime,
stopJSAnimation, tc.setTimeout, setStyle, interpolateCSSValue and duration
accordingly.

In `@src/cartesian/Bar.tsx`:
- Around line 819-820: The interpolator is recreated every render because
animationInterpolateFn is set to props.animationInterpolateFn ??
defaultBarAnimateItems(layout); change this to memoize the fallback: compute
defaultBarAnimateItems(layout) inside a useMemo (or equivalent) keyed on layout
(and any layout-derived values) and then use props.animationInterpolateFn ??
memoizedInterpolator; update the assignment of animationInterpolateFn in the Bar
component to reference that memoized value so reference equality is stable for
AnimatedItems and prevents unnecessary animation restarts.

In `@www/src/components/GuideView/Animations/CustomAnimationExample.tsx`:
- Around line 29-31: The mapping that computes staggered animation values can
divide by zero when count is 0; in the function in CustomAnimationExample.tsx
that uses index/count to compute delay and progress, add a guard for count === 0
(or count <= 0) before computing delay — e.g., if count === 0 simply return the
original entry (or set a sensible default delay/progress) to avoid NaN, and
ensure you still return a valid opacity value; update the code that calculates
delay, progress, and the returned { ...entry, opacity } to use this guard
(references: index, count, delay, progress).
🪄 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: 77ce1d60-43d5-43b6-9f56-1c3b8f7f9d98

📥 Commits

Reviewing files that changed from the base of the PR and between 3f25c0d and f237791.

📒 Files selected for processing (30)
  • omnidoc/readProject.spec.ts
  • omnidoc/verifyExamples.spec.ts
  • scripts/snapshots/es6Files.txt
  • scripts/snapshots/libFiles.txt
  • scripts/snapshots/typesFiles.txt
  • src/animation/AnimatedItems.tsx
  • src/animation/AnimationManager.ts
  • src/animation/AnimationProgressProvider.tsx
  • src/animation/CSSTransitionAnimate.tsx
  • src/animation/ManualTimeoutController.ts
  • src/animation/ProgressAnimationManager.ts
  • src/animation/matchBy.ts
  • src/animation/util.ts
  • src/cartesian/Area.tsx
  • src/cartesian/Bar.tsx
  • src/cartesian/Funnel.tsx
  • src/cartesian/Line.tsx
  • src/cartesian/Scatter.tsx
  • src/index.ts
  • src/polar/Pie.tsx
  • src/polar/Radar.tsx
  • src/polar/RadialBar.tsx
  • test/animation/ManualTimeoutController.spec.ts
  • test/animation/ProgressAnimationManager.spec.ts
  • test/animation/matchBy.spec.ts
  • test/animation/util.spec.ts
  • www/src/components/GuideView/Animations/CustomAnimationExample.tsx
  • www/src/components/GuideView/Animations/MatchingExample.tsx
  • www/src/components/GuideView/Animations/index.tsx
  • www/src/docs/apiCates.ts

Comment thread src/animation/AnimatedItems.tsx Outdated
Comment on lines +43 to +47
export type AnimationInterpolateFn<T> = (
prevItems: ReadonlyArray<T> | null,
nextItems: ReadonlyArray<T>,
t: number,
) => ReadonlyArray<T>;

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.

⚠️ Potential issue | 🟠 Major

Model unmatched predecessors in the public interpolator type.

alignPreviousItems() explicitly allows empty slots, but AnimationInterpolateFn still promises prevItems: ReadonlyArray<T> | null. Any custom interpolator used with animationMatchBy can therefore treat prevItems[i] as defined and crash as soon as data is inserted or removed.

Suggested fix
 export type AnimationInterpolateFn<T> = (
-  prevItems: ReadonlyArray<T> | null,
+  prevItems: ReadonlyArray<T | undefined> | null,
   nextItems: ReadonlyArray<T>,
   t: number,
 ) => ReadonlyArray<T>;

Also applies to: 129-135

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

In `@src/animation/AnimatedItems.tsx` around lines 43 - 47, The public
interpolator type AnimationInterpolateFn currently types prevItems as
ReadonlyArray<T> | null which disallows empty slots; change that parameter to
ReadonlyArray<T | null> | null so predecessors may contain holes (or use T |
undefined if your codebase prefers undefined), and make the same change to the
other interpolator type declared later (the second occurrence around lines
129-135) so any custom interpolator used with animationMatchBy can safely handle
missing entries.

Comment on lines +100 to +108
animationManager.start([onAnimationStart, begin, onAnimationActive, duration, onAnimationEnd]);

return () => {
animationManager.stop();
if (stopJSAnimation.current) {
stopJSAnimation.current();
}
onAnimationEnd();
};

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.

⚠️ Potential issue | 🟡 Minor

Potential double invocation of onAnimationEnd.

The onAnimationEnd callback is passed to animationManager.start() on line 100, and also called directly in the cleanup function on line 107. If the animation completes normally (triggering onAnimationEnd via the manager) and then the component unmounts or effect re-runs, onAnimationEnd could be called twice.

Consider tracking whether onAnimationEnd has already been called, or removing the direct call in cleanup if the manager is responsible for invoking it upon completion.

🛡️ Suggested approach
+  const animationEndedRef = useRef(false);
+
+  // Wrap onAnimationEnd to prevent double-calling
+  const safeOnAnimationEnd = useCallback(() => {
+    if (!animationEndedRef.current) {
+      animationEndedRef.current = true;
+      onAnimationEnd();
+    }
+  }, [onAnimationEnd]);

Then use safeOnAnimationEnd in both places instead of onAnimationEnd.

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

In `@src/animation/CSSTransitionAnimate.tsx` around lines 100 - 108, The cleanup
calls onAnimationEnd directly while the animationManager also receives
onAnimationEnd via animationManager.start, risking double invocation; create a
safe wrapper (e.g., safeOnAnimationEnd) that guards with an internal "called"
flag and use that wrapper both when passing to animationManager.start and in the
cleanup instead of calling onAnimationEnd directly (or remove the direct call
and only call animationManager.stop with the wrapper if needed); reference
animationManager.start, stopJSAnimation.current, onAnimationEnd and replace
usages with safeOnAnimationEnd to ensure onAnimationEnd is only invoked once.

Comment on lines +13 to +17
setTimeout(callback: CallbackType, _delay?: number): CancelableTimeout {
this.timeouts.push({ callback });
return () => {
this.timeouts = this.timeouts.filter(t => t.callback !== callback);
};

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.

⚠️ Potential issue | 🟠 Major

Callback-based removal drops multiple scheduled timeouts.

At Line 16 and Line 23, removal by callback !== ... removes all queued entries with the same function reference. setTimeout/cancel should operate on one scheduled entry.

Proposed fix
 export class ManualTimeoutController implements TimeoutController {
-  private timeouts: Array<{ callback: CallbackType }> = [];
+  private nextId = 0;
+  private timeouts: Array<{ id: number; callback: CallbackType }> = [];

   setTimeout(callback: CallbackType, _delay?: number): CancelableTimeout {
-    this.timeouts.push({ callback });
+    const id = this.nextId++;
+    this.timeouts.push({ id, callback });
     return () => {
-      this.timeouts = this.timeouts.filter(t => t.callback !== callback);
+      this.timeouts = this.timeouts.filter(t => t.id !== id);
     };
   }

   triggerNextTimeout(now: number): void {
     const next = this.timeouts.shift();
     if (next == null) return;
-    this.timeouts = this.timeouts.filter(t => t.callback !== next.callback);
     next.callback(now);
   }

Also applies to: 20-24

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

In `@src/animation/ManualTimeoutController.ts` around lines 13 - 17, The current
setTimeout implementation pushes an entry with only { callback } and the
returned cancel function filters out all entries with the same callback, which
removes multiple scheduled timeouts; change it to create a distinct entry object
(e.g., const entry = { callback, ... } or assign a unique id) and push that
entry into this.timeouts, then have the returned CancelableTimeout remove only
that specific entry by reference or id (e.g., this.timeouts =
this.timeouts.filter(t => t !== entry || t.id !== entry.id)). Apply the same fix
to the analogous removal logic in the other method around lines 20-24 so
cancellations remove only the single scheduled item.

Comment thread src/animation/matchBy.ts
Comment thread src/cartesian/Area.tsx Outdated
Comment thread src/cartesian/Line.tsx Outdated
Comment thread src/cartesian/Scatter.tsx
Comment on lines +625 to +627
<AnimatedItems
animationInput={props}
animationIdPrefix="recharts-scatter-"

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.

⚠️ Potential issue | 🟠 Major

Use a stable animation trigger instead of the whole props object.

On Line 626, animationInput={props} makes every re-render look like a brand-new animation, because props is recreated on each render. That will restart Scatter transitions for unrelated updates. Use points or a memoized subset that only changes when the animated geometry changes.

Suggested fix
-        animationInput={props}
+        animationInput={points}

Based on learnings: Value consistency, usability, and performance in Recharts implementation.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<AnimatedItems
animationInput={props}
animationIdPrefix="recharts-scatter-"
<AnimatedItems
animationInput={points}
animationIdPrefix="recharts-scatter-"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/cartesian/Scatter.tsx` around lines 625 - 627, The animationInput
currently passes the entire props object to AnimatedItems
(animationInput={props}), causing unnecessary animation restarts; update Scatter
to pass a stable, minimal input such as the computed points array or a memoized
subset of props that only changes when geometry changes (e.g., use the existing
points variable or create a useMemo that derives [points, dataKey, cx, cy] and
pass that to animationInput), and ensure AnimatedItems still receives
animationIdPrefix="recharts-scatter-" so only genuine geometry updates trigger
transitions.

@github-actions

github-actions Bot commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

Comment thread src/cartesian/Area.tsx Outdated
* @param t A normalized time value (0 = start, 1 = end)
* @returns The interpolated items at time t
*/
animationInterpolateFn?: AnimationInterpolateFn<AreaPointItem>;

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I'm not a fan of having this done in two functions - would be simpler with one. But if we do that then the type variety doesn't allow us to reuse helpers so we would need to re-export 2 variants for each graphical item.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we don't need more variants to maintain

@github-actions

github-actions Bot commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

@ckifer ckifer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks nice

@PavelVanecek

Copy link
Copy Markdown
Collaborator Author

#7235

@PavelVanecek
PavelVanecek force-pushed the animation-new branch 2 times, most recently from 2d9d86d to bccddbe Compare April 19, 2026 00:29
@github-actions

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

@github-actions

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

PavelVanecek added a commit that referenced this pull request May 2, 2026
## Description

This function is useful for writing custom animations. So let's export
it for 3.9

## Related Issue

#7215

## Types of changes

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

## Checklist:

- [x] My change requires a change to the documentation.
- [x] 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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a public interpolate utility for animation-related numeric
interpolation.

* **Documentation**
* Added an "Animation" API category and localized docs (English/Chinese)
for the new API.

* **Bug Fixes**
* Improved interpolation behavior for area baseline animations to handle
numeric and non-finite baselines more predictably.

* **Tests**
  * Expanded tests to cover new interpolate behaviors and export.

* **Chores**
  * Updated bundle/treeshaking expectations to include the new utility.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@github-actions

github-actions Bot commented May 2, 2026

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

@github-actions

github-actions Bot commented May 4, 2026

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

@chromatic-com

chromatic-com Bot commented May 9, 2026

Copy link
Copy Markdown

Tip

All tests passed and all changes approved!

🟢 UI Tests: 198 tests unchanged
🟢 UI Review: 198 stories published -- no changes
Storybook icon Storybook Publish: 198 stories published

@github-actions

github-actions Bot commented May 9, 2026

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

@github-actions

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

@PavelVanecek
PavelVanecek marked this pull request as ready for review May 22, 2026 23:34

@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: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/polar/Radar.tsx (1)

499-527: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Key Radar's animation state from geometry, not the full props bag.

Both useAnimationStartSnapshot(props, ...) and animationInput={props} depend on the entire props object, so unrelated prop/object churn can freeze a new baseline and restart the polygon animation even when points and baseLinePoints are unchanged. A memoized { points, baseLinePoints } input would avoid that.

🤖 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/polar/Radar.tsx` around lines 499 - 527, The animation snapshot and
AnimatedItems input are currently keyed on the entire props object causing
unrelated prop churn to restart animations; change both the call to
useAnimationStartSnapshot and the animationInput passed to <AnimatedItems> to
use a stable, memoized object containing only { points, baseLinePoints } (e.g.,
compute a const animationKey = useMemo(() => ({ points, baseLinePoints }),
[points, baseLinePoints]) and pass that into
useAnimationStartSnapshot(animationKey, previousBaseLinePointsRef) and
animationInput={animationKey}) so animation state is driven only by geometry
changes.
♻️ Duplicate comments (1)
src/cartesian/Scatter.tsx (1)

649-653: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a stable animation trigger instead of the full props object.

animationInput={props} ties the animation lifecycle to prop object churn, so unrelated re-renders can restart Scatter transitions. points or a memoized geometry-only subset is the stable input here.

Suggested fix
-        animationInput={props}
+        animationInput={points}
🤖 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/cartesian/Scatter.tsx` around lines 649 - 653, The animation input
currently uses the whole props object (animationInput={props}) which causes
unnecessary animation restarts; change AnimatedItems to use a stable trigger
like animationInput={points} or a memoized geometry-only subset (e.g., a
memoized array of point positions) so animations only restart when the actual
point data changes; update the AnimatedItems usage near
ScatterLabelListProvider/AnimatedItems and ensure any memoization is done where
points are computed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/animation/AnimatedItems.tsx`:
- Around line 108-111: The default snapshot guard for updating previousItemsRef
must be strictly t > 0; update the AnimatedItems component so the fallback for
the prop shouldUpdatePreviousRef is (t) => t > 0 (not including totalLength),
and use that predicate when deciding to write previousItemsRef so no update
occurs at t === 0; also apply the same change to the other occurrence referenced
(the second check around lines 181-182) so both places use the same fallback
guard and prevent snapshotting the synthetic first frame.
- Around line 23-40: The hook useAnimationCallbacks lacks an explicit return
type; add a named return type (e.g., { isAnimating: boolean;
handleAnimationStart: () => void; handleAnimationEnd: () => void }) to the
function signature so the exported hook surface is explicit and stable, keeping
the existing state and callbacks (isAnimating, handleAnimationStart,
handleAnimationEnd) intact and updating the function declaration to include that
return type.

In `@src/animation/CSSTransitionAnimate.tsx`:
- Around line 87-97: The manual animation loop in onAnimationActive's
timingUpdate can produce NaN when duration === 0 and continues re-queuing after
completion; modify timingUpdate so it treats duration <= 0 as an immediate
completion (set t = 1), guard the computed t with Number.isFinite and clamp to
[0,1], call setStyle(interpolateCSSValue(from, to, t)) with that valid t, and
only call stopJSAnimation.current = tc.setTimeout(timingUpdate) when t < 1 so
the timeout is not re-queued after the animation finishes; update references to
timingUpdate, beginTime, duration, setStyle, interpolateCSSValue,
stopJSAnimation, and tc.setTimeout accordingly.

In `@src/animation/matchBy.ts`:
- Around line 148-159: The current buildPrevKeyMap (and the analogous code at
176-199) only keeps one item per match key so duplicate keys cause one prev to
match many nexts; change buildPrevKeyMap to store all items for a key (use
Map<string|number, T[]> and push each item instead of skipping duplicates), then
update the matching/consumption logic to pull (e.g. shift/pop) a single item
from the array when a key is matched and delete the map entry when the array
becomes empty so each prev is consumed at most once.

In `@src/animation/ProgressAnimationManager.ts`:
- Around line 173-177: The code in ProgressAnimationManager is using a forbidden
type assertion (head as ReactSmoothStyle); instead, narrow head with the
existing typeof checks and pass it directly to the listener. Replace the branch
to call this.listener?.(head) when typeof head === 'object' || typeof head ===
'string' (remove the "as ReactSmoothStyle" assertion) and if necessary update
the listener signature/type to accept string | object | ReactSmoothStyle so the
call is type-safe without casting.

In `@src/cartesian/Area.tsx`:
- Around line 168-199: The JSDoc `@see` {`@link` ...} entries in the Area.tsx prop
docs (notably for animationInterpolateFn and animationMatchBy) are missing their
closing "}" which breaks generated API docs; update those `@see` tags to use
properly closed {`@link` ...} syntax (e.g., {`@link` https://...}) and scan the
other new animation-related prop docs in the same file to fix any identical
malformed {`@link` ...} tags so all links render correctly.

In `@src/cartesian/AreaRevealShape.tsx`:
- Around line 42-43: The clip bounds currently use parseInt on strokeWidth (in
AreaRevealShape height and the similar calculation at Line 70), which truncates
fractional widths and can under-size the clip; replace
parseInt(`${strokeWidth}`, 10) with a non-truncating parse + roundup, e.g. use
Math.ceil(parseFloat(`${strokeWidth}`) || 1) (or Math.ceil(Number(strokeWidth)
|| 1)) so fractional stroke widths like 0.5 are preserved and the clip rect is
large enough; update both occurrences in the AreaRevealShape calculations where
height/clip size is computed.

In `@src/cartesian/LineDrawShape.tsx`:
- Around line 164-166: Remove the unsafe "as React.RefObject" cast on
curveProps.pathRef and narrow the ref structurally before accessing .current:
check if curveProps.pathRef is an object and has a 'current' property (e.g.,
typeof curveProps.pathRef === 'object' && curveProps.pathRef !== null &&
'current' in curveProps.pathRef) and only then read const pathRef =
curveProps.pathRef as React.RefObject<SVGPathElement | null>; const totalLength
= getTotalLength(pathRef.current ?? null); otherwise treat totalLength as 0 (or
handle callback refs separately) and then call
computeAnimatedStrokeDasharray(userStrokeDasharray, totalLength, visibleLength)
to compute strokeDasharray.

In `@src/cartesian/Scatter.tsx`:
- Around line 640-644: The Scatter component currently calls
useAnimationCallbacks() without passing the public callbacks, so its
onAnimationStart/onAnimationEnd props are never invoked; update the hook
invocation in Scatter to forward props.onAnimationStart and props.onAnimationEnd
(e.g., call useAnimationCallbacks with an options object containing
onAnimationStart: props.onAnimationStart and onAnimationEnd:
props.onAnimationEnd) so that the returned handleAnimationStart and
handleAnimationEnd will call the public callbacks.

In `@src/polar/Pie.tsx`:
- Around line 973-976: The AnimatedItems call in PieImpl is using the entire
props object as animationInput (animationInput={props}), which causes
unnecessary restart of animations on non-geometry updates; change animationInput
to use the geometry-only data such as sectors (animationInput={sectors}) or a
memoized value derived from sectors so animations only react to geometry
changes; update the AnimatedItems invocation in PieImpl to pass sectors or a
memoized geometry input instead of props.
- Around line 917-949: The animation computes paddingAngle using the original
array index which causes an extra leading gap when earlier items are removed;
update defaultPieAnimateItems to compute paddingAngle based on the
emitted-sector sequence instead: introduce a local emittedIndex (increment only
for items where item.status !== 'removed') and use emittedIndex > 0 to decide
whether to apply get(item.next, 'paddingAngle', 0), keeping curAngle seeded from
firstNonRemoved.next.startAngle and updating emittedIndex each time you push a
sector so padding is applied relative to emitted sectors rather than source
indices.

In `@src/polar/RadialBar.tsx`:
- Around line 254-257: The AnimatedItems call in RadialBar uses
animationInput={props}, which causes unrelated prop changes to restart
animations; change animationInput to a stable value such as the existing sectors
array or a memoized geometry-only subset (e.g. compute a useMemo that maps
sectors to {startAngle,endAngle,radius,...} and pass that) so animations depend
only on geometry; update the AnimatedItems prop (animationInput) accordingly and
ensure any memoization uses relevant keys (like sectors or data keys) to keep
the input stable.
- Around line 206-224: The defaultRadialBarAnimateItems interpolator currently
always tweens startAngle/endAngle, causing centric layouts (which encode value
changes via innerRadius/outerRadius) to jump or animate incorrectly; update
defaultRadialBarAnimateItems to branch on the PolarLayout.layout value: if
layout === 'radial' keep the existing startAngle/endAngle interpolation for
matched and added items, otherwise (centric layout) interpolate innerRadius and
outerRadius for matched items (use interpolate(item.prev.innerRadius,
item.next.innerRadius, t) and same for outerRadius) and for added items set next
with innerRadius interpolated from next.innerRadius to next.outerRadius as
appropriate (or interpolate innerRadius from 0 to next.innerRadius / outerRadius
as your design requires), and on t === 1 continue returning item.next unchanged;
ensure removed items remain filtered out as before and reference the
defaultRadialBarAnimateItems symbol to implement the branch.

---

Outside diff comments:
In `@src/polar/Radar.tsx`:
- Around line 499-527: The animation snapshot and AnimatedItems input are
currently keyed on the entire props object causing unrelated prop churn to
restart animations; change both the call to useAnimationStartSnapshot and the
animationInput passed to <AnimatedItems> to use a stable, memoized object
containing only { points, baseLinePoints } (e.g., compute a const animationKey =
useMemo(() => ({ points, baseLinePoints }), [points, baseLinePoints]) and pass
that into useAnimationStartSnapshot(animationKey, previousBaseLinePointsRef) and
animationInput={animationKey}) so animation state is driven only by geometry
changes.

---

Duplicate comments:
In `@src/cartesian/Scatter.tsx`:
- Around line 649-653: The animation input currently uses the whole props object
(animationInput={props}) which causes unnecessary animation restarts; change
AnimatedItems to use a stable trigger like animationInput={points} or a memoized
geometry-only subset (e.g., a memoized array of point positions) so animations
only restart when the actual point data changes; update the AnimatedItems usage
near ScatterLabelListProvider/AnimatedItems and ensure any memoization is done
where points are computed.
🪄 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: b6a97df9-749a-4df1-81be-8ec04a244981

📥 Commits

Reviewing files that changed from the base of the PR and between f237791 and 66a71f7.

⛔ Files ignored due to path filters (3)
  • test-vr/__snapshots__/tests/www/dark-mode.spec-vr.tsx-snapshots/dark-mode-examples-list-1-chromium-linux.png is excluded by !**/*.png
  • test-vr/__snapshots__/tests/www/dark-mode.spec-vr.tsx-snapshots/dark-mode-examples-list-1-firefox-linux.png is excluded by !**/*.png
  • test-vr/__snapshots__/tests/www/dark-mode.spec-vr.tsx-snapshots/dark-mode-examples-list-1-webkit-linux.png is excluded by !**/*.png
📒 Files selected for processing (45)
  • omnidoc/commentSimilarityExceptions.ts
  • omnidoc/generateStorybookArgs.ts
  • omnidoc/readProject.spec.ts
  • omnidoc/verifyExamples.spec.ts
  • scripts/snapshots/es6Files.txt
  • scripts/snapshots/libFiles.txt
  • scripts/snapshots/typesFiles.txt
  • scripts/treeshaking-groups/Area.ts
  • scripts/treeshaking-groups/Bar.ts
  • scripts/treeshaking-groups/BarStack.ts
  • scripts/treeshaking-groups/Funnel.ts
  • scripts/treeshaking-groups/Line.ts
  • scripts/treeshaking-groups/Pie.ts
  • scripts/treeshaking-groups/Radar.ts
  • scripts/treeshaking-groups/RadialBar.ts
  • scripts/treeshaking-groups/Scatter.ts
  • scripts/treeshaking-groups/index.ts
  • src/animation/AnimatedItems.tsx
  • src/animation/AnimationManager.ts
  • src/animation/AnimationProgressProvider.tsx
  • src/animation/CSSTransitionAnimate.tsx
  • src/animation/ManualTimeoutController.ts
  • src/animation/ProgressAnimationManager.ts
  • src/animation/matchBy.ts
  • src/animation/useAnimationStartSnapshot.ts
  • src/animation/util.ts
  • src/cartesian/Area.tsx
  • src/cartesian/AreaRevealShape.tsx
  • src/cartesian/Bar.tsx
  • src/cartesian/Funnel.tsx
  • src/cartesian/Line.tsx
  • src/cartesian/LineDrawShape.tsx
  • src/cartesian/Scatter.tsx
  • src/cartesian/useAnimatedLineLength.ts
  • src/index.ts
  • src/polar/Pie.tsx
  • src/polar/Radar.tsx
  • src/polar/RadialBar.tsx
  • src/shape/Curve.tsx
  • src/util/ActiveShapeUtils.tsx
  • src/util/BarUtils.tsx
  • src/util/FunnelUtils.tsx
  • src/util/RadialBarUtils.tsx
  • src/util/ScatterUtils.tsx
  • src/util/types.ts
💤 Files with no reviewable changes (1)
  • src/shape/Curve.tsx

Comment thread src/animation/AnimatedItems.tsx Outdated
Comment thread src/animation/AnimatedItems.tsx Outdated
Comment on lines +87 to +97
const onAnimationActive = () => {
let beginTime: number;
const timingUpdate = (now: number) => {
if (!beginTime) {
beginTime = now;
}
const t = Math.min(1, Math.max(0, (now - beginTime) / duration));
setStyle(interpolateCSSValue(from, to, t));
stopJSAnimation.current = tc.setTimeout(timingUpdate);
};
stopJSAnimation.current = tc.setTimeout(timingUpdate);

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle terminal and zero-duration progress in the manual loop.

timingUpdate can compute NaN on the first tick when duration === 0, and it re-queues itself even after t reaches 1. That leaves the manual path with an invalid interpolated value and an extra live timeout past completion.

Proposed fix
       const tc = animationManager.getTimeoutController();
       const onAnimationActive = () => {
-        let beginTime: number;
+        let beginTime: number | undefined;
         const timingUpdate = (now: number) => {
-          if (!beginTime) {
+          if (beginTime === undefined) {
             beginTime = now;
           }
-          const t = Math.min(1, Math.max(0, (now - beginTime) / duration));
+          const t = duration <= 0 ? 1 : Math.min(1, Math.max(0, (now - beginTime) / duration));
           setStyle(interpolateCSSValue(from, to, t));
-          stopJSAnimation.current = tc.setTimeout(timingUpdate);
+          if (t < 1) {
+            stopJSAnimation.current = tc.setTimeout(timingUpdate);
+          } else {
+            stopJSAnimation.current = null;
+          }
         };
         stopJSAnimation.current = tc.setTimeout(timingUpdate);
       };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const onAnimationActive = () => {
let beginTime: number;
const timingUpdate = (now: number) => {
if (!beginTime) {
beginTime = now;
}
const t = Math.min(1, Math.max(0, (now - beginTime) / duration));
setStyle(interpolateCSSValue(from, to, t));
stopJSAnimation.current = tc.setTimeout(timingUpdate);
};
stopJSAnimation.current = tc.setTimeout(timingUpdate);
const onAnimationActive = () => {
let beginTime: number | undefined;
const timingUpdate = (now: number) => {
if (beginTime === undefined) {
beginTime = now;
}
const t = duration <= 0 ? 1 : Math.min(1, Math.max(0, (now - beginTime) / duration));
setStyle(interpolateCSSValue(from, to, t));
if (t < 1) {
stopJSAnimation.current = tc.setTimeout(timingUpdate);
} else {
stopJSAnimation.current = null;
}
};
stopJSAnimation.current = tc.setTimeout(timingUpdate);
};
🤖 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/animation/CSSTransitionAnimate.tsx` around lines 87 - 97, The manual
animation loop in onAnimationActive's timingUpdate can produce NaN when duration
=== 0 and continues re-queuing after completion; modify timingUpdate so it
treats duration <= 0 as an immediate completion (set t = 1), guard the computed
t with Number.isFinite and clamp to [0,1], call
setStyle(interpolateCSSValue(from, to, t)) with that valid t, and only call
stopJSAnimation.current = tc.setTimeout(timingUpdate) when t < 1 so the timeout
is not re-queued after the animation finishes; update references to
timingUpdate, beginTime, duration, setStyle, interpolateCSSValue,
stopJSAnimation, and tc.setTimeout accordingly.

Comment thread src/animation/matchBy.ts
Comment on lines +148 to +159
function buildPrevKeyMap<T>(
prevItems: ReadonlyArray<T>,
matchBy: AnimationMatchBy<T>,
): ReadonlyMap<string | number, T> {
const prevMap = new Map<string | number, T>();
for (let i = 0; i < prevItems.length; i++) {
const item = prevItems[i];
if (item == null) continue;
const key = matchBy(item, i);
if (key != null && !prevMap.has(key)) {
prevMap.set(key, item);
}

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Consume each match key only once.

With duplicate keys, this can pair the same prev item to multiple next items and also drop extra prev duplicates entirely because buildPrevKeyMap() only keeps one entry per key. For example, [A(key=1)] -> [B(key=1), C(key=1)] becomes two matched items instead of matched + added.

♻️ One localized fix
 function buildPrevKeyMap<T>(
   prevItems: ReadonlyArray<T>,
   matchBy: AnimationMatchBy<T>,
-): ReadonlyMap<string | number, T> {
-  const prevMap = new Map<string | number, T>();
+): ReadonlyMap<string | number, T[]> {
+  const prevMap = new Map<string | number, T[]>();
   for (let i = 0; i < prevItems.length; i++) {
     const item = prevItems[i];
     if (item == null) continue;
     const key = matchBy(item, i);
-    if (key != null && !prevMap.has(key)) {
-      prevMap.set(key, item);
+    if (key != null) {
+      const bucket = prevMap.get(key);
+      if (bucket == null) {
+        prevMap.set(key, [item]);
+      } else {
+        bucket.push(item);
+      }
     }
   }
   return prevMap;
 }
@@
-  // Track which prev keys were matched
-  const matchedKeys = new Set<string | number>();
-
   const alignedPrevItems = nextItems.map((next, i) => {
     const key = matchBy(next, i);
     if (key != null) {
-      const prev = prevMap.get(key);
+      const prev = prevMap.get(key)?.shift();
       if (prev !== undefined) {
-        matchedKeys.add(key);
         return prev;
       }
     }
     return undefined;
   });
 
-  // Removed = prev items whose keys were not matched to any next item
   const removedPrevItems: T[] = [];
-  for (const [key, item] of prevMap) {
-    if (!matchedKeys.has(key)) {
-      removedPrevItems.push(item);
-    }
+  for (const [, bucket] of prevMap) {
+    removedPrevItems.push(...bucket);
   }

Also applies to: 176-199

🤖 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/animation/matchBy.ts` around lines 148 - 159, The current buildPrevKeyMap
(and the analogous code at 176-199) only keeps one item per match key so
duplicate keys cause one prev to match many nexts; change buildPrevKeyMap to
store all items for a key (use Map<string|number, T[]> and push each item
instead of skipping duplicates), then update the matching/consumption logic to
pull (e.g. shift/pop) a single item from the array when a key is matched and
delete the map entry when the array becomes empty so each prev is consumed at
most once.

Comment on lines +173 to +177
if (typeof head === 'function') {
head();
} else if (typeof head === 'object' || typeof head === 'string') {
this.listener?.(head as ReactSmoothStyle);
}

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Expect no matches after the fix
rg -nP --type=ts '\sas\s(?!const\b)' src/animation/ProgressAnimationManager.ts

Repository: recharts/recharts

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Expect no matches after the fix
rg -nP --type=ts '\sas\s(?!const\b)' src/animation/ProgressAnimationManager.ts

Repository: recharts/recharts

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Expect no matches after the fix
rg -nP --type=ts '\sas\s(?!const\b)' src/animation/ProgressAnimationManager.ts

Repository: recharts/recharts

Length of output: 114


🏁 Script executed:

#!/bin/bash
# Expect no matches after the fix
rg -nP --type=ts '\sas\s(?!const\b)' src/animation/ProgressAnimationManager.ts

Repository: recharts/recharts

Length of output: 114


Remove forbidden as ReactSmoothStyle type assertion in src/animation/ProgressAnimationManager.ts (line 176).

The code still uses this.listener?.(head as ReactSmoothStyle); narrow head (string/object) and pass it directly instead of asserting.

Proposed fix
-    if (typeof head === 'function') {
-      head();
-    } else if (typeof head === 'object' || typeof head === 'string') {
-      this.listener?.(head as ReactSmoothStyle);
-    }
+    if (typeof head === 'function') {
+      head();
+    } else if (typeof head === 'string') {
+      this.listener?.(head);
+    } else if (head != null && typeof head === 'object') {
+      this.listener?.(head);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof head === 'function') {
head();
} else if (typeof head === 'object' || typeof head === 'string') {
this.listener?.(head as ReactSmoothStyle);
}
if (typeof head === 'function') {
head();
} else if (typeof head === 'string') {
this.listener?.(head);
} else if (head != null && typeof head === 'object') {
this.listener?.(head);
}
🤖 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/animation/ProgressAnimationManager.ts` around lines 173 - 177, The code
in ProgressAnimationManager is using a forbidden type assertion (head as
ReactSmoothStyle); instead, narrow head with the existing typeof checks and pass
it directly to the listener. Replace the branch to call this.listener?.(head)
when typeof head === 'object' || typeof head === 'string' (remove the "as
ReactSmoothStyle" assertion) and if necessary update the listener signature/type
to accept string | object | ReactSmoothStyle so the call is type-safe without
casting.

Comment thread src/cartesian/Scatter.tsx
Comment on lines +640 to +644
const { points, isAnimationActive, animationBegin, animationDuration, animationEasing, animationInterpolateFn } =
props;

const { isAnimating, handleAnimationStart, handleAnimationEnd } = useAnimationCallbacks();
const layout = useCartesianChartLayout();

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wire the public animation callbacks through useAnimationCallbacks.

Scatter stops firing onAnimationStart and onAnimationEnd here because the hook is invoked without props.onAnimationStart / props.onAnimationEnd.

Suggested fix
-  const { isAnimating, handleAnimationStart, handleAnimationEnd } = useAnimationCallbacks();
+  const { isAnimating, handleAnimationStart, handleAnimationEnd } = useAnimationCallbacks(
+    props.onAnimationStart,
+    props.onAnimationEnd,
+  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { points, isAnimationActive, animationBegin, animationDuration, animationEasing, animationInterpolateFn } =
props;
const { isAnimating, handleAnimationStart, handleAnimationEnd } = useAnimationCallbacks();
const layout = useCartesianChartLayout();
const { points, isAnimationActive, animationBegin, animationDuration, animationEasing, animationInterpolateFn } =
props;
const { isAnimating, handleAnimationStart, handleAnimationEnd } = useAnimationCallbacks(
props.onAnimationStart,
props.onAnimationEnd,
);
const layout = useCartesianChartLayout();
🤖 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/cartesian/Scatter.tsx` around lines 640 - 644, The Scatter component
currently calls useAnimationCallbacks() without passing the public callbacks, so
its onAnimationStart/onAnimationEnd props are never invoked; update the hook
invocation in Scatter to forward props.onAnimationStart and props.onAnimationEnd
(e.g., call useAnimationCallbacks with an options object containing
onAnimationStart: props.onAnimationStart and onAnimationEnd:
props.onAnimationEnd) so that the returned handleAnimationStart and
handleAnimationEnd will call the public callbacks.

Comment thread src/polar/Pie.tsx Outdated
Comment on lines +917 to +949
const defaultPieAnimateItems: AnimationInterpolateFn<PieSectorDataItem, PolarLayout> = (items, t) => {
if (items == null) return [];
const stepData: PieSectorDataItem[] = [];
const firstNonRemoved = items.find(item => item.status !== 'removed');
let curAngle: number = firstNonRemoved ? firstNonRemoved.next.startAngle : 0;

items.forEach((item, index) => {
if (item.status === 'removed') return;
const paddingAngle = index > 0 ? get(item.next, 'paddingAngle', 0) : 0;

if (item.status === 'matched') {
const angle = interpolate(
item.prev.endAngle - item.prev.startAngle,
item.next.endAngle - item.next.startAngle,
t,
);
const latest = { ...item.next, startAngle: curAngle + paddingAngle, endAngle: curAngle + angle + paddingAngle };
stepData.push(latest);
curAngle = latest.endAngle;
} else {
// added
const deltaAngle = interpolate(0, item.next.endAngle - item.next.startAngle, t);
const latest = {
...item.next,
startAngle: curAngle + paddingAngle,
endAngle: curAngle + deltaAngle + paddingAngle,
};
stepData.push(latest);
curAngle = latest.endAngle;
}
});
return stepData;
};

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Base sector padding on emitted sectors, not source indices.

When an earlier item is removed, the first surviving sector can still have index > 0, so this code inserts a leading paddingAngle even though curAngle was already seeded from that sector's target start angle. That shifts the whole intermediate pie and produces an extra gap.

Suggested fix
-  items.forEach((item, index) => {
+  items.forEach(item => {
     if (item.status === 'removed') return;
-    const paddingAngle = index > 0 ? get(item.next, 'paddingAngle', 0) : 0;
+    const paddingAngle = stepData.length > 0 ? get(item.next, 'paddingAngle', 0) : 0;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const defaultPieAnimateItems: AnimationInterpolateFn<PieSectorDataItem, PolarLayout> = (items, t) => {
if (items == null) return [];
const stepData: PieSectorDataItem[] = [];
const firstNonRemoved = items.find(item => item.status !== 'removed');
let curAngle: number = firstNonRemoved ? firstNonRemoved.next.startAngle : 0;
items.forEach((item, index) => {
if (item.status === 'removed') return;
const paddingAngle = index > 0 ? get(item.next, 'paddingAngle', 0) : 0;
if (item.status === 'matched') {
const angle = interpolate(
item.prev.endAngle - item.prev.startAngle,
item.next.endAngle - item.next.startAngle,
t,
);
const latest = { ...item.next, startAngle: curAngle + paddingAngle, endAngle: curAngle + angle + paddingAngle };
stepData.push(latest);
curAngle = latest.endAngle;
} else {
// added
const deltaAngle = interpolate(0, item.next.endAngle - item.next.startAngle, t);
const latest = {
...item.next,
startAngle: curAngle + paddingAngle,
endAngle: curAngle + deltaAngle + paddingAngle,
};
stepData.push(latest);
curAngle = latest.endAngle;
}
});
return stepData;
};
const defaultPieAnimateItems: AnimationInterpolateFn<PieSectorDataItem, PolarLayout> = (items, t) => {
if (items == null) return [];
const stepData: PieSectorDataItem[] = [];
const firstNonRemoved = items.find(item => item.status !== 'removed');
let curAngle: number = firstNonRemoved ? firstNonRemoved.next.startAngle : 0;
items.forEach(item => {
if (item.status === 'removed') return;
const paddingAngle = stepData.length > 0 ? get(item.next, 'paddingAngle', 0) : 0;
if (item.status === 'matched') {
const angle = interpolate(
item.prev.endAngle - item.prev.startAngle,
item.next.endAngle - item.next.startAngle,
t,
);
const latest = { ...item.next, startAngle: curAngle + paddingAngle, endAngle: curAngle + angle + paddingAngle };
stepData.push(latest);
curAngle = latest.endAngle;
} else {
// added
const deltaAngle = interpolate(0, item.next.endAngle - item.next.startAngle, t);
const latest = {
...item.next,
startAngle: curAngle + paddingAngle,
endAngle: curAngle + deltaAngle + paddingAngle,
};
stepData.push(latest);
curAngle = latest.endAngle;
}
});
return stepData;
};
🤖 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/polar/Pie.tsx` around lines 917 - 949, The animation computes
paddingAngle using the original array index which causes an extra leading gap
when earlier items are removed; update defaultPieAnimateItems to compute
paddingAngle based on the emitted-sector sequence instead: introduce a local
emittedIndex (increment only for items where item.status !== 'removed') and use
emittedIndex > 0 to decide whether to apply get(item.next, 'paddingAngle', 0),
keeping curAngle seeded from firstNonRemoved.next.startAngle and updating
emittedIndex each time you push a sector so padding is applied relative to
emitted sectors rather than source indices.

Comment thread src/polar/Pie.tsx
Comment thread src/polar/RadialBar.tsx Outdated
Comment on lines +206 to +224
const defaultRadialBarAnimateItems: AnimationInterpolateFn<RadialBarDataItem, PolarLayout> = (items, t) => {
if (items == null) return [];
if (t === 1) {
return items.flatMap(item => (item.status === 'removed' ? [] : [item.next]));
}
return items.flatMap(item => {
if (item.status === 'removed') return [];
if (item.status === 'matched') {
return [
{
...item.next,
startAngle: interpolate(item.prev.startAngle, item.next.startAngle, t),
endAngle: interpolate(item.prev.endAngle, item.next.endAngle, t),
},
];
}
// added
return [{ ...item.next, endAngle: interpolate(item.next.startAngle, item.next.endAngle, t) }];
});

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle centric RadialBar animations in the default interpolator.

This interpolator always animates startAngle/endAngle, but centric radial bars encode value changes in innerRadius/outerRadius instead. In that layout, matched bars jump to the final radius and added bars animate with the wrong geometry. Branch on layout and interpolate radii for the non-radial path.

🤖 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/polar/RadialBar.tsx` around lines 206 - 224, The
defaultRadialBarAnimateItems interpolator currently always tweens
startAngle/endAngle, causing centric layouts (which encode value changes via
innerRadius/outerRadius) to jump or animate incorrectly; update
defaultRadialBarAnimateItems to branch on the PolarLayout.layout value: if
layout === 'radial' keep the existing startAngle/endAngle interpolation for
matched and added items, otherwise (centric layout) interpolate innerRadius and
outerRadius for matched items (use interpolate(item.prev.innerRadius,
item.next.innerRadius, t) and same for outerRadius) and for added items set next
with innerRadius interpolated from next.innerRadius to next.outerRadius as
appropriate (or interpolate innerRadius from 0 to next.innerRadius / outerRadius
as your design requires), and on t === 1 continue returning item.next unchanged;
ensure removed items remain filtered out as before and reference the
defaultRadialBarAnimateItems symbol to implement the branch.

Comment thread src/polar/RadialBar.tsx
@PavelVanecek

Copy link
Copy Markdown
Collaborator Author

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/polar/Radar.tsx (2)

90-101: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the animationInterpolateFn docs to match the new callback contract.

This JSDoc still describes the removed (prevItems, nextItems, t) signature, but the public type now receives (items, t, layout). Shipping this will document the wrong API for custom interpolators. As per coding guidelines: "JSDoc comments and TypeScript definitions in source files must be modified to update auto-generated API documentation."

🤖 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/polar/Radar.tsx` around lines 90 - 101, The JSDoc for the
animationInterpolateFn prop is out of sync with the new callback signature:
update the comment on animationInterpolateFn (type
AnimationInterpolateFn<RadarPoint, PolarLayout>) to describe the new parameters
(items, t, layout) instead of (prevItems, nextItems, t); specifically change the
`@param` tags to document "items" (current items to interpolate), "t" (normalized
time 0..1) and "layout" (PolarLayout), and ensure the return description still
notes interpolated RadarPoint[] for time t so generated API docs match the
TypeScript contract.

527-539: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a stable animation trigger instead of the whole props object.

animationInput={props} will restart Radar transitions whenever the component rerenders or any non-geometry prop changes. Tie this to points/baseLinePoints (or a memoized geometry-only subset) so animations only restart when the polygon actually changes.

🤖 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/polar/Radar.tsx` around lines 527 - 539, The animationInput currently
uses the entire props object (animationInput={props}), causing animations to
restart on any render; change it to use a stable geometry-specific value such as
the computed points or baseline points (e.g., animationInput={points} or
animationInput={baseLinePoints}) or create a memoized geometry-only subset via
useMemo and pass that (reference animationInput, props, points, baseLinePoints,
and any geometry compute functions) so transitions only restart when the polygon
coordinates actually change.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/cartesian/LineDrawShape.tsx`:
- Around line 114-123: The LineDrawShapeProps currently forces pathRef to be a
required RefObject, which narrows the public CurveProps contract; change the
prop to be optional and accept the broader Ref type by updating the pathRef
declaration in LineDrawShapeProps from "pathRef: RefObject<SVGPathElement>" to
an optional "pathRef?: Ref<SVGPathElement>" (or equivalent using the project's
Ref import), and ensure any internal reading still uses the existing safe access
(pathRef?.current ?? null); update any related type annotations in the
LineDrawShape component if necessary to match the optional Ref type.

---

Outside diff comments:
In `@src/polar/Radar.tsx`:
- Around line 90-101: The JSDoc for the animationInterpolateFn prop is out of
sync with the new callback signature: update the comment on
animationInterpolateFn (type AnimationInterpolateFn<RadarPoint, PolarLayout>) to
describe the new parameters (items, t, layout) instead of (prevItems, nextItems,
t); specifically change the `@param` tags to document "items" (current items to
interpolate), "t" (normalized time 0..1) and "layout" (PolarLayout), and ensure
the return description still notes interpolated RadarPoint[] for time t so
generated API docs match the TypeScript contract.
- Around line 527-539: The animationInput currently uses the entire props object
(animationInput={props}), causing animations to restart on any render; change it
to use a stable geometry-specific value such as the computed points or baseline
points (e.g., animationInput={points} or animationInput={baseLinePoints}) or
create a memoized geometry-only subset via useMemo and pass that (reference
animationInput, props, points, baseLinePoints, and any geometry compute
functions) so transitions only restart when the polygon coordinates actually
change.
🪄 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: c687a710-0218-454b-9fa9-f43626586a8a

📥 Commits

Reviewing files that changed from the base of the PR and between 66a71f7 and 9214614.

📒 Files selected for processing (10)
  • src/animation/AnimatedItems.tsx
  • src/cartesian/Area.tsx
  • src/cartesian/Bar.tsx
  • src/cartesian/Funnel.tsx
  • src/cartesian/Line.tsx
  • src/cartesian/LineDrawShape.tsx
  • src/cartesian/Scatter.tsx
  • src/polar/Pie.tsx
  • src/polar/Radar.tsx
  • src/polar/RadialBar.tsx

Comment on lines +114 to +123
export type LineDrawShapeProps = Omit<CurveProps, 'pathRef'> &
ShapeAnimationProps & {
/**
* CurveProps use Ref which includes the object plus callback.
* Here in LineDrawShape we have to read from it so we only allow the object - but we keep `Curve.ref` the same
* for backwards compatibility.
* LineDrawShape is new since 3.9 so there's no backwards to go.
*/
pathRef: RefObject<SVGPathElement>;
/**

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print the relevant section of LineDrawShape.tsx
FILE="src/cartesian/LineDrawShape.tsx"
if [ ! -f "$FILE" ]; then
  echo "Missing $FILE"
  exit 1
fi

echo "---- $FILE (lines 90-210) ----"
nl -ba "$FILE" | sed -n '90,210p'

# Find where LineDrawShape reads pathRef
echo
echo "---- grep pathRef usage ----"
rg -n "pathRef" "$FILE"

# Find the CurveProps import source and (if present in repo) the CurveProps type definition
echo
echo "---- imports from recharts (for CurveProps) ----"
rg -n "CurveProps" "$FILE" || true

# Search repo for LineDrawShapeProps definition references
echo
echo "---- usages of LineDrawShapeProps / shape props ----"
rg -n "LineDrawShapeProps" src || true

Repository: recharts/recharts

Length of output: 180


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/cartesian/LineDrawShape.tsx"

if [ ! -f "$FILE" ]; then
  echo "Missing $FILE"
  exit 1
fi

echo "---- $FILE (lines 90-210) ----"
awk 'NR>=90 && NR<=210 {printf "%d:%s\n", NR, $0}' "$FILE"

echo
echo "---- rg: pathRef in $FILE ----"
rg -n "pathRef" "$FILE"

echo
echo "---- rg: CurveProps in $FILE ----"
rg -n "CurveProps" "$FILE" || true

echo
echo "---- Find CurveProps definition (pathRef typing) ----"
# Prefer local definition; if not present, show likely type source locations via grep
rg -n "export (type|interface) CurveProps" src || true
rg -n "pathRef.*CurveProps|pathRef.*SVGPathElement" src || true

echo
echo "---- rg: LineDrawShapeProps usages ----"
rg -n "LineDrawShapeProps" src || true

Repository: recharts/recharts

Length of output: 5769


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- src/shape/Curve.tsx (around pathRef) ----"
FILE="src/shape/Curve.tsx"
awk 'NR>=110 && NR<=190 {printf "%d:%s\n", NR, $0}' "$FILE"

echo
echo "---- src/cartesian/Line.tsx (constructing curveProps) ----"
FILE="src/cartesian/Line.tsx"
awk 'NR>=540 && NR<=660 {printf "%d:%s\n", NR, $0}' "$FILE"

echo
echo "---- src/cartesian/Line.tsx (where Shape is rendered / shape prop typing) ----"
awk 'NR>=250 && NR<=330 {printf "%d:%s\n", NR, $0}' "$FILE"

Repository: recharts/recharts

Length of output: 10068


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="src/cartesian/Line.tsx"
awk 'NR>=670 && NR<=730 {printf "%d:%s\n", NR, $0}' "$FILE"

Repository: recharts/recharts

Length of output: 2352


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- usages: shape={LineDrawShape} ----"
rg -n "shape=\{LineDrawShape\}" -S . || true

echo
echo "---- usages: <LineDrawShape ----"
rg -n "<LineDrawShape\b" -S . || true

echo
echo "---- index export for LineDrawShapeProps ----"
rg -n "export type \{ LineDrawShapeProps \}" -S src/index.ts && awk 'NR>=140 && NR<=210 {printf "%d:%s\n", NR, $0}' src/index.ts

Repository: recharts/recharts

Length of output: 3041


Make pathRef optional in LineDrawShapeProps to avoid narrowing CurveProps’ public ref contract.

CurveProps defines pathRef?: Ref<SVGPathElement>, but LineDrawShapeProps currently requires pathRef: RefObject<SVGPathElement> (also restricting it to object refs). LineDrawShape already tolerates a missing ref at runtime via pathRef?.current ?? null, so the exported type shouldn’t require it.

Suggested fix
 export type LineDrawShapeProps = Omit<CurveProps, 'pathRef'> &
   ShapeAnimationProps & {
-    pathRef: RefObject<SVGPathElement>;
+    pathRef?: RefObject<SVGPathElement> | null;
     /**
      * The computed visible length of the SVG path during entrance animation, in pixels.
      * - When a number: the shape should apply stroke-dasharray to reveal exactly this many pixels.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export type LineDrawShapeProps = Omit<CurveProps, 'pathRef'> &
ShapeAnimationProps & {
/**
* CurveProps use Ref which includes the object plus callback.
* Here in LineDrawShape we have to read from it so we only allow the object - but we keep `Curve.ref` the same
* for backwards compatibility.
* LineDrawShape is new since 3.9 so there's no backwards to go.
*/
pathRef: RefObject<SVGPathElement>;
/**
export type LineDrawShapeProps = Omit<CurveProps, 'pathRef'> &
ShapeAnimationProps & {
/**
* CurveProps use Ref which includes the object plus callback.
* Here in LineDrawShape we have to read from it so we only allow the object - but we keep `Curve.ref` the same
* for backwards compatibility.
* LineDrawShape is new since 3.9 so there's no backwards to go.
*/
pathRef?: RefObject<SVGPathElement> | null;
/**
🤖 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/cartesian/LineDrawShape.tsx` around lines 114 - 123, The
LineDrawShapeProps currently forces pathRef to be a required RefObject, which
narrows the public CurveProps contract; change the prop to be optional and
accept the broader Ref type by updating the pathRef declaration in
LineDrawShapeProps from "pathRef: RefObject<SVGPathElement>" to an optional
"pathRef?: Ref<SVGPathElement>" (or equivalent using the project's Ref import),
and ensure any internal reading still uses the existing safe access
(pathRef?.current ?? null); update any related type annotations in the
LineDrawShape component if necessary to match the optional Ref type.

@github-actions

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

@nikolasrieble nikolasrieble 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.

I do not want to block this work, as I see the value and the velocity.

That being said, I propose to split this PR up.
If only because as maintainers, we are a role model to the community. I do not see how 6000 LOC PRs are reviewable - no matter who created them.

I started, left a few first comments, and then realized that I can not realistically review this PR with the attention it deserves.

The value proposition is clear, but there are a few things to consider:

Documentation inconsistency

When do we use which props in the comments? I see @since is used sometimes and sometimes not. It is not clear to me by which criteria

Duplicate comments

I have only found one in my time boxed review, but think this should be cleaned up, see https://github.com/recharts/recharts/pull/7215/changes#r3294375584

PR Seams

The PR adds a core new package for animations, and then it also integrates it into many charts, and introduces a fully standalone ProgressManager.

Actionable feedback:

  • Please consider splitting this PR up into:
    • Core animation package (matchBy.ts - pure utils)
    • AnimatedItems and integration into 1 chart only (BarChart), allowing a focussed review of the Items and their integration
    • Integrate into all Cartesian Charts
    • Integrate into all Polar Chart
    • ProgressManager

Now importantly, in the age of LLMs such a split up do not cost much time nor effort, but a few tokens. That is why in the age of AI, I believe we should double down on best practice, and require smaller steps.

Comment thread scripts/treeshaking-groups/Scatter.ts
Comment thread src/animation/AnimatedItems.tsx Outdated
Comment thread src/animation/AnimatedItems.tsx
*
* @param onAnimationStart optional callback to call when animation starts
* @param onAnimationEnd optional callback to call when animation ends
*/

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.

❓ I see that below in AnimationInterpolateFn, we also document more metadata (i.e. @returns and @SInCE). Why do we not do that here as well?

>
{(t: number) => {
const isEntrance = rawPrevItems == null;
// TODO performance; how does the matching work with 10k items? Can we skip interpolation and just jump to final state after some threshold?

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.

❓ Should we create a separate issue to follow up? Possibly even link to it here?

getTimeoutController(): TimeoutController;
/**
* When true, this manager controls animation progress manually (e.g. via a scrubber).
* CSS-transition-based animations should fall back to JS interpolation in this mode,

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.

❓ Should? Do we not know? (I certainly do not 😅 )

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is entry point for the devtools package and other animation libraries, I plan to look closer into this part before releasing

Comment thread src/util/types.ts Outdated

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/cartesian/Line.tsx (2)

160-167: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix the animationInterpolateFn parameter docs.

The callback no longer receives separate prevItems / nextItems; it receives the matched animation items list. The current JSDoc will publish the wrong contract.

As per coding guidelines, "Update TypeScript interfaces and JSDoc comments in source code to automatically update API documentation".

🤖 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/cartesian/Line.tsx` around lines 160 - 167, Update the JSDoc for the
animationInterpolateFn parameter on Line (in src/cartesian/Line.tsx) to reflect
the new contract: it no longer receives prevItems and nextItems separately but
instead receives a single list of matched animation items (the "animation items
list") and the normalized animationElapsedTime (0..1), and must return the
interpolated items for the current frame; update the param descriptions and
return description accordingly so the published API docs match the actual
animationInterpolateFn signature.

372-413: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make added/removed point extrapolation layout-aware.

The default interpolator always computes shift on x and only animates x for added/removed points. That breaks layout="vertical" transitions, where appended/removed categories move along y, so the slide animation will come from the wrong direction or not happen at all.

Use the layout argument to choose the category axis before computing shift and entry/exit coordinates.

🤖 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/cartesian/Line.tsx` around lines 372 - 413, The defaultLineAnimateItems
interpolator currently always uses the x-axis for extrapolation/shift which
breaks layout="vertical"; update defaultLineAnimateItems to use the provided
layout parameter to pick the category axis (x for horizontal, y for vertical)
when calling averageShift and when computing entry/exit coordinates and
interpolation for added/removed points (e.g., compute a shiftCoord via
averageShift(items, layout) or call averageShift on the chosen axis, derive
entryCoord/exitCoord from item.next[itemAxis] or item.prev[itemAxis], and
interpolate that coordinate instead of always item.x), while keeping matched
items interpolation the same; refer to defaultLineAnimateItems, averageShift,
LinePointItem, and the layout argument to locate where to branch on layout and
apply the correct coordinate.
src/cartesian/Scatter.tsx (2)

323-329: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document the actual animationInterpolateFn input shape.

This JSDoc still describes separate prevItems / nextItems, but the callback now receives the matched animation items collection. Leaving it as-is will generate misleading API docs for JS consumers implementing custom interpolators.

As per coding guidelines, "Update TypeScript interfaces and JSDoc comments in source code to automatically update API documentation".

🤖 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/cartesian/Scatter.tsx` around lines 323 - 329, The JSDoc for
animationInterpolateFn in Scatter.tsx is incorrect: it still documents separate
prevItems/nextItems but the callback actually receives a collection of matched
animation items; update the comment and the associated TypeScript interface so
the API docs reflect the actual input shape (e.g., a single array/object of
MatchedAnimationItem entries with fields like id, from, to) and describe the
structure and types of each matched item and the animationElapsedTime parameter;
reference the animationInterpolateFn name and the matched animation item fields
(e.g., id, from, to) so consumers implementing custom interpolators know the
exact input shape.

611-633: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Recompute derived bounds while animating scatter points.

defaultScatterAnimateItems updates cx/cy/size, but x, y, width, and height stay pinned to item.next. Custom shape / activeShape renderers that use bounds will jump to the final geometry on the first frame instead of animating with the point.

Suggested direction
 const defaultScatterAnimateItems: AnimationInterpolateFn<ScatterPointItem, CartesianLayout> = (
   items,
   animationElapsedTime,
 ) => {
+  const toRadius = (size: number) => Math.sqrt(Math.max(size, 0) / Math.PI);
+
   if (items == null) return [];
   if (animationElapsedTime === 1) {
     return items.flatMap(item => (item.status === 'removed' ? [] : [item.next]));
   }
   return items.flatMap(item => {
     if (item.status === 'removed') return [];
     if (item.status === 'matched') {
+      const cx = item.next.cx == null ? undefined : interpolate(item.prev.cx, item.next.cx, animationElapsedTime);
+      const cy = item.next.cy == null ? undefined : interpolate(item.prev.cy, item.next.cy, animationElapsedTime);
+      const size = interpolate(item.prev.size, item.next.size, animationElapsedTime);
+      const radius = toRadius(size);
       return [
         {
           ...item.next,
-          cx: item.next.cx == null ? undefined : interpolate(item.prev.cx, item.next.cx, animationElapsedTime),
-          cy: item.next.cy == null ? undefined : interpolate(item.prev.cy, item.next.cy, animationElapsedTime),
-          size: interpolate(item.prev.size, item.next.size, animationElapsedTime),
+          cx,
+          cy,
+          size,
+          x: cx == null ? undefined : cx - radius,
+          y: cy == null ? undefined : cy - radius,
+          width: radius * 2,
+          height: radius * 2,
         },
       ];
     }
-    return [{ ...item.next, size: interpolate(0, item.next.size, animationElapsedTime) }];
+    const size = interpolate(0, item.next.size, animationElapsedTime);
+    const radius = toRadius(size);
+    return [
+      {
+        ...item.next,
+        size,
+        x: item.next.cx == null ? undefined : item.next.cx - radius,
+        y: item.next.cy == null ? undefined : item.next.cy - radius,
+        width: radius * 2,
+        height: radius * 2,
+      },
+    ];
   });
 };
♻️ Duplicate comments (1)
src/cartesian/Scatter.tsx (1)

653-655: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a stable animation trigger.

animationInput={props} still makes unrelated rerenders look like brand-new animations because this object is rebuilt on each render. Passing points or a memoized geometry-only subset keeps scatter transitions tied to actual animated data changes.

Minimal fix
-        animationInput={props}
+        animationInput={points}
🤖 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/cartesian/Scatter.tsx` around lines 653 - 655, The animationInput
currently uses the entire props object (animationInput={props}), causing
unrelated renders to retrigger animations; change AnimatedItems to receive a
stable, animation-specific key such as animationInput={points} (the points array
passed into the Scatter component) or a memoized geometry-only subset computed
with useMemo (e.g., memoize [{x,y,id,...}] from props.points) so rerenders only
animate when actual point geometry changes; update the Scatter component to
compute that memoized value and pass it to AnimatedItems.animationInput while
keeping animationIdPrefix the same.
🧹 Nitpick comments (1)
test/cartesian/Line.animation.spec.tsx (1)

1965-1970: ⚡ Quick win

Cover visibleLength in the custom-shape contract test.

The new custom-shape API also forwards visibleLength, but this test only guards the renamed animationElapsedTime. Adding one assertion here would keep the full shape contract covered end-to-end.

As per coding guidelines, "Aim for 100% unit test code coverage when writing new code".

Also applies to: 1992-2000

🤖 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/cartesian/Line.animation.spec.tsx` around lines 1965 - 1970, The test
for CustomLineShape only asserts the forwarded animationElapsedTime prop; update
the custom-shape contract test(s) that use CustomLineShape to also assert that
visibleLength is forwarded (e.g., check the rendered element has a
data-visible-length or equivalent attribute matching the visibleLength value
passed by the Line component). Locate the test block that renders
CustomLineShape (function CustomLineShape) and add an assertion mirroring the
animationElapsedTime check to verify the visibleLength prop is present and
correct; apply the same addition to the similar test around the other occurrence
noted (the 1992-2000 region).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/animation/easing.ts`:
- Around line 5-6: evaluatePolynomial, cubicBezier and derivativeCubicBezier are
allocating per-frame (evaluatePolynomial uses params.map/reduce and the bezier
functions rebuild coefficient arrays each call); refactor evaluatePolynomial to
compute the polynomial without allocations (use an in-place for-loop or Horner's
method to accumulate the sum) and change cubicBezier and derivativeCubicBezier
to precompute and capture their coefficient arrays once (e.g., build the
coefficients when the bezier is created and return a closure that calls
evaluatePolynomial with that stable array) so no new arrays are created on each
evaluation; update references to evaluatePolynomial(params, t) accordingly.

In `@src/cartesian/Line.tsx`:
- Around line 267-271: Update the JSDoc for the Line component's `shape` prop
(and any related shape docs) to document the `visibleLength` prop forwarded by
`StaticCurve` into `shapeProps` in addition to `animationElapsedTime`,
`isAnimating`, and `isEntrance`; mention its type (number), when it is provided
(during entrance/exit animations), and how custom shapes should use it to render
partial strokes (e.g., to implement stroke-dashoffset/dasharray effects). Also
ensure any TypeScript interface/type for `shapeProps` (referencing `StaticCurve`
and `shapeProps`) includes `visibleLength: number | undefined` so the generated
API docs include the new field.

---

Outside diff comments:
In `@src/cartesian/Line.tsx`:
- Around line 160-167: Update the JSDoc for the animationInterpolateFn parameter
on Line (in src/cartesian/Line.tsx) to reflect the new contract: it no longer
receives prevItems and nextItems separately but instead receives a single list
of matched animation items (the "animation items list") and the normalized
animationElapsedTime (0..1), and must return the interpolated items for the
current frame; update the param descriptions and return description accordingly
so the published API docs match the actual animationInterpolateFn signature.
- Around line 372-413: The defaultLineAnimateItems interpolator currently always
uses the x-axis for extrapolation/shift which breaks layout="vertical"; update
defaultLineAnimateItems to use the provided layout parameter to pick the
category axis (x for horizontal, y for vertical) when calling averageShift and
when computing entry/exit coordinates and interpolation for added/removed points
(e.g., compute a shiftCoord via averageShift(items, layout) or call averageShift
on the chosen axis, derive entryCoord/exitCoord from item.next[itemAxis] or
item.prev[itemAxis], and interpolate that coordinate instead of always item.x),
while keeping matched items interpolation the same; refer to
defaultLineAnimateItems, averageShift, LinePointItem, and the layout argument to
locate where to branch on layout and apply the correct coordinate.

In `@src/cartesian/Scatter.tsx`:
- Around line 323-329: The JSDoc for animationInterpolateFn in Scatter.tsx is
incorrect: it still documents separate prevItems/nextItems but the callback
actually receives a collection of matched animation items; update the comment
and the associated TypeScript interface so the API docs reflect the actual input
shape (e.g., a single array/object of MatchedAnimationItem entries with fields
like id, from, to) and describe the structure and types of each matched item and
the animationElapsedTime parameter; reference the animationInterpolateFn name
and the matched animation item fields (e.g., id, from, to) so consumers
implementing custom interpolators know the exact input shape.

---

Duplicate comments:
In `@src/cartesian/Scatter.tsx`:
- Around line 653-655: The animationInput currently uses the entire props object
(animationInput={props}), causing unrelated renders to retrigger animations;
change AnimatedItems to receive a stable, animation-specific key such as
animationInput={points} (the points array passed into the Scatter component) or
a memoized geometry-only subset computed with useMemo (e.g., memoize
[{x,y,id,...}] from props.points) so rerenders only animate when actual point
geometry changes; update the Scatter component to compute that memoized value
and pass it to AnimatedItems.animationInput while keeping animationIdPrefix the
same.

---

Nitpick comments:
In `@test/cartesian/Line.animation.spec.tsx`:
- Around line 1965-1970: The test for CustomLineShape only asserts the forwarded
animationElapsedTime prop; update the custom-shape contract test(s) that use
CustomLineShape to also assert that visibleLength is forwarded (e.g., check the
rendered element has a data-visible-length or equivalent attribute matching the
visibleLength value passed by the Line component). Locate the test block that
renders CustomLineShape (function CustomLineShape) and add an assertion
mirroring the animationElapsedTime check to verify the visibleLength prop is
present and correct; apply the same addition to the similar test around the
other occurrence noted (the 1992-2000 region).
🪄 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: fd1ff0f3-b59e-4675-849f-de651258ed91

📥 Commits

Reviewing files that changed from the base of the PR and between 9214614 and b6f87bd.

📒 Files selected for processing (41)
  • src/animation/AnimatedItems.tsx
  • src/animation/AnimationProgressProvider.tsx
  • src/animation/CSSTransitionAnimate.tsx
  • src/animation/JavascriptAnimate.tsx
  • src/animation/configUpdate.ts
  • src/animation/easing.ts
  • src/animation/useAnimationStartSnapshot.ts
  • src/animation/util.ts
  • src/cartesian/Area.tsx
  • src/cartesian/AreaRevealShape.tsx
  • src/cartesian/Bar.tsx
  • src/cartesian/Funnel.tsx
  • src/cartesian/Line.tsx
  • src/cartesian/LineDrawShape.tsx
  • src/cartesian/Scatter.tsx
  • src/cartesian/useAnimatedLineLength.ts
  • src/chart/Sankey.tsx
  • src/polar/Pie.tsx
  • src/polar/Radar.tsx
  • src/polar/RadialBar.tsx
  • src/shape/Rectangle.tsx
  • src/shape/Trapezoid.tsx
  • src/util/DataUtils.ts
  • src/util/types.ts
  • test/cartesian/Area.animation.spec.tsx
  • test/cartesian/Bar.animation.spec.tsx
  • test/cartesian/Funnel.animation.spec.tsx
  • test/cartesian/Line.animation.spec.tsx
  • test/cartesian/Scatter.animation.spec.tsx
  • test/cartesian/useAnimatedLineLength.spec.ts
  • test/component/Legend.spec.tsx
  • test/polar/Pie/Pie.animation.spec.tsx
  • test/polar/Pie/Pie.spec.tsx
  • test/polar/Radar.animation.spec.tsx
  • test/polar/RadialBar/RadialBar.animation.spec.tsx
  • www/src/docs/exampleComponents/AreaChart/AreaChartCustomAnimationExample/index.tsx
  • www/src/docs/exampleComponents/AreaChart/RangeAreaChartCustomAnimation.tsx
  • www/src/docs/exampleComponents/BarChart/AnimatedBarTimeSeriesExample.tsx
  • www/src/docs/exampleComponents/LineChart/LineChartCustomShapeExample/index.tsx
  • www/src/docs/exampleComponents/RadarChart/RangeRadarChartCustomAnimation.tsx
  • www/src/docs/exampleComponents/ScatterChart/CustomAnimationExample.tsx
✅ Files skipped from review due to trivial changes (3)
  • src/chart/Sankey.tsx
  • src/shape/Trapezoid.tsx
  • src/animation/configUpdate.ts
🚧 Files skipped from review as they are similar to previous changes (12)
  • src/animation/CSSTransitionAnimate.tsx
  • src/cartesian/useAnimatedLineLength.ts
  • src/cartesian/LineDrawShape.tsx
  • src/animation/AnimationProgressProvider.tsx
  • src/cartesian/AreaRevealShape.tsx
  • src/polar/RadialBar.tsx
  • src/animation/AnimatedItems.tsx
  • src/polar/Pie.tsx
  • src/cartesian/Bar.tsx
  • src/polar/Radar.tsx
  • src/cartesian/Funnel.tsx
  • src/cartesian/Area.tsx

Comment thread src/animation/easing.ts
Comment on lines +5 to +6
const evaluatePolynomial = (params: ReadonlyArray<number>, animationElapsedTime: number) =>
params.map((param, i) => param * animationElapsedTime ** i).reduce((pre, curr) => pre + curr);

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid per-frame allocations in Bezier evaluation.

cubicBezier/derivativeCubicBezier now recreate coefficient arrays on each evaluation, and evaluatePolynomial allocates via map on every call. This runs in the animation hot path and can cause unnecessary GC churn.

⚡ Suggested allocation-free refactor
-const evaluatePolynomial = (params: ReadonlyArray<number>, animationElapsedTime: number) =>
-  params.map((param, i) => param * animationElapsedTime ** i).reduce((pre, curr) => pre + curr);
+const evaluatePolynomial = (params: ReadonlyArray<number>, animationElapsedTime: number): number => {
+  let result = 0;
+  for (let i = 0; i < params.length; i += 1) {
+    result += params[i] * animationElapsedTime ** i;
+  }
+  return result;
+};

-const cubicBezier = (c1: number, c2: number) => (animationElapsedTime: number) => {
-  const params = cubicBezierFactor(c1, c2);
-
-  return evaluatePolynomial(params, animationElapsedTime);
-};
+const cubicBezier = (c1: number, c2: number) => {
+  const params = cubicBezierFactor(c1, c2);
+  return (animationElapsedTime: number) => evaluatePolynomial(params, animationElapsedTime);
+};

-const derivativeCubicBezier = (c1: number, c2: number) => (animationElapsedTime: number) => {
-  const params = cubicBezierFactor(c1, c2);
-  const newParams = [...params.map((param, i) => param * i).slice(1), 0];
-
-  return evaluatePolynomial(newParams, animationElapsedTime);
-};
+const derivativeCubicBezier = (c1: number, c2: number) => {
+  const params = cubicBezierFactor(c1, c2);
+  const derivativeParams = [params[1], params[2] * 2, params[3] * 3, 0] as const;
+  return (animationElapsedTime: number) => evaluatePolynomial(derivativeParams, animationElapsedTime);
+};

Also applies to: 8-18

🤖 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/animation/easing.ts` around lines 5 - 6, evaluatePolynomial, cubicBezier
and derivativeCubicBezier are allocating per-frame (evaluatePolynomial uses
params.map/reduce and the bezier functions rebuild coefficient arrays each
call); refactor evaluatePolynomial to compute the polynomial without allocations
(use an in-place for-loop or Horner's method to accumulate the sum) and change
cubicBezier and derivativeCubicBezier to precompute and capture their
coefficient arrays once (e.g., build the coefficients when the bezier is created
and return a closure that calls evaluatePolynomial with that stable array) so no
new arrays are created on each evaluation; update references to
evaluatePolynomial(params, t) accordingly.

Comment thread src/cartesian/Line.tsx
Comment on lines 267 to +271
* If set a ReactElement, the shape of line can be customized.
* If set a function, the function will be called to render customized shape.
*
* During animations the shape receives additional props: `animationElapsedTime`, `isAnimating`, and `isEntrance`.
* When a custom shape is provided, the built-in stroke-dasharray entrance animation is skipped.

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document visibleLength on custom line shapes.

StaticCurve now forwards visibleLength into shapeProps, but the public shape docs only mention animationElapsedTime, isAnimating, and isEntrance. That leaves the generated API docs incomplete for custom shape authors.

As per coding guidelines, "Update TypeScript interfaces and JSDoc comments in source code to automatically update API documentation".

🤖 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/cartesian/Line.tsx` around lines 267 - 271, Update the JSDoc for the Line
component's `shape` prop (and any related shape docs) to document the
`visibleLength` prop forwarded by `StaticCurve` into `shapeProps` in addition to
`animationElapsedTime`, `isAnimating`, and `isEntrance`; mention its type
(number), when it is provided (during entrance/exit animations), and how custom
shapes should use it to render partial strokes (e.g., to implement
stroke-dashoffset/dasharray effects). Also ensure any TypeScript interface/type
for `shapeProps` (referencing `StaticCurve` and `shapeProps`) includes
`visibleLength: number | undefined` so the generated API docs include the new
field.

@github-actions

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

# Conflicts:
#	test-vr/__snapshots__/tests/www/dark-mode.spec-vr.tsx-snapshots/dark-mode-examples-list-1-chromium-linux.png
#	test-vr/__snapshots__/tests/www/dark-mode.spec-vr.tsx-snapshots/dark-mode-examples-list-1-firefox-linux.png
#	test-vr/__snapshots__/tests/www/dark-mode.spec-vr.tsx-snapshots/dark-mode-examples-list-1-webkit-linux.png
@github-actions

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

1 similar comment
@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Staging Deployment Details

These deployments will remain available for 30 days.

To update snapshots: Comment /update-snapshots on this PR to automatically update the baseline screenshots.

@PavelVanecek
PavelVanecek merged commit 95fa9a8 into main Jun 3, 2026
114 of 117 checks passed
@PavelVanecek
PavelVanecek deleted the animation-new branch June 3, 2026 12:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

4 participants