From f9b1a6328faf2721cab27e303fdbba38e693a416 Mon Sep 17 00:00:00 2001 From: Alexandre Fauquette <45398769+alexfauquette@users.noreply.github.com> Date: Mon, 29 Apr 2024 11:25:58 +0200 Subject: [PATCH] [charts] Add an overlay for "no data" or "loading" states (#12817) --- docs/data/charts/styling/CustomOverlay.js | 76 +++++++++++++++++++ docs/data/charts/styling/CustomOverlay.tsx | 76 +++++++++++++++++++ docs/data/charts/styling/Overlay.js | 18 +++++ docs/data/charts/styling/Overlay.tsx | 18 +++++ docs/data/charts/styling/Overlay.tsx.preview | 2 + docs/data/charts/styling/OverlayWithAxis.js | 38 ++++++++++ docs/data/charts/styling/OverlayWithAxis.tsx | 38 ++++++++++ docs/data/charts/styling/styling.md | 34 +++++++++ docs/pages/x/api/charts/bar-chart.json | 13 ++++ .../default-charts-axis-tooltip-content.json | 4 +- docs/pages/x/api/charts/line-chart.json | 13 ++++ docs/pages/x/api/charts/pie-chart.json | 13 ++++ docs/pages/x/api/charts/scatter-chart.json | 13 ++++ .../api-docs/charts/bar-chart/bar-chart.json | 3 + .../charts/line-chart/line-chart.json | 3 + .../api-docs/charts/pie-chart/pie-chart.json | 3 + .../charts/scatter-chart/scatter-chart.json | 3 + .../data-grid-premium/data-grid-premium.json | 2 +- .../data-grid-pro/data-grid-pro.json | 2 +- .../data-grid/data-grid/data-grid.json | 2 +- packages/x-charts/src/BarChart/BarChart.tsx | 21 ++++- .../ChartsOverlay/ChartsLoadingOverlay.tsx | 23 ++++++ .../src/ChartsOverlay/ChartsNoDataOverlay.tsx | 23 ++++++ .../src/ChartsOverlay/ChartsOverlay.tsx | 68 +++++++++++++++++ packages/x-charts/src/ChartsOverlay/index.ts | 3 + .../ChartsAxisTooltipContent.tsx | 8 +- .../DefaultChartsAxisTooltipContent.tsx | 2 +- .../x-charts/src/ChartsXAxis/ChartsXAxis.tsx | 7 ++ .../x-charts/src/ChartsYAxis/ChartsYAxis.tsx | 7 ++ packages/x-charts/src/LineChart/LineChart.tsx | 21 ++++- packages/x-charts/src/PieChart/PieChart.tsx | 21 ++++- .../src/ScatterChart/ScatterChart.tsx | 21 ++++- packages/x-charts/src/hooks/useScale.ts | 10 ++- packages/x-charts/src/hooks/useTicks.ts | 5 ++ packages/x-charts/src/models/axis.ts | 2 +- .../src/DataGridPremium/DataGridPremium.tsx | 2 +- .../src/DataGridPro/DataGridPro.tsx | 2 +- .../x-data-grid/src/DataGrid/DataGrid.tsx | 2 +- .../src/models/props/DataGridProps.ts | 2 +- scripts/buildApiDocs/chartsSettings/index.ts | 3 + 40 files changed, 600 insertions(+), 27 deletions(-) create mode 100644 docs/data/charts/styling/CustomOverlay.js create mode 100644 docs/data/charts/styling/CustomOverlay.tsx create mode 100644 docs/data/charts/styling/Overlay.js create mode 100644 docs/data/charts/styling/Overlay.tsx create mode 100644 docs/data/charts/styling/Overlay.tsx.preview create mode 100644 docs/data/charts/styling/OverlayWithAxis.js create mode 100644 docs/data/charts/styling/OverlayWithAxis.tsx create mode 100644 packages/x-charts/src/ChartsOverlay/ChartsLoadingOverlay.tsx create mode 100644 packages/x-charts/src/ChartsOverlay/ChartsNoDataOverlay.tsx create mode 100644 packages/x-charts/src/ChartsOverlay/ChartsOverlay.tsx create mode 100644 packages/x-charts/src/ChartsOverlay/index.ts diff --git a/docs/data/charts/styling/CustomOverlay.js b/docs/data/charts/styling/CustomOverlay.js new file mode 100644 index 000000000000..502ce26a93e2 --- /dev/null +++ b/docs/data/charts/styling/CustomOverlay.js @@ -0,0 +1,76 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import Stack from '@mui/material/Stack'; +import { BarChart } from '@mui/x-charts/BarChart'; +import { useDrawingArea, useXScale, useYScale } from '@mui/x-charts/hooks'; + +const ratios = [0.2, 0.8, 0.6, 0.5]; + +const LoadingReact = styled('rect')({ + opacity: 0.2, + fill: 'lightgray', +}); + +const LoadingText = styled('text')(({ theme }) => ({ + stroke: 'none', + fill: theme.palette.text.primary, + shapeRendering: 'crispEdges', + textAnchor: 'middle', + dominantBaseline: 'middle', +})); + +function LoadingOverlay() { + const xScale = useXScale(); + const yScale = useYScale(); + const { left, width, height } = useDrawingArea(); + + const bandWidth = xScale.bandwidth(); + + const [bottom, top] = yScale.range(); + + return ( + + {xScale.domain().map((item, index) => { + const ratio = ratios[index % ratios.length]; + const barHeight = ratio * (bottom - top); + + return ( + + ); + })} + + Loading data ... + + + ); +} + +export default function CustomOverlay() { + return ( + + + + + ); +} diff --git a/docs/data/charts/styling/CustomOverlay.tsx b/docs/data/charts/styling/CustomOverlay.tsx new file mode 100644 index 000000000000..99af073ed8d5 --- /dev/null +++ b/docs/data/charts/styling/CustomOverlay.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import Stack from '@mui/material/Stack'; +import { BarChart } from '@mui/x-charts/BarChart'; +import { useDrawingArea, useXScale, useYScale } from '@mui/x-charts/hooks'; + +const ratios = [0.2, 0.8, 0.6, 0.5]; + +const LoadingReact = styled('rect')({ + opacity: 0.2, + fill: 'lightgray', +}); + +const LoadingText = styled('text')(({ theme }) => ({ + stroke: 'none', + fill: theme.palette.text.primary, + shapeRendering: 'crispEdges', + textAnchor: 'middle', + dominantBaseline: 'middle', +})); + +function LoadingOverlay() { + const xScale = useXScale<'band'>(); + const yScale = useYScale(); + const { left, width, height } = useDrawingArea(); + + const bandWidth = xScale.bandwidth(); + + const [bottom, top] = yScale.range(); + + return ( + + {xScale.domain().map((item, index) => { + const ratio = ratios[index % ratios.length]; + const barHeight = ratio * (bottom - top); + + return ( + + ); + })} + + Loading data ... + + + ); +} + +export default function CustomOverlay() { + return ( + + + + + ); +} diff --git a/docs/data/charts/styling/Overlay.js b/docs/data/charts/styling/Overlay.js new file mode 100644 index 000000000000..586e8f5b2ad0 --- /dev/null +++ b/docs/data/charts/styling/Overlay.js @@ -0,0 +1,18 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import { LineChart } from '@mui/x-charts/LineChart'; + +const emptySeries = { + series: [], + margin: { top: 10, right: 10, left: 25, bottom: 25 }, + height: 150, +}; + +export default function Overlay() { + return ( + + + + + ); +} diff --git a/docs/data/charts/styling/Overlay.tsx b/docs/data/charts/styling/Overlay.tsx new file mode 100644 index 000000000000..586e8f5b2ad0 --- /dev/null +++ b/docs/data/charts/styling/Overlay.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import { LineChart } from '@mui/x-charts/LineChart'; + +const emptySeries = { + series: [], + margin: { top: 10, right: 10, left: 25, bottom: 25 }, + height: 150, +}; + +export default function Overlay() { + return ( + + + + + ); +} diff --git a/docs/data/charts/styling/Overlay.tsx.preview b/docs/data/charts/styling/Overlay.tsx.preview new file mode 100644 index 000000000000..caa7653ae1e2 --- /dev/null +++ b/docs/data/charts/styling/Overlay.tsx.preview @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docs/data/charts/styling/OverlayWithAxis.js b/docs/data/charts/styling/OverlayWithAxis.js new file mode 100644 index 000000000000..3a9aaab01e60 --- /dev/null +++ b/docs/data/charts/styling/OverlayWithAxis.js @@ -0,0 +1,38 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import { LineChart } from '@mui/x-charts/LineChart'; + +const emptySeries = { + series: [], + margin: { top: 10, right: 10, left: 25, bottom: 25 }, + height: 150, +}; + +export default function OverlayWithAxis() { + return ( + + + + + ); +} diff --git a/docs/data/charts/styling/OverlayWithAxis.tsx b/docs/data/charts/styling/OverlayWithAxis.tsx new file mode 100644 index 000000000000..3a9aaab01e60 --- /dev/null +++ b/docs/data/charts/styling/OverlayWithAxis.tsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import { LineChart } from '@mui/x-charts/LineChart'; + +const emptySeries = { + series: [], + margin: { top: 10, right: 10, left: 25, bottom: 25 }, + height: 150, +}; + +export default function OverlayWithAxis() { + return ( + + + + + ); +} diff --git a/docs/data/charts/styling/styling.md b/docs/data/charts/styling/styling.md index 61d1a1357f6d..3ab1003b62f9 100644 --- a/docs/data/charts/styling/styling.md +++ b/docs/data/charts/styling/styling.md @@ -102,6 +102,40 @@ This configuration can be used in Bar Charts to set colors according to string c } ``` +## Overlay + +Charts have a _loading_ and _noData_ overlays that appears if + +- `loading` prop is set to `true`. +- There is no data to display. + +{{"demo": "Overlay.js"}} + +### Axis display + +You can provide the axes data in order to display them while loading the data. + +{{"demo": "OverlayWithAxis.js"}} + +### Custom overlay + +To modify the overly message, you can use the `message` props as follow. + +```jsx + +``` + +For more advanced customization, use the `loadingOverlay` and `noDataOverlay` slots link in the following demo. + +{{"demo": "CustomOverlay.js"}} + ## Styling ### Size diff --git a/docs/pages/x/api/charts/bar-chart.json b/docs/pages/x/api/charts/bar-chart.json index 6b8bb27f372b..5e16516b000a 100644 --- a/docs/pages/x/api/charts/bar-chart.json +++ b/docs/pages/x/api/charts/bar-chart.json @@ -36,6 +36,7 @@ "type": { "name": "union", "description": "object
| string" }, "default": "yAxisIds[0] The id of the first provided axis" }, + "loading": { "type": { "name": "bool" } }, "margin": { "type": { "name": "shape", @@ -152,6 +153,18 @@ "description": "Custom component for displaying tooltip content when triggered by item event.", "default": "DefaultChartsItemTooltipContent", "class": null + }, + { + "name": "loadingOverlay", + "description": "Overlay component rendered when the chart is in a loading state.", + "default": "ChartsLoadingOverlay", + "class": null + }, + { + "name": "noDataOverlay", + "description": "Overlay component rendered when the chart has no data to display.", + "default": "ChartsNoDataOverlay", + "class": null } ], "classes": [], 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 951dac84a962..0e922896786a 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 @@ -8,7 +8,6 @@ }, "required": true }, - "axisValue": { "type": { "name": "any" }, "required": true }, "classes": { "type": { "name": "object" }, "required": true, @@ -18,6 +17,9 @@ "type": { "name": "arrayOf", "description": "Array<object>" }, "required": true }, + "axisValue": { + "type": { "name": "union", "description": "Date
| number
| string" } + }, "dataIndex": { "type": { "name": "number" } } }, "name": "DefaultChartsAxisTooltipContent", diff --git a/docs/pages/x/api/charts/line-chart.json b/docs/pages/x/api/charts/line-chart.json index b1d80bcaf2f8..0da1697a0aa1 100644 --- a/docs/pages/x/api/charts/line-chart.json +++ b/docs/pages/x/api/charts/line-chart.json @@ -34,6 +34,7 @@ "type": { "name": "union", "description": "object
| string" }, "default": "yAxisIds[0] The id of the first provided axis" }, + "loading": { "type": { "name": "bool" } }, "margin": { "type": { "name": "shape", @@ -155,6 +156,18 @@ "description": "Custom component for displaying tooltip content when triggered by item event.", "default": "DefaultChartsItemTooltipContent", "class": null + }, + { + "name": "loadingOverlay", + "description": "Overlay component rendered when the chart is in a loading state.", + "default": "ChartsLoadingOverlay", + "class": null + }, + { + "name": "noDataOverlay", + "description": "Overlay component rendered when the chart has no data to display.", + "default": "ChartsNoDataOverlay", + "class": null } ], "classes": [], diff --git a/docs/pages/x/api/charts/pie-chart.json b/docs/pages/x/api/charts/pie-chart.json index 69293d2a8da2..8583de3dbaa3 100644 --- a/docs/pages/x/api/charts/pie-chart.json +++ b/docs/pages/x/api/charts/pie-chart.json @@ -39,6 +39,7 @@ "deprecated": true, "deprecationInfo": "Consider using slotProps.legend instead." }, + "loading": { "type": { "name": "bool" } }, "margin": { "type": { "name": "shape", @@ -139,6 +140,18 @@ "description": "Custom component for displaying tooltip content when triggered by item event.", "default": "DefaultChartsItemTooltipContent", "class": null + }, + { + "name": "loadingOverlay", + "description": "Overlay component rendered when the chart is in a loading state.", + "default": "ChartsLoadingOverlay", + "class": null + }, + { + "name": "noDataOverlay", + "description": "Overlay component rendered when the chart has no data to display.", + "default": "ChartsNoDataOverlay", + "class": null } ], "classes": [], diff --git a/docs/pages/x/api/charts/scatter-chart.json b/docs/pages/x/api/charts/scatter-chart.json index d5ef16720554..ff1370a2a4d3 100644 --- a/docs/pages/x/api/charts/scatter-chart.json +++ b/docs/pages/x/api/charts/scatter-chart.json @@ -34,6 +34,7 @@ "type": { "name": "union", "description": "object
| string" }, "default": "yAxisIds[0] The id of the first provided axis" }, + "loading": { "type": { "name": "bool" } }, "margin": { "type": { "name": "shape", @@ -145,6 +146,18 @@ "description": "Custom component for displaying tooltip content when triggered by item event.", "default": "DefaultChartsItemTooltipContent", "class": null + }, + { + "name": "loadingOverlay", + "description": "Overlay component rendered when the chart is in a loading state.", + "default": "ChartsLoadingOverlay", + "class": null + }, + { + "name": "noDataOverlay", + "description": "Overlay component rendered when the chart has no data to display.", + "default": "ChartsNoDataOverlay", + "class": null } ], "classes": [], 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 176f08e5a0a9..ee7dd914cfdb 100644 --- a/docs/translations/api-docs/charts/bar-chart/bar-chart.json +++ b/docs/translations/api-docs/charts/bar-chart/bar-chart.json @@ -23,6 +23,7 @@ "leftAxis": { "description": "Indicate which axis to display the left of the charts. Can be a string (the id of the axis) or an object ChartsYAxisProps." }, + "loading": { "description": "If true, a loading overlay is displayed." }, "margin": { "description": "The margin between the SVG and the drawing area. It's used for leaving some space for extra information such as the x- and y-axis or legend. Accepts an object with the optional properties: top, bottom, left, and right." }, @@ -76,6 +77,8 @@ "bar": "The component that renders the bar.", "itemContent": "Custom component for displaying tooltip content when triggered by item event.", "legend": "Custom rendering of the legend.", + "loadingOverlay": "Overlay component rendered when the chart is in a loading state.", + "noDataOverlay": "Overlay component rendered when the chart has no data to display.", "popper": "Custom component for the tooltip popper." } } 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 eed651782034..2df6f3875d55 100644 --- a/docs/translations/api-docs/charts/line-chart/line-chart.json +++ b/docs/translations/api-docs/charts/line-chart/line-chart.json @@ -25,6 +25,7 @@ "leftAxis": { "description": "Indicate which axis to display the left of the charts. Can be a string (the id of the axis) or an object ChartsYAxisProps." }, + "loading": { "description": "If true, a loading overlay is displayed." }, "margin": { "description": "The margin between the SVG and the drawing area. It's used for leaving some space for extra information such as the x- and y-axis or legend. Accepts an object with the optional properties: top, bottom, left, and right." }, @@ -76,7 +77,9 @@ "legend": "Custom rendering of the legend.", "line": "The component that renders the line.", "lineHighlight": "", + "loadingOverlay": "Overlay component rendered when the chart is in a loading state.", "mark": "", + "noDataOverlay": "Overlay component rendered when the chart has no data to display.", "popper": "Custom component for the tooltip popper." } } 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 fdbc83823da9..d5a4ea4df15e 100644 --- a/docs/translations/api-docs/charts/pie-chart/pie-chart.json +++ b/docs/translations/api-docs/charts/pie-chart/pie-chart.json @@ -22,6 +22,7 @@ "description": "Indicate which axis to display the left of the charts. Can be a string (the id of the axis) or an object ChartsYAxisProps." }, "legend": { "description": "The props of the legend." }, + "loading": { "description": "If true, a loading overlay is displayed." }, "margin": { "description": "The margin between the SVG and the drawing area. It's used for leaving some space for extra information such as the x- and y-axis or legend. Accepts an object with the optional properties: top, bottom, left, and right." }, @@ -61,6 +62,8 @@ "axisTickLabel": "Custom component for tick label.", "itemContent": "Custom component for displaying tooltip content when triggered by item event.", "legend": "Custom rendering of the legend.", + "loadingOverlay": "Overlay component rendered when the chart is in a loading state.", + "noDataOverlay": "Overlay component rendered when the chart has no data to display.", "pieArc": "", "pieArcLabel": "", "popper": "Custom component for the tooltip popper." diff --git a/docs/translations/api-docs/charts/scatter-chart/scatter-chart.json b/docs/translations/api-docs/charts/scatter-chart/scatter-chart.json index e21764d22d4c..7c64bdaf7757 100644 --- a/docs/translations/api-docs/charts/scatter-chart/scatter-chart.json +++ b/docs/translations/api-docs/charts/scatter-chart/scatter-chart.json @@ -25,6 +25,7 @@ "leftAxis": { "description": "Indicate which axis to display the left of the charts. Can be a string (the id of the axis) or an object ChartsYAxisProps." }, + "loading": { "description": "If true, a loading overlay is displayed." }, "margin": { "description": "The margin between the SVG and the drawing area. It's used for leaving some space for extra information such as the x- and y-axis or legend. Accepts an object with the optional properties: top, bottom, left, and right." }, @@ -73,6 +74,8 @@ "axisTickLabel": "Custom component for tick label.", "itemContent": "Custom component for displaying tooltip content when triggered by item event.", "legend": "Custom rendering of the legend.", + "loadingOverlay": "Overlay component rendered when the chart is in a loading state.", + "noDataOverlay": "Overlay component rendered when the chart has no data to display.", "popper": "Custom component for the tooltip popper.", "scatter": "" } diff --git a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json index dc1028649c3e..891f2d299dc2 100644 --- a/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json +++ b/docs/translations/api-docs/data-grid/data-grid-premium/data-grid-premium.json @@ -232,7 +232,7 @@ "keepNonExistentRowsSelected": { "description": "If true, the selection model will retain selected rows that do not exist. Useful when using server side pagination and row selections need to be retained when changing pages." }, - "loading": { "description": "If true, a loading overlay is displayed." }, + "loading": { "description": "If true, a loading overlay is displayed." }, "localeText": { "description": "Set the locale text of the Data Grid. You can find all the translation keys supported in the source in the GitHub repository." }, diff --git a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json index 3bc27f5060e0..be79188d0bbc 100644 --- a/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json +++ b/docs/translations/api-docs/data-grid/data-grid-pro/data-grid-pro.json @@ -213,7 +213,7 @@ "keepNonExistentRowsSelected": { "description": "If true, the selection model will retain selected rows that do not exist. Useful when using server side pagination and row selections need to be retained when changing pages." }, - "loading": { "description": "If true, a loading overlay is displayed." }, + "loading": { "description": "If true, a loading overlay is displayed." }, "localeText": { "description": "Set the locale text of the Data Grid. You can find all the translation keys supported in the source in the GitHub repository." }, diff --git a/docs/translations/api-docs/data-grid/data-grid/data-grid.json b/docs/translations/api-docs/data-grid/data-grid/data-grid.json index abe89e1a6073..7ea702d9b210 100644 --- a/docs/translations/api-docs/data-grid/data-grid/data-grid.json +++ b/docs/translations/api-docs/data-grid/data-grid/data-grid.json @@ -156,7 +156,7 @@ "keepNonExistentRowsSelected": { "description": "If true, the selection model will retain selected rows that do not exist. Useful when using server side pagination and row selections need to be retained when changing pages." }, - "loading": { "description": "If true, a loading overlay is displayed." }, + "loading": { "description": "If true, a loading overlay is displayed." }, "localeText": { "description": "Set the locale text of the Data Grid. You can find all the translation keys supported in the source in the GitHub repository." }, diff --git a/packages/x-charts/src/BarChart/BarChart.tsx b/packages/x-charts/src/BarChart/BarChart.tsx index 6d812f1bdc27..e2514278171b 100644 --- a/packages/x-charts/src/BarChart/BarChart.tsx +++ b/packages/x-charts/src/BarChart/BarChart.tsx @@ -30,22 +30,31 @@ import { ChartsOnAxisClickHandler, ChartsOnAxisClickHandlerProps, } from '../ChartsOnAxisClickHandler'; +import { + ChartsOverlay, + ChartsOverlayProps, + ChartsOverlaySlotProps, + ChartsOverlaySlots, +} from '../ChartsOverlay/ChartsOverlay'; export interface BarChartSlots extends ChartsAxisSlots, BarPlotSlots, ChartsLegendSlots, - ChartsTooltipSlots {} + ChartsTooltipSlots, + ChartsOverlaySlots {} export interface BarChartSlotProps extends ChartsAxisSlotProps, BarPlotSlotProps, ChartsLegendSlotProps, - ChartsTooltipSlotProps {} + ChartsTooltipSlotProps, + ChartsOverlaySlotProps {} export interface BarChartProps extends Omit, Omit, Omit, + Omit, ChartsOnAxisClickHandlerProps { /** * The series to display in the bar chart. @@ -128,6 +137,7 @@ const BarChart = React.forwardRef(function BarChart(props: BarChartProps, ref) { children, slots, slotProps, + loading, } = props; const id = useId(); @@ -187,6 +197,7 @@ const BarChart = React.forwardRef(function BarChart(props: BarChartProps, ref) { skipAnimation={skipAnimation} onItemClick={onItemClick} /> + - + {!loading && } {children} @@ -280,6 +291,10 @@ BarChart.propTypes = { slotProps: PropTypes.object, slots: PropTypes.object, }), + /** + * If `true`, a loading overlay is displayed. + */ + loading: PropTypes.bool, /** * The margin between the SVG and the drawing area. * It's used for leaving some space for extra information such as the x- and y-axis or legend. diff --git a/packages/x-charts/src/ChartsOverlay/ChartsLoadingOverlay.tsx b/packages/x-charts/src/ChartsOverlay/ChartsLoadingOverlay.tsx new file mode 100644 index 000000000000..aff9dc1678b7 --- /dev/null +++ b/packages/x-charts/src/ChartsOverlay/ChartsLoadingOverlay.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import { useDrawingArea } from '../hooks/useDrawingArea'; +import type { CommonOverlayProps } from './ChartsOverlay'; + +const StyledText = styled('text')(({ theme }) => ({ + stroke: 'none', + fill: theme.palette.text.primary, + shapeRendering: 'crispEdges', + textAnchor: 'middle', + dominantBaseline: 'middle', +})); + +export function ChartsLoadingOverlay(props: CommonOverlayProps) { + const { message, ...other } = props; + const { top, left, height, width } = useDrawingArea(); + + return ( + + {message ?? 'Loading data ...'} + + ); +} diff --git a/packages/x-charts/src/ChartsOverlay/ChartsNoDataOverlay.tsx b/packages/x-charts/src/ChartsOverlay/ChartsNoDataOverlay.tsx new file mode 100644 index 000000000000..3567e83b4b1e --- /dev/null +++ b/packages/x-charts/src/ChartsOverlay/ChartsNoDataOverlay.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { styled } from '@mui/material/styles'; +import { useDrawingArea } from '../hooks/useDrawingArea'; +import type { CommonOverlayProps } from './ChartsOverlay'; + +const StyledText = styled('text')(({ theme }) => ({ + stroke: 'none', + fill: theme.palette.text.primary, + shapeRendering: 'crispEdges', + textAnchor: 'middle', + dominantBaseline: 'middle', +})); + +export function ChartsNoDataOverlay(props: CommonOverlayProps) { + const { message, ...other } = props; + const { top, left, height, width } = useDrawingArea(); + + return ( + + {message ?? 'No data to display'} + + ); +} diff --git a/packages/x-charts/src/ChartsOverlay/ChartsOverlay.tsx b/packages/x-charts/src/ChartsOverlay/ChartsOverlay.tsx new file mode 100644 index 000000000000..ef1e7371336f --- /dev/null +++ b/packages/x-charts/src/ChartsOverlay/ChartsOverlay.tsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import { SxProps, Theme } from '@mui/material/styles'; +import { ChartsLoadingOverlay } from './ChartsLoadingOverlay'; +import { useSeries } from '../hooks/useSeries'; +import { SeriesId } from '../models/seriesType/common'; +import { ChartsNoDataOverlay } from './ChartsNoDataOverlay'; + +export function useNoData() { + const seriesPerType = useSeries(); + + return Object.values(seriesPerType).every((seriesOfGivenType) => { + if (!seriesOfGivenType) { + return true; + } + const { series, seriesOrder } = seriesOfGivenType; + + return seriesOrder.every((seriesId: SeriesId) => series[seriesId].data.length === 0); + }); +} + +export type CommonOverlayProps = React.SVGAttributes & { + /** + * The message displayed by the overlay. + */ + message?: string; + sx?: SxProps; +}; + +export interface ChartsOverlaySlots { + /** + * Overlay component rendered when the chart is in a loading state. + * @default ChartsLoadingOverlay + */ + loadingOverlay?: React.ElementType; + /** + * Overlay component rendered when the chart has no data to display. + * @default ChartsNoDataOverlay + */ + noDataOverlay?: React.ElementType; +} +export interface ChartsOverlaySlotProps { + loadingOverlay?: Partial; + noDataOverlay?: Partial; +} + +export interface ChartsOverlayProps { + /** + * If `true`, a loading overlay is displayed. + */ + loading?: boolean; + + slots?: ChartsOverlaySlots; + slotProps?: ChartsOverlaySlotProps; +} + +export function ChartsOverlay(props: ChartsOverlayProps) { + const noData = useNoData(); + + if (props.loading) { + const LoadingOverlay = props.slots?.loadingOverlay ?? ChartsLoadingOverlay; + return ; + } + if (noData) { + const NoDataOverlay = props.slots?.noDataOverlay ?? ChartsNoDataOverlay; + return ; + } + return null; +} diff --git a/packages/x-charts/src/ChartsOverlay/index.ts b/packages/x-charts/src/ChartsOverlay/index.ts new file mode 100644 index 000000000000..200c86e4c607 --- /dev/null +++ b/packages/x-charts/src/ChartsOverlay/index.ts @@ -0,0 +1,3 @@ +export { ChartsOverlay } from './ChartsOverlay'; +export { ChartsLoadingOverlay } from './ChartsLoadingOverlay'; +export { ChartsNoDataOverlay } from './ChartsNoDataOverlay'; diff --git a/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx b/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx index 8eaadafc69c0..29c4d26e8835 100644 --- a/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx +++ b/packages/x-charts/src/ChartsTooltip/ChartsAxisTooltipContent.tsx @@ -37,7 +37,7 @@ export type ChartsAxisContentProps = { /** * The value associated to the current mouse position. */ - axisValue: any; + axisValue: string | number | Date | null; /** * Override or extend the styles applied to the component. */ @@ -157,7 +157,11 @@ ChartsAxisTooltipContent.propTypes = { .isRequired, }), }), - axisValue: PropTypes.any, + axisValue: PropTypes.oneOfType([ + PropTypes.instanceOf(Date), + PropTypes.number, + PropTypes.string, + ]), classes: PropTypes.object, dataIndex: PropTypes.number, series: PropTypes.arrayOf(PropTypes.object), diff --git a/packages/x-charts/src/ChartsTooltip/DefaultChartsAxisTooltipContent.tsx b/packages/x-charts/src/ChartsTooltip/DefaultChartsAxisTooltipContent.tsx index 1b224e78fb2e..8254e1c9ed64 100644 --- a/packages/x-charts/src/ChartsTooltip/DefaultChartsAxisTooltipContent.tsx +++ b/packages/x-charts/src/ChartsTooltip/DefaultChartsAxisTooltipContent.tsx @@ -96,7 +96,7 @@ DefaultChartsAxisTooltipContent.propTypes = { /** * The value associated to the current mouse position. */ - axisValue: PropTypes.any.isRequired, + axisValue: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.number, PropTypes.string]), /** * Override or extend the styles applied to the component. */ diff --git a/packages/x-charts/src/ChartsXAxis/ChartsXAxis.tsx b/packages/x-charts/src/ChartsXAxis/ChartsXAxis.tsx index 1bd054d7e6e3..35917b184509 100644 --- a/packages/x-charts/src/ChartsXAxis/ChartsXAxis.tsx +++ b/packages/x-charts/src/ChartsXAxis/ChartsXAxis.tsx @@ -192,6 +192,13 @@ function ChartsXAxis(inProps: ChartsXAxisProps) { ownerState: {}, }); + const domain = xScale.domain(); + if (domain.length === 0 || domain[0] === domain[1]) { + // Skip axis rendering if + // - the data is empty (for band and point axis) + // - No data is associated to the axis (other scale types) + return null; + } return ( , Omit, + Omit, ChartsOnAxisClickHandlerProps { /** * The series to display in the line chart. @@ -155,6 +164,7 @@ const LineChart = React.forwardRef(function LineChart(props: LineChartProps, ref slots, slotProps, skipAnimation, + loading, } = props; const id = useId(); @@ -209,6 +219,7 @@ const LineChart = React.forwardRef(function LineChart(props: LineChartProps, ref onItemClick={onLineClick} skipAnimation={skipAnimation} /> + - + {!loading && } {children} @@ -307,6 +318,10 @@ LineChart.propTypes = { slotProps: PropTypes.object, slots: PropTypes.object, }), + /** + * If `true`, a loading overlay is displayed. + */ + loading: PropTypes.bool, /** * The margin between the SVG and the drawing area. * It's used for leaving some space for extra information such as the x- and y-axis or legend. diff --git a/packages/x-charts/src/PieChart/PieChart.tsx b/packages/x-charts/src/PieChart/PieChart.tsx index b358e0e00576..380252524043 100644 --- a/packages/x-charts/src/PieChart/PieChart.tsx +++ b/packages/x-charts/src/PieChart/PieChart.tsx @@ -30,22 +30,31 @@ import { ChartsYAxisProps, } from '../models/axis'; import { useIsRTL } from '../internals/useIsRTL'; +import { + ChartsOverlay, + ChartsOverlayProps, + ChartsOverlaySlotProps, + ChartsOverlaySlots, +} from '../ChartsOverlay/ChartsOverlay'; export interface PieChartSlots extends ChartsAxisSlots, PiePlotSlots, ChartsLegendSlots, - ChartsTooltipSlots {} + ChartsTooltipSlots, + ChartsOverlaySlots {} export interface PieChartSlotProps extends ChartsAxisSlotProps, PiePlotSlotProps, ChartsLegendSlotProps, - ChartsTooltipSlotProps {} + ChartsTooltipSlotProps, + ChartsOverlaySlotProps {} export interface PieChartProps extends Omit, Omit, + Omit, Pick { /** * Indicate which axis to display the bottom of the charts. @@ -133,6 +142,7 @@ function PieChart(props: PieChartProps) { slots, slotProps, onItemClick, + loading, } = props; const isRTL = useIsRTL(); @@ -181,9 +191,10 @@ function PieChart(props: PieChartProps) { onItemClick={onItemClick} skipAnimation={skipAnimation} /> + - + {!loading && } {children} ); @@ -253,6 +264,10 @@ PieChart.propTypes = { slotProps: PropTypes.object, slots: PropTypes.object, }), + /** + * If `true`, a loading overlay is displayed. + */ + loading: PropTypes.bool, /** * The margin between the SVG and the drawing area. * It's used for leaving some space for extra information such as the x- and y-axis or legend. diff --git a/packages/x-charts/src/ScatterChart/ScatterChart.tsx b/packages/x-charts/src/ScatterChart/ScatterChart.tsx index ba6afc0469af..eb8666d4ec51 100644 --- a/packages/x-charts/src/ScatterChart/ScatterChart.tsx +++ b/packages/x-charts/src/ScatterChart/ScatterChart.tsx @@ -25,6 +25,12 @@ import { ChartsLegendSlotProps, ChartsLegendSlots, } from '../ChartsLegend'; +import { + ChartsOverlay, + ChartsOverlayProps, + ChartsOverlaySlotProps, + ChartsOverlaySlots, +} from '../ChartsOverlay/ChartsOverlay'; import { ChartsAxisHighlight, ChartsAxisHighlightProps } from '../ChartsAxisHighlight'; import { ChartsAxisSlots, ChartsAxisSlotProps } from '../models/axis'; import { @@ -38,17 +44,20 @@ export interface ScatterChartSlots extends ChartsAxisSlots, ScatterPlotSlots, ChartsLegendSlots, - ChartsTooltipSlots {} + ChartsTooltipSlots, + ChartsOverlaySlots {} export interface ScatterChartSlotProps extends ChartsAxisSlotProps, ScatterPlotSlotProps, ChartsLegendSlotProps, - ChartsTooltipSlotProps {} + ChartsTooltipSlotProps, + ChartsOverlaySlotProps {} export interface ScatterChartProps extends Omit, Omit, Omit, + Omit, Omit { /** * The series to display in the scatter chart. @@ -133,6 +142,7 @@ const ScatterChart = React.forwardRef(function ScatterChart(props: ScatterChartP children, slots, slotProps, + loading, } = props; return ( + - + {!loading && } {children} @@ -253,6 +264,10 @@ ScatterChart.propTypes = { slotProps: PropTypes.object, slots: PropTypes.object, }), + /** + * If `true`, a loading overlay is displayed. + */ + loading: PropTypes.bool, /** * The margin between the SVG and the drawing area. * It's used for leaving some space for extra information such as the x- and y-axis or legend. diff --git a/packages/x-charts/src/hooks/useScale.ts b/packages/x-charts/src/hooks/useScale.ts index c891a82d0af1..736435eeab43 100644 --- a/packages/x-charts/src/hooks/useScale.ts +++ b/packages/x-charts/src/hooks/useScale.ts @@ -1,7 +1,7 @@ import * as React from 'react'; import { CartesianContext } from '../context/CartesianContextProvider'; import { isBandScale } from '../internals/isBandScale'; -import { D3Scale } from '../models/axis'; +import { AxisScaleConfig, D3Scale, ScaleName } from '../models/axis'; /** * For a given scale return a function that map value to their position. @@ -16,7 +16,9 @@ export function getValueToPositionMapper(scale: D3Scale) { return (value: any) => scale(value) as number; } -export function useXScale(identifier?: number | string) { +export function useXScale( + identifier?: number | string, +): AxisScaleConfig[S]['scale'] { const { xAxis, xAxisIds } = React.useContext(CartesianContext); const id = typeof identifier === 'string' ? identifier : xAxisIds[identifier ?? 0]; @@ -24,7 +26,9 @@ export function useXScale(identifier?: number | string) { return xAxis[id].scale; } -export function useYScale(identifier?: number | string) { +export function useYScale( + identifier?: number | string, +): AxisScaleConfig[S]['scale'] { const { yAxis, yAxisIds } = React.useContext(CartesianContext); const id = typeof identifier === 'string' ? identifier : yAxisIds[identifier ?? 0]; diff --git a/packages/x-charts/src/hooks/useTicks.ts b/packages/x-charts/src/hooks/useTicks.ts index fe8f84f3f39e..2e861a33c88e 100644 --- a/packages/x-charts/src/hooks/useTicks.ts +++ b/packages/x-charts/src/hooks/useTicks.ts @@ -139,6 +139,11 @@ export function useTicks( })); } + if (scale.domain().length === 0 || scale.domain()[0] === scale.domain()[1]) { + // The axis should not be visible, so ticks should also be hidden. + return []; + } + const ticks = typeof tickInterval === 'object' ? tickInterval : scale.ticks(tickNumber); return ticks.map((value: any) => ({ value, diff --git a/packages/x-charts/src/models/axis.ts b/packages/x-charts/src/models/axis.ts index fe9bba7b5109..cb6b11300956 100644 --- a/packages/x-charts/src/models/axis.ts +++ b/packages/x-charts/src/models/axis.ts @@ -159,7 +159,7 @@ export interface ChartsXAxisProps extends ChartsAxisProps { export type ScaleName = 'linear' | 'band' | 'point' | 'log' | 'pow' | 'sqrt' | 'time' | 'utc'; export type ContinuousScaleName = 'linear' | 'log' | 'pow' | 'sqrt' | 'time' | 'utc'; -interface AxisScaleConfig { +export interface AxisScaleConfig { band: { scaleType: 'band'; scale: ScaleBand; diff --git a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx index 9e3f04214546..2528ab754e58 100644 --- a/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx +++ b/packages/x-data-grid-premium/src/DataGridPremium/DataGridPremium.tsx @@ -515,7 +515,7 @@ DataGridPremiumRaw.propTypes = { */ keepNonExistentRowsSelected: PropTypes.bool, /** - * If `true`, a loading overlay is displayed. + * If `true`, a loading overlay is displayed. */ loading: PropTypes.bool, /** diff --git a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx index cd6c469747b2..00861a129946 100644 --- a/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx +++ b/packages/x-data-grid-pro/src/DataGridPro/DataGridPro.tsx @@ -456,7 +456,7 @@ DataGridProRaw.propTypes = { */ keepNonExistentRowsSelected: PropTypes.bool, /** - * If `true`, a loading overlay is displayed. + * If `true`, a loading overlay is displayed. */ loading: PropTypes.bool, /** diff --git a/packages/x-data-grid/src/DataGrid/DataGrid.tsx b/packages/x-data-grid/src/DataGrid/DataGrid.tsx index 56a9868fa182..5be3925e2bfd 100644 --- a/packages/x-data-grid/src/DataGrid/DataGrid.tsx +++ b/packages/x-data-grid/src/DataGrid/DataGrid.tsx @@ -367,7 +367,7 @@ DataGridRaw.propTypes = { */ keepNonExistentRowsSelected: PropTypes.bool, /** - * If `true`, a loading overlay is displayed. + * If `true`, a loading overlay is displayed. */ loading: PropTypes.bool, /** diff --git a/packages/x-data-grid/src/models/props/DataGridProps.ts b/packages/x-data-grid/src/models/props/DataGridProps.ts index 3d80dcdb101c..dab6bbdef927 100644 --- a/packages/x-data-grid/src/models/props/DataGridProps.ts +++ b/packages/x-data-grid/src/models/props/DataGridProps.ts @@ -735,7 +735,7 @@ export interface DataGridPropsWithoutDefaultValue; /** - * If `true`, a loading overlay is displayed. + * If `true`, a loading overlay is displayed. */ loading?: boolean; /** diff --git a/scripts/buildApiDocs/chartsSettings/index.ts b/scripts/buildApiDocs/chartsSettings/index.ts index bf83efd12a60..1beaabaa78d6 100644 --- a/scripts/buildApiDocs/chartsSettings/index.ts +++ b/scripts/buildApiDocs/chartsSettings/index.ts @@ -65,6 +65,9 @@ export default apiPages; 'x-charts/src/Gauge/GaugeReferenceArc.tsx', 'x-charts/src/Gauge/GaugeValueArc.tsx', 'x-charts/src/Gauge/GaugeValueText.tsx', + 'x-charts/src/ChartsOverlay/ChartsOverlay.tsx', + 'x-charts/src/ChartsOverlay/ChartsNoDataOverlay.tsx', + 'x-charts/src/ChartsOverlay/ChartsLoadingOverlay.tsx', ].some((invalidPath) => filename.endsWith(invalidPath)); }, skipAnnotatingComponentDefinition: true,