"use client"; import { useMemo, useState } from "react"; import { Area, AreaChart, Cell, Legend, Pie, PieChart, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis, } from "recharts"; import { formatChf } from "@/lib/format"; import type { PlanComputed } from "@/lib/calculations"; // Diagramm-Palette aus den Theme-Tokens (globals.css) statt fester Hex-Werte -- so passt sie // sich Hell/Dunkel/Warm an. Recharts reicht die Werte als SVG-Attribut durch, der Browser // löst `var(...)` dort auf. export const CHART_PALETTE = [ "var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)", "var(--chart-6)", ]; // Einheitliches Aussehen der Recharts-Tooltips (folgt dem Farbschema). export const TOOLTIP_STYLE = { background: "var(--surface)", border: "1px solid var(--border)", borderRadius: "0.5rem", fontSize: 12, color: "var(--fg)", } as const; const ASSET_CATS = ["PENSION_FUND", "PILLAR_3A", "REAL_ESTATE", "OTHER_ASSET"]; // Gestapelte Vermögensaufteilung. // // Bis 0.30 waren das gestapelte Balken mit je zwei Säulen pro Phase (Beginn/Ende) und schräg // gestellter Beschriftung -- schwer lesbar, sobald es mehr als zwei Phasen gab. Neu: // * eine gestapelte FLÄCHE über alle Planjahre (zeigt den Verlauf statt zweier Stichproben), // Phasengrenzen als feine senkrechte Linien, // * daneben ein RING für ein wählbares Jahr, der die RELATIVE Aufteilung zeigt. // Ein Klick in die Fläche wählt das Jahr des Rings. export function AllocationChart({ computed, height = 300, // Auf eine einzelne Lebensphase einschraenken (Detailansicht der Phase). Ohne Angabe wird // der ganze Plan gezeigt. onlyPhaseId, }: { computed: PlanComputed; height?: number; onlyPhaseId?: string; }) { const phases = useMemo( () => (onlyPhaseId ? computed.phases.filter((p) => p.id === onlyPhaseId) : computed.phases), [computed, onlyPhaseId] ); // Asset-Elemente (nach id, damit gleiche Namen nicht kollidieren), die irgendwann einen // positiven Wert haben -- in Reihenfolge ihres ersten Auftretens. const assetEls = useMemo(() => { const info = new Map(); for (const phase of phases) { for (const el of phase.elements) { if (!ASSET_CATS.includes(el.category)) continue; const cur = info.get(el.elementId) ?? { name: el.name, any: false }; cur.name = el.name; if (el.startValue > 0 || el.endValue > 0) cur.any = true; info.set(el.elementId, cur); } } return [...info.entries()].filter(([, v]) => v.any).map(([id, v]) => ({ id, name: v.name })); }, [phases]); // Je Planjahr eine Zeile mit dem Wert jedes Elements. Die Jahreswerte liegen bereits in // `ElementPhaseComputed.yearly` (seit 0.11) -- hier wird nur umsortiert, nichts gerechnet. const areaData = useMemo(() => { const byYear = new Map>(); const ageOf = new Map(); for (const phase of phases) { for (const el of phase.elements) { if (!ASSET_CATS.includes(el.category)) continue; for (const y of el.yearly) { const row = byYear.get(y.year) ?? {}; row[el.elementId] = Math.max(0, Math.round(y.value)); byYear.set(y.year, row); ageOf.set(y.year, y.age); } } } return [...byYear.entries()] .sort(([a], [b]) => a - b) .map(([year, values]) => ({ year, age: ageOf.get(year) ?? 0, ...values })); }, [phases]); // Phasengrenzen (kumulierte Dauer) für die Trennlinien. Bei einer einzelnen Phase gibt es keine. const boundaryAges = useMemo(() => { if (onlyPhaseId) return []; const out: number[] = []; let acc = 0; for (let i = 0; i < computed.phases.length - 1; i++) { acc += computed.phases[i].durationYears; const age = areaData.find((r) => r.year === acc)?.age; if (typeof age === "number") out.push(age); } return out; }, [computed, areaData, onlyPhaseId]); // Gewähltes Jahr für den Ring -- Vorgabe: das letzte (Endzustand). const [pickedYear, setPickedYear] = useState(null); const ringRow = areaData.find((r) => r.year === pickedYear) ?? areaData[areaData.length - 1]; const ringData = useMemo(() => { if (!ringRow) return []; return assetEls .map((el) => ({ name: el.name, value: Number(ringRow[el.id as keyof typeof ringRow] ?? 0) })) .filter((d) => d.value > 0); }, [assetEls, ringRow]); const ringTotal = ringData.reduce((s, d) => s + d.value, 0); if (assetEls.length === 0) { return

Dieser Plan enthält keine Vermögenselemente.

; } return (
{/* Verlauf */}
{onlyPhaseId ? "Verlauf in dieser Lebensphase" : "Verlauf über alle Planjahre"}{" "} · Klick wählt das Jahr für den Ring
{ const raw = state?.activeIndex ?? state?.activeTooltipIndex; const idx = typeof raw === "number" ? raw : Number(raw); if (!Number.isInteger(idx) || idx < 0 || idx >= areaData.length) return; setPickedYear(areaData[idx].year); }} > `${v} J.`} stroke="var(--faint)" /> Intl.NumberFormat("de-CH", { notation: "compact" }).format(v)} /> (typeof v === "number" ? formatChf(v) : v)} labelFormatter={(v) => `Alter ${v}`} /> {boundaryAges.map((age) => ( ))} {/* Markierung des Ring-Jahres. */} {ringRow && } {assetEls.map((el, i) => ( ))}
{/* Relative Aufteilung im gewählten Jahr */}
Aufteilung mit {ringRow?.age ?? "–"} Jahren {pickedYear !== null && ( )}
{ringData.map((d, i) => ( ))} typeof v === "number" ? `${formatChf(v)} · ${ringTotal > 0 ? Math.round((v / ringTotal) * 100) : 0} %` : v } />
Total {formatChf(ringTotal)}
); }