diff --git a/frontend/src/components/NetWorthChart.test.tsx b/frontend/src/components/NetWorthChart.test.tsx
new file mode 100644
index 0000000..161e1fe
--- /dev/null
+++ b/frontend/src/components/NetWorthChart.test.tsx
@@ -0,0 +1,39 @@
+import { render, screen } from '@testing-library/react';
+import { describe, it, expect, vi } from 'vitest';
+
+import { NetWorthChart } from './NetWorthChart';
+
+vi.mock('echarts-for-react', () => ({
+ default: ({ option }: { option: unknown }) => (
+
{JSON.stringify(option).length}
+ ),
+}));
+
+describe('NetWorthChart', () => {
+ it('shows an empty-state when there are no points', () => {
+ render();
+ expect(screen.getByText('No history yet.')).toBeInTheDocument();
+ });
+
+ it('renders an ECharts option per account', () => {
+ render(
+ ,
+ );
+ expect(screen.getByTestId('chart')).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/components/NetWorthChart.tsx b/frontend/src/components/NetWorthChart.tsx
new file mode 100644
index 0000000..5ee013e
--- /dev/null
+++ b/frontend/src/components/NetWorthChart.tsx
@@ -0,0 +1,78 @@
+/**
+ * Stacked area chart of net worth by account over time.
+ *
+ * Reads from `GET /networth/history` and groups by account name. Uses
+ * ECharts (sankey/fan/waterfall come from the same lib in later phases,
+ * keeping bundle simple).
+ */
+import { useMemo } from 'react';
+import ReactECharts from 'echarts-for-react';
+import type { EChartsOption } from 'echarts';
+
+import type { api } from '@/api/client';
+import { gbpCompact } from '@/lib/format';
+
+type History = Awaited>;
+
+interface Props {
+ history: History;
+ height?: number;
+}
+
+export function NetWorthChart({ history, height = 360 }: Props) {
+ const option = useMemo(() => buildOption(history), [history]);
+
+ if (history.points.length === 0) {
+ return No history yet.
;
+ }
+
+ return ;
+}
+
+function buildOption(history: History): EChartsOption {
+ const accountNames = collectAccountNames(history);
+
+ const seriesByAccount = accountNames.map((name) => ({
+ name,
+ type: 'line' as const,
+ stack: 'nw',
+ areaStyle: { opacity: 0.6 },
+ smooth: true,
+ showSymbol: false,
+ emphasis: { focus: 'series' as const },
+ data: history.points.map((p) => Number(p.by_account[name] ?? 0)),
+ }));
+
+ const dates = history.points.map((p) => p.snapshot_date);
+
+ return {
+ grid: { left: 60, right: 24, top: 24, bottom: 40 },
+ tooltip: {
+ trigger: 'axis',
+ valueFormatter: (v) => gbpCompact(typeof v === 'number' ? v : 0),
+ },
+ legend: {
+ bottom: 0,
+ type: 'scroll',
+ },
+ xAxis: {
+ type: 'category',
+ data: dates,
+ boundaryGap: false,
+ axisLabel: { hideOverlap: true },
+ },
+ yAxis: {
+ type: 'value',
+ axisLabel: { formatter: (v: number) => gbpCompact(v) },
+ },
+ series: seriesByAccount,
+ };
+}
+
+function collectAccountNames(history: History): string[] {
+ const names = new Set();
+ for (const point of history.points) {
+ for (const name of Object.keys(point.by_account)) names.add(name);
+ }
+ return [...names].sort();
+}
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx
index 2a2d654..0b2706c 100644
--- a/frontend/src/pages/Dashboard.tsx
+++ b/frontend/src/pages/Dashboard.tsx
@@ -1,14 +1,23 @@
/**
- * Dashboard — landing page. Wire up gets fleshed out in Phase 1a.
- * For now: confirm we can reach /networth and render today's NW.
+ * Dashboard — net worth at a glance.
+ *
+ * Three sections, top to bottom:
+ * 1. Headline NW total (latest snapshot)
+ * 2. Stacked-area history chart (per-account, /networth/history)
+ * 3. Per-account cards (latest values, /networth)
*/
import { useQuery } from '@tanstack/react-query';
import { api } from '@/api/client';
+import { NetWorthChart } from '@/components/NetWorthChart';
import { gbp } from '@/lib/format';
export function Dashboard() {
const nw = useQuery({ queryKey: ['networth', 'current'], queryFn: api.networth.current });
+ const history = useQuery({
+ queryKey: ['networth', 'history', 365],
+ queryFn: () => api.networth.history(365),
+ });
if (nw.isLoading) {
return Loading net worth…
;
@@ -45,10 +54,23 @@ export function Dashboard() {
Net worth
As of {data.snapshot_date}
+
{gbp(data.total_gbp)}
{data.accounts.length} accounts
+
+
+
Last 12 months
+ {history.isLoading ? (
+
Loading…
+ ) : history.isError || !history.data ? (
+
History unavailable.
+ ) : (
+
+ )}
+
+
{data.accounts.map((a) => (