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; |   age: number; | ||||||
|   year: number; |   year: number; | ||||||
|   balance: number; |   balance: number; | ||||||
|  |   untouchedBalance: number; | ||||||
|   phase: "accumulation" | "retirement"; |   phase: "accumulation" | "retirement"; | ||||||
|   monthlyAllowance: number; |   monthlyAllowance: number; | ||||||
|  |   untouchedMonthlyAllowance: number; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface CalculationResult { | interface CalculationResult { | ||||||
|   fireNumber: number | null; |   fireNumber: number | null; | ||||||
|  |   fireNumber4percent: number | null; | ||||||
|  |   retirementAge4percent: number | null; | ||||||
|   yearlyData: YearlyData[]; |   yearlyData: YearlyData[]; | ||||||
|   error?: string; |   error?: string; | ||||||
| } | } | ||||||
| @@ -101,8 +105,8 @@ const tooltipRenderer = ({ | |||||||
|     return ( |     return ( | ||||||
|       <div className="bg-background border p-2 shadow-sm"> |       <div className="bg-background border p-2 shadow-sm"> | ||||||
|         <p className="font-medium">{`Year: ${data.year.toString()} (Age: ${data.age.toString()})`}</p> |         <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-orange-500">{`Balance: ${formatNumber(data.balance)}`}</p> | ||||||
|         <p className="text-chart-2">{`Monthly allowance: ${formatNumber(data.monthlyAllowance)}`}</p> |         <p className="text-red-600">{`Monthly allowance: ${formatNumber(data.monthlyAllowance)}`}</p> | ||||||
|         <p>{`Phase: ${data.phase === "accumulation" ? "Accumulation" : "Retirement"}`}</p> |         <p>{`Phase: ${data.phase === "accumulation" ? "Accumulation" : "Retirement"}`}</p> | ||||||
|       </div> |       </div> | ||||||
|     ); |     ); | ||||||
| @@ -113,6 +117,7 @@ const tooltipRenderer = ({ | |||||||
| export default function FireCalculatorForm() { | export default function FireCalculatorForm() { | ||||||
|   const [result, setResult] = useState<CalculationResult | null>(null); |   const [result, setResult] = useState<CalculationResult | null>(null); | ||||||
|   const irlYear = new Date().getFullYear(); |   const irlYear = new Date().getFullYear(); | ||||||
|  |   const [showing4percent, setShowing4percent] = useState(false); | ||||||
|  |  | ||||||
|   // Initialize form with default values |   // Initialize form with default values | ||||||
|   const form = useForm<FormValues>({ |   const form = useForm<FormValues>({ | ||||||
| @@ -149,8 +154,10 @@ export default function FireCalculatorForm() { | |||||||
|       age: age, |       age: age, | ||||||
|       year: irlYear, |       year: irlYear, | ||||||
|       balance: startingCapital, |       balance: startingCapital, | ||||||
|  |       untouchedBalance: startingCapital, | ||||||
|       phase: "accumulation", |       phase: "accumulation", | ||||||
|       monthlyAllowance: initialMonthlyAllowance, |       monthlyAllowance: 0, | ||||||
|  |       untouchedMonthlyAllowance: initialMonthlyAllowance, | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     // Calculate accumulation phase (before retirement) |     // Calculate accumulation phase (before retirement) | ||||||
| @@ -175,13 +182,18 @@ export default function FireCalculatorForm() { | |||||||
|         newBalance = |         newBalance = | ||||||
|           previousYearData.balance * annualGrowthRate - inflatedAllowance * 12; |           previousYearData.balance * annualGrowthRate - inflatedAllowance * 12; | ||||||
|       } |       } | ||||||
|  |       const untouchedBalance = | ||||||
|  |         previousYearData.untouchedBalance * annualGrowthRate + | ||||||
|  |         monthlySavings * 12; | ||||||
|  |       const allowance = phase === "retirement" ? inflatedAllowance : 0; | ||||||
|       yearlyData.push({ |       yearlyData.push({ | ||||||
|         age: currentAge, |         age: currentAge, | ||||||
|         year: year, |         year: year, | ||||||
|         balance: newBalance, |         balance: newBalance, | ||||||
|  |         untouchedBalance: untouchedBalance, | ||||||
|         phase: phase, |         phase: phase, | ||||||
|         monthlyAllowance: inflatedAllowance, |         monthlyAllowance: allowance, | ||||||
|  |         untouchedMonthlyAllowance: inflatedAllowance, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -192,9 +204,23 @@ export default function FireCalculatorForm() { | |||||||
|     ); |     ); | ||||||
|     const retirementData = yearlyData[retirementIndex]; |     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) { |     if (retirementIndex === -1 || !retirementData) { | ||||||
|       setResult({ |       setResult({ | ||||||
|         fireNumber: null, |         fireNumber: null, | ||||||
|  |         fireNumber4percent: null, | ||||||
|  |         retirementAge4percent: null, | ||||||
|         error: "Could not calculate retirement data", |         error: "Could not calculate retirement data", | ||||||
|         yearlyData: yearlyData, |         yearlyData: yearlyData, | ||||||
|       }); |       }); | ||||||
| @@ -202,6 +228,8 @@ export default function FireCalculatorForm() { | |||||||
|       // Set the result |       // Set the result | ||||||
|       setResult({ |       setResult({ | ||||||
|         fireNumber: retirementData.balance, |         fireNumber: retirementData.balance, | ||||||
|  |         fireNumber4percent: fireNumber4percent, | ||||||
|  |         retirementAge4percent: retirementAge4percent, | ||||||
|         yearlyData: yearlyData, |         yearlyData: yearlyData, | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
| @@ -430,10 +458,10 @@ export default function FireCalculatorForm() { | |||||||
|                             offset: -10, |                             offset: -10, | ||||||
|                           }} |                           }} | ||||||
|                         /> |                         /> | ||||||
|                         {/* Left Y axis */} |                         {/* Right Y axis */} | ||||||
|                         <YAxis |                         <YAxis | ||||||
|                           yAxisId={"left"} |                           yAxisId={"right"} | ||||||
|                           orientation="left" |                           orientation="right" | ||||||
|                           tickFormatter={(value: number) => { |                           tickFormatter={(value: number) => { | ||||||
|                             if (value >= 1000000) { |                             if (value >= 1000000) { | ||||||
|                               return `${(value / 1000000).toPrecision(3)}M`; |                               return `${(value / 1000000).toPrecision(3)}M`; | ||||||
| @@ -448,10 +476,10 @@ export default function FireCalculatorForm() { | |||||||
|                           }} |                           }} | ||||||
|                           width={30} |                           width={30} | ||||||
|                         /> |                         /> | ||||||
|                         {/* Right Y axis */} |                         {/* Left Y axis */} | ||||||
|                         <YAxis |                         <YAxis | ||||||
|                           yAxisId="right" |                           yAxisId="left" | ||||||
|                           orientation="right" |                           orientation="left" | ||||||
|                           tickFormatter={(value: number) => { |                           tickFormatter={(value: number) => { | ||||||
|                             if (value >= 1000000) { |                             if (value >= 1000000) { | ||||||
|                               return `${(value / 1000000).toPrecision(3)}M`; |                               return `${(value / 1000000).toPrecision(3)}M`; | ||||||
| @@ -473,12 +501,12 @@ export default function FireCalculatorForm() { | |||||||
|                           > |                           > | ||||||
|                             <stop |                             <stop | ||||||
|                               offset="5%" |                               offset="5%" | ||||||
|                               stopColor="var(--chart-1)" |                               stopColor="var(--color-orange-500)" | ||||||
|                               stopOpacity={0.8} |                               stopOpacity={0.8} | ||||||
|                             /> |                             /> | ||||||
|                             <stop |                             <stop | ||||||
|                               offset="95%" |                               offset="95%" | ||||||
|                               stopColor="var(--chart-1)" |                               stopColor="var(--color-orange-500)" | ||||||
|                               stopOpacity={0.1} |                               stopOpacity={0.1} | ||||||
|                             /> |                             /> | ||||||
|                           </linearGradient> |                           </linearGradient> | ||||||
| @@ -487,34 +515,46 @@ export default function FireCalculatorForm() { | |||||||
|                           type="monotone" |                           type="monotone" | ||||||
|                           dataKey="balance" |                           dataKey="balance" | ||||||
|                           name="balance" |                           name="balance" | ||||||
|                           stroke="var(--chart-1)" |                           stroke="var(--color-orange-500)" | ||||||
|                           fill="url(#fillBalance)" |                           fill="url(#fillBalance)" | ||||||
|                           fillOpacity={0.9} |                           fillOpacity={0.9} | ||||||
|                           activeDot={{ r: 6 }} |                           activeDot={{ r: 6 }} | ||||||
|                           yAxisId={"left"} |                           yAxisId={"right"} | ||||||
|                           stackId={"a"} |                           stackId={"a"} | ||||||
|                         /> |                         /> | ||||||
|                         <Area |                         <Area | ||||||
|                           type="monotone" |                           type="step" | ||||||
|                           dataKey="monthlyAllowance" |                           dataKey="monthlyAllowance" | ||||||
|                           name="allowance" |                           name="allowance" | ||||||
|                           stroke="var(--chart-2)" |                           stroke="var(--color-red-600)" | ||||||
|                           fill="none" |                           fill="none" | ||||||
|                           activeDot={{ r: 6 }} |                           activeDot={{ r: 6 }} | ||||||
|                           yAxisId="right" |                           yAxisId="left" | ||||||
|                           stackId={"a"} |  | ||||||
|                         /> |                         /> | ||||||
|                         {result.fireNumber && ( |                         {result.fireNumber && ( | ||||||
|                           <ReferenceLine |                           <ReferenceLine | ||||||
|                             y={result.fireNumber} |                             y={result.fireNumber} | ||||||
|                             stroke="var(--chart-3)" |                             stroke="var(--primary)" | ||||||
|                             strokeWidth={1} |                             strokeWidth={2} | ||||||
|                             strokeDasharray="2 2" |                             strokeDasharray="2 1" | ||||||
|                             label={{ |                             label={{ | ||||||
|                               value: "FIRE Number", |                               value: "FIRE Number", | ||||||
|                               position: "insideBottomRight", |                               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 |                         <ReferenceLine | ||||||
| @@ -523,7 +563,7 @@ export default function FireCalculatorForm() { | |||||||
|                             (form.getValues("retirementAge") - |                             (form.getValues("retirementAge") - | ||||||
|                               form.getValues("currentAge")) |                               form.getValues("currentAge")) | ||||||
|                           } |                           } | ||||||
|                           stroke="var(--chart-2)" |                           stroke="var(--primary)" | ||||||
|                           strokeWidth={2} |                           strokeWidth={2} | ||||||
|                           label={{ |                           label={{ | ||||||
|                             value: "Retirement", |                             value: "Retirement", | ||||||
| @@ -531,11 +571,36 @@ export default function FireCalculatorForm() { | |||||||
|                           }} |                           }} | ||||||
|                           yAxisId={"left"} |                           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> |                       </AreaChart> | ||||||
|                     </ChartContainer> |                     </ChartContainer> | ||||||
|                   </CardContent> |                   </CardContent> | ||||||
|                 </Card> |                 </Card> | ||||||
|               )} |               )} | ||||||
|  |               {result && ( | ||||||
|  |                 <Button | ||||||
|  |                   onClick={() => setShowing4percent(!showing4percent)} | ||||||
|  |                   variant={showing4percent ? "secondary" : "default"} | ||||||
|  |                   size={"sm"} | ||||||
|  |                 > | ||||||
|  |                   {showing4percent ? "Hide" : "Show"} 4%-Rule | ||||||
|  |                 </Button> | ||||||
|  |               )} | ||||||
|             </form> |             </form> | ||||||
|           </Form> |           </Form> | ||||||
|         </CardContent> |         </CardContent> | ||||||
| @@ -579,6 +644,40 @@ export default function FireCalculatorForm() { | |||||||
|                   </p> |                   </p> | ||||||
|                 </CardContent> |                 </CardContent> | ||||||
|               </Card> |               </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> |         </div> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user