Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[charts] Add an overlay for "no data" or "loading" states #12817

Merged
merged 14 commits into from
Apr 29, 2024
22 changes: 22 additions & 0 deletions packages/x-charts/src/ChartsOverlay/ChartsLoadingOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 { top, left, height, width } = useDrawingArea();

return (
<StyledText x={left + width / 2} y={top + height / 2} {...props}>
Loading data ...
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you don't want to introduce a localization system for this text 😆
But people should be able to pass slotProps={{ loadingOverlay: { children: 'Chargement des données' } }} and right now I don't think it works

The following might do the trick:

Suggested change
Loading data ...
props.children ?? 'Loading data ...'

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I didn't see it was in draft 👼

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issue I was motivated to introduce a localization system, but the notion of props might be better as long as we do not see other translation needs

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as long as we do not see other translation needs

I agree with your approach
If this will likely be the only text in the foreseeable future, a prop is fine
If we think of some other texts that might be introduced soon, the localization system is required

</StyledText>
);
}
22 changes: 22 additions & 0 deletions packages/x-charts/src/ChartsOverlay/ChartsNoDataOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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 { top, left, height, width } = useDrawingArea();

return (
<StyledText x={left + width / 2} y={top + height / 2} {...props}>
No data to display
</StyledText>
);
}
64 changes: 64 additions & 0 deletions packages/x-charts/src/ChartsOverlay/ChartsOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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<SVGTextElement> & {
sx?: SxProps<Theme>;
};

export interface ChartsOverlaySlots {
/**
* Loading overlay component rendered when the chart is in a loading state.
alexfauquette marked this conversation as resolved.
Show resolved Hide resolved
* @default ChartsLoadingOverlay
*/
loadingOverlay?: React.ElementType<CommonOverlayProps>;
/**
* No data overlay component rendered when the chart has no data to display.
alexfauquette marked this conversation as resolved.
Show resolved Hide resolved
* @default ChartsNoDataOverlay
*/
noDataOverlay?: React.ElementType<CommonOverlayProps>;
JCQuintas marked this conversation as resolved.
Show resolved Hide resolved
}
export interface ChartsOverlaySlotProps {
loadingOverlay?: Partial<CommonOverlayProps>;
noDataOverlay?: Partial<CommonOverlayProps>;
}

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 <LoadingOverlay {...props.slotProps?.loadingOverlay} />;
}
if (noData) {
const NoDataOverlay = props.slots?.noDataOverlay ?? ChartsNoDataOverlay;
return <NoDataOverlay {...props.slotProps?.noDataOverlay} />;
}
return null;
}
3 changes: 3 additions & 0 deletions packages/x-charts/src/ChartsOverlay/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { ChartsOverlay } from './ChartsOverlay';
export { ChartsLoadingOverlay } from './ChartsLoadingOverlay';
export { ChartsNoDataOverlay } from './ChartsNoDataOverlay';
10 changes: 8 additions & 2 deletions packages/x-charts/src/LineChart/LineChart.tsx
Original file line number Diff line number Diff line change
@@ -37,6 +37,7 @@ import {
ChartsOnAxisClickHandler,
ChartsOnAxisClickHandlerProps,
} from '../ChartsOnAxisClickHandler';
import { ChartsOverlay, ChartsOverlayProps, ChartsOverlaySlotProps, ChartsOverlaySlots } from '../ChartsOverlay/ChartsOverlay';

export interface LineChartSlots
extends ChartsAxisSlots,
@@ -45,19 +46,22 @@ export interface LineChartSlots
MarkPlotSlots,
LineHighlightPlotSlots,
ChartsLegendSlots,
ChartsTooltipSlots {}
ChartsTooltipSlots,
ChartsOverlaySlots {}
export interface LineChartSlotProps
extends ChartsAxisSlotProps,
AreaPlotSlotProps,
LinePlotSlotProps,
MarkPlotSlotProps,
LineHighlightPlotSlotProps,
ChartsLegendSlotProps,
ChartsTooltipSlotProps {}
ChartsTooltipSlotProps,
ChartsOverlaySlotProps {}

export interface LineChartProps
extends Omit<ResponsiveChartContainerProps, 'series'>,
Omit<ChartsAxisProps, 'slots' | 'slotProps'>,
Omit<ChartsOverlayProps, 'slots' | 'slotProps'>,
ChartsOnAxisClickHandlerProps {
/**
* The series to display in the line chart.
@@ -154,6 +158,7 @@ const LineChart = React.forwardRef(function LineChart(props: LineChartProps, ref
slots,
slotProps,
skipAnimation,
loading,
} = props;

const id = useId();
@@ -229,6 +234,7 @@ const LineChart = React.forwardRef(function LineChart(props: LineChartProps, ref
<ChartsTooltip {...tooltip} slots={slots} slotProps={slotProps} />
<ChartsClipPath id={clipPathId} />
{children}
<ChartsOverlay loading={loading} slots={slots} slotProps={slotProps} />
</ResponsiveChartContainer>
);
});