Dashboard-Grafiken neu: Asset-Aufteilung Beginn/Ende je Phase; Vermoegensverlauf nach Alter
Deploy App / deploy (push) Successful in 56s

- Balkendiagramm: je Phase zwei x-Kategorien (Beginn/Ende), gestapelt nach Asset-Element
  (Schluessel = elementId, damit gleiche Namen nicht kollidieren). Zeigt die Umschichtung;
  Ende Phase N liegt neben Beginn Phase N+1.
- Liniendiagramm: x-Achse ist neu das Alter (Referenz Person A) von Startalter bis Ende der
  letzten Phase, mit Start- und Phasen-Endwerten (nominal + real), statt Phasenindex.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-14 08:59:28 +02:00
parent f8d77ef864
commit bbe5acee85
2 changed files with 70 additions and 31 deletions
+41 -17
View File
@@ -19,25 +19,38 @@ export interface TimelineSeries {
computed: PlanComputed;
}
// Liniendiagramm: Endvermoegen (nominal + real) je Lebensphase. Unterstuetzt mehrere
// Datenpunkte je Serie: Start (Beginn Phase 1) + Endwert je Phase, verortet auf dem
// Alter der Referenzperson (Person A). So laeuft die x-Achse ueber das Alter statt ueber
// die Phasen.
function pointsFor(computed: PlanComputed) {
const phases = computed.phases;
if (phases.length === 0) return [] as { age: number; nominal: number; real: number }[];
const refAge = (ph: PlanComputed["phases"][number]) => ph.persons[0]?.startAge ?? 0;
const pts = [
{ age: refAge(phases[0]), nominal: Math.round(phases[0].startWealthNominal), real: Math.round(phases[0].startWealthNominal) },
];
for (const p of phases) {
pts.push({ age: refAge(p) + p.durationYears, nominal: Math.round(p.endWealthNominal), real: Math.round(p.endWealthReal) });
}
return pts;
}
// Liniendiagramm: Gesamtvermoegen (nominal + real) ueber das Alter. Unterstuetzt mehrere
// ueberlagerte Plaene fuer den Szenario-Vergleich.
export function WealthChart({ series }: { series: TimelineSeries[] }) {
if (series.length === 0 || series[0].computed.phases.length === 0) {
return <p className="text-sm text-muted">Noch keine Phasen vorhanden.</p>;
}
// Datenpunkte je Phasen-Index; X-Achse = Phasenname des Hauptplans.
const maxLen = Math.max(...series.map((s) => s.computed.phases.length));
const data = Array.from({ length: maxLen }, (_, i) => {
const row: Record<string, number | string> = {
phase: series[0].computed.phases[i]?.name ?? `Phase ${i + 1}`,
};
for (const s of series) {
const p = s.computed.phases[i];
if (p) {
row[`${s.label} (nominal)`] = Math.round(p.endWealthNominal);
row[`${s.label} (real)`] = Math.round(p.endWealthReal);
}
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<string, number | null> = { age };
for (const s of withPoints) {
const pt = s.points.find((p) => p.age === age);
row[`${s.label} (nominal)`] = pt ? pt.nominal : null;
row[`${s.label} (real)`] = pt ? pt.real : null;
}
return row;
});
@@ -47,14 +60,23 @@ export function WealthChart({ series }: { series: TimelineSeries[] }) {
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 8, right: 16, left: 8, bottom: 8 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
<XAxis dataKey="phase" tick={{ fontSize: 11 }} />
<XAxis
dataKey="age"
type="number"
domain={["dataMin", "dataMax"]}
tick={{ fontSize: 11 }}
tickFormatter={(v) => `${v} J.`}
/>
<YAxis
tick={{ fontSize: 11 }}
tickFormatter={(v) => Intl.NumberFormat("de-CH", { notation: "compact" }).format(v)}
/>
<Tooltip formatter={(v) => (typeof v === "number" ? formatChf(v) : v)} />
<Tooltip
formatter={(v) => (typeof v === "number" ? formatChf(v) : v)}
labelFormatter={(v) => `Alter ${v}`}
/>
<Legend wrapperStyle={{ fontSize: 12 }} />
{series.map((s) => (
{withPoints.map((s) => (
<Line
key={`${s.label}-nominal`}
type="monotone"
@@ -62,9 +84,10 @@ export function WealthChart({ series }: { series: TimelineSeries[] }) {
stroke={s.color}
strokeWidth={2}
dot={false}
connectNulls
/>
))}
{series.map((s) => (
{withPoints.map((s) => (
<Line
key={`${s.label}-real`}
type="monotone"
@@ -73,6 +96,7 @@ export function WealthChart({ series }: { series: TimelineSeries[] }) {
strokeWidth={2}
strokeDasharray="5 3"
dot={false}
connectNulls
/>
))}
</LineChart>