diff --git a/frontend/src/component/insights/components/LineChart/createTooltip.ts b/frontend/src/component/insights/components/LineChart/createTooltip.ts index 0f604fb825..a8f97fce37 100644 --- a/frontend/src/component/insights/components/LineChart/createTooltip.ts +++ b/frontend/src/component/insights/components/LineChart/createTooltip.ts @@ -12,7 +12,6 @@ export const createTooltip = setTooltip(null); return; } - setTooltip({ caretX: tooltip?.caretX, caretY: tooltip?.caretY, diff --git a/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveChart.tsx b/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveChart.tsx index 965f48f43d..d41ce90a6e 100644 --- a/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveChart.tsx +++ b/frontend/src/component/insights/componentsChart/CreationArchiveChart/CreationArchiveChart.tsx @@ -1,15 +1,43 @@ import 'chartjs-adapter-date-fns'; -import { type FC, useMemo } from 'react'; +import { type FC, useMemo, useState } from 'react'; import type { InstanceInsightsSchema } from 'openapi'; import { useProjectChartData } from 'component/insights/hooks/useProjectChartData'; -import { - fillGradientPrimary, - LineChart, - NotEnoughData, -} from 'component/insights/components/LineChart/LineChart'; import { useTheme } from '@mui/material'; import type { GroupedDataByProject } from 'component/insights/hooks/useGroupedProjectTrends'; import { usePlaceholderData } from 'component/insights/hooks/usePlaceholderData'; +import { Chart } from 'react-chartjs-2'; +import { + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + Tooltip, + Legend, + TimeScale, + Chart as ChartJS, + Filler, +} from 'chart.js'; +import { useLocationSettings } from 'hooks/useLocationSettings'; +import { + ChartTooltip, + type TooltipState, +} from 'component/insights/components/LineChart/ChartTooltip/ChartTooltip'; +import { createTooltip } from 'component/insights/components/LineChart/createTooltip'; +import { CreationArchiveTooltip } from './CreationArchiveTooltip.tsx'; +import { CreationArchiveRatioTooltip } from './CreationArchiveRatioTooltip.tsx'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + LineElement, + BarElement, + TimeScale, + Tooltip, + Legend, + Filler, +); interface ICreationArchiveChartProps { creationArchiveTrends: GroupedDataByProject< @@ -22,6 +50,8 @@ interface ICreationArchiveChartProps { type WeekData = { archivedFlags: number; totalCreatedFlags: number; + createdFlagsByType: Record; + archivePercentage: number; week: string; date?: string; }; @@ -41,6 +71,8 @@ export const CreationArchiveChart: FC = ({ const creationArchiveData = useProjectChartData(creationArchiveTrends); const theme = useTheme(); const placeholderData = usePlaceholderData(); + const { locationSettings } = useLocationSettings(); + const [tooltip, setTooltip] = useState(null); const aggregateHealthData = useMemo(() => { const labels: string[] = Array.from( @@ -51,6 +83,18 @@ export const CreationArchiveChart: FC = ({ ), ); + // Get all unique flag types + const allFlagTypes = new Set(); + creationArchiveData.datasets.forEach((d) => + d.data.forEach((item: any) => { + if (item.createdFlags) { + Object.keys(item.createdFlags).forEach((type) => + allFlagTypes.add(type), + ); + } + }), + ); + const weeks: WeekData[] = labels .map((label) => { return creationArchiveData.datasets @@ -59,14 +103,17 @@ export const CreationArchiveChart: FC = ({ (acc: WeekData, item: RawWeekData) => { if (item) { acc.archivedFlags += item.archivedFlags || 0; - const createdFlagsSum = item.createdFlags - ? Object.values(item.createdFlags).reduce( - (sum: number, count: number) => - sum + count, - 0, - ) - : 0; - acc.totalCreatedFlags += createdFlagsSum; + + if (item.createdFlags) { + Object.entries(item.createdFlags).forEach( + ([type, count]) => { + acc.createdFlagsByType[type] = + (acc.createdFlagsByType[type] || + 0) + count; + acc.totalCreatedFlags += count; + }, + ); + } } if (!acc.date) { acc.date = item?.date; @@ -76,20 +123,74 @@ export const CreationArchiveChart: FC = ({ { archivedFlags: 0, totalCreatedFlags: 0, + createdFlagsByType: {}, + archivePercentage: 0, week: label, } as WeekData, ); }) + .map((week) => ({ + ...week, + archivePercentage: + week.totalCreatedFlags > 0 + ? (week.archivedFlags / week.totalCreatedFlags) * 100 + : 0, + })) .sort((a, b) => (a.week > b.week ? 1 : -1)); + + // Create datasets for each flag type + const flagTypeColors = [ + theme.palette.success.border, + theme.palette.success.main, + theme.palette.success.dark, + '#4D8007', + '#7D935E', + ]; + + const flagTypeDatasets = Array.from(allFlagTypes).map( + (flagType, index) => ({ + label: `Created: ${flagType}`, + data: weeks, + backgroundColor: flagTypeColors[index % flagTypeColors.length], + borderColor: flagTypeColors[index % flagTypeColors.length], + type: 'bar' as const, + parsing: { + yAxisKey: `createdFlagsByType.${flagType}`, + xAxisKey: 'date', + }, + yAxisID: 'y', + stack: 'created', + order: 2, + }), + ); + return { datasets: [ { - label: 'Number of created flags', + label: 'Archived flags', + data: weeks, + backgroundColor: theme.palette.background.application, + borderColor: theme.palette.background.application, + type: 'bar' as const, + parsing: { yAxisKey: 'archivedFlags', xAxisKey: 'date' }, + yAxisID: 'y', + stack: 'archived', + order: 2, + }, + ...flagTypeDatasets, + { + label: 'Flags archived / Flags created', data: weeks, borderColor: theme.palette.primary.light, - backgroundColor: fillGradientPrimary, - fill: true, - order: 3, + backgroundColor: theme.palette.primary.light, + fill: false, + type: 'line' as const, + parsing: { + yAxisKey: 'archivePercentage', + xAxisKey: 'date', + }, + yAxisID: 'y1', + order: 1, }, ], }; @@ -108,20 +209,110 @@ export const CreationArchiveChart: FC = ({ notEnoughData || isLoading ? placeholderData : aggregateOrProjectData; return ( - : isLoading} - /> + <> + { + // Hide individual created flag type labels + return !legendItem.text?.startsWith( + 'Created:', + ); + }, + generateLabels: (chart) => { + const original = + ChartJS.defaults.plugins.legend.labels.generateLabels( + chart, + ); + const filtered = original.filter( + (item) => + !item.text?.startsWith('Created:'), + ); + + // Add custom "Created Flags" legend item + filtered.push({ + text: 'Created Flags', + fillStyle: theme.palette.success.main, + strokeStyle: theme.palette.success.main, + lineWidth: 0, + hidden: false, + index: filtered.length, + datasetIndex: -1, + }); + + return filtered; + }, + }, + }, + tooltip: { + enabled: false, + position: 'nearest', + external: createTooltip(setTooltip), + }, + }, + locale: locationSettings.locale, + interaction: { + intersect: false, + axis: 'xy', + mode: 'nearest', + }, + scales: { + x: { + type: 'time', + time: { + unit: 'week', + tooltipFormat: 'PPP', + }, + stacked: true, + }, + y: { + type: 'linear', + display: true, + position: 'left', + beginAtZero: true, + stacked: true, + title: { + display: true, + text: 'Number of Flags', + }, + }, + y1: { + type: 'linear', + display: true, + position: 'right', + beginAtZero: true, + title: { + display: true, + text: 'Archive Percentage (%)', + }, + grid: { + drawOnChartArea: false, + }, + }, + }, + }} + height={100} + width={250} + /> + {tooltip?.dataPoints?.some((point) => + point.dataset.label?.startsWith('Created:'), + ) ? ( + + ) : tooltip?.dataPoints?.some( + (point) => + point.dataset.label === 'Flags archived / Flags created', + ) ? ( + + ) : ( + + )} + ); }; diff --git a/frontend/src/component/insights/sections/PerformanceInsights.tsx b/frontend/src/component/insights/sections/PerformanceInsights.tsx index 3b5e08241e..8e46602447 100644 --- a/frontend/src/component/insights/sections/PerformanceInsights.tsx +++ b/frontend/src/component/insights/sections/PerformanceInsights.tsx @@ -28,6 +28,7 @@ import { useUiFlag } from 'hooks/useUiFlag'; import { NewProductionFlagsChart } from '../componentsChart/NewProductionFlagsChart/NewProductionFlagsChart.tsx'; import Lightbulb from '@mui/icons-material/LightbulbOutlined'; import { CreationArchiveChart } from '../componentsChart/CreationArchiveChart/CreationArchiveChart.tsx'; +import { CreationArchiveStats } from '../componentsStat/CreationArchiveStats/CreationArchiveStats.tsx'; export const PerformanceInsights: FC = () => { const statePrefix = 'performance-'; @@ -80,6 +81,39 @@ export const PerformanceInsights: FC = () => { : flagsPerUserCalculation.toFixed(2); } + // Calculate current archive ratio from latest data + function getCurrentArchiveRatio() { + if ( + !groupedCreationArchiveData || + Object.keys(groupedCreationArchiveData).length === 0 + ) { + return 0; + } + + let totalArchived = 0; + let totalCreated = 0; + + Object.values(groupedCreationArchiveData).forEach((projectData) => { + const latestData = projectData[projectData.length - 1]; + if (latestData) { + totalArchived += latestData.archivedFlags || 0; + const createdSum = latestData.createdFlags + ? Object.values(latestData.createdFlags).reduce( + (sum: number, count: number) => sum + count, + 0, + ) + : 0; + totalCreated += createdSum; + } + }); + + return totalCreated > 0 + ? Math.round((totalArchived / totalCreated) * 100) + : 0; + } + + const currentRatio = getCurrentArchiveRatio(); + const isLifecycleGraphsEnabled = useUiFlag('lifecycleGraphs'); return ( @@ -116,6 +150,10 @@ export const PerformanceInsights: FC = () => { +