add optional "show 4%-rule" button with extra cards and reference lines
This commit is contained in:
		| @@ -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 ( | ||||
|       <div className="bg-background border p-2 shadow-sm"> | ||||
|         <p className="font-medium">{`Year: ${data.year.toString()} (Age: ${data.age.toString()})`}</p> | ||||
|         <p className="text-chart-1">{`Balance: ${formatNumber(data.balance)}`}</p> | ||||
|         <p className="text-chart-2">{`Monthly allowance: ${formatNumber(data.monthlyAllowance)}`}</p> | ||||
|         <p className="text-orange-500">{`Balance: ${formatNumber(data.balance)}`}</p> | ||||
|         <p className="text-red-600">{`Monthly allowance: ${formatNumber(data.monthlyAllowance)}`}</p> | ||||
|         <p>{`Phase: ${data.phase === "accumulation" ? "Accumulation" : "Retirement"}`}</p> | ||||
|       </div> | ||||
|     ); | ||||
| @@ -113,6 +117,7 @@ const tooltipRenderer = ({ | ||||
| export default function FireCalculatorForm() { | ||||
|   const [result, setResult] = useState<CalculationResult | null>(null); | ||||
|   const irlYear = new Date().getFullYear(); | ||||
|   const [showing4percent, setShowing4percent] = useState(false); | ||||
|  | ||||
|   // Initialize form with default values | ||||
|   const form = useForm<FormValues>({ | ||||
| @@ -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 */} | ||||
|                         <YAxis | ||||
|                           yAxisId={"left"} | ||||
|                           orientation="left" | ||||
|                           yAxisId={"right"} | ||||
|                           orientation="right" | ||||
|                           tickFormatter={(value: number) => { | ||||
|                             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 */} | ||||
|                         <YAxis | ||||
|                           yAxisId="right" | ||||
|                           orientation="right" | ||||
|                           yAxisId="left" | ||||
|                           orientation="left" | ||||
|                           tickFormatter={(value: number) => { | ||||
|                             if (value >= 1000000) { | ||||
|                               return `${(value / 1000000).toPrecision(3)}M`; | ||||
| @@ -473,12 +501,12 @@ export default function FireCalculatorForm() { | ||||
|                           > | ||||
|                             <stop | ||||
|                               offset="5%" | ||||
|                               stopColor="var(--chart-1)" | ||||
|                               stopColor="var(--color-orange-500)" | ||||
|                               stopOpacity={0.8} | ||||
|                             /> | ||||
|                             <stop | ||||
|                               offset="95%" | ||||
|                               stopColor="var(--chart-1)" | ||||
|                               stopColor="var(--color-orange-500)" | ||||
|                               stopOpacity={0.1} | ||||
|                             /> | ||||
|                           </linearGradient> | ||||
| @@ -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"} | ||||
|                         /> | ||||
|                         <Area | ||||
|                           type="monotone" | ||||
|                           type="step" | ||||
|                           dataKey="monthlyAllowance" | ||||
|                           name="allowance" | ||||
|                           stroke="var(--chart-2)" | ||||
|                           stroke="var(--color-red-600)" | ||||
|                           fill="none" | ||||
|                           activeDot={{ r: 6 }} | ||||
|                           yAxisId="right" | ||||
|                           stackId={"a"} | ||||
|                           yAxisId="left" | ||||
|                         /> | ||||
|                         {result.fireNumber && ( | ||||
|                           <ReferenceLine | ||||
|                             y={result.fireNumber} | ||||
|                             stroke="var(--chart-3)" | ||||
|                             strokeWidth={1} | ||||
|                             strokeDasharray="2 2" | ||||
|                             stroke="var(--primary)" | ||||
|                             strokeWidth={2} | ||||
|                             strokeDasharray="2 1" | ||||
|                             label={{ | ||||
|                               value: "FIRE Number", | ||||
|                               position: "insideBottomRight", | ||||
|                             }} | ||||
|                             yAxisId={"left"} | ||||
|                             yAxisId={"right"} | ||||
|                           /> | ||||
|                         )} | ||||
|                         {result.fireNumber4percent && showing4percent && ( | ||||
|                           <ReferenceLine | ||||
|                             y={result.fireNumber4percent} | ||||
|                             stroke="var(--secondary)" | ||||
|                             strokeWidth={1} | ||||
|                             strokeDasharray="1 1" | ||||
|                             label={{ | ||||
|                               value: "4%-Rule FIRE Number", | ||||
|                               position: "insideBottomLeft", | ||||
|                             }} | ||||
|                             yAxisId={"right"} | ||||
|                           /> | ||||
|                         )} | ||||
|                         <ReferenceLine | ||||
| @@ -523,7 +563,7 @@ export default function FireCalculatorForm() { | ||||
|                             (form.getValues("retirementAge") - | ||||
|                               form.getValues("currentAge")) | ||||
|                           } | ||||
|                           stroke="var(--chart-2)" | ||||
|                           stroke="var(--primary)" | ||||
|                           strokeWidth={2} | ||||
|                           label={{ | ||||
|                             value: "Retirement", | ||||
| @@ -531,11 +571,36 @@ export default function FireCalculatorForm() { | ||||
|                           }} | ||||
|                           yAxisId={"left"} | ||||
|                         /> | ||||
|                         {result.retirementAge4percent && showing4percent && ( | ||||
|                           <ReferenceLine | ||||
|                             x={ | ||||
|                               irlYear + | ||||
|                               (result.retirementAge4percent - | ||||
|                                 form.getValues("currentAge")) | ||||
|                             } | ||||
|                             stroke="var(--secondary)" | ||||
|                             strokeWidth={1} | ||||
|                             label={{ | ||||
|                               value: "4%-Rule Retirement", | ||||
|                               position: "insideBottomLeft", | ||||
|                             }} | ||||
|                             yAxisId={"left"} | ||||
|                           /> | ||||
|                         )} | ||||
|                       </AreaChart> | ||||
|                     </ChartContainer> | ||||
|                   </CardContent> | ||||
|                 </Card> | ||||
|               )} | ||||
|               {result && ( | ||||
|                 <Button | ||||
|                   onClick={() => setShowing4percent(!showing4percent)} | ||||
|                   variant={showing4percent ? "secondary" : "default"} | ||||
|                   size={"sm"} | ||||
|                 > | ||||
|                   {showing4percent ? "Hide" : "Show"} 4%-Rule | ||||
|                 </Button> | ||||
|               )} | ||||
|             </form> | ||||
|           </Form> | ||||
|         </CardContent> | ||||
| @@ -579,6 +644,40 @@ export default function FireCalculatorForm() { | ||||
|                   </p> | ||||
|                 </CardContent> | ||||
|               </Card> | ||||
|               {showing4percent && ( | ||||
|                 <> | ||||
|                   <Card> | ||||
|                     <CardHeader> | ||||
|                       <CardTitle>4%-Rule FIRE Number</CardTitle> | ||||
|                       <CardDescription className="text-xs"> | ||||
|                         Capital needed for 4% of it to be greater than your | ||||
|                         yearly allowance | ||||
|                       </CardDescription> | ||||
|                     </CardHeader> | ||||
|                     <CardContent> | ||||
|                       <p className="text-3xl font-bold"> | ||||
|                         {formatNumber(result.fireNumber4percent)} | ||||
|                       </p> | ||||
|                     </CardContent> | ||||
|                   </Card> | ||||
|  | ||||
|                   <Card> | ||||
|                     <CardHeader> | ||||
|                       <CardTitle>4%-Rule Retirement Duration</CardTitle> | ||||
|                       <CardDescription className="text-xs"> | ||||
|                         Years to enjoy your financial independence if you follow | ||||
|                         the 4% rule | ||||
|                       </CardDescription> | ||||
|                     </CardHeader> | ||||
|                     <CardContent> | ||||
|                       <p className="text-3xl font-bold"> | ||||
|                         {form.getValues("lifeExpectancy") - | ||||
|                           (result.retirementAge4percent ?? 0)} | ||||
|                       </p> | ||||
|                     </CardContent> | ||||
|                   </Card> | ||||
|                 </> | ||||
|               )} | ||||
|             </> | ||||
|           )} | ||||
|         </div> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user