diff --git a/packages/views/runtimes/components/charts/daily-token-chart.tsx b/packages/views/runtimes/components/charts/daily-token-chart.tsx index d37efc180..ea06f1a73 100644 --- a/packages/views/runtimes/components/charts/daily-token-chart.tsx +++ b/packages/views/runtimes/components/charts/daily-token-chart.tsx @@ -1,34 +1,118 @@ +import { useMemo } from "react"; import { AreaChart, Area, XAxis, YAxis, CartesianGrid, + Tooltip, } from "recharts"; import { ChartContainer, - ChartTooltip, - ChartTooltipContent, - ChartLegend, - ChartLegendContent, type ChartConfig, } from "@multica/ui/components/ui/chart"; import type { DailyTokenData } from "../../utils"; import { formatTokens } from "../../utils"; const tokenChartConfig = { - input: { label: "Input", color: "hsl(var(--chart-1))" }, - output: { label: "Output", color: "hsl(var(--chart-2))" }, - cacheRead: { label: "Cache Read", color: "hsl(var(--chart-3))" }, - cacheWrite: { label: "Cache Write", color: "hsl(var(--chart-4))" }, + total: { label: "Total", color: "hsl(var(--chart-1))" }, } satisfies ChartConfig; +type DailyTokenRow = DailyTokenData & { total: number }; + +function computeNiceTicks(data: DailyTokenRow[], tickCount = 5): number[] { + const maxTotal = data.reduce( + (max, d) => (d.total > max ? d.total : max), + 0, + ); + if (maxTotal === 0) return [0]; + + const rawStep = maxTotal / (tickCount - 1); + const magnitude = Math.pow(10, Math.floor(Math.log10(rawStep))); + const niceSteps = [1, 2, 2.5, 5, 10]; + const niceStep = + magnitude * (niceSteps.find((s) => s * magnitude >= rawStep) ?? 10); + + const ticks: number[] = []; + for (let i = 0; i < tickCount; i++) { + ticks.push(niceStep * i); + } + if ((ticks[ticks.length - 1] ?? 0) < maxTotal) { + ticks.push(niceStep * tickCount); + } + return ticks; +} + +function TokenTooltipContent({ + active, + payload, + label, +}: { + active?: boolean; + payload?: Array<{ payload: DailyTokenRow }>; + label?: string; +}) { + if (!active || !payload?.length) return null; + const row = payload[0]!.payload; + + const items = [ + { label: "Input", value: row.input }, + { label: "Output", value: row.output }, + { label: "Cache Read", value: row.cacheRead }, + { label: "Cache Write", value: row.cacheWrite }, + ]; + + return ( +
+
{label}
+
+ {items.map((item) => ( +
+ {item.label} + + {formatTokens(item.value)} + +
+ ))} +
+ Total + + {formatTokens(row.total)} + +
+
+
+ ); +} + export function DailyTokenChart({ data }: { data: DailyTokenData[] }) { + const chartData = useMemo( + () => + data.map((d) => ({ + ...d, + total: d.input + d.output + d.cacheRead + d.cacheWrite, + })), + [data], + ); + const ticks = useMemo(() => computeNiceTicks(chartData), [chartData]); + const yMax = ticks[ticks.length - 1] ?? 0; + return (
-

Daily Token Usage

- - +

+ Daily Token Usage +

+ + formatTokens(v)} - width={50} + width={65} + domain={[0, yMax]} + ticks={ticks} /> - - typeof value === "number" ? formatTokens(value) : String(value) - } - /> - } - /> - } /> + } /> - - - diff --git a/packages/views/runtimes/utils.ts b/packages/views/runtimes/utils.ts index 1096301fa..63218b681 100644 --- a/packages/views/runtimes/utils.ts +++ b/packages/views/runtimes/utils.ts @@ -14,8 +14,14 @@ export function formatLastSeen(lastSeenAt: string | null): string { } export function formatTokens(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`; + if (n >= 1_000_000) { + const m = n / 1_000_000; + return m % 1 < 0.05 ? `${Math.round(m)}M` : `${m.toFixed(1)}M`; + } + if (n >= 1_000) { + const k = n / 1_000; + return k % 1 < 0.05 ? `${Math.round(k)}K` : `${k.toFixed(1)}K`; + } return n.toLocaleString(); }