Skip to content

feat: add animationMatchBy prop for identity-based animation#6973

Closed
robjampar wants to merge 1 commit into
recharts:mainfrom
robjampar:pr/animation-match-by
Closed

feat: add animationMatchBy prop for identity-based animation#6973
robjampar wants to merge 1 commit into
recharts:mainfrom
robjampar:pr/animation-match-by

Conversation

@robjampar

@robjampar robjampar commented Feb 7, 2026

Copy link
Copy Markdown

Description

Adds an animationMatchBy prop to Line and Area that controls how points are matched
between data updates during animation. Instead of the default index-based stretching, points
can be matched by identity (x-coordinate, data key, or custom function) so that existing
points slide smoothly to new positions, new points animate in from the edge, and removed
points slide off-screen.

Also adds animationClipOverflow to clip points that slide beyond axis bounds, and
isAnimationActive to ReferenceLine and ReferenceArea for smooth position transitions.

Related Issue

Closes #1961

#1997

#2575

Motivation and Context

When datapoints are added/removed from a chart, the default index-based animation interpolates
every point to the wrong position — the line "dances" instead of sliding. This has been a
long-standing issue forcing users to disable animation entirely for time-series data.
animationMatchBy solves this by matching points by identity rather than array index.

Types of changes

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

Checklist

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

Summary by CodeRabbit

  • New Features
    • Area and Line charts gain a new customizable animation hook plus two built-in interpolation presets for smoother point matching.
  • Tests
    • Large end-to-end test suite added covering matching strategies, interpolation, entry/exit behavior, and animation scenarios.
  • Documentation
    • API docs updated to include the new animation utilities and usage guidance.
  • Examples
    • New interactive story demonstrating matching strategies and animation behaviors.

@coderabbitai

coderabbitai Bot commented Feb 7, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a new point-matching and interpolation module plus two interpolation presets; exposes an animationStepFunction prop on Area and Line; updates public exports, docs, storybook demo, and adds comprehensive tests for matching and animation behavior.

Changes

Cohort / File(s) Summary
Snapshot manifests
scripts/snapshots/es6Files.txt, scripts/snapshots/libFiles.txt, scripts/snapshots/typesFiles.txt
Added references to the new animation/pointMatching module files.
Point-matching module
src/animation/pointMatching.ts
New TypeScript module implementing matching strategies, directional entry logic, interpolation engine, helpers indexMatchLinearInterpolation and xMatchLinearInterpolation, and exported AnimationStepFunction type.
Component integration
src/cartesian/Area.tsx, src/cartesian/Line.tsx
Added optional animationStepFunction prop to Area and Line; integrated the step function path into animation render pipelines with fallback to legacy interpolation.
Public exports
src/index.ts
Exported AnimationStepFunction, indexMatchLinearInterpolation, and xMatchLinearInterpolation.
Docs & verification
www/src/docs/apiCates.ts, omnidoc/verifyExamples.spec.ts
Registered new interpolation functions in docs category and marked them for example verification.
Storybook
storybook/stories/Examples/LineChart/AnimationMatchBy.stories.tsx
New story demonstrating comparison of default animation vs. xMatchLinearInterpolation with interactive controls.
Tests
test/animation/pointMatching.spec.ts, test/cartesian/Line.animation.spec.tsx, test/cartesian/Area.spec.tsx
Added extensive unit/integration tests for matching strategies, interpolation, removals, and animation scenarios; small whitespace tweak in Area tests.

Sequence Diagram(s)

sequenceDiagram
    participant App
    participant LineComponent as Line Component
    participant Matcher as Point Matcher
    participant Interpolator as Interpolator

    App->>LineComponent: update data points
    LineComponent->>Matcher: matchPointsByStrategy(current, previous, strategy)
    Matcher-->>LineComponent: matched points mapping

    loop frames (t from 0 to 1)
        alt animationStepFunction provided
            LineComponent->>Interpolator: animationStepFunction(current, previous, t)
            Interpolator-->>LineComponent: stepPoints (interpolated)
        else
            LineComponent->>LineComponent: fallback per-point interpolation
        end
        LineComponent-->>App: render intermediate frame
    end
    LineComponent-->>App: render final frame
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • ckifer
  • PavelVanecek
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main feature being introduced: an animationMatchBy prop for identity-based point matching during animation.
Description check ✅ Passed The PR description covers all major sections: it describes the changes, links related issues, explains motivation, documents testing, specifies the change type, and completes the checklist items.
Linked Issues check ✅ Passed The PR addresses issue #1961 by exposing an animationStepFunction prop with xMatchLinearInterpolation preset, allowing identity-based point matching to fix incorrect index-based interpolation when data points are added/removed.
Out of Scope Changes check ✅ Passed All changes are scoped to the animation matching feature: new pointMatching module, animationStepFunction prop on Line/Area, test coverage, storybook example, and snapshot/manifest updates. Unrelated features (animationClipOverflow, isAnimationActive) were removed as requested.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
test/component/Legend.itemSorter.spec.tsx (1)

58-142: ⚠️ Potential issue | 🟡 Minor

Use expectLastCalledWith for the legend payload assertions.

These two expectations still use toHaveBeenLastCalledWith, but the repo standard is the expectLastCalledWith helper for better typing and consistency.

♻️ Suggested change
-      expect(customContent).toHaveBeenLastCalledWith(
-        {
+      expectLastCalledWith(
+        customContent,
+        {
           align: 'center',
           chartHeight: 500,
           chartWidth: 500,
           content: customContent,
@@
-        },
-        {},
-      );
+        },
+        {},
+      );
@@
-      expect(customContent).toHaveBeenLastCalledWith(
-        {
+      expectLastCalledWith(
+        customContent,
+        {
           align: 'center',
           chartHeight: 500,
           chartWidth: 500,
           content: customContent,
@@
-        },
-        {},
-      );
+        },
+        {},
+      );

As per coding guidelines, use the expectLastCalledWith helper function instead of expect(spy).toHaveBeenLastCalledWith(...).

Also applies to: 156-241

🤖 Fix all issues with AI agents
In `@src/animation/pointMatching.ts`:
- Around line 155-163: The function getComparisonValue currently calls a custom
PointMatchingStrategy with a hardcoded index (strategy(point, 0)), which can
break user-provided strategies that depend on the true point index; update
getComparisonValue to accept an index parameter (e.g., add idx: number) and pass
that index into the strategy call, updating all callers of getComparisonValue
accordingly (referencing getComparisonValue, PointMatchingStrategy, and
AnimatablePoint), or alternatively document in the type/comment that the index
is unavailable—prefer the index-parameter approach so strategy(point, idx)
receives the real array index.

In `@src/cartesian/ReferenceArea.tsx`:
- Around line 119-128: In ReferenceArea.tsx (and similarly ReferenceLine.tsx)
replace the inline union for the animationEasing prop and the numeric type for
animationDuration with the shared types: import and use AnimationTiming for
animationEasing and AnimationDuration for animationDuration (matching Line and
Area components), and update the JSDoc comments to the expanded descriptive
style (e.g., "The type of easing function." for animationEasing and a
descriptive comment for animationDuration) so the prop signatures and docs match
existing components.
- Line 2: The code imports useId from 'react' which breaks React 16/17; update
the import to use the project's custom hook instead: replace the React import of
useId with the local useId hook (the implementation in src/util/useId.ts) in the
ReferenceArea and ReferenceLine components so they call the custom useId
fallback rather than React's built-in useId.

In `@test/animation/pointMatching.spec.ts`:
- Around line 1-687: The test suite is missing unit tests for two exported
functions used by the animation pipeline: matchPointsWithRemovals and
interpolateMatchedPoints; add focused tests that verify (1)
matchPointsWithRemovals correctly drops unmatched previous points, preserves
ordering, and returns data structures expected by the interpolator (use the
function name matchPointsWithRemovals to locate it), and (2)
interpolateMatchedPoints performs frame-by-frame interpolation including correct
extrapolated entry positions for new points and proper handling of removed
points (use interpolateMatchedPoints to locate it); mimic existing test patterns
for strategy='x' and 'index', assert both matched.previous references and
resulting interpolated frames, and add a few integration-style cases that mirror
how Line.tsx and Area.tsx consume these functions (sliding windows, bulk
add/remove, single-point replace) to cover edge behaviors.

In `@test/cartesian/Line.animation.spec.tsx`:
- Around line 1631-1802: Add concrete assertions that verify
animationMatchBy="x" behavior by inspecting rendered point positions and/or the
SVG path at t=0, mid-animation, and t=1: in each test use the existing
renderTestCase and prime helpers plus
animationManager.setAnimationProgress/completeAnimation to snapshot coordinates
(querySelectorAll('.recharts-dot') or the path 'd' via
querySelector('.recharts-line-curve')) and assert that existing items (e.g.,
symbols named B and C) start from their previous x coordinates and end at their
new x coordinates, and that newly added points (D or A) begin outside the chart
on the correct side (right for append/shift-in, left for prepend) and end
inside; add these assertions at the same places where the tests currently call
setAnimationProgress(0.5) and completeAnimation() to validate t=0, t=0.5, and
t=1 behavior.
- Around line 1631-1660: Add fake timers for the animation tests by inserting a
beforeEach that calls vi.useFakeTimers() and an afterEach that calls
vi.useRealTimers() within the same describe block that contains
renderTestCase/shiftData (the block using initialData, shiftedData, LineChart
and Line with animationMatchBy="x"); this ensures requestAnimationFrame-based
animations are deterministic during tests and you can then advance timers (e.g.,
vi.advanceTimersByTime) when clicking the Shift data button to assert animation
states.
🧹 Nitpick comments (9)
src/cartesian/GraphicalItemClipPath.tsx (1)

16-16: Consider adding a JSDoc comment for the forceClip prop.

Since GraphicalItemClipPath is part of the public type surface (listed in typesFiles.txt), the new forceClip prop would benefit from a brief JSDoc description to support auto-generated API docs.

As per coding guidelines: "JSDoc comments and TypeScript definitions in source files must be kept up-to-date as they are used to autogenerate API documentation via 'npm run omnidoc'."

src/animation/pointMatching.ts (3)

10-15: Clarify JSDoc for 'x' strategy — it matches by payload key, not geometric coordinate.

The JSDoc says 'x' matches by coordinate, but tracing through getPointKey, when strategy is 'x', it falls through to the DataKey branch and looks up payload.x via getValueByDataKey — not the point's geometric x property. While these often coincide, the distinction matters when they diverge (e.g., derived/computed coordinates). Consider clarifying the JSDoc:

-/** 'index' preserves legacy behavior; 'x' matches by coordinate; a DataKey or function matches by identity. */
+/** 'index' preserves legacy behavior; 'x' matches by the 'x' key in the data payload; a DataKey or function matches by identity. */

262-322: interpolateMatchedPoints — solid interpolation logic with a few observations.

The function handles three cases well: matched points (interpolated), new points (entry from edge), and removed points (slide out). The early-return at t === 1 is a good optimization.

Two minor notes:

  1. Line 251: avgSpacing defaults to 100 when there's only one point — consider documenting or extracting this magic number.
  2. Line 302: new points don't interpolate y — they appear at their final y-position immediately. This is intentional for horizontal line charts but may look odd for vertical layouts. Worth a comment explaining the design choice.

50-68: buildPreviousPointsMap — first-occurrence-wins on duplicate keys.

Line 61's if (!map.has(key)) means only the first occurrence of a duplicate key is kept. This is a valid design choice but might surprise users with non-unique keys. Consider adding a brief JSDoc note about this behavior.

src/cartesian/ReferenceArea.tsx (2)

197-199: lerp helper is duplicated between ReferenceArea.tsx and ReferenceLine.tsx.

Both files define identical lerp functions. Consider extracting this to a shared utility (e.g., src/util/DataUtils.ts where interpolate already lives, or a new animation utility).


261-287: Interrupted animations will jump to the previous target position.

When a new rect arrives while an animation is in progress, previousRectRef.current has already been set to the target of the in-flight animation (line 267), not its current interpolated position. This means a mid-animation data change will cause a visual discontinuity — the new animation starts from where the old one was heading, not from where it currently is.

Compare with Line.tsx's approach where previousPointsRef.current is updated inside the animation callback (line 706) with the interpolated stepData, enabling smooth interruption.

This is likely acceptable for ReferenceArea (which changes less frequently), but worth noting if smooth interruption is desired.

src/cartesian/ReferenceLine.tsx (3)

5-5: Same useId import concern as ReferenceArea.tsx.

Consider using the custom useId from src/util/useId.ts for backward compatibility with React < 18.


286-290: lerp is duplicated (also in ReferenceArea.tsx) with a slightly different signature.

This version accepts number | null | undefined while ReferenceArea's accepts number | undefined. Consider extracting a single lerp utility with the broader signature.


130-138: Animation props use inline union type instead of AnimationTiming.

Same as the ReferenceArea comment — animationEasing is typed as 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'linear' rather than reusing AnimationTiming from src/util/types.ts. This creates a maintenance risk if the timing options are extended.

Proposed fix
+ import { AnimationTiming } from '../util/types';

  // In ReferenceLineProps:
- animationEasing?: 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'linear';
+ animationEasing?: AnimationTiming;

Apply the same change in ReferenceArea.tsx.

Also applies to: 412-415

Comment on lines +155 to +163
function getComparisonValue<T extends AnimatablePoint>(point: T, strategy: PointMatchingStrategy<T>): number | null {
if (strategy === 'index') {
return point.x;
}

if (typeof strategy === 'function') {
const key = strategy(point, 0);
return typeof key === 'number' ? key : null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded index 0 passed to custom strategy function.

getComparisonValue calls strategy(point, 0) with a hardcoded index. If a user's custom function relies on the index parameter to compute a comparison value, this would produce incorrect results. Consider either:

  • Accepting an index parameter in getComparisonValue, or
  • Documenting that the index is unavailable in this context.

In practice this is low-risk since comparison values are typically derived from point data, not array position.

🤖 Prompt for AI Agents
In `@src/animation/pointMatching.ts` around lines 155 - 163, The function
getComparisonValue currently calls a custom PointMatchingStrategy with a
hardcoded index (strategy(point, 0)), which can break user-provided strategies
that depend on the true point index; update getComparisonValue to accept an
index parameter (e.g., add idx: number) and pass that index into the strategy
call, updating all callers of getComparisonValue accordingly (referencing
getComparisonValue, PointMatchingStrategy, and AnimatablePoint), or
alternatively document in the type/comment that the index is unavailable—prefer
the index-parameter approach so strategy(point, idx) receives the real array
index.

Comment thread src/cartesian/ReferenceArea.tsx Outdated
Comment thread src/cartesian/ReferenceArea.tsx Outdated
Comment thread test/animation/pointMatching.spec.ts
Comment thread test/cartesian/Line.animation.spec.tsx Outdated
Comment thread test/cartesian/Line.animation.spec.tsx Outdated
Comment on lines +1631 to +1660
describe('with animationMatchBy="x" (sliding window scenarios)', () => {
describe('sliding window shift left [A,B,C] -> [B,C,D]', () => {
// Initial data points: A at x=0, B at x=1, C at x=2
// After shift: B at x=0, C at x=1, D at x=2 (but with same original x values mapped to new positions)
const initialData = [
{ name: 'A', x: 0, value: 100 },
{ name: 'B', x: 1, value: 200 },
{ name: 'C', x: 2, value: 300 },
];
const shiftedData = [
{ name: 'B', x: 1, value: 200 },
{ name: 'C', x: 2, value: 300 },
{ name: 'D', x: 3, value: 400 },
];

const renderTestCase = createSelectorTestCase(({ children }) => {
const [data, setData] = useState(initialData);
const shiftData = () => setData(shiftedData);
return (
<div>
<button type="button" onClick={shiftData}>
Shift data
</button>
<LineChart data={data} width={100} height={100}>
<Line dataKey="value" animationEasing="linear" animationMatchBy="x" />
{children}
</LineChart>
</div>
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and examine the lines in question
stat test/cartesian/Line.animation.spec.tsx 2>/dev/null && echo "File exists" || echo "File not found"

Repository: recharts/recharts

Length of output: 75


🏁 Script executed:

# Check the beginning and imports of the test file to see current setup
head -50 test/cartesian/Line.animation.spec.tsx

Repository: recharts/recharts

Length of output: 1602


🏁 Script executed:

# Look for vi.useFakeTimers in the test file
rg "vi\.useFakeTimers|useFakeTimers" test/cartesian/Line.animation.spec.tsx -A 2 -B 2

Repository: recharts/recharts

Length of output: 43


🏁 Script executed:

# Check for beforeEach and afterEach setup in the file
rg "beforeEach|afterEach" test/cartesian/Line.animation.spec.tsx -A 5 -B 1

Repository: recharts/recharts

Length of output: 682


🏁 Script executed:

# Check if there's a vitest configuration file that might set up fake timers globally
find . -maxdepth 2 -name "vitest.config.*" -o -name "vite.config.*" | xargs ls -la 2>/dev/null

Repository: recharts/recharts

Length of output: 199


🏁 Script executed:

# Look at the area around line 1631-1660 to verify the code snippet matches
sed -n '1625,1665p' test/cartesian/Line.animation.spec.tsx

Repository: recharts/recharts

Length of output: 1663


Enable fake timers for animation tests using vi.useFakeTimers() in the beforeEach hook and vi.useRealTimers() in afterEach, as required by coding guidelines for all test files due to Redux autoBatchEnhancer dependency on timers and requestAnimationFrame. These animation tests rely on deterministic timing to verify animation behavior.

Setup fake timers
-import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
+import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest';
@@
 beforeEach(() => {
+  vi.useFakeTimers();
   mockSequenceOfGetBoundingClientRect([
     { width: 0, height: 0, left: 0, top: 50 },
     { width: 50, height: 50, left: 0, top: 50 },
   ]);
 });
+
+afterEach(() => {
+  vi.useRealTimers();
+});
🤖 Prompt for AI Agents
In `@test/cartesian/Line.animation.spec.tsx` around lines 1631 - 1660, Add fake
timers for the animation tests by inserting a beforeEach that calls
vi.useFakeTimers() and an afterEach that calls vi.useRealTimers() within the
same describe block that contains renderTestCase/shiftData (the block using
initialData, shiftedData, LineChart and Line with animationMatchBy="x"); this
ensures requestAnimationFrame-based animations are deterministic during tests
and you can then advance timers (e.g., vi.advanceTimersByTime) when clicking the
Shift data button to assert animation states.

@PavelVanecek

Copy link
Copy Markdown
Collaborator

That's a lot of change for something that can be much simpler. First, why can't we match by index? What would the animation fix look like with the current API and without new props?

@robjampar
robjampar force-pushed the pr/animation-match-by branch from 3595103 to 4fd97a8 Compare February 7, 2026 10:02

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/animation/pointMatching.ts`:
- Around line 92-104: In the pointMatching.ts logic for the 'index' strategy,
avoid dividing by zero by adding an explicit guard: if currentPoints.length ===
0 return [] before computing prevPointsDiffFactor; this prevents
previousPoints.length / currentPoints.length from producing Infinity and makes
the function contract explicit for the MatchedPoint<T> mapping that follows
(references: strategy === 'index', prevPointsDiffFactor, currentPoints.map).
🧹 Nitpick comments (10)
src/cartesian/GraphicalItemClipPath.tsx (1)

16-16: Consider adding a JSDoc comment for the forceClip prop.

The inline comment on line 41 explains the intent well, but a brief JSDoc on the prop itself would help discoverability — especially since the coding guidelines note that JSDoc is used for auto-generated documentation. That said, this is an internal component, so it's a nice-to-have.

src/cartesian/ReferenceLine.tsx (3)

296-300: Duplicate lerp helper — also defined in ReferenceArea.tsx.

Both ReferenceLine.tsx and ReferenceArea.tsx define their own lerp function with slightly different signatures (number | null | undefined here vs number | undefined in ReferenceArea). Consider extracting this into a shared utility (e.g., alongside the animation helpers in src/animation/) to reduce duplication and keep the signatures consistent.


383-389: Ref mutation during render — works but worth noting.

previousEndPointsRef.current is read and written during the render phase (lines 384 and 389). While this is a common pattern for "track previous value" and works correctly today, React's documentation advises against side effects during render. If strict mode double-rendering or future concurrent features cause issues, consider moving the ref update into a useEffect. For now this is fine in practice.


392-398: Nullish coalescing fallbacks are redundant with resolveDefaultProps.

animationBegin, animationDuration, and animationEasing are destructured from PropsWithDefaults, which already guarantees values via referenceLineDefaultProps. The ?? 0 / ?? 300 / ?? 'ease' fallbacks are dead code.

Not harmful, but they obscure the fact that defaults are handled upstream.

Proposed cleanup
     <JavascriptAnimate
       animationId={`${animationId}-${JSON.stringify(currentEndPoints)}`}
-      begin={animationBegin ?? 0}
-      duration={animationDuration ?? 300}
+      begin={animationBegin}
+      duration={animationDuration}
       isActive
-      easing={animationEasing ?? 'ease'}
+      easing={animationEasing}
     >
src/cartesian/ReferenceArea.tsx (2)

207-209: Duplicate lerp — same function exists in ReferenceLine.tsx.

This is the same helper noted in ReferenceLine.tsx (with a slightly narrower type: number | undefined vs number | null | undefined). Worth extracting to a shared location to keep the signatures consistent and avoid duplication.


279-297: Redundant nullish coalescing and ref mutation during render — same patterns as ReferenceLine.

Same observations as in ReferenceLine.tsx:

  • animationBegin ?? 0, animationDuration ?? 300, animationEasing ?? 'ease' are redundant since resolveDefaultProps already guarantees values.
  • previousRectRef.current is written during render (lines 272, 277), which works in practice but is technically a render-phase side effect.

Both are minor nits, not blockers.

src/animation/pointMatching.ts (3)

267-273: from parameter typed as unknown is unnecessarily broad.

Every call site passes number | null (e.g., previous.x, previous.y, removed.x). Narrowing the signature to (from: number | null, to: number | null, t: number) => number | null would catch misuse at compile time and align with the coding guideline to type parameters explicitly.

Proposed fix
 export function interpolateMatchedPoints<T extends AnimatablePoint>(
   matchedPoints: ReadonlyArray<MatchedPoint<T>>,
   removedPoints: ReadonlyArray<T>,
   t: number,
-  interpolateFn: (from: unknown, to: number | null, t: number) => number | null,
+  interpolateFn: (from: number | null, to: number | null, t: number) => number | null,
   animateNewValues: boolean,
 ): T[] {

As per coding guidelines, type function parameters and return values explicitly and prefer unknown only when truly needed.


249-257: Magic default avgSpacing = 100 when only one point exists.

When there's a single point in the current data, avgSpacing defaults to 100. This arbitrary value directly controls how far off-screen new/removed points start their entry animation. A very different chart width could produce jarring jumps or barely-visible motion. Consider deriving a fallback from the actual chart dimensions or documenting why 100 is acceptable.


313-327: Removed points stay in place when avgShift is zero.

If existing points only change their y-coordinates (same x positions), avgShift will be 0, and removed points interpolate x from removed.x to removed.x - 0, i.e., they never move horizontally. They'll visually persist at their position until t = 1 when they're excluded from the output. This is likely fine for the common sliding-window use case but worth documenting.

src/cartesian/Line.tsx (1)

883-884: Duplicated needClip derivation logic between LineImpl and AreaImpl.

The expression needClipFromAxes || (animationClipOverflow && animationMatchBy !== 'index') is repeated identically in both Line.tsx (Line 884) and Area.tsx (Line 941). Consider extracting this into a shared helper or hook to keep the logic DRY and reduce the risk of divergence.

Comment on lines +92 to +104
// For 'index' strategy, use the legacy stretching algorithm for backward compatibility.
if (strategy === 'index') {
const prevPointsDiffFactor = previousPoints.length / currentPoints.length;
return currentPoints.map((current, currentIndex): MatchedPoint<T> => {
const prevPointIndex = Math.floor(currentIndex * prevPointsDiffFactor);
const previous = previousPoints[prevPointIndex] ?? null;
return {
current,
currentIndex,
previous,
};
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential division issue when currentPoints is empty.

At Line 94, previousPoints.length / currentPoints.length produces Infinity when currentPoints is empty. The subsequent .map() on an empty array means no harm occurs, but the intent is unclear. A guard if (currentPoints.length === 0) return []; before this block would make the contract explicit.

🤖 Prompt for AI Agents
In `@src/animation/pointMatching.ts` around lines 92 - 104, In the
pointMatching.ts logic for the 'index' strategy, avoid dividing by zero by
adding an explicit guard: if currentPoints.length === 0 return [] before
computing prevPointsDiffFactor; this prevents previousPoints.length /
currentPoints.length from producing Infinity and makes the function contract
explicit for the MatchedPoint<T> mapping that follows (references: strategy ===
'index', prevPointsDiffFactor, currentPoints.map).

@robjampar

Copy link
Copy Markdown
Author

That's a lot of change for something that can be much simpler. First, why can't we match by index? What would the animation fix look like with the current API and without new props?

Index matching maps by array position... so if data shifts from [A, B, C] to [B, C, D], index 0 maps A→B, index 1 maps B→C, etc and every point animates to the wrong place (which is the dancing in #1961)

So i think ithout a new prop the only option would be changing the default to match by x-coordinate instead of index, but I think that would break categorical charts where x positions come from array order not data values.

I could be wrong here though, might be a simpler way to handle it that I'm not seeing.

@PavelVanecek

Copy link
Copy Markdown
Collaborator

Let's see the storybook. I'm still hoping we can find some simpler solution.

Why not disable animations?

@PavelVanecek

Copy link
Copy Markdown
Collaborator
Screen.Recording.2026-02-07.at.20.25.50.mov

Ah okay it's nicely visible in the storybook.

@PavelVanecek

Copy link
Copy Markdown
Collaborator

Right and if I disable animation I get the same effect but, well, without the animation:

Screen.Recording.2026-02-07.at.20.27.33.mov

Right I see the gap

@robjampar
robjampar force-pushed the pr/animation-match-by branch from 4fd97a8 to 80cdb56 Compare February 7, 2026 11:33
@PavelVanecek

Copy link
Copy Markdown
Collaborator

This approach (matchBy='x') only works if you are changing the XAxis domain on every tick. If you want to keep the domain the same, you are back where you started:

Screen.Recording.2026-02-07.at.20.36.49.mov

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (2)
test/component/Legend.itemSorter.spec.tsx (1)

58-142: ⚠️ Potential issue | 🟡 Minor

Use expectLastCalledWith for Legend content assertions.

These updated assertions still call toHaveBeenLastCalledWith; please switch to the helper for the preferred typed API.

♻️ Suggested update
-      expect(customContent).toHaveBeenLastCalledWith(
+      expectLastCalledWith(
+        customContent,
         {
           /* ... */
         },
         {},
       );
-      expect(customContent).toHaveBeenLastCalledWith(
+      expectLastCalledWith(
+        customContent,
         {
           /* ... */
         },
         {},
       );

As per coding guidelines, Use the expectLastCalledWith helper function instead of expect(spy).toHaveBeenLastCalledWith(...) for better typing and autocompletion.

Also applies to: 156-241

test/cartesian/Area.spec.tsx (1)

781-812: ⚠️ Potential issue | 🟡 Minor

Use expectLastCalledWith for Legend payload assertions.

These modified assertions should use the helper for consistent typed calls.

♻️ Suggested update
-      expect(legendSpy).toHaveBeenLastCalledWith([
+      expectLastCalledWith(legendSpy, [
         {
           /* ... */
         },
       ]);
-      expect(legendSpy).toHaveBeenLastCalledWith([
+      expectLastCalledWith(legendSpy, [
         {
           /* ... */
         },
       ]);

As per coding guidelines, Use the expectLastCalledWith helper function instead of expect(spy).toHaveBeenLastCalledWith(...) for better typing and autocompletion.

Also applies to: 841-872

🤖 Fix all issues with AI agents
In `@src/animation/pointMatching.ts`:
- Around line 289-302: The code computes spacingUnits by dividing by avgSpacing
which can be 0 (from findMatchedEndpoints), producing Infinity/NaN; fix by
guarding avgSpacing === 0: when avgSpacing is 0 do not divide — for
leftmost/rightmost branches compute entryX using the previous.x plus/minus
avgShift (e.g., entryX = (leftmost.previous.x ?? current.x) - avgShift for left,
and entryX = (rightmost.previous.x ?? current.x) + avgShift for right) or
otherwise set spacingUnits = 0 so multiplication yields no NaN; update the logic
around animateNewValues/current.x/leftmost/rightmost/avgSpacing/entryX so
interpolateFn never receives NaN.

In `@src/cartesian/Line.tsx`:
- Around line 697-706: The current assignment to previousPointsRef.current uses
stepData (from interpolateMatchedPoints) for t < 1, which can include appended
removed/phantom points; change the logic so that if t < 1 and
removedPoints.length > 0 you store the clean target points array (points)
instead of stepData to avoid carrying intermediate/removed points across frames,
otherwise you may keep stepData for smooth interpolation; retain the existing
behavior of setting previousPointsRef.current = points when t === 1. Use the
variables/methods matchPointsWithRemovals, interpolateMatchedPoints,
matchedPoints/removedPoints, stepData, t, animateNewValues and previousPointsRef
to locate and update this conditional assignment.
- Line 26: The public API is missing the PointMatchingStrategy type export (and
optionally the helper functions) required by consumers using the
animationMatchBy prop; add an export line to the package entry that re-exports
PointMatchingStrategy from animation/pointMatching and optionally re-export
matchPointsWithRemovals and interpolateMatchedPoints so users can import those
helpers when implementing custom matching logic (i.e. add exports for
PointMatchingStrategy, and if desired matchPointsWithRemovals and
interpolateMatchedPoints).

In `@src/cartesian/ReferenceLine.tsx`:
- Around line 383-409: The jump happens because previousEndPointsRef.current is
overwritten with currentEndPoints immediately when a new update arrives, so an
interrupted animation's visible position is lost; fix by tracking the last
rendered/interpolated position and using that as the start of the next animation
instead of the original target. Concretely: add a lastAnimatedRef (or similar)
and update it inside the JavascriptAnimate render callback (where you compute
animated via lerp) to the current interpolated endpoints, use
lastAnimatedRef.current (falling back to previousEndPointsRef.current) as the
prev value when starting a new animation, and only set
previousEndPointsRef.current to currentEndPoints once the animation completes
(or set it from lastAnimatedRef on interruption); references:
previousEndPointsRef, currentEndPoints, JavascriptAnimate, animationId,
renderContent, lerp.
🧹 Nitpick comments (5)
src/cartesian/Line.tsx (1)

883-884: Clipping is always forced for non-index strategies, even when animation is off.

needClip is true whenever animationClipOverflow && animationMatchBy !== 'index', regardless of isAnimationActive. This means a permanent clip path is applied even if the user has disabled animation. Consider gating on isAnimationActive as well:

- const needClip = needClipFromAxes || (animationClipOverflow && animationMatchBy !== 'index');
+ const needClip = needClipFromAxes || (animationClipOverflow && animationMatchBy !== 'index' && isAnimationActive !== false);
src/cartesian/ReferenceLine.tsx (2)

296-300: lerp silently coerces null/undefined to 0.

The ?? 0 fallback means if either endpoint coordinate is null or undefined, interpolation silently treats it as 0. For reference lines this is unlikely to be a problem since endpoints are calculated from scales, but it hides potential upstream bugs. Consider logging or returning NaN/null for invalid inputs.


393-398: animationBegin ?? 0 and animationDuration ?? 300 are redundant.

These props already have defaults via resolveDefaultProps at line 446, so they can never be undefined here. The ?? fallbacks are harmless but misleading.

-      begin={animationBegin ?? 0}
-      duration={animationDuration ?? 300}
+      begin={animationBegin}
+      duration={animationDuration}
       isActive
-      easing={animationEasing ?? 'ease'}
+      easing={animationEasing}
src/animation/pointMatching.ts (2)

10-15: 'x' in PointMatchingStrategy is indistinguishable from a DataKey at runtime.

The 'x' literal is listed as a special strategy in docs ("Use 'x' for time-series sliding windows"), but getPointKey has no special branch for it — it falls through to the DataKey path looking up payload.x. This works if the user's data objects have an x field, but may silently fail to match if they don't (e.g., {date: '...', value: 10}).

Consider either:

  1. Adding a dedicated 'x' branch in getPointKey that uses point.x (the pixel coordinate) directly, or
  2. Clarifying in the JSDoc that 'x' means "use x as a DataKey on the payload", not "match by x-coordinate."

224-261: avgSpacing defaults to 100 — magic number.

When there is only one point, avgSpacing falls back to 100 (Line 256). This arbitrary pixel value may produce unexpected entry animation distances depending on chart dimensions. Consider deriving a fallback from the chart width or documenting why 100 was chosen.

Comment on lines +289 to +302
if (animateNewValues && current.x !== null) {
let entryX: number;

if (leftmost && current.x < (leftmost.current.x ?? Infinity)) {
// New point is to the LEFT of all matched points
const spacingUnits = ((leftmost.current.x ?? 0) - current.x) / avgSpacing;
entryX = (leftmost.previous.x ?? 0) - spacingUnits * avgSpacing;
} else if (rightmost && current.x > (rightmost.current.x ?? -Infinity)) {
// New point is to the RIGHT of all matched points
const spacingUnits = (current.x - (rightmost.current.x ?? 0)) / avgSpacing;
entryX = (rightmost.previous.x ?? 0) + spacingUnits * avgSpacing;
} else {
entryX = current.x + avgShift;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Division by zero when avgSpacing is 0 produces NaN entry positions.

When all current points share the same x-coordinate, avgSpacing from findMatchedEndpoints is 0. Then spacingUnits = distance / 0 = Infinity, and entryX = prev.x - Infinity * 0 = NaN. The downstream interpolateFn(NaN, ...) likely falls back to current.x (since isNumber(NaN) is false in most implementations), but this is fragile and relies on an implicit safety net.

Suggested guard
     if (animateNewValues && current.x !== null) {
       let entryX: number;

-      if (leftmost && current.x < (leftmost.current.x ?? Infinity)) {
+      if (avgSpacing === 0) {
+        // All points at same x; no meaningful extrapolation possible
+        entryX = current.x;
+      } else if (leftmost && current.x < (leftmost.current.x ?? Infinity)) {
         const spacingUnits = ((leftmost.current.x ?? 0) - current.x) / avgSpacing;
         entryX = (leftmost.previous.x ?? 0) - spacingUnits * avgSpacing;
       } else if (rightmost && current.x > (rightmost.current.x ?? -Infinity)) {
🤖 Prompt for AI Agents
In `@src/animation/pointMatching.ts` around lines 289 - 302, The code computes
spacingUnits by dividing by avgSpacing which can be 0 (from
findMatchedEndpoints), producing Infinity/NaN; fix by guarding avgSpacing === 0:
when avgSpacing is 0 do not divide — for leftmost/rightmost branches compute
entryX using the previous.x plus/minus avgShift (e.g., entryX =
(leftmost.previous.x ?? current.x) - avgShift for left, and entryX =
(rightmost.previous.x ?? current.x) + avgShift for right) or otherwise set
spacingUnits = 0 so multiplication yields no NaN; update the logic around
animateNewValues/current.x/leftmost/rightmost/avgSpacing/entryX so interpolateFn
never receives NaN.

Comment thread src/cartesian/Line.tsx Outdated
import { Dots } from '../component/Dots';
import { ErrorBarDataItem, ErrorBarDataPointFormatter } from './ErrorBar';
import { interpolate, isNullish, noop } from '../util/DataUtils';
import { PointMatchingStrategy, matchPointsWithRemovals, interpolateMatchedPoints } from '../animation/pointMatching';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check if PointMatchingStrategy is exported from the public API entry point
rg -n 'PointMatchingStrategy' src/index.ts

Repository: recharts/recharts

Length of output: 43


🏁 Script executed:

# Find where PointMatchingStrategy is defined
rg -n 'type PointMatchingStrategy\|interface PointMatchingStrategy' src/

Repository: recharts/recharts

Length of output: 43


🏁 Script executed:

# Check how animationMatchBy is defined and what type it expects
rg -n 'animationMatchBy' src/ -A 3 -B 1 | head -50

Repository: recharts/recharts

Length of output: 2530


🏁 Script executed:

# Search more broadly for PointMatchingStrategy definition
rg 'PointMatchingStrategy' src/ --type ts --type tsx | head -20

Repository: recharts/recharts

Length of output: 88


🏁 Script executed:

# Check what's exported from the animation module
cat src/animation/pointMatching.ts | head -100

Repository: recharts/recharts

Length of output: 2895


🏁 Script executed:

# Check what's exported from src/index.ts, especially around Line
rg 'export.*Line' src/index.ts

Repository: recharts/recharts

Length of output: 367


🏁 Script executed:

# Check if animation utilities are exported from public API
rg 'animation|pointMatching' src/index.ts

Repository: recharts/recharts

Length of output: 43


🏁 Script executed:

# Look at the full structure of src/index.ts to understand exports
wc -l src/index.ts

Repository: recharts/recharts

Length of output: 77


🏁 Script executed:

# Read the full src/index.ts to understand the export pattern
cat -n src/index.ts

Repository: recharts/recharts

Length of output: 10496


🏁 Script executed:

# Check what the LineProps interface contains and if it re-exports PointMatchingStrategy
rg -A 30 'export type.*Props as LineProps' src/cartesian/Line.tsx | head -50

Repository: recharts/recharts

Length of output: 43


🏁 Script executed:

# Check if PointMatchingStrategy is used in other components besides Line
rg 'PointMatchingStrategy' src/cartesian/ --type ts

Repository: recharts/recharts

Length of output: 576


🏁 Script executed:

# Check what types are in the Props interface that uses PointMatchingStrategy
sed -n '160,180p' src/cartesian/Line.tsx

Repository: recharts/recharts

Length of output: 795


🏁 Script executed:

# Check if users importing LineProps would have access to the types they reference
rg 'interface Props|type Props' src/cartesian/Line.tsx -A 50 | head -80

Repository: recharts/recharts

Length of output: 1258


Export PointMatchingStrategy from the public API entry point.

End users need access to this type when using the animationMatchBy prop with custom functions in Line and Area components. Add to src/index.ts:

export type { PointMatchingStrategy } from './animation/pointMatching';

Consider also exporting utility functions matchPointsWithRemovals and interpolateMatchedPoints if users are expected to implement custom animation matching logic.

🤖 Prompt for AI Agents
In `@src/cartesian/Line.tsx` at line 26, The public API is missing the
PointMatchingStrategy type export (and optionally the helper functions) required
by consumers using the animationMatchBy prop; add an export line to the package
entry that re-exports PointMatchingStrategy from animation/pointMatching and
optionally re-export matchPointsWithRemovals and interpolateMatchedPoints so
users can import those helpers when implementing custom matching logic (i.e. add
exports for PointMatchingStrategy, and if desired matchPointsWithRemovals and
interpolateMatchedPoints).

Comment thread src/cartesian/Line.tsx Outdated
Comment on lines +697 to +706
const { matched: matchedPoints, removed: removedPoints } = matchPointsWithRemovals(
points,
prevPoints,
animationMatchBy,
);

const stepData = interpolateMatchedPoints(matchedPoints, removedPoints, t, interpolate, animateNewValues);

// eslint-disable-next-line no-param-reassign
previousPointsRef.current = stepData;
previousPointsRef.current = t === 1 ? points : stepData;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

During interrupted animations, previousPointsRef holds intermediate/removed points.

When t < 1, previousPointsRef.current is set to stepData (Line 706), which includes interpolated positions and appended removed points. If a data update arrives mid-animation, the next render picks up this intermediate state as prevPoints, so the matching algorithm will try to match against ghost removed points with in-between coordinates.

This matches the pre-existing pattern (Line 674 also updates the ref mid-animation), but with identity-based matching it's riskier because removed phantom points could produce spurious matches by key. Consider guarding so that on interruption, only the clean points array is stored, not stepData.

Suggested approach
-            previousPointsRef.current = t === 1 ? points : stepData;
+            // Always store the target points so an interrupted animation
+            // doesn't leave phantom removed-points in the ref.
+            previousPointsRef.current = points;

This way, if the animation is interrupted, the next cycle starts from the actual target data rather than an intermediate frame.

📝 Committable suggestion

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

Suggested change
const { matched: matchedPoints, removed: removedPoints } = matchPointsWithRemovals(
points,
prevPoints,
animationMatchBy,
);
const stepData = interpolateMatchedPoints(matchedPoints, removedPoints, t, interpolate, animateNewValues);
// eslint-disable-next-line no-param-reassign
previousPointsRef.current = stepData;
previousPointsRef.current = t === 1 ? points : stepData;
const { matched: matchedPoints, removed: removedPoints } = matchPointsWithRemovals(
points,
prevPoints,
animationMatchBy,
);
const stepData = interpolateMatchedPoints(matchedPoints, removedPoints, t, interpolate, animateNewValues);
// eslint-disable-next-line no-param-reassign
// Always store the target points so an interrupted animation
// doesn't leave phantom removed-points in the ref.
previousPointsRef.current = points;
🤖 Prompt for AI Agents
In `@src/cartesian/Line.tsx` around lines 697 - 706, The current assignment to
previousPointsRef.current uses stepData (from interpolateMatchedPoints) for t <
1, which can include appended removed/phantom points; change the logic so that
if t < 1 and removedPoints.length > 0 you store the clean target points array
(points) instead of stepData to avoid carrying intermediate/removed points
across frames, otherwise you may keep stepData for smooth interpolation; retain
the existing behavior of setting previousPointsRef.current = points when t ===
1. Use the variables/methods matchPointsWithRemovals, interpolateMatchedPoints,
matchedPoints/removedPoints, stepData, t, animateNewValues and previousPointsRef
to locate and update this conditional assignment.

Comment thread src/cartesian/ReferenceLine.tsx Outdated
Comment on lines 383 to 409
if (!isAnimationActive || !previousEndPointsRef.current) {
previousEndPointsRef.current = currentEndPoints;
return renderContent(currentEndPoints);
}

const prev = previousEndPointsRef.current;
previousEndPointsRef.current = currentEndPoints;

return (
<ZIndexLayer zIndex={props.zIndex}>
<Layer className={clsx('recharts-reference-line', className)}>
{renderLine(shape, lineProps)}
<CartesianLabelContextProvider {...rect} lowerWidth={rect.width} upperWidth={rect.width}>
<CartesianLabelFromLabelProp label={props.label} />
{props.children}
</CartesianLabelContextProvider>
</Layer>
</ZIndexLayer>
<JavascriptAnimate
animationId={`${animationId}-${JSON.stringify(currentEndPoints)}`}
begin={animationBegin ?? 0}
duration={animationDuration ?? 300}
isActive
easing={animationEasing ?? 'ease'}
>
{(t: number) => {
const animated: LineEndPoints = {
x1: lerp(prev.x1, currentEndPoints.x1, t),
y1: lerp(prev.y1, currentEndPoints.y1, t),
x2: lerp(prev.x2, currentEndPoints.x2, t),
y2: lerp(prev.y2, currentEndPoints.y2, t),
};
return renderContent(animated);
}}
</JavascriptAnimate>
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Animation interruption causes a visual jump.

When a new data update arrives mid-animation, previousEndPointsRef.current is already set to the target endpoints (Line 389), not the current visual position. So the next animation starts from the target of the interrupted animation rather than the actual interpolated position, causing a visible jump.

To fix, capture the interpolated position at the moment of interruption:

Sketch of a smoother approach
+ const lastRenderedRef = useRef<LineEndPoints | null>(null);
  ...
  if (!isAnimationActive || !previousEndPointsRef.current) {
    previousEndPointsRef.current = currentEndPoints;
+   lastRenderedRef.current = currentEndPoints;
    return renderContent(currentEndPoints);
  }

- const prev = previousEndPointsRef.current;
+ const prev = lastRenderedRef.current ?? previousEndPointsRef.current;
  previousEndPointsRef.current = currentEndPoints;

  return (
    <JavascriptAnimate ...>
      {(t: number) => {
        const animated: LineEndPoints = { ... };
+       lastRenderedRef.current = animated;
        return renderContent(animated);
      }}
    </JavascriptAnimate>
  );
🤖 Prompt for AI Agents
In `@src/cartesian/ReferenceLine.tsx` around lines 383 - 409, The jump happens
because previousEndPointsRef.current is overwritten with currentEndPoints
immediately when a new update arrives, so an interrupted animation's visible
position is lost; fix by tracking the last rendered/interpolated position and
using that as the start of the next animation instead of the original target.
Concretely: add a lastAnimatedRef (or similar) and update it inside the
JavascriptAnimate render callback (where you compute animated via lerp) to the
current interpolated endpoints, use lastAnimatedRef.current (falling back to
previousEndPointsRef.current) as the prev value when starting a new animation,
and only set previousEndPointsRef.current to currentEndPoints once the animation
completes (or set it from lastAnimatedRef on interruption); references:
previousEndPointsRef, currentEndPoints, JavascriptAnimate, animationId,
renderContent, lerp.

@PavelVanecek

Copy link
Copy Markdown
Collaborator

Now hear me out @robjampar for a moment. The animation has a ton of orchestration around it but eventually it boils down to this part here https://github.com/recharts/recharts/blob/main/src/cartesian/Line.tsx#L683 where it calculates stepData. This part includes the selection by index.

What if we exposed this part as a prop? <Line animationStepFunction={indexMatchLinearInterpolationFunction} /> and then people could write their own to do whatever matching they want. We could even export the most common cases ("legacy" Recharts animation, time series) to make things easier, and also allow people to fine-tune and even provide their own completely custom animations.

What do you think?

@PavelVanecek

Copy link
Copy Markdown
Collaborator

Also, to keep things focused:

Also adds animationClipOverflow to clip points that slide beyond axis bounds,

This should be the default, let's treat it as a bugfix, merge in separate PR, doesn't need to be a prop

isAnimationActive to ReferenceLine and ReferenceArea for smooth position transitions.

Separate PR please

@robjampar

Copy link
Copy Markdown
Author

Ok good idea - reworked this based on your suggestions:

replaced animationMatchBy with animationStepFunction — the step data computation is now exposed as a prop like you described. the default code path (no prop) is completely unchanged from main, and recharts exports two presets:

  • indexMatchLinearInterpolation — the existing behaviour
  • xMatchLinearInterpolation — time-series sliding windows
  import { xMatchLinearInterpolation } from 'recharts';
  <Line animationStepFunction={xMatchLinearInterpolation} />

also split out the other bits:

  • removed isAnimationActive on ReferenceLine/ReferenceArea —that got pulled in from my fork but shouldn't have

  • removed animationClipOverflow entirely — agree it should just be default behaviour, can do that as a bugfix PR

    the scope is much smaller now, what do you think?

@robjampar
robjampar force-pushed the pr/animation-match-by branch from 80cdb56 to 6b75096 Compare February 7, 2026 15:58

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/animation/pointMatching.ts`:
- Around line 314-328: The removed points currently interpolate toward removed.x
- avgShift and freeze when matchedCount is 0 because avgShift becomes 0; change
the logic that computes the target shift used in removedInterpolated so if
matchedCount === 0 you use a non-zero fallbackShift (e.g., a small constant or a
direction-based offset computed from removed.x relative to the chart
center/median) and apply that fallback instead of 0 when calling interpolateFn
inside removedInterpolated so removed points visibly exit even on complete
replacements.

In `@src/cartesian/Area.tsx`:
- Around line 685-704: The code uses an unsafe "as ReadonlyArray<AreaPointItem>"
cast on prevBaseLine before calling animationStepFunction; remove the assertion
and add a runtime type-narrowing guard (e.g.,
isAreaPointItemArray(prevBaseLine)) to confirm each element has the required
AreaPointItem fields before calling animationStepFunction(baseLine,
prevBaseLine, t), or alternatively change the stored type of previousBaselineRef
to the more precise ReadonlyArray<AreaPointItem> if that is always true; ensure
the branch uses the guarded type so no "as" cast is needed and adjust
AnimationStepFunction/previousBaselineRef types if necessary.
🧹 Nitpick comments (3)
omnidoc/verifyExamples.spec.ts (1)

182-184: New exports correctly registered as needing examples.

These are added to the exportsThatNeedExamples list following the established pattern. Since a Storybook story (AnimationMatchBy.stories.tsx) already demonstrates xMatchLinearInterpolation, you may be able to remove it from this list if the ExampleReader picks up storybook stories as examples.

test/animation/pointMatching.spec.ts (1)

755-804: Consider adding tests for animateNewValues=false and new-point entry extrapolation.

interpolateMatchedPoints has branching logic for animateNewValues (true vs false) and entry position extrapolation (left of leftmost, right of rightmost, between matched). The current tests cover the happy path but don't exercise:

  • animateNewValues=false (new points should be returned as-is without entry animation)
  • Entry position extrapolation for new points appearing to the left/right of matched endpoints
  • The avgShift fallback path (new point between matched endpoints)

These paths are critical for the animation correctness that this PR is solving.

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

src/cartesian/Line.tsx (1)

887-888: Unnecessary intermediate variable from removed feature.

needClipFromAxes is immediately assigned to needClip without any transformation. This appears to be a leftover from the removed animationClipOverflow feature. Simplify:

Suggested fix
-  const { needClip: needClipFromAxes } = useNeedsClip(xAxisId, yAxisId);
-  const needClip = needClipFromAxes;
+  const { needClip } = useNeedsClip(xAxisId, yAxisId);

Comment on lines +314 to +328
// Removed points slide out in the direction of the overall shift
const removedInterpolated: T[] = removedPoints.map((removed): T => {
if (removed.x !== null) {
return {
...removed,
x: interpolateFn(removed.x, removed.x - avgShift, t),
y: removed.y,
};
}
return removed;
});

const result = [...interpolated, ...removedInterpolated];
result.sort((a, b) => (a.x ?? 0) - (b.x ?? 0));
return result;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Removed points animate toward removed.x - avgShift, but when avgShift is 0 they freeze in place.

When data is completely replaced (no overlap between previous and current), matchedCount is 0, so avgShift is 0. Removed points then interpolate from removed.x to removed.x - 0 = removed.x — they don't visually exit; they just stay and then vanish at t=1. This may or may not be the intended behavior for a complete data replacement scenario.

🤖 Prompt for AI Agents
In `@src/animation/pointMatching.ts` around lines 314 - 328, The removed points
currently interpolate toward removed.x - avgShift and freeze when matchedCount
is 0 because avgShift becomes 0; change the logic that computes the target shift
used in removedInterpolated so if matchedCount === 0 you use a non-zero
fallbackShift (e.g., a small constant or a direction-based offset computed from
removed.x relative to the chart center/median) and apply that fallback instead
of 0 when calling interpolateFn inside removedInterpolated so removed points
visibly exit even on complete replacements.

Comment thread src/cartesian/Area.tsx
@robjampar
robjampar force-pushed the pr/animation-match-by branch from 6b75096 to dda770e Compare February 7, 2026 16:14
@robjampar
robjampar force-pushed the pr/animation-match-by branch from dda770e to a0d30f3 Compare February 7, 2026 16:18

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/cartesian/Area.tsx`:
- Around line 695-706: The interpolate(...) calls can return null but
stepBaseLine is typed BaseLineType (no null); update the two places calling
interpolate (in the isNumber and isNullish/isNan branches) to coerce a null
result to an acceptable BaseLineType value (e.g., use the numeric fallback 0
when interpolate returns null) or otherwise convert the nullable result before
assigning to stepBaseLine; ensure this change references the interpolate
function, the stepBaseLine variable, and preserves existing branches (isNumber,
isNullish, isNan) so the assignment always produces a value conforming to
BaseLineType.

In `@test/cartesian/Line.animation.spec.tsx`:
- Around line 1631-1660: The test currently uses a categorical x-axis so
xMatchLinearInterpolation isn't exercised; update the renderTestCase to include
a numeric XAxis (e.g., add an <XAxis dataKey="x" type="number"
domain={['dataMin','dataMax']} /> inside the LineChart alongside the <Line ...
animationStepFunction={xMatchLinearInterpolation} /> so pixel positions are
derived from the numeric x value and the sliding-window shift-left, append, and
prepend scenarios truly match points by x rather than by array index.
🧹 Nitpick comments (3)
src/animation/pointMatching.ts (2)

24-49: getPointKey with strategy = 'x' silently falls through to the DataKey code path.

When the strategy is the string literal 'x', the function doesn't hit the 'index' guard (Line 29) or the typeof === 'function' guard (Line 33), so it proceeds to getValueByDataKey(point.payload, 'x'). This is intentional but not obvious — a brief comment at the // strategy is a DataKey line (37) noting that 'x' is included here by design would help maintainability.


250-262: Magic number 100 for avgSpacing fallback when there's a single point.

When all current points share the same x or there's only one point, avgSpacing defaults to 100 (Line 257). This arbitrary constant affects entry/exit animation positions for new and removed points. Consider extracting it as a named constant with a brief comment explaining the rationale.

Suggested change
+const DEFAULT_SPACING_FALLBACK = 100;
+
 function findMatchedEndpoints<T extends AnimatablePoint>(
   matchedPoints: ReadonlyArray<MatchedPoint<T>>,
 ): MatchedEndpoints<T> {
   ...
-  const avgSpacing = allCurrentX.length > 1 ? (lastX - firstX) / (allCurrentX.length - 1) : 100;
+  const avgSpacing = allCurrentX.length > 1 ? (lastX - firstX) / (allCurrentX.length - 1) : DEFAULT_SPACING_FALLBACK;
src/cartesian/Area.tsx (1)

916-984: animationStepFunction flows through everythingElse spread — correct but implicit.

AreaImpl destructures many props from resolveDefaultProps but animationStepFunction is not listed, so it lands in ...everythingElse and is spread onto <AreaWithState>. This works because InternalAreaProps declares the prop. However, it's less discoverable than the explicitly listed props.

Comment thread src/cartesian/Area.tsx
Comment on lines +695 to +706
if (isNumber(baseLine)) {
stepBaseLine = interpolate(prevBaseLine, baseLine, t);
} else if (isNullish(baseLine) || isNan(baseLine)) {
stepBaseLine = interpolate(prevBaseLine, 0, t);
} else if (Array.isArray(prevBaseLine)) {
// baseLine and prevBaseLine are both coordinate arrays here; use a generic step function
// to avoid an unsafe type assertion between NullableCoordinate and AreaPointItem
const baselineStepFn: AnimationStepFunction<AnimatablePoint> = animationStepFunction;
stepBaseLine = t === 1 ? baseLine : baselineStepFn(baseLine, prevBaseLine, t);
} else {
stepBaseLine = baseLine;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

interpolate returns number | null but stepBaseLine is typed BaseLineType (which excludes null).

On Lines 696 and 698, interpolate(...) returns number | null, but stepBaseLine is declared as BaseLineType (number | ReadonlyArray<NullableCoordinate>). If interpolate returns null, the type contract is violated. This is the same pattern as the existing default branch (Lines 731-733), so it's a pre-existing issue, but worth noting since it's replicated in the new code path.

🤖 Prompt for AI Agents
In `@src/cartesian/Area.tsx` around lines 695 - 706, The interpolate(...) calls
can return null but stepBaseLine is typed BaseLineType (no null); update the two
places calling interpolate (in the isNumber and isNullish/isNan branches) to
coerce a null result to an acceptable BaseLineType value (e.g., use the numeric
fallback 0 when interpolate returns null) or otherwise convert the nullable
result before assigning to stepBaseLine; ensure this change references the
interpolate function, the stepBaseLine variable, and preserves existing branches
(isNumber, isNullish, isNan) so the assignment always produces a value
conforming to BaseLineType.

Comment on lines +1631 to +1660
describe('with animationStepFunction={xMatchLinearInterpolation} (sliding window scenarios)', () => {
describe('sliding window shift left [A,B,C] -> [B,C,D]', () => {
// Initial data points: A at x=0, B at x=1, C at x=2
// After shift: B at x=0, C at x=1, D at x=2 (but with same original x values mapped to new positions)
const initialData = [
{ name: 'A', x: 0, value: 100 },
{ name: 'B', x: 1, value: 200 },
{ name: 'C', x: 2, value: 300 },
];
const shiftedData = [
{ name: 'B', x: 1, value: 200 },
{ name: 'C', x: 2, value: 300 },
{ name: 'D', x: 3, value: 400 },
];

const renderTestCase = createSelectorTestCase(({ children }) => {
const [data, setData] = useState(initialData);
const shiftData = () => setData(shiftedData);
return (
<div>
<button type="button" onClick={shiftData}>
Shift data
</button>
<LineChart data={data} width={100} height={100}>
<Line dataKey="value" animationEasing="linear" animationStepFunction={xMatchLinearInterpolation} />
{children}
</LineChart>
</div>
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Test setup may not exercise xMatchLinearInterpolation as intended — categorical x-axis matches by pixel index, not by data identity.

Without a numeric <XAxis dataKey="x" type="number" domain={...} />, the x-axis defaults to categorical, assigning pixel positions by array index. In that case, xMatchLinearInterpolation matching by pixel x is effectively the same as index-based matching — B at index 0 in the new data gets the same pixel x as A at index 0 in the old data, not B at index 1.

To truly exercise x-based matching (B→B, C→C), add a numeric XAxis so that each data point's pixel x derives from its x data value:

Suggested fix for the shift-left test case
 <LineChart data={data} width={100} height={100}>
+  <XAxis dataKey="x" type="number" domain={['dataMin', 'dataMax']} />
   <Line dataKey="value" animationEasing="linear" animationStepFunction={xMatchLinearInterpolation} />
   {children}
 </LineChart>

Apply the same pattern to the append and prepend test cases.

📝 Committable suggestion

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

Suggested change
describe('with animationStepFunction={xMatchLinearInterpolation} (sliding window scenarios)', () => {
describe('sliding window shift left [A,B,C] -> [B,C,D]', () => {
// Initial data points: A at x=0, B at x=1, C at x=2
// After shift: B at x=0, C at x=1, D at x=2 (but with same original x values mapped to new positions)
const initialData = [
{ name: 'A', x: 0, value: 100 },
{ name: 'B', x: 1, value: 200 },
{ name: 'C', x: 2, value: 300 },
];
const shiftedData = [
{ name: 'B', x: 1, value: 200 },
{ name: 'C', x: 2, value: 300 },
{ name: 'D', x: 3, value: 400 },
];
const renderTestCase = createSelectorTestCase(({ children }) => {
const [data, setData] = useState(initialData);
const shiftData = () => setData(shiftedData);
return (
<div>
<button type="button" onClick={shiftData}>
Shift data
</button>
<LineChart data={data} width={100} height={100}>
<Line dataKey="value" animationEasing="linear" animationStepFunction={xMatchLinearInterpolation} />
{children}
</LineChart>
</div>
);
});
describe('with animationStepFunction={xMatchLinearInterpolation} (sliding window scenarios)', () => {
describe('sliding window shift left [A,B,C] -> [B,C,D]', () => {
// Initial data points: A at x=0, B at x=1, C at x=2
// After shift: B at x=0, C at x=1, D at x=2 (but with same original x values mapped to new positions)
const initialData = [
{ name: 'A', x: 0, value: 100 },
{ name: 'B', x: 1, value: 200 },
{ name: 'C', x: 2, value: 300 },
];
const shiftedData = [
{ name: 'B', x: 1, value: 200 },
{ name: 'C', x: 2, value: 300 },
{ name: 'D', x: 3, value: 400 },
];
const renderTestCase = createSelectorTestCase(({ children }) => {
const [data, setData] = useState(initialData);
const shiftData = () => setData(shiftedData);
return (
<div>
<button type="button" onClick={shiftData}>
Shift data
</button>
<LineChart data={data} width={100} height={100}>
<XAxis dataKey="x" type="number" domain={['dataMin', 'dataMax']} />
<Line dataKey="value" animationEasing="linear" animationStepFunction={xMatchLinearInterpolation} />
{children}
</LineChart>
</div>
);
});
🤖 Prompt for AI Agents
In `@test/cartesian/Line.animation.spec.tsx` around lines 1631 - 1660, The test
currently uses a categorical x-axis so xMatchLinearInterpolation isn't
exercised; update the renderTestCase to include a numeric XAxis (e.g., add an
<XAxis dataKey="x" type="number" domain={['dataMin','dataMax']} /> inside the
LineChart alongside the <Line ...
animationStepFunction={xMatchLinearInterpolation} /> so pixel positions are
derived from the numeric x value and the sliding-window shift-left, append, and
prepend scenarios truly match points by x rather than by array index.

@codecov

codecov Bot commented Feb 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.68769% with 41 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.40%. Comparing base (f45f6d8) to head (a0d30f3).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/cartesian/Area.tsx 42.50% 23 Missing ⚠️
src/animation/pointMatching.ts 96.53% 9 Missing ⚠️
src/cartesian/Line.tsx 70.00% 9 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6973      +/-   ##
==========================================
+ Coverage   90.38%   90.40%   +0.01%     
==========================================
  Files         515      516       +1     
  Lines       38522    38804     +282     
  Branches     5342     5434      +92     
==========================================
+ Hits        34818    35080     +262     
- Misses       3695     3715      +20     
  Partials        9        9              

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

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

@codecov

codecov Bot commented Feb 8, 2026

Copy link
Copy Markdown

Bundle Report

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

Detailed changes
Bundle name Size Change
recharts/bundle-cjs 1.27MB 15.28kB (1.22%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: recharts/bundle-cjs

Assets Changed:

Asset Name Size Change Total Size Change (%)
cartesian/Area.js 1.42kB 30.75kB 4.84%
cartesian/Line.js 218 bytes 26.71kB 0.82%
index.js 401 bytes 14.46kB 2.85%
animation/pointMatching.js (New) 13.24kB 13.24kB 100.0% 🚀

@PavelVanecek PavelVanecek left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Okay I think it's much better like this. @ckifer wdyt?

Can we please polish this a little bit before merging @robjampar ? Extra docs and examples would be nice too. Please prefer the website instead of storybooks because #6668

'useYAxisScale',
'useXAxisTicks',
'useYAxisTicks',
'AnimationStepFunction',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Let's add examples on the website, or even better write a whole new guide on how to work with these.

payload?: unknown;
}

/** 'index' preserves legacy behavior; 'x' matches by coordinate; a DataKey or function matches by identity. */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nothing in this behaviour is "legacy", don't call it that or we spook people into thinking we're removing it.

}

/** 'index' preserves legacy behavior; 'x' matches by coordinate; a DataKey or function matches by identity. */
export type PointMatchingStrategy<T extends AnimatablePoint = AnimatablePoint> =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We can now simplify this a bit since we expose the interpolation function. The strategy structure can go away

}

// For 'index' strategy, use the legacy stretching algorithm for backward compatibility.
if (strategy === 'index') {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The "index" strategy is now implemented three times: once here, then in indexMatchLinearInterpolation, and then third time inside of Area. Let's keep indexMatchLinearInterpolation, make that a default prop, and remove the other two.

Comment thread src/cartesian/Area.tsx
* import { xMatchLinearInterpolation } from 'recharts';
* <Area animationStepFunction={xMatchLinearInterpolation} />
*/
animationStepFunction?: AnimationStepFunction<AreaPointItem>;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is the first time when we actually expose the AreaPointItem (and LinePointItem below too), isn't it. Let's also export the hooks that select these two, before people start hacking the animation to get access to the points directly, shall we. And add JSDoc to the type definition of these two to explain what is going on.

* Matches points by x-coordinate so existing points slide smoothly,
* new points animate in from the edges, and removed points slide off-screen.
*/
export function xMatchLinearInterpolation<T extends AnimatablePoint>(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we use this method to fix #1997 too? If yes can you please add the same prop to Bar as well?

removedPoints: ReadonlyArray<T>,
t: number,
interpolateFn: (from: unknown, to: number | null, t: number) => number | null,
animateNewValues: boolean,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

animateNewValues is always true, can we remove the param?

matchedPoints: ReadonlyArray<MatchedPoint<T>>,
removedPoints: ReadonlyArray<T>,
t: number,
interpolateFn: (from: unknown, to: number | null, t: number) => number | null,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why not hardcode interpolate from DataUtils? It's doing the same thing

@ckifer

ckifer commented Feb 10, 2026

Copy link
Copy Markdown
Member

yeah this is fine to do. Agreed with all of @PavelVanecek 's comments though - this needs cleaned up

@PavelVanecek PavelVanecek mentioned this pull request Mar 28, 2026
PavelVanecek added a commit that referenced this pull request Mar 29, 2026
I want to work on #6973 next so
first I wanted to have some place where we document animations so I
wrote one.

While in there I noticed that our public animation type does not allow
`spring` and custom curves even though the actual implementation does so
I fixed the type too.

Also includes some copilot skills because the AI is now quite capable of
writing the website examples and it may save some time.

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

* **New Features**
* Added a comprehensive "Animations" guide with interactive example and
controls; introduced a new easing input type and widened
animation-easing support across chart components to accept more easing
formats while preserving existing defaults.

* **Documentation**
* Added contributor guides for examples, guide pages, and
visual-regression (VR) test authoring and workflows; localized and
registered the new Animations guide in the site navigation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@PavelVanecek PavelVanecek mentioned this pull request Apr 4, 2026
@PavelVanecek

Copy link
Copy Markdown
Collaborator

I'm going to decline this one @robjampar and merge #7215 instead. I'll still include you as contributor

PavelVanecek added a commit that referenced this pull request Jun 3, 2026
# Description

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

This PR includes:

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

Discussion: #7235

# Related issues

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

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

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

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

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

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/recharts/recharts/pull/7215?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wrong animation interpolation when adding datapoints

3 participants