import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, ReferenceLine, LabelList, } from "recharts"; import { formatCurrency } from "@/utils/currency"; import type { PortfolioSummary, HoldingResponse } from "@/api/investments"; import { TOOLTIP_STYLE } from "@/utils/chartTheme"; const COLORS = [ "#6366f1","#22c55e","#f97316","#ec4899","#14b8a6", "#f59e0b","#8b5cf6","#06b6d4","#84cc16","#ef4444", ]; const TYPE_COLORS: Record = { stock: "#6366f1", etf: "#22c55e", crypto: "#f97316", fund: "#14b8a6", bond: "#f59e0b", other: "#94a3b8", }; function typeColor(type: string) { return TYPE_COLORS[type.toLowerCase()] ?? TYPE_COLORS.other; } function DonutTooltip({ active, payload, currency }: any) { if (!active || !payload?.length) return null; const d = payload[0]; return (

{d.name}

{formatCurrency(d.value, currency)}

{d.payload.pct?.toFixed(1)}%

); } // ── Donut: allocation by holding ───────────────────────────────────────────── export function AllocationDonut({ portfolio }: { portfolio: PortfolioSummary }) { const val = (h: HoldingResponse) => Number(h.current_value || h.cost_basis_total); const total = portfolio.holdings.reduce((s, h) => s + val(h), 0); const data = portfolio.holdings .filter((h) => val(h) > 0) .map((h, i) => ({ name: h.symbol, value: val(h), pct: (val(h) / total) * 100, fill: COLORS[i % COLORS.length], })); if (data.length === 0) return null; return (

Allocation by Holding

{data.map((d) => ( ))} } />
{data.map((d) => (
{d.name} {d.pct.toFixed(1)}%
))}
); } // ── Donut: allocation by asset type ────────────────────────────────────────── export function AssetTypeDonut({ portfolio }: { portfolio: PortfolioSummary }) { const byType: Record = {}; for (const h of portfolio.holdings) { const t = (h.asset_type || "other").toLowerCase(); byType[t] = (byType[t] ?? 0) + Number(h.current_value || h.cost_basis_total); } const total = Object.values(byType).reduce((s, v) => s + v, 0); const data = Object.entries(byType) .filter(([, v]) => v > 0) .map(([type, value]) => ({ name: type.charAt(0).toUpperCase() + type.slice(1), value, pct: (value / total) * 100, fill: typeColor(type), })) .sort((a, b) => b.value - a.value); if (data.length === 0) return null; return (

Allocation by Asset Type

{data.map((d) => ( ))} } />
{data.map((d) => (
{d.name} {d.pct.toFixed(1)}%
))}
); } // ── Bar: cost basis vs current value ───────────────────────────────────────── function CurrencyYAxis({ value, currency }: { value: number; currency: string }) { if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`; if (value >= 1_000) return `${(value / 1_000).toFixed(0)}k`; return formatCurrency(value, currency); } export function CostVsValueChart({ portfolio }: { portfolio: PortfolioSummary }) { const data = portfolio.holdings .filter((h) => h.cost_basis_total > 0) .map((h) => ({ symbol: h.symbol, cost: h.cost_basis_total, value: h.current_value ?? h.cost_basis_total, gain: (h.current_value ?? h.cost_basis_total) - h.cost_basis_total, })) .sort((a, b) => b.value - a.value); if (data.length === 0) return null; return (

Cost Basis vs Current Value

CurrencyYAxis({ value: v, currency: portfolio.currency })} tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} axisLine={false} tickLine={false} width={52} /> [ formatCurrency(value, portfolio.currency), name === "cost" ? "Cost basis" : "Current value", ]} /> {data.map((d) => ( = 0 ? "hsl(var(--success))" : "hsl(var(--destructive))"} /> ))}
Cost basis
Current value (gain)
Current value (loss)
); } // ── Bar: return % per holding ───────────────────────────────────────────────── export function ReturnChart({ portfolio }: { portfolio: PortfolioSummary }) { const data = portfolio.holdings .filter((h) => h.unrealised_gain_pct != null) .map((h) => ({ symbol: h.symbol, pct: Number(h.unrealised_gain_pct), gain: h.unrealised_gain ?? 0, })) .sort((a, b) => b.pct - a.pct); if (data.length === 0) return null; const absMax = Math.max(...data.map((d) => Math.abs(d.pct)), 5); return (

Return % by Holding

`${v.toFixed(0)}%`} tick={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} axisLine={false} tickLine={false} /> [`${Number(value).toFixed(2)}%`, "Return"]} /> `${v >= 0 ? "+" : ""}${v.toFixed(1)}%`} style={{ fontSize: 10, fill: "hsl(var(--muted-foreground))" }} /> {data.map((d) => ( = 0 ? "hsl(var(--success))" : "hsl(var(--destructive))"} fillOpacity={0.85} /> ))}
); }