Perf: filter zero-dimension rectangles early#6800
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:
WalkthroughThis PR introduces index stability for BarStack rendering by using Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. 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. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6800 +/- ##
==========================================
+ Coverage 90.12% 90.13% +0.01%
==========================================
Files 526 526
Lines 39183 39254 +71
Branches 5422 5438 +16
==========================================
+ Hits 35312 35380 +68
- Misses 3862 3865 +3
Partials 9 9 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
PavelVanecek
left a comment
There was a problem hiding this comment.
Thanks for the PR. Do you have some measurements on the performance improvement?
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (1)
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx (1)
66-69: Consider improving type annotation.The inline type annotation could be replaced with proper typing from the selector's return type for better type safety and maintainability.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CONTRIBUTING.md)
**/*.{ts,tsx}: Never useanytype (implicit or explicit) in TypeScript code
Preferunknownoveranyand refine the type in TypeScript
Type function parameters and return values explicitly in TypeScript, do not rely on implicit any or inference; exceptions are React components and trivial functions
Do not useastype assertions in TypeScript; the only exception isas const
Files:
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx
test/**/*.spec.{ts,tsx}
📄 CodeRabbit inference engine (CONTRIBUTING.md)
Aim for 100% unit test code coverage when writing new code
Files:
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx
**/*.{js,ts,tsx}
📄 CodeRabbit inference engine (CONTRIBUTING.md)
Ensure code lints by running
npm run lintand follows Airbnb's Style Guide
Files:
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx
test/**/*.{test,spec}.{ts,tsx}
📄 CodeRabbit inference engine (test/README.md)
test/**/*.{test,spec}.{ts,tsx}: Aim for 100% unit test code coverage when writing new code
Prefer to use thecreateSelectorTestCasehelper function when writing or modifying tests
Use theexpectLastCalledWithhelper function instead ofexpect(spy).toHaveBeenLastCalledWith(...)for better typing and autocompletion
Verify the number of selector calls using the spy object fromcreateSelectorTestCaseto spot unnecessary re-renders and improve performance
MockgetBoundingClientRectin tests using the helper function provided intest/helper/MockGetBoundingClientRect.ts
Usevi.useFakeTimers()in all tests due to Redux autoBatchEnhancer dependency on timers andrequestAnimationFrame
Callvi.runOnlyPendingTimers()to advance timers after renders when not usingcreateSelectorTestCasehelper, and avoidvi.runAllTimers()to prevent infinite loops
UseuserEvent.setup({ advanceTimers: vi.runOnlyPendingTimers })or theuserEventSetuphelper function fromtest/helper/userEventSetup.tswhen creating userEvent instances
When testing tooltips on hover, usevi.runOnlyPendingTimers()after eachuserEvent.hover()call or use theshowTooltiphelper function fromtooltipTestHelpers.tsto account for requestAnimationFrame delays
Files:
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx
**/*.spec.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
When running unit tests, prefer to run a single test file using
npm run test -- path/to/TestFile.spec.tsxrather than running all tests withnpm test
Files:
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (DEVELOPING.md)
All imports from
rechartsmust use the public API entry point; imports from internal paths likerecharts/types/*orrecharts/src/*are not allowed
Files:
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx
🧠 Learnings (7)
📓 Common learnings
Learnt from: PavelVanecek
Repo: recharts/recharts PR: 6659
File: www/src/components/GuideView/Performance/index.tsx:232-250
Timestamp: 2025-11-19T14:08:01.728Z
Learning: In Recharts version 3.4.2, object-as-prop optimizations were introduced to reduce unnecessary re-renders when new object references are passed as props. This affects the recommendation for the `react-perf/jsx-no-new-object-as-prop` ESLint rule.
📚 Learning: 2025-11-25T01:23:08.250Z
Learnt from: CR
Repo: recharts/recharts PR: 0
File: test/README.md:0-0
Timestamp: 2025-11-25T01:23:08.250Z
Learning: Applies to test/**/*.{test,spec}.{ts,tsx} : Verify the number of selector calls using the spy object from `createSelectorTestCase` to spot unnecessary re-renders and improve performance
Applied to files:
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx
📚 Learning: 2025-12-16T08:12:06.809Z
Learnt from: PavelVanecek
Repo: recharts/recharts PR: 6783
File: test/util/ChartUtils.spec.tsx:15-16
Timestamp: 2025-12-16T08:12:06.809Z
Learning: In tests (files under any test directory, e.g., test/, __tests__/, etc.), allow importing internal module paths (e.g., ../../src/...) and do not require imports to use the public API entry point (src/index.ts). The public API import restriction should apply only to non-test code. During reviews, accept internal imports in test files and enforce the public API pattern only for non-test code.
Applied to files:
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx
📚 Learning: 2025-11-25T01:23:08.250Z
Learnt from: CR
Repo: recharts/recharts PR: 0
File: test/README.md:0-0
Timestamp: 2025-11-25T01:23:08.250Z
Learning: Applies to test/**/*.{test,spec}.{ts,tsx} : Mock `getBoundingClientRect` in tests using the helper function provided in `test/helper/MockGetBoundingClientRect.ts`
Applied to files:
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx
📚 Learning: 2025-11-25T01:23:08.250Z
Learnt from: CR
Repo: recharts/recharts PR: 0
File: test/README.md:0-0
Timestamp: 2025-11-25T01:23:08.250Z
Learning: Applies to test/**/*.{test,spec}.{ts,tsx} : Prefer to use the `createSelectorTestCase` helper function when writing or modifying tests
Applied to files:
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx
📚 Learning: 2025-11-25T01:23:08.250Z
Learnt from: CR
Repo: recharts/recharts PR: 0
File: test/README.md:0-0
Timestamp: 2025-11-25T01:23:08.250Z
Learning: Applies to test/**/*.{test,spec}.{ts,tsx} : Use `vi.useFakeTimers()` in all tests due to Redux autoBatchEnhancer dependency on timers and `requestAnimationFrame`
Applied to files:
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx
📚 Learning: 2025-11-25T01:23:08.250Z
Learnt from: CR
Repo: recharts/recharts PR: 0
File: test/README.md:0-0
Timestamp: 2025-11-25T01:23:08.250Z
Learning: Applies to test/**/*.{test,spec}.{ts,tsx} : Call `vi.runOnlyPendingTimers()` to advance timers after renders when not using `createSelectorTestCase` helper, and avoid `vi.runAllTimers()` to prevent infinite loops
Applied to files:
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx
🧬 Code graph analysis (1)
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx (3)
test/helper/createSelectorTestCase.tsx (2)
createSelectorTestCase(78-139)rechartsTestRender(172-184)src/state/selectors/barSelectors.ts (1)
selectBarRectangles(264-343)test/helper/expectBars.ts (1)
getAllBars(13-15)
🔇 Additional comments (1)
test/cartesian/Bar.zeroDimensionFiltering.spec.tsx (1)
1-13: LGTM: Imports and file header are well-structured.The file header clearly explains the optimization being tested, and the imports are appropriate for this test suite.
| it('should filter out zero-dimension rectangles from selectBarRectangles', () => { | ||
| const totalCategories = 100; | ||
| const nonZeroCategories = 10; | ||
| const data = generateSparseStackedData(totalCategories, nonZeroCategories); | ||
|
|
||
| const renderTestCase = createSelectorTestCase(({ children }) => ( | ||
| <BarChart width={800} height={400} data={data}> | ||
| <XAxis dataKey="category" /> | ||
| <YAxis /> | ||
| <Bar id="bar-a" dataKey="stackA" stackId="stack" fill="#8884d8" isAnimationActive={false} /> | ||
| <Bar id="bar-b" dataKey="stackB" stackId="stack" fill="#82ca9d" isAnimationActive={false} /> | ||
| <Bar id="bar-c" dataKey="stackC" stackId="stack" fill="#ffc658" isAnimationActive={false} /> | ||
| {children} | ||
| </BarChart> | ||
| )); | ||
|
|
||
| const { spy } = renderTestCase(state => selectBarRectangles(state, 'bar-a', false, undefined)); | ||
| const rectangles = spy.mock.lastCall?.[0] ?? []; | ||
|
|
||
| expect(rectangles.length).toBeLessThanOrEqual(nonZeroCategories); | ||
|
|
||
| rectangles.forEach((rect: { width: number; height: number }) => { | ||
| expect(rect.width).not.toBe(0); | ||
| expect(rect.height).not.toBe(0); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Add required vi.useFakeTimers() setup.
According to coding guidelines, all tests must use vi.useFakeTimers() due to Redux autoBatchEnhancer dependency on timers and requestAnimationFrame.
🔎 Proposed fix
it('should filter out zero-dimension rectangles from selectBarRectangles', () => {
+ vi.useFakeTimers();
const totalCategories = 100;
const nonZeroCategories = 10;
const data = generateSparseStackedData(totalCategories, nonZeroCategories);
const renderTestCase = createSelectorTestCase(({ children }) => (As per coding guidelines, vi.useFakeTimers() is required in all tests.
📝 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.
| it('should filter out zero-dimension rectangles from selectBarRectangles', () => { | |
| const totalCategories = 100; | |
| const nonZeroCategories = 10; | |
| const data = generateSparseStackedData(totalCategories, nonZeroCategories); | |
| const renderTestCase = createSelectorTestCase(({ children }) => ( | |
| <BarChart width={800} height={400} data={data}> | |
| <XAxis dataKey="category" /> | |
| <YAxis /> | |
| <Bar id="bar-a" dataKey="stackA" stackId="stack" fill="#8884d8" isAnimationActive={false} /> | |
| <Bar id="bar-b" dataKey="stackB" stackId="stack" fill="#82ca9d" isAnimationActive={false} /> | |
| <Bar id="bar-c" dataKey="stackC" stackId="stack" fill="#ffc658" isAnimationActive={false} /> | |
| {children} | |
| </BarChart> | |
| )); | |
| const { spy } = renderTestCase(state => selectBarRectangles(state, 'bar-a', false, undefined)); | |
| const rectangles = spy.mock.lastCall?.[0] ?? []; | |
| expect(rectangles.length).toBeLessThanOrEqual(nonZeroCategories); | |
| rectangles.forEach((rect: { width: number; height: number }) => { | |
| expect(rect.width).not.toBe(0); | |
| expect(rect.height).not.toBe(0); | |
| }); | |
| }); | |
| it('should filter out zero-dimension rectangles from selectBarRectangles', () => { | |
| vi.useFakeTimers(); | |
| const totalCategories = 100; | |
| const nonZeroCategories = 10; | |
| const data = generateSparseStackedData(totalCategories, nonZeroCategories); | |
| const renderTestCase = createSelectorTestCase(({ children }) => ( | |
| <BarChart width={800} height={400} data={data}> | |
| <XAxis dataKey="category" /> | |
| <YAxis /> | |
| <Bar id="bar-a" dataKey="stackA" stackId="stack" fill="#8884d8" isAnimationActive={false} /> | |
| <Bar id="bar-b" dataKey="stackB" stackId="stack" fill="#82ca9d" isAnimationActive={false} /> | |
| <Bar id="bar-c" dataKey="stackC" stackId="stack" fill="#ffc658" isAnimationActive={false} /> | |
| {children} | |
| </BarChart> | |
| )); | |
| const { spy } = renderTestCase(state => selectBarRectangles(state, 'bar-a', false, undefined)); | |
| const rectangles = spy.mock.lastCall?.[0] ?? []; | |
| expect(rectangles.length).toBeLessThanOrEqual(nonZeroCategories); | |
| rectangles.forEach((rect: { width: number; height: number }) => { | |
| expect(rect.width).not.toBe(0); | |
| expect(rect.height).not.toBe(0); | |
| }); | |
| }); |
🤖 Prompt for AI Agents
In test/cartesian/Bar.zeroDimensionFiltering.spec.tsx around lines 45 to 70, the
test is missing vi.useFakeTimers() which is required because Redux
autoBatchEnhancer relies on timers and requestAnimationFrame; update the test to
call vi.useFakeTimers() before rendering (e.g., in the test body or a
beforeEach) and ensure timers are restored after the test with
vi.useRealTimers() (or in an afterEach) so other tests are not affected.
| it('should render only non-zero rectangles as DOM elements', () => { | ||
| const totalCategories = 50; | ||
| const nonZeroCategories = 5; | ||
| const data = generateSparseStackedData(totalCategories, nonZeroCategories); | ||
|
|
||
| const { container } = rechartsTestRender( | ||
| <BarChart width={800} height={400} data={data}> | ||
| <XAxis dataKey="category" /> | ||
| <YAxis /> | ||
| <Bar dataKey="stackA" stackId="stack" fill="#8884d8" isAnimationActive={false} /> | ||
| </BarChart>, | ||
| ); | ||
|
|
||
| const bars = getAllBars(container); | ||
|
|
||
| expect(bars.length).toBeLessThanOrEqual(nonZeroCategories); | ||
| }); |
There was a problem hiding this comment.
Add required timer setup for render test.
This test is missing:
vi.useFakeTimers()setup (required in all tests)vi.runOnlyPendingTimers()after render (required when usingrechartsTestRender)
🔎 Proposed fix
it('should render only non-zero rectangles as DOM elements', () => {
+ vi.useFakeTimers();
const totalCategories = 50;
const nonZeroCategories = 5;
const data = generateSparseStackedData(totalCategories, nonZeroCategories);
const { container } = rechartsTestRender(
<BarChart width={800} height={400} data={data}>
<XAxis dataKey="category" />
<YAxis />
<Bar dataKey="stackA" stackId="stack" fill="#8884d8" isAnimationActive={false} />
</BarChart>,
);
+ vi.runOnlyPendingTimers();
const bars = getAllBars(container);As per coding guidelines, timer setup is mandatory for all tests.
🤖 Prompt for AI Agents
In test/cartesian/Bar.zeroDimensionFiltering.spec.tsx around lines 72 to 88, the
test is missing the required fake-timer setup and post-render timer flush; add
vi.useFakeTimers() before calling rechartsTestRender(...) and call
vi.runOnlyPendingTimers() immediately after the render returns (before querying
DOM with getAllBars), and optionally restore timers (vi.useRealTimers()) at the
end of the test if other tests rely on real timers.
| it('should reduce component tree size proportionally to sparsity', () => { | ||
| const scenarios = [ | ||
| { totalCategories: 100, nonZeroCategories: 10 }, | ||
| { totalCategories: 500, nonZeroCategories: 25 }, | ||
| { totalCategories: 1000, nonZeroCategories: 50 }, | ||
| ]; | ||
|
|
||
| scenarios.forEach(({ totalCategories, nonZeroCategories }) => { | ||
| const data = generateSparseStackedData(totalCategories, nonZeroCategories); | ||
|
|
||
| const { container } = rechartsTestRender( | ||
| <BarChart width={800} height={400} data={data}> | ||
| <XAxis dataKey="category" /> | ||
| <YAxis /> | ||
| <Bar dataKey="stackA" stackId="stack" fill="#8884d8" isAnimationActive={false} /> | ||
| <Bar dataKey="stackB" stackId="stack" fill="#82ca9d" isAnimationActive={false} /> | ||
| <Bar dataKey="stackC" stackId="stack" fill="#ffc658" isAnimationActive={false} /> | ||
| </BarChart>, | ||
| ); | ||
|
|
||
| const actualBars = getAllBars(container).length; | ||
| const potentialBars = totalCategories * 3; | ||
| const reductionPercent = Math.round((1 - actualBars / potentialBars) * 100); | ||
|
|
||
| expect(actualBars).toBeLessThan(potentialBars); | ||
| expect(reductionPercent).toBeGreaterThanOrEqual(90); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Add required timer setup for multiple render scenarios.
This test renders multiple scenarios but is missing:
vi.useFakeTimers()setup (required in all tests)vi.runOnlyPendingTimers()after each render in the loop
🔎 Proposed fix
it('should reduce component tree size proportionally to sparsity', () => {
+ vi.useFakeTimers();
const scenarios = [
{ totalCategories: 100, nonZeroCategories: 10 },
{ totalCategories: 500, nonZeroCategories: 25 },
{ totalCategories: 1000, nonZeroCategories: 50 },
];
scenarios.forEach(({ totalCategories, nonZeroCategories }) => {
const data = generateSparseStackedData(totalCategories, nonZeroCategories);
const { container } = rechartsTestRender(
<BarChart width={800} height={400} data={data}>
<XAxis dataKey="category" />
<YAxis />
<Bar dataKey="stackA" stackId="stack" fill="#8884d8" isAnimationActive={false} />
<Bar dataKey="stackB" stackId="stack" fill="#82ca9d" isAnimationActive={false} />
<Bar dataKey="stackC" stackId="stack" fill="#ffc658" isAnimationActive={false} />
</BarChart>,
);
+ vi.runOnlyPendingTimers();
const actualBars = getAllBars(container).length;As per coding guidelines, timer setup is mandatory for all tests.
🤖 Prompt for AI Agents
In test/cartesian/Bar.zeroDimensionFiltering.spec.tsx around lines 90 to 117,
the test runs multiple render scenarios but lacks timer setup and flushing; call
vi.useFakeTimers() before the scenarios loop to enable fake timers for the test,
and after each rechartsTestRender(...) inside the loop call
vi.runOnlyPendingTimers() to flush pending timers from that render (and
optionally vi.clearAllTimers() if needed); finally restore timers at the end of
the test with vi.useRealTimers() to avoid affecting other tests.
| it('should reduce rectangle count at selector level', () => { | ||
| const totalCategories = 200; | ||
| const nonZeroCategories = 20; | ||
| const numberOfStackedBars = 3; | ||
| const data = generateSparseStackedData(totalCategories, nonZeroCategories); | ||
|
|
||
| const renderTestCase = createSelectorTestCase(({ children }) => ( | ||
| <BarChart width={800} height={400} data={data}> | ||
| <XAxis dataKey="category" /> | ||
| <YAxis /> | ||
| <Bar id="bar-a" dataKey="stackA" stackId="stack" fill="#8884d8" isAnimationActive={false} /> | ||
| <Bar id="bar-b" dataKey="stackB" stackId="stack" fill="#82ca9d" isAnimationActive={false} /> | ||
| <Bar id="bar-c" dataKey="stackC" stackId="stack" fill="#ffc658" isAnimationActive={false} /> | ||
| {children} | ||
| </BarChart> | ||
| )); | ||
|
|
||
| const { spy: spyA } = renderTestCase(state => selectBarRectangles(state, 'bar-a', false, undefined)); | ||
| const { spy: spyB } = renderTestCase(state => selectBarRectangles(state, 'bar-b', false, undefined)); | ||
| const { spy: spyC } = renderTestCase(state => selectBarRectangles(state, 'bar-c', false, undefined)); | ||
|
|
||
| const actualRectangles = | ||
| (spyA.mock.lastCall?.[0]?.length ?? 0) + | ||
| (spyB.mock.lastCall?.[0]?.length ?? 0) + | ||
| (spyC.mock.lastCall?.[0]?.length ?? 0); | ||
|
|
||
| const potentialRectangles = totalCategories * numberOfStackedBars; | ||
| const percentSaved = Math.round(((potentialRectangles - actualRectangles) / potentialRectangles) * 100); | ||
|
|
||
| expect(percentSaved).toBeGreaterThan(80); | ||
| expect(actualRectangles).toBeLessThanOrEqual(nonZeroCategories * numberOfStackedBars); | ||
| }); |
There was a problem hiding this comment.
Add required vi.useFakeTimers() setup.
This test is missing vi.useFakeTimers() setup, which is required in all tests according to coding guidelines.
🔎 Proposed fix
it('should reduce rectangle count at selector level', () => {
+ vi.useFakeTimers();
const totalCategories = 200;
const nonZeroCategories = 20;
const numberOfStackedBars = 3;As per coding guidelines, vi.useFakeTimers() is required in all tests.
📝 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.
| it('should reduce rectangle count at selector level', () => { | |
| const totalCategories = 200; | |
| const nonZeroCategories = 20; | |
| const numberOfStackedBars = 3; | |
| const data = generateSparseStackedData(totalCategories, nonZeroCategories); | |
| const renderTestCase = createSelectorTestCase(({ children }) => ( | |
| <BarChart width={800} height={400} data={data}> | |
| <XAxis dataKey="category" /> | |
| <YAxis /> | |
| <Bar id="bar-a" dataKey="stackA" stackId="stack" fill="#8884d8" isAnimationActive={false} /> | |
| <Bar id="bar-b" dataKey="stackB" stackId="stack" fill="#82ca9d" isAnimationActive={false} /> | |
| <Bar id="bar-c" dataKey="stackC" stackId="stack" fill="#ffc658" isAnimationActive={false} /> | |
| {children} | |
| </BarChart> | |
| )); | |
| const { spy: spyA } = renderTestCase(state => selectBarRectangles(state, 'bar-a', false, undefined)); | |
| const { spy: spyB } = renderTestCase(state => selectBarRectangles(state, 'bar-b', false, undefined)); | |
| const { spy: spyC } = renderTestCase(state => selectBarRectangles(state, 'bar-c', false, undefined)); | |
| const actualRectangles = | |
| (spyA.mock.lastCall?.[0]?.length ?? 0) + | |
| (spyB.mock.lastCall?.[0]?.length ?? 0) + | |
| (spyC.mock.lastCall?.[0]?.length ?? 0); | |
| const potentialRectangles = totalCategories * numberOfStackedBars; | |
| const percentSaved = Math.round(((potentialRectangles - actualRectangles) / potentialRectangles) * 100); | |
| expect(percentSaved).toBeGreaterThan(80); | |
| expect(actualRectangles).toBeLessThanOrEqual(nonZeroCategories * numberOfStackedBars); | |
| }); | |
| it('should reduce rectangle count at selector level', () => { | |
| vi.useFakeTimers(); | |
| const totalCategories = 200; | |
| const nonZeroCategories = 20; | |
| const numberOfStackedBars = 3; | |
| const data = generateSparseStackedData(totalCategories, nonZeroCategories); | |
| const renderTestCase = createSelectorTestCase(({ children }) => ( | |
| <BarChart width={800} height={400} data={data}> | |
| <XAxis dataKey="category" /> | |
| <YAxis /> | |
| <Bar id="bar-a" dataKey="stackA" stackId="stack" fill="#8884d8" isAnimationActive={false} /> | |
| <Bar id="bar-b" dataKey="stackB" stackId="stack" fill="#82ca9d" isAnimationActive={false} /> | |
| <Bar id="bar-c" dataKey="stackC" stackId="stack" fill="#ffc658" isAnimationActive={false} /> | |
| {children} | |
| </BarChart> | |
| )); | |
| const { spy: spyA } = renderTestCase(state => selectBarRectangles(state, 'bar-a', false, undefined)); | |
| const { spy: spyB } = renderTestCase(state => selectBarRectangles(state, 'bar-b', false, undefined)); | |
| const { spy: spyC } = renderTestCase(state => selectBarRectangles(state, 'bar-c', false, undefined)); | |
| const actualRectangles = | |
| (spyA.mock.lastCall?.[0]?.length ?? 0) + | |
| (spyB.mock.lastCall?.[0]?.length ?? 0) + | |
| (spyC.mock.lastCall?.[0]?.length ?? 0); | |
| const potentialRectangles = totalCategories * numberOfStackedBars; | |
| const percentSaved = Math.round(((potentialRectangles - actualRectangles) / potentialRectangles) * 100); | |
| expect(percentSaved).toBeGreaterThan(80); | |
| expect(actualRectangles).toBeLessThanOrEqual(nonZeroCategories * numberOfStackedBars); | |
| }); |
🤖 Prompt for AI Agents
In test/cartesian/Bar.zeroDimensionFiltering.spec.tsx around lines 119 to 150,
the test is missing the required vi.useFakeTimers() setup; add a call to
vi.useFakeTimers() before the test logic runs (either at the top of this it
block or in a surrounding beforeEach), and ensure timers are restored after the
test (e.g., call vi.useRealTimers() in an afterEach or at the end of the test)
so the test follows the project's testing guidelines.
| it('should handle sparse dashboard data efficiently', () => { | ||
| const months = 36; | ||
| const metrics = 50; | ||
| const sparsity = 0.85; | ||
|
|
||
| const data: Array<Record<string, number | string>> = []; | ||
| for (let month = 0; month < months; month++) { | ||
| const entry: Record<string, number | string> = { month: `Month ${month + 1}` }; | ||
| for (let metric = 0; metric < metrics; metric++) { | ||
| entry[`metric${metric}`] = Math.random() > sparsity ? Math.floor(Math.random() * 100) + 1 : 0; | ||
| } | ||
| data.push(entry); | ||
| } | ||
|
|
||
| const { container } = rechartsTestRender( | ||
| <BarChart width={1200} height={600} data={data}> | ||
| <XAxis dataKey="month" /> | ||
| <YAxis /> | ||
| {Array.from({ length: metrics }, (_, i) => ( | ||
| <Bar | ||
| key={i} | ||
| dataKey={`metric${i}`} | ||
| stackId="stack" | ||
| fill={`hsl(${(i * 7) % 360}, 70%, 50%)`} | ||
| isAnimationActive={false} | ||
| /> | ||
| ))} | ||
| </BarChart>, | ||
| ); | ||
|
|
||
| const actualBars = getAllBars(container).length; | ||
| const potentialBars = months * metrics; | ||
|
|
||
| expect(actualBars).toBeLessThan(potentialBars * 0.5); | ||
| }); |
There was a problem hiding this comment.
Add required timer setup for dashboard data test.
This test is missing:
vi.useFakeTimers()setup (required in all tests)vi.runOnlyPendingTimers()after render (required when usingrechartsTestRender)
🔎 Proposed fix
it('should handle sparse dashboard data efficiently', () => {
+ vi.useFakeTimers();
const months = 36;
const metrics = 50;
const sparsity = 0.85;
const data: Array<Record<string, number | string>> = [];
for (let month = 0; month < months; month++) {
const entry: Record<string, number | string> = { month: `Month ${month + 1}` };
for (let metric = 0; metric < metrics; metric++) {
entry[`metric${metric}`] = Math.random() > sparsity ? Math.floor(Math.random() * 100) + 1 : 0;
}
data.push(entry);
}
const { container } = rechartsTestRender(
<BarChart width={1200} height={600} data={data}>
<XAxis dataKey="month" />
<YAxis />
{Array.from({ length: metrics }, (_, i) => (
<Bar
key={i}
dataKey={`metric${i}`}
stackId="stack"
fill={`hsl(${(i * 7) % 360}, 70%, 50%)`}
isAnimationActive={false}
/>
))}
</BarChart>,
);
+ vi.runOnlyPendingTimers();
const actualBars = getAllBars(container).length;As per coding guidelines, timer setup is mandatory for all tests.
📝 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.
| it('should handle sparse dashboard data efficiently', () => { | |
| const months = 36; | |
| const metrics = 50; | |
| const sparsity = 0.85; | |
| const data: Array<Record<string, number | string>> = []; | |
| for (let month = 0; month < months; month++) { | |
| const entry: Record<string, number | string> = { month: `Month ${month + 1}` }; | |
| for (let metric = 0; metric < metrics; metric++) { | |
| entry[`metric${metric}`] = Math.random() > sparsity ? Math.floor(Math.random() * 100) + 1 : 0; | |
| } | |
| data.push(entry); | |
| } | |
| const { container } = rechartsTestRender( | |
| <BarChart width={1200} height={600} data={data}> | |
| <XAxis dataKey="month" /> | |
| <YAxis /> | |
| {Array.from({ length: metrics }, (_, i) => ( | |
| <Bar | |
| key={i} | |
| dataKey={`metric${i}`} | |
| stackId="stack" | |
| fill={`hsl(${(i * 7) % 360}, 70%, 50%)`} | |
| isAnimationActive={false} | |
| /> | |
| ))} | |
| </BarChart>, | |
| ); | |
| const actualBars = getAllBars(container).length; | |
| const potentialBars = months * metrics; | |
| expect(actualBars).toBeLessThan(potentialBars * 0.5); | |
| }); | |
| it('should handle sparse dashboard data efficiently', () => { | |
| vi.useFakeTimers(); | |
| const months = 36; | |
| const metrics = 50; | |
| const sparsity = 0.85; | |
| const data: Array<Record<string, number | string>> = []; | |
| for (let month = 0; month < months; month++) { | |
| const entry: Record<string, number | string> = { month: `Month ${month + 1}` }; | |
| for (let metric = 0; metric < metrics; metric++) { | |
| entry[`metric${metric}`] = Math.random() > sparsity ? Math.floor(Math.random() * 100) + 1 : 0; | |
| } | |
| data.push(entry); | |
| } | |
| const { container } = rechartsTestRender( | |
| <BarChart width={1200} height={600} data={data}> | |
| <XAxis dataKey="month" /> | |
| <YAxis /> | |
| {Array.from({ length: metrics }, (_, i) => ( | |
| <Bar | |
| key={i} | |
| dataKey={`metric${i}`} | |
| stackId="stack" | |
| fill={`hsl(${(i * 7) % 360}, 70%, 50%)`} | |
| isAnimationActive={false} | |
| /> | |
| ))} | |
| </BarChart>, | |
| ); | |
| vi.runOnlyPendingTimers(); | |
| const actualBars = getAllBars(container).length; | |
| const potentialBars = months * metrics; | |
| expect(actualBars).toBeLessThan(potentialBars * 0.5); | |
| }); |
🤖 Prompt for AI Agents
In test/cartesian/Bar.zeroDimensionFiltering.spec.tsx around lines 152 to 186,
the test is missing fake-timer setup and execution: call vi.useFakeTimers()
before rendering the BarChart (so timers in rechartsTestRender are controlled)
and call vi.runOnlyPendingTimers() immediately after the render to flush any
pending timers; optionally call vi.useRealTimers() or restore timers after the
test if the suite requires it.
| it('should handle extreme sparsity efficiently', () => { | ||
| const dataPoints = 672; | ||
| const errorRate = 0.01; | ||
|
|
||
| const data = Array.from({ length: dataPoints }, (_, i) => ({ | ||
| time: `Hour ${i}`, | ||
| errors: Math.random() < errorRate ? Math.floor(Math.random() * 10) + 1 : 0, | ||
| warnings: Math.random() < errorRate * 2 ? Math.floor(Math.random() * 20) + 1 : 0, | ||
| info: Math.random() < errorRate * 5 ? Math.floor(Math.random() * 50) + 1 : 0, | ||
| })); | ||
|
|
||
| const { container } = rechartsTestRender( | ||
| <BarChart width={1200} height={400} data={data}> | ||
| <XAxis dataKey="time" /> | ||
| <YAxis /> | ||
| <Bar dataKey="errors" stackId="log" fill="#ff4444" isAnimationActive={false} /> | ||
| <Bar dataKey="warnings" stackId="log" fill="#ffaa00" isAnimationActive={false} /> | ||
| <Bar dataKey="info" stackId="log" fill="#4488ff" isAnimationActive={false} /> | ||
| </BarChart>, | ||
| ); | ||
|
|
||
| const actualBars = getAllBars(container).length; | ||
| const potentialBars = dataPoints * 3; | ||
|
|
||
| expect(actualBars).toBeLessThan(potentialBars * 0.2); | ||
| }); |
There was a problem hiding this comment.
Add required timer setup for extreme sparsity test.
This test is missing:
vi.useFakeTimers()setup (required in all tests)vi.runOnlyPendingTimers()after render (required when usingrechartsTestRender)
🔎 Proposed fix
it('should handle extreme sparsity efficiently', () => {
+ vi.useFakeTimers();
const dataPoints = 672;
const errorRate = 0.01;
const data = Array.from({ length: dataPoints }, (_, i) => ({
time: `Hour ${i}`,
errors: Math.random() < errorRate ? Math.floor(Math.random() * 10) + 1 : 0,
warnings: Math.random() < errorRate * 2 ? Math.floor(Math.random() * 20) + 1 : 0,
info: Math.random() < errorRate * 5 ? Math.floor(Math.random() * 50) + 1 : 0,
}));
const { container } = rechartsTestRender(
<BarChart width={1200} height={400} data={data}>
<XAxis dataKey="time" />
<YAxis />
<Bar dataKey="errors" stackId="log" fill="#ff4444" isAnimationActive={false} />
<Bar dataKey="warnings" stackId="log" fill="#ffaa00" isAnimationActive={false} />
<Bar dataKey="info" stackId="log" fill="#4488ff" isAnimationActive={false} />
</BarChart>,
);
+ vi.runOnlyPendingTimers();
const actualBars = getAllBars(container).length;As per coding guidelines, timer setup is mandatory for all tests.
📝 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.
| it('should handle extreme sparsity efficiently', () => { | |
| const dataPoints = 672; | |
| const errorRate = 0.01; | |
| const data = Array.from({ length: dataPoints }, (_, i) => ({ | |
| time: `Hour ${i}`, | |
| errors: Math.random() < errorRate ? Math.floor(Math.random() * 10) + 1 : 0, | |
| warnings: Math.random() < errorRate * 2 ? Math.floor(Math.random() * 20) + 1 : 0, | |
| info: Math.random() < errorRate * 5 ? Math.floor(Math.random() * 50) + 1 : 0, | |
| })); | |
| const { container } = rechartsTestRender( | |
| <BarChart width={1200} height={400} data={data}> | |
| <XAxis dataKey="time" /> | |
| <YAxis /> | |
| <Bar dataKey="errors" stackId="log" fill="#ff4444" isAnimationActive={false} /> | |
| <Bar dataKey="warnings" stackId="log" fill="#ffaa00" isAnimationActive={false} /> | |
| <Bar dataKey="info" stackId="log" fill="#4488ff" isAnimationActive={false} /> | |
| </BarChart>, | |
| ); | |
| const actualBars = getAllBars(container).length; | |
| const potentialBars = dataPoints * 3; | |
| expect(actualBars).toBeLessThan(potentialBars * 0.2); | |
| }); | |
| it('should handle extreme sparsity efficiently', () => { | |
| vi.useFakeTimers(); | |
| const dataPoints = 672; | |
| const errorRate = 0.01; | |
| const data = Array.from({ length: dataPoints }, (_, i) => ({ | |
| time: `Hour ${i}`, | |
| errors: Math.random() < errorRate ? Math.floor(Math.random() * 10) + 1 : 0, | |
| warnings: Math.random() < errorRate * 2 ? Math.floor(Math.random() * 20) + 1 : 0, | |
| info: Math.random() < errorRate * 5 ? Math.floor(Math.random() * 50) + 1 : 0, | |
| })); | |
| const { container } = rechartsTestRender( | |
| <BarChart width={1200} height={400} data={data}> | |
| <XAxis dataKey="time" /> | |
| <YAxis /> | |
| <Bar dataKey="errors" stackId="log" fill="#ff4444" isAnimationActive={false} /> | |
| <Bar dataKey="warnings" stackId="log" fill="#ffaa00" isAnimationActive={false} /> | |
| <Bar dataKey="info" stackId="log" fill="#4488ff" isAnimationActive={false} /> | |
| </BarChart>, | |
| ); | |
| vi.runOnlyPendingTimers(); | |
| const actualBars = getAllBars(container).length; | |
| const potentialBars = dataPoints * 3; | |
| expect(actualBars).toBeLessThan(potentialBars * 0.2); | |
| }); |
🤖 Prompt for AI Agents
In test/cartesian/Bar.zeroDimensionFiltering.spec.tsx around lines 188 to 213,
the "extreme sparsity" test is missing the mandatory fake-timer setup and runner
calls: call vi.useFakeTimers() before rendering the BarChart, then after calling
rechartsTestRender invoke vi.runOnlyPendingTimers() to flush any pending timers
created by the render; ensure timers are reset/ restored in teardown if the test
file's convention requires it.
|
Here are some tests I ran:
Each avoided rectangle saves a This is safe because I'm already using this via a patch on a work project that needed it, and this solved a major performance problem for us. I also added a test ( |
|
How much reduction is that in terms of milliseconds? |
|
On my machine (M4 Mac), the difference is in seconds, not milliseconds. the UI was noticeably laggy before the fix. The exact timing depends on the data shape, but for our use case with ~1000 categories and 95% sparsity across multiple stacked bars, the render went from multiple seconds of lag to essentially instant. |
|
Looks like this change broke the BarStack radius:
Artifact download URL: https://github.com/recharts/recharts/actions/runs/20408390978/artifacts/4936317389 https://github.com/recharts/recharts/tree/main/test-vr#how-to-run-tests |
|
Nice performance increase @MendyLanda! We'll have to fix the above first though - I have a feeling BarStack and this might not work well together if barstack required empty elements to be present |
|
bump? |
|
@ckifer Sorry for the delay. It has been a busy stretch. I patched this locally for now and have not had time to circle back yet. I’m hoping to revisit it next week. If anyone wants to take a stab at it in the meantime, please feel free. |
|
no worries at all, just double checking staleness older PRs 😃 |
There was a problem hiding this comment.
🧹 Nitpick comments (3)
src/cartesian/Bar.tsx (1)
1183-1183: Nit:isInBarStack ? true : undefinedcan be simplified.Since
isInBarStackis already aboolean(from the hook's return type), andcomputeBarRectanglesdefaults it tofalse, you can pass the value directly:- isInBarStack={isInBarStack ? true : undefined} + isInBarStack={isInBarStack || undefined}Both are equivalent, but the shorter form is more idiomatic. If the intent is specifically to avoid storing
falsein Redux state, a brief comment would clarify that.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cartesian/Bar.tsx` at line 1183, Replace the verbose ternary prop assignment isInBarStack={isInBarStack ? true : undefined} with the direct boolean prop isInBarStack={isInBarStack} in Bar.tsx; update the call site that uses the hook-returned isInBarStack (and note computeBarRectangles defaults it to false) — if the original intent was to avoid storing false in Redux, add a one-line comment near the prop or hook call explaining that behavior instead of the ternary.src/cartesian/BarStack.tsx (1)
69-70: Add JSDoc foruseIsInBarStackconsistent with sibling hooks.Other exported hooks in this file (e.g.,
useStackIdat Line 51) have JSDoc comments. Adding one here keeps the file consistent and supports the auto-generated API docs.Suggested doc
+/** + * Returns true when the component is rendered inside a BarStack context, false otherwise. + */ export const useIsInBarStack = (): boolean => useContext(BarStackContext) != null;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cartesian/BarStack.tsx` around lines 69 - 70, Add a JSDoc comment for the exported hook useIsInBarStack to match the style of sibling hooks (e.g., useStackId) in this file: include a short one-line description stating it returns whether the current component is inside a BarStack, an `@returns` tag describing the boolean result, and any relevant tags used by other hooks in this module so the auto-generated API docs remain consistent with the rest of the file.src/state/types/BarSettings.ts (1)
10-10: Missing JSDoc forisInBarStack.Per the project's coding guidelines, TypeScript definitions in source files should be documented as they feed the auto-generated API docs. A brief JSDoc comment clarifying the purpose and default behavior would help.
Suggested doc
+ /** + * Whether this bar is rendered inside a BarStack context. + * When true, zero-dimension rectangles are preserved for clip-path calculations. + * `@default` false + */ isInBarStack?: boolean;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/state/types/BarSettings.ts` at line 10, Add a JSDoc comment for the isInBarStack property on the BarSettings interface explaining that it is a boolean flag indicating whether the item is part of the "bar stack" and documenting the default behavior (e.g., when omitted it is false), so the auto-generated API docs convey its purpose and default. Ensure the JSDoc is placed immediately above the isInBarStack declaration and references the property name exactly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/cartesian/Bar.tsx`:
- Line 1183: Replace the verbose ternary prop assignment
isInBarStack={isInBarStack ? true : undefined} with the direct boolean prop
isInBarStack={isInBarStack} in Bar.tsx; update the call site that uses the
hook-returned isInBarStack (and note computeBarRectangles defaults it to false)
— if the original intent was to avoid storing false in Redux, add a one-line
comment near the prop or hook call explaining that behavior instead of the
ternary.
In `@src/cartesian/BarStack.tsx`:
- Around line 69-70: Add a JSDoc comment for the exported hook useIsInBarStack
to match the style of sibling hooks (e.g., useStackId) in this file: include a
short one-line description stating it returns whether the current component is
inside a BarStack, an `@returns` tag describing the boolean result, and any
relevant tags used by other hooks in this module so the auto-generated API docs
remain consistent with the rest of the file.
In `@src/state/types/BarSettings.ts`:
- Line 10: Add a JSDoc comment for the isInBarStack property on the BarSettings
interface explaining that it is a boolean flag indicating whether the item is
part of the "bar stack" and documenting the default behavior (e.g., when omitted
it is false), so the auto-generated API docs convey its purpose and default.
Ensure the JSDoc is placed immediately above the isInBarStack declaration and
references the property name exactly.
|
@PavelVanecek @ckifer I think this one's ready |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/cartesian/Bar.tsx (1)
1183-1184:isInBarStack || undefinedis non-idiomatic; preferisInBarStack ? true : undefined.
false || undefinedevaluates toundefined, so the expression works, but it reads as a boolean OR rather than a conditional assignment. The inline comment clarifies intent but the ternary form makes it self-evident:- // false fallback is intentional, otherwise we need to update 10 test files with the new prop - isInBarStack={isInBarStack || undefined} + isInBarStack={isInBarStack ? true : undefined}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cartesian/Bar.tsx` around lines 1183 - 1184, Replace the non-idiomatic fallback expression used when passing the isInBarStack prop (currently written as isInBarStack || undefined) with an explicit conditional so it reads as intended; update the prop assignment in Bar (the isInBarStack prop passed in the Bar component) to use isInBarStack ? true : undefined to make the boolean-to-undefined conversion explicit and self-documenting.
🤖 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/cartesian/Bar.tsx`:
- Around line 1110-1122: The zero-dimension filter is currently skipped for
stacked bars because clip-path indexing relies on filtered-array indices;
instead, change clip-paths and stack-rect indexing to use the preserved
originalDataIndex so zero-size entries can be filtered early. Update
BarRectangles to pass originalDataIndex through each BarRectangleItem, change
BarStackClipPath to generate/lookup clip-path IDs keyed by originalDataIndex
(not the filtered loop index), and modify combineStackRects in
barStackSelectors.ts to store/access stackRects by originalDataIndex so the
filtered array and clip-path references remain consistent.
---
Nitpick comments:
In `@src/cartesian/Bar.tsx`:
- Around line 1183-1184: Replace the non-idiomatic fallback expression used when
passing the isInBarStack prop (currently written as isInBarStack || undefined)
with an explicit conditional so it reads as intended; update the prop assignment
in Bar (the isInBarStack prop passed in the Bar component) to use isInBarStack ?
true : undefined to make the boolean-to-undefined conversion explicit and
self-documenting.
Bundle ReportChanges will increase total bundle size by 835 bytes (0.03%) ⬆️. This is within the configured threshold ✅ Detailed changes
Affected Assets, Files, and Routes:view changes for bundle: recharts/bundle-cjsAssets Changed:
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/cartesian/Bar.tsx (1)
594-601:⚠️ Potential issue | 🟠 Major
BarBackground.isActive(line 512) has the same stale-index bug but was not updated.The fix here correctly uses
entry.originalDataIndexto matchactiveIndex(which is based on the pre-filter data position). However,BarBackgroundat line 512 still computes:isActive: String(i) === activeIndex,
iis the position in the filtered array, whileactiveIndexfromselectActiveTooltipIndextracks the pre-filter data index. For a sparse chart where entry at position 0 is filtered out and entry at position 1 is rendered at filtered index0,String(0) === "1"is false — the background never appears active. This should be updated for consistency:🐛 Proposed fix for `BarBackground` (line 512)
-isActive: String(i) === activeIndex, +isActive: String(entry.originalDataIndex) === activeIndex,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cartesian/Bar.tsx` around lines 594 - 601, BarBackground.isActive is using the filtered-array index `i` to compare against `activeIndex`, causing a stale-index bug when entries are filtered; update the isActive computation in the BarBackground component to use the underlying data index (e.g., `entry.originalDataIndex`) instead of `i` so it matches how the active index is selected (cast to String if necessary) and preserve any existing activeDataKey/dataKey checks used elsewhere (mirror the fix applied in Bar.tsx where isActive compares String(entry.originalDataIndex) === activeIndex).
🧹 Nitpick comments (2)
test/state/selectors/barStackSelectors.spec.tsx (1)
219-221: Consider usingexpectLastCalledWithfor the length/index assertions where possible.Lines 219–221 access
spy.mock.lastCall?.[0]directly. The cross-comparison on line 230 legitimately requires a local reference, but the individual property assertions (toHaveLength,toBeUndefined,objectContaining) could useexpectLastCalledWithfor type safety. As per coding guidelines, preferexpectLastCalledWithover direct mock access.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test/state/selectors/barStackSelectors.spec.tsx` around lines 219 - 221, Replace direct access to spy.mock.lastCall?.[0] for individual property assertions by using Jest's expectLastCalledWith on the relevant spies: use expect(stackSpy).lastCalledWith(...) / expect(firstBarSpy).lastCalledWith(...) / expect(secondBarSpy).lastCalledWith(...) (or expect(spy).toHaveBeenLastCalledWith) to assert lengths and properties (the assertions currently using toHaveLength, toBeUndefined, and objectContaining) for better type-safety; retain the local stackRects variable (from stackSpy.mock.lastCall?.[0]) only where you need to cross-compare arrays between spies (the cross-comparison logic must keep the local reference), but convert the individual assertions against firstBarSpy and secondBarSpy to expect(...).toHaveBeenLastCalledWith(...) patterns.src/cartesian/Bar.tsx (1)
723-757:BarStackClipLayerclip-path index is correct; animation frame interpolation has a related gap.Line 726 correctly uses
entry.originalDataIndexfor clip-path lookup — the core of the index-stability fix. LGTM.However,
RectanglesWithAnimationat line ~817 usesprevData[index]whereindexis the current filtered-array position, notoriginalDataIndex. If the filtered set changes between renders (e.g., a bar transitions from non-zero to zero),prevData[0]may correspond to a logically different entry, causing the bar to animate from a wrong previous position.♻️ Suggested fix for animation interpolation in `RectanglesWithAnimation`
Build a lookup map from
originalDataIndex → prev rectbefore the animation step, so each entry interpolates from its own previous frame regardless of filtering changes:+ const prevDataMap = React.useMemo( + () => new Map(prevData?.map(r => [r.originalDataIndex, r])), + [prevData], + ); const stepData = t === 1 ? data : data?.map((entry: BarRectangleItem, index: number): BarRectangleItem => { - const prev = prevData && prevData[index]; + const prev = prevDataMap?.get(entry.originalDataIndex);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cartesian/Bar.tsx` around lines 723 - 757, The animation uses prevData[index] which is the filtered-array position and can mismatch entries when filtering changes; in RectanglesWithAnimation, build a lookup map keyed by each rectangle's originalDataIndex (e.g., const prevMap = new Map(prevData.map(r => [r.originalDataIndex, r]))) before interpolation, and then replace uses of prevData[index] with prevMap.get(entry.originalDataIndex) (falling back to a sensible default if missing) so each entry interpolates from its true previous frame; keep BarStackClipLayer usage as-is.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@src/cartesian/Bar.tsx`:
- Around line 594-601: BarBackground.isActive is using the filtered-array index
`i` to compare against `activeIndex`, causing a stale-index bug when entries are
filtered; update the isActive computation in the BarBackground component to use
the underlying data index (e.g., `entry.originalDataIndex`) instead of `i` so it
matches how the active index is selected (cast to String if necessary) and
preserve any existing activeDataKey/dataKey checks used elsewhere (mirror the
fix applied in Bar.tsx where isActive compares String(entry.originalDataIndex)
=== activeIndex).
---
Duplicate comments:
In `@test/cartesian/Bar.zeroDimensionFiltering.spec.tsx`:
- Line 9: The test file imports describe/expect/it from 'vitest' but is missing
the vi object required for vi.useFakeTimers(); update the import so the vi
symbol is imported from 'vitest' (e.g., include vi in the import that currently
imports describe/expect/it) so vi.useFakeTimers() can be called in the test
setup.
- Around line 78-100: Add timer setup/flush around the calls to
rechartsTestRender: call vi.useFakeTimers() immediately before invoking
rechartsTestRender(...) in each affected it block, then call
vi.runOnlyPendingTimers() (or vi.advanceTimersToNext()/appropriate flush) right
after the render to ensure pending timers/RAF run, and finally restore timers
with vi.useRealTimers() if other tests rely on real timers; update the it blocks
containing rechartsTestRender and reference the test helper rechartsTestRender
and the timer helpers vi.useFakeTimers and vi.runOnlyPendingTimers when making
the changes.
- Around line 15-42: The test uses non-deterministic randoms inside
generateSparseStackedData which makes the suite flaky; replace the
Math.random-based index selection and value generation with a deterministic
pattern (e.g., pick the first nonZeroCount indices or use (i % k) stepping for
non-zero positions) and assign fixed or step-based values for
stackA/stackB/stackC (e.g., 10, 20, 30 or i*10+10) so assertions like those
checking reductionPercent and the ranges in the stacked-series assertions are
stable; update generateSparseStackedData to compute nonZeroIndices
deterministically and to return fixed non-zero values instead of calling
Math.random().
---
Nitpick comments:
In `@src/cartesian/Bar.tsx`:
- Around line 723-757: The animation uses prevData[index] which is the
filtered-array position and can mismatch entries when filtering changes; in
RectanglesWithAnimation, build a lookup map keyed by each rectangle's
originalDataIndex (e.g., const prevMap = new Map(prevData.map(r =>
[r.originalDataIndex, r]))) before interpolation, and then replace uses of
prevData[index] with prevMap.get(entry.originalDataIndex) (falling back to a
sensible default if missing) so each entry interpolates from its true previous
frame; keep BarStackClipLayer usage as-is.
In `@test/state/selectors/barStackSelectors.spec.tsx`:
- Around line 219-221: Replace direct access to spy.mock.lastCall?.[0] for
individual property assertions by using Jest's expectLastCalledWith on the
relevant spies: use expect(stackSpy).lastCalledWith(...) /
expect(firstBarSpy).lastCalledWith(...) /
expect(secondBarSpy).lastCalledWith(...) (or
expect(spy).toHaveBeenLastCalledWith) to assert lengths and properties (the
assertions currently using toHaveLength, toBeUndefined, and objectContaining)
for better type-safety; retain the local stackRects variable (from
stackSpy.mock.lastCall?.[0]) only where you need to cross-compare arrays between
spies (the cross-comparison logic must keep the local reference), but convert
the individual assertions against firstBarSpy and secondBarSpy to
expect(...).toHaveBeenLastCalledWith(...) patterns.
…hape` prop (#7067) ## Description PR #6800 introduced zero-dimension bar filtering for perf (skip rendering `width === 0 || height === 0` bars). This broke the BoxPlot pattern, which uses zero-height stacked bars with a custom `shape` prop to render horizontal line markers at stack positions. ### Changes - **`src/state/types/BarSettings.ts`**: Added `hasCustomShape: boolean` to `BarSettings` - **`src/cartesian/Bar.tsx`** (registration): Passes `hasCustomShape={props.shape != null}` into Redux state when Bar mounts - **`src/cartesian/Bar.tsx`** (`computeBarRectangles`): Zero-dimension filter now skips bars with a custom shape — they may still produce visible output even at zero height/width ```tsx // BoxPlot pattern: zero-height bar, but shape renders a line at that y position <Bar stackId="a" dataKey="marker" shape={<HorizonBar />} /> // Previously filtered out by width===0||height===0; now preserved when shape is set ``` - Updated all `BarSettings` snapshots across 8+ test files to include `hasCustomShape: false` - Added test case in `Bar.zeroDimensionFiltering.spec.tsx` asserting custom-shape zero-height bars are not filtered ## Related Issue https://github.com/recharts/recharts/issues/ <!-- linked by system --> ## Motivation and Context The BoxPlot storybook example went blank after #6800. Zero-height bars are a legitimate pattern when a custom `shape` handles rendering — the bar coordinates are still needed for positioning. ## How Has This Been Tested? - New unit test: `should not filter out zero-height bars that have a custom shape (BoxPlot use case)` — verifies all data points produce a `BarRectangleItem` with `height === 0` instead of being dropped - Full test suite: 286 test files, 14 105 tests pass - TypeScript: `tsc --noEmit` clean across `src/`, `test/`, and `storybook/` ## Screenshots (if appropriate): N/A (storybook visual regression covers this via Chromatic) ## Types of changes - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) ## Checklist: - [ ] My change requires a change to the documentation. - [ ] I have updated the documentation accordingly. - [x] 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 <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > > ---- > > *This section details on the original issue you should resolve* > > <issue_title>[Unreleased] Missing error bars in BoxPlot example</issue_title> > <issue_description>https://www.chromatic.com/test?appId=63da8268a0da9970db6992aa&id=69a18bb251ba943c7ece0737 > > Must be from some recent PR</issue_description> > > ## Comments on the Issue (you are @copilot in this section) > > <comments> > <comment_new><author>@PavelVanecek</author><body> > Hm it's #6800 > > Perf improvement is nice but it erases zero-height bars which we used to fake boxplot.</body></comment_new> > <comment_new><author>@ckifer</author><body> > Hmmmmm. Gotta fix before release I suppose. Revert the filter? Something else?</body></comment_new> > <comment_new><author>@ckifer</author><body> > Probably revert and add explicit box plot support later? Then can filter when not doing box plot?</body></comment_new> > <comment_new><author>@PavelVanecek</author><body> > Or render bars that have nonzero stroke width </body></comment_new> > </comments> > </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #7064 <!-- START COPILOT CODING AGENT TIPS --> --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Custom-shaped bars now render correctly with zero dimensions, enabling expanded visualization customization options for bar charts. * **Tests** * Updated test coverage for custom-shaped bar rendering behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: PavelVanecek <1100170+PavelVanecek@users.noreply.github.com>



Filter out zero-dimension rectangles early in computeBarRectangles to avoid creating unnecessary React component trees for sparse stacked bar charts.
Problem:
For stacked bars with sparse data, recharts builds the full component tree for each empty rectangle before Rectangle returns null, causing lag with many categories.
Solution:
Add width === 0 || height === 0 to the existing null check in computeBarRectangles. Safe because Rectangle already returns null for these—we're just checking earlier.
Changes:
Updated computeBarRectangles to filter zero-dimension rectangles early
Updated test to expect only non-zero rectangles
Fixes #354
Summary by CodeRabbit
Release Notes