add optional "show 4%-rule" button with extra cards and reference lines
This commit is contained in:
parent
090cbcaecd
commit
bb5d17b9cf
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user