Skip to content

Commit

Permalink
[charts] Move interaction state in store (#15426)
Browse files Browse the repository at this point in the history
Signed-off-by: Alexandre Fauquette <[email protected]>
Co-authored-by: Jose C Quintas Jr <[email protected]>
  • Loading branch information
alexfauquette and JCQuintas authored Nov 18, 2024
1 parent 2d413f3 commit 0da596e
Show file tree
Hide file tree
Showing 34 changed files with 658 additions and 333 deletions.
5 changes: 4 additions & 1 deletion packages/x-charts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
},
Expand Down
150 changes: 7 additions & 143 deletions packages/x-charts/src/ChartsAxisHighlight/ChartsAxisHighlight.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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:
*
Expand All @@ -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 (
<React.Fragment>
{isBandScaleX && xScale(axisX.value) !== undefined && (
<ChartsAxisHighlightPath
// @ts-expect-error, xScale value is checked in the statement above
d={`M ${xScale(axisX.value) - (xScale.step() - xScale.bandwidth()) / 2} ${
yScale.range()[0]
} l ${xScale.step()} 0 l 0 ${
yScale.range()[1] - yScale.range()[0]
} l ${-xScale.step()} 0 Z`}
className={classes.root}
ownerState={{ axisHighlight: 'band' }}
/>
)}

{isBandScaleY && yScale(axisY.value) !== undefined && (
<ChartsAxisHighlightPath
d={`M ${xScale.range()[0]} ${
// @ts-expect-error, yScale value is checked in the statement above
yScale(axisY.value) - (yScale.step() - yScale.bandwidth()) / 2
} l 0 ${yScale.step()} l ${
xScale.range()[1] - xScale.range()[0]
} 0 l 0 ${-yScale.step()} Z`}
className={classes.root}
ownerState={{ axisHighlight: 'band' }}
/>
)}

{xAxisHighlight === 'line' && axis.x !== null && (
<ChartsAxisHighlightPath
d={`M ${getXPosition(axis.x.value)} ${yScale.range()[0]} L ${getXPosition(
axis.x.value,
)} ${yScale.range()[1]}`}
className={classes.root}
ownerState={{ axisHighlight: 'line' }}
/>
)}

{yAxisHighlight === 'line' && axis.y !== null && (
<ChartsAxisHighlightPath
d={`M ${xScale.range()[0]} ${getYPosition(axis.y.value)} L ${
xScale.range()[1]
} ${getYPosition(axis.y.value)}`}
className={classes.root}
ownerState={{ axisHighlight: 'line' }}
/>
)}
{xAxisHighlight && <ChartsXHighlight type={xAxisHighlight} classes={classes} />}
{yAxisHighlight && <ChartsYHighlight type={yAxisHighlight} classes={classes} />}
</React.Fragment>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type ChartsAxisHighlightType = 'none' | 'line' | 'band';

export type ChartsAxisHighlightProps = {
x?: ChartsAxisHighlightType;
y?: ChartsAxisHighlightType;
};
Original file line number Diff line number Diff line change
@@ -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',
}),
},
},
],
}));
69 changes: 69 additions & 0 deletions packages/x-charts/src/ChartsAxisHighlight/ChartsXAxisHighlight.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<React.Fragment>
{isBandScaleX && xScale(axisX.value) !== undefined && (
<ChartsAxisHighlightPath
// @ts-expect-error, xScale value is checked in the statement above
d={`M ${xScale(axisX.value) - (xScale.step() - xScale.bandwidth()) / 2} ${
top
} l ${xScale.step()} 0 l 0 ${height} l ${-xScale.step()} 0 Z`}
className={classes.root}
ownerState={{ axisHighlight: 'band' }}
/>
)}

{type === 'line' && axisX !== null && (
<ChartsAxisHighlightPath
d={`M ${getXPosition(axisX.value)} ${top} L ${getXPosition(axisX.value)} ${top + height}`}
className={classes.root}
ownerState={{ axisHighlight: 'line' }}
/>
)}
</React.Fragment>
);
}
71 changes: 71 additions & 0 deletions packages/x-charts/src/ChartsAxisHighlight/ChartsYAxisHighlight.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<React.Fragment>
{isBandScaleY && yScale(axisY.value) !== undefined && (
<ChartsAxisHighlightPath
d={`M ${left} ${
// @ts-expect-error, yScale value is checked in the statement above
yScale(axisY.value) - (yScale.step() - yScale.bandwidth()) / 2
} l 0 ${yScale.step()} l ${width} 0 l 0 ${-yScale.step()} Z`}
className={classes.root}
ownerState={{ axisHighlight: 'band' }}
/>
)}

{type === 'line' && axisY !== null && (
<ChartsAxisHighlightPath
d={`M ${left} ${getYPosition(axisY.value)} L ${left + width} ${getYPosition(
axisY.value,
)}`}
className={classes.root}
ownerState={{ axisHighlight: 'line' }}
/>
)}
</React.Fragment>
);
}
Original file line number Diff line number Diff line change
@@ -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'],
);
Loading

0 comments on commit 0da596e

Please sign in to comment.