`;
html += `
`; // end summary grid
}
// Accumulation Table
html += `Accumulation Phase Projection:
`; if(data.accumulationTable.length > 0) { html += ``;
html += `
`;
} else {
html += `| Year | Age | Start Balance | Savings | Growth | End Balance |
|---|---|---|---|---|---|
| ${row.year} | ${row.age} | ${formatCurrency(row.yearStartBalance)} | ${formatCurrency(row.savingsThisYear)} | ${formatCurrency(row.investmentGrowth)} | ${formatCurrency(row.endBalance)} |
No accumulation data to display.
`; } // Decumulation Table if (data.fireSummary.fireReached && data.decumulationTable.length > 0) { html += `Retirement (Decumulation) Phase Projection:
`; html += ``;
html += `
`;
} else if (data.fireSummary.fireReached) {
html += `| Ret. Year | Age | Start Balance | Withdrawal | Growth | End Balance |
|---|---|---|---|---|---|
| ${row.year} | ${row.age} | ${formatCurrency(row.startBalance)} | ${formatCurrency(row.withdrawal)} | ${formatCurrency(row.growth)} | ${formatCurrency(row.endBalance)} ${row.notes ? `(${row.notes})` : ''} |
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(); });