Skip to content

Commit 8aeaac3

Browse files
ersinkocclaude
andcommitted
✨ feat(costs): upgrade charts from div-bars to recharts
Overview tab: - Daily Cost Trend: div-based bars → recharts AreaChart with gradient fill - New Token Volume: stacked BarChart (input vs output tokens) Breakdown tab: - New Cost Distribution donut chart with provider legend - Existing provider list renamed to "Provider Details" Both tabs use same chart styling (green area, indigo/purple bars, pink donut) as AnalyticsPage for visual consistency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6cf2fce commit 8aeaac3

File tree

1 file changed

+224
-31
lines changed

1 file changed

+224
-31
lines changed

packages/ui/src/pages/CostsPage.tsx

Lines changed: 224 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ import type { CostSummary, BudgetStatus, ProviderBreakdown, DailyUsage } from '.
55
import { LoadingSpinner } from '../components/LoadingSpinner';
66
import { useToast } from '../components/ToastProvider';
77
import { DollarSign, Home, BarChart, TrendingUp, Calendar, Layers } from '../components/icons';
8+
import {
9+
AreaChart,
10+
Area,
11+
BarChart as RechartsBarChart,
12+
Bar,
13+
PieChart,
14+
Pie,
15+
Cell,
16+
XAxis,
17+
YAxis,
18+
CartesianGrid,
19+
Tooltip,
20+
Legend,
21+
ResponsiveContainer,
22+
} from 'recharts';
823
import { PageHomeTab } from '../components/PageHomeTab';
924

1025
type Period = 'day' | 'week' | 'month' | 'year';
@@ -304,49 +319,227 @@ export function CostsPage() {
304319
</div>
305320
)}
306321

307-
{/* Daily Chart */}
322+
{/* Daily Cost Trend */}
308323
{breakdown && breakdown.daily.length > 0 && (
309324
<div className="card-elevated bg-bg-secondary dark:bg-dark-bg-secondary rounded-lg border border-border dark:border-dark-border p-4">
310-
<h3 className="text-lg font-medium text-text-primary dark:text-dark-text-primary mb-4">
311-
Daily Usage
325+
<h3 className="text-sm font-medium text-text-primary dark:text-dark-text-primary flex items-center gap-2 mb-4">
326+
<TrendingUp className="w-4 h-4 text-success" />
327+
Daily Cost Trend
312328
</h3>
313-
<div className="flex gap-1 items-end h-32">
314-
{breakdown.daily.slice(-14).map((day) => {
315-
const maxCost = Math.max(...breakdown.daily.map((d) => d.cost));
316-
const height = maxCost > 0 ? (day.cost / maxCost) * 100 : 0;
317-
return (
318-
<div
319-
key={day.date}
320-
className="flex-1 group relative"
321-
title={`${day.date}: ${day.costFormatted}`}
322-
>
323-
<div
324-
className="bg-success rounded-t transition-all hover:bg-success/80"
325-
style={{ height: `${Math.max(height, 2)}%` }}
326-
/>
327-
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-1 hidden group-hover:block bg-gray-900 text-white text-xs px-2 py-1 rounded whitespace-nowrap">
328-
{day.date}: {day.costFormatted}
329-
</div>
330-
</div>
331-
);
332-
})}
333-
</div>
334-
<div className="flex justify-between mt-2 text-xs text-text-muted">
335-
<span>{breakdown.daily[0]?.date}</span>
336-
<span>{breakdown.daily[breakdown.daily.length - 1]?.date}</span>
337-
</div>
329+
<ResponsiveContainer width="100%" height={220}>
330+
<AreaChart
331+
data={breakdown.daily.map((d) => ({
332+
...d,
333+
date: new Date(d.date).toLocaleDateString('en', {
334+
month: 'short',
335+
day: 'numeric',
336+
}),
337+
}))}
338+
>
339+
<defs>
340+
<linearGradient id="costAreaGrad" x1="0" y1="0" x2="0" y2="1">
341+
<stop offset="5%" stopColor="#22c55e" stopOpacity={0.25} />
342+
<stop offset="95%" stopColor="#22c55e" stopOpacity={0} />
343+
</linearGradient>
344+
</defs>
345+
<CartesianGrid
346+
strokeDasharray="3 3"
347+
stroke="var(--color-border, #334155)"
348+
opacity={0.4}
349+
/>
350+
<XAxis
351+
dataKey="date"
352+
tick={{ fontSize: 11 }}
353+
stroke="var(--color-text-muted, #94a3b8)"
354+
/>
355+
<YAxis
356+
tick={{ fontSize: 11 }}
357+
stroke="var(--color-text-muted, #94a3b8)"
358+
tickFormatter={(v) => `$${v}`}
359+
/>
360+
<Tooltip
361+
contentStyle={{
362+
background: 'var(--color-bg-secondary, #1e293b)',
363+
border: '1px solid var(--color-border, #334155)',
364+
borderRadius: '8px',
365+
fontSize: '12px',
366+
}}
367+
formatter={(value: unknown) => [`$${Number(value).toFixed(4)}`, 'Cost']}
368+
/>
369+
<Area
370+
type="monotone"
371+
dataKey="cost"
372+
stroke="#22c55e"
373+
fill="url(#costAreaGrad)"
374+
strokeWidth={2}
375+
/>
376+
</AreaChart>
377+
</ResponsiveContainer>
378+
</div>
379+
)}
380+
381+
{/* Token Volume */}
382+
{breakdown && breakdown.daily.length > 0 && (
383+
<div className="card-elevated bg-bg-secondary dark:bg-dark-bg-secondary rounded-lg border border-border dark:border-dark-border p-4">
384+
<h3 className="text-sm font-medium text-text-primary dark:text-dark-text-primary flex items-center gap-2 mb-4">
385+
<BarChart className="w-4 h-4 text-indigo-500" />
386+
Token Volume
387+
</h3>
388+
<ResponsiveContainer width="100%" height={200}>
389+
<RechartsBarChart
390+
data={breakdown.daily.map((d) => ({
391+
...d,
392+
date: new Date(d.date).toLocaleDateString('en', {
393+
month: 'short',
394+
day: 'numeric',
395+
}),
396+
}))}
397+
>
398+
<CartesianGrid
399+
strokeDasharray="3 3"
400+
stroke="var(--color-border, #334155)"
401+
opacity={0.4}
402+
/>
403+
<XAxis
404+
dataKey="date"
405+
tick={{ fontSize: 11 }}
406+
stroke="var(--color-text-muted, #94a3b8)"
407+
/>
408+
<YAxis
409+
tick={{ fontSize: 11 }}
410+
stroke="var(--color-text-muted, #94a3b8)"
411+
tickFormatter={(v) => (v >= 1000 ? `${(v / 1000).toFixed(0)}K` : v)}
412+
/>
413+
<Tooltip
414+
contentStyle={{
415+
background: 'var(--color-bg-secondary, #1e293b)',
416+
border: '1px solid var(--color-border, #334155)',
417+
borderRadius: '8px',
418+
fontSize: '12px',
419+
}}
420+
/>
421+
<Legend iconType="circle" iconSize={8} wrapperStyle={{ fontSize: 11 }} />
422+
<Bar
423+
dataKey="inputTokens"
424+
name="Input"
425+
fill="#6366f1"
426+
radius={[3, 3, 0, 0]}
427+
/>
428+
<Bar
429+
dataKey="outputTokens"
430+
name="Output"
431+
fill="#a855f7"
432+
radius={[3, 3, 0, 0]}
433+
/>
434+
</RechartsBarChart>
435+
</ResponsiveContainer>
338436
</div>
339437
)}
340438
</div>
341439
)}
342440

343441
{activeTab === 'breakdown' && breakdown && (
344442
<div className="space-y-6">
345-
{/* Provider Breakdown */}
443+
{/* Provider Cost Distribution */}
444+
{breakdown.byProvider.filter((p) => p.cost > 0).length > 0 && (
445+
<div className="card-elevated bg-bg-secondary dark:bg-dark-bg-secondary rounded-lg border border-border dark:border-dark-border p-4">
446+
<h3 className="text-sm font-medium text-text-primary dark:text-dark-text-primary flex items-center gap-2 mb-4">
447+
<DollarSign className="w-4 h-4 text-pink-500" />
448+
Cost Distribution
449+
</h3>
450+
<div className="flex items-center gap-8">
451+
<div className="w-48 h-48 flex-shrink-0">
452+
<ResponsiveContainer width="100%" height="100%">
453+
<PieChart>
454+
<Pie
455+
data={breakdown.byProvider
456+
.filter((p) => p.cost > 0)
457+
.map((p) => ({
458+
name: p.provider,
459+
value: Math.round(p.cost * 100) / 100,
460+
}))}
461+
cx="50%"
462+
cy="50%"
463+
innerRadius="50%"
464+
outerRadius="85%"
465+
paddingAngle={2}
466+
dataKey="value"
467+
stroke="none"
468+
>
469+
{breakdown.byProvider
470+
.filter((p) => p.cost > 0)
471+
.map((_, i) => (
472+
<Cell
473+
key={i}
474+
fill={
475+
[
476+
'#6366f1',
477+
'#8b5cf6',
478+
'#ec4899',
479+
'#f97316',
480+
'#22c55e',
481+
'#06b6d4',
482+
'#3b82f6',
483+
'#eab308',
484+
][i % 8]
485+
}
486+
/>
487+
))}
488+
</Pie>
489+
<Tooltip
490+
contentStyle={{
491+
background: 'var(--color-bg-secondary, #1e293b)',
492+
border: '1px solid var(--color-border, #334155)',
493+
borderRadius: '8px',
494+
fontSize: '12px',
495+
}}
496+
formatter={(value: unknown) => [`$${Number(value).toFixed(4)}`, 'Cost']}
497+
/>
498+
</PieChart>
499+
</ResponsiveContainer>
500+
</div>
501+
<div className="space-y-2 min-w-0">
502+
{breakdown.byProvider
503+
.filter((p) => p.cost > 0)
504+
.map((p, i) => (
505+
<div key={p.provider} className="flex items-center gap-2 text-sm">
506+
<span
507+
className="w-3 h-3 rounded-full flex-shrink-0"
508+
style={{
509+
background: [
510+
'#6366f1',
511+
'#8b5cf6',
512+
'#ec4899',
513+
'#f97316',
514+
'#22c55e',
515+
'#06b6d4',
516+
'#3b82f6',
517+
'#eab308',
518+
][i % 8],
519+
}}
520+
/>
521+
<span className="text-text-muted dark:text-dark-text-muted capitalize truncate">
522+
{p.provider}
523+
</span>
524+
<span className="ml-auto font-medium text-text-primary dark:text-dark-text-primary whitespace-nowrap">
525+
{p.costFormatted}
526+
</span>
527+
<span className="text-text-muted dark:text-dark-text-muted text-xs w-12 text-right">
528+
{p.percentOfTotal.toFixed(0)}%
529+
</span>
530+
</div>
531+
))}
532+
</div>
533+
</div>
534+
</div>
535+
)}
536+
537+
{/* Provider Breakdown List */}
346538
<div className="card-elevated bg-bg-secondary dark:bg-dark-bg-secondary rounded-lg border border-border dark:border-dark-border">
347539
<div className="p-4 border-b border-border dark:border-dark-border">
348-
<h3 className="text-lg font-medium text-text-primary dark:text-dark-text-primary">
349-
By Provider
540+
<h3 className="text-sm font-medium text-text-primary dark:text-dark-text-primary flex items-center gap-2">
541+
<Layers className="w-4 h-4 text-orange-500" />
542+
Provider Details
350543
</h3>
351544
</div>
352545
<div className="divide-y divide-border dark:divide-dark-border">

0 commit comments

Comments
 (0)