diff --git a/docs/data/charts-component-api-pages.ts b/docs/data/charts-component-api-pages.ts index 5d3e702baff5a..b2a117fef59bc 100644 --- a/docs/data/charts-component-api-pages.ts +++ b/docs/data/charts-component-api-pages.ts @@ -1,6 +1,14 @@ import type { MuiPage } from '@mui/monorepo/docs/src/MuiPage'; const apiPages: MuiPage[] = [ + { + pathname: '/x/api/charts/animated-area', + title: 'AnimatedArea', + }, + { + pathname: '/x/api/charts/animated-line', + title: 'AnimatedLine', + }, { pathname: '/x/api/charts/area-element', title: 'AreaElement', diff --git a/docs/data/charts/lines/ConnectNulls.js b/docs/data/charts/lines/ConnectNulls.js index 1a543aeecda35..0032c2383eab1 100644 --- a/docs/data/charts/lines/ConnectNulls.js +++ b/docs/data/charts/lines/ConnectNulls.js @@ -31,6 +31,7 @@ export default function ConnectNulls() { ]} height={200} margin={{ top: 10, bottom: 20 }} + skipAnimation /> ); diff --git a/docs/data/charts/lines/ConnectNulls.tsx b/docs/data/charts/lines/ConnectNulls.tsx index 1a543aeecda35..0032c2383eab1 100644 --- a/docs/data/charts/lines/ConnectNulls.tsx +++ b/docs/data/charts/lines/ConnectNulls.tsx @@ -31,6 +31,7 @@ export default function ConnectNulls() { ]} height={200} margin={{ top: 10, bottom: 20 }} + skipAnimation /> ); diff --git a/docs/data/charts/lines/InterpolationDemoNoSnap.js b/docs/data/charts/lines/InterpolationDemoNoSnap.js index 9ee64cc05e75c..7d96ed6587b13 100644 --- a/docs/data/charts/lines/InterpolationDemoNoSnap.js +++ b/docs/data/charts/lines/InterpolationDemoNoSnap.js @@ -52,6 +52,7 @@ export default function InterpolationDemoNoSnap() { ]} height={300} margin={{ top: 10, bottom: 30 }} + skipAnimation /> diff --git a/docs/data/charts/lines/LineAnimation.js b/docs/data/charts/lines/LineAnimation.js new file mode 100644 index 0000000000000..7bb2d20fda951 --- /dev/null +++ b/docs/data/charts/lines/LineAnimation.js @@ -0,0 +1,87 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Stack from '@mui/material/Stack'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import { LineChart } from '@mui/x-charts/LineChart'; +import { mangoFusionPalette } from '@mui/x-charts/colorPalettes'; + +const defaultSeries = [ + { id: '1', data: [4, 5, 1, 2, 3, 3, 2], area: true, stack: '1' }, + { id: '2', data: [7, 4, 6, 7, 2, 3, 5], area: true, stack: '1' }, + { id: '3', data: [6, 4, 1, 2, 6, 3, 3], area: true, stack: '1' }, + { id: '4', data: [4, 7, 6, 1, 2, 7, 7], area: true, stack: '1' }, + { id: '5', data: [2, 2, 1, 7, 1, 5, 3], area: true, stack: '1' }, + { id: '6', data: [6, 6, 1, 6, 7, 1, 1], area: true, stack: '1' }, + { id: '7', data: [7, 6, 1, 6, 4, 4, 6], area: true, stack: '1' }, + { id: '8', data: [4, 3, 1, 6, 6, 3, 5], area: true, stack: '1' }, + { id: '9', data: [7, 6, 2, 7, 4, 2, 7], area: true, stack: '1' }, +].map((item, index) => ({ + ...item, + color: mangoFusionPalette('light')[index], +})); + +export default function LineAnimation() { + const [series, setSeries] = React.useState(defaultSeries); + const [nbSeries, setNbSeries] = React.useState(3); + const [skipAnimation, setSkipAnimation] = React.useState(false); + + return ( +
+
+ +
+ + + + + setSkipAnimation(event.target.checked)} /> + } + label="skipAnimation" + labelPlacement="end" + /> + +
+ ); +} diff --git a/docs/data/charts/lines/LineAnimation.tsx b/docs/data/charts/lines/LineAnimation.tsx new file mode 100644 index 0000000000000..7bb2d20fda951 --- /dev/null +++ b/docs/data/charts/lines/LineAnimation.tsx @@ -0,0 +1,87 @@ +import * as React from 'react'; +import Button from '@mui/material/Button'; +import Stack from '@mui/material/Stack'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import { LineChart } from '@mui/x-charts/LineChart'; +import { mangoFusionPalette } from '@mui/x-charts/colorPalettes'; + +const defaultSeries = [ + { id: '1', data: [4, 5, 1, 2, 3, 3, 2], area: true, stack: '1' }, + { id: '2', data: [7, 4, 6, 7, 2, 3, 5], area: true, stack: '1' }, + { id: '3', data: [6, 4, 1, 2, 6, 3, 3], area: true, stack: '1' }, + { id: '4', data: [4, 7, 6, 1, 2, 7, 7], area: true, stack: '1' }, + { id: '5', data: [2, 2, 1, 7, 1, 5, 3], area: true, stack: '1' }, + { id: '6', data: [6, 6, 1, 6, 7, 1, 1], area: true, stack: '1' }, + { id: '7', data: [7, 6, 1, 6, 4, 4, 6], area: true, stack: '1' }, + { id: '8', data: [4, 3, 1, 6, 6, 3, 5], area: true, stack: '1' }, + { id: '9', data: [7, 6, 2, 7, 4, 2, 7], area: true, stack: '1' }, +].map((item, index) => ({ + ...item, + color: mangoFusionPalette('light')[index], +})); + +export default function LineAnimation() { + const [series, setSeries] = React.useState(defaultSeries); + const [nbSeries, setNbSeries] = React.useState(3); + const [skipAnimation, setSkipAnimation] = React.useState(false); + + return ( +
+
+ +
+ + + + + setSkipAnimation(event.target.checked)} /> + } + label="skipAnimation" + labelPlacement="end" + /> + +
+ ); +} diff --git a/docs/data/charts/lines/lines.md b/docs/data/charts/lines/lines.md index 8bc1fd8eb89bb..37a1250e87f8a 100644 --- a/docs/data/charts/lines/lines.md +++ b/docs/data/charts/lines/lines.md @@ -1,7 +1,7 @@ --- title: React Line chart productId: x-charts -components: LineChart, LineElement, LineHighlightElement, LineHighlightPlot, LinePlot, MarkElement, MarkPlot, AreaElement, AreaPlot +components: LineChart, LineElement, LineHighlightElement, LineHighlightPlot, LinePlot, MarkElement, MarkPlot, AreaElement, AreaPlot, AnimatedLine, AnimatedArea --- # Charts - Lines @@ -143,3 +143,30 @@ sx={{ ``` {{"demo": "CSSCustomization.js"}} + +## Animation + +To skip animation at the creation and update of your chart, you can use the `skipAnimation` prop. +When set to `true` it skips animation powered by `@react-spring/web`. + +Charts containers already use the `useReducedMotion` from `@react-spring/web` to skip animation [according to user preferences](https://react-spring.dev/docs/utilities/use-reduced-motion#why-is-it-important). + +:::warning +If you support interactive ways to add or remove series from your chart, you have to provide the series' id. + +Otherwise the chart will have no way to know if you are modifying, removing, or adding some series. +This will lead to strange behaviors. +::: + +```jsx +// For a single component chart + + +// For a composed chart + + + + +``` + +{{"demo": "LineAnimation.js"}} diff --git a/docs/pages/x/api/charts/animated-area.js b/docs/pages/x/api/charts/animated-area.js new file mode 100644 index 0000000000000..f03605dd0bcfd --- /dev/null +++ b/docs/pages/x/api/charts/animated-area.js @@ -0,0 +1,23 @@ +import * as React from 'react'; +import ApiPage from 'docs/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './animated-area.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context( + 'docsx/translations/api-docs/charts/animated-area', + false, + /\.\/animated-area.*.json$/, + ); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/x/api/charts/animated-area.json b/docs/pages/x/api/charts/animated-area.json new file mode 100644 index 0000000000000..709e7b00d3ff9 --- /dev/null +++ b/docs/pages/x/api/charts/animated-area.json @@ -0,0 +1,14 @@ +{ + "props": { "skipAnimation": { "type": { "name": "bool" }, "default": "false" } }, + "name": "AnimatedArea", + "imports": [ + "import { AnimatedArea } from '@mui/x-charts/LineChart';", + "import { AnimatedArea } from '@mui/x-charts';" + ], + "classes": [], + "muiName": "MuiAnimatedArea", + "filename": "/packages/x-charts/src/LineChart/AnimatedArea.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/x/api/charts/animated-line.js b/docs/pages/x/api/charts/animated-line.js new file mode 100644 index 0000000000000..ac113d11aa47e --- /dev/null +++ b/docs/pages/x/api/charts/animated-line.js @@ -0,0 +1,23 @@ +import * as React from 'react'; +import ApiPage from 'docs/src/modules/components/ApiPage'; +import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations'; +import jsonPageContent from './animated-line.json'; + +export default function Page(props) { + const { descriptions, pageContent } = props; + return ; +} + +Page.getInitialProps = () => { + const req = require.context( + 'docsx/translations/api-docs/charts/animated-line', + false, + /\.\/animated-line.*.json$/, + ); + const descriptions = mapApiPageTranslations(req); + + return { + descriptions, + pageContent: jsonPageContent, + }; +}; diff --git a/docs/pages/x/api/charts/animated-line.json b/docs/pages/x/api/charts/animated-line.json new file mode 100644 index 0000000000000..b107967c75f46 --- /dev/null +++ b/docs/pages/x/api/charts/animated-line.json @@ -0,0 +1,14 @@ +{ + "props": { "skipAnimation": { "type": { "name": "bool" }, "default": "false" } }, + "name": "AnimatedLine", + "imports": [ + "import { AnimatedLine } from '@mui/x-charts/LineChart';", + "import { AnimatedLine } from '@mui/x-charts';" + ], + "classes": [], + "muiName": "MuiAnimatedLine", + "filename": "/packages/x-charts/src/LineChart/AnimatedLine.tsx", + "inheritance": null, + "demos": "", + "cssComponent": false +} diff --git a/docs/pages/x/api/charts/area-element.json b/docs/pages/x/api/charts/area-element.json index 7ca180d2214d3..b5dfedae13bd5 100644 --- a/docs/pages/x/api/charts/area-element.json +++ b/docs/pages/x/api/charts/area-element.json @@ -1,5 +1,6 @@ { "props": { + "skipAnimation": { "type": { "name": "bool" }, "default": "false" }, "slotProps": { "type": { "name": "object" }, "default": "{}" }, "slots": { "type": { "name": "object" }, @@ -12,6 +13,14 @@ "import { AreaElement } from '@mui/x-charts/LineChart';", "import { AreaElement } from '@mui/x-charts';" ], + "slots": [ + { + "name": "area", + "description": "The component that renders the area.", + "default": "AnimatedArea", + "class": null + } + ], "classes": [ { "key": "faded", diff --git a/docs/pages/x/api/charts/area-plot.json b/docs/pages/x/api/charts/area-plot.json index c0a421fbd92da..30c6fa3f2ce3c 100644 --- a/docs/pages/x/api/charts/area-plot.json +++ b/docs/pages/x/api/charts/area-plot.json @@ -1,5 +1,6 @@ { "props": { + "skipAnimation": { "type": { "name": "bool" }, "default": "false" }, "slotProps": { "type": { "name": "object" }, "default": "{}" }, "slots": { "type": { "name": "object" }, @@ -12,7 +13,14 @@ "import { AreaPlot } from '@mui/x-charts/LineChart';", "import { AreaPlot } from '@mui/x-charts';" ], - "slots": [{ "name": "area", "description": "", "class": null }], + "slots": [ + { + "name": "area", + "description": "The component that renders the area.", + "default": "AnimatedArea", + "class": null + } + ], "classes": [], "muiName": "MuiAreaPlot", "filename": "/packages/x-charts/src/LineChart/AreaPlot.tsx", diff --git a/docs/pages/x/api/charts/default-charts-axis-tooltip-content.json b/docs/pages/x/api/charts/default-charts-axis-tooltip-content.json index fbb3dfe92b823..951dac84a962a 100644 --- a/docs/pages/x/api/charts/default-charts-axis-tooltip-content.json +++ b/docs/pages/x/api/charts/default-charts-axis-tooltip-content.json @@ -4,7 +4,7 @@ "axisData": { "type": { "name": "shape", - "description": "{ x?: { index?: number, value: Date
| number }, y?: { index?: number, value: Date
| number } }" + "description": "{ x?: { index?: number, value: Date
| number
| string }, y?: { index?: number, value: Date
| number
| string } }" }, "required": true }, diff --git a/docs/pages/x/api/charts/line-chart.json b/docs/pages/x/api/charts/line-chart.json index 94b00d38bddfa..1ae374f7c5e72 100644 --- a/docs/pages/x/api/charts/line-chart.json +++ b/docs/pages/x/api/charts/line-chart.json @@ -43,6 +43,7 @@ }, "default": "null" }, + "skipAnimation": { "type": { "name": "bool" }, "default": "false" }, "slotProps": { "type": { "name": "object" }, "default": "{}" }, "slots": { "type": { "name": "object" }, @@ -80,8 +81,18 @@ { "name": "axisTick", "description": "", "class": null }, { "name": "axisTickLabel", "description": "", "class": null }, { "name": "axisLabel", "description": "", "class": null }, - { "name": "area", "description": "", "class": null }, - { "name": "line", "description": "", "class": null }, + { + "name": "area", + "description": "The component that renders the area.", + "default": "AnimatedArea", + "class": null + }, + { + "name": "line", + "description": "The component that renders the line.", + "default": "LineElementPath", + "class": null + }, { "name": "mark", "description": "", "class": null }, { "name": "lineHighlight", "description": "", "class": null }, { "name": "legend", "description": "", "class": null }, diff --git a/docs/pages/x/api/charts/line-element.json b/docs/pages/x/api/charts/line-element.json index 92a5ebdbd4d62..3d971c68082e7 100644 --- a/docs/pages/x/api/charts/line-element.json +++ b/docs/pages/x/api/charts/line-element.json @@ -1,5 +1,6 @@ { "props": { + "skipAnimation": { "type": { "name": "bool" }, "default": "false" }, "slotProps": { "type": { "name": "object" }, "default": "{}" }, "slots": { "type": { "name": "object" }, @@ -12,6 +13,14 @@ "import { LineElement } from '@mui/x-charts/LineChart';", "import { LineElement } from '@mui/x-charts';" ], + "slots": [ + { + "name": "line", + "description": "The component that renders the line.", + "default": "LineElementPath", + "class": null + } + ], "classes": [ { "key": "faded", diff --git a/docs/pages/x/api/charts/line-plot.json b/docs/pages/x/api/charts/line-plot.json index db76736025062..695302fba7fa5 100644 --- a/docs/pages/x/api/charts/line-plot.json +++ b/docs/pages/x/api/charts/line-plot.json @@ -1,5 +1,6 @@ { "props": { + "skipAnimation": { "type": { "name": "bool" }, "default": "false" }, "slotProps": { "type": { "name": "object" }, "default": "{}" }, "slots": { "type": { "name": "object" }, @@ -12,7 +13,14 @@ "import { LinePlot } from '@mui/x-charts/LineChart';", "import { LinePlot } from '@mui/x-charts';" ], - "slots": [{ "name": "line", "description": "", "class": null }], + "slots": [ + { + "name": "line", + "description": "The component that renders the line.", + "default": "LineElementPath", + "class": null + } + ], "classes": [], "muiName": "MuiLinePlot", "filename": "/packages/x-charts/src/LineChart/LinePlot.tsx", diff --git a/docs/pages/x/api/charts/mark-element.json b/docs/pages/x/api/charts/mark-element.json index 7440419178cfc..492c60b29943e 100644 --- a/docs/pages/x/api/charts/mark-element.json +++ b/docs/pages/x/api/charts/mark-element.json @@ -7,7 +7,8 @@ "description": "'circle'
| 'cross'
| 'diamond'
| 'square'
| 'star'
| 'triangle'
| 'wye'" }, "required": true - } + }, + "skipAnimation": { "type": { "name": "bool" }, "default": "false" } }, "name": "MarkElement", "imports": [ diff --git a/docs/pages/x/api/charts/mark-plot.json b/docs/pages/x/api/charts/mark-plot.json index 9cd5db89c4ff5..e334cc78e61d5 100644 --- a/docs/pages/x/api/charts/mark-plot.json +++ b/docs/pages/x/api/charts/mark-plot.json @@ -1,5 +1,6 @@ { "props": { + "skipAnimation": { "type": { "name": "bool" }, "default": "false" }, "slotProps": { "type": { "name": "object" }, "default": "{}" }, "slots": { "type": { "name": "object" }, diff --git a/docs/pages/x/api/charts/spark-line-chart.json b/docs/pages/x/api/charts/spark-line-chart.json index 3b17773325964..6ac9a094ae81f 100644 --- a/docs/pages/x/api/charts/spark-line-chart.json +++ b/docs/pages/x/api/charts/spark-line-chart.json @@ -54,8 +54,18 @@ "import { SparkLineChart } from '@mui/x-charts';" ], "slots": [ - { "name": "area", "description": "", "class": null }, - { "name": "line", "description": "", "class": null }, + { + "name": "area", + "description": "The component that renders the area.", + "default": "AnimatedArea", + "class": null + }, + { + "name": "line", + "description": "The component that renders the line.", + "default": "LineElementPath", + "class": null + }, { "name": "mark", "description": "", "class": null }, { "name": "lineHighlight", "description": "", "class": null }, { "name": "bar", "description": "", "class": null }, diff --git a/docs/translations/api-docs/charts/animated-area/animated-area.json b/docs/translations/api-docs/charts/animated-area/animated-area.json new file mode 100644 index 0000000000000..5300fa0d8559a --- /dev/null +++ b/docs/translations/api-docs/charts/animated-area/animated-area.json @@ -0,0 +1,7 @@ +{ + "componentDescription": "", + "propDescriptions": { + "skipAnimation": { "description": "If true, animations are skipped." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/charts/animated-line/animated-line.json b/docs/translations/api-docs/charts/animated-line/animated-line.json new file mode 100644 index 0000000000000..5300fa0d8559a --- /dev/null +++ b/docs/translations/api-docs/charts/animated-line/animated-line.json @@ -0,0 +1,7 @@ +{ + "componentDescription": "", + "propDescriptions": { + "skipAnimation": { "description": "If true, animations are skipped." } + }, + "classDescriptions": {} +} diff --git a/docs/translations/api-docs/charts/area-element/area-element.json b/docs/translations/api-docs/charts/area-element/area-element.json index fca3901855f08..35ae130885231 100644 --- a/docs/translations/api-docs/charts/area-element/area-element.json +++ b/docs/translations/api-docs/charts/area-element/area-element.json @@ -1,6 +1,7 @@ { "componentDescription": "", "propDescriptions": { + "skipAnimation": { "description": "If true, animations are skipped." }, "slotProps": { "description": "The props used for each component slot." }, "slots": { "description": "Overridable component slots." } }, @@ -16,5 +17,6 @@ "conditions": "higlighted" }, "root": { "description": "Styles applied to the root element." } - } + }, + "slotDescriptions": { "area": "The component that renders the area." } } diff --git a/docs/translations/api-docs/charts/area-plot/area-plot.json b/docs/translations/api-docs/charts/area-plot/area-plot.json index dd9fd23a9934c..44b71c6c124ce 100644 --- a/docs/translations/api-docs/charts/area-plot/area-plot.json +++ b/docs/translations/api-docs/charts/area-plot/area-plot.json @@ -1,9 +1,10 @@ { "componentDescription": "", "propDescriptions": { + "skipAnimation": { "description": "If true, animations are skipped." }, "slotProps": { "description": "The props used for each component slot." }, "slots": { "description": "Overridable component slots." } }, "classDescriptions": {}, - "slotDescriptions": { "area": "" } + "slotDescriptions": { "area": "The component that renders the area." } } diff --git a/docs/translations/api-docs/charts/bar-chart/bar-chart.json b/docs/translations/api-docs/charts/bar-chart/bar-chart.json index e3ecc71677a8c..52c7ccee6d9e5 100644 --- a/docs/translations/api-docs/charts/bar-chart/bar-chart.json +++ b/docs/translations/api-docs/charts/bar-chart/bar-chart.json @@ -26,7 +26,7 @@ "rightAxis": { "description": "Indicate which axis to display the right of the charts. Can be a string (the id of the axis) or an object ChartsYAxisProps." }, - "skipAnimation": { "description": "If true, animations are skiped." }, + "skipAnimation": { "description": "If true, animations are skipped." }, "slotProps": { "description": "The props used for each component slot." }, "slots": { "description": "Overridable component slots." }, "topAxis": { diff --git a/docs/translations/api-docs/charts/bar-plot/bar-plot.json b/docs/translations/api-docs/charts/bar-plot/bar-plot.json index d9ee286961782..b84aa3efd86ae 100644 --- a/docs/translations/api-docs/charts/bar-plot/bar-plot.json +++ b/docs/translations/api-docs/charts/bar-plot/bar-plot.json @@ -1,7 +1,7 @@ { "componentDescription": "", "propDescriptions": { - "skipAnimation": { "description": "If true, animations are skiped." }, + "skipAnimation": { "description": "If true, animations are skipped." }, "slotProps": { "description": "The props used for each component slot." }, "slots": { "description": "Overridable component slots." } }, diff --git a/docs/translations/api-docs/charts/line-chart/line-chart.json b/docs/translations/api-docs/charts/line-chart/line-chart.json index f279ed2629d81..a6d9fe50d903f 100644 --- a/docs/translations/api-docs/charts/line-chart/line-chart.json +++ b/docs/translations/api-docs/charts/line-chart/line-chart.json @@ -29,6 +29,7 @@ "rightAxis": { "description": "Indicate which axis to display the right of the charts. Can be a string (the id of the axis) or an object ChartsYAxisProps." }, + "skipAnimation": { "description": "If true, animations are skipped." }, "slotProps": { "description": "The props used for each component slot." }, "slots": { "description": "Overridable component slots." }, "topAxis": { @@ -46,7 +47,7 @@ }, "classDescriptions": {}, "slotDescriptions": { - "area": "", + "area": "The component that renders the area.", "axisContent": "", "axisLabel": "", "axisLine": "", @@ -54,7 +55,7 @@ "axisTickLabel": "", "itemContent": "", "legend": "", - "line": "", + "line": "The component that renders the line.", "lineHighlight": "", "mark": "", "popper": "" diff --git a/docs/translations/api-docs/charts/line-element/line-element.json b/docs/translations/api-docs/charts/line-element/line-element.json index fca3901855f08..c638934e73ea0 100644 --- a/docs/translations/api-docs/charts/line-element/line-element.json +++ b/docs/translations/api-docs/charts/line-element/line-element.json @@ -1,6 +1,7 @@ { "componentDescription": "", "propDescriptions": { + "skipAnimation": { "description": "If true, animations are skipped." }, "slotProps": { "description": "The props used for each component slot." }, "slots": { "description": "Overridable component slots." } }, @@ -16,5 +17,6 @@ "conditions": "higlighted" }, "root": { "description": "Styles applied to the root element." } - } + }, + "slotDescriptions": { "line": "The component that renders the line." } } diff --git a/docs/translations/api-docs/charts/line-plot/line-plot.json b/docs/translations/api-docs/charts/line-plot/line-plot.json index d1f64423b9414..32105cc21e585 100644 --- a/docs/translations/api-docs/charts/line-plot/line-plot.json +++ b/docs/translations/api-docs/charts/line-plot/line-plot.json @@ -1,9 +1,10 @@ { "componentDescription": "", "propDescriptions": { + "skipAnimation": { "description": "If true, animations are skipped." }, "slotProps": { "description": "The props used for each component slot." }, "slots": { "description": "Overridable component slots." } }, "classDescriptions": {}, - "slotDescriptions": { "line": "" } + "slotDescriptions": { "line": "The component that renders the line." } } diff --git a/docs/translations/api-docs/charts/mark-element/mark-element.json b/docs/translations/api-docs/charts/mark-element/mark-element.json index 81441020bb99f..522d722c7a871 100644 --- a/docs/translations/api-docs/charts/mark-element/mark-element.json +++ b/docs/translations/api-docs/charts/mark-element/mark-element.json @@ -2,7 +2,8 @@ "componentDescription": "", "propDescriptions": { "dataIndex": { "description": "The index to the element in the series' data array." }, - "shape": { "description": "The shape of the marker." } + "shape": { "description": "The shape of the marker." }, + "skipAnimation": { "description": "If true, animations are skipped." } }, "classDescriptions": { "faded": { diff --git a/docs/translations/api-docs/charts/mark-plot/mark-plot.json b/docs/translations/api-docs/charts/mark-plot/mark-plot.json index aace660db0caa..c338ac1652e53 100644 --- a/docs/translations/api-docs/charts/mark-plot/mark-plot.json +++ b/docs/translations/api-docs/charts/mark-plot/mark-plot.json @@ -1,6 +1,7 @@ { "componentDescription": "", "propDescriptions": { + "skipAnimation": { "description": "If true, animations are skipped." }, "slotProps": { "description": "The props used for each component slot." }, "slots": { "description": "Overridable component slots." } }, diff --git a/docs/translations/api-docs/charts/pie-arc-label-plot/pie-arc-label-plot.json b/docs/translations/api-docs/charts/pie-arc-label-plot/pie-arc-label-plot.json index 599a5d9f91662..c4bf2a3494302 100644 --- a/docs/translations/api-docs/charts/pie-arc-label-plot/pie-arc-label-plot.json +++ b/docs/translations/api-docs/charts/pie-arc-label-plot/pie-arc-label-plot.json @@ -16,7 +16,7 @@ }, "outerRadius": { "description": "The radius between circle center and the end of the arc." }, "paddingAngle": { "description": "The padding angle (deg) between two arcs." }, - "skipAnimation": { "description": "If true, animations are skiped." }, + "skipAnimation": { "description": "If true, animations are skipped." }, "slotProps": { "description": "The props used for each component slot." }, "slots": { "description": "Overridable component slots." } }, diff --git a/docs/translations/api-docs/charts/pie-arc-plot/pie-arc-plot.json b/docs/translations/api-docs/charts/pie-arc-plot/pie-arc-plot.json index 8c4cdaea4809b..58bfec3d2112b 100644 --- a/docs/translations/api-docs/charts/pie-arc-plot/pie-arc-plot.json +++ b/docs/translations/api-docs/charts/pie-arc-plot/pie-arc-plot.json @@ -22,7 +22,7 @@ }, "outerRadius": { "description": "The radius between circle center and the end of the arc." }, "paddingAngle": { "description": "The padding angle (deg) between two arcs." }, - "skipAnimation": { "description": "If true, animations are skiped." }, + "skipAnimation": { "description": "If true, animations are skipped." }, "slotProps": { "description": "The props used for each component slot." }, "slots": { "description": "Overridable component slots." } }, diff --git a/docs/translations/api-docs/charts/pie-chart/pie-chart.json b/docs/translations/api-docs/charts/pie-chart/pie-chart.json index 8960e6b3a746f..c9c6863b0053a 100644 --- a/docs/translations/api-docs/charts/pie-chart/pie-chart.json +++ b/docs/translations/api-docs/charts/pie-chart/pie-chart.json @@ -23,7 +23,7 @@ "rightAxis": { "description": "Indicate which axis to display the right of the charts. Can be a string (the id of the axis) or an object ChartsYAxisProps." }, - "skipAnimation": { "description": "If true, animations are skiped." }, + "skipAnimation": { "description": "If true, animations are skipped." }, "slotProps": { "description": "The props used for each component slot." }, "topAxis": { "description": "Indicate which axis to display the top of the charts. Can be a string (the id of the axis) or an object ChartsXAxisProps." diff --git a/docs/translations/api-docs/charts/pie-plot/pie-plot.json b/docs/translations/api-docs/charts/pie-plot/pie-plot.json index d156b39b03064..bf05960bf8799 100644 --- a/docs/translations/api-docs/charts/pie-plot/pie-plot.json +++ b/docs/translations/api-docs/charts/pie-plot/pie-plot.json @@ -9,7 +9,7 @@ "item": "The pie item." } }, - "skipAnimation": { "description": "If true, animations are skiped." }, + "skipAnimation": { "description": "If true, animations are skipped." }, "slotProps": { "description": "The props used for each component slot." }, "slots": { "description": "Overridable component slots." } }, diff --git a/docs/translations/api-docs/charts/spark-line-chart/spark-line-chart.json b/docs/translations/api-docs/charts/spark-line-chart/spark-line-chart.json index 9ab42a24b8b30..6e43932009df6 100644 --- a/docs/translations/api-docs/charts/spark-line-chart/spark-line-chart.json +++ b/docs/translations/api-docs/charts/spark-line-chart/spark-line-chart.json @@ -40,11 +40,11 @@ }, "classDescriptions": {}, "slotDescriptions": { - "area": "", + "area": "The component that renders the area.", "axisContent": "", "bar": "", "itemContent": "", - "line": "", + "line": "The component that renders the line.", "lineHighlight": "", "mark": "", "popper": "" diff --git a/packages/x-charts/package.json b/packages/x-charts/package.json index 4b37ba5922ae2..2a3a4c90fc047 100644 --- a/packages/x-charts/package.json +++ b/packages/x-charts/package.json @@ -50,6 +50,7 @@ "d3-delaunay": "^6.0.4", "d3-scale": "^4.0.2", "d3-shape": "^3.2.0", + "d3-interpolate": "^3.0.1", "prop-types": "^15.8.1" }, "peerDependencies": { @@ -71,6 +72,7 @@ "@types/d3-color": "^3.1.3", "@types/d3-delaunay": "^6.0.4", "@types/d3-scale": "^4.0.8", + "@types/d3-interpolate": "^3.0.4", "@types/d3-shape": "^3.1.6" }, "exports": { diff --git a/packages/x-charts/src/BarChart/BarChart.tsx b/packages/x-charts/src/BarChart/BarChart.tsx index f3215547a8974..b83f033fdcdd9 100644 --- a/packages/x-charts/src/BarChart/BarChart.tsx +++ b/packages/x-charts/src/BarChart/BarChart.tsx @@ -339,7 +339,7 @@ BarChart.propTypes = { ]), series: PropTypes.arrayOf(PropTypes.object).isRequired, /** - * If `true`, animations are skiped. + * If `true`, animations are skipped. * @default false */ skipAnimation: PropTypes.bool, diff --git a/packages/x-charts/src/BarChart/BarElement.tsx b/packages/x-charts/src/BarChart/BarElement.tsx index 0bcca243e516f..df1a0dfe483ea 100644 --- a/packages/x-charts/src/BarChart/BarElement.tsx +++ b/packages/x-charts/src/BarChart/BarElement.tsx @@ -78,7 +78,7 @@ export type BarElementProps = Omit { /** - * If `true`, animations are skiped. + * If `true`, animations are skipped. * @default false */ skipAnimation?: boolean; @@ -74,7 +74,7 @@ interface CompletedBarData { highlightScope?: Partial; } -const useCompletedData = (): CompletedBarData[] => { +const useAggregatedData = (): CompletedBarData[] => { const seriesData = React.useContext(SeriesContext).bar ?? ({ series: {}, stackingGroups: [], seriesOrder: [] } as FormatterResult<'bar'>); @@ -215,7 +215,7 @@ const getInStyle = ({ x, width, y, height }: CompletedBarData) => ({ * - [BarPlot API](https://mui.com/x/api/charts/bar-plot/) */ function BarPlot(props: BarPlotProps) { - const completedData = useCompletedData(); + const completedData = useAggregatedData(); const { skipAnimation, ...other } = props; const transition = useTransition(completedData, { @@ -248,7 +248,7 @@ BarPlot.propTypes = { // | To update them edit the TypeScript types and run "yarn proptypes" | // ---------------------------------------------------------------------- /** - * If `true`, animations are skiped. + * If `true`, animations are skipped. * @default false */ skipAnimation: PropTypes.bool, diff --git a/packages/x-charts/src/ChartsReferenceLine/ChartsXReferenceLine.tsx b/packages/x-charts/src/ChartsReferenceLine/ChartsXReferenceLine.tsx index 765d7d43f1b0e..6e668f814b13e 100644 --- a/packages/x-charts/src/ChartsReferenceLine/ChartsXReferenceLine.tsx +++ b/packages/x-charts/src/ChartsReferenceLine/ChartsXReferenceLine.tsx @@ -96,7 +96,7 @@ function ChartsXReferenceLine(props: ChartsXReferenceLineProps) { if (!warnedOnce) { warnedOnce = true; console.error( - `MUI X: the value ${x} does not exist in the data of x axis with id ${axisId}.`, + `MUI X Charts: the value ${x} does not exist in the data of x axis with id ${axisId}.`, ); } } diff --git a/packages/x-charts/src/ChartsReferenceLine/ChartsYReferenceLine.tsx b/packages/x-charts/src/ChartsReferenceLine/ChartsYReferenceLine.tsx index e66c086020c6c..376f1c3bfd996 100644 --- a/packages/x-charts/src/ChartsReferenceLine/ChartsYReferenceLine.tsx +++ b/packages/x-charts/src/ChartsReferenceLine/ChartsYReferenceLine.tsx @@ -96,7 +96,7 @@ function ChartsYReferenceLine(props: ChartsYReferenceLineProps) { if (!warnedOnce) { warnedOnce = true; console.error( - `MUI X: the value ${y} does not exist in the data of y axis with id ${axisId}.`, + `MUI X Charts: the value ${y} does not exist in the data of y axis with id ${axisId}.`, ); } } diff --git a/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx b/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx index 9b0109cfefb14..102bb42989ea0 100644 --- a/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx +++ b/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx @@ -109,11 +109,13 @@ ChartsAxisTooltipContent.propTypes = { axisData: PropTypes.shape({ x: PropTypes.shape({ index: PropTypes.number, - value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number]).isRequired, + value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number, PropTypes.string]) + .isRequired, }), y: PropTypes.shape({ index: PropTypes.number, - value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number]).isRequired, + value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number, PropTypes.string]) + .isRequired, }), }).isRequired, classes: PropTypes.object.isRequired, @@ -123,11 +125,13 @@ ChartsAxisTooltipContent.propTypes = { axisData: PropTypes.shape({ x: PropTypes.shape({ index: PropTypes.number, - value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number]).isRequired, + value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number, PropTypes.string]) + .isRequired, }), y: PropTypes.shape({ index: PropTypes.number, - value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number]).isRequired, + value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number, PropTypes.string]) + .isRequired, }), }), axisValue: PropTypes.any, diff --git a/packages/x-charts/src/ChartsTooltip/DefaultChartsAxisTooltipContent.tsx b/packages/x-charts/src/ChartsTooltip/DefaultChartsAxisTooltipContent.tsx index 27ca0f3158cc7..9a708be0ee29f 100644 --- a/packages/x-charts/src/ChartsTooltip/DefaultChartsAxisTooltipContent.tsx +++ b/packages/x-charts/src/ChartsTooltip/DefaultChartsAxisTooltipContent.tsx @@ -79,11 +79,13 @@ DefaultChartsAxisTooltipContent.propTypes = { axisData: PropTypes.shape({ x: PropTypes.shape({ index: PropTypes.number, - value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number]).isRequired, + value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number, PropTypes.string]) + .isRequired, }), y: PropTypes.shape({ index: PropTypes.number, - value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number]).isRequired, + value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number, PropTypes.string]) + .isRequired, }), }).isRequired, /** diff --git a/packages/x-charts/src/LineChart/AnimatedArea.tsx b/packages/x-charts/src/LineChart/AnimatedArea.tsx new file mode 100644 index 0000000000000..12d27c96c17cd --- /dev/null +++ b/packages/x-charts/src/LineChart/AnimatedArea.tsx @@ -0,0 +1,90 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { styled } from '@mui/material/styles'; +import { color as d3Color } from 'd3-color'; +import { animated, useSpring } from '@react-spring/web'; +import { useAnimatedPath } from '../internals/useAnimatedPath'; +import { DrawingContext } from '../context/DrawingProvider'; +import { cleanId } from '../internals/utils'; +import type { AreaElementOwnerState } from './AreaElement'; + +export const AreaElementPath = styled(animated.path, { + name: 'MuiAreaElement', + slot: 'Root', + overridesResolver: (_, styles) => styles.root, +})<{ ownerState: AreaElementOwnerState }>(({ ownerState }) => ({ + stroke: 'none', + fill: ownerState.isHighlighted + ? d3Color(ownerState.color)!.brighter(1).formatHex() + : d3Color(ownerState.color)!.brighter(0.5).formatHex(), + transition: 'opacity 0.2s ease-in, fill 0.2s ease-in', + opacity: ownerState.isFaded ? 0.3 : 1, +})); + +export interface AnimatedAreaProps extends React.ComponentPropsWithoutRef<'path'> { + ownerState: AreaElementOwnerState; + d: string; + /** + * If `true`, animations are skipped. + * @default false + */ + skipAnimation?: boolean; +} + +/** + * Demos: + * + * - [Lines](https://mui.com/x/react-charts/lines/) + * - [Areas demonstration](https://mui.com/x/react-charts/areas-demo/) + * + * API: + * + * - [AreaElement API](https://mui.com/x/api/charts/animated-area/) + */ +function AnimatedArea(props: AnimatedAreaProps) { + const { d, skipAnimation, ownerState, ...other } = props; + const { left, top, right, bottom, width, height, chartId } = React.useContext(DrawingContext); + + const path = useAnimatedPath(d!, skipAnimation); + + const { animatedWidth } = useSpring({ + from: { animatedWidth: left }, + to: { animatedWidth: width + left + right }, + reset: false, + immediate: skipAnimation, + }); + + const clipId = cleanId(`${chartId}-${ownerState.id}-area-clip`); + return ( + + + + + + + + + ); +} + +AnimatedArea.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + d: PropTypes.string.isRequired, + ownerState: PropTypes.shape({ + classes: PropTypes.object, + color: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + isFaded: PropTypes.bool.isRequired, + isHighlighted: PropTypes.bool.isRequired, + }).isRequired, + /** + * If `true`, animations are skipped. + * @default false + */ + skipAnimation: PropTypes.bool, +} as any; + +export { AnimatedArea }; diff --git a/packages/x-charts/src/LineChart/AnimatedLine.tsx b/packages/x-charts/src/LineChart/AnimatedLine.tsx new file mode 100644 index 0000000000000..593ff6a9d2fe8 --- /dev/null +++ b/packages/x-charts/src/LineChart/AnimatedLine.tsx @@ -0,0 +1,92 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { animated, useSpring } from '@react-spring/web'; +import { color as d3Color } from 'd3-color'; +import { styled } from '@mui/material/styles'; +import { useAnimatedPath } from '../internals/useAnimatedPath'; +import { DrawingContext } from '../context/DrawingProvider'; +import { cleanId } from '../internals/utils'; +import type { LineElementOwnerState } from './LineElement'; + +export const LineElementPath = styled(animated.path, { + name: 'MuiLineElement', + slot: 'Root', + overridesResolver: (_, styles) => styles.root, +})<{ ownerState: LineElementOwnerState }>(({ ownerState }) => ({ + strokeWidth: 2, + strokeLinejoin: 'round', + fill: 'none', + stroke: ownerState.isHighlighted + ? d3Color(ownerState.color)!.brighter(0.5).formatHex() + : ownerState.color, + transition: 'opacity 0.2s ease-in, stroke 0.2s ease-in', + opacity: ownerState.isFaded ? 0.3 : 1, +})); + +export interface AnimatedLineProps extends React.ComponentPropsWithoutRef<'path'> { + ownerState: LineElementOwnerState; + d: string; + /** + * If `true`, animations are skipped. + * @default false + */ + skipAnimation?: boolean; +} + +/** + * Demos: + * + * - [Lines](https://mui.com/x/react-charts/lines/) + * - [Line demonstration](https://mui.com/x/react-charts/line-demo/) + * + * API: + * + * - [AnimatedLine API](https://mui.com/x/api/charts/animated-line/) + */ +function AnimatedLine(props: AnimatedLineProps) { + const { d, skipAnimation, ownerState, ...other } = props; + const { left, top, bottom, width, height, right, chartId } = React.useContext(DrawingContext); + + const path = useAnimatedPath(d, skipAnimation); + + const { animatedWidth } = useSpring({ + from: { animatedWidth: left }, + to: { animatedWidth: width + left + right }, + reset: false, + immediate: skipAnimation, + }); + + const clipId = cleanId(`${chartId}-${ownerState.id}-line-clip`); + return ( + + + + + + + + + ); +} + +AnimatedLine.propTypes = { + // ----------------------------- Warning -------------------------------- + // | These PropTypes are generated from the TypeScript type definitions | + // | To update them edit the TypeScript types and run "yarn proptypes" | + // ---------------------------------------------------------------------- + d: PropTypes.string.isRequired, + ownerState: PropTypes.shape({ + classes: PropTypes.object, + color: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + isFaded: PropTypes.bool.isRequired, + isHighlighted: PropTypes.bool.isRequired, + }).isRequired, + /** + * If `true`, animations are skipped. + * @default false + */ + skipAnimation: PropTypes.bool, +} as any; + +export { AnimatedLine }; diff --git a/packages/x-charts/src/LineChart/AreaElement.tsx b/packages/x-charts/src/LineChart/AreaElement.tsx index d9610d9ec7848..d8819b94cefe4 100644 --- a/packages/x-charts/src/LineChart/AreaElement.tsx +++ b/packages/x-charts/src/LineChart/AreaElement.tsx @@ -1,11 +1,9 @@ import * as React from 'react'; import PropTypes from 'prop-types'; import composeClasses from '@mui/utils/composeClasses'; -import { useSlotProps, SlotComponentProps } from '@mui/base/utils'; +import { useSlotProps } from '@mui/base/utils'; import generateUtilityClass from '@mui/utils/generateUtilityClass'; -import { styled } from '@mui/material/styles'; import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; -import { color as d3Color } from 'd3-color'; import { getIsFaded, getIsHighlighted, @@ -13,6 +11,7 @@ import { } from '../hooks/useInteractionItemProps'; import { InteractionContext } from '../context/InteractionProvider'; import { HighlightScope } from '../context/HighlightProvider'; +import { AnimatedArea, AnimatedAreaProps } from './AnimatedArea'; export interface AreaElementClasses { /** Styles applied to the root element. */ @@ -25,7 +24,7 @@ export interface AreaElementClasses { export type AreaElementClassKey = keyof AreaElementClasses; -interface AreaElementOwnerState { +export interface AreaElementOwnerState { id: string; color: string; isFaded: boolean; @@ -52,61 +51,34 @@ const useUtilityClasses = (ownerState: AreaElementOwnerState) => { return composeClasses(slots, getAreaElementUtilityClass, classes); }; -export const AreaElementPath = styled('path', { - name: 'MuiAreaElement', - slot: 'Root', - overridesResolver: (_, styles) => styles.root, -})<{ ownerState: AreaElementOwnerState }>(({ ownerState }) => ({ - stroke: 'none', - fill: ownerState.isHighlighted - ? d3Color(ownerState.color)!.brighter(1).formatHex() - : d3Color(ownerState.color)!.brighter(0.5).formatHex(), - transition: 'opacity 0.2s ease-in, fill 0.2s ease-in', - opacity: ownerState.isFaded ? 0.3 : 1, -})); - -AreaElementPath.propTypes = { - // ----------------------------- Warning -------------------------------- - // | These PropTypes are generated from the TypeScript type definitions | - // | To update them edit the TypeScript types and run "yarn proptypes" | - // ---------------------------------------------------------------------- - as: PropTypes.elementType, - ownerState: PropTypes.shape({ - classes: PropTypes.object, - color: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, - isFaded: PropTypes.bool.isRequired, - isHighlighted: PropTypes.bool.isRequired, - }).isRequired, - sx: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), - PropTypes.func, - PropTypes.object, - ]), -} as any; +export interface AreaElementSlots { + /** + * The component that renders the area. + * @default AnimatedArea + */ + area?: React.JSXElementConstructor; +} -export type AreaElementProps = Omit & - React.ComponentPropsWithoutRef<'path'> & { - highlightScope?: Partial; - /** - * The props used for each component slot. - * @default {} - */ - slotProps?: { - area?: SlotComponentProps<'path', {}, AreaElementOwnerState>; - }; - /** - * Overridable component slots. - * @default {} - */ - slots?: { - /** - * The component that renders the root. - * @default AreaElementPath - */ - area?: React.ElementType; - }; - }; +export interface AreaElementSlotProps { + area?: AnimatedAreaProps; +} + +export interface AreaElementProps + extends Omit, + Pick { + d: string; + highlightScope?: Partial; + /** + * The props used for each component slot. + * @default {} + */ + slotProps?: AreaElementSlotProps; + /** + * Overridable component slots. + * @default {} + */ + slots?: AreaElementSlots; +} /** * Demos: @@ -120,10 +92,10 @@ export type AreaElementProps = Omit; } @@ -157,10 +130,18 @@ AreaElement.propTypes = { // | To update them edit the TypeScript types and run "yarn proptypes" | // ---------------------------------------------------------------------- classes: PropTypes.object, + color: PropTypes.string.isRequired, + d: PropTypes.string.isRequired, highlightScope: PropTypes.shape({ faded: PropTypes.oneOf(['global', 'none', 'series']), highlighted: PropTypes.oneOf(['item', 'none', 'series']), }), + id: PropTypes.string.isRequired, + /** + * If `true`, animations are skipped. + * @default false + */ + skipAnimation: PropTypes.bool, /** * The props used for each component slot. * @default {} diff --git a/packages/x-charts/src/LineChart/AreaPlot.tsx b/packages/x-charts/src/LineChart/AreaPlot.tsx index 786b7a441e5c6..8d4446ba7ff7a 100644 --- a/packages/x-charts/src/LineChart/AreaPlot.tsx +++ b/packages/x-charts/src/LineChart/AreaPlot.tsx @@ -3,22 +3,90 @@ import PropTypes from 'prop-types'; import { area as d3Area } from 'd3-shape'; import { SeriesContext } from '../context/SeriesContextProvider'; import { CartesianContext } from '../context/CartesianContextProvider'; -import { AreaElement, AreaElementProps } from './AreaElement'; +import { + AreaElement, + AreaElementProps, + AreaElementSlotProps, + AreaElementSlots, +} from './AreaElement'; import { getValueToPositionMapper } from '../hooks/useScale'; import getCurveFactory from '../internals/getCurve'; import { DEFAULT_X_AXIS_KEY } from '../constants'; -export interface AreaPlotSlots { - area?: React.JSXElementConstructor; -} +export interface AreaPlotSlots extends AreaElementSlots {} -export interface AreaPlotSlotProps { - area?: Partial; -} +export interface AreaPlotSlotProps extends AreaElementSlotProps {} export interface AreaPlotProps extends React.SVGAttributes, - Pick {} + Pick {} + +const useAggregatedData = () => { + const seriesData = React.useContext(SeriesContext).line; + const axisData = React.useContext(CartesianContext); + + if (seriesData === undefined) { + return []; + } + + const { series, stackingGroups } = seriesData; + const { xAxis, yAxis, xAxisIds, yAxisIds } = axisData; + const defaultXAxisId = xAxisIds[0]; + const defaultYAxisId = yAxisIds[0]; + + return stackingGroups.flatMap(({ ids: groupIds }) => { + return groupIds.flatMap((seriesId) => { + const { + xAxisKey = defaultXAxisId, + yAxisKey = defaultYAxisId, + stackedData, + data, + connectNulls, + } = series[seriesId]; + + const xScale = getValueToPositionMapper(xAxis[xAxisKey].scale); + const yScale = yAxis[yAxisKey].scale; + const xData = xAxis[xAxisKey].data; + + if (process.env.NODE_ENV !== 'production') { + if (xData === undefined) { + throw new Error( + `MUI X Charts: ${ + xAxisKey === DEFAULT_X_AXIS_KEY + ? 'The first `xAxis`' + : `The x-axis with id "${xAxisKey}"` + } should have data property to be able to display a line plot.`, + ); + } + if (xData.length < stackedData.length) { + throw new Error( + `MUI X Charts: The data length of the x axis (${xData.length} items) is lower than the length of series (${stackedData.length} items).`, + ); + } + } + + const areaPath = d3Area<{ + x: any; + y: [number, number]; + }>() + .x((d) => xScale(d.x)) + .defined((_, i) => connectNulls || data[i] != null) + .y0((d) => d.y && yScale(d.y[0])!) + .y1((d) => d.y && yScale(d.y[1])!); + + const curve = getCurveFactory(series[seriesId].curve); + const formattedData = xData?.map((x, index) => ({ x, y: stackedData[index] })) ?? []; + const d3Data = connectNulls ? formattedData.filter((_, i) => data[i] != null) : formattedData; + + const d = areaPath.curve(curve)(d3Data) || ''; + return { + ...series[seriesId], + d, + seriesId, + }; + }); + }); +}; /** * Demos: @@ -32,82 +100,29 @@ export interface AreaPlotProps * - [AreaPlot API](https://mui.com/x/api/charts/area-plot/) */ function AreaPlot(props: AreaPlotProps) { - const { slots, slotProps, ...other } = props; + const { slots, slotProps, skipAnimation, ...other } = props; - const seriesData = React.useContext(SeriesContext).line; - const axisData = React.useContext(CartesianContext); - - if (seriesData === undefined) { - return null; - } - const { series, stackingGroups } = seriesData; - const { xAxis, yAxis, xAxisIds, yAxisIds } = axisData; - const defaultXAxisId = xAxisIds[0]; - const defaultYAxisId = yAxisIds[0]; + const completedData = useAggregatedData(); return ( - {stackingGroups.flatMap(({ ids: groupIds }) => { - return groupIds.flatMap((seriesId) => { - const { - xAxisKey = defaultXAxisId, - yAxisKey = defaultYAxisId, - stackedData, - data, - connectNulls, - } = series[seriesId]; - - const xScale = getValueToPositionMapper(xAxis[xAxisKey].scale); - const yScale = yAxis[yAxisKey].scale; - const xData = xAxis[xAxisKey].data; - - if (process.env.NODE_ENV !== 'production') { - if (xData === undefined) { - throw new Error( - `MUI X Charts: ${ - xAxisKey === DEFAULT_X_AXIS_KEY - ? 'The first `xAxis`' - : `The x-axis with id "${xAxisKey}"` - } should have data property to be able to display a line plot.`, - ); - } - if (xData.length < stackedData.length) { - throw new Error( - `MUI X Charts: The data length of the x axis (${xData.length} items) is lower than the length of series (${stackedData.length} items).`, - ); - } - } - - const areaPath = d3Area<{ - x: any; - y: [number, number]; - }>() - .x((d) => xScale(d.x)) - .defined((_, i) => connectNulls || data[i] != null) - .y0((d) => d.y && yScale(d.y[0])!) - .y1((d) => d.y && yScale(d.y[1])!); - - const curve = getCurveFactory(series[seriesId].curve); - const formattedData = xData?.map((x, index) => ({ x, y: stackedData[index] })) ?? []; - const d3Data = connectNulls - ? formattedData.filter((_, i) => data[i] != null) - : formattedData; - - return ( - !!series[seriesId].area && ( + {completedData + .reverse() + .map( + ({ d, seriesId, color, highlightScope, area }) => + !!area && ( - ) - ); - }); - })} + ), + )} ); } @@ -117,6 +132,11 @@ AreaPlot.propTypes = { // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "yarn proptypes" | // ---------------------------------------------------------------------- + /** + * If `true`, animations are skipped. + * @default false + */ + skipAnimation: PropTypes.bool, /** * The props used for each component slot. * @default {} diff --git a/packages/x-charts/src/LineChart/LineChart.tsx b/packages/x-charts/src/LineChart/LineChart.tsx index 8fcc82e7cf92e..2e0d71190d8ee 100644 --- a/packages/x-charts/src/LineChart/LineChart.tsx +++ b/packages/x-charts/src/LineChart/LineChart.tsx @@ -82,6 +82,11 @@ export interface LineChartProps * @default {} */ slotProps?: LineChartSlotProps; + /** + * If `true`, animations are skipped. + * @default false + */ + skipAnimation?: boolean; } /** @@ -116,6 +121,7 @@ const LineChart = React.forwardRef(function LineChart(props: LineChartProps, ref children, slots, slotProps, + skipAnimation, } = props; const id = useId(); @@ -153,8 +159,8 @@ const LineChart = React.forwardRef(function LineChart(props: LineChartProps, ref } > - - + + - + @@ -348,6 +354,11 @@ LineChart.propTypes = { PropTypes.string, ]), series: PropTypes.arrayOf(PropTypes.object).isRequired, + /** + * If `true`, animations are skipped. + * @default false + */ + skipAnimation: PropTypes.bool, /** * The props used for each component slot. * @default {} diff --git a/packages/x-charts/src/LineChart/LineElement.tsx b/packages/x-charts/src/LineChart/LineElement.tsx index 14d13e28598fb..a03e2486c81c6 100644 --- a/packages/x-charts/src/LineChart/LineElement.tsx +++ b/packages/x-charts/src/LineChart/LineElement.tsx @@ -1,10 +1,8 @@ import * as React from 'react'; import PropTypes from 'prop-types'; -import { color as d3Color } from 'd3-color'; import composeClasses from '@mui/utils/composeClasses'; -import { useSlotProps, SlotComponentProps } from '@mui/base/utils'; +import { useSlotProps } from '@mui/base/utils'; import generateUtilityClass from '@mui/utils/generateUtilityClass'; -import { styled } from '@mui/material/styles'; import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; import { InteractionContext } from '../context/InteractionProvider'; import { @@ -13,6 +11,7 @@ import { useInteractionItemProps, } from '../hooks/useInteractionItemProps'; import { HighlightScope } from '../context/HighlightProvider'; +import { AnimatedLine, AnimatedLineProps } from './AnimatedLine'; export interface LineElementClasses { /** Styles applied to the root element. */ @@ -25,7 +24,7 @@ export interface LineElementClasses { export type LineElementClassKey = keyof LineElementClasses; -interface LineElementOwnerState { +export interface LineElementOwnerState { id: string; color: string; isFaded: boolean; @@ -52,63 +51,34 @@ const useUtilityClasses = (ownerState: LineElementOwnerState) => { return composeClasses(slots, getLineElementUtilityClass, classes); }; -export const LineElementPath = styled('path', { - name: 'MuiLineElement', - slot: 'Root', - overridesResolver: (_, styles) => styles.root, -})<{ ownerState: LineElementOwnerState }>(({ ownerState }) => ({ - strokeWidth: 2, - strokeLinejoin: 'round', - fill: 'none', - stroke: ownerState.isHighlighted - ? d3Color(ownerState.color)!.brighter(0.5).formatHex() - : ownerState.color, - transition: 'opacity 0.2s ease-in, stroke 0.2s ease-in', - opacity: ownerState.isFaded ? 0.3 : 1, -})); - -LineElementPath.propTypes = { - // ----------------------------- Warning -------------------------------- - // | These PropTypes are generated from the TypeScript type definitions | - // | To update them edit the TypeScript types and run "yarn proptypes" | - // ---------------------------------------------------------------------- - as: PropTypes.elementType, - ownerState: PropTypes.shape({ - classes: PropTypes.object, - color: PropTypes.string.isRequired, - id: PropTypes.string.isRequired, - isFaded: PropTypes.bool.isRequired, - isHighlighted: PropTypes.bool.isRequired, - }).isRequired, - sx: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), - PropTypes.func, - PropTypes.object, - ]), -} as any; +export interface LineElementSlots { + /** + * The component that renders the line. + * @default LineElementPath + */ + line?: React.JSXElementConstructor; +} -export type LineElementProps = Omit & - React.ComponentPropsWithoutRef<'path'> & { - highlightScope?: Partial; - /** - * The props used for each component slot. - * @default {} - */ - slotProps?: { - line?: SlotComponentProps<'path', {}, LineElementOwnerState>; - }; - /** - * Overridable component slots. - * @default {} - */ - slots?: { - /** - * The component that renders the root. - * @default LineElementPath - */ - line?: React.ElementType; - }; - }; +export interface LineElementSlotProps { + line?: AnimatedLineProps; +} + +export interface LineElementProps + extends Omit, + Pick { + d: string; + highlightScope?: Partial; + /** + * The props used for each component slot. + * @default {} + */ + slotProps?: LineElementSlotProps; + /** + * Overridable component slots. + * @default {} + */ + slots?: LineElementSlots; +} /** * Demos: @@ -122,7 +92,6 @@ export type LineElementProps = Omit; } @@ -160,10 +130,18 @@ LineElement.propTypes = { // | To update them edit the TypeScript types and run "yarn proptypes" | // ---------------------------------------------------------------------- classes: PropTypes.object, + color: PropTypes.string.isRequired, + d: PropTypes.string.isRequired, highlightScope: PropTypes.shape({ faded: PropTypes.oneOf(['global', 'none', 'series']), highlighted: PropTypes.oneOf(['item', 'none', 'series']), }), + id: PropTypes.string.isRequired, + /** + * If `true`, animations are skipped. + * @default false + */ + skipAnimation: PropTypes.bool, /** * The props used for each component slot. * @default {} diff --git a/packages/x-charts/src/LineChart/LinePlot.tsx b/packages/x-charts/src/LineChart/LinePlot.tsx index e97b47097c6c0..1d180d423a323 100644 --- a/packages/x-charts/src/LineChart/LinePlot.tsx +++ b/packages/x-charts/src/LineChart/LinePlot.tsx @@ -3,22 +3,88 @@ import PropTypes from 'prop-types'; import { line as d3Line } from 'd3-shape'; import { SeriesContext } from '../context/SeriesContextProvider'; import { CartesianContext } from '../context/CartesianContextProvider'; -import { LineElement, LineElementProps } from './LineElement'; +import { + LineElement, + LineElementProps, + LineElementSlotProps, + LineElementSlots, +} from './LineElement'; import { getValueToPositionMapper } from '../hooks/useScale'; import getCurveFactory from '../internals/getCurve'; import { DEFAULT_X_AXIS_KEY } from '../constants'; -export interface LinePlotSlots { - line?: React.JSXElementConstructor; -} +export interface LinePlotSlots extends LineElementSlots {} -export interface LinePlotSlotProps { - line?: Partial; -} +export interface LinePlotSlotProps extends LineElementSlotProps {} export interface LinePlotProps extends React.SVGAttributes, - Pick {} + Pick {} + +const useAggregatedData = () => { + const seriesData = React.useContext(SeriesContext).line; + const axisData = React.useContext(CartesianContext); + + if (seriesData === undefined) { + return []; + } + + const { series, stackingGroups } = seriesData; + const { xAxis, yAxis, xAxisIds, yAxisIds } = axisData; + const defaultXAxisId = xAxisIds[0]; + const defaultYAxisId = yAxisIds[0]; + + return stackingGroups.flatMap(({ ids: groupIds }) => { + return groupIds.flatMap((seriesId) => { + const { + xAxisKey = defaultXAxisId, + yAxisKey = defaultYAxisId, + stackedData, + data, + connectNulls, + } = series[seriesId]; + + const xScale = getValueToPositionMapper(xAxis[xAxisKey].scale); + const yScale = yAxis[yAxisKey].scale; + const xData = xAxis[xAxisKey].data; + + if (process.env.NODE_ENV !== 'production') { + if (xData === undefined) { + throw new Error( + `MUI X Charts: ${ + xAxisKey === DEFAULT_X_AXIS_KEY + ? 'The first `xAxis`' + : `The x-axis with id "${xAxisKey}"` + } should have data property to be able to display a line plot.`, + ); + } + if (xData.length < stackedData.length) { + throw new Error( + `MUI X Charts: The data length of the x axis (${xData.length} items) is lower than the length of series (${stackedData.length} items).`, + ); + } + } + + const linePath = d3Line<{ + x: any; + y: [number, number]; + }>() + .x((d) => xScale(d.x)) + .defined((_, i) => connectNulls || data[i] != null) + .y((d) => yScale(d.y[1])!); + + const formattedData = xData?.map((x, index) => ({ x, y: stackedData[index] })) ?? []; + const d3Data = connectNulls ? formattedData.filter((_, i) => data[i] != null) : formattedData; + + const d = linePath.curve(getCurveFactory(series[seriesId].curve))(d3Data) || ''; + return { + ...series[seriesId], + d, + seriesId, + }; + }); + }); +}; /** * Demos: @@ -31,77 +97,25 @@ export interface LinePlotProps * - [LinePlot API](https://mui.com/x/api/charts/line-plot/) */ function LinePlot(props: LinePlotProps) { - const { slots, slotProps, ...other } = props; - const seriesData = React.useContext(SeriesContext).line; - const axisData = React.useContext(CartesianContext); + const { slots, slotProps, skipAnimation, ...other } = props; - if (seriesData === undefined) { - return null; - } - const { series, stackingGroups } = seriesData; - const { xAxis, yAxis, xAxisIds, yAxisIds } = axisData; - const defaultXAxisId = xAxisIds[0]; - const defaultYAxisId = yAxisIds[0]; + const completedData = useAggregatedData(); return ( - {stackingGroups.flatMap(({ ids: groupIds }) => { - return groupIds.flatMap((seriesId) => { - const { - xAxisKey = defaultXAxisId, - yAxisKey = defaultYAxisId, - stackedData, - data, - connectNulls, - } = series[seriesId]; - - const xScale = getValueToPositionMapper(xAxis[xAxisKey].scale); - const yScale = yAxis[yAxisKey].scale; - const xData = xAxis[xAxisKey].data; - - if (process.env.NODE_ENV !== 'production') { - if (xData === undefined) { - throw new Error( - `MUI X Charts: ${ - xAxisKey === DEFAULT_X_AXIS_KEY - ? 'The first `xAxis`' - : `The x-axis with id "${xAxisKey}"` - } should have data property to be able to display a line plot.`, - ); - } - if (xData.length < stackedData.length) { - throw new Error( - `MUI X Charts: The data length of the x axis (${xData.length} items) is lower than the length of series (${stackedData.length} items).`, - ); - } - } - - const linePath = d3Line<{ - x: any; - y: [number, number]; - }>() - .x((d) => xScale(d.x)) - .defined((_, i) => connectNulls || data[i] != null) - .y((d) => yScale(d.y[1])!); - - const curve = getCurveFactory(series[seriesId].curve); - const formattedData = xData?.map((x, index) => ({ x, y: stackedData[index] })) ?? []; - const d3Data = connectNulls - ? formattedData.filter((_, i) => data[i] != null) - : formattedData; - - return ( - - ); - }); + {completedData.map(({ d, seriesId, color, highlightScope }) => { + return ( + + ); })} ); @@ -112,6 +126,11 @@ LinePlot.propTypes = { // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "yarn proptypes" | // ---------------------------------------------------------------------- + /** + * If `true`, animations are skipped. + * @default false + */ + skipAnimation: PropTypes.bool, /** * The props used for each component slot. * @default {} diff --git a/packages/x-charts/src/LineChart/MarkElement.tsx b/packages/x-charts/src/LineChart/MarkElement.tsx index 83af7d050517b..de78d5b1e8d8e 100644 --- a/packages/x-charts/src/LineChart/MarkElement.tsx +++ b/packages/x-charts/src/LineChart/MarkElement.tsx @@ -5,6 +5,7 @@ import generateUtilityClass from '@mui/utils/generateUtilityClass'; import { styled } from '@mui/material/styles'; import generateUtilityClasses from '@mui/utils/generateUtilityClasses'; import { symbol as d3Symbol, symbolsFill as d3SymbolsFill } from 'd3-shape'; +import { animated, to, useSpring } from '@react-spring/web'; import { getSymbol } from '../internals/utils'; import { InteractionContext } from '../context/InteractionProvider'; import { HighlightScope } from '../context/HighlightProvider'; @@ -30,8 +31,6 @@ interface MarkElementOwnerState { color: string; isFaded: boolean; isHighlighted: boolean; - x: number; - y: number; classes?: Partial; } @@ -54,13 +53,11 @@ const useUtilityClasses = (ownerState: MarkElementOwnerState) => { return composeClasses(slots, getMarkElementUtilityClass, classes); }; -const MarkElementPath = styled('path', { +const MarkElementPath = styled(animated.path, { name: 'MuiMarkElement', slot: 'Root', overridesResolver: (_, styles) => styles.root, })<{ ownerState: MarkElementOwnerState }>(({ ownerState, theme }) => ({ - transform: `translate(${ownerState.x}px, ${ownerState.y}px)`, - transformOrigin: `${ownerState.x}px ${ownerState.y}px`, fill: (theme.vars || theme).palette.background.paper, stroke: ownerState.color, strokeWidth: 2, @@ -78,8 +75,6 @@ MarkElementPath.propTypes = { id: PropTypes.string.isRequired, isFaded: PropTypes.bool.isRequired, isHighlighted: PropTypes.bool.isRequired, - x: PropTypes.number.isRequired, - y: PropTypes.number.isRequired, }).isRequired, sx: PropTypes.oneOfType([ PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), @@ -90,6 +85,11 @@ MarkElementPath.propTypes = { export type MarkElementProps = Omit & React.ComponentPropsWithoutRef<'path'> & { + /** + * If `true`, animations are skipped. + * @default false + */ + skipAnimation?: boolean; /** * The shape of the marker. */ @@ -121,6 +121,7 @@ function MarkElement(props: MarkElementProps) { shape, dataIndex, highlightScope, + skipAnimation, ...other } = props; @@ -134,20 +135,23 @@ function MarkElement(props: MarkElementProps) { const isFaded = !isHighlighted && getIsFaded(item, { type: 'line', seriesId: id }, highlightScope); + const position = useSpring({ x, y, immediate: skipAnimation }); const ownerState = { id, classes: innerClasses, isHighlighted, isFaded, color, - x, - y, }; const classes = useUtilityClasses(ownerState); return ( `translate(${pX}px, ${pY}px)`), + transformOrigin: to([position.x, position.y], (pX, pY) => `${pX}px ${pY}px`), + }} ownerState={ownerState} className={classes.root} d={d3Symbol(d3SymbolsFill[getSymbol(shape)])()!} @@ -175,6 +179,11 @@ MarkElement.propTypes = { */ shape: PropTypes.oneOf(['circle', 'cross', 'diamond', 'square', 'star', 'triangle', 'wye']) .isRequired, + /** + * If `true`, animations are skipped. + * @default false + */ + skipAnimation: PropTypes.bool, } as any; export { MarkElement }; diff --git a/packages/x-charts/src/LineChart/MarkPlot.tsx b/packages/x-charts/src/LineChart/MarkPlot.tsx index c0916c449fd8e..69baf3061cbdf 100644 --- a/packages/x-charts/src/LineChart/MarkPlot.tsx +++ b/packages/x-charts/src/LineChart/MarkPlot.tsx @@ -5,6 +5,8 @@ import { CartesianContext } from '../context/CartesianContextProvider'; import { MarkElement, MarkElementProps } from './MarkElement'; import { getValueToPositionMapper } from '../hooks/useScale'; import { DEFAULT_X_AXIS_KEY } from '../constants'; +import { DrawingContext } from '../context/DrawingProvider'; +import { cleanId } from '../internals/utils'; export interface MarkPlotSlots { mark?: React.JSXElementConstructor; @@ -14,7 +16,9 @@ export interface MarkPlotSlotProps { mark?: Partial; } -export interface MarkPlotProps extends React.SVGAttributes { +export interface MarkPlotProps + extends React.SVGAttributes, + Pick { /** * Overridable component slots. * @default {} @@ -38,10 +42,11 @@ export interface MarkPlotProps extends React.SVGAttributes { * - [MarkPlot API](https://mui.com/x/api/charts/mark-plot/) */ function MarkPlot(props: MarkPlotProps) { - const { slots, slotProps, ...other } = props; + const { slots, slotProps, skipAnimation, ...other } = props; const seriesData = React.useContext(SeriesContext).line; const axisData = React.useContext(CartesianContext); + const { chartId } = React.useContext(DrawingContext); const Mark = slots?.mark ?? MarkElement; @@ -56,7 +61,7 @@ function MarkPlot(props: MarkPlotProps) { return ( {stackingGroups.flatMap(({ ids: groupIds }) => { - return groupIds.flatMap((seriesId) => { + return groupIds.map((seriesId) => { const { xAxisKey = defaultXAxisId, yAxisKey = defaultYAxisId, @@ -96,52 +101,59 @@ function MarkPlot(props: MarkPlotProps) { ); } - return xData - ?.map((x, index) => { - const value = data[index] == null ? null : stackedData[index][1]; - return { - x: xScale(x), - y: value === null ? null : yScale(value)!, - position: x, - value, - index, - }; - }) - .filter(({ x, y, index, position, value }) => { - if (value === null || y === null) { - // Remove missing data point - return false; - } - if (!isInRange({ x, y })) { - // Remove out of range - return false; - } - if (showMark === true) { - return true; - } - return showMark({ - x, - y, - index, - position, - value, - }); - }) - .map(({ x, y, index }) => { - return ( - - ); - }); + const clipId = cleanId(`${chartId}-${seriesId}-line-clip`); // We assume that if displaying line mark, the line will also be rendered + + return ( + + {xData + ?.map((x, index) => { + const value = data[index] == null ? null : stackedData[index][1]; + return { + x: xScale(x), + y: value === null ? null : yScale(value)!, + position: x, + value, + index, + }; + }) + .filter(({ x, y, index, position, value }) => { + if (value === null || y === null) { + // Remove missing data point + return false; + } + if (!isInRange({ x, y })) { + // Remove out of range + return false; + } + if (showMark === true) { + return true; + } + return showMark({ + x, + y, + index, + position, + value, + }); + }) + .map(({ x, y, index }) => { + return ( + + ); + })} + + ); }); })} @@ -153,6 +165,11 @@ MarkPlot.propTypes = { // | These PropTypes are generated from the TypeScript type definitions | // | To update them edit the TypeScript types and run "yarn proptypes" | // ---------------------------------------------------------------------- + /** + * If `true`, animations are skipped. + * @default false + */ + skipAnimation: PropTypes.bool, /** * The props used for each component slot. * @default {} diff --git a/packages/x-charts/src/LineChart/index.tsx b/packages/x-charts/src/LineChart/index.tsx index 742fba2f4d38f..6ac8144590359 100644 --- a/packages/x-charts/src/LineChart/index.tsx +++ b/packages/x-charts/src/LineChart/index.tsx @@ -6,6 +6,8 @@ export * from './MarkPlot'; export * from './LineHighlightPlot'; export * from './AreaElement'; +export * from './AnimatedArea'; export * from './LineElement'; +export * from './AnimatedLine'; export * from './MarkElement'; export * from './LineHighlightElement'; diff --git a/packages/x-charts/src/PieChart/PieArcLabelPlot.tsx b/packages/x-charts/src/PieChart/PieArcLabelPlot.tsx index 6ce3782a109f1..831369e981057 100644 --- a/packages/x-charts/src/PieChart/PieArcLabelPlot.tsx +++ b/packages/x-charts/src/PieChart/PieArcLabelPlot.tsx @@ -75,7 +75,7 @@ export interface PieArcLabelPlotProps */ slotProps?: PieArcLabelPlotSlotProps; /** - * If `true`, animations are skiped. + * If `true`, animations are skipped. * @default false */ skipAnimation?: boolean; @@ -249,7 +249,7 @@ PieArcLabelPlot.propTypes = { */ paddingAngle: PropTypes.number, /** - * If `true`, animations are skiped. + * If `true`, animations are skipped. * @default false */ skipAnimation: PropTypes.bool, diff --git a/packages/x-charts/src/PieChart/PieArcPlot.tsx b/packages/x-charts/src/PieChart/PieArcPlot.tsx index ae3d9f200b036..1db0508cc4175 100644 --- a/packages/x-charts/src/PieChart/PieArcPlot.tsx +++ b/packages/x-charts/src/PieChart/PieArcPlot.tsx @@ -56,7 +56,7 @@ export interface PieArcPlotProps item: DefaultizedPieValueType, ) => void; /** - * If `true`, animations are skiped. + * If `true`, animations are skipped. * @default false */ skipAnimation?: boolean; @@ -230,7 +230,7 @@ PieArcPlot.propTypes = { */ paddingAngle: PropTypes.number, /** - * If `true`, animations are skiped. + * If `true`, animations are skipped. * @default false */ skipAnimation: PropTypes.bool, diff --git a/packages/x-charts/src/PieChart/PieChart.tsx b/packages/x-charts/src/PieChart/PieChart.tsx index 95a7d6bd04a3b..cb0cd54077d67 100644 --- a/packages/x-charts/src/PieChart/PieChart.tsx +++ b/packages/x-charts/src/PieChart/PieChart.tsx @@ -321,7 +321,7 @@ PieChart.propTypes = { ]), series: PropTypes.arrayOf(PropTypes.object).isRequired, /** - * If `true`, animations are skiped. + * If `true`, animations are skipped. * @default false */ skipAnimation: PropTypes.bool, diff --git a/packages/x-charts/src/PieChart/PiePlot.tsx b/packages/x-charts/src/PieChart/PiePlot.tsx index 35733978c672c..198ef8accc157 100644 --- a/packages/x-charts/src/PieChart/PiePlot.tsx +++ b/packages/x-charts/src/PieChart/PiePlot.tsx @@ -156,7 +156,7 @@ PiePlot.propTypes = { */ onClick: PropTypes.func, /** - * If `true`, animations are skiped. + * If `true`, animations are skipped. * @default false */ skipAnimation: PropTypes.bool, diff --git a/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx b/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx index 1ecb4634511a6..8214bec2e2fe0 100644 --- a/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx +++ b/packages/x-charts/src/SparkLineChart/SparkLineChart.tsx @@ -197,8 +197,8 @@ const SparkLineChart = React.forwardRef(function SparkLineChart(props: SparkLine {plotType === 'line' && ( - - + + )} diff --git a/packages/x-charts/src/context/DrawingProvider.tsx b/packages/x-charts/src/context/DrawingProvider.tsx index 9311c045e2e2f..4429fabffba66 100644 --- a/packages/x-charts/src/context/DrawingProvider.tsx +++ b/packages/x-charts/src/context/DrawingProvider.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import PropTypes from 'prop-types'; +import useId from '@mui/utils/useId'; import useChartDimensions from '../hooks/useChartDimensions'; import { LayoutConfig } from '../models/layout'; @@ -38,13 +39,21 @@ export type DrawingArea = { height: number; }; -export const DrawingContext = React.createContext({ +export const DrawingContext = React.createContext< + DrawingArea & { + /** + * A random id used to distinguish each chart on the same page. + */ + chartId: string; + } +>({ top: 0, left: 0, bottom: 0, right: 0, height: 300, width: 400, + chartId: '', }); export const SVGContext = React.createContext>({ current: null }); @@ -56,10 +65,16 @@ export const SVGContext = React.createContext>({ function DrawingProvider(props: DrawingProviderProps) { const { width, height, margin, svgRef, children } = props; const drawingArea = useChartDimensions(width, height, margin); + const chartId = useId(); + + const value = React.useMemo( + () => ({ chartId: chartId ?? '', ...drawingArea }), + [chartId, drawingArea], + ); return ( - {children} + {children} ); } diff --git a/packages/x-charts/src/context/InteractionProvider.tsx b/packages/x-charts/src/context/InteractionProvider.tsx index bf82c6323ca5b..a8c3f75d37fe9 100644 --- a/packages/x-charts/src/context/InteractionProvider.tsx +++ b/packages/x-charts/src/context/InteractionProvider.tsx @@ -9,11 +9,11 @@ export type ItemInteractionData = ChartItemIdentifier export type AxisInteractionData = { x: null | { - value: number | Date; + value: number | Date | string; index?: number; }; y: null | { - value: number | Date; + value: number | Date | string; index?: number; }; }; diff --git a/packages/x-charts/src/internals/geometry.ts b/packages/x-charts/src/internals/geometry.ts index 43fd024cacc39..593ba18e20a0b 100644 --- a/packages/x-charts/src/internals/geometry.ts +++ b/packages/x-charts/src/internals/geometry.ts @@ -16,7 +16,7 @@ export function getMinXTranslation(width: number, height: number, angle: number warnedOnce = true; console.warn( [ - `MUI X: It seems you applied an angle larger than 90° or smaller than -90° to an axis text.`, + `MUI X Charts: It seems you applied an angle larger than 90° or smaller than -90° to an axis text.`, `This could cause some text overlapping.`, `If you encounter a use case where it's needed, please open an issue.`, ].join('\n'), diff --git a/packages/x-charts/src/internals/useAnimatedPath.ts b/packages/x-charts/src/internals/useAnimatedPath.ts new file mode 100644 index 0000000000000..034302c08f7bf --- /dev/null +++ b/packages/x-charts/src/internals/useAnimatedPath.ts @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { interpolateString } from 'd3-interpolate'; +import { useSpring, to } from '@react-spring/web'; + +function usePrevious(value: T) { + const ref = React.useRef(null); + React.useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; +} + +// Taken from Nivo +export const useAnimatedPath = (path: string, skipAnimation?: boolean) => { + const previousPath = usePrevious(path); + const interpolator = React.useMemo( + () => (previousPath ? interpolateString(previousPath, path) : () => path), + [previousPath, path], + ); + + const { value } = useSpring({ + from: { value: 0 }, + to: { value: 1 }, + reset: true, + immediate: skipAnimation, + }); + + return to([value], interpolator); +}; diff --git a/packages/x-charts/src/internals/utils.ts b/packages/x-charts/src/internals/utils.ts index 81a79b389ab4f..8dfdd4ea9d826 100644 --- a/packages/x-charts/src/internals/utils.ts +++ b/packages/x-charts/src/internals/utils.ts @@ -51,3 +51,10 @@ export function getPercentageValue(value: number | string, refValue: number) { `MUI-Charts: Received an unknown value "${value}". It should be a number, or a string with a percentage value.`, ); } + +/** + * Remove spaces to have viable ids + */ +export function cleanId(id: string) { + return id.replace(' ', '_'); +} diff --git a/scripts/x-charts.exports.json b/scripts/x-charts.exports.json index 46daf646da99a..49a150ddaa4b9 100644 --- a/scripts/x-charts.exports.json +++ b/scripts/x-charts.exports.json @@ -3,12 +3,19 @@ { "name": "AnchorPosition", "kind": "TypeAlias" }, { "name": "AnchorX", "kind": "TypeAlias" }, { "name": "AnchorY", "kind": "TypeAlias" }, + { "name": "AnimatedArea", "kind": "Function" }, + { "name": "AnimatedAreaProps", "kind": "Interface" }, + { "name": "AnimatedLine", "kind": "Function" }, + { "name": "AnimatedLineProps", "kind": "Interface" }, { "name": "AreaElement", "kind": "Function" }, { "name": "areaElementClasses", "kind": "Variable" }, { "name": "AreaElementClasses", "kind": "Interface" }, { "name": "AreaElementClassKey", "kind": "TypeAlias" }, + { "name": "AreaElementOwnerState", "kind": "Interface" }, { "name": "AreaElementPath", "kind": "Variable" }, - { "name": "AreaElementProps", "kind": "TypeAlias" }, + { "name": "AreaElementProps", "kind": "Interface" }, + { "name": "AreaElementSlotProps", "kind": "Interface" }, + { "name": "AreaElementSlots", "kind": "Interface" }, { "name": "AreaPlot", "kind": "Function" }, { "name": "AreaPlotProps", "kind": "Interface" }, { "name": "AreaPlotSlotProps", "kind": "Interface" }, @@ -137,8 +144,11 @@ { "name": "lineElementClasses", "kind": "Variable" }, { "name": "LineElementClasses", "kind": "Interface" }, { "name": "LineElementClassKey", "kind": "TypeAlias" }, + { "name": "LineElementOwnerState", "kind": "Interface" }, { "name": "LineElementPath", "kind": "Variable" }, - { "name": "LineElementProps", "kind": "TypeAlias" }, + { "name": "LineElementProps", "kind": "Interface" }, + { "name": "LineElementSlotProps", "kind": "Interface" }, + { "name": "LineElementSlots", "kind": "Interface" }, { "name": "LineHighlightElement", "kind": "Function" }, { "name": "lineHighlightElementClasses", "kind": "Variable" }, { "name": "LineHighlightElementClasses", "kind": "Interface" }, diff --git a/yarn.lock b/yarn.lock index c27e8bdaea853..961dbf4ef5199 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2962,7 +2962,7 @@ dependencies: "@types/node" "*" -"@types/d3-color@^3.1.3": +"@types/d3-color@*", "@types/d3-color@^3.1.3": version "3.1.3" resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== @@ -2972,6 +2972,13 @@ resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== +"@types/d3-interpolate@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + "@types/d3-path@*": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.0.2.tgz#4327f4a05d475cf9be46a93fc2e0f8d23380805a" @@ -5763,7 +5770,7 @@ d3-delaunay@^6.0.4: resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== -"d3-interpolate@1.2.0 - 3": +"d3-interpolate@1.2.0 - 3", d3-interpolate@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==