/** * Usage Trend Chart Component * * Displays usage trends over time with tokens and cost. * Supports daily, hourly, and monthly granularity with interactive tooltips. * Respects privacy mode to blur sensitive data. */ import { useMemo } from 'react'; import { XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Area, AreaChart, } from 'recharts'; import { format } from 'date-fns'; import { useTranslation } from 'react-i18next'; import { Skeleton } from '@/components/ui/skeleton'; import { cn } from '@/lib/utils'; import type { DailyUsage, HourlyUsage } from '@/hooks/use-usage'; import { usePrivacy, PRIVACY_BLUR_CLASS } from '@/contexts/privacy-context'; // TODO i18n: import { useTranslation } from 'react-i18next'; when keys are ready type ChartData = DailyUsage | HourlyUsage; interface UsageTrendChartProps { data: ChartData[]; isLoading?: boolean; granularity?: 'daily' | 'monthly' | 'hourly'; className?: string; } export function UsageTrendChart({ data, isLoading, granularity = 'daily', className, }: UsageTrendChartProps) { const { privacyMode } = usePrivacy(); const { t } = useTranslation(); const chartData = useMemo(() => { if (!data || data.length === 0) return []; // For hourly data, already sorted ascending from API const sortedData = data; return sortedData.map((item) => { // Handle hourly vs daily data format const timeKey = 'hour' in item ? item.hour : (item as DailyUsage).date; return { ...item, dateFormatted: formatTime(timeKey, granularity), costRounded: Number(item.cost.toFixed(4)), }; }); }, [data, granularity]); if (isLoading) { return ; } if (!data || data.length === 0) { return (

{granularity === 'hourly' ? t('analytics.noDailyUsage') : t('analytics.noUsageData')}

); } // Custom tick component for privacy-aware axis labels const PrivacyTick = ({ x, y, payload, isRight, }: { x: number; y: number; payload: { value: string | number }; isRight?: boolean; }) => { const displayValue = isRight ? `$${payload.value}` : formatNumber(Number(payload.value)); return ( {displayValue} ); }; return (
} tickLine={false} axisLine={{ className: 'stroke-muted' }} /> } tickLine={false} axisLine={{ className: 'stroke-muted' }} /> { if (!active || !payload || !payload.length) return null; return (

{label}

{payload.map((entry, index) => (

{entry.name}:{' '} {entry.name === 'Tokens' ? formatNumber(Number(entry.value) || 0) : `$${entry.value}`}

))}
); }} />
); } // Helper functions function formatTime(timeStr: string, granularity: 'daily' | 'monthly' | 'hourly'): string { if (granularity === 'hourly') { // Format: "YYYY-MM-DD HH:00" -> convert UTC to local time -> "HH:00" // Parse as UTC and format in local timezone const [datePart, timePart] = timeStr.split(' '); if (datePart && timePart) { // Create date in UTC: "2025-12-12 20:00" -> "2025-12-12T20:00:00Z" const utcDate = new Date(`${datePart}T${timePart}:00Z`); return format(utcDate, 'HH:mm'); } return timeStr; } const date = new Date(timeStr); if (granularity === 'monthly') { return format(date, 'MMM yyyy'); } // For daily, show shorter format return format(date, 'MMM dd'); } function formatNumber(num: number): string { if (num >= 1000000) { return `${(num / 1000000).toFixed(1)}M`; } if (num >= 1000) { return `${(num / 1000).toFixed(1)}K`; } return num.toLocaleString(); }