diff --git a/packages/x-charts/package.json b/packages/x-charts/package.json
index f4f1fa97de31..f04c575f7cf4 100644
--- a/packages/x-charts/package.json
+++ b/packages/x-charts/package.json
@@ -46,7 +46,9 @@
"@react-spring/rafz": "^9.7.5",
"@react-spring/web": "^9.7.5",
"clsx": "^2.1.1",
- "prop-types": "^15.8.1"
+ "prop-types": "^15.8.1",
+ "reselect": "^5.1.1",
+ "use-sync-external-store": "^1.0.0"
},
"peerDependencies": {
"@emotion/react": "^11.9.0",
@@ -71,6 +73,7 @@
"@react-spring/core": "^9.7.5",
"@react-spring/shared": "^9.7.5",
"@types/prop-types": "^15.7.13",
+ "@types/use-sync-external-store": "^0.0.6",
"csstype": "^3.1.3",
"rimraf": "^6.0.1"
},
diff --git a/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.tsx b/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.tsx
index 5712979e916c..a53bfafff95f 100644
--- a/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.tsx
+++ b/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.tsx
@@ -2,29 +2,10 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import composeClasses from '@mui/utils/composeClasses';
-import generateUtilityClass from '@mui/utils/generateUtilityClass';
-import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
-import { styled } from '@mui/material/styles';
-import { InteractionContext } from '../context/InteractionProvider';
-import { useCartesianContext } from '../context/CartesianProvider';
-import { getValueToPositionMapper } from '../hooks/useScale';
-import { isBandScale } from '../internals/isBandScale';
-
-export interface ChartsAxisHighlightClasses {
- /** Styles applied to the root element. */
- root: string;
-}
-
-export type ChartsAxisHighlightClassKey = keyof ChartsAxisHighlightClasses;
-
-export function getAxisHighlightUtilityClass(slot: string) {
- return generateUtilityClass('MuiChartsAxisHighlight', slot);
-}
-
-export const chartsAxisHighlightClasses: ChartsAxisHighlightClasses = generateUtilityClasses(
- 'MuiChartsAxisHighlight',
- ['root'],
-);
+import { getAxisHighlightUtilityClass } from './chartsAxisHighlightClasses';
+import ChartsYHighlight from './ChartsYAxisHighlight';
+import ChartsXHighlight from './ChartsXAxisHighlight';
+import { ChartsAxisHighlightProps } from './ChartsAxisHighlight.types';
const useUtilityClasses = () => {
const slots = {
@@ -34,47 +15,6 @@ const useUtilityClasses = () => {
return composeClasses(slots, getAxisHighlightUtilityClass);
};
-export const ChartsAxisHighlightPath = styled('path', {
- name: 'MuiChartsAxisHighlight',
- slot: 'Root',
- overridesResolver: (_, styles) => styles.root,
-})<{ ownerState: { axisHighlight: AxisHighlight } }>(({ theme }) => ({
- pointerEvents: 'none',
- variants: [
- {
- props: {
- axisHighlight: 'band',
- },
- style: {
- fill: 'white',
- fillOpacity: 0.1,
- ...theme.applyStyles('light', {
- fill: 'gray',
- }),
- },
- },
- {
- props: {
- axisHighlight: 'line',
- },
- style: {
- strokeDasharray: '5 2',
- stroke: '#ffffff',
- ...theme.applyStyles('light', {
- stroke: '#000000',
- }),
- },
- },
- ],
-}));
-
-type AxisHighlight = 'none' | 'line' | 'band';
-
-export type ChartsAxisHighlightProps = {
- x?: AxisHighlight;
- y?: AxisHighlight;
-};
-
/**
* Demos:
*
@@ -86,88 +26,12 @@ export type ChartsAxisHighlightProps = {
*/
function ChartsAxisHighlight(props: ChartsAxisHighlightProps) {
const { x: xAxisHighlight, y: yAxisHighlight } = props;
- const { xAxisIds, xAxis, yAxisIds, yAxis } = useCartesianContext();
- const classes = useUtilityClasses();
-
- const USED_X_AXIS_ID = xAxisIds[0];
- const USED_Y_AXIS_ID = yAxisIds[0];
-
- const xScale = xAxis[USED_X_AXIS_ID].scale;
- const yScale = yAxis[USED_Y_AXIS_ID].scale;
-
- const { axis } = React.useContext(InteractionContext);
-
- const getXPosition = getValueToPositionMapper(xScale);
- const getYPosition = getValueToPositionMapper(yScale);
-
- const axisX = axis.x;
- const axisY = axis.y;
-
- const isBandScaleX = xAxisHighlight === 'band' && axisX !== null && isBandScale(xScale);
- const isBandScaleY = yAxisHighlight === 'band' && axisY !== null && isBandScale(yScale);
-
- if (process.env.NODE_ENV !== 'production') {
- const isXError = isBandScaleX && xScale(axisX.value) === undefined;
- const isYError = isBandScaleY && yScale(axisY.value) === undefined;
-
- if (isXError || isYError) {
- console.error(
- [
- `MUI X: The position value provided for the axis is not valid for the current scale.`,
- `This probably means something is wrong with the data passed to the chart.`,
- `The ChartsAxisHighlight component will not be displayed.`,
- ].join('\n'),
- );
- }
- }
+ const classes = useUtilityClasses();
return (
- {isBandScaleX && xScale(axisX.value) !== undefined && (
-
- )}
-
- {isBandScaleY && yScale(axisY.value) !== undefined && (
-
- )}
-
- {xAxisHighlight === 'line' && axis.x !== null && (
-
- )}
-
- {yAxisHighlight === 'line' && axis.y !== null && (
-
- )}
+ {xAxisHighlight && }
+ {yAxisHighlight && }
);
}
diff --git a/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.types.ts b/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.types.ts
new file mode 100644
index 000000000000..de1b75d43438
--- /dev/null
+++ b/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.types.ts
@@ -0,0 +1,6 @@
+export type ChartsAxisHighlightType = 'none' | 'line' | 'band';
+
+export type ChartsAxisHighlightProps = {
+ x?: ChartsAxisHighlightType;
+ y?: ChartsAxisHighlightType;
+};
diff --git a/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlightPath.ts b/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlightPath.ts
new file mode 100644
index 000000000000..d015d2b5d15c
--- /dev/null
+++ b/packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlightPath.ts
@@ -0,0 +1,37 @@
+'use client';
+import { styled } from '@mui/material/styles';
+import { ChartsAxisHighlightType } from './ChartsAxisHighlight.types';
+
+export const ChartsAxisHighlightPath = styled('path', {
+ name: 'MuiChartsAxisHighlight',
+ slot: 'Root',
+ overridesResolver: (_, styles) => styles.root,
+})<{ ownerState: { axisHighlight: ChartsAxisHighlightType } }>(({ theme }) => ({
+ pointerEvents: 'none',
+ variants: [
+ {
+ props: {
+ axisHighlight: 'band',
+ },
+ style: {
+ fill: 'white',
+ fillOpacity: 0.1,
+ ...theme.applyStyles('light', {
+ fill: 'gray',
+ }),
+ },
+ },
+ {
+ props: {
+ axisHighlight: 'line',
+ },
+ style: {
+ strokeDasharray: '5 2',
+ stroke: '#ffffff',
+ ...theme.applyStyles('light', {
+ stroke: '#000000',
+ }),
+ },
+ },
+ ],
+}));
diff --git a/packages/x-charts/src/ChartsAxisHighlight/ChartsXAxisHighlight.tsx b/packages/x-charts/src/ChartsAxisHighlight/ChartsXAxisHighlight.tsx
new file mode 100644
index 000000000000..d828547af94e
--- /dev/null
+++ b/packages/x-charts/src/ChartsAxisHighlight/ChartsXAxisHighlight.tsx
@@ -0,0 +1,69 @@
+'use client';
+import * as React from 'react';
+import { getValueToPositionMapper, useXScale } from '../hooks/useScale';
+import { isBandScale } from '../internals/isBandScale';
+import { useSelector } from '../internals/useSelector';
+import { useStore } from '../internals/useStore';
+import { selectorChartsInteractionXAxis } from '../context/InteractionSelectors';
+import { useDrawingArea } from '../hooks';
+import { ChartsAxisHighlightType } from './ChartsAxisHighlight.types';
+import { ChartsAxisHighlightClasses } from './chartsAxisHighlightClasses';
+import { ChartsAxisHighlightPath } from './ChartsAxisHighlightPath';
+
+/**
+ * @ignore - internal component.
+ */
+export default function ChartsXHighlight(props: {
+ type: ChartsAxisHighlightType;
+ classes: ChartsAxisHighlightClasses;
+}) {
+ const { type, classes } = props;
+
+ const { top, height } = useDrawingArea();
+
+ const xScale = useXScale();
+
+ const store = useStore();
+ const axisX = useSelector(store, selectorChartsInteractionXAxis);
+
+ const getXPosition = getValueToPositionMapper(xScale);
+
+ const isBandScaleX = type === 'band' && axisX !== null && isBandScale(xScale);
+
+ if (process.env.NODE_ENV !== 'production') {
+ const isError = isBandScaleX && xScale(axisX.value) === undefined;
+
+ if (isError) {
+ console.error(
+ [
+ `MUI X: The position value provided for the axis is not valid for the current scale.`,
+ `This probably means something is wrong with the data passed to the chart.`,
+ `The ChartsAxisHighlight component will not be displayed.`,
+ ].join('\n'),
+ );
+ }
+ }
+
+ return (
+
+ {isBandScaleX && xScale(axisX.value) !== undefined && (
+
+ )}
+
+ {type === 'line' && axisX !== null && (
+
+ )}
+
+ );
+}
diff --git a/packages/x-charts/src/ChartsAxisHighlight/ChartsYAxisHighlight.tsx b/packages/x-charts/src/ChartsAxisHighlight/ChartsYAxisHighlight.tsx
new file mode 100644
index 000000000000..ce996fb544df
--- /dev/null
+++ b/packages/x-charts/src/ChartsAxisHighlight/ChartsYAxisHighlight.tsx
@@ -0,0 +1,71 @@
+'use client';
+import * as React from 'react';
+import { getValueToPositionMapper, useYScale } from '../hooks/useScale';
+import { isBandScale } from '../internals/isBandScale';
+import { useSelector } from '../internals/useSelector';
+import { useStore } from '../internals/useStore';
+import { selectorChartsInteractionYAxis } from '../context/InteractionSelectors';
+import { useDrawingArea } from '../hooks';
+import { ChartsAxisHighlightType } from './ChartsAxisHighlight.types';
+import { ChartsAxisHighlightClasses } from './chartsAxisHighlightClasses';
+import { ChartsAxisHighlightPath } from './ChartsAxisHighlightPath';
+
+/**
+ * @ignore - internal component.
+ */
+export default function ChartsYHighlight(props: {
+ type: ChartsAxisHighlightType;
+ classes: ChartsAxisHighlightClasses;
+}) {
+ const { type, classes } = props;
+
+ const { left, width } = useDrawingArea();
+
+ const yScale = useYScale();
+
+ const store = useStore();
+ const axisY = useSelector(store, selectorChartsInteractionYAxis);
+
+ const getYPosition = getValueToPositionMapper(yScale);
+
+ const isBandScaleY = type === 'band' && axisY !== null && isBandScale(yScale);
+
+ if (process.env.NODE_ENV !== 'production') {
+ const isError = isBandScaleY && yScale(axisY.value) === undefined;
+
+ if (isError) {
+ console.error(
+ [
+ `MUI X: The position value provided for the axis is not valid for the current scale.`,
+ `This probably means something is wrong with the data passed to the chart.`,
+ `The ChartsAxisHighlight component will not be displayed.`,
+ ].join('\n'),
+ );
+ }
+ }
+
+ return (
+
+ {isBandScaleY && yScale(axisY.value) !== undefined && (
+
+ )}
+
+ {type === 'line' && axisY !== null && (
+
+ )}
+
+ );
+}
diff --git a/packages/x-charts/src/ChartsAxisHighlight/chartsAxisHighlightClasses.ts b/packages/x-charts/src/ChartsAxisHighlight/chartsAxisHighlightClasses.ts
new file mode 100644
index 000000000000..7d62c32d82fa
--- /dev/null
+++ b/packages/x-charts/src/ChartsAxisHighlight/chartsAxisHighlightClasses.ts
@@ -0,0 +1,18 @@
+import generateUtilityClass from '@mui/utils/generateUtilityClass';
+import generateUtilityClasses from '@mui/utils/generateUtilityClasses';
+
+export interface ChartsAxisHighlightClasses {
+ /** Styles applied to the root element. */
+ root: string;
+}
+
+export type ChartsAxisHighlightClassKey = keyof ChartsAxisHighlightClasses;
+
+export function getAxisHighlightUtilityClass(slot: string) {
+ return generateUtilityClass('MuiChartsAxisHighlight', slot);
+}
+
+export const chartsAxisHighlightClasses: ChartsAxisHighlightClasses = generateUtilityClasses(
+ 'MuiChartsAxisHighlight',
+ ['root'],
+);
diff --git a/packages/x-charts/src/ChartsAxisHighlight/index.ts b/packages/x-charts/src/ChartsAxisHighlight/index.ts
index 14c071e360be..653afd1cfd87 100644
--- a/packages/x-charts/src/ChartsAxisHighlight/index.ts
+++ b/packages/x-charts/src/ChartsAxisHighlight/index.ts
@@ -1 +1,4 @@
export * from './ChartsAxisHighlight';
+export * from './chartsAxisHighlightClasses';
+export * from './ChartsAxisHighlight.types';
+export * from './ChartsAxisHighlightPath';
diff --git a/packages/x-charts/src/ChartsOnAxisClickHandler/ChartsOnAxisClickHandler.tsx b/packages/x-charts/src/ChartsOnAxisClickHandler/ChartsOnAxisClickHandler.tsx
index 8c26ff5650e6..a5a7d65b0ab7 100644
--- a/packages/x-charts/src/ChartsOnAxisClickHandler/ChartsOnAxisClickHandler.tsx
+++ b/packages/x-charts/src/ChartsOnAxisClickHandler/ChartsOnAxisClickHandler.tsx
@@ -1,7 +1,7 @@
'use client';
import * as React from 'react';
import PropTypes from 'prop-types';
-import { InteractionContext } from '../context/InteractionProvider';
+import { useStore } from '../internals/useStore';
import { useSeries } from '../hooks/useSeries';
import { useSvgRef } from '../hooks';
import { useCartesianContext } from '../context/CartesianProvider';
@@ -27,7 +27,8 @@ function ChartsOnAxisClickHandler(props: ChartsOnAxisClickHandlerProps) {
const svgRef = useSvgRef();
const series = useSeries();
- const { axis } = React.useContext(InteractionContext);
+ const store = useStore();
+
const { xAxisIds, xAxis, yAxisIds, yAxis } = useCartesianContext();
React.useEffect(() => {
@@ -39,9 +40,10 @@ function ChartsOnAxisClickHandler(props: ChartsOnAxisClickHandlerProps) {
const handleMouseClick = (event: MouseEvent) => {
event.preventDefault();
- const isXaxis = axis.x && axis.x.index !== -1;
+ const { x: axisX, y: axisY } = store.value.interaction.axis;
+ const isXaxis = axisX && axisX.index !== -1;
const USED_AXIS_ID = isXaxis ? xAxisIds[0] : yAxisIds[0];
- const dataIndex = isXaxis ? axis.x && axis.x.index : axis.y && axis.y.index;
+ const dataIndex = isXaxis ? axisX && axisX.index : axisY && axisY.index;
if (dataIndex == null) {
return;
@@ -72,7 +74,7 @@ function ChartsOnAxisClickHandler(props: ChartsOnAxisClickHandlerProps) {
return () => {
element.removeEventListener('click', handleMouseClick);
};
- }, [axis.x, axis.y, onAxisClick, series, svgRef, xAxis, xAxisIds, yAxis, yAxisIds]);
+ }, [onAxisClick, series, store, svgRef, xAxis, xAxisIds, yAxis, yAxisIds]);
// eslint-disable-next-line react/jsx-no-useless-fragment
return ;
diff --git a/packages/x-charts/src/ChartsSurface/ChartsSurface.test.tsx b/packages/x-charts/src/ChartsSurface/ChartsSurface.test.tsx
index f9bd7b41068c..8a61ef8212f6 100644
--- a/packages/x-charts/src/ChartsSurface/ChartsSurface.test.tsx
+++ b/packages/x-charts/src/ChartsSurface/ChartsSurface.test.tsx
@@ -19,7 +19,10 @@ describe('', () => {
render(
-
+
diff --git a/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx b/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx
index 28b6f6f833e0..ed4f1a6608b4 100644
--- a/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx
+++ b/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import { SxProps, Theme } from '@mui/material/styles';
import useSlotProps from '@mui/utils/useSlotProps';
-import { AxisInteractionData } from '../context/InteractionProvider';
+import { AxisInteractionData } from '../internals/plugins/models';
import { useCartesianContext } from '../context/CartesianProvider';
import { ChartSeriesDefaultized, ChartSeriesType } from '../models/seriesType/config';
import { AxisDefaultized } from '../models/axis';
diff --git a/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx b/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx
index 30140400a948..8d374db974f7 100644
--- a/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx
+++ b/packages/x-charts/src/ChartsTooltip/ChartsItemTooltipContent.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import { SxProps, Theme } from '@mui/material/styles';
import useSlotProps from '@mui/utils/useSlotProps';
-import { ItemInteractionData } from '../context/InteractionProvider';
+import { ItemInteractionData } from '../internals/plugins/models';
import { ChartSeriesDefaultized, ChartSeriesType } from '../models/seriesType/config';
import { ChartsTooltipClasses } from './chartsTooltipClasses';
import { DefaultChartsItemTooltipContent } from './DefaultChartsItemTooltipContent';
diff --git a/packages/x-charts/src/ChartsTooltip/ChartsTooltip.tsx b/packages/x-charts/src/ChartsTooltip/ChartsTooltip.tsx
index 560a83ccc9a4..544dac733866 100644
--- a/packages/x-charts/src/ChartsTooltip/ChartsTooltip.tsx
+++ b/packages/x-charts/src/ChartsTooltip/ChartsTooltip.tsx
@@ -7,17 +7,19 @@ import { styled, useThemeProps, SxProps, Theme } from '@mui/material/styles';
import Popper, { PopperProps as BasePopperProps } from '@mui/material/Popper';
import NoSsr from '@mui/material/NoSsr';
import useSlotProps from '@mui/utils/useSlotProps';
-import {
- AxisInteractionData,
- InteractionContext,
- ItemInteractionData,
-} from '../context/InteractionProvider';
import { useSvgRef } from '../hooks/useSvgRef';
import { getTooltipHasData, TriggerOptions, usePointerType } from './utils';
import { ChartSeriesType } from '../models/seriesType/config';
import { ChartsItemContentProps, ChartsItemTooltipContent } from './ChartsItemTooltipContent';
import { ChartsAxisContentProps, ChartsAxisTooltipContent } from './ChartsAxisTooltipContent';
import { ChartsTooltipClasses, getChartsTooltipUtilityClass } from './chartsTooltipClasses';
+import {
+ selectorChartsInteractionItem,
+ selectorChartsInteractionAxis,
+} from '../context/InteractionSelectors';
+import { ItemInteractionData, AxisInteractionData } from '../internals/plugins/models';
+import { useSelector } from '../internals/useSelector';
+import { useStore } from '../internals/useStore';
export type PopperProps = BasePopperProps & {
/**
@@ -137,7 +139,9 @@ function ChartsTooltip(inProps: ChartsTooltipProps
const positionRef = useLazyRef(() => ({ x: 0, y: 0 }));
- const { item, axis } = React.useContext(InteractionContext);
+ const store = useStore();
+ const item = useSelector(store, selectorChartsInteractionItem);
+ const axis = useSelector(store, selectorChartsInteractionAxis);
const displayedData = trigger === 'item' ? item : axis;
diff --git a/packages/x-charts/src/ChartsTooltip/useAxisTooltip.tsx b/packages/x-charts/src/ChartsTooltip/useAxisTooltip.tsx
index 109665b90e5d..aff76dca6187 100644
--- a/packages/x-charts/src/ChartsTooltip/useAxisTooltip.tsx
+++ b/packages/x-charts/src/ChartsTooltip/useAxisTooltip.tsx
@@ -1,6 +1,5 @@
'use client';
import * as React from 'react';
-import { AxisInteractionData, InteractionContext } from '../context/InteractionProvider';
import { useSeries } from '../hooks/useSeries';
import { useCartesianContext } from '../context/CartesianProvider';
import { ZAxisContext } from '../context/ZAxisContextProvider';
@@ -10,6 +9,15 @@ import { CartesianChartSeriesType, ChartsSeriesConfig } from '../models/seriesTy
import { getLabel } from '../internals/getLabel';
import { isCartesianSeriesType } from '../internals/isCartesian';
import { utcFormatter } from './utils';
+import {
+ selectorChartsInteractionAxis,
+ selectorChartsInteractionXAxis,
+ selectorChartsInteractionYAxis,
+} from '../context/InteractionSelectors';
+import { useXAxis, useYAxis } from '../hooks';
+import { useSelector } from '../internals/useSelector';
+import { useStore } from '../internals/useStore';
+import { AxisInteractionData } from '../internals/plugins/models';
export interface UseAxisTooltipReturnValue<
SeriesT extends CartesianChartSeriesType = CartesianChartSeriesType,
@@ -30,26 +38,35 @@ interface SeriesItem {
}
export function useAxisTooltip(): null | UseAxisTooltipReturnValue {
- const { axis } = React.useContext(InteractionContext);
+ const defaultXAxis = useXAxis();
+ const defaultYAxis = useYAxis();
+
+ const xAxisHasData = defaultXAxis.data !== undefined && defaultXAxis.data.length !== 0;
+
+ const store = useStore();
+
+ // This line will be removed in v8 because it degrade perfs for no reason except avoiding breaking change.
+ const axis = useSelector(store, selectorChartsInteractionAxis);
+ const axisData = useSelector(
+ store,
+ xAxisHasData ? selectorChartsInteractionXAxis : selectorChartsInteractionYAxis,
+ );
+
const series = useSeries();
- const { xAxis, yAxis, xAxisIds, yAxisIds } = useCartesianContext();
+ const { xAxis, yAxis } = useCartesianContext();
+
const { zAxis, zAxisIds } = React.useContext(ZAxisContext);
const colorProcessors = useColorProcessor();
- // By default use the x-axis
- const isXaxis = axis.x !== null && axis.x.index !== -1;
-
- const axisData = isXaxis ? axis.x && axis.x : axis.y && axis.y;
-
if (axisData === null) {
return null;
}
const { index: dataIndex, value: axisValue } = axisData;
- const USED_AXIS_ID = isXaxis ? xAxisIds[0] : yAxisIds[0];
- const usedAxis = isXaxis ? xAxis[USED_AXIS_ID] : yAxis[USED_AXIS_ID];
+ const USED_AXIS_ID = xAxisHasData ? defaultXAxis.id : defaultYAxis.id;
+ const usedAxis = xAxisHasData ? defaultXAxis : defaultYAxis;
const relevantSeries = Object.keys(series)
.filter(isCartesianSeriesType)
@@ -64,20 +81,20 @@ export function useAxisTooltip(): null | UseAxisTooltipReturnValue {
const providedXAxisId = seriesToAdd.xAxisId;
const providedYAxisId = seriesToAdd.yAxisId;
- const axisKey = isXaxis ? providedXAxisId : providedYAxisId;
+ const axisKey = xAxisHasData ? providedXAxisId : providedYAxisId;
// Test if the series uses the default axis
if (axisKey === undefined || axisKey === USED_AXIS_ID) {
- const xAxisId = providedXAxisId ?? xAxisIds[0];
- const yAxisId = providedYAxisId ?? yAxisIds[0];
- const zAxisId = (seriesToAdd as any).zAxisId ?? zAxisIds[0];
+ const xAxisId = providedXAxisId ?? defaultXAxis.id;
+ const yAxisId = providedYAxisId ?? defaultYAxis.id;
+ const zAxisId = 'zAxisId' in seriesToAdd ? seriesToAdd.zAxisId : zAxisIds[0];
const color =
colorProcessors[seriesType]?.(
seriesToAdd,
xAxis[xAxisId],
yAxis[yAxisId],
- zAxisId && zAxis[zAxisId],
+ zAxisId ? zAxis[zAxisId] : undefined,
)(dataIndex) ?? '';
const value = seriesToAdd.data[dataIndex] ?? null;
@@ -107,7 +124,7 @@ export function useAxisTooltip(): null | UseAxisTooltipReturnValue {
const axisFormattedValue = axisFormatter(axisValue, { location: 'tooltip' });
return {
- identifier: axis as AxisInteractionData,
+ identifier: axis,
seriesItems: relevantSeries,
axisValue,
axisFormattedValue,
diff --git a/packages/x-charts/src/ChartsTooltip/useItemTooltip.tsx b/packages/x-charts/src/ChartsTooltip/useItemTooltip.tsx
index a3ab97c287b1..96023ae1b272 100644
--- a/packages/x-charts/src/ChartsTooltip/useItemTooltip.tsx
+++ b/packages/x-charts/src/ChartsTooltip/useItemTooltip.tsx
@@ -1,6 +1,5 @@
'use client';
import * as React from 'react';
-import { InteractionContext, ItemInteractionData } from '../context/InteractionProvider';
import { useSeries } from '../hooks/useSeries';
import { useCartesianContext } from '../context/CartesianProvider';
import { ZAxisContext } from '../context/ZAxisContextProvider';
@@ -12,6 +11,10 @@ import {
} from '../models/seriesType/config';
import { getLabel } from '../internals/getLabel';
import { CommonSeriesType } from '../models/seriesType/common';
+import { selectorChartsInteractionItem } from '../context/InteractionSelectors';
+import { useSelector } from '../internals/useSelector';
+import { useStore } from '../internals/useStore';
+import { ItemInteractionData } from '../internals/plugins/models';
export interface UseItemTooltipReturnValue {
identifier: ItemInteractionData;
@@ -22,7 +25,8 @@ export interface UseItemTooltipReturnValue {
}
export function useItemTooltip(): null | UseItemTooltipReturnValue {
- const { item } = React.useContext(InteractionContext);
+ const stroe = useStore();
+ const item = useSelector(stroe, selectorChartsInteractionItem);
const series = useSeries();
const { xAxis, yAxis, xAxisIds, yAxisIds } = useCartesianContext();
diff --git a/packages/x-charts/src/ChartsTooltip/utils.tsx b/packages/x-charts/src/ChartsTooltip/utils.tsx
index 227c7f58e24e..6964b5ef3e45 100644
--- a/packages/x-charts/src/ChartsTooltip/utils.tsx
+++ b/packages/x-charts/src/ChartsTooltip/utils.tsx
@@ -1,5 +1,5 @@
import * as React from 'react';
-import { AxisInteractionData, ItemInteractionData } from '../context/InteractionProvider';
+import { AxisInteractionData, ItemInteractionData } from '../internals/plugins/models';
import { ChartSeriesType } from '../models/seriesType/config';
import { useSvgRef } from '../hooks';
diff --git a/packages/x-charts/src/ChartsVoronoiHandler/ChartsVoronoiHandler.tsx b/packages/x-charts/src/ChartsVoronoiHandler/ChartsVoronoiHandler.tsx
index ad1dea102fc6..f0169fb95025 100644
--- a/packages/x-charts/src/ChartsVoronoiHandler/ChartsVoronoiHandler.tsx
+++ b/packages/x-charts/src/ChartsVoronoiHandler/ChartsVoronoiHandler.tsx
@@ -3,9 +3,9 @@ import * as React from 'react';
import PropTypes from 'prop-types';
import { Delaunay } from '@mui/x-charts-vendor/d3-delaunay';
import useEnhancedEffect from '@mui/utils/useEnhancedEffect';
-import { InteractionContext } from '../context/InteractionProvider';
import { useCartesianContext } from '../context/CartesianProvider';
import { getValueToPositionMapper } from '../hooks/useScale';
+import { useStore } from '../internals/useStore';
import { getSVGPoint } from '../internals/getSVGPoint';
import { ScatterItemIdentifier } from '../models';
import { SeriesId } from '../models/seriesType/common';
@@ -34,7 +34,7 @@ function ChartsVoronoiHandler(props: ChartsVoronoiHandlerProps) {
const svgRef = useSvgRef();
const drawingArea = useDrawingArea();
const { xAxis, yAxis, xAxisIds, yAxisIds } = useCartesianContext();
- const { dispatch } = React.useContext(InteractionContext);
+ const store = useStore();
const { series, seriesOrder } = useScatterSeries() ?? {};
const voronoiRef = React.useRef>({});
@@ -47,11 +47,18 @@ function ChartsVoronoiHandler(props: ChartsVoronoiHandlerProps) {
const defaultYAxisId = yAxisIds[0];
useEnhancedEffect(() => {
- dispatch({ type: 'updateVoronoiUsage', useVoronoiInteraction: true });
+ store.update((prev) => ({
+ ...prev,
+ interaction: { ...prev.interaction, useVoronoiInteraction: true },
+ }));
+
return () => {
- dispatch({ type: 'updateVoronoiUsage', useVoronoiInteraction: false });
+ store.update((prev) => ({
+ ...prev,
+ interaction: { ...prev.interaction, useVoronoiInteraction: false },
+ }));
};
- }, [dispatch]);
+ }, [store]);
useEnhancedEffect(() => {
// This effect generate and store the Delaunay object that's used to map coordinate to closest point.
@@ -153,7 +160,10 @@ function ChartsVoronoiHandler(props: ChartsVoronoiHandlerProps) {
}
const handleMouseLeave = () => {
- dispatch({ type: 'exitChart' });
+ store.update((prev) => ({
+ ...prev,
+ interaction: { ...prev.interaction, axis: { x: null, y: null }, item: null },
+ }));
clearHighlighted();
};
@@ -161,19 +171,29 @@ function ChartsVoronoiHandler(props: ChartsVoronoiHandlerProps) {
const closestPoint = getClosestPoint(event);
if (closestPoint === 'outside-chart') {
- dispatch({ type: 'exitChart' });
+ store.update((prev) => ({
+ ...prev,
+ interaction: { ...prev.interaction, axis: { x: null, y: null }, item: null },
+ }));
clearHighlighted();
return;
}
if (closestPoint === 'outside-voronoi-max-radius' || closestPoint === 'no-point-found') {
- dispatch({ type: 'leaveItem', data: { type: 'scatter' } });
+ store.update((prev) => ({
+ ...prev,
+ interaction: { ...prev.interaction, item: null },
+ }));
clearHighlighted();
return;
}
const { seriesId, dataIndex } = closestPoint;
- dispatch({ type: 'enterItem', data: { type: 'scatter', seriesId, dataIndex } });
+ store.update((prev) => ({
+ ...prev,
+ interaction: { ...prev.interaction, item: { type: 'scatter', seriesId, dataIndex } },
+ }));
+
setHighlighted({
seriesId,
dataIndex,
@@ -205,7 +225,6 @@ function ChartsVoronoiHandler(props: ChartsVoronoiHandlerProps) {
};
}, [
svgRef,
- dispatch,
yAxis,
xAxis,
voronoiMaxRadius,
@@ -213,6 +232,7 @@ function ChartsVoronoiHandler(props: ChartsVoronoiHandlerProps) {
setHighlighted,
clearHighlighted,
drawingArea,
+ store,
]);
// eslint-disable-next-line react/jsx-no-useless-fragment
diff --git a/packages/x-charts/src/LineChart/CircleMarkElement.tsx b/packages/x-charts/src/LineChart/CircleMarkElement.tsx
index 584c15bab4d8..ddc1608cc44c 100644
--- a/packages/x-charts/src/LineChart/CircleMarkElement.tsx
+++ b/packages/x-charts/src/LineChart/CircleMarkElement.tsx
@@ -4,10 +4,12 @@ import PropTypes from 'prop-types';
import { useTheme } from '@mui/material/styles';
import { warnOnce } from '@mui/x-internals/warning';
import { animated, useSpring } from '@react-spring/web';
-import { InteractionContext } from '../context/InteractionProvider';
import { useInteractionItemProps } from '../hooks/useInteractionItemProps';
import { useItemHighlighted } from '../context';
import { MarkElementOwnerState, useUtilityClasses } from './markElementClasses';
+import { useSelector } from '../internals/useSelector';
+import { selectorChartsInteractionXAxis } from '../context/InteractionSelectors';
+import { useStore } from '../internals/useStore';
export type CircleMarkElementProps = Omit &
Omit, 'ref' | 'id'> & {
@@ -66,13 +68,14 @@ function CircleMarkElement(props: CircleMarkElementProps) {
const { isFaded, isHighlighted } = useItemHighlighted({
seriesId: id,
});
- const { axis } = React.useContext(InteractionContext);
+ const store = useStore();
+ const xAxisIdentifier = useSelector(store, selectorChartsInteractionXAxis);
const position = useSpring({ to: { x, y }, immediate: skipAnimation });
const ownerState = {
id,
classes: innerClasses,
- isHighlighted: axis.x?.index === dataIndex || isHighlighted,
+ isHighlighted: xAxisIdentifier?.index === dataIndex || isHighlighted,
isFaded,
color,
};
diff --git a/packages/x-charts/src/LineChart/LineHighlightPlot.tsx b/packages/x-charts/src/LineChart/LineHighlightPlot.tsx
index a5911052eb10..7c6e30437bd0 100644
--- a/packages/x-charts/src/LineChart/LineHighlightPlot.tsx
+++ b/packages/x-charts/src/LineChart/LineHighlightPlot.tsx
@@ -5,11 +5,13 @@ import { SlotComponentPropsFromProps } from '@mui/x-internals/types';
import { useCartesianContext } from '../context/CartesianProvider';
import { LineHighlightElement, LineHighlightElementProps } from './LineHighlightElement';
import { getValueToPositionMapper } from '../hooks/useScale';
-import { InteractionContext } from '../context/InteractionProvider';
import { DEFAULT_X_AXIS_KEY } from '../constants';
import getColor from './getColor';
import { useLineSeries } from '../hooks/useSeries';
import { useDrawingArea } from '../hooks/useDrawingArea';
+import { selectorChartsInteractionXAxis } from '../context/InteractionSelectors';
+import { useSelector } from '../internals/useSelector';
+import { useStore } from '../internals/useStore';
export interface LineHighlightPlotSlots {
lineHighlight?: React.JSXElementConstructor;
@@ -48,9 +50,11 @@ function LineHighlightPlot(props: LineHighlightPlotProps) {
const seriesData = useLineSeries();
const axisData = useCartesianContext();
const drawingArea = useDrawingArea();
- const { axis } = React.useContext(InteractionContext);
+ const store = useStore();
+ const xAxisIdentifier = useSelector(store, selectorChartsInteractionXAxis);
+
+ const highlightedIndex = xAxisIdentifier?.index;
- const highlightedIndex = axis.x?.index;
if (highlightedIndex === undefined) {
return null;
}
diff --git a/packages/x-charts/src/LineChart/MarkElement.tsx b/packages/x-charts/src/LineChart/MarkElement.tsx
index 93a77ca25e25..a3d9d6f93857 100644
--- a/packages/x-charts/src/LineChart/MarkElement.tsx
+++ b/packages/x-charts/src/LineChart/MarkElement.tsx
@@ -5,10 +5,12 @@ import { styled } from '@mui/material/styles';
import { symbol as d3Symbol, symbolsFill as d3SymbolsFill } from '@mui/x-charts-vendor/d3-shape';
import { animated, to, useSpring } from '@react-spring/web';
import { getSymbol } from '../internals/getSymbol';
-import { InteractionContext } from '../context/InteractionProvider';
import { useInteractionItemProps } from '../hooks/useInteractionItemProps';
import { useItemHighlighted } from '../context';
import { MarkElementOwnerState, useUtilityClasses } from './markElementClasses';
+import { selectorChartsInteractionXAxis } from '../context/InteractionSelectors';
+import { useSelector } from '../internals/useSelector';
+import { useStore } from '../internals/useStore';
const MarkElementPath = styled(animated.path, {
name: 'MuiMarkElement',
@@ -65,13 +67,15 @@ function MarkElement(props: MarkElementProps) {
const { isFaded, isHighlighted } = useItemHighlighted({
seriesId: id,
});
- const { axis } = React.useContext(InteractionContext);
+
+ const store = useStore();
+ const xAxisIdentifier = useSelector(store, selectorChartsInteractionXAxis);
const position = useSpring({ to: { x, y }, immediate: skipAnimation });
const ownerState = {
id,
classes: innerClasses,
- isHighlighted: axis.x?.index === dataIndex || isHighlighted,
+ isHighlighted: xAxisIdentifier?.index === dataIndex || isHighlighted,
isFaded,
color,
};
diff --git a/packages/x-charts/src/ScatterChart/Scatter.tsx b/packages/x-charts/src/ScatterChart/Scatter.tsx
index 74aea129dc00..b6a95e703587 100644
--- a/packages/x-charts/src/ScatterChart/Scatter.tsx
+++ b/packages/x-charts/src/ScatterChart/Scatter.tsx
@@ -8,10 +8,12 @@ import {
} from '../models/seriesType/scatter';
import { getValueToPositionMapper } from '../hooks/useScale';
import { useInteractionItemProps } from '../hooks/useInteractionItemProps';
-import { InteractionContext } from '../context/InteractionProvider';
import { D3Scale } from '../models/axis';
import { useHighlighted } from '../context';
import { useDrawingArea } from '../hooks/useDrawingArea';
+import { selectorChartsInteractionIsVoronoiEnabled } from '../context/InteractionSelectors';
+import { useSelector } from '../internals/useSelector';
+import { useStore } from '../internals/useStore';
export interface ScatterProps {
series: DefaultizedScatterSeriesType;
@@ -46,9 +48,10 @@ function Scatter(props: ScatterProps) {
const drawingArea = useDrawingArea();
- const { useVoronoiInteraction } = React.useContext(InteractionContext);
+ const store = useStore();
+ const isVoronoiEnabled = useSelector(store, selectorChartsInteractionIsVoronoiEnabled);
- const skipInteractionHandlers = useVoronoiInteraction || series.disableHover;
+ const skipInteractionHandlers = isVoronoiEnabled || series.disableHover;
const getInteractionItemProps = useInteractionItemProps(skipInteractionHandlers);
const { isFaded, isHighlighted } = useHighlighted();
diff --git a/packages/x-charts/src/context/InteractionProvider.tsx b/packages/x-charts/src/context/InteractionProvider.tsx
index 80c4658bfc1f..f31eac1c155d 100644
--- a/packages/x-charts/src/context/InteractionProvider.tsx
+++ b/packages/x-charts/src/context/InteractionProvider.tsx
@@ -1,132 +1,19 @@
'use client';
import * as React from 'react';
-import { ChartItemIdentifier, ChartSeriesType } from '../models/seriesType/config';
+import { useCharts } from '../internals/useCharts';
+import { ChartStore } from '../internals/plugins/utils/ChartStore';
-export interface InteractionProviderProps {
- children: React.ReactNode;
-}
-
-export type ItemInteractionData = ChartItemIdentifier;
-
-export type AxisInteractionData = {
- x: null | {
- value: number | Date | string;
- // Set to -1 if no index.
- index: number;
- };
- y: null | {
- value: number | Date | string;
- // Set to -1 if no index.
- index: number;
- };
-};
-
-type InteractionActions =
- | {
- type: 'enterItem';
- data: ItemInteractionData;
- }
- | {
- type: 'leaveItem';
- data: Partial>;
- }
- | {
- type: 'exitChart';
- }
- | {
- type: 'updateVoronoiUsage';
- useVoronoiInteraction: boolean;
- }
- | {
- type: 'updateAxis';
- data: AxisInteractionData;
- };
-
-type InteractionState = {
- /**
- * The item currently interacting.
- */
- item: null | ItemInteractionData;
- /**
- * The x- and y-axes currently interacting.
- */
- axis: AxisInteractionData;
- /**
- * Set to `true` when `VoronoiHandler` is active.
- * Used to prevent collision with mouseEnter events.
- */
- useVoronoiInteraction: boolean;
- dispatch: React.Dispatch;
-};
-
-export const InteractionContext = React.createContext({
- item: null,
- axis: { x: null, y: null },
- useVoronoiInteraction: false,
- dispatch: () => null,
-});
+export const ChartsContext = React.createContext<{ store: ChartStore } | null>(null);
if (process.env.NODE_ENV !== 'production') {
- InteractionContext.displayName = 'InteractionContext';
+ ChartsContext.displayName = 'ChartsContext';
}
-const dataReducer: React.Reducer, InteractionActions> = (
- prevState,
- action,
-) => {
- switch (action.type) {
- case 'enterItem':
- return { ...prevState, item: action.data };
-
- case 'exitChart':
- if (prevState.item === null && prevState.axis.x === null && prevState.axis.y === null) {
- return prevState;
- }
- return { ...prevState, axis: { x: null, y: null }, item: null };
-
- case 'updateVoronoiUsage':
- return { ...prevState, useVoronoiInteraction: action.useVoronoiInteraction };
-
- case 'leaveItem':
- if (
- prevState.item === null ||
- (Object.keys(action.data) as (keyof ItemInteractionData)[]).some(
- (key) => action.data[key] !== prevState.item![key],
- )
- ) {
- // The item is already something else
- return prevState;
- }
- return { ...prevState, item: null };
-
- case 'updateAxis':
- if (action.data.x === prevState.axis.x && action.data.y === prevState.axis.y) {
- return prevState;
- }
- return { ...prevState, axis: action.data };
-
- default:
- return prevState;
- }
-};
-
-function InteractionProvider(props: InteractionProviderProps) {
+function InteractionProvider(props: React.PropsWithChildren) {
const { children } = props;
- const [data, dispatch] = React.useReducer(dataReducer, {
- item: null,
- axis: { x: null, y: null },
- useVoronoiInteraction: false,
- });
-
- const value = React.useMemo(
- () => ({
- ...data,
- dispatch,
- }),
- [data],
- );
- return {children};
+ const { contextValue } = useCharts();
+ return {children};
}
export { InteractionProvider };
diff --git a/packages/x-charts/src/context/InteractionSelectors.ts b/packages/x-charts/src/context/InteractionSelectors.ts
new file mode 100644
index 000000000000..c911baeae216
--- /dev/null
+++ b/packages/x-charts/src/context/InteractionSelectors.ts
@@ -0,0 +1,46 @@
+import { ChartState } from '../internals/plugins/models';
+import { createSelector } from '../internals/plugins/utils/selectors';
+
+function selectInteraction(state: ChartState) {
+ return state.interaction;
+}
+
+export const selectorChartsInteractionItem = createSelector(
+ selectInteraction,
+ (interaction) => interaction.item,
+);
+
+export const selectorChartsInteractionAxis = createSelector(
+ selectInteraction,
+ (interaction) => interaction.axis,
+);
+
+export const selectorChartsInteractionXAxis = createSelector(
+ selectInteraction,
+ (interaction) => interaction.axis.x,
+);
+
+export const selectorChartsInteractionYAxis = createSelector(
+ selectInteraction,
+ (interaction) => interaction.axis.y,
+);
+
+export const selectorChartsInteractionItemIsDefined = createSelector(
+ selectorChartsInteractionItem,
+ (item) => item !== null,
+);
+
+export const selectorChartsInteractionXAxisIsDefined = createSelector(
+ selectorChartsInteractionXAxis,
+ (x) => x !== null,
+);
+
+export const selectorChartsInteractionYAxisIsDefined = createSelector(
+ selectorChartsInteractionYAxis,
+ (y) => y !== null,
+);
+
+export const selectorChartsInteractionIsVoronoiEnabled = createSelector(
+ selectInteraction,
+ (interaction) => interaction.isVoronoiEnabled,
+);
diff --git a/packages/x-charts/src/hooks/useAxisEvents.ts b/packages/x-charts/src/hooks/useAxisEvents.ts
index f3973f3fb393..2860ba00e32e 100644
--- a/packages/x-charts/src/hooks/useAxisEvents.ts
+++ b/packages/x-charts/src/hooks/useAxisEvents.ts
@@ -1,12 +1,12 @@
'use client';
import * as React from 'react';
-import { InteractionContext } from '../context/InteractionProvider';
import { useCartesianContext } from '../context/CartesianProvider';
import { isBandScale } from '../internals/isBandScale';
import { AxisDefaultized } from '../models/axis';
import { getSVGPoint } from '../internals/getSVGPoint';
import { useSvgRef } from './useSvgRef';
import { useDrawingArea } from './useDrawingArea';
+import { useStore } from '../internals/useStore';
function getAsANumber(value: number | Date) {
return value instanceof Date ? value.getTime() : value;
@@ -15,7 +15,8 @@ export const useAxisEvents = (disableAxisListener: boolean) => {
const svgRef = useSvgRef();
const drawingArea = useDrawingArea();
const { xAxis, yAxis, xAxisIds, yAxisIds } = useCartesianContext();
- const { dispatch } = React.useContext(InteractionContext);
+
+ const store = useStore(disableAxisListener);
const usedXAxis = xAxisIds[0];
const usedYAxis = yAxisIds[0];
@@ -29,7 +30,7 @@ export const useAxisEvents = (disableAxisListener: boolean) => {
React.useEffect(() => {
const element = svgRef.current;
- if (element === null || disableAxisListener) {
+ if (element === null || disableAxisListener || !store) {
return () => {};
}
@@ -100,7 +101,11 @@ export const useAxisEvents = (disableAxisListener: boolean) => {
x: -1,
y: -1,
};
- dispatch({ type: 'exitChart' });
+
+ store.update((prev) => ({
+ ...prev,
+ interaction: { item: null, axis: { x: null, y: null } },
+ }));
};
const handleMove = (event: MouseEvent | TouchEvent) => {
@@ -112,7 +117,10 @@ export const useAxisEvents = (disableAxisListener: boolean) => {
if (!drawingArea.isPointInside(svgPoint, { targetElement: event.target as SVGElement })) {
if (mousePosition.current.isInChart) {
- dispatch({ type: 'exitChart' });
+ store.update((prev) => ({
+ ...prev,
+ interaction: { item: null, axis: { x: null, y: null } },
+ }));
mousePosition.current.isInChart = false;
}
return;
@@ -121,7 +129,24 @@ export const useAxisEvents = (disableAxisListener: boolean) => {
const newStateX = getNewAxisState(xAxis[usedXAxis], svgPoint.x);
const newStateY = getNewAxisState(yAxis[usedYAxis], svgPoint.y);
- dispatch({ type: 'updateAxis', data: { x: newStateX, y: newStateY } });
+ store.update((prev) => ({
+ ...prev,
+ interaction: {
+ ...prev.interaction,
+ axis: {
+ // A bit verbose, but prevent losing the x value if only y got modified.
+ ...prev.interaction.axis,
+ ...(prev.interaction.axis.x?.index !== newStateX?.index ||
+ prev.interaction.axis.x?.value !== newStateX?.value
+ ? { x: newStateX }
+ : {}),
+ ...(prev.interaction.axis.y?.index !== newStateY?.index ||
+ prev.interaction.axis.y?.value !== newStateY?.value
+ ? { y: newStateY }
+ : {}),
+ },
+ },
+ }));
};
const handleDown = (event: PointerEvent) => {
@@ -147,5 +172,5 @@ export const useAxisEvents = (disableAxisListener: boolean) => {
element.removeEventListener('pointercancel', handleOut);
element.removeEventListener('pointerleave', handleOut);
};
- }, [svgRef, dispatch, usedYAxis, yAxis, usedXAxis, xAxis, disableAxisListener, drawingArea]);
+ }, [svgRef, store, usedYAxis, yAxis, usedXAxis, xAxis, disableAxisListener, drawingArea]);
};
diff --git a/packages/x-charts/src/hooks/useInteractionItemProps.ts b/packages/x-charts/src/hooks/useInteractionItemProps.ts
index 49a6e5e70c90..7474425fe650 100644
--- a/packages/x-charts/src/hooks/useInteractionItemProps.ts
+++ b/packages/x-charts/src/hooks/useInteractionItemProps.ts
@@ -1,11 +1,11 @@
'use client';
import * as React from 'react';
-import { InteractionContext } from '../context/InteractionProvider';
import { SeriesItemIdentifier } from '../models';
import { useHighlighted } from '../context';
+import { useStore } from '../internals/useStore';
export const useInteractionItemProps = (skip?: boolean) => {
- const { dispatch: dispatchInteraction } = React.useContext(InteractionContext);
+ const store = useStore();
const { setHighlighted, clearHighlighted } = useHighlighted();
if (skip) {
@@ -18,10 +18,13 @@ export const useInteractionItemProps = (skip?: boolean) => {
}
};
const onPointerEnter = () => {
- dispatchInteraction({
- type: 'enterItem',
- data,
- });
+ store.update((prev) => ({
+ ...prev,
+ interaction: {
+ ...prev.interaction,
+ item: data,
+ },
+ }));
setHighlighted({
seriesId: data.seriesId,
dataIndex: data.dataIndex,
@@ -29,7 +32,26 @@ export const useInteractionItemProps = (skip?: boolean) => {
};
const onPointerLeave = (event: React.PointerEvent) => {
event.currentTarget.releasePointerCapture(event.pointerId);
- dispatchInteraction({ type: 'leaveItem', data });
+
+ store.update((prev) => {
+ const prevItem = prev.interaction.item;
+ if (
+ prevItem === null ||
+ Object.keys(data).some(
+ (key) => data[key as keyof typeof data] !== prevItem[key as keyof typeof prevItem],
+ )
+ ) {
+ // The item is already something else, no need to clean it.
+ return prev;
+ }
+ return {
+ ...prev,
+ interaction: {
+ ...prev.interaction,
+ item: null,
+ },
+ };
+ });
clearHighlighted();
};
return {
diff --git a/packages/x-charts/src/internals/plugins/models/index.ts b/packages/x-charts/src/internals/plugins/models/index.ts
new file mode 100644
index 000000000000..ba5ae5bd5f5f
--- /dev/null
+++ b/packages/x-charts/src/internals/plugins/models/index.ts
@@ -0,0 +1,39 @@
+import { ChartItemIdentifier, ChartSeriesType } from '../../../models/seriesType/config';
+
+export type ItemInteractionData = ChartItemIdentifier;
+
+export type AxisInteractionData = {
+ x: null | {
+ value: number | Date | string;
+ // Set to -1 if no index.
+ index: number;
+ };
+ y: null | {
+ value: number | Date | string;
+ // Set to -1 if no index.
+ index: number;
+ };
+};
+
+type InteractionState = {
+ /**
+ * The item currently interacting.
+ */
+ item: null | ItemInteractionData;
+ /**
+ * The x- and y-axes currently interacting.
+ */
+ axis: AxisInteractionData;
+ /**
+ * Set to `true` when `VoronoiHandler` is active.
+ * Used to prevent collision with mouseEnter events.
+ */
+ isVoronoiEnabled?: boolean;
+};
+
+export type ChartStateCacheKey = { id: number };
+
+export type ChartState = {
+ interaction: InteractionState;
+ cacheKey: ChartStateCacheKey;
+};
diff --git a/packages/x-charts/src/internals/plugins/utils/ChartStore.ts b/packages/x-charts/src/internals/plugins/utils/ChartStore.ts
new file mode 100644
index 000000000000..f4cdbc2324d9
--- /dev/null
+++ b/packages/x-charts/src/internals/plugins/utils/ChartStore.ts
@@ -0,0 +1,35 @@
+import type { ChartState } from '../models'; // For now this is fixed. Will need to support generic if we add plugins
+
+type Listener = (value: T) => void;
+
+export type StoreUpdater = (prevState: ChartState) => ChartState;
+
+export class ChartStore {
+ public value: ChartState;
+
+ private listeners: Set>;
+
+ constructor(value: ChartState) {
+ this.value = value;
+ this.listeners = new Set();
+ }
+
+ public subscribe = (fn: Listener) => {
+ this.listeners.add(fn);
+ return () => {
+ this.listeners.delete(fn);
+ };
+ };
+
+ public getSnapshot = () => {
+ return this.value;
+ };
+
+ public update = (updater: StoreUpdater) => {
+ const newState = updater(this.value);
+ if (newState !== this.value) {
+ this.value = newState;
+ this.listeners.forEach((l) => l(newState));
+ }
+ };
+}
diff --git a/packages/x-charts/src/internals/plugins/utils/selectors.ts b/packages/x-charts/src/internals/plugins/utils/selectors.ts
new file mode 100644
index 000000000000..a91ac4a1a0fa
--- /dev/null
+++ b/packages/x-charts/src/internals/plugins/utils/selectors.ts
@@ -0,0 +1,50 @@
+import { lruMemoize, createSelectorCreator, CreateSelectorFunction } from 'reselect';
+import { ChartState, ChartStateCacheKey } from '../models';
+
+const reselectCreateSelector = createSelectorCreator({
+ memoize: lruMemoize,
+ memoizeOptions: {
+ maxSize: 1,
+ equalityCheck: Object.is,
+ },
+});
+
+const cache = new WeakMap<
+ ChartStateCacheKey,
+ Map, any>
+>();
+
+export type ChartsRootSelector = (state: ChartState) => ChartState[keyof ChartState];
+
+export type ChartsSelector = (state: TState, args: TArgs) => TResult;
+
+/**
+ * Method wrapping reselect's createSelector to provide caching for chart instances.
+ *
+ */
+export const createSelector = ((...createSelectorArgs: any) => {
+ const selector: ChartsSelector = (state, selectorArgs) => {
+ const cacheKey = state.cacheKey;
+
+ // If there is no cache for the current chart instance, create one.
+ let cacheForCurrentChartInstance = cache.get(cacheKey);
+ if (!cacheForCurrentChartInstance) {
+ cacheForCurrentChartInstance = new Map();
+ cache.set(cacheKey, cacheForCurrentChartInstance);
+ }
+
+ // If there is a cached selector, execute it.
+ const cachedSelector = cacheForCurrentChartInstance.get(createSelectorArgs);
+ if (cachedSelector) {
+ return cachedSelector(state, selectorArgs);
+ }
+
+ // Otherwise, create a new selector and cache it and execute it.
+ const fn = reselectCreateSelector(...createSelectorArgs);
+ cacheForCurrentChartInstance.set(createSelectorArgs, fn);
+
+ return fn(state, selectorArgs);
+ };
+
+ return selector;
+}) as unknown as CreateSelectorFunction;
diff --git a/packages/x-charts/src/internals/useCharts.ts b/packages/x-charts/src/internals/useCharts.ts
new file mode 100644
index 000000000000..dddafa853ba8
--- /dev/null
+++ b/packages/x-charts/src/internals/useCharts.ts
@@ -0,0 +1,25 @@
+import * as React from 'react';
+import { ChartStore } from './plugins/utils/ChartStore';
+import { ChartState } from './plugins/models';
+
+let globalId = 0;
+
+export function useCharts() {
+ const storeRef = React.useRef(null);
+ if (storeRef.current == null) {
+ // eslint-disable-next-line react-compiler/react-compiler
+ globalId += 1;
+ const initialState: ChartState = {
+ interaction: {
+ item: null,
+ axis: { x: null, y: null },
+ },
+ cacheKey: { id: globalId },
+ };
+ storeRef.current = new ChartStore(initialState);
+ }
+
+ const contextValue = React.useMemo(() => ({ store: storeRef.current as ChartStore }), []);
+
+ return { contextValue };
+}
diff --git a/packages/x-charts/src/internals/useSelector.ts b/packages/x-charts/src/internals/useSelector.ts
new file mode 100644
index 000000000000..b4940ae5c8d3
--- /dev/null
+++ b/packages/x-charts/src/internals/useSelector.ts
@@ -0,0 +1,23 @@
+import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/with-selector';
+import { ChartState } from './plugins/models';
+import { ChartsSelector } from './plugins/utils/selectors';
+import { ChartStore } from './plugins/utils/ChartStore';
+
+const defaultCompare = Object.is;
+
+export const useSelector = (
+ store: ChartStore,
+ selector: ChartsSelector,
+ args: TArgs = undefined as TArgs,
+ equals: (a: TValue, b: TValue) => boolean = defaultCompare,
+): TValue => {
+ const selectorWithArgs = (state: ChartState) => selector(state, args);
+
+ return useSyncExternalStoreWithSelector(
+ store.subscribe,
+ store.getSnapshot,
+ store.getSnapshot,
+ selectorWithArgs,
+ equals,
+ );
+};
diff --git a/packages/x-charts/src/internals/useStore.ts b/packages/x-charts/src/internals/useStore.ts
new file mode 100644
index 000000000000..17e6d3b6316f
--- /dev/null
+++ b/packages/x-charts/src/internals/useStore.ts
@@ -0,0 +1,27 @@
+import * as React from 'react';
+import { ChartsContext } from '../context/InteractionProvider';
+import { ChartStore } from './plugins/utils/ChartStore';
+
+export function useStore(skipError?: boolean): ChartStore {
+ const charts = React.useContext(ChartsContext);
+
+ if (skipError) {
+ // TODO: Remove once store is used by all charts.
+ // TODO: Remove once store is used by all charts.
+ // This line is only for `useAxisEvents` which is in the surface of the Gauge.
+ // But the Gauge don't have store yet because it does not need the interaction provider.
+ // Will be fixed when every thing move to the store since every component will have access to it.
+ // @ts-ignore
+ return charts?.store;
+ }
+ if (!charts) {
+ throw new Error(
+ [
+ 'MUI X: Could not find the charts context.',
+ 'It looks like you rendered your component outside of a ChartsContainer parent component.',
+ ].join('\n'),
+ );
+ }
+
+ return charts.store;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 12dfa325544c..94b2fc4488a2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -769,6 +769,12 @@ importers:
react-dom:
specifier: ^17.0.0 || ^18.0.0
version: 18.3.1(react@18.3.1)
+ reselect:
+ specifier: ^5.1.1
+ version: 5.1.1
+ use-sync-external-store:
+ specifier: ^1.0.0
+ version: 1.2.2(react@18.3.1)
devDependencies:
'@mui/internal-test-utils':
specifier: ^1.0.20
@@ -788,6 +794,9 @@ importers:
'@types/prop-types':
specifier: ^15.7.13
version: 15.7.13
+ '@types/use-sync-external-store':
+ specifier: ^0.0.6
+ version: 0.0.6
csstype:
specifier: ^3.1.3
version: 3.1.3
diff --git a/scripts/x-charts-pro.exports.json b/scripts/x-charts-pro.exports.json
index ca4a94f205c2..faae92e438c1 100644
--- a/scripts/x-charts-pro.exports.json
+++ b/scripts/x-charts-pro.exports.json
@@ -68,6 +68,7 @@
{ "name": "ChartsAxisHighlightClassKey", "kind": "TypeAlias" },
{ "name": "ChartsAxisHighlightPath", "kind": "Variable" },
{ "name": "ChartsAxisHighlightProps", "kind": "TypeAlias" },
+ { "name": "ChartsAxisHighlightType", "kind": "TypeAlias" },
{ "name": "ChartsAxisProps", "kind": "Interface" },
{ "name": "ChartsAxisTooltipContent", "kind": "Function" },
{ "name": "ChartsClipPath", "kind": "Function" },
diff --git a/scripts/x-charts.exports.json b/scripts/x-charts.exports.json
index ced333367ebc..eb38124c022f 100644
--- a/scripts/x-charts.exports.json
+++ b/scripts/x-charts.exports.json
@@ -66,6 +66,7 @@
{ "name": "ChartsAxisHighlightClassKey", "kind": "TypeAlias" },
{ "name": "ChartsAxisHighlightPath", "kind": "Variable" },
{ "name": "ChartsAxisHighlightProps", "kind": "TypeAlias" },
+ { "name": "ChartsAxisHighlightType", "kind": "TypeAlias" },
{ "name": "ChartsAxisProps", "kind": "Interface" },
{ "name": "ChartsAxisTooltipContent", "kind": "Function" },
{ "name": "ChartsClipPath", "kind": "Function" },