mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: batch week data if the shown time span is greater than 12 weeks. (#10745)
Implements batching of data points in the archived:created chart: when there's 12 or more weeks of data, batch data into batches of 4 weeks at a time. When we batch data, we also switch the labeling to be month-based and auto-generated (cf the inline comment with more details). <img width="798" height="317" alt="image" src="https://github.com/user-attachments/assets/068ee528-a6d6-4aaf-ac81-c729c2c813d1" /> The current implementation batches into groups of 4 weeks, but this can easily be parameterized to support arbitrary batch sizes. Because of the batching, we also now need to adjust the tooltip title in those cases. This is handled by a callback.
This commit is contained in:
		
							parent
							
								
									5e2d95e0be
								
							
						
					
					
						commit
						28d7672a58
					
				| @ -15,10 +15,11 @@ import { | |||||||
|     TimeScale, |     TimeScale, | ||||||
|     Chart as ChartJS, |     Chart as ChartJS, | ||||||
|     Filler, |     Filler, | ||||||
|  |     type TooltipItem, | ||||||
| } from 'chart.js'; | } from 'chart.js'; | ||||||
| import { useLocationSettings } from 'hooks/useLocationSettings'; | import { useLocationSettings } from 'hooks/useLocationSettings'; | ||||||
| import type { TooltipState } from 'component/insights/components/LineChart/ChartTooltip/ChartTooltip'; | 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 { createTooltip } from 'component/insights/components/LineChart/createTooltip.ts'; | ||||||
| import { CreationArchiveRatioTooltip } from './CreationArchiveRatioTooltip.tsx'; | import { CreationArchiveRatioTooltip } from './CreationArchiveRatioTooltip.tsx'; | ||||||
| import { getDateFnsLocale } from '../../getDateFnsLocale.ts'; | import { getDateFnsLocale } from '../../getDateFnsLocale.ts'; | ||||||
| @ -27,6 +28,8 @@ import { NotEnoughData } from 'component/insights/components/LineChart/LineChart | |||||||
| import { placeholderData } from './placeholderData.ts'; | import { placeholderData } from './placeholderData.ts'; | ||||||
| import { Bar } from 'react-chartjs-2'; | import { Bar } from 'react-chartjs-2'; | ||||||
| import { GraphCover } from 'component/insights/GraphCover.tsx'; | import { GraphCover } from 'component/insights/GraphCover.tsx'; | ||||||
|  | import { format, startOfWeek } from 'date-fns'; | ||||||
|  | import { batchWeekData } from './batchWeekData.ts'; | ||||||
| 
 | 
 | ||||||
| ChartJS.register( | ChartJS.register( | ||||||
|     CategoryScale, |     CategoryScale, | ||||||
| @ -47,6 +50,8 @@ interface ICreationArchiveChartProps { | |||||||
|     isLoading?: boolean; |     isLoading?: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | type DataResult = 'Not Enough Data' | 'Batched' | 'Weekly'; | ||||||
|  | 
 | ||||||
| export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({ | export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({ | ||||||
|     creationArchiveTrends, |     creationArchiveTrends, | ||||||
|     isLoading, |     isLoading, | ||||||
| @ -56,7 +61,7 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({ | |||||||
|     const { locationSettings } = useLocationSettings(); |     const { locationSettings } = useLocationSettings(); | ||||||
|     const [tooltip, setTooltip] = useState<null | TooltipState>(null); |     const [tooltip, setTooltip] = useState<null | TooltipState>(null); | ||||||
| 
 | 
 | ||||||
|     const { notEnoughData, aggregateOrProjectData } = useMemo(() => { |     const { dataResult, aggregateOrProjectData } = useMemo(() => { | ||||||
|         const labels: string[] = Array.from( |         const labels: string[] = Array.from( | ||||||
|             new Set( |             new Set( | ||||||
|                 creationVsArchivedChart.datasets.flatMap((d) => |                 creationVsArchivedChart.datasets.flatMap((d) => | ||||||
| @ -65,8 +70,9 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({ | |||||||
|             ), |             ), | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         const aggregateWeekData = (acc: WeekData, item: RawWeekData) => { |         const aggregateWeekData = (acc: WeekData, item?: RawWeekData) => { | ||||||
|             if (item) { |             if (!item) return acc; | ||||||
|  | 
 | ||||||
|             acc.archivedFlags += item.archivedFlags || 0; |             acc.archivedFlags += item.archivedFlags || 0; | ||||||
| 
 | 
 | ||||||
|             if (item.createdFlags) { |             if (item.createdFlags) { | ||||||
| @ -74,10 +80,11 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({ | |||||||
|                     acc.totalCreatedFlags += count; |                     acc.totalCreatedFlags += count; | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|             } | 
 | ||||||
|             if (!acc.date) { |             if (!acc.date) { | ||||||
|                 acc.date = item?.date; |                 acc.date = item.date; | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|             return acc; |             return acc; | ||||||
|         }; |         }; | ||||||
| 
 | 
 | ||||||
| @ -86,6 +93,7 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({ | |||||||
|             totalCreatedFlags: 0, |             totalCreatedFlags: 0, | ||||||
|             archivePercentage: 0, |             archivePercentage: 0, | ||||||
|             week: label, |             week: label, | ||||||
|  |             date: '', | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         const weeks: WeekData[] = labels |         const weeks: WeekData[] = labels | ||||||
| @ -103,13 +111,23 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({ | |||||||
|             })) |             })) | ||||||
|             .sort((a, b) => (a.week > b.week ? 1 : -1)); |             .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 { |         return { | ||||||
|             notEnoughData: weeks.length < 2, |             dataResult, | ||||||
|             aggregateOrProjectData: { |             aggregateOrProjectData: { | ||||||
|                 datasets: [ |                 datasets: [ | ||||||
|                     { |                     { | ||||||
|                         label: 'Flags archived', |                         label: 'Flags archived', | ||||||
|                         data: weeks, |                         data: displayData, | ||||||
|                         backgroundColor: theme.palette.charts.A2, |                         backgroundColor: theme.palette.charts.A2, | ||||||
|                         borderColor: theme.palette.charts.A2, |                         borderColor: theme.palette.charts.A2, | ||||||
|                         hoverBackgroundColor: theme.palette.charts.A2, |                         hoverBackgroundColor: theme.palette.charts.A2, | ||||||
| @ -122,7 +140,7 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({ | |||||||
|                     }, |                     }, | ||||||
|                     { |                     { | ||||||
|                         label: 'Flags created', |                         label: 'Flags created', | ||||||
|                         data: weeks, |                         data: displayData, | ||||||
|                         backgroundColor: theme.palette.charts.A1, |                         backgroundColor: theme.palette.charts.A1, | ||||||
|                         borderColor: theme.palette.charts.A1, |                         borderColor: theme.palette.charts.A1, | ||||||
|                         hoverBackgroundColor: theme.palette.charts.A1, |                         hoverBackgroundColor: theme.palette.charts.A1, | ||||||
| @ -138,10 +156,26 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({ | |||||||
|         }; |         }; | ||||||
|     }, [creationVsArchivedChart, theme]); |     }, [creationVsArchivedChart, theme]); | ||||||
| 
 | 
 | ||||||
|     const useGraphCover = notEnoughData || isLoading; |     const useGraphCover = dataResult === 'Not Enough Data' || isLoading; | ||||||
|     const showNotEnoughDataText = notEnoughData && !isLoading; |     const showNotEnoughDataText = | ||||||
|  |         dataResult === 'Not Enough Data' && !isLoading; | ||||||
|     const data = useGraphCover ? placeholderData : aggregateOrProjectData; |     const data = useGraphCover ? placeholderData : aggregateOrProjectData; | ||||||
| 
 | 
 | ||||||
|  |     const locale = getDateFnsLocale(locationSettings.locale); | ||||||
|  |     const batchedTooltipTitle = (datapoints: TooltipItem<any>[]) => { | ||||||
|  |         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( |     const options = useMemo( | ||||||
|         () => ({ |         () => ({ | ||||||
|             responsive: true, |             responsive: true, | ||||||
| @ -172,6 +206,12 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({ | |||||||
|                     enabled: false, |                     enabled: false, | ||||||
|                     position: 'average' as const, |                     position: 'average' as const, | ||||||
|                     external: createTooltip(setTooltip), |                     external: createTooltip(setTooltip), | ||||||
|  |                     callbacks: { | ||||||
|  |                         title: | ||||||
|  |                             dataResult === 'Batched' | ||||||
|  |                                 ? batchedTooltipTitle | ||||||
|  |                                 : undefined, | ||||||
|  |                     }, | ||||||
|                 }, |                 }, | ||||||
|             }, |             }, | ||||||
|             locale: locationSettings.locale, |             locale: locationSettings.locale, | ||||||
| @ -179,20 +219,26 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({ | |||||||
|                 x: { |                 x: { | ||||||
|                     adapters: { |                     adapters: { | ||||||
|                         date: { |                         date: { | ||||||
|                             locale: getDateFnsLocale(locationSettings.locale), |                             locale, | ||||||
|                         }, |                         }, | ||||||
|                     }, |                     }, | ||||||
|                     type: 'time' as const, |                     type: 'time' as const, | ||||||
|                     display: true, |                     display: true, | ||||||
|                     time: { |                     time: { | ||||||
|                         unit: 'week' as const, |                         unit: | ||||||
|  |                             dataResult === 'Batched' | ||||||
|  |                                 ? ('month' as const) | ||||||
|  |                                 : ('week' as const), | ||||||
|                         tooltipFormat: 'P', |                         tooltipFormat: 'P', | ||||||
|                     }, |                     }, | ||||||
|                     grid: { |                     grid: { | ||||||
|                         display: false, |                         display: false, | ||||||
|                     }, |                     }, | ||||||
|                     ticks: { |                     ticks: { | ||||||
|                         source: 'data' as const, |                         source: | ||||||
|  |                             dataResult === 'Batched' | ||||||
|  |                                 ? ('auto' as const) | ||||||
|  |                                 : ('data' as const), | ||||||
|                         display: !useGraphCover, |                         display: !useGraphCover, | ||||||
|                     }, |                     }, | ||||||
|                 }, |                 }, | ||||||
|  | |||||||
| @ -44,6 +44,7 @@ interface CreationArchiveRatioTooltipProps { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| const Timestamp = styled('span')(({ theme }) => ({ | const Timestamp = styled('span')(({ theme }) => ({ | ||||||
|  |     whiteSpace: 'nowrap', | ||||||
|     fontSize: theme.typography.body2.fontSize, |     fontSize: theme.typography.body2.fontSize, | ||||||
|     color: theme.palette.text.secondary, |     color: theme.palette.text.secondary, | ||||||
| })); | })); | ||||||
|  | |||||||
| @ -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', | ||||||
|  |         }, | ||||||
|  |     ]); | ||||||
|  | }); | ||||||
| @ -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[]); | ||||||
| @ -3,7 +3,7 @@ export type WeekData = { | |||||||
|     totalCreatedFlags: number; |     totalCreatedFlags: number; | ||||||
|     archivePercentage: number; |     archivePercentage: number; | ||||||
|     week: string; |     week: string; | ||||||
|     date?: string; |     date: string; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export type RawWeekData = { | export type RawWeekData = { | ||||||
| @ -12,3 +12,7 @@ export type RawWeekData = { | |||||||
|     week: string; |     week: string; | ||||||
|     date: string; |     date: string; | ||||||
| }; | }; | ||||||
|  | 
 | ||||||
|  | export type BatchedWeekData = Omit<WeekData, 'week'> & { | ||||||
|  |     endDate: string; | ||||||
|  | }; | ||||||
|  | |||||||
| @ -12,21 +12,12 @@ export const useInsightsData = ( | |||||||
|     const allMetricsDatapoints = useAllDatapoints( |     const allMetricsDatapoints = useAllDatapoints( | ||||||
|         instanceInsights.metricsSummaryTrends, |         instanceInsights.metricsSummaryTrends, | ||||||
|     ); |     ); | ||||||
|  | 
 | ||||||
|     const projectsData = useFilteredTrends( |     const projectsData = useFilteredTrends( | ||||||
|         instanceInsights.projectFlagTrends, |         instanceInsights.projectFlagTrends, | ||||||
|         projects, |         projects, | ||||||
|     ); |     ); | ||||||
| 
 | 
 | ||||||
|     const lifecycleData = useFilteredTrends( |  | ||||||
|         instanceInsights.lifecycleTrends, |  | ||||||
|         projects, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     const creationArchiveData = useFilteredTrends( |  | ||||||
|         instanceInsights.creationArchiveTrends, |  | ||||||
|         projects, |  | ||||||
|     ); |  | ||||||
| 
 |  | ||||||
|     const groupedProjectsData = useGroupedProjectTrends(projectsData); |     const groupedProjectsData = useGroupedProjectTrends(projectsData); | ||||||
| 
 | 
 | ||||||
|     const metricsData = useFilteredTrends( |     const metricsData = useFilteredTrends( | ||||||
| @ -37,8 +28,16 @@ export const useInsightsData = ( | |||||||
| 
 | 
 | ||||||
|     const summary = useFilteredFlagsSummary(projectsData); |     const summary = useFilteredFlagsSummary(projectsData); | ||||||
| 
 | 
 | ||||||
|  |     const lifecycleData = useFilteredTrends( | ||||||
|  |         instanceInsights.lifecycleTrends, | ||||||
|  |         projects, | ||||||
|  |     ); | ||||||
|     const groupedLifecycleData = useGroupedProjectTrends(lifecycleData); |     const groupedLifecycleData = useGroupedProjectTrends(lifecycleData); | ||||||
| 
 | 
 | ||||||
|  |     const creationArchiveData = useFilteredTrends( | ||||||
|  |         instanceInsights.creationArchiveTrends, | ||||||
|  |         projects, | ||||||
|  |     ); | ||||||
|     const groupedCreationArchiveData = |     const groupedCreationArchiveData = | ||||||
|         useGroupedProjectTrends(creationArchiveData); |         useGroupedProjectTrends(creationArchiveData); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -71,13 +71,6 @@ export const PerformanceInsights: FC = () => { | |||||||
|     const lastFlagTrend = flagTrends[flagTrends.length - 1]; |     const lastFlagTrend = flagTrends[flagTrends.length - 1]; | ||||||
|     const flagsTotal = lastFlagTrend?.total ?? 0; |     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'); |     const isLifecycleGraphsEnabled = useUiFlag('lifecycleGraphs'); | ||||||
| 
 | 
 | ||||||
|     return ( |     return ( | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user