`;
}
html += ``;
html += `
`;
// Accumulation Table (if applicable)
if(data.accumulationTable.length > 0) {
html += `Total Withdrawn During Retirement: ${formatCurrency(data.retirementSummary.totalWithdrawn)}
Accumulation Phase Projection (Up to Age ${data.inputs.retirementAge}):
`; html += ``;
html += `
`;
} else {
html += ``;
}
// Decumulation Table
if (data.decumulationTable.length > 0) {
html += `| Year | Age | Start Balance | Contributions | Growth | End Balance |
|---|---|---|---|---|---|
| ${row.year} | ${row.age} | ${formatCurrency(row.yearStartBalance)} | ${formatCurrency(row.savingsThisYear)} | ${formatCurrency(row.investmentGrowth)} | ${formatCurrency(row.endBalance)} |
Retirement (Decumulation) Phase Projection:
`; html += ``;
html += `
`;
} else {
html += `| Ret. Year | Age | Start Balance | Annual Return | Withdrawal | Growth | End Balance |
|---|---|---|---|---|---|---|
| ${row.year} | ${row.age} | ${formatCurrency(row.startBalance)} | ${formatPercent(row.annualReturnUsed/100, 1)} | ${formatCurrency(row.withdrawal)} | ${formatCurrency(row.growth)} | ${formatCurrency(row.endBalance)} ${row.notes && !row.notes.includes('Poor Return Year') ? `(${row.notes})` : ''} |
No decumulation phase to display.
`; } simulationResultsDiv.innerHTML = html; } if (runSimulationBtn) { runSimulationBtn.addEventListener('click', runSORRSimulation); } // --- 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(44, 62, 80); // 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(52, 152, 219); // Secondary doc.text(text, pageMargin, y); y += 25; } function addLine(key, value, keyColor = [52,73,94], valueColor = [52,73,94]) { 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 ? 231 : 108, isError ? 76 : 120, isError ? 60 : 125); const splitText = doc.splitTextToSize(text, pageWidth); doc.text(splitText, pageMargin, y); y += (doc.getTextDimensions(splitText).h) + 10; } function addTable(headers, tableData, columnWidths) { if (y > doc.internal.pageSize.getHeight() - 60) { doc.addPage(); y = pageMargin; } doc.setFontSize(8); const headerFillColor = [52, 152, 219]; const headerTextColor = [255,255,255]; const rowTextColor = [52,73,94]; 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) => { 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; rowArray.forEach((cell, i) => { doc.rect(currentX, y, columnWidths[i], 18); const cellText = String(cell); // Check if cell is for "Annual Return" and if it's a poor return year let cellFillColor = null; if (headers[i] === "Ann. Return (%)" && cell.includes('-')) { // Simple check for negative return // Could also pass the raw 'notes' field for more precise poor year highlighting } if (cellText.toLowerCase().includes("depleted")) { doc.setTextColor(220, 53, 69); // Error color } else { doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]); } const textLines = doc.splitTextToSize(cellText, columnWidths[i] - 8); doc.text(textLines, currentX + 4, y + 12); currentX += columnWidths[i]; }); doc.setTextColor(rowTextColor[0], rowTextColor[1], rowTextColor[2]); // Reset color y += 18; }); y += 10; } addMainTitle("Sequence of Returns Risk Simulation Report"); addInfo(`Report Generated: ${new Date().toLocaleString()}`); y += 10; addSectionTitle("Input Parameters"); addLine("Current Age:", `${data.inputs.currentAge} years`); addLine("Retirement Age:", `${data.inputs.retirementAge} years`); addLine("Initial Portfolio (at Current Age/Retirement):", formatCurrency(data.inputs.initialPortfolioAtRetirementInput)); if (data.inputs.currentAge < data.inputs.retirementAge) { addLine("Annual Savings (Pre-Retirement):", formatCurrency(data.inputs.annualSavings)); addLine("Avg. Annual Return (Accumulation):", formatPercent(data.inputs.accumulationReturnRatePct/100)); } addLine("Initial Annual Withdrawal:", formatCurrency(data.inputs.initialAnnualWithdrawal)); addLine("Retirement Duration Simulated:", `${data.inputs.retirementDuration} years`); addLine("Number of Initial Poor Return Years:", `${data.inputs.numPoorReturnYears} years`); addLine("Annual Return During Poor Years:", formatPercent(data.inputs.poorReturnRatePct/100)); addLine("Avg. Annual Return (Normal Retirement Years):", formatPercent(data.inputs.normalReturnRatePct/100)); addLine("Expected Avg. Annual Inflation:", formatPercent(data.inputs.inflationRatePct/100)); addLine("Adjust Withdrawals for Inflation:", data.inputs.adjustWithdrawalsForInflation ? "Yes" : "No"); y += 10; addSectionTitle("Simulation Summary"); addLine("Portfolio Value at Retirement Start (Age " + data.inputs.retirementAge + "):", formatCurrency(data.accumulationSummary.portfolioAtRetirementActualStart)); addLine("Actual Initial Withdrawal Rate:", `${data.retirementSummary.actualInitialSWR.toFixed(2)}%`); if (data.retirementSummary.fundsDepletedYear !== null && data.retirementSummary.fundsDepletedYear <= data.inputs.retirementDuration) { addLine("Portfolio Longevity:", `Depleted in Year ${data.retirementSummary.fundsDepletedYear} of retirement`,[220,53,69],[220,53,69]); addLine("Age at Depletion:", `${data.inputs.retirementAge + data.retirementSummary.fundsDepletedYear -1} years old`); } else { addLine("Portfolio 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.finalBalance)); } addLine("Total Withdrawn During Retirement:", formatCurrency(data.retirementSummary.totalWithdrawn)); y += 10; if (data.accumulationTable.length > 0) { addSectionTitle("Accumulation Phase Projection (Up to Age " + data.inputs.retirementAge + ")"); const accHeaders = ["Year", "Age", "Start Bal.", "Savings", "Growth", "End Bal."]; const accColWidths = [35, 35, 100, 80, 80, 100]; 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); } if (data.decumulationTable.length > 0) { addSectionTitle("Retirement (Decumulation) Phase Projection"); const decHeaders = ["Ret. Yr", "Age", "Start Bal.", "Ann. Return (%)", "Withdrawal", "Growth", "End Bal."]; const decColWidths = [45, 35, 90, 65, 80, 80, 95]; const decTableFormattedData = data.decumulationTable.map(r => [ r.year, r.age, formatCurrency(r.startBalance,0,0), `${r.annualReturnUsed.toFixed(1)}%`, // Annual Return Used formatCurrency(r.withdrawal,0,0), formatCurrency(r.growth,0,0), formatCurrency(r.endBalance,0,0) + (r.notes && !r.notes.includes('Poor Return Year') ? ` (${r.notes.substring(0,10)}..)` : '') ]); addTable(decHeaders, decTableFormattedData, decColWidths); } addInfo("Disclaimer: This simulation uses simplified assumptions for returns and does not account for full market volatility, 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} - Sequence of Returns Risk Simulator`, pageMargin, doc.internal.pageSize.getHeight() - 20); } doc.save('Sequence_of_Returns_Risk_Simulation.pdf'); } if (downloadPdfBtn) { downloadPdfBtn.addEventListener('click', () => loadJsPdfIfNeeded(downloadResultsAsPdf)); } // --- Initialization --- showTab(0); loadJsPdfIfNeeded(); });