Repository: recharts/recharts (TypeScript)

Issue:
Incorrect hovering behaviour of StackedBarChart
## Do you want to request a feature or report a bug?

Bug

## What is the current behavior?

I want to highlight only the rectangles within the bar that I hovered over. I set it up to have a different tooltip for each rectangle in the bar via `<Tooltip shared={false} />`. I use `<Bar activeBar={{ fill: "#82ca9dCC" }} />` to fill the hovered area with a lighter color. However, when I hover over the bar, the active bar overlaps the remaining bars.

## Steps to reproduce and demo of the problem

Codesandbox: https://codesandbox.io/p/sandbox/stacked-bar-chart-forked-p3tp97

Here is the demo:

![Screen Recording 2024-10-17 at 20 02 29](https://github.com/user-attachments/assets/6f7a653a-d972-4995-a148-58bd1d5dec95)


## What is the expected behavior?

When I hover an area - it is highlighted.

## Which versions of Recharts, and which browser / OS are affected by this issue? 

Recharts: 2.13.0
Google Chrome: 129
OS: MacOS 15.0




Discussion:
Does this happen on previous versions or no?
v2.13.0-alpha.5 - yes, same behaviour
v2.13.0-alpha.4 - yes

releases from 2.12.0 to 2.12.7 show me error `minPointSize is not a function` for this codesandbox


I'll take a look but I'm currently on vacation and can't address this for ~2 weeks
it looks like the hovering functionality for `shared` is messed up in general and it has been since 2.9.0 when `activeBar` was added.
https://codesandbox.io/p/sandbox/stacked-bar-chart-forked-p3tp97

This is possible only without an `activeBar` prop at this time in 2.x.

`activeBar` is based on indices and with `shared={false}` we can no longer rely on indices as the indices were axis item based. This should be possible in 3.x
Adding `onMouseEnter` and `onMouseLeave` to `<Bar />` with tooltip shared `false`. Doesn't render the active value on the tooltip. Will this also fix on version 3.0?
I would think so but not sure, I'll check and get back
I can't seem to reproduce the issue with onMouseEnter/Leave
Hello everyone who's watching this issue! We have released an alpha version `3.0.0-alpha.2` which fixes this problem.

Here is a migration guide: https://github.com/recharts/recharts/wiki/3.0-migration-guide

As of now we have fixed all the new 3.x bugs we are aware of so, and are ready for your feedback. Please feel free to try `recharts: 3.0.0-alpha.2` in a test build and let us know how it went!
@PavelVanecek 

Seems like after upgrading to v3 i get a "XXX cannot be used as a JSX component" whenever i import anything like a Barchart or similar.
In my ts config i have ES2022 as a target, were there any other typescript requirement changes?


```
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "Default",
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "incremental": false,
    "isolatedModules": true,
    "lib": ["es2022", "DOM", "DOM.Iterable"],
    "moduleDetection": "force",
    "resolveJsonModule": true,
    "skipLibCheck": true,
    "strict": true,
    "target": "ES2022",
    "strictNullChecks": true,
    "plugins": [{ "name": "next" }],
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "allowJs": true,
    "jsx": "preserve",
    "noEmit": true
    }
}
```
Full error:

```'BarChart' cannot be used as a JSX component.
  Its type 'ForwardRefExoticComponent<CategoricalChartProps & RefAttributes<{ clipPathId: string; container?: HTMLElement | undefined; parseEventsOfWrapper(): { [x: string]: (e?: Event | undefined) => any; } | { ...; }; ... 19 more ...; UNSAFE_componentWillUpdate?(nextProps: Readonly<...>, nextState: Readonly<...>, nextContext:...' is not a valid JSX element type.
    Type 'ForwardRefExoticComponent<CategoricalChartProps & RefAttributes<{ clipPathId: string; container?: HTMLElement | undefined; parseEventsOfWrapper(): { [x: string]: (e?: Event | undefined) => any; } | { ...; }; ... 19 more ...; UNSAFE_componentWillUpdate?(nextProps: Readonly<...>, nextState: Readonly<...>, nextContext:...' is not assignable to type '(props: any) => ReactNode | Promise<ReactNode>'.
      Type 'ReactNode' is not assignable to type 'ReactNode | Promise<ReactNode>'.
        Type 'ReactElement<any, string | JSXElementConstructor<any>>' is not assignable to type 'ReactNode | Promise<ReactNode>'.
          Property 'children' is missing in type 'ReactElement<any, string | JSXElementConstructor<any>>' but required in type 'ReactPortal'.ts(2786)
index.d.ts(387, 9): 'children' is declared here.```


What React and Typescript versions are you using? I can run your config without issue
"typescript": "^5.7.3"
"react": "19.0.0",

I will create a full example, i am using a monorepo so there might be something i missed
@ckifer  I made a brand new nextjs project and just installed recharts: 3.0.0-alpha.2 without even changing the default config to mine and i got the same error:
![image](https://github.com/user-attachments/assets/3d245d18-9b2a-4e6c-84fe-505f09bbddbd)


Here is a full repo: 
https://github.com/zommerberg/nextjs-recharts-v3-types-bug

CartesianGrid works fine

@zommerberg  looks like this is something specific to React 19 and React 19 types or something specific to Next 15 ts-config. Please create a separate issue since this issue isn't related
@zommerberg this is your solution https://github.com/mapbox/mapbox-sdk-js/issues/477#issuecomment-2078567892
Unrelated to recharts or react19. Seems to be a next problem
Can confirm `npm install @types/react` fixes that on the sample project provided. Not sure what we can do from our side.
@PavelVanecek  You mean moving the types/react from dev dependency to normal dependency? The sample project had this library pre-installer already.

Is there a reason why CartesianGrid  works fine without any patches, but the other components require a patch?
Ah I didn't notice. I also modified the tsconfig. Following the link from Coltin above.
@PavelVanecek  @ckifer 

The fix might be tricky to do in monorepos and it definitely feels like something is broken with the library itself.

I looked into the CartesianGrid component which works fine without any fixes and compared it with ResponsiveContainer

It seems like ```export const ResponsiveContainer``` is bad, but `export function CartesianGrid` with a `CartesianGrid.displayName = 'CartesianGrid'` solves the issue

But i havent verified it 100%
> but the other components require a patch

Nothing requires a patch. This is a next issue with react types. It seems next cannot find the correct version of react types for whatever reason upon install. Once it can find the types with the TS config changes, everything is fine.

Not sure what else we can do when it works correctly for all other types of new projects. Feel free to investigate more if you'd like



Patch:
diff --git a/src/cartesian/Bar.tsx b/src/cartesian/Bar.tsx
index 2fbbc0dfc5..822a051231 100644
--- a/src/cartesian/Bar.tsx
+++ b/src/cartesian/Bar.tsx
@@ -54,7 +54,7 @@ import { BarSettings, selectBarRectangles } from '../state/selectors/barSelector
 import { BaseAxisWithScale } from '../state/selectors/axisSelectors';
 import { useAppSelector } from '../state/hooks';
 import { useIsPanorama } from '../context/PanoramaContext';
-import { selectActiveTooltipIndex } from '../state/selectors/tooltipSelectors';
+import { selectActiveTooltipDataKey, selectActiveTooltipIndex } from '../state/selectors/tooltipSelectors';
 
 export interface BarRectangleItem extends RectangleProps {
   value?: number | [number, number];
@@ -280,6 +280,8 @@ function BarRectangles(props: BarRectanglesProps) {
   const { data, shape, dataKey, activeBar } = props;
 
   const activeIndex = useAppSelector(selectActiveTooltipIndex);
+  const activeDataKey = useAppSelector(selectActiveTooltipDataKey);
+
   const {
     onMouseEnter: onMouseEnterFromProps,
     onClick: onItemClickFromProps,
@@ -298,7 +300,16 @@ function BarRectangles(props: BarRectanglesProps) {
   return (
     <>
       {data.map((entry: BarRectangleItem, i: number) => {
-        const isActive = activeBar && String(i) === activeIndex;
+        /*
+         * Bars support stacking, meaning that there can be multiple bars at the same x value.
+         * With Tooltip shared=false we only want to highlight the currently active Bar, not all.
+         *
+         * Also, if the tooltip is shared, we want to highlight all bars at the same x value
+         * regardless of the dataKey.
+         *
+         * With shared Tooltip, the activeDataKey is undefined.
+         */
+        const isActive = activeBar && String(i) === activeIndex && (activeDataKey == null || dataKey === activeDataKey);
         const option = isActive ? activeBar : shape;
         const barRectangleProps = {
           ...baseProps,
diff --git a/src/state/selectors/tooltipSelectors.ts b/src/state/selectors/tooltipSelectors.ts
index 43fadb95f8..90dc2bf241 100644
--- a/src/state/selectors/tooltipSelectors.ts
+++ b/src/state/selectors/tooltipSelectors.ts
@@ -39,7 +39,15 @@ import {
 import { selectChartLayout } from '../../context/chartLayoutContext';
 import { AxisId } from '../cartesianAxisSlice';
 import { isCategoricalAxis, RechartsScale, StackId } from '../../util/ChartUtils';
-import { AxisDomain, CategoricalDomain, LayoutType, NumberDomain, TickItem, TooltipEventType } from '../../util/types';
+import {
+  AxisDomain,
+  CategoricalDomain,
+  DataKey,
+  LayoutType,
+  NumberDomain,
+  TickItem,
+  TooltipEventType,
+} from '../../util/types';
 import { AppliedChartData, ChartData } from '../chartDataSlice';
 import { selectChartDataWithIndexes } from './dataSelectors';
 import { GraphicalItemSettings } from '../graphicalItemsSlice';
@@ -116,7 +124,7 @@ export const selectTooltipGraphicalItemsData = createSelector(
 /**
  * Data for tooltip always use the data with indexes set by a Brush,
  * and never accept the isPanorama flag:
- * because Tooltip never displays inside of the panorama anyway
+ * because Tooltip never displays inside the panorama anyway
  * so we don't need to worry what would happen there.
  */
 export const selectTooltipDisplayedData: (state: RechartsRootState) => ChartData = createSelector(
@@ -350,3 +358,14 @@ export const selectActiveLabel: (state: RechartsRootState) => string | undefined
   [selectTooltipAxisTicks, selectActiveTooltipIndex],
   combineActiveLabel,
 );
+
+export const selectActiveTooltipDataKey: (state: RechartsRootState) => DataKey<any> | undefined = createSelector(
+  [selectTooltipInteractionState],
+  (tooltipInteraction: TooltipInteractionState): DataKey<any> | undefined => {
+    if (!tooltipInteraction) {
+      return undefined;
+    }
+
+    return tooltipInteraction.dataKey;
+  },
+);
diff --git a/storybook/stories/Examples/BarChart/BarChart.stories.tsx b/storybook/stories/Examples/BarChart/BarChart.stories.tsx
index 44ea03178e..c002b9eeb9 100644
--- a/storybook/stories/Examples/BarChart/BarChart.stories.tsx
+++ b/storybook/stories/Examples/BarChart/BarChart.stories.tsx
@@ -81,9 +81,9 @@ export const Stacked = {
           <XAxis dataKey="name" />
           <YAxis />
           <Legend />
-          <Bar dataKey="pv" stackId="a" fill="#8884d8" />
-          <Bar dataKey="uv" stackId="a" fill="#82ca9d" />
-          <Tooltip />
+          <Bar dataKey="pv" stackId="a" fill="#8884d8" activeBar={{ fill: 'gold' }} />
+          <Bar dataKey="uv" stackId="a" fill="#82ca9d" activeBar={{ fill: 'silver' }} />
+          <Tooltip shared={false} />
         </BarChart>
       </ResponsiveContainer>
     );


Test patch:
diff --git a/test/cartesian/Bar.spec.tsx b/test/cartesian/Bar.spec.tsx
index a4dd1dac7d..a60732ed5e 100644
--- a/test/cartesian/Bar.spec.tsx
+++ b/test/cartesian/Bar.spec.tsx
@@ -2,7 +2,7 @@ import React, { ComponentType, ReactNode } from 'react';
 import { describe, expect, it, test, vi } from 'vitest';
 import { render } from '@testing-library/react';
 import uniqueId from 'lodash/uniqueId';
-import { Bar, Customized, Legend, LegendType, XAxis, YAxis } from '../../src';
+import { Bar, BarChart, Customized, Legend, LegendType, Tooltip, XAxis, YAxis } from '../../src';
 import {
   allCategoricalsChartsExcept,
   AreaChartCase,
@@ -13,10 +13,14 @@ import {
 } from '../helper/parameterizedTestCases';
 import { useAppSelector } from '../../src/state/hooks';
 import { CartesianGraphicalItemSettings } from '../../src/state/graphicalItemsSlice';
-import { expectBars } from '../helper/expectBars';
+import { expectBarIsActive, expectBarIsNotActive, expectBars, getAllBars } from '../helper/expectBars';
 import { ChartData } from '../../src/state/chartDataSlice';
 import { expectLabels } from '../helper/expectLabel';
 import { LayoutType } from '../../src/util/types';
+import { createSelectorTestCase } from '../helper/createSelectorTestCase';
+import { expectTooltipPayload, showTooltipOnCoordinate } from '../component/Tooltip/tooltipTestHelpers';
+import { selectActiveTooltipIndex } from '../../src/state/selectors/tooltipSelectors';
+import { barChartMouseHoverTooltipSelector } from '../component/Tooltip/tooltipMouseHoverSelectors';
 
 type TestCase = {
   ChartElement: ComponentType<{
@@ -1304,3 +1308,132 @@ describe.each(chartsThatDoNotSupportBar)('<Bar /> as a child of $testName', ({ C
     expect(container.querySelectorAll('.recharts-bar-rectangle')).toHaveLength(0);
   });
 });
+
+describe('mouse interactions in stacked bar: https://github.com/recharts/recharts/issues/5124', () => {
+  const stackedData = [{ x: 1, y: 2 }];
+
+  describe('with Tooltip shared=true', () => {
+    const renderTestCase = createSelectorTestCase(({ children }) => {
+      return (
+        <BarChart data={stackedData} width={100} height={100}>
+          <Bar activeBar dataKey="x" stackId="1" isAnimationActive={false} />
+          <Bar activeBar dataKey="y" stackId="1" isAnimationActive={false} />
+          <Tooltip shared isAnimationActive={false} />
+          {children}
+        </BarChart>
+      );
+    });
+
+    it('should render two bars on the same index', () => {
+      const { container } = renderTestCase();
+      expectBars(container, [
+        {
+          d: 'M 14,65.00000000000001 h 72 v 29.999999999999986 h -72 Z',
+          height: '29.999999999999986',
+          radius: '0',
+          width: '72',
+          x: '14',
+          y: '65.00000000000001',
+        },
+        {
+          d: 'M 14,5 h 72 v 60.000000000000014 h -72 Z',
+          height: '60.000000000000014',
+          radius: '0',
+          width: '72',
+          x: '14',
+          y: '5',
+        },
+      ]);
+    });
+
+    it('should render all bars inactive before user interaction', () => {
+      const { container } = renderTestCase();
+      const bars = getAllBars(container);
+      expectBarIsNotActive(bars[0]);
+      expectBarIsNotActive(bars[1]);
+      expect(bars).toHaveLength(2);
+    });
+
+    it('should show tooltip when hovering over a chart', () => {
+      const { container, spy } = renderTestCase(selectActiveTooltipIndex);
+      expect(spy).toHaveBeenLastCalledWith(null);
+
+      showTooltipOnCoordinate(container, barChartMouseHoverTooltipSelector, { clientX: 10, clientY: 10 });
+      expect(spy).toHaveBeenLastCalledWith('0');
+      expectTooltipPayload(container, '0', ['x : 1', 'y : 2']);
+    });
+
+    it('should make both bars active when hovering over the chart', () => {
+      const { container } = renderTestCase();
+
+      showTooltipOnCoordinate(container, barChartMouseHoverTooltipSelector, { clientX: 10, clientY: 10 });
+
+      const bars = getAllBars(container);
+      expectBarIsActive(bars[0]);
+      expectBarIsActive(bars[1]);
+    });
+  });
+
+  describe('with Tooltip shared=false', () => {
+    const renderTestCase = createSelectorTestCase(({ children }) => {
+      return (
+        <BarChart data={stackedData} width={100} height={100}>
+          <Bar activeBar dataKey="x" stackId="1" isAnimationActive={false} />
+          <Bar activeBar dataKey="y" stackId="1" isAnimationActive={false} />
+          <Tooltip shared={false} isAnimationActive={false} />
+          {children}
+        </BarChart>
+      );
+    });
+
+    it('should render two bars on the same index', () => {
+      const { container } = renderTestCase();
+      expectBars(container, [
+        {
+          d: 'M 14,65.00000000000001 h 72 v 29.999999999999986 h -72 Z',
+          height: '29.999999999999986',
+          radius: '0',
+          width: '72',
+          x: '14',
+          y: '65.00000000000001',
+        },
+        {
+          d: 'M 14,5 h 72 v 60.000000000000014 h -72 Z',
+          height: '60.000000000000014',
+          radius: '0',
+          width: '72',
+          x: '14',
+          y: '5',
+        },
+      ]);
+    });
+
+    it('should render all bars inactive before user interaction', () => {
+      const { container } = renderTestCase();
+      const bars = getAllBars(container);
+
+      expectBarIsNotActive(bars[0]);
+      expectBarIsNotActive(bars[1]);
+      expect(bars).toHaveLength(2);
+    });
+
+    it('should show tooltip when hovering over a bar', () => {
+      const { container, spy } = renderTestCase(selectActiveTooltipIndex);
+      expect(spy).toHaveBeenLastCalledWith(null);
+
+      const bars = getAllBars(container);
+      showTooltipOnCoordinate(bars[0], undefined, { clientX: 10, clientY: 10 });
+      expect(spy).toHaveBeenLastCalledWith('0');
+      expectTooltipPayload(container, '', ['x : 1']);
+    });
+
+    it('should make the first bar active - but not the second one - when hovering over it', () => {
+      const { container } = renderTestCase();
+      const bars = getAllBars(container);
+
+      showTooltipOnCoordinate(bars[0], undefined, { clientX: 10, clientY: 10 });
+      expectBarIsActive(bars[0]);
+      expectBarIsNotActive(bars[1]);
+    });
+  });
+});
diff --git a/test/helper/expectBars.ts b/test/helper/expectBars.ts
index e0821a223f..2c961917e8 100644
--- a/test/helper/expectBars.ts
+++ b/test/helper/expectBars.ts
@@ -10,10 +10,14 @@ type ExpectedBar = {
   d: string;
 };
 
+export function getAllBars(container: Element) {
+  return container.querySelectorAll('.recharts-bar .recharts-bar-rectangle');
+}
+
 export function expectBars(container: Element, expectedBars: ReadonlyArray<ExpectedBar>) {
   assertNotNull(container);
-  const allBars = container.querySelectorAll('.recharts-bar .recharts-bar-rectangle path.recharts-rectangle');
-  const actualBars = Array.from(allBars).map(bar => ({
+  const allBarPaths = container.querySelectorAll('.recharts-bar .recharts-bar-rectangle path.recharts-rectangle');
+  const actualBars = Array.from(allBarPaths).map(bar => ({
     x: bar.getAttribute('x'),
     y: bar.getAttribute('y'),
     width: bar.getAttribute('width'),
@@ -23,3 +27,17 @@ export function expectBars(container: Element, expectedBars: ReadonlyArray<Expec
   }));
   expect(actualBars).toEqual(expectedBars);
 }
+
+function getActiveChildren(bar: Element) {
+  return bar.querySelectorAll('.recharts-active-bar');
+}
+
+export function expectBarIsActive(bar: Element) {
+  const activeChildren = getActiveChildren(bar);
+  expect(activeChildren).toHaveLength(1);
+}
+
+export function expectBarIsNotActive(bar: Element) {
+  const activeChildren = getActiveChildren(bar);
+  expect(activeChildren).toHaveLength(0);
+}