/** * Pure SVG line chart for token usage trends. * No external chart library — renders with axis labels. */ import { useMemo } from "preact/hooks"; import type { UsageDataPoint } from "../../../shared/hooks/use-usage-stats"; interface UsageChartProps { data: UsageDataPoint[]; height?: number; } const PADDING = { top: 20, right: 20, bottom: 40, left: 65 }; export function formatNumber(n: number): string { if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + "M"; if (n >= 1_000) return (n / 1_000).toFixed(1) + "K"; return String(n); } function formatTime(iso: string): string { const d = new Date(iso); return `${String(d.getMonth() + 1).padStart(2, "0")}/${String(d.getDate()).padStart(2, "0")} ${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; } export function UsageChart({ data, height = 260 }: UsageChartProps) { const width = 720; // SVG viewBox width, responsive via CSS const reqHeight = Math.round(height * 0.6); const { inputPoints, outputPoints, requestPoints, xLabels, yTokenLabels, yReqLabels } = useMemo(() => { if (data.length === 0) { return { inputPoints: "", outputPoints: "", requestPoints: "", xLabels: [], yTokenLabels: [], yReqLabels: [] }; } const chartW = width - PADDING.left - PADDING.right; const chartH = height - PADDING.top - PADDING.bottom; const reqChartH = reqHeight - PADDING.top - PADDING.bottom; const maxInput = Math.max(...data.map((d) => d.input_tokens)); const maxOutput = Math.max(...data.map((d) => d.output_tokens)); const yMaxT = Math.max(maxInput, maxOutput, 1); const yMaxR = Math.max(...data.map((d) => d.request_count), 1); const toX = (i: number) => PADDING.left + (i / Math.max(data.length - 1, 1)) * chartW; const toYTokens = (v: number) => PADDING.top + chartH - (v / yMaxT) * chartH; const toYReqs = (v: number) => PADDING.top + reqChartH - (v / yMaxR) * reqChartH; const inp = data.map((d, i) => `${toX(i)},${toYTokens(d.input_tokens)}`).join(" "); const out = data.map((d, i) => `${toX(i)},${toYTokens(d.output_tokens)}`).join(" "); const req = data.map((d, i) => `${toX(i)},${toYReqs(d.request_count)}`).join(" "); // X axis labels (up to 6) const step = Math.max(1, Math.floor(data.length / 5)); const xl = []; for (let i = 0; i < data.length; i += step) { xl.push({ x: toX(i), label: formatTime(data[i].timestamp) }); } // Y axis labels (5 ticks) const yTL = []; const yRL = []; for (let i = 0; i <= 4; i++) { const frac = i / 4; yTL.push({ y: PADDING.top + chartH - frac * chartH, label: formatNumber(Math.round(yMaxT * frac)) }); yRL.push({ y: PADDING.top + reqChartH - frac * reqChartH, label: formatNumber(Math.round(yMaxR * frac)) }); } return { inputPoints: inp, outputPoints: out, requestPoints: req, xLabels: xl, yTokenLabels: yTL, yReqLabels: yRL }; }, [data, height, reqHeight]); if (data.length === 0) { return (
No usage data yet
); } return (
{/* Token chart */}
Input Tokens Output Tokens
{/* Grid lines */} {yTokenLabels.map((tick) => ( ))} {/* Y axis labels */} {yTokenLabels.map((tick) => ( {tick.label} ))} {/* X axis labels */} {xLabels.map((tick) => ( {tick.label} ))} {/* Lines */}
{/* Request count chart */}
Requests
{/* Grid lines */} {yReqLabels.map((tick) => ( ))} {yReqLabels.map((tick) => ( {tick.label} ))} {xLabels.map((tick) => ( {tick.label} ))}
); }