From 778eaa98731c7ba4bfccdcb924a7fd33bf599f5d Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 3 Sep 2025 09:02:10 +0200 Subject: [PATCH] chore: Add a cover for when we don't have enough data to show the chart (#10599) Adds a cover to the archived vs created flag chart if we don't have enough data or if the data is loading. Follows the pattern established in the other analytics charts. Because the values for a placeholder bar chart are different than for a placeholder line chart, I've added in actual placeholder values. This also makes the gives the chart in question two bars per category, even in the placeholder data, which matches nicely with the actual graph. Before: image After: image In context with the other charts: image --- .../src/component/insights/GraphCover.tsx | 32 ++++++ .../LineChart/LineChartComponent.tsx | 29 +----- .../CreationArchiveChart.tsx | 97 +++++++++++-------- .../CreationArchiveChart/placeholderData.ts | 63 ++++++++++++ 4 files changed, 154 insertions(+), 67 deletions(-) create mode 100644 frontend/src/component/insights/GraphCover.tsx create mode 100644 frontend/src/component/insights/componentsChart/CreationArchiveChart/placeholderData.ts diff --git a/frontend/src/component/insights/GraphCover.tsx b/frontend/src/component/insights/GraphCover.tsx new file mode 100644 index 0000000000..915acb7ac9 --- /dev/null +++ b/frontend/src/component/insights/GraphCover.tsx @@ -0,0 +1,32 @@ +import { styled } from '@mui/material'; +import type { FC, PropsWithChildren } from 'react'; + +const StyledCover = styled('div')(({ theme }) => ({ + position: 'absolute', + inset: 0, + display: 'flex', + zIndex: theme.zIndex.appBar, + '&::before': { + zIndex: theme.zIndex.fab, + content: '""', + position: 'absolute', + inset: 0, + backgroundColor: theme.palette.background.paper, + opacity: 0.8, + }, +})); + +const StyledCoverContent = styled('div')(({ theme }) => ({ + zIndex: theme.zIndex.modal, + margin: 'auto', + color: theme.palette.text.secondary, + textAlign: 'center', +})); + +export const GraphCover: FC = ({ children }) => { + return ( + + {children} + + ); +}; diff --git a/frontend/src/component/insights/components/LineChart/LineChartComponent.tsx b/frontend/src/component/insights/components/LineChart/LineChartComponent.tsx index 5c684742bc..f9f71688bc 100644 --- a/frontend/src/component/insights/components/LineChart/LineChartComponent.tsx +++ b/frontend/src/component/insights/components/LineChart/LineChartComponent.tsx @@ -26,33 +26,12 @@ import { styled } from '@mui/material'; import { createOptions } from './createChartOptions.ts'; import merge from 'deepmerge'; import { customHighlightPlugin } from 'component/common/Chart/customHighlightPlugin.ts'; +import { GraphCover } from 'component/insights/GraphCover.tsx'; const StyledContainer = styled('div')(({ theme }) => ({ position: 'relative', })); -const StyledCover = styled('div')(({ theme }) => ({ - position: 'absolute', - inset: 0, - display: 'flex', - zIndex: theme.zIndex.appBar, - '&::before': { - zIndex: theme.zIndex.fab, - content: '""', - position: 'absolute', - inset: 0, - backgroundColor: theme.palette.background.paper, - opacity: 0.8, - }, -})); - -const StyledCoverContent = styled('div')(({ theme }) => ({ - zIndex: theme.zIndex.modal, - margin: 'auto', - color: theme.palette.text.secondary, - textAlign: 'center', -})); - function mergeAll(objects: Partial[]): T { return merge.all(objects.filter((i) => i)); } @@ -113,11 +92,7 @@ const LineChartComponent: FC<{ ) } elseShow={ - - - {cover !== true ? cover : ' '} - - + {cover !== true ? cover : ' '} } /> diff --git a/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveChart.tsx b/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveChart.tsx index 87652e1433..413e6738b8 100644 --- a/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveChart.tsx +++ b/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveChart.tsx @@ -4,7 +4,6 @@ import type { InstanceInsightsSchema } from 'openapi'; import { useProjectChartData } from 'component/insights/hooks/useProjectChartData'; import { useTheme } from '@mui/material'; import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends'; -import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData'; import { CategoryScale, LinearScale, @@ -22,9 +21,12 @@ import type { TooltipState } from 'component/insights/components/LineChart/Chart import type { WeekData, RawWeekData } from './types.ts'; import { createTooltip } from 'component/insights/components/LineChart/createTooltip.ts'; import { CreationArchiveRatioTooltip } from './CreationArchiveRatioTooltip.tsx'; -import { Chart } from 'react-chartjs-2'; import { getDateFnsLocale } from '../../getDateFnsLocale.ts'; import { customHighlightPlugin } from 'component/common/Chart/customHighlightPlugin.ts'; +import { NotEnoughData } from 'component/insights/components/LineChart/LineChart.tsx'; +import { placeholderData } from './placeholderData.ts'; +import { Bar } from 'react-chartjs-2'; +import { GraphCover } from 'component/insights/GraphCover.tsx'; ChartJS.register( CategoryScale, @@ -51,11 +53,10 @@ export const CreationArchiveChart: FC = ({ }) => { const creationVsArchivedChart = useProjectChartData(creationArchiveTrends); const theme = useTheme(); - const placeholderData = usePlaceholderData(); const { locationSettings } = useLocationSettings(); const [tooltip, setTooltip] = useState(null); - const aggregateOrProjectData = useMemo(() => { + const { notEnoughData, aggregateOrProjectData } = useMemo(() => { const labels: string[] = Array.from( new Set( creationVsArchivedChart.datasets.flatMap((d) => @@ -103,46 +104,53 @@ export const CreationArchiveChart: FC = ({ .sort((a, b) => (a.week > b.week ? 1 : -1)); return { - datasets: [ - { - label: 'Flags archived', - data: weeks, - backgroundColor: theme.palette.charts.A2, - borderColor: theme.palette.charts.A2, - hoverBackgroundColor: theme.palette.charts.A2, - hoverBorderColor: theme.palette.charts.A2, - parsing: { yAxisKey: 'archivedFlags', xAxisKey: 'date' }, - order: 1, - }, - { - label: 'Flags created', - data: weeks, - backgroundColor: theme.palette.charts.A1, - borderColor: theme.palette.charts.A1, - hoverBackgroundColor: theme.palette.charts.A1, - hoverBorderColor: theme.palette.charts.A1, - parsing: { - yAxisKey: 'totalCreatedFlags', - xAxisKey: 'date', + notEnoughData: weeks.length < 2, + aggregateOrProjectData: { + datasets: [ + { + label: 'Flags archived', + data: weeks, + backgroundColor: theme.palette.charts.A2, + borderColor: theme.palette.charts.A2, + hoverBackgroundColor: theme.palette.charts.A2, + hoverBorderColor: theme.palette.charts.A2, + parsing: { + yAxisKey: 'archivedFlags', + xAxisKey: 'date', + }, + order: 1, }, - order: 2, - }, - ], + { + label: 'Flags created', + data: weeks, + backgroundColor: theme.palette.charts.A1, + borderColor: theme.palette.charts.A1, + hoverBackgroundColor: theme.palette.charts.A1, + hoverBorderColor: theme.palette.charts.A1, + parsing: { + yAxisKey: 'totalCreatedFlags', + xAxisKey: 'date', + }, + order: 2, + }, + ], + }, }; }, [creationVsArchivedChart, theme]); - const notEnoughData = useMemo( - () => - !isLoading && - !creationVsArchivedChart.datasets.some((d) => d.data.length > 1), - [creationVsArchivedChart, isLoading], - ); - const data = - notEnoughData || isLoading ? placeholderData : aggregateOrProjectData; + const useGraphCover = notEnoughData || isLoading; + const data = useGraphCover ? placeholderData : aggregateOrProjectData; const options = useMemo( () => ({ responsive: true, + ...(useGraphCover + ? { + animation: { + duration: 0, + }, + } + : {}), interaction: { mode: 'index' as const, intersect: false, @@ -157,6 +165,7 @@ export const CreationArchiveChart: FC = ({ padding: 21, boxHeight: 8, }, + display: !useGraphCover, }, tooltip: { enabled: false, @@ -181,7 +190,10 @@ export const CreationArchiveChart: FC = ({ grid: { display: false, }, - ticks: { source: 'data' }, + ticks: { + source: 'data' as const, + display: !useGraphCover, + }, }, y: { type: 'linear' as const, @@ -193,17 +205,17 @@ export const CreationArchiveChart: FC = ({ }, ticks: { stepSize: 1, + display: !useGraphCover, }, }, }, }), - [theme, locationSettings, setTooltip], + [theme, locationSettings, setTooltip, useGraphCover], ); return ( <> - = ({ ]} /> + {useGraphCover ? ( + + {notEnoughData ? : isLoading} + + ) : null} ); }; diff --git a/frontend/src/component/insights/componentsChart/CreationArchiveChart/placeholderData.ts b/frontend/src/component/insights/componentsChart/CreationArchiveChart/placeholderData.ts new file mode 100644 index 0000000000..171f7ce2fe --- /dev/null +++ b/frontend/src/component/insights/componentsChart/CreationArchiveChart/placeholderData.ts @@ -0,0 +1,63 @@ +import type { ChartData } from 'chart.js'; +import type { WeekData } from './types.ts'; + +const data = [ + { + archivedFlags: 3, + totalCreatedFlags: 4, + archivePercentage: 75, + week: '2025-30', + date: '2025-07-27T01:00:00.000Z', + }, + { + archivedFlags: 7, + totalCreatedFlags: 3, + archivePercentage: 140, + week: '2025-31', + date: '2025-08-03T01:00:00.000Z', + }, + { + archivedFlags: 2, + totalCreatedFlags: 3, + archivePercentage: 50, + week: '2025-32', + date: '2025-08-10T01:00:00.000Z', + }, + { + archivedFlags: 2, + totalCreatedFlags: 6, + archivePercentage: 25, + week: '2025-33', + date: '2025-08-17T00:40:40.606Z', + }, + { + archivedFlags: 4, + totalCreatedFlags: 9, + archivePercentage: 66.66666666666666, + week: '2025-34', + date: '2025-08-24T00:29:19.583Z', + }, +]; + +export const placeholderData: ChartData = { + datasets: [ + { + label: 'Flags archived', + data, + parsing: { + yAxisKey: 'archivedFlags', + xAxisKey: 'date', + }, + order: 1, + }, + { + label: 'Flags created', + data, + parsing: { + yAxisKey: 'totalCreatedFlags', + xAxisKey: 'date', + }, + order: 2, + }, + ], +};