From bb5d17b9cfa05c8cfd38ce9f22511cdcb82f59bf Mon Sep 17 00:00:00 2001 From: Felix Schulze Date: Sun, 4 May 2025 19:44:18 +0200 Subject: [PATCH] add optional "show 4%-rule" button with extra cards and reference lines --- src/app/components/FireCalculatorForm.tsx | 147 ++++++++++++++++++---- 1 file changed, 123 insertions(+), 24 deletions(-) diff --git a/src/app/components/FireCalculatorForm.tsx b/src/app/components/FireCalculatorForm.tsx index e183f6c..a7b8332 100644 --- a/src/app/components/FireCalculatorForm.tsx +++ b/src/app/components/FireCalculatorForm.tsx @@ -73,12 +73,16 @@ interface YearlyData { age: number; year: number; balance: number; + untouchedBalance: number; phase: "accumulation" | "retirement"; monthlyAllowance: number; + untouchedMonthlyAllowance: number; } interface CalculationResult { fireNumber: number | null; + fireNumber4percent: number | null; + retirementAge4percent: number | null; yearlyData: YearlyData[]; error?: string; } @@ -101,8 +105,8 @@ const tooltipRenderer = ({ return (

{`Year: ${data.year.toString()} (Age: ${data.age.toString()})`}

-

{`Balance: ${formatNumber(data.balance)}`}

-

{`Monthly allowance: ${formatNumber(data.monthlyAllowance)}`}

+

{`Balance: ${formatNumber(data.balance)}`}

+

{`Monthly allowance: ${formatNumber(data.monthlyAllowance)}`}

{`Phase: ${data.phase === "accumulation" ? "Accumulation" : "Retirement"}`}

); @@ -113,6 +117,7 @@ const tooltipRenderer = ({ export default function FireCalculatorForm() { const [result, setResult] = useState(null); const irlYear = new Date().getFullYear(); + const [showing4percent, setShowing4percent] = useState(false); // Initialize form with default values const form = useForm({ @@ -149,8 +154,10 @@ export default function FireCalculatorForm() { age: age, year: irlYear, balance: startingCapital, + untouchedBalance: startingCapital, phase: "accumulation", - monthlyAllowance: initialMonthlyAllowance, + monthlyAllowance: 0, + untouchedMonthlyAllowance: initialMonthlyAllowance, }); // Calculate accumulation phase (before retirement) @@ -175,13 +182,18 @@ export default function FireCalculatorForm() { newBalance = previousYearData.balance * annualGrowthRate - inflatedAllowance * 12; } - + const untouchedBalance = + previousYearData.untouchedBalance * annualGrowthRate + + monthlySavings * 12; + const allowance = phase === "retirement" ? inflatedAllowance : 0; yearlyData.push({ age: currentAge, year: year, balance: newBalance, + untouchedBalance: untouchedBalance, phase: phase, - monthlyAllowance: inflatedAllowance, + monthlyAllowance: allowance, + untouchedMonthlyAllowance: inflatedAllowance, }); } @@ -192,9 +204,23 @@ export default function FireCalculatorForm() { ); const retirementData = yearlyData[retirementIndex]; + const [fireNumber4percent, retirementAge4percent] = (() => { + for (const yearData of yearlyData) { + if ( + yearData.untouchedBalance > + (yearData.untouchedMonthlyAllowance * 12) / 0.04 + ) { + return [yearData.untouchedBalance, yearData.age]; + } + } + return [0, 0]; + })(); + if (retirementIndex === -1 || !retirementData) { setResult({ fireNumber: null, + fireNumber4percent: null, + retirementAge4percent: null, error: "Could not calculate retirement data", yearlyData: yearlyData, }); @@ -202,6 +228,8 @@ export default function FireCalculatorForm() { // Set the result setResult({ fireNumber: retirementData.balance, + fireNumber4percent: fireNumber4percent, + retirementAge4percent: retirementAge4percent, yearlyData: yearlyData, }); } @@ -430,10 +458,10 @@ export default function FireCalculatorForm() { offset: -10, }} /> - {/* Left Y axis */} + {/* Right Y axis */} { if (value >= 1000000) { return `${(value / 1000000).toPrecision(3)}M`; @@ -448,10 +476,10 @@ export default function FireCalculatorForm() { }} width={30} /> - {/* Right Y axis */} + {/* Left Y axis */} { if (value >= 1000000) { return `${(value / 1000000).toPrecision(3)}M`; @@ -473,12 +501,12 @@ export default function FireCalculatorForm() { > @@ -487,34 +515,46 @@ export default function FireCalculatorForm() { type="monotone" dataKey="balance" name="balance" - stroke="var(--chart-1)" + stroke="var(--color-orange-500)" fill="url(#fillBalance)" fillOpacity={0.9} activeDot={{ r: 6 }} - yAxisId={"left"} + yAxisId={"right"} stackId={"a"} /> {result.fireNumber && ( + )} + {result.fireNumber4percent && showing4percent && ( + )} + {result.retirementAge4percent && showing4percent && ( + + )} )} + {result && ( + + )} @@ -579,6 +644,40 @@ export default function FireCalculatorForm() {

+ {showing4percent && ( + <> + + + 4%-Rule FIRE Number + + Capital needed for 4% of it to be greater than your + yearly allowance + + + +

+ {formatNumber(result.fireNumber4percent)} +

+
+
+ + + + 4%-Rule Retirement Duration + + Years to enjoy your financial independence if you follow + the 4% rule + + + +

+ {form.getValues("lifeExpectancy") - + (result.retirementAge4percent ?? 0)} +

+
+
+ + )} )}