Skip to content

Perf: filter zero-dimension rectangles early#6800

Merged
ckifer merged 5 commits intorecharts:mainfrom
MendyLanda:main
Feb 22, 2026
Merged

Perf: filter zero-dimension rectangles early#6800
ckifer merged 5 commits intorecharts:mainfrom
MendyLanda:main

Conversation

@MendyLanda
Copy link
Contributor

@MendyLanda MendyLanda commented Dec 18, 2025

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

  • Bug Fixes
    • Fixed bar chart rendering with sparse or filtered data to ensure tooltips and active states align correctly.
    • Zero-dimension bars (bars with no visible width or height) are now automatically filtered to improve rendering performance and reduce DOM overhead.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 18, 2025

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR introduces index stability for BarStack rendering by using entry.originalDataIndex instead of iteration-based indices. Changes include filtering zero-dimension rectangles, updating clip-path indexing in multiple render paths, and comprehensive tests validating sparse data handling and index alignment after filtering.

Changes

Cohort / File(s) Summary
BarStack Index Stability
src/cartesian/Bar.tsx
Updated BarRectangleItem, BarRectangleWithActiveState, and BarRectangles to use entry.originalDataIndex for clip-path indexing instead of iteration indices. Added zero-dimension rectangle filtering (width/height === 0) in computeBarRectangles with documentation reflecting stable pre-filter index usage for tooltip matching and clipping behavior.
Selector Index Alignment
src/state/selectors/barStackSelectors.ts
Updated combineStackRects to use rect.originalDataIndex instead of loop iteration index when merging bar rectangles in stackRects array, ensuring index alignment across filtered data.
Bar Component Tests
test/cartesian/Bar.truncateByDomain.spec.tsx, test/cartesian/Bar.zeroDimensionFiltering.spec.tsx
Updated existing test expectations and added comprehensive new test suite validating zero-dimension rectangle filtering, originalDataIndex preservation, clip-path alignment, and performance with sparse dashboard data across multiple bar series.
Selector Tests
test/state/selectors/barStackSelectors.spec.tsx
Added test scenario for sparse BarStack data after zero-dimension filtering, verifying originalDataIndex preservation and combined stack rectangle alignment through expandRectangle logic.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • #6906: Modifies Bar.tsx rendering with BarStackClipLayer index prop usage, directly related to the clip-layer indexing changes in this PR.
  • #6593: Updates BarRectangleItem and computeBarRectangles in Bar.tsx for stacked bar animation, shares overlapping code regions with this PR's index stability implementation.

Suggested labels

enhancement

Suggested reviewers

  • ckifer
  • PavelVanecek
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning A visual regression with BarStack radius rendering was reported by reviewers, suggesting implementation may have unintended side effects beyond the stated scope of filtering zero-dimension rectangles. Address the reported BarStack radius rendering regression and verify that filtering does not break other features that may depend on empty element presence before merging.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed Title clearly and concisely summarizes the main change: filtering zero-dimension rectangles early for performance optimization in bar charts.
Description check ✅ Passed Description covers the problem, solution, and changes with clear context, though it could be more detailed on testing approach and does not explicitly check all template sections.
Linked Issues check ✅ Passed Changes substantially address issue #354 by filtering zero-dimension rectangles early to reduce unnecessary React component creation for sparse stacked bars, with documented performance improvements (90-95% reduction).

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

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

@codecov
Copy link

codecov bot commented Dec 18, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.13%. Comparing base (bcc8f6f) to head (241fbea).
⚠️ Report is 11 commits behind head on main.

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

Copy link
Collaborator

@PavelVanecek PavelVanecek left a comment

Choose a reason for hiding this comment

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

Thanks for the PR. Do you have some measurements on the performance improvement?

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (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

📥 Commits

Reviewing files that changed from the base of the PR and between fdad2d9 and 96614a9.

📒 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 use any type (implicit or explicit) in TypeScript code
Prefer unknown over any and 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 use as type assertions in TypeScript; the only exception is as 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 lint and 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 the createSelectorTestCase helper function when writing or modifying tests
Use the expectLastCalledWith helper function instead of expect(spy).toHaveBeenLastCalledWith(...) for better typing and autocompletion
Verify the number of selector calls using the spy object from createSelectorTestCase to spot unnecessary re-renders and improve performance
Mock getBoundingClientRect in tests using the helper function provided in test/helper/MockGetBoundingClientRect.ts
Use vi.useFakeTimers() in all tests due to Redux autoBatchEnhancer dependency on timers and requestAnimationFrame
Call vi.runOnlyPendingTimers() to advance timers after renders when not using createSelectorTestCase helper, and avoid vi.runAllTimers() to prevent infinite loops
Use userEvent.setup({ advanceTimers: vi.runOnlyPendingTimers }) or the userEventSetup helper function from test/helper/userEventSetup.ts when creating userEvent instances
When testing tooltips on hover, use vi.runOnlyPendingTimers() after each userEvent.hover() call or use the showTooltip helper function from tooltipTestHelpers.ts to 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.tsx rather than running all tests with npm test

Files:

  • test/cartesian/Bar.zeroDimensionFiltering.spec.tsx
**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (DEVELOPING.md)

All imports from recharts must use the public API entry point; imports from internal paths like recharts/types/* or recharts/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.

Comment on lines +45 to +70
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);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.

Comment on lines +72 to +88
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);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add required timer setup for render test.

This test is missing:

  1. vi.useFakeTimers() setup (required in all tests)
  2. vi.runOnlyPendingTimers() after render (required when using rechartsTestRender)
🔎 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.

Comment on lines +90 to +117
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);
});
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add required timer setup for multiple render scenarios.

This test renders multiple scenarios but is missing:

  1. vi.useFakeTimers() setup (required in all tests)
  2. 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.

Comment on lines +119 to +150
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);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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.

Suggested change
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.

Comment on lines +152 to +186
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);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add required timer setup for dashboard data test.

This test is missing:

  1. vi.useFakeTimers() setup (required in all tests)
  2. vi.runOnlyPendingTimers() after render (required when using rechartsTestRender)
🔎 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.

Suggested change
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.

Comment on lines +188 to +213
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);
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add required timer setup for extreme sparsity test.

This test is missing:

  1. vi.useFakeTimers() setup (required in all tests)
  2. vi.runOnlyPendingTimers() after render (required when using rechartsTestRender)
🔎 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.

Suggested change
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.

@MendyLanda
Copy link
Contributor Author

@PavelVanecek

Here are some tests I ran:

Scenario Potential Bars Actual Bars Reduction
100 categories, 90% sparse 300 ~30 90%
500 categories, 95% sparse 1,500 ~75 95%
1,000 categories, 95% sparse 3,000 ~150 95%

Each avoided rectangle saves a BarRectangleItem allocation, React.createElement() call, reconciliation, and hooks execution in Rectangle.

This is safe because Rectangle already returns null for zero dimensions (line 170), we're just checking earlier.

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 (Bar.zeroDimensionFiltering.spec.tsx) to detect future regressions.

@PavelVanecek
Copy link
Collaborator

How much reduction is that in terms of milliseconds?

@MendyLanda
Copy link
Contributor Author

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.

@MendyLanda
Copy link
Contributor Author

Before:
image

After:
image

@PavelVanecek
Copy link
Collaborator

@ckifer
Copy link
Member

ckifer commented Jan 2, 2026

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

@ckifer
Copy link
Member

ckifer commented Feb 17, 2026

bump?

@MendyLanda
Copy link
Contributor Author

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

@ckifer
Copy link
Member

ckifer commented Feb 17, 2026

no worries at all, just double checking staleness older PRs 😃

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
src/cartesian/Bar.tsx (1)

1183-1183: Nit: isInBarStack ? true : undefined can be simplified.

Since isInBarStack is already a boolean (from the hook's return type), and computeBarRectangles defaults it to false, 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 false in 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 for useIsInBarStack consistent with sibling hooks.

Other exported hooks in this file (e.g., useStackId at 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 for isInBarStack.

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.

@MendyLanda
Copy link
Contributor Author

@PavelVanecek @ckifer I think this one's ready

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/cartesian/Bar.tsx (1)

1183-1184: isInBarStack || undefined is non-idiomatic; prefer isInBarStack ? true : undefined.

false || undefined evaluates to undefined, 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.

@codecov
Copy link

codecov bot commented Feb 20, 2026

Bundle Report

Changes will increase total bundle size by 835 bytes (0.03%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
recharts/bundle-cjs 1.26MB 835 bytes (0.07%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: recharts/bundle-cjs

Assets Changed:

Asset Name Size Change Total Size Change (%)
cartesian/Bar.js 505 bytes 28.72kB 1.79%
cartesian/BarStack.js 330 bytes 6.09kB 5.73% ⚠️

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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.originalDataIndex to match activeIndex (which is based on the pre-filter data position). However, BarBackground at line 512 still computes:

isActive: String(i) === activeIndex,

i is the position in the filtered array, while activeIndex from selectActiveTooltipIndex tracks 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 index 0, 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 using expectLastCalledWith for 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 use expectLastCalledWith for type safety. As per coding guidelines, prefer expectLastCalledWith over 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: BarStackClipLayer clip-path index is correct; animation frame interpolation has a related gap.

Line 726 correctly uses entry.originalDataIndex for clip-path lookup — the core of the index-stability fix. LGTM.

However, RectanglesWithAnimation at line ~817 uses prevData[index] where index is the current filtered-array position, not originalDataIndex. 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 rect before 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.

@ckifer ckifer merged commit c49b263 into recharts:main Feb 22, 2026
45 of 46 checks passed
PavelVanecek added a commit that referenced this pull request Mar 3, 2026
…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>
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.

Stacked bars performance

3 participants