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,
|
||||
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<ICreationArchiveChartProps> = ({
|
||||
creationArchiveTrends,
|
||||
isLoading,
|
||||
@ -56,7 +61,7 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
|
||||
const { locationSettings } = useLocationSettings();
|
||||
const [tooltip, setTooltip] = useState<null | TooltipState>(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<ICreationArchiveChartProps> = ({
|
||||
),
|
||||
);
|
||||
|
||||
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<ICreationArchiveChartProps> = ({
|
||||
totalCreatedFlags: 0,
|
||||
archivePercentage: 0,
|
||||
week: label,
|
||||
date: '',
|
||||
});
|
||||
|
||||
const weeks: WeekData[] = labels
|
||||
@ -103,13 +111,23 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
|
||||
}))
|
||||
.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<ICreationArchiveChartProps> = ({
|
||||
},
|
||||
{
|
||||
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<ICreationArchiveChartProps> = ({
|
||||
};
|
||||
}, [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<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(
|
||||
() => ({
|
||||
responsive: true,
|
||||
@ -172,6 +206,12 @@ export const CreationArchiveChart: FC<ICreationArchiveChartProps> = ({
|
||||
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<ICreationArchiveChartProps> = ({
|
||||
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,
|
||||
},
|
||||
},
|
||||
|
||||
@ -44,6 +44,7 @@ interface CreationArchiveRatioTooltipProps {
|
||||
}
|
||||
|
||||
const Timestamp = styled('span')(({ theme }) => ({
|
||||
whiteSpace: 'nowrap',
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
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;
|
||||
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<WeekData, 'week'> & {
|
||||
endDate: string;
|
||||
};
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user