"use client"; import { useEffect, useState } from "react"; import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"; import { TOOLTIP_STYLE } from "@/components/AllocationChart"; import { formatChf } from "@/lib/format"; import type { PlanComputed } from "@/lib/calculations"; export interface TimelineSeries { // Eindeutiger Schlüssel der Serie. WICHTIG: Bis 0.30 diente der ANZEIGENAME als Datenschlüssel // -- zwei Szenarien mit gleichem Namen überschrieben sich dadurch gegenseitig (die Legende // zeigte beide, der Chart nur eine). Deshalb ein eigener, garantiert eindeutiger Schlüssel. id: string; label: string; color: string; computed: PlanComputed; // Plandaten werden gestrichelt gezeichnet, effektive Daten durchgezogen. Das Stilbudget // geht bewusst an Plan/Ist statt an nominal/real (siehe SPEZIFIKATION 9.29). dashed?: boolean; } // Datenpunkte je Serie: JEDES Planjahr (nicht nur die Phasengrenzen), verortet auf dem Alter // der Referenzperson (Person A). Der erste Punkt ist das Startvermögen vor Jahr 1. function pointsFor(computed: PlanComputed) { const phases = computed.phases; if (phases.length === 0) return [] as { age: number; nominal: number; real: number }[]; const startAge = phases[0].persons[0]?.startAge ?? 0; const start = Math.round(phases[0].startWealthNominal); const pts = [{ age: startAge, nominal: start, real: start }]; for (const y of computed.yearly) { pts.push({ age: y.age, nominal: y.wealthNominal, real: y.wealthReal }); } return pts; } // `var(--chart-1)` in einen konkreten Hex-Wert auflösen -- das Farbwahl-Feld (input[type=color]) // braucht einen echten Wert. Läuft nur im Browser und nur, wenn eine Variable übergeben wurde. function resolveColor(color: string): string { if (!color.startsWith("var(")) return color; if (typeof window === "undefined") return "#888888"; const name = color.slice(4, -1).trim(); const v = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return v || "#888888"; } // Liniendiagramm: Gesamtvermögen (nominal + real) über das Alter. Unterstützt mehrere // überlagerte Pläne für den Szenario-Vergleich. export function WealthChart({ series, metric = "nominal", onColorChange, }: { series: TimelineSeries[]; metric?: "nominal" | "real"; // Optional: erlaubt das Umfärben einer Serie über die Legende. onColorChange?: (id: string, color: string) => void; }) { // Aufgelöste Farben für die Farbwahl-Felder; nach dem ersten Rendern (und bei Theme-Wechsel). const [resolved, setResolved] = useState>({}); const colorKey = series.map((s) => `${s.id}:${s.color}`).join("|"); useEffect(() => { const next: Record = {}; for (const s of series) next[s.id] = resolveColor(s.color); // eslint-disable-next-line react-hooks/set-state-in-effect -- Farbauflösung braucht das DOM setResolved(next); // eslint-disable-next-line react-hooks/exhaustive-deps }, [colorKey]); if (series.length === 0 || series[0].computed.phases.length === 0) { return

Noch keine Phasen vorhanden.

; } const withPoints = series.map((s) => ({ ...s, points: pointsFor(s.computed) })); const ages = Array.from(new Set(withPoints.flatMap((s) => s.points.map((p) => p.age)))).sort((a, b) => a - b); const data = ages.map((age) => { const row: Record = { age }; for (const s of withPoints) { const pt = s.points.find((p) => p.age === age); row[s.id] = pt ? (metric === "real" ? pt.real : pt.nominal) : null; } return row; }); const nameById = new Map(series.map((s) => [s.id, s.label])); const hasDashed = series.some((s) => s.dashed); return (
`${v} J.`} /> Intl.NumberFormat("de-CH", { notation: "compact" }).format(v)} /> [typeof v === "number" ? formatChf(v) : v, nameById.get(String(key)) ?? key]} labelFormatter={(v) => `Alter ${v}`} /> {withPoints.map((s) => ( ))}
{/* Eigene Legende statt der von Recharts: Sie trägt die Farbwahl und erklärt den Linienstil -- ohne das wäre unklar, wofür die gestrichelte Linie steht. */}
{series.map((s) => ( {onColorChange ? ( ) : ( )} {s.label} ))} {hasDashed && ( gestrichelt = reine Planwerte · durchgezogen = mit effektiven Werten )}
); }