diff --git a/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveChart.tsx b/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveChart.tsx index c3332cdb80..ac8488dd01 100644 --- a/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveChart.tsx +++ b/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveChart.tsx @@ -15,10 +15,11 @@ import { TimeScale, Chart as ChartJS, Filler, + type TooltipItem, } from 'chart.js'; import { useLocationSettings } from 'hooks/useLocationSettings'; import type { TooltipState } from 'component/insights/components/LineChart/ChartTooltip/ChartTooltip'; -import type { WeekData, RawWeekData } from './types.ts'; +import type { WeekData, RawWeekData, BatchedWeekData } from './types.ts'; import { createTooltip } from 'component/insights/components/LineChart/createTooltip.ts'; import { CreationArchiveRatioTooltip } from './CreationArchiveRatioTooltip.tsx'; import { getDateFnsLocale } from '../../getDateFnsLocale.ts'; @@ -27,6 +28,8 @@ import { NotEnoughData } from 'component/insights/components/LineChart/LineChart import { placeholderData } from './placeholderData.ts'; import { Bar } from 'react-chartjs-2'; import { GraphCover } from 'component/insights/GraphCover.tsx'; +import { format, startOfWeek } from 'date-fns'; +import { batchWeekData } from './batchWeekData.ts'; ChartJS.register( CategoryScale, @@ -47,6 +50,8 @@ interface ICreationArchiveChartProps { isLoading?: boolean; } +type DataResult = 'Not Enough Data' | 'Batched' | 'Weekly'; + export const CreationArchiveChart: FC = ({ creationArchiveTrends, isLoading, @@ -56,7 +61,7 @@ export const CreationArchiveChart: FC = ({ const { locationSettings } = useLocationSettings(); const [tooltip, setTooltip] = useState(null); - const { notEnoughData, aggregateOrProjectData } = useMemo(() => { + const { dataResult, aggregateOrProjectData } = useMemo(() => { const labels: string[] = Array.from( new Set( creationVsArchivedChart.datasets.flatMap((d) => @@ -65,19 +70,21 @@ export const CreationArchiveChart: FC = ({ ), ); - const aggregateWeekData = (acc: WeekData, item: RawWeekData) => { - if (item) { - acc.archivedFlags += item.archivedFlags || 0; + const aggregateWeekData = (acc: WeekData, item?: RawWeekData) => { + if (!item) return acc; - if (item.createdFlags) { - Object.entries(item.createdFlags).forEach(([_, count]) => { - acc.totalCreatedFlags += count; - }); - } + acc.archivedFlags += item.archivedFlags || 0; + + if (item.createdFlags) { + Object.entries(item.createdFlags).forEach(([_, count]) => { + acc.totalCreatedFlags += count; + }); } + if (!acc.date) { - acc.date = item?.date; + acc.date = item.date; } + return acc; }; @@ -86,6 +93,7 @@ export const CreationArchiveChart: FC = ({ totalCreatedFlags: 0, archivePercentage: 0, week: label, + date: '', }); const weeks: WeekData[] = labels @@ -103,13 +111,23 @@ export const CreationArchiveChart: FC = ({ })) .sort((a, b) => (a.week > b.week ? 1 : -1)); + let dataResult: DataResult = 'Weekly'; + let displayData: WeekData[] | BatchedWeekData[] = weeks; + + if (weeks.length < 2) { + dataResult = 'Not Enough Data'; + } else if (weeks.length >= 12) { + dataResult = 'Batched'; + displayData = batchWeekData(weeks); + } + return { - notEnoughData: weeks.length < 2, + dataResult, aggregateOrProjectData: { datasets: [ { label: 'Flags archived', - data: weeks, + data: displayData, backgroundColor: theme.palette.charts.A2, borderColor: theme.palette.charts.A2, hoverBackgroundColor: theme.palette.charts.A2, @@ -122,7 +140,7 @@ export const CreationArchiveChart: FC = ({ }, { label: 'Flags created', - data: weeks, + data: displayData, backgroundColor: theme.palette.charts.A1, borderColor: theme.palette.charts.A1, hoverBackgroundColor: theme.palette.charts.A1, @@ -138,10 +156,26 @@ export const CreationArchiveChart: FC = ({ }; }, [creationVsArchivedChart, theme]); - const useGraphCover = notEnoughData || isLoading; - const showNotEnoughDataText = notEnoughData && !isLoading; + const useGraphCover = dataResult === 'Not Enough Data' || isLoading; + const showNotEnoughDataText = + dataResult === 'Not Enough Data' && !isLoading; const data = useGraphCover ? placeholderData : aggregateOrProjectData; + const locale = getDateFnsLocale(locationSettings.locale); + const batchedTooltipTitle = (datapoints: TooltipItem[]) => { + const rawData = datapoints[0].raw as BatchedWeekData; + const startDate = format( + startOfWeek(new Date(rawData.date), { + locale, + weekStartsOn: 1, + }), + `PP`, + { locale }, + ); + const endDate = format(new Date(rawData.endDate), `PP`, { locale }); + return `${startDate} – ${endDate}`; + }; + const options = useMemo( () => ({ responsive: true, @@ -172,6 +206,12 @@ export const CreationArchiveChart: FC = ({ enabled: false, position: 'average' as const, external: createTooltip(setTooltip), + callbacks: { + title: + dataResult === 'Batched' + ? batchedTooltipTitle + : undefined, + }, }, }, locale: locationSettings.locale, @@ -179,20 +219,26 @@ export const CreationArchiveChart: FC = ({ x: { adapters: { date: { - locale: getDateFnsLocale(locationSettings.locale), + locale, }, }, type: 'time' as const, display: true, time: { - unit: 'week' as const, + unit: + dataResult === 'Batched' + ? ('month' as const) + : ('week' as const), tooltipFormat: 'P', }, grid: { display: false, }, ticks: { - source: 'data' as const, + source: + dataResult === 'Batched' + ? ('auto' as const) + : ('data' as const), display: !useGraphCover, }, }, diff --git a/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveRatioTooltip.tsx b/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveRatioTooltip.tsx index 3a33da08e7..4250f72d7c 100644 --- a/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveRatioTooltip.tsx +++ b/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveRatioTooltip.tsx @@ -44,6 +44,7 @@ interface CreationArchiveRatioTooltipProps { } const Timestamp = styled('span')(({ theme }) => ({ + whiteSpace: 'nowrap', fontSize: theme.typography.body2.fontSize, color: theme.palette.text.secondary, })); diff --git a/frontend/src/component/insights/componentsChart/CreationArchiveChart/batchWeekData.test.ts b/frontend/src/component/insights/componentsChart/CreationArchiveChart/batchWeekData.test.ts new file mode 100644 index 0000000000..abd319f571 --- /dev/null +++ b/frontend/src/component/insights/componentsChart/CreationArchiveChart/batchWeekData.test.ts @@ -0,0 +1,79 @@ +import { batchWeekData } from './batchWeekData.ts'; + +it('handles empty input', () => { + expect(batchWeekData([])).toEqual([]); +}); + +it('handles a single data point', () => { + const input = { + archivedFlags: 5, + totalCreatedFlags: 1, + archivePercentage: 500, + week: '50', + date: '2022-01-01', + }; + expect(batchWeekData([input])).toStrictEqual([ + { + archivedFlags: 5, + totalCreatedFlags: 1, + archivePercentage: 500, + date: input.date, + endDate: input.date, + }, + ]); +}); +it('batches by 4, starting from the first entry', () => { + const input = [ + { + archivedFlags: 1, + totalCreatedFlags: 1, + archivePercentage: 100, + week: '50', + date: '2022-01-01', + }, + { + archivedFlags: 5, + totalCreatedFlags: 1, + archivePercentage: 500, + week: '50', + date: '2022-02-02', + }, + { + archivedFlags: 3, + totalCreatedFlags: 0, + archivePercentage: 0, + week: '50', + date: '2022-03-03', + }, + { + archivedFlags: 3, + totalCreatedFlags: 4, + archivePercentage: 75, + week: '50', + date: '2022-04-04', + }, + { + archivedFlags: 3, + totalCreatedFlags: 2, + archivePercentage: 150, + week: '50', + date: '2022-05-05', + }, + ]; + expect(batchWeekData(input)).toStrictEqual([ + { + archivedFlags: 12, + totalCreatedFlags: 6, + archivePercentage: 200, + date: '2022-01-01', + endDate: '2022-04-04', + }, + { + archivedFlags: 3, + totalCreatedFlags: 2, + archivePercentage: 150, + date: '2022-05-05', + endDate: '2022-05-05', + }, + ]); +}); diff --git a/frontend/src/component/insights/componentsChart/CreationArchiveChart/batchWeekData.ts b/frontend/src/component/insights/componentsChart/CreationArchiveChart/batchWeekData.ts new file mode 100644 index 0000000000..b063ac6e0c --- /dev/null +++ b/frontend/src/component/insights/componentsChart/CreationArchiveChart/batchWeekData.ts @@ -0,0 +1,29 @@ +import type { BatchedWeekData, WeekData } from './types.ts'; + +const batchSize = 4; + +export const batchWeekData = (weeks: WeekData[]): BatchedWeekData[] => + weeks.reduce((acc, curr, index) => { + const currentAggregatedIndex = Math.floor(index / batchSize); + + const data = acc[currentAggregatedIndex]; + + if (data) { + data.totalCreatedFlags += curr.totalCreatedFlags; + data.archivedFlags += curr.archivedFlags; + + data.archivePercentage = + data.totalCreatedFlags > 0 + ? (data.archivedFlags / data.totalCreatedFlags) * 100 + : 0; + + data.endDate = curr.date; + } else { + const { week: _, ...shared } = curr; + acc[currentAggregatedIndex] = { + ...shared, + endDate: curr.date, + }; + } + return acc; + }, [] as BatchedWeekData[]); diff --git a/frontend/src/component/insights/componentsChart/CreationArchiveChart/types.ts b/frontend/src/component/insights/componentsChart/CreationArchiveChart/types.ts index 3a3570aa95..2d368bd5a6 100644 --- a/frontend/src/component/insights/componentsChart/CreationArchiveChart/types.ts +++ b/frontend/src/component/insights/componentsChart/CreationArchiveChart/types.ts @@ -3,7 +3,7 @@ export type WeekData = { totalCreatedFlags: number; archivePercentage: number; week: string; - date?: string; + date: string; }; export type RawWeekData = { @@ -12,3 +12,7 @@ export type RawWeekData = { week: string; date: string; }; + +export type BatchedWeekData = Omit & { + endDate: string; +}; diff --git a/frontend/src/component/insights/hooks/useInsightsData.ts b/frontend/src/component/insights/hooks/useInsightsData.ts index 6276396f85..081d373e73 100644 --- a/frontend/src/component/insights/hooks/useInsightsData.ts +++ b/frontend/src/component/insights/hooks/useInsightsData.ts @@ -12,21 +12,12 @@ export const useInsightsData = ( const allMetricsDatapoints = useAllDatapoints( instanceInsights.metricsSummaryTrends, ); + const projectsData = useFilteredTrends( instanceInsights.projectFlagTrends, projects, ); - const lifecycleData = useFilteredTrends( - instanceInsights.lifecycleTrends, - projects, - ); - - const creationArchiveData = useFilteredTrends( - instanceInsights.creationArchiveTrends, - projects, - ); - const groupedProjectsData = useGroupedProjectTrends(projectsData); const metricsData = useFilteredTrends( @@ -37,8 +28,16 @@ export const useInsightsData = ( const summary = useFilteredFlagsSummary(projectsData); + const lifecycleData = useFilteredTrends( + instanceInsights.lifecycleTrends, + projects, + ); const groupedLifecycleData = useGroupedProjectTrends(lifecycleData); + const creationArchiveData = useFilteredTrends( + instanceInsights.creationArchiveTrends, + projects, + ); const groupedCreationArchiveData = useGroupedProjectTrends(creationArchiveData); diff --git a/frontend/src/component/insights/sections/PerformanceInsights.tsx b/frontend/src/component/insights/sections/PerformanceInsights.tsx index 4e1caf9ba1..9596edd8b2 100644 --- a/frontend/src/component/insights/sections/PerformanceInsights.tsx +++ b/frontend/src/component/insights/sections/PerformanceInsights.tsx @@ -71,13 +71,6 @@ export const PerformanceInsights: FC = () => { const lastFlagTrend = flagTrends[flagTrends.length - 1]; const flagsTotal = lastFlagTrend?.total ?? 0; - function getFlagsPerUser(flagsTotal: number, usersTotal: number) { - const flagsPerUserCalculation = flagsTotal / usersTotal; - return Number.isNaN(flagsPerUserCalculation) - ? 'N/A' - : flagsPerUserCalculation.toFixed(2); - } - const isLifecycleGraphsEnabled = useUiFlag('lifecycleGraphs'); return (