New animation props#7215
Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughRefactors 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. ChangesCore Animation Infrastructure
Shape Animation Props and Implementations
Chart Animation Refactors
Public API, Docs, Tests, and Examples
Estimated code review effort: 🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels: Suggested reviewers:
✨ Finishing Touches🧪 Generate unit tests (beta)
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
Bundle ReportChanges will increase total bundle size by 133.6kB (2.44%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: recharts/bundle-treeshaking-cartesianAssets Changed:
view changes for bundle: recharts/bundle-cjsAssets Changed:
view changes for bundle: recharts/bundle-es6Assets Changed:
view changes for bundle: recharts/bundle-umdAssets Changed:
view changes for bundle: recharts/bundle-treeshaking-polarAssets Changed:
view changes for bundle: recharts/bundle-treeshaking-sankeyAssets Changed:
view changes for bundle: recharts/bundle-treeshaking-treemapAssets Changed:
|
There was a problem hiding this comment.
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, anduseAnimationProgressweakens 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 pastt=1.The
timingUpdatefunction schedules itself recursively viatc.setTimeout(timingUpdate)on every frame, even aftertreaches 1. While the animation manager'sstart()will eventually trigger cleanup afterduration, the interpolation loop will keep running and re-settingstyleto 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. WhileAnimatedItemsmay 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.lengthis 0,countwill be 0 andindex / countresults inNaN. Whilet === 1early return at line 26 handles the final state, if called with emptynextItemsbefore 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
📒 Files selected for processing (30)
omnidoc/readProject.spec.tsomnidoc/verifyExamples.spec.tsscripts/snapshots/es6Files.txtscripts/snapshots/libFiles.txtscripts/snapshots/typesFiles.txtsrc/animation/AnimatedItems.tsxsrc/animation/AnimationManager.tssrc/animation/AnimationProgressProvider.tsxsrc/animation/CSSTransitionAnimate.tsxsrc/animation/ManualTimeoutController.tssrc/animation/ProgressAnimationManager.tssrc/animation/matchBy.tssrc/animation/util.tssrc/cartesian/Area.tsxsrc/cartesian/Bar.tsxsrc/cartesian/Funnel.tsxsrc/cartesian/Line.tsxsrc/cartesian/Scatter.tsxsrc/index.tssrc/polar/Pie.tsxsrc/polar/Radar.tsxsrc/polar/RadialBar.tsxtest/animation/ManualTimeoutController.spec.tstest/animation/ProgressAnimationManager.spec.tstest/animation/matchBy.spec.tstest/animation/util.spec.tswww/src/components/GuideView/Animations/CustomAnimationExample.tsxwww/src/components/GuideView/Animations/MatchingExample.tsxwww/src/components/GuideView/Animations/index.tsxwww/src/docs/apiCates.ts
| export type AnimationInterpolateFn<T> = ( | ||
| prevItems: ReadonlyArray<T> | null, | ||
| nextItems: ReadonlyArray<T>, | ||
| t: number, | ||
| ) => ReadonlyArray<T>; |
There was a problem hiding this comment.
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.
| animationManager.start([onAnimationStart, begin, onAnimationActive, duration, onAnimationEnd]); | ||
|
|
||
| return () => { | ||
| animationManager.stop(); | ||
| if (stopJSAnimation.current) { | ||
| stopJSAnimation.current(); | ||
| } | ||
| onAnimationEnd(); | ||
| }; |
There was a problem hiding this comment.
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.
| setTimeout(callback: CallbackType, _delay?: number): CancelableTimeout { | ||
| this.timeouts.push({ callback }); | ||
| return () => { | ||
| this.timeouts = this.timeouts.filter(t => t.callback !== callback); | ||
| }; |
There was a problem hiding this comment.
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.
| <AnimatedItems | ||
| animationInput={props} | ||
| animationIdPrefix="recharts-scatter-" |
There was a problem hiding this comment.
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.
| <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.
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
| * @param t A normalized time value (0 = start, 1 = end) | ||
| * @returns The interpolated items at time t | ||
| */ | ||
| animationInterpolateFn?: AnimationInterpolateFn<AreaPointItem>; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
we don't need more variants to maintain
f237791 to
3d0f95b
Compare
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
2d9d86d to
bccddbe
Compare
bccddbe to
65d6bbc
Compare
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
1 similar comment
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
b02bb57 to
b054ae0
Compare
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
b054ae0 to
18450c5
Compare
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
## 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 -->
18450c5 to
50cba68
Compare
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
50cba68 to
36bf419
Compare
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
36bf419 to
a7bd95d
Compare
|
Tip All tests passed and all changes approved!🟢 UI Tests: 198 tests unchanged |
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
There was a problem hiding this comment.
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 winKey Radar's animation state from geometry, not the full props bag.
Both
useAnimationStartSnapshot(props, ...)andanimationInput={props}depend on the entire props object, so unrelated prop/object churn can freeze a new baseline and restart the polygon animation even whenpointsandbaseLinePointsare 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 winUse 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.pointsor 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
⛔ 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.pngis excluded by!**/*.pngtest-vr/__snapshots__/tests/www/dark-mode.spec-vr.tsx-snapshots/dark-mode-examples-list-1-firefox-linux.pngis excluded by!**/*.pngtest-vr/__snapshots__/tests/www/dark-mode.spec-vr.tsx-snapshots/dark-mode-examples-list-1-webkit-linux.pngis excluded by!**/*.png
📒 Files selected for processing (45)
omnidoc/commentSimilarityExceptions.tsomnidoc/generateStorybookArgs.tsomnidoc/readProject.spec.tsomnidoc/verifyExamples.spec.tsscripts/snapshots/es6Files.txtscripts/snapshots/libFiles.txtscripts/snapshots/typesFiles.txtscripts/treeshaking-groups/Area.tsscripts/treeshaking-groups/Bar.tsscripts/treeshaking-groups/BarStack.tsscripts/treeshaking-groups/Funnel.tsscripts/treeshaking-groups/Line.tsscripts/treeshaking-groups/Pie.tsscripts/treeshaking-groups/Radar.tsscripts/treeshaking-groups/RadialBar.tsscripts/treeshaking-groups/Scatter.tsscripts/treeshaking-groups/index.tssrc/animation/AnimatedItems.tsxsrc/animation/AnimationManager.tssrc/animation/AnimationProgressProvider.tsxsrc/animation/CSSTransitionAnimate.tsxsrc/animation/ManualTimeoutController.tssrc/animation/ProgressAnimationManager.tssrc/animation/matchBy.tssrc/animation/useAnimationStartSnapshot.tssrc/animation/util.tssrc/cartesian/Area.tsxsrc/cartesian/AreaRevealShape.tsxsrc/cartesian/Bar.tsxsrc/cartesian/Funnel.tsxsrc/cartesian/Line.tsxsrc/cartesian/LineDrawShape.tsxsrc/cartesian/Scatter.tsxsrc/cartesian/useAnimatedLineLength.tssrc/index.tssrc/polar/Pie.tsxsrc/polar/Radar.tsxsrc/polar/RadialBar.tsxsrc/shape/Curve.tsxsrc/util/ActiveShapeUtils.tsxsrc/util/BarUtils.tsxsrc/util/FunnelUtils.tsxsrc/util/RadialBarUtils.tsxsrc/util/ScatterUtils.tsxsrc/util/types.ts
💤 Files with no reviewable changes (1)
- src/shape/Curve.tsx
| 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); |
There was a problem hiding this comment.
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.
| 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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| if (typeof head === 'function') { | ||
| head(); | ||
| } else if (typeof head === 'object' || typeof head === 'string') { | ||
| this.listener?.(head as ReactSmoothStyle); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Expect no matches after the fix
rg -nP --type=ts '\sas\s(?!const\b)' src/animation/ProgressAnimationManager.tsRepository: 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.tsRepository: 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.tsRepository: 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.tsRepository: 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.
| 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.
| const { points, isAnimationActive, animationBegin, animationDuration, animationEasing, animationInterpolateFn } = | ||
| props; | ||
|
|
||
| const { isAnimating, handleAnimationStart, handleAnimationEnd } = useAnimationCallbacks(); | ||
| const layout = useCartesianChartLayout(); |
There was a problem hiding this comment.
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.
| 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.
| 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; | ||
| }; |
There was a problem hiding this comment.
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.
| 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.
| 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) }]; | ||
| }); |
There was a problem hiding this comment.
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.
|
@ckifer if you have a few minutes to review please have a look on the new guide: https://recharts.github.io/recharts/pr-7215/www/en-US/guide/animations/ |
There was a problem hiding this comment.
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 winFix the
animationInterpolateFndocs 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 winUse a stable animation trigger instead of the whole
propsobject.
animationInput={props}will restart Radar transitions whenever the component rerenders or any non-geometry prop changes. Tie this topoints/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
📒 Files selected for processing (10)
src/animation/AnimatedItems.tsxsrc/cartesian/Area.tsxsrc/cartesian/Bar.tsxsrc/cartesian/Funnel.tsxsrc/cartesian/Line.tsxsrc/cartesian/LineDrawShape.tsxsrc/cartesian/Scatter.tsxsrc/polar/Pie.tsxsrc/polar/Radar.tsxsrc/polar/RadialBar.tsx
| 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>; | ||
| /** |
There was a problem hiding this comment.
🧩 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 || trueRepository: 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 || trueRepository: 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.tsRepository: 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.
| 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.
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
There was a problem hiding this comment.
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.
| * | ||
| * @param onAnimationStart optional callback to call when animation starts | ||
| * @param onAnimationEnd optional callback to call when animation ends | ||
| */ |
| > | ||
| {(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? |
There was a problem hiding this comment.
❓ 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, |
There was a problem hiding this comment.
❓ Should? Do we not know? (I certainly do not 😅 )
There was a problem hiding this comment.
This is entry point for the devtools package and other animation libraries, I plan to look closer into this part before releasing
There was a problem hiding this comment.
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 winFix the
animationInterpolateFnparameter 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 winMake added/removed point extrapolation layout-aware.
The default interpolator always computes shift on
xand only animatesxfor added/removed points. That breakslayout="vertical"transitions, where appended/removed categories move alongy, so the slide animation will come from the wrong direction or not happen at all.Use the
layoutargument to choose the category axis before computingshiftand 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 winDocument the actual
animationInterpolateFninput 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 winRecompute derived bounds while animating scatter points.
defaultScatterAnimateItemsupdatescx/cy/size, butx,y,width, andheightstay pinned toitem.next. Customshape/activeShaperenderers 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 winUse a stable animation trigger.
animationInput={props}still makes unrelated rerenders look like brand-new animations because this object is rebuilt on each render. Passingpointsor 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 winCover
visibleLengthin the custom-shape contract test.The new custom-shape API also forwards
visibleLength, but this test only guards the renamedanimationElapsedTime. 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
📒 Files selected for processing (41)
src/animation/AnimatedItems.tsxsrc/animation/AnimationProgressProvider.tsxsrc/animation/CSSTransitionAnimate.tsxsrc/animation/JavascriptAnimate.tsxsrc/animation/configUpdate.tssrc/animation/easing.tssrc/animation/useAnimationStartSnapshot.tssrc/animation/util.tssrc/cartesian/Area.tsxsrc/cartesian/AreaRevealShape.tsxsrc/cartesian/Bar.tsxsrc/cartesian/Funnel.tsxsrc/cartesian/Line.tsxsrc/cartesian/LineDrawShape.tsxsrc/cartesian/Scatter.tsxsrc/cartesian/useAnimatedLineLength.tssrc/chart/Sankey.tsxsrc/polar/Pie.tsxsrc/polar/Radar.tsxsrc/polar/RadialBar.tsxsrc/shape/Rectangle.tsxsrc/shape/Trapezoid.tsxsrc/util/DataUtils.tssrc/util/types.tstest/cartesian/Area.animation.spec.tsxtest/cartesian/Bar.animation.spec.tsxtest/cartesian/Funnel.animation.spec.tsxtest/cartesian/Line.animation.spec.tsxtest/cartesian/Scatter.animation.spec.tsxtest/cartesian/useAnimatedLineLength.spec.tstest/component/Legend.spec.tsxtest/polar/Pie/Pie.animation.spec.tsxtest/polar/Pie/Pie.spec.tsxtest/polar/Radar.animation.spec.tsxtest/polar/RadialBar/RadialBar.animation.spec.tsxwww/src/docs/exampleComponents/AreaChart/AreaChartCustomAnimationExample/index.tsxwww/src/docs/exampleComponents/AreaChart/RangeAreaChartCustomAnimation.tsxwww/src/docs/exampleComponents/BarChart/AnimatedBarTimeSeriesExample.tsxwww/src/docs/exampleComponents/LineChart/LineChartCustomShapeExample/index.tsxwww/src/docs/exampleComponents/RadarChart/RangeRadarChartCustomAnimation.tsxwww/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
| const evaluatePolynomial = (params: ReadonlyArray<number>, animationElapsedTime: number) => | ||
| params.map((param, i) => param * animationElapsedTime ** i).reduce((pre, curr) => pre + curr); |
There was a problem hiding this comment.
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.
| * 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. |
There was a problem hiding this comment.
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.
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
# 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
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
1 similar comment
|
Staging Deployment Details
These deployments will remain available for 30 days. To update snapshots: Comment |
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:
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
Documentation
Tests