No simulation data to display. Please run the simulation.
";
this.resultsSummaryEl.style.display = 'block';
this.projectionTableBodyEl.innerHTML = '
| No data. |
';
this.pdfButtonContainerEl.style.display = 'none';
if(this.growthChartInstance) this.growthChartInstance.destroy();
if(this.allocationChartInstance) this.allocationChartInstance.destroy();
return;
}
const finalData = this.simulationData[this.simulationData.length - 1];
this.resultsSummaryEl.innerHTML = `
Projected Portfolio Value at Age ${finalData.age}: ${this.formatCurrency(finalData.portfolioValue)} (Nominal)
Projected Value in Today's Dollars (Inflation-Adjusted): ${this.formatCurrency(finalData.portfolioValueInflAdj)}
Asset Allocation at Retirement: Equities: ${(finalData.allocation.equity * 100).toFixed(0)}%, Bonds: ${(finalData.allocation.bonds * 100).toFixed(0)}%, Cash: ${(finalData.allocation.cash * 100).toFixed(0)}%
`;
this.resultsSummaryEl.style.display = 'block';
let tableHtml = '';
this.simulationData.forEach(data => {
tableHtml += `
| ${data.year} |
${data.age} |
${this.formatCurrency(data.contributions)} |
${(data.allocation.equity * 100).toFixed(0)}% / ${(data.allocation.bonds * 100).toFixed(0)}% / ${(data.allocation.cash * 100).toFixed(0)}% |
${data.simulatedReturn.toFixed(2)}% |
${this.formatCurrency(data.portfolioValue)} |
${this.formatCurrency(data.portfolioValueInflAdj)} |
`;
});
this.projectionTableBodyEl.innerHTML = tableHtml;
this.pdfButtonContainerEl.style.display = 'block';
this.renderCharts();
},
renderCharts: function() {
const labels = this.simulationData.map(d => d.age);
const portfolioValues = this.simulationData.map(d => d.portfolioValue);
const portfolioValuesInflAdj = this.simulationData.map(d => d.portfolioValueInflAdj);
const equityAlloc = this.simulationData.map(d => d.allocation.equity * 100);
const bondAlloc = this.simulationData.map(d => d.allocation.bonds * 100);
const cashAlloc = this.simulationData.map(d => d.allocation.cash * 100);
// Growth Chart
const growthCtx = document.getElementById('tdfpsGrowthChart').getContext('2d');
if (this.growthChartInstance) {
this.growthChartInstance.destroy();
}
this.growthChartInstance = new Chart(growthCtx, {
type: 'line',
data: {
labels: labels,
datasets: [
{ label: 'Portfolio Value (Nominal)', data: portfolioValues, borderColor: 'rgb(74, 144, 226)', backgroundColor: 'rgba(74, 144, 226, 0.1)', fill: true, tension: 0.1 },
{ label: 'Portfolio Value (Inflation-Adjusted)', data: portfolioValuesInflAdj, borderColor: 'rgb(80, 227, 194)', backgroundColor: 'rgba(80, 227, 194, 0.1)', fill: true, tension: 0.1 }
]
},
options: { responsive: true, maintainAspectRatio: false, scales: { y: { ticks: { callback: value => this.formatCurrency(value) } } } }
});
// Allocation Chart
const allocationCtx = document.getElementById('tdfpsAllocationChart').getContext('2d');
if (this.allocationChartInstance) {
this.allocationChartInstance.destroy();
}
this.allocationChartInstance = new Chart(allocationCtx, {
type: 'line', // Can also be a stacked area chart
data: {
labels: labels,
datasets: [
{ label: 'Equities %', data: equityAlloc, borderColor: 'rgb(255, 99, 132)', backgroundColor: 'rgba(255, 99, 132, 0.5)', fill: 'origin', tension: 0.1 },
{ label: 'Bonds %', data: bondAlloc, borderColor: 'rgb(54, 162, 235)', backgroundColor: 'rgba(54, 162, 235, 0.5)', fill: 'origin', tension: 0.1 },
{ label: 'Cash %', data: cashAlloc, borderColor: 'rgb(255, 205, 86)', backgroundColor: 'rgba(255, 205, 86, 0.5)', fill: 'origin', tension: 0.1 }
]
},
options: { responsive: true, maintainAspectRatio: false, scales: { y: { stacked: true, title: { display: true, text: 'Allocation %' }, max: 100 } } }
});
},
showTab: function(tabIndex) {
if (!this.tabs || this.tabs.length === 0 || !this.tabButtons || this.tabButtons.length === 0) return;
if (tabIndex < 0 || tabIndex >= this.tabs.length) return;
// Validation before moving from Tab 1 (Profile)
if (this.currentTab === 0 && tabIndex > 0) {
const currentAge = parseInt(this.currentAgeEl.value);
const retirementAge = parseInt(this.retirementAgeEl.value);
if (currentAge >= retirementAge) {
alert("Current age must be less than target retirement age.");
this.currentAgeEl.focus();
return;
}
if (parseFloat(this.initialInvestmentEl.value) < 0 || parseFloat(this.annualContributionEl.value) < 0) {
alert("Investment and contribution amounts cannot be negative.");
return;
}
}
// Validation before moving from Tab 2 (Assumptions) to Results, ensure simulation has run or run it.
if (this.currentTab === 1 && tabIndex === 2 && this.simulationData.length === 0) {
// This is handled by the "Run Simulation" button now, which auto-navigates.
// If user clicks tab directly, they might see empty results if sim not run.
// We can prompt them or just show empty. For now, "Run Simulation" is the primary path.
}
this.tabs.forEach(tab => tab.classList.remove('active'));
this.tabButtons.forEach(button => button.classList.remove('active'));
if(this.tabs[tabIndex]) this.tabs[tabIndex].classList.add('active');
if(this.tabButtons[tabIndex]) this.tabButtons[tabIndex].classList.add('active');
this.currentTab = tabIndex;
this.updateNavButtons();
if (tabIndex === 2) { // Results tab
if (this.simulationData.length === 0) { // If results tab is directly clicked without running sim
this.resultsSummaryEl.innerHTML = "
Please go to the 'Market Assumptions' tab and click 'Run Simulation' to see results.
";
this.resultsSummaryEl.style.display = 'block';
this.projectionTableBodyEl.innerHTML = '
| No data. Run simulation. |
';
this.pdfButtonContainerEl.style.display = 'none';
if(this.growthChartInstance) this.growthChartInstance.destroy(); // Clear old charts
if(this.allocationChartInstance) this.allocationChartInstance.destroy();
} else {
this.displayResults(); // Re-display/re-render if data exists
}
} else {
if(this.pdfButtonContainerEl) this.pdfButtonContainerEl.style.display = 'none';
}
},
navigateTabs: function(direction) {
const newTabIndex = this.currentTab + direction;
// If moving to results tab via "Next" from assumptions, ensure simulation runs
if (this.currentTab === 1 && direction === 1 && newTabIndex === 2) { // From Assumptions to Results
this.runSimulationAndShowResults(); // This will also call showTab(2)
} else {
this.showTab(newTabIndex);
}
},
updateNavButtons: function() {
if (this.prevButtonEl) this.prevButtonEl.style.display = (this.currentTab === 0) ? 'none' : 'inline-block';
if (this.nextButtonEl) {
// Hide "Next" button if on Assumptions tab, as "Run Simulation" is primary action to proceed
if (this.currentTab === 1) { // Assumptions tab
this.nextButtonEl.style.display = 'none';
} else {
this.nextButtonEl.style.display = (this.currentTab === (this.tabs.length - 1) || this.tabs.length === 0) ? 'none' : 'inline-block';
}
}
},
formatCurrency: function(value) {
if (isNaN(value) || value === null) value = 0;
return value.toLocaleString('en-US', { style: 'currency', currency: 'USD' });
},
getCssVariableValue(variableName) {
try {
const colorStr = getComputedStyle(document.documentElement).getPropertyValue(variableName).trim();
if (colorStr.startsWith('#')) {
let hex = colorStr.substring(1);
if (hex.length === 3) hex = hex.split('').map(char => char + char).join('');
const bigint = parseInt(hex, 16);
return [(bigint >> 16) & 255, (bigint >> 8) & 255, bigint & 255];
} else if (colorStr.startsWith('rgb')) {
return colorStr.match(/\d+/g).map(Number);
}
} catch (e) { console.warn("Could not parse CSS variable for PDF:", variableName, e); }
return [74, 144, 226]; // Fallback to primary color
},
generatePdf: function() {
if (typeof window.jspdf === 'undefined' || typeof window.jspdf.jsPDF === 'undefined') {
alert('jsPDF library not loaded.'); return;
}
if (this.simulationData.length === 0) {
alert("No simulation data to generate PDF. Please run the simulation first."); return;
}
const JsPDFConstructor = window.jspdf.jsPDF;
let doc;
try { doc = new JsPDFConstructor(); } catch (e) {
alert('Failed to create PDF instance.'); return;
}
if (typeof doc.autoTable !== 'function') {
alert('jsPDF-AutoTable plugin not loaded.'); return;
}
const primaryColorRgb = this.getCssVariableValue('--primary-color-tdfps');
const finalData = this.simulationData[this.simulationData.length - 1];
doc.setFontSize(18); doc.setTextColor(primaryColorRgb[0], primaryColorRgb[1], primaryColorRgb[2]);
doc.text("Target-Date Fund Performance Simulation", 14, 22);
doc.setFontSize(11); doc.setTextColor(50);
doc.text(`Simulation Date: ${new Date().toLocaleDateString()}`, 14, 30);
let yPos = 40;
// Inputs Summary
doc.setFontSize(12); doc.setTextColor(primaryColorRgb[0], primaryColorRgb[1], primaryColorRgb[2]);
doc.text("Inputs & Assumptions", 14, yPos); yPos += 7;
doc.setFontSize(10); doc.setTextColor(50);
doc.text(`Current Age: ${this.currentAgeEl.value}, Target Retirement Age: ${this.retirementAgeEl.value}`, 14, yPos); yPos += 6;
doc.text(`Initial Investment: ${this.formatCurrency(parseFloat(this.initialInvestmentEl.value))}, Annual Contribution: ${this.formatCurrency(parseFloat(this.annualContributionEl.value))}`, 14, yPos); yPos += 6;
doc.text(`Equity Return: ${this.equityReturnEl.value}%, Bond Return: ${this.bondReturnEl.value}%, Cash Return: ${this.cashReturnEl.value}%`, 14, yPos); yPos += 6;
doc.text(`Inflation Rate: ${this.inflationRateEl.value}%, Expense Ratio: ${this.expenseRatioEl.value}%`, 14, yPos); yPos += 10;
// Results Summary
doc.setFontSize(12); doc.setTextColor(primaryColorRgb[0], primaryColorRgb[1], primaryColorRgb[2]);
doc.text("Projected Results at Retirement (Age " + finalData.age + ")", 14, yPos); yPos += 7;
doc.setFontSize(10); doc.setTextColor(50);
doc.text(`Nominal Portfolio Value: ${this.formatCurrency(finalData.portfolioValue)}`, 14, yPos); yPos += 6;
doc.text(`Inflation-Adjusted Value (Today's Dollars): ${this.formatCurrency(finalData.portfolioValueInflAdj)}`, 14, yPos); yPos += 6;
doc.text(`Asset Allocation: ${(finalData.allocation.equity*100).toFixed(0)}% Equity, ${(finalData.allocation.bonds*100).toFixed(0)}% Bonds, ${(finalData.allocation.cash*100).toFixed(0)}% Cash`, 14, yPos); yPos += 10;
// Optional: A small table for first few and last few years? Or just the final summary.
// For brevity, we'll skip the full table in PDF by default.
const summaryTableData = [
['Metric', 'Value'],
['Years Simulated', this.simulationData.length.toString()],
['Total Contributions (Approx)', this.formatCurrency(parseFloat(this.initialInvestmentEl.value) + (parseFloat(this.annualContributionEl.value) * (this.simulationData.length -1)))], // Approximation
['Total Growth (Nominal, Approx)', this.formatCurrency(finalData.portfolioValue - (parseFloat(this.initialInvestmentEl.value) + (parseFloat(this.annualContributionEl.value) * (this.simulationData.length -1))))]
];
doc.autoTable({
startY: yPos,
head: [['Key Metrics Summary', '']],
body: summaryTableData,
theme: 'grid',
headStyles: { fillColor: primaryColorRgb }
});
yPos = doc.lastAutoTable.finalY + 10;
doc.setFontSize(9); doc.setTextColor(150);
if (yPos > 260) { doc.addPage(); yPos = 20; }
doc.text("This is a hypothetical simulation based on the provided assumptions. Actual investment performance will vary and is not guaranteed. This tool is for illustrative purposes only and not financial advice.", 14, yPos);
doc.save(`TDF_Performance_Simulation_${new Date().toISOString().slice(0,10)}.pdf`);
}
};
tdfpsApp.init();
window.tdfpsApp = tdfpsApp;
});