'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 (
No data available yet.
{cell.date}
{formatCost(cell.cost)}