Projection table.
';
return;
}
const r = lrisk_results; const s = r.summary; const i = r.inputsSnapshot;
const yearsToRetirement = i.retirementAge - i.currentAge;
let longevityMsgPrimary = "";
if (s.lastsThroughPrimary) {
longevityMsgPrimary = `
Portfolio is projected to last until Primary Planning Age (${i.primaryPlanningAge}). `;
} else {
longevityMsgPrimary = `
Portfolio projected to deplete in Simulation Year ${s.portfolioDepletionSimYear} (Age ${i.currentAge + s.portfolioDepletionSimYear -1}), BEFORE Primary Planning Age (${i.primaryPlanningAge}). `;
}
let longevityMsgExtended = "";
if (i.extendedPlanningAge > i.primaryPlanningAge) {
if (s.lastsThroughExtended) {
longevityMsgExtended = `
Portfolio also projected to last until Extended Planning Age (${i.extendedPlanningAge}). `;
} else if (s.portfolioDepletionSimYear !== null && s.portfolioDepletionSimYear <= (yearsToRetirement + (i.extendedPlanningAge - i.retirementAge))) {
longevityMsgExtended = `
Portfolio projected to deplete in Simulation Year ${s.portfolioDepletionSimYear} (Age ${i.currentAge + s.portfolioDepletionSimYear -1}), BEFORE Extended Planning Age (${i.extendedPlanningAge}). `;
} else { // Lasts past primary but we didn't check extended specifically because depletion was beyond its scope or it matched primary
longevityMsgExtended = "Extended lifespan not significantly beyond primary or portfolio sustained.";
}
}
summaryDiv.innerHTML = `
Longevity Risk Assessment Summary
Portfolio Longevity (Primary Age ${i.primaryPlanningAge}): ${longevityMsgPrimary}
${i.extendedPlanningAge > i.primaryPlanningAge ? `
Portfolio Longevity (Extended Age ${i.extendedPlanningAge}): ${longevityMsgExtended}
` : ''}
Projected Nominal Value at Age ${i.primaryPlanningAge}: ${lrisk_formatCurrency(s.finalValueAtPrimaryAgeNominal)}
Projected Real Value at Age ${i.primaryPlanningAge} (Today's $): ${lrisk_formatCurrency(s.finalValueAtPrimaryAgeReal)}
${i.extendedPlanningAge > i.primaryPlanningAge ? `
Projected Nominal Value at Age ${i.extendedPlanningAge}: ${lrisk_formatCurrency(s.finalValueAtExtendedAgeNominal)}
Projected Real Value at Age ${i.extendedPlanningAge} (Today's $): ${lrisk_formatCurrency(s.finalValueAtExtendedAgeReal)}
` : ''}
Total Nominal Withdrawals (Retirement): ${lrisk_formatCurrency(s.totalNominalWithdrawals)}
Total Real Withdrawals (Retirement, Today's $): ${lrisk_formatCurrency(s.totalRealWithdrawals)}
`;
const nominalValues = [i.initialSavings].concat(r.projectionsTable.map(p => p.endValueNominal));
const realValues = [i.initialSavings].concat(r.projectionsTable.map(p => p.endValueReal));
const totalSimYears = yearsToRetirement + i.retirementDurationYears;
lrisk_renderDualLineChart(nominalValues, realValues, 'Portfolio Value ($)', valueChartContainer, 'Nominal Value', 'Real Value', '#A78BFA', '#6EE7B7', totalSimYears);
let tableHTML = `
Sim.Year Age Phase Start(N) Contrib(N) Withdraw(N) Growth(N) End(N) End(Real)
`;
r.projectionsTable.forEach(p => {
tableHTML += `
${p.simYear} ${p.age} ${p.phase}
${lrisk_formatCurrency(p.startValueNominal)}
${lrisk_formatCurrency(p.contributionNominal)}
${lrisk_formatCurrency(p.netWithdrawalNominal)}
${lrisk_formatCurrency(p.growthNominal)}
${lrisk_formatCurrency(p.endValueNominal)}
${lrisk_formatCurrency(p.endValueReal)}
`;
});
tableHTML += `
`;
tableContainer.innerHTML = tableHTML;
const pdfBtn = document.getElementById('lrisk_downloadPdfButton');
if(pdfBtn) pdfBtn.style.display = 'block';
}
function lrisk_renderDualLineChart(data1, data2, yAxisLabel, container, label1, label2, color1, color2, xMaxOverride) {
// This function is identical to iprpm_renderDualLineChart. For brevity, assume it's defined as in the IPRPM tool.
// If separate styling or logic is needed, it would be defined here.
// For now, let's use a simplified placeholder or reuse the IPRPM one conceptually.
container.innerHTML = '';
const svgWidth = Math.min(800, (lrisk_getEl('lriskContainer_main').offsetWidth || 400) - 60);
const svgHeight = 280;
const m = {top: 20, right: 110, bottom: 40, left: 75};
const w = svgWidth - m.left - m.right;
const h = svgHeight - m.top - m.bottom;
const allYValues = data1.concat(data2);
const yMin = Math.min(0, ...allYValues.filter(v=>!isNaN(v) && v !== null && isFinite(v)));
const yMax = Math.max(...allYValues.filter(v=>!isNaN(v) && v !== null && isFinite(v)), 0.01);
const xMax = xMaxOverride !== null ? xMaxOverride : Math.max(data1.length -1, data2.length -1);
const xScale = x => (x / (xMax === 0 ? 1 : xMax)) * w;
const yScale = y => h - ((y - yMin) / (yMax - yMin === 0 ? 1 : yMax - yMin)) * h;
let path1Data = data1.length > 0 ? "M" + data1.map((val, i) => `${xScale(i).toFixed(2)},${yScale(val).toFixed(2)}`).join(" L") : "";
let path2Data = data2 && data2.length > 0 ? "M" + data2.map((val, i) => `${xScale(i).toFixed(2)},${yScale(val).toFixed(2)}`).join(" L") : "";
container.innerHTML = `
Simulation Years (0 - ${xMax})
${yAxisLabel}
${lrisk_formatCurrency(yMax,0)}
${lrisk_formatCurrency(yMin,0)}
${path1Data ? ` ` : ''}
${path2Data ? ` ` : ''}
${label1}
${path2Data ? ` ${label2} ` : ''}
`;
}
// --- PDF Generation ---
function lrisk_downloadPDF() {
if (!lrisk_results) { alert("Please run the simulation first."); return; }
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') {
alert('Core PDF library (jsPDF) is not loaded.'); console.error('jsPDF library not found.'); return;
}
const { jsPDF: JSPDF } = window.jspdf;
const doc = new JSPDF('landscape');
if (typeof doc.autoTable !== 'function') {
alert('PDF Table plugin (jsPDF-AutoTable) not loaded correctly. Tables in PDF may be missing.');
console.error('doc.autoTable is not a function.');
}
let y = 15; const m = 10;
const r = lrisk_results;
const i = r.inputsSnapshot;
const yearsToRetirement = i.retirementAge - i.currentAge;
const totalSimYears = yearsToRetirement + i.retirementDurationYears;
function addLine(text, size, style = 'normal', indent = 0, spacing = 2.5) {
const cw = doc.internal.pageSize.getWidth() - (2 * m);
if (y > 185 && size > 8) { doc.addPage(); y = m; }
else if (y > 190) { doc.addPage(); y = m; }
doc.setFontSize(size); doc.setFont(undefined, style);
const lines = doc.splitTextToSize(text, cw - indent); doc.text(lines, m + indent, y);
y += (lines.length * (size * 0.35)) + spacing;
}
addLine(`Longevity Risk Assessment Report`, 16, 'bold', 0, 5);
addLine(`Report Date: ${new Date().toLocaleDateString()}`, 9, 'italic', 0, 5);
addLine("Initial Setup & Assumptions:", 11, 'bold', 0, 3);
let inputData = [
["Current Age:", `${i.currentAge} yrs`, "Retirement Age:", `${i.retirementAge} yrs`],
["Planned Retirement Duration:", `${i.retirementDurationYears} yrs (to age ${i.retirementAge + i.retirementDurationYears})`],
["Primary Planning Age:", `${i.primaryPlanningAge} yrs`, "Extended Planning Age:", `${i.extendedPlanningAge} yrs`],
["Initial Savings:", lrisk_formatCurrency(i.initialSavings), "Desired Legacy:", lrisk_formatCurrency(i.desiredLegacy)],
["Annual Other Income (Nominal):", lrisk_formatCurrency(i.annualOtherIncomeNominal), "Other Income COLA:", i.otherIncomeCola ? 'Yes':'No'],
["Portfolio Nominal E(R):", lrisk_formatPercent(i.expectedNominalReturn*100), "Annual Inflation (CPI):", lrisk_formatPercent(i.inflationRate*100)],
["Desired Annual Spending (Today's $):", lrisk_formatCurrency(i.desiredAnnualSpendingTodayDollars), "", ""]
];
if (typeof doc.autoTable === 'function') {
doc.autoTable({startY: y, body: inputData, theme: 'plain', styles:{fontSize:8, cellPadding:1}, columnStyles:{0:{fontStyle:'bold'}, 2:{fontStyle:'bold'}}});
y = doc.lastAutoTable.finalY + 5;
} else { inputData.forEach(row => addLine(`${row[0]} ${row[1]} | ${row[2]} ${row[3] || ''}`, 8)); y+=5; }
addLine("Longevity Risk Summary:", 11, 'bold', 0, 3);
const s = r.summary;
let summaryData = [
["Portfolio Depletion Simulation Year:", s.portfolioDepletionSimYear ? `Year ${s.portfolioDepletionSimYear} (Age ${i.currentAge + s.portfolioDepletionSimYear -1})` : "Lasts Full Simulation"],
[`Lasts through Primary Age (${i.primaryPlanningAge})?:`, s.lastsThroughPrimary ? "Yes" : "No"],
i.extendedPlanningAge > i.primaryPlanningAge ? [`Lasts through Extended Age (${i.extendedPlanningAge})?:`, s.lastsThroughExtended ? "Yes" : "No"] : ["Extended Age Same as Primary",""],
["Final Nominal Portfolio Value:", lrisk_formatCurrency(s.finalNominalValue)],
["Final Real Portfolio Value (Today's $):", lrisk_formatCurrency(s.finalRealValue)],
["Meets Desired Legacy:", s.meetsLegacy ? "Yes" : "No"],
["Total Nominal Withdrawals (Retirement):", lrisk_formatCurrency(s.totalNominalWithdrawals)],
["Total Real Withdrawals (Retirement, Today's $):", lrisk_formatCurrency(s.totalRealWithdrawals)],
];
if (typeof doc.autoTable === 'function') {
doc.autoTable({startY: y, body: summaryData, theme: 'plain', styles:{fontSize:8, cellPadding:1}, columnStyles:{0:{fontStyle:'bold'}}});
y = doc.lastAutoTable.finalY + 6;
} else { summaryData.forEach(row => addLine(`${row[0]} ${row[1]}`, 8)); y+=5; }
if (y > 180 && r.projectionsTable.length > 5) {doc.addPage(); y=m;}
addLine(`Year-by-Year Projections (Excerpt if long):`, 10, 'bold', 0, 3);
const head = [['Sim.Yr', 'Age', 'Phase', 'Start(N)', 'OtherInc(N)', 'SpendNeed(N)', 'NetWithdraw(N)', 'Growth(N)', 'End(N)', 'End(Real)']];
const maxPdfRows = 15;
const getExcerpt = (projections) => {
if (projections.length <= maxPdfRows) return projections;
const half = Math.floor(maxPdfRows/2);
const midStartIndex = Math.max(0, Math.floor(projections.length/2) - Math.floor(maxPdfRows/6));
const midEndIndex = Math.min(projections.length, midStartIndex + Math.ceil(maxPdfRows/3));
const startSlice = projections.slice(0, Math.floor(maxPdfRows/3));
const midSlice = projections.slice(midStartIndex, midEndIndex);
const endSlice = projections.slice(projections.length - Math.floor(maxPdfRows/3));
const combined = [...startSlice, ...midSlice, ...endSlice];
return combined.filter((v,idx,a) => a.findIndex(t=>(t.simYear === v.simYear))===idx);
};
let body = getExcerpt(r.projectionsTable).map(p => [ p.simYear, p.age, p.phase.substring(0,10), p.startValueNominal.toFixed(0), p.otherIncomeNominal.toFixed(0), p.spendingNominal.toFixed(0), p.netWithdrawalNominal.toFixed(0), p.growthNominal.toFixed(0), p.endValueNominal.toFixed(0), p.endValueReal.toFixed(0) ]);
if (typeof doc.autoTable === 'function') {
doc.autoTable({
startY: y, head: head, body: body, theme: 'grid',
headStyles: { fillColor: [245,158,11], textColor: 255, fontSize: 6.5, cellPadding: 1 },
styles: { fontSize: 6, cellPadding: 1, halign: 'right', overflow:'linebreak' },
columnStyles: { 0: { halign: 'center', fontStyle: 'bold'}, 1: {halign:'center'}, 2:{halign:'left', cellWidth:18} }
});
y = doc.lastAutoTable.finalY + 7;
} else { addLine("Projection table cannot be generated (plugin issue).", 8, 'italic'); y+=5;}
if (y > 275) { doc.addPage(); y = m; }
addLine("Note: This is a conceptual simulator based on user-defined assumptions (deterministic returns, constant inflation). It does not account for taxes or specific investment risks and is not financial advice.", 7, 'italic');
doc.save(`LongevityRisk_Analysis.pdf`);
}