'use client' import { useMemo } from 'react' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { formatCost } from '@/lib/format' import type { PluginProps } from '@/lib/plugins' // ─── Helpers ──────────────────────────────────────────────────────── function getWeekday(dateStr: string): number { return new Date(dateStr + 'T00:00:00').getDay() } function getMonthLabel(dateStr: string): string { return new Date(dateStr + 'T00:00:00').toLocaleString('en-US', { month: 'short' }) } function intensityStyle(ratio: number): React.CSSProperties { if (ratio === 0) return { backgroundColor: 'var(--muted)' } // Use chart-2 (teal) with increasing opacity for a clean gradient const alpha = 0.15 + ratio * 0.85 return { backgroundColor: `oklch(0.55 0.15 170 / ${alpha})` } } const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] // ─── Component ────────────────────────────────────────────────────── export default function CostHeatmap({ data }: PluginProps) { const { grid, weeks, monthLabels, stats } = useMemo(() => { const dailyMap = new Map(data.daily.map((d) => [d.date, d.costUSD])) const dates = data.daily.map((d) => d.date).sort() if (dates.length === 0) { return { grid: [], maxCost: 0, weeks: 0, monthLabels: [], stats: null } } const first = dates[0] const last = dates[dates.length - 1] // Build a continuous date range const allDates: string[] = [] const cur = new Date(first + 'T00:00:00') const end = new Date(last + 'T00:00:00') while (cur <= end) { allDates.push(cur.toISOString().slice(0, 10)) cur.setDate(cur.getDate() + 1) } // Pad the start to align to Sunday const startPad = getWeekday(allDates[0]) const padded = [ ...Array.from({ length: startPad }, () => null), ...allDates, ] const maxVal = Math.max(...allDates.map((d) => dailyMap.get(d) || 0), 0.01) const numWeeks = Math.ceil(padded.length / 7) // Build grid: grid[weekday][weekIndex] const g: (({ date: string; cost: number; ratio: number } | null))[][] = Array.from( { length: 7 }, () => Array.from({ length: numWeeks }, () => null), ) for (let i = 0; i < padded.length; i++) { const week = Math.floor(i / 7) const day = i % 7 const date = padded[i] if (date) { const cost = dailyMap.get(date) || 0 g[day][week] = { date, cost, ratio: cost / maxVal } } } // Month labels at week boundaries const labels: { label: string; week: number }[] = [] let lastMonth = '' for (let i = 0; i < padded.length; i++) { const date = padded[i] if (!date) continue const month = getMonthLabel(date) if (month !== lastMonth) { labels.push({ label: month, week: Math.floor(i / 7) }) lastMonth = month } } // Stats const costs = allDates.map((d) => dailyMap.get(d) || 0) const total = costs.reduce((a, b) => a + b, 0) const activeDays = costs.filter((c) => c > 0).length const peakIdx = costs.indexOf(Math.max(...costs)) const peakDay = allDates[peakIdx] return { grid: g, maxCost: maxVal, weeks: numWeeks, monthLabels: labels, stats: { total, activeDays, totalDays: allDates.length, peakDay, peakCost: maxVal }, } }, [data.daily]) if (!stats) { return ( Cost Heatmap

No data available yet.

) } const cellSize = 18 const cellGap = 3 return (
{/* Stats row */}
Total Spend {formatCost(stats.total)} Active Days {stats.activeDays} / {stats.totalDays} Peak Day {stats.peakDay} Peak Cost {formatCost(stats.peakCost)}
{/* Heatmap */} Daily Cost Heatmap Each cell represents one day. Darker cells = higher spending.
{/* Month labels */}
{monthLabels.map((m, i) => ( {m.label} ))}
{/* Grid */}
{/* Weekday labels */}
{WEEKDAYS.map((d, i) => ( {i % 2 === 1 ? d : ''} ))}
{/* Weeks */} {Array.from({ length: weeks }, (_, weekIdx) => (
{Array.from({ length: 7 }, (_, dayIdx) => { const cell = grid[dayIdx]?.[weekIdx] if (!cell) { return (
) } return ( } />

{cell.date}

{formatCost(cell.cost)}

) })}
))}
{/* Legend */}
Less {[0, 0.2, 0.4, 0.6, 0.8, 1].map((ratio) => (
))} More
) }