import { useMemo, useState } from "react"; import { Card, CardContent, CardHeader } from "@/components/ui/card"; import { TimeSeriesChart } from "@/components/charts/TimeSeriesChart"; import { SegmentedControl } from "@/components/charts/SegmentedControl"; import { ChartTypeToggle } from "@/components/charts/ChartTypeToggle"; import { portfolioValueHistory, mergeSnapshotsIntoHistory } from "@/lib/holdings"; import { periodChange } from "@/lib/balanceSheet"; import { toDateString, formatCompactMoney } from "@/lib/format"; import { usePrivacy } from "@/lib/privacy"; import { formatMoney } from "@/lib/format"; import type { Holding } from "@/types/finance"; import { TrendingDown, TrendingUp, LineChart as LineChartIcon, BarChart3 } from "lucide-react"; import { cn } from "@/lib/utils"; const PERIODS = [ { label: "1M", days: 30 }, { label: "3M", days: 90 }, { label: "1Y", days: 365 }, { label: "All", days: 999 }, ]; const PERIOD_OPTIONS = PERIODS.map((p) => ({ value: p.label, label: p.label })); const CHART_TYPES = [ { value: "line", icon: LineChartIcon, label: "Line" }, { value: "bar", icon: BarChart3, label: "Bar" }, ] as const; function fmt(v: number, c: string) { return formatMoney(v, c, { decimals: 2 }); } /** * "Total portfolio value" hero: the combined value of every same-currency * holding over time, see portfolioValueHistory for how sparse per-holding * history is turned into one series. A portfolio that's mostly brokerage- * synced (no stored history) will show a flatter early curve than reality, * an honest gap given the data actually available rather than a fabricated * smooth line - though as `snapshots` (a real recorded total per day, * see usePortfolioSnapshotOnMount) accumulate, they take over for any date * they cover, since they're the actual figure rather than an approximation. */ export function PortfolioValueChart({ holdings, currency, snapshots, }: { holdings: Holding[]; currency: string; snapshots?: { date: string; value: number }[]; }) { usePrivacy(); const [period, setPeriod] = useState("1Y"); const [chartType, setChartType] = useState<"line" | "bar">("line"); const allData = useMemo( () => mergeSnapshotsIntoHistory(portfolioValueHistory(holdings, currency), snapshots ?? []), [holdings, currency, snapshots] ); const data = useMemo(() => { const days = PERIODS.find((p) => p.label === period)?.days; if (!days || days >= 999) return allData; const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - days); const cutoffStr = toDateString(cutoff); return allData.filter((d) => d.date >= cutoffStr); }, [allData, period]); const change = useMemo(() => periodChange(data.map((d) => d.value)), [data]); const latest = data[data.length - 1]?.value ?? 0; return (

Total portfolio value

{fmt(latest, currency)}

{change && (

{change.amount > 0 ? ( ) : change.amount < 0 ? ( ) : null} 0 ? "text-chart-green" : change.amount < 0 ? "text-chart-red" : "text-foreground")}> {change.amount > 0 ? "+" : change.amount < 0 ? "-" : ""}{fmt(Math.abs(change.amount), currency)} {change.pct !== null && ` (${change.pct > 0 ? "+" : ""}${change.pct.toFixed(1)}%)`} over {period === "All" ? "all time" : period}

)}
{data.length === 0 ? (

No value history yet.

) : ( fmt(v, currency)} formatAxisValue={(v) => formatCompactMoney(v, currency)} chartType={chartType} /> )}
); }