From ce4a243165c66a1ea06cecb2472e44f2490bcc2e Mon Sep 17 00:00:00 2001 From: andreas-unleash Date: Thu, 21 Mar 2024 14:21:53 +0200 Subject: [PATCH] feat: ttp for all projects should show median value per day (#6656) Separates out the calculation of the median to its own file and tested Closes # [1-2212](https://linear.app/unleash/issue/1-2212/timetoproduction-for-all-projects-should-show-the-change-in-the-median) --------- Signed-off-by: andreas-unleash --- .../executiveDashboard/chart-info.ts | 8 ++-- .../TimeToProductionChart.tsx | 33 ++------------- .../median-time-to-production.test.ts | 42 +++++++++++++++++++ .../median-time-to-production.ts | 35 ++++++++++++++++ 4 files changed, 84 insertions(+), 34 deletions(-) create mode 100644 frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/median-time-to-production.test.ts create mode 100644 frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/median-time-to-production.ts diff --git a/frontend/src/component/executiveDashboard/chart-info.ts b/frontend/src/component/executiveDashboard/chart-info.ts index 4754956386..8a3a28d86a 100644 --- a/frontend/src/component/executiveDashboard/chart-info.ts +++ b/frontend/src/component/executiveDashboard/chart-info.ts @@ -51,19 +51,19 @@ export const chartInfo = { 'How the overall health changes over time for the selected projects.', }, averageTimeToProduction: { - title: 'Average time to production', + title: 'Median time to production', tooltip: - 'How long does it currently take on average from when a feature flag was created until it was enabled in a "production" type environment. This is calculated only from feature flags of the type "release" and is averaged across the selected projects.', + 'How long does it currently take on average from when a feature flag was created until it was enabled in a "production" type environment. This is calculated only from feature flags of the type "release" and is the median across the selected projects.', }, timeToProduction: { title: 'Time to production', tooltip: - 'How the average time to production changes over time across all projects.', + 'How the median time to production changes over time across all projects.', }, timeToProductionPerProject: { title: 'Time to production per project', tooltip: - 'How the average time to production changes over time for the selected projects.', + 'How the median time to production changes over time for the selected projects.', }, metrics: { title: 'Flag evaluation metrics', diff --git a/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/TimeToProductionChart.tsx b/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/TimeToProductionChart.tsx index d251604d87..280f56b99e 100644 --- a/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/TimeToProductionChart.tsx +++ b/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/TimeToProductionChart.tsx @@ -11,6 +11,7 @@ import type { GroupedDataByProject } from '../../hooks/useGroupedProjectTrends'; import { usePlaceholderData } from '../../hooks/usePlaceholderData'; import { TimeToProductionTooltip } from './TimeToProductionTooltip/TimeToProductionTooltip'; import { useTheme } from '@mui/material'; +import { medianTimeToProduction } from './median-time-to-production'; interface ITimeToProductionChartProps { projectFlagTrends: GroupedDataByProject< @@ -19,34 +20,6 @@ interface ITimeToProductionChartProps { isAggregate?: boolean; } -type GroupedDataByDate = Record; - -type DateResult = Record; - -function averageTimeToProduction( - projectsData: ExecutiveSummarySchema['projectFlagTrends'], -): DateResult { - // Group the data by date - const groupedData: GroupedDataByDate = {}; - projectsData.forEach((item) => { - const { date, timeToProduction } = item; - if (!groupedData[date]) { - groupedData[date] = []; - } - if (timeToProduction !== undefined) { - groupedData[date].push(timeToProduction); - } - }); - // Calculate the average time to production for each date - const averageByDate: DateResult = {}; - Object.entries(groupedData).forEach(([date, times]) => { - const sum = times.reduce((acc, curr) => acc + curr, 0); - const average = sum / times.length; - averageByDate[date] = average; - }); - return averageByDate; -} - export const TimeToProductionChart: VFC = ({ projectFlagTrends, isAggregate, @@ -59,7 +32,7 @@ export const TimeToProductionChart: VFC = ({ ); const aggregatedPerDay = useMemo(() => { - const result = averageTimeToProduction( + const result = medianTimeToProduction( Object.values(projectsDatasets.datasets).flatMap( (item) => item.data, ), @@ -74,7 +47,7 @@ export const TimeToProductionChart: VFC = ({ return { datasets: [ { - label: 'Time to production', + label: 'Median time to production', data, borderColor: theme.palette.primary.light, backgroundColor: fillGradientPrimary, diff --git a/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/median-time-to-production.test.ts b/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/median-time-to-production.test.ts new file mode 100644 index 0000000000..10eb75a7b3 --- /dev/null +++ b/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/median-time-to-production.test.ts @@ -0,0 +1,42 @@ +import { medianTimeToProduction } from './median-time-to-production'; +import type { ExecutiveSummarySchema } from 'openapi'; + +describe('medianTimeToProduction', () => { + it('calculates the median with a single date and an odd number of projects', () => { + const projectsData = [ + { date: '2023-03-21', timeToProduction: 10 }, + { date: '2023-03-21', timeToProduction: 20 }, + { date: '2023-03-21', timeToProduction: 30 }, + ] as unknown as ExecutiveSummarySchema['projectFlagTrends']; + const expected = { '2023-03-21': 20 }; + expect(medianTimeToProduction(projectsData)).toEqual(expected); + }); + + it('calculates the median with a single date and an even number of projects', () => { + const projectsData = [ + { date: '2023-03-22', timeToProduction: 10 }, + { date: '2023-03-22', timeToProduction: 20 }, + { date: '2023-03-22', timeToProduction: 30 }, + { date: '2023-03-22', timeToProduction: 40 }, + ] as unknown as ExecutiveSummarySchema['projectFlagTrends']; + const expected = { '2023-03-22': 25 }; + expect(medianTimeToProduction(projectsData)).toEqual(expected); + }); + + it('calculates the median for multiple dates with varying numbers of projects', () => { + const projectsData = [ + { date: '2023-03-23', timeToProduction: 5 }, + { date: '2023-03-23', timeToProduction: 15 }, + { date: '2023-03-24', timeToProduction: 10 }, + { date: '2023-03-24', timeToProduction: 20 }, + { date: '2023-03-24', timeToProduction: 30 }, + { date: '2023-03-25', timeToProduction: 25 }, + ] as unknown as ExecutiveSummarySchema['projectFlagTrends']; + const expected = { + '2023-03-23': 10, + '2023-03-24': 20, + '2023-03-25': 25, + }; + expect(medianTimeToProduction(projectsData)).toEqual(expected); + }); +}); diff --git a/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/median-time-to-production.ts b/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/median-time-to-production.ts new file mode 100644 index 0000000000..1fe3507b11 --- /dev/null +++ b/frontend/src/component/executiveDashboard/componentsChart/TimeToProductionChart/median-time-to-production.ts @@ -0,0 +1,35 @@ +import type { ExecutiveSummarySchema } from 'openapi'; + +type GroupedDataByDate = Record; + +type DateResult = Record; + +export function medianTimeToProduction( + projectsData: ExecutiveSummarySchema['projectFlagTrends'], +): DateResult { + const groupedData: GroupedDataByDate = {}; + projectsData.forEach((item) => { + const { date, timeToProduction } = item; + if (!groupedData[date]) { + groupedData[date] = []; + } + if (timeToProduction !== undefined) { + groupedData[date].push(timeToProduction); + } + }); + + // Calculate the median time to production for each date + const medianByDate: DateResult = {}; + Object.entries(groupedData).forEach(([date, times]) => { + const sortedTimes = times.sort((a, b) => a - b); + const midIndex = Math.floor(sortedTimes.length / 2); + + const median = + sortedTimes.length % 2 === 0 + ? (sortedTimes[midIndex - 1] + sortedTimes[midIndex]) / 2 + : sortedTimes[midIndex]; + + medianByDate[date] = median; + }); + return medianByDate; +}