FIRE Planning Calculator

FIRE Planning Calculator

Your Current Situation & FIRE Goals

Simulation stops if FIRE not reached by this age.
Amount you save towards retirement each year.
Your desired spending in today's dollars.
Percentage of portfolio to withdraw in first year of retirement.

Financial & Economic Assumptions

Expected return on investments during accumulation.
Expected return on investments during retirement.
How long to simulate withdrawals after reaching FIRE.

Note: These are fixed average assumptions for an illustrative simulation. Real-world returns and inflation will vary and impact outcomes.

FIRE Projection & Simulation

Click "Calculate FIRE Projection" to view results. Ensure all inputs are set.

Total Withdrawn During Retirement: ${formatCurrency(data.retirementSummary.totalWithdrawnPostFIRE)}

`; html += `
`; // end summary grid } // Accumulation Table html += `

Accumulation Phase Projection:

`; if(data.accumulationTable.length > 0) { html += `
`; html += ``; data.accumulationTable.forEach(row => { html += ``; }); html += `
YearAgeStart BalanceSavingsGrowthEnd Balance
${row.year}${row.age} ${formatCurrency(row.yearStartBalance)}${formatCurrency(row.savingsThisYear)} ${formatCurrency(row.investmentGrowth)}${formatCurrency(row.endBalance)}
`; } else { html += `

No accumulation data to display.

`; } // Decumulation Table if (data.fireSummary.fireReached && data.decumulationTable.length > 0) { html += `

Retirement (Decumulation) Phase Projection:

`; html += `
`; html += ``; data.decumulationTable.forEach(row => { html += ``; }); html += `
Ret. YearAgeStart BalanceWithdrawalGrowthEnd Balance
${row.year}${row.age} ${formatCurrency(row.startBalance)}${formatCurrency(row.withdrawal)} ${formatCurrency(row.growth)} ${formatCurrency(row.endBalance)} ${row.notes ? `(${row.notes})` : ''}
`; } else if (data.fireSummary.fireReached) { html += `

No decumulation data to display (e.g., retirement duration was 0 or funds depleted immediately).

`; } simulationResultsDiv.innerHTML = html; } if (runSimulationBtn) { runSimulationBtn.addEventListener('click', runFIRESimulation); } // --- PDF Download --- function loadJsPdfIfNeeded(callback) { if (jsPdfLoaded) { if (callback) callback(); return; } const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js'; script.onload = () => { jsPdfLoaded = true; console.log("jsPDF loaded dynamically."); if (callback) callback(); }; script.onerror = () => { console.error("Failed to load jsPDF."); alert("Error: Could not load PDF library."); }; document.head.appendChild(script); } function downloadResultsAsPdf() { if (!jsPdfLoaded) { alert("PDF library not loaded."); return; } if (!simulationDataForPdf) { alert("No simulation data to download. Please run the simulation first."); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF({ unit: 'pt', format: 'a4' }); const data = simulationDataForPdf; const pageMargin = 40; const pageWidth = doc.internal.pageSize.getWidth() - 2 * pageMargin; let y = pageMargin; function addMainTitle(text) { doc.setFontSize(18); doc.setFont(undefined, 'bold'); doc.setTextColor(26, 83, 92); // Primary doc.text(text, doc.internal.pageSize.getWidth() / 2, y, { align: 'center' }); y += 35; } function addSectionTitle(text) { if (y > doc.internal.pageSize.getHeight() - 80) { doc.addPage(); y = pageMargin; } doc.setFontSize(14); doc.setFont(undefined, 'bold'); doc.setTextColor(78, 205, 196); // Secondary doc.text(text, pageMargin, y); y += 25; } function addLine(key, value, keyColor = [33,37,41], valueColor = [33,37,41]) { if (y > doc.internal.pageSize.getHeight() - 40) { doc.addPage(); y = pageMargin; } doc.setFontSize(10); doc.setFont(undefined, 'bold'); doc.setTextColor(keyColor[0], keyColor[1], keyColor[2]); doc.text(key, pageMargin, y); doc.setFont(undefined, 'normal'); doc.setTextColor(valueColor[0], valueColor[1], valueColor[2]); const valueText = String(value); doc.text(valueText, pageMargin + 240, y, { align: 'left', maxWidth: pageWidth - 240 - 5 }); y += 18; } function addInfo(text, isError = false) { if (y > doc.internal.pageSize.getHeight() - 50) { doc.addPage(); y = pageMargin; } doc.setFontSize(8.5); doc.setFont(undefined, 'italic'); doc.setTextColor(isError ? 220 : 108, isError ? 53 : 117, isError ? 69 : 125); const splitText = doc.splitTextToSize(text, pageWidth); doc.text(splitText, pageMargin, y); y += (doc.getTextDimensions(splitText).h) + 10; } function addTable(headers, tableData, columnWidths, highlightFireRowLogic = null) { if (y > doc.internal.pageSize.getHeight() - 60) { doc.addPage(); y = pageMargin; } doc.setFontSize(8); const headerFillColor = [78, 205, 196]; // Secondary const headerTextColor = [26, 83, 92]; // Primary const rowTextColor = [33,37,41]; const fireRowFillColor = [209, 247, 213]; // Light green // Draw headers doc.setFillColor(headerFillColor[0], headerFillColor[1], headerFillColor[2]); doc.setTextColor(headerTextColor[0], headerTextColor[1], headerTextColor[2]); doc.setFont(undefined, 'bold'); let currentX = pageMargin; headers.forEach((header, i) => { doc.rect(currentX, y, columnWidths[i], 20, 'F'); doc.text(header, currentX + 5, y + 14); currentX += columnWidths[i]; }); y += 20; doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]); doc.setFont(undefined, 'normal'); tableData.forEach((rowArray, rowIndex) => { if (y > doc.internal.pageSize.getHeight() - 35) { doc.addPage(); y = pageMargin; currentX = pageMargin; doc.setFillColor(headerFillColor[0], headerFillColor[1], headerFillColor[2]); doc.setTextColor(headerTextColor[0], headerTextColor[1], headerTextColor[2]); doc.setFont(undefined, 'bold'); headers.forEach((header, i) => { doc.rect(currentX, y, columnWidths[i], 20, 'F'); doc.text(header, currentX + 5, y + 14); currentX += columnWidths[i]; }); y += 20; doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]); doc.setFont(undefined, 'normal'); } currentX = pageMargin; let isFireRow = false; if (highlightFireRowLogic) { isFireRow = highlightFireRowLogic(rowArray, data.fireSummary.fireNumber); } if(isFireRow){ doc.setFillColor(fireRowFillColor[0], fireRowFillColor[1], fireRowFillColor[2]); doc.rect(currentX, y, pageWidth , 18, 'F'); // Highlight full row } rowArray.forEach((cell, i) => { if(!isFireRow) doc.rect(currentX, y, columnWidths[i], 18); // Draw cell border if not highlighted const cellText = String(cell); const textLines = doc.splitTextToSize(cellText, columnWidths[i] - 8); doc.text(textLines, currentX + 4, y + 12); currentX += columnWidths[i]; }); y += 18; }); y += 10; } addMainTitle("FIRE (Financial Independence, Retire Early) Report"); addInfo(`Report Generated: ${new Date().toLocaleString()}`); y += 10; addSectionTitle("Input Parameters"); addLine("Current Age:", `${data.inputs.currentAge} years`); addLine("Target Retirement Age (Max):", `${data.inputs.targetRetirementAge} years`); addLine("Current Portfolio Value:", formatCurrency(data.inputs.currentPortfolioValue)); addLine("Annual Savings:", formatCurrency(data.inputs.annualSavings)); addLine("Target Annual Expenses in Retirement:", formatCurrency(data.inputs.annualExpensesInRetirement)); addLine("Desired Safe Withdrawal Rate (SWR):", `${data.inputs.safeWithdrawalRatePct.toFixed(1)}%`); addLine("Avg. Annual Return (Pre-Retirement):", `${data.inputs.preRetirementReturnRatePct.toFixed(1)}%`); addLine("Avg. Annual Return (Post-Retirement):", `${data.inputs.postRetirementReturnRatePct.toFixed(1)}%`); addLine("Expected Avg. Annual Inflation:", `${data.inputs.inflationRatePct.toFixed(1)}%`); addLine("Retirement Duration (Post-FIRE):", `${data.inputs.retirementDuration} years`); addLine("Adjust Expenses for Inflation:", data.inputs.adjustExpensesForInflation ? "Yes" : "No"); y += 10; addSectionTitle("FIRE Projection Summary"); addLine("Your FIRE Number:", formatCurrency(data.fireSummary.fireNumber), [255,107,107], [255,107,107]); if (data.fireSummary.fireReached) { addLine("FIRE Reached:", "Yes", [40,167,69],[40,167,69]); addLine("Age at FIRE:", `${data.fireSummary.ageAtFIRE} years old`); addLine("Years to FIRE:", `${data.fireSummary.yearsToFIRE} years`); addLine("Portfolio Value at FIRE:", formatCurrency(data.fireSummary.portfolioAtFIRE)); } else { addLine("FIRE Reached by Target Age:", "No", [220,53,69],[220,53,69]); addLine("Projected Portfolio at Target Age (" + data.inputs.targetRetirementAge + "):", formatCurrency(data.fireSummary.portfolioAtFIRE)); } y += 10; if (data.fireSummary.fireReached) { addSectionTitle("Retirement Phase Summary"); if (data.retirementSummary.fundsDepletedYear !== null && data.retirementSummary.fundsDepletedYear <= data.inputs.retirementDuration) { addLine("Retirement Funds Longevity:", `Depleted in Year ${data.retirementSummary.fundsDepletedYear} of retirement`,[220,53,69],[220,53,69]); } else { addLine("Retirement Funds Longevity:", `Lasted for simulated ${data.inputs.retirementDuration} years`,[40,167,69],[40,167,69]); addLine("Final Balance After "+data.inputs.retirementDuration+" Years:", formatCurrency(data.retirementSummary.finalBalancePostFIRE)); } addLine("Total Withdrawn During Retirement:", formatCurrency(data.retirementSummary.totalWithdrawnPostFIRE)); y += 10; } addSectionTitle("Accumulation Phase Projection"); const accHeaders = ["Year", "Age", "Start Balance", "Savings", "Growth", "End Balance"]; const accColWidths = [40, 40, 110, 90, 90, 110]; const accTableFormattedData = data.accumulationTable.map(r => [ r.year, r.age, formatCurrency(r.yearStartBalance,0,0), formatCurrency(r.savingsThisYear,0,0), formatCurrency(r.investmentGrowth,0,0), formatCurrency(r.endBalance,0,0) ]); addTable(accHeaders, accTableFormattedData, accColWidths, (rowArray, fireNum) => { // Logic to check if this row's end balance (last element) reached FIRE // Need to parse it back from formatted string, or better, pass raw data to table func // For simplicity, we'll assume the last row where FIRE is reached is already marked in JS if needed // Or, pass the raw data.endBalance to check. // For this PDF, we can check if it's the last row of accumulation IF fireReached. const endBalanceStr = rowArray[5]; // End Balance is the 6th element const endBalanceNum = parseFloat(endBalanceStr.replace(/[$,]/g, '')); return data.fireSummary.fireReached && data.fireSummary.yearsToFIRE == rowArray[0] && endBalanceNum >= fireNum; }); if (data.fireSummary.fireReached && data.decumulationTable.length > 0) { addSectionTitle("Retirement (Decumulation) Phase Projection"); const decHeaders = ["Ret. Yr", "Age", "Start Balance", "Withdrawal", "Growth", "End Balance"]; const decColWidths = [50, 40, 110, 90, 90, 110]; const decTableFormattedData = data.decumulationTable.map(r => [ r.year, r.age, formatCurrency(r.startBalance,0,0), formatCurrency(r.withdrawal,0,0), formatCurrency(r.growth,0,0), formatCurrency(r.endBalance,0,0) + (r.notes ? ` (${r.notes.substring(0,10)}..)` : '') // Abbreviate notes for PDF ]); addTable(decHeaders, decTableFormattedData, decColWidths); } addInfo("Disclaimer: This simulation uses fixed average assumptions and does not account for market volatility, sequence of returns risk, taxes, or fees. It is for illustrative and educational purposes only and should not be considered financial advice. Consult with a qualified financial advisor for personalized retirement planning."); // Footer const pageCount = doc.internal.getNumberOfPages(); for (let i = 1; i <= pageCount; i++) { doc.setPage(i); doc.setFontSize(8); doc.setTextColor(150); doc.text(`Page ${i} of ${pageCount} - FIRE Planning Calculator`, pageMargin, doc.internal.pageSize.getHeight() - 20); } doc.save('FIRE_Planning_Projection.pdf'); } if (downloadPdfBtn) { downloadPdfBtn.addEventListener('click', () => loadJsPdfIfNeeded(downloadResultsAsPdf)); } // --- Initialization --- showTab(0); loadJsPdfIfNeeded(); });
Scroll to Top