mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: Add batching functionality to new flags in production (#10756)
Adds the same batching functionality that was added to the archived:created chart to the new flags in production chart. In doing so, I've extracted the batching algorithm and the batched tooltip title creation, as well as the ChartDataResult type (though naming suggestions are still welcome on that front). Locale 'ja': <img width="1143" height="370" alt="image" src="https://github.com/user-attachments/assets/827b41c6-0e67-46f4-8f82-4ba12e2120bb" /> Locale 'no': <img width="1475" height="554" alt="image" src="https://github.com/user-attachments/assets/6125c318-25fb-42bd-a520-44e6a7f7ece7" />
This commit is contained in:
		
							parent
							
								
									cf018020df
								
							
						
					
					
						commit
						7044cd4b1a
					
				| @ -2,7 +2,7 @@ import { Box, Paper, styled, Typography } from '@mui/material'; | |||||||
| import type { TooltipItem } from 'chart.js'; | import type { TooltipItem } from 'chart.js'; | ||||||
| import { Truncator } from 'component/common/Truncator/Truncator'; | import { Truncator } from 'component/common/Truncator/Truncator'; | ||||||
| import type React from 'react'; | import type React from 'react'; | ||||||
| import type { FC, VFC } from 'react'; | import type { FC } from 'react'; | ||||||
| import { objectId } from 'utils/objectId'; | import { objectId } from 'utils/objectId'; | ||||||
| 
 | 
 | ||||||
| export type TooltipState = { | export type TooltipState = { | ||||||
| @ -91,12 +91,12 @@ export const ChartTooltipContainer: FC<IChartTooltipProps> = ({ | |||||||
|     </Box> |     </Box> | ||||||
| ); | ); | ||||||
| 
 | 
 | ||||||
| export const ChartTooltip: VFC<IChartTooltipProps> = ({ tooltip }) => ( | export const ChartTooltip: FC<IChartTooltipProps> = ({ tooltip }) => ( | ||||||
|     <ChartTooltipContainer tooltip={tooltip}> |     <ChartTooltipContainer tooltip={tooltip}> | ||||||
|         <Paper |         <Paper | ||||||
|             elevation={3} |             elevation={3} | ||||||
|             sx={(theme) => ({ |             sx={(theme) => ({ | ||||||
|                 width: 220, |                 width: 'max-content', | ||||||
|                 padding: theme.spacing(1.5, 2), |                 padding: theme.spacing(1.5, 2), | ||||||
|             })} |             })} | ||||||
|         > |         > | ||||||
|  | |||||||
| @ -15,7 +15,6 @@ 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'; | ||||||
| @ -28,8 +27,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 { batchCreationArchiveData } from './batchCreationArchiveData.ts'; | ||||||
| import { batchWeekData } from './batchWeekData.ts'; | import { useBatchedTooltipDate } from '../useBatchedTooltipDate.ts'; | ||||||
| 
 | 
 | ||||||
| ChartJS.register( | ChartJS.register( | ||||||
|     CategoryScale, |     CategoryScale, | ||||||
| @ -50,8 +49,6 @@ 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, | ||||||
| @ -111,14 +108,14 @@ 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 dataResult: ChartDataResult = 'Weekly'; | ||||||
|         let displayData: WeekData[] | BatchedWeekData[] = weeks; |         let displayData: WeekData[] | BatchedWeekData[] = weeks; | ||||||
| 
 | 
 | ||||||
|         if (weeks.length < 2) { |         if (weeks.length < 2) { | ||||||
|             dataResult = 'Not Enough Data'; |             dataResult = 'Not Enough Data'; | ||||||
|         } else if (weeks.length >= 12) { |         } else if (weeks.length >= 12) { | ||||||
|             dataResult = 'Batched'; |             dataResult = 'Batched'; | ||||||
|             displayData = batchWeekData(weeks); |             displayData = batchCreationArchiveData(weeks); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return { |         return { | ||||||
| @ -162,19 +159,7 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({ | |||||||
|     const data = useGraphCover ? placeholderData : aggregateOrProjectData; |     const data = useGraphCover ? placeholderData : aggregateOrProjectData; | ||||||
| 
 | 
 | ||||||
|     const locale = getDateFnsLocale(locationSettings.locale); |     const locale = getDateFnsLocale(locationSettings.locale); | ||||||
|     const batchedTooltipTitle = (datapoints: TooltipItem<any>[]) => { |     const batchedTooltipTitle = useBatchedTooltipDate(); | ||||||
|         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( | ||||||
|         () => ({ |         () => ({ | ||||||
|  | |||||||
| @ -0,0 +1,48 @@ | |||||||
|  | import { batchCreationArchiveData } from './batchCreationArchiveData.ts'; | ||||||
|  | 
 | ||||||
|  | it('handles a single data point', () => { | ||||||
|  |     const input = { | ||||||
|  |         archivedFlags: 5, | ||||||
|  |         totalCreatedFlags: 1, | ||||||
|  |         archivePercentage: 500, | ||||||
|  |         week: '50', | ||||||
|  |         date: '2022-01-01', | ||||||
|  |     }; | ||||||
|  |     expect(batchCreationArchiveData([input])).toStrictEqual([ | ||||||
|  |         { | ||||||
|  |             archivedFlags: 5, | ||||||
|  |             totalCreatedFlags: 1, | ||||||
|  |             archivePercentage: 500, | ||||||
|  |             date: input.date, | ||||||
|  |             endDate: input.date, | ||||||
|  |         }, | ||||||
|  |     ]); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('adds data in the expected way', () => { | ||||||
|  |     const input = [ | ||||||
|  |         { | ||||||
|  |             archivedFlags: 5, | ||||||
|  |             totalCreatedFlags: 1, | ||||||
|  |             archivePercentage: 500, | ||||||
|  |             week: '50', | ||||||
|  |             date: '2022-01-01', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             archivedFlags: 3, | ||||||
|  |             totalCreatedFlags: 3, | ||||||
|  |             archivePercentage: 150, | ||||||
|  |             week: '51', | ||||||
|  |             date: '2022-02-01', | ||||||
|  |         }, | ||||||
|  |     ]; | ||||||
|  |     expect(batchCreationArchiveData(input)).toStrictEqual([ | ||||||
|  |         { | ||||||
|  |             archivedFlags: 8, | ||||||
|  |             totalCreatedFlags: 4, | ||||||
|  |             archivePercentage: 200, | ||||||
|  |             date: '2022-01-01', | ||||||
|  |             endDate: '2022-02-01', | ||||||
|  |         }, | ||||||
|  |     ]); | ||||||
|  | }); | ||||||
| @ -0,0 +1,26 @@ | |||||||
|  | import { batchData } from '../batchData.ts'; | ||||||
|  | import type { BatchedWeekData, WeekData } from './types.ts'; | ||||||
|  | 
 | ||||||
|  | export const batchCreationArchiveData = batchData({ | ||||||
|  |     merge: (accumulated: BatchedWeekData, next: WeekData) => { | ||||||
|  |         accumulated.totalCreatedFlags += next.totalCreatedFlags; | ||||||
|  |         accumulated.archivedFlags += next.archivedFlags; | ||||||
|  | 
 | ||||||
|  |         accumulated.archivePercentage = | ||||||
|  |             accumulated.totalCreatedFlags > 0 | ||||||
|  |                 ? (accumulated.archivedFlags / accumulated.totalCreatedFlags) * | ||||||
|  |                   100 | ||||||
|  |                 : 0; | ||||||
|  | 
 | ||||||
|  |         accumulated.endDate = next.date; | ||||||
|  |         return accumulated; | ||||||
|  |     }, | ||||||
|  |     map: (item: WeekData) => { | ||||||
|  |         const { week: _, ...shared } = item; | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             ...shared, | ||||||
|  |             endDate: item.date, | ||||||
|  |         }; | ||||||
|  |     }, | ||||||
|  | }); | ||||||
| @ -1,79 +0,0 @@ | |||||||
| 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', |  | ||||||
|         }, |  | ||||||
|     ]); |  | ||||||
| }); |  | ||||||
| @ -1,29 +0,0 @@ | |||||||
| 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[]); |  | ||||||
| @ -10,6 +10,9 @@ import { | |||||||
| import { useTheme } from '@mui/material'; | import { useTheme } from '@mui/material'; | ||||||
| import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends'; | import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends'; | ||||||
| import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData'; | import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData'; | ||||||
|  | import type { BatchedWeekData, WeekData } from './types.ts'; | ||||||
|  | import { batchProductionFlagsData } from './batchProductionFlagsData.ts'; | ||||||
|  | import { useBatchedTooltipDate } from '../useBatchedTooltipDate.ts'; | ||||||
| 
 | 
 | ||||||
| interface IProjectHealthChartProps { | interface IProjectHealthChartProps { | ||||||
|     lifecycleTrends: GroupedDataByProject< |     lifecycleTrends: GroupedDataByProject< | ||||||
| @ -19,10 +22,43 @@ interface IProjectHealthChartProps { | |||||||
|     isLoading?: boolean; |     isLoading?: boolean; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type WeekData = { | const useOverrideOptions = (chartDataResult: ChartDataResult) => { | ||||||
|     newProductionFlags: number; |     const batchedTooltipTitle = useBatchedTooltipDate(); | ||||||
|     week: string; |     const sharedOptions = { | ||||||
|     date?: string; |         parsing: { | ||||||
|  |             yAxisKey: 'newProductionFlags', | ||||||
|  |             xAxisKey: 'date', | ||||||
|  |         }, | ||||||
|  |     }; | ||||||
|  |     switch (chartDataResult) { | ||||||
|  |         case 'Batched': { | ||||||
|  |             return { | ||||||
|  |                 ...sharedOptions, | ||||||
|  |                 scales: { | ||||||
|  |                     x: { | ||||||
|  |                         time: { | ||||||
|  |                             unit: 'month' as const, | ||||||
|  |                             tooltipFormat: 'P', | ||||||
|  |                         }, | ||||||
|  |                         ticks: { | ||||||
|  |                             source: 'auto' as const, | ||||||
|  |                         }, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 plugins: { | ||||||
|  |                     tooltip: { | ||||||
|  |                         callbacks: { | ||||||
|  |                             title: batchedTooltipTitle, | ||||||
|  |                         }, | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  |         case 'Weekly': | ||||||
|  |             return sharedOptions; | ||||||
|  |         case 'Not Enough Data': | ||||||
|  |             return {}; | ||||||
|  |     } | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| export const NewProductionFlagsChart: FC<IProjectHealthChartProps> = ({ | export const NewProductionFlagsChart: FC<IProjectHealthChartProps> = ({ | ||||||
| @ -34,7 +70,7 @@ export const NewProductionFlagsChart: FC<IProjectHealthChartProps> = ({ | |||||||
|     const theme = useTheme(); |     const theme = useTheme(); | ||||||
|     const placeholderData = usePlaceholderData(); |     const placeholderData = usePlaceholderData(); | ||||||
| 
 | 
 | ||||||
|     const aggregateHealthData = useMemo(() => { |     const { aggregateHealthData, chartDataResult } = useMemo(() => { | ||||||
|         const labels: string[] = Array.from( |         const labels: string[] = Array.from( | ||||||
|             new Set( |             new Set( | ||||||
|                 lifecycleData.datasets.flatMap((d) => |                 lifecycleData.datasets.flatMap((d) => | ||||||
| @ -65,46 +101,51 @@ export const NewProductionFlagsChart: FC<IProjectHealthChartProps> = ({ | |||||||
|                     ); |                     ); | ||||||
|             }) |             }) | ||||||
|             .sort((a, b) => (a.week > b.week ? 1 : -1)); |             .sort((a, b) => (a.week > b.week ? 1 : -1)); | ||||||
|  | 
 | ||||||
|  |         let chartDataResult: ChartDataResult = 'Weekly'; | ||||||
|  |         let displayData: WeekData[] | BatchedWeekData[] = weeks; | ||||||
|  | 
 | ||||||
|  |         if ( | ||||||
|  |             !isLoading && | ||||||
|  |             !lifecycleData.datasets.some((d) => d.data.length > 1) | ||||||
|  |         ) { | ||||||
|  |             chartDataResult = 'Not Enough Data'; | ||||||
|  |         } else if (weeks.length >= 12) { | ||||||
|  |             chartDataResult = 'Batched'; | ||||||
|  |             displayData = batchProductionFlagsData(weeks); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         return { |         return { | ||||||
|             datasets: [ |             chartDataResult, | ||||||
|                 { |             aggregateHealthData: { | ||||||
|                     label: 'Number of new flags', |                 datasets: [ | ||||||
|                     data: weeks, |                     { | ||||||
|                     borderColor: theme.palette.primary.light, |                         label: 'Number of new flags', | ||||||
|                     backgroundColor: fillGradientPrimary, |                         data: displayData, | ||||||
|                     fill: true, |                         borderColor: theme.palette.primary.light, | ||||||
|                     order: 3, |                         backgroundColor: fillGradientPrimary, | ||||||
|                 }, |                         fill: true, | ||||||
|             ], |                         order: 3, | ||||||
|  |                     }, | ||||||
|  |                 ], | ||||||
|  |             }, | ||||||
|         }; |         }; | ||||||
|     }, [lifecycleData, theme]); |     }, [lifecycleData, theme, isLoading]); | ||||||
| 
 | 
 | ||||||
|     const aggregateOrProjectData = isAggregate |     const aggregateOrProjectData = isAggregate | ||||||
|         ? aggregateHealthData |         ? aggregateHealthData | ||||||
|         : lifecycleData; |         : lifecycleData; | ||||||
|     const notEnoughData = useMemo( |     const notEnoughData = chartDataResult === 'Not Enough Data'; | ||||||
|         () => |  | ||||||
|             !isLoading && |  | ||||||
|             !lifecycleData.datasets.some((d) => d.data.length > 1), |  | ||||||
|         [lifecycleData, isLoading], |  | ||||||
|     ); |  | ||||||
|     const data = |     const data = | ||||||
|         notEnoughData || isLoading ? placeholderData : aggregateOrProjectData; |         notEnoughData || isLoading ? placeholderData : aggregateOrProjectData; | ||||||
| 
 | 
 | ||||||
|  |     const overrideOptions = useOverrideOptions(chartDataResult); | ||||||
|  | 
 | ||||||
|     return ( |     return ( | ||||||
|         <LineChart |         <LineChart | ||||||
|             key={isAggregate ? 'aggregate' : 'project'} |             key={isAggregate ? 'aggregate' : 'project'} | ||||||
|             data={data} |             data={data} | ||||||
|             overrideOptions={ |             overrideOptions={overrideOptions} | ||||||
|                 notEnoughData |  | ||||||
|                     ? {} |  | ||||||
|                     : { |  | ||||||
|                           parsing: { |  | ||||||
|                               yAxisKey: 'newProductionFlags', |  | ||||||
|                               xAxisKey: 'date', |  | ||||||
|                           }, |  | ||||||
|                       } |  | ||||||
|             } |  | ||||||
|             cover={notEnoughData ? <NotEnoughData /> : isLoading} |             cover={notEnoughData ? <NotEnoughData /> : isLoading} | ||||||
|         /> |         /> | ||||||
|     ); |     ); | ||||||
|  | |||||||
| @ -0,0 +1,38 @@ | |||||||
|  | import { batchProductionFlagsData } from './batchProductionFlagsData.ts'; | ||||||
|  | 
 | ||||||
|  | it('handles a single data point', () => { | ||||||
|  |     const input = { | ||||||
|  |         newProductionFlags: 5, | ||||||
|  |         week: '50', | ||||||
|  |         date: '2022-01-01', | ||||||
|  |     }; | ||||||
|  |     expect(batchProductionFlagsData([input])).toStrictEqual([ | ||||||
|  |         { | ||||||
|  |             newProductionFlags: 5, | ||||||
|  |             date: input.date, | ||||||
|  |             endDate: input.date, | ||||||
|  |         }, | ||||||
|  |     ]); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('adds data in the expected way', () => { | ||||||
|  |     const input = [ | ||||||
|  |         { | ||||||
|  |             newProductionFlags: 5, | ||||||
|  |             week: '50', | ||||||
|  |             date: '2022-01-01', | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             newProductionFlags: 6, | ||||||
|  |             week: '51', | ||||||
|  |             date: '2022-02-01', | ||||||
|  |         }, | ||||||
|  |     ]; | ||||||
|  |     expect(batchProductionFlagsData(input)).toStrictEqual([ | ||||||
|  |         { | ||||||
|  |             newProductionFlags: 11, | ||||||
|  |             date: '2022-01-01', | ||||||
|  |             endDate: '2022-02-01', | ||||||
|  |         }, | ||||||
|  |     ]); | ||||||
|  | }); | ||||||
| @ -0,0 +1,19 @@ | |||||||
|  | import { batchData } from '../batchData.ts'; | ||||||
|  | import type { BatchedWeekData, WeekData } from './types.ts'; | ||||||
|  | 
 | ||||||
|  | export const batchProductionFlagsData = batchData({ | ||||||
|  |     merge: (accumulated: BatchedWeekData, next: WeekData) => { | ||||||
|  |         accumulated.newProductionFlags += next.newProductionFlags; | ||||||
|  |         accumulated.endDate = next.date; | ||||||
|  | 
 | ||||||
|  |         return accumulated; | ||||||
|  |     }, | ||||||
|  |     map: (item: WeekData) => { | ||||||
|  |         const { week: _, ...shared } = item; | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             ...shared, | ||||||
|  |             endDate: item.date, | ||||||
|  |         }; | ||||||
|  |     }, | ||||||
|  | }); | ||||||
| @ -0,0 +1,9 @@ | |||||||
|  | export type WeekData = { | ||||||
|  |     newProductionFlags: number; | ||||||
|  |     week: string; | ||||||
|  |     date: string; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export type BatchedWeekData = Omit<WeekData, 'week'> & { | ||||||
|  |     endDate: string; | ||||||
|  | }; | ||||||
| @ -0,0 +1,15 @@ | |||||||
|  | import { batchData } from './batchData.ts'; | ||||||
|  | 
 | ||||||
|  | it('handles empty input', () => { | ||||||
|  |     expect(batchData({ merge: (x, _) => x, map: (x) => x })([])).toEqual([]); | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | it('batches by 4, starting from the first entry', () => { | ||||||
|  |     const input = [7, 11, 13, 19, 23]; | ||||||
|  | 
 | ||||||
|  |     expect( | ||||||
|  |         batchData<number, number>({ merge: (x, y) => x + y, map: (x) => x })( | ||||||
|  |             input, | ||||||
|  |         ), | ||||||
|  |     ).toStrictEqual([50, 23]); | ||||||
|  | }); | ||||||
							
								
								
									
										26
									
								
								frontend/src/component/insights/componentsChart/batchData.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								frontend/src/component/insights/componentsChart/batchData.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | |||||||
|  | const defaultBatchSize = 4; | ||||||
|  | 
 | ||||||
|  | export type BatchDataOptions<T, TBatched> = { | ||||||
|  |     merge: (accumulated: TBatched, next: T) => TBatched; | ||||||
|  |     map: (item: T) => TBatched; | ||||||
|  |     batchSize?: number; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export const batchData = | ||||||
|  |     <T, TBatched>({ | ||||||
|  |         merge, | ||||||
|  |         map, | ||||||
|  |         batchSize = defaultBatchSize, | ||||||
|  |     }: BatchDataOptions<T, TBatched>) => | ||||||
|  |     (xs: T[]): TBatched[] => | ||||||
|  |         xs.reduce((acc, curr, index) => { | ||||||
|  |             const currentAggregatedIndex = Math.floor(index / batchSize); | ||||||
|  |             const data = acc[currentAggregatedIndex]; | ||||||
|  | 
 | ||||||
|  |             if (data) { | ||||||
|  |                 acc[currentAggregatedIndex] = merge(data, curr); | ||||||
|  |             } else { | ||||||
|  |                 acc[currentAggregatedIndex] = map(curr); | ||||||
|  |             } | ||||||
|  |             return acc; | ||||||
|  |         }, [] as TBatched[]); | ||||||
| @ -0,0 +1 @@ | |||||||
|  | type ChartDataResult = 'Not Enough Data' | 'Batched' | 'Weekly'; | ||||||
| @ -0,0 +1,35 @@ | |||||||
|  | import { useLocationSettings } from 'hooks/useLocationSettings'; | ||||||
|  | import { getDateFnsLocale } from '../getDateFnsLocale.ts'; | ||||||
|  | import type { ChartTypeRegistry, TooltipItem } from 'chart.js'; | ||||||
|  | import { format, startOfWeek } from 'date-fns'; | ||||||
|  | 
 | ||||||
|  | export const useBatchedTooltipDate = <T extends keyof ChartTypeRegistry>( | ||||||
|  |     fallback: string = 'Unknown date range', | ||||||
|  | ) => { | ||||||
|  |     const { locationSettings } = useLocationSettings(); | ||||||
|  |     const locale = getDateFnsLocale(locationSettings.locale); | ||||||
|  |     return (datapoints: TooltipItem<T>[]) => { | ||||||
|  |         const dataPoint = datapoints[0].raw as any; | ||||||
|  |         if ( | ||||||
|  |             'date' in dataPoint && | ||||||
|  |             typeof dataPoint.date === 'string' && | ||||||
|  |             'endDate' in dataPoint && | ||||||
|  |             typeof dataPoint.endDate === 'string' | ||||||
|  |         ) { | ||||||
|  |             const startDate = format( | ||||||
|  |                 startOfWeek(new Date(dataPoint.date), { | ||||||
|  |                     locale, | ||||||
|  |                     weekStartsOn: 1, | ||||||
|  |                 }), | ||||||
|  |                 `PP`, | ||||||
|  |                 { locale }, | ||||||
|  |             ); | ||||||
|  |             const endDate = format(new Date(dataPoint.endDate), `PP`, { | ||||||
|  |                 locale, | ||||||
|  |             }); | ||||||
|  |             return `${startDate} – ${endDate}`; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return fallback; | ||||||
|  |     }; | ||||||
|  | }; | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user