let chartInstance = null; document.querySelectorAll('[data-dollar]').forEach(function (input) { input.addEventListener('input', function () { var raw = this.value.replace(/[^0-9]/g, ''); if (raw === '') { this.value = ''; return; } this.value = parseInt(raw, 10).toLocaleString('en-US'); }); input.addEventListener('blur', function () { var raw = this.value.replace(/[^0-9]/g, ''); if (raw === '') { this.value = '0'; } else { this.value = parseInt(raw, 10).toLocaleString('en-US'); } }); }); document.getElementById('calculator-form').addEventListener('submit', function (e) { e.preventDefault(); const resultsInflation = runProjection(true); const resultsNoInflation = runProjection(false); renderTable(resultsInflation, 'results-table-inflation'); renderTable(resultsNoInflation, 'results-table-noinflation'); renderChart(resultsInflation); document.getElementById('results').classList.remove('hidden'); }); function getVal(id) { var el = document.getElementById(id); var raw = el.value.replace(/[^0-9.]/g, ''); return parseFloat(raw) || 0; } function fmt(n) { return n.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 }); } function runProjection(useInflation) { const years = 30; const inflationFactor = useInflation ? 1 + getVal('inflation') / 100 : 1; const growthFactor = 1 + getVal('ror') / 100; const taxRate = getVal('tax-rate') / 100; let pretax = getVal('pretax-value'); let aftertax = getVal('aftertax-value'); let roth = getVal('roth-value'); let realEstate = getVal('realestate-value'); let pretaxSpend = getVal('pretax-spending'); let aftertaxSpend = getVal('aftertax-spending'); let rothSpend = getVal('roth-spending'); const rows = []; for (let y = 0; y <= years; y++) { const total = Math.round(pretax + aftertax + roth + realEstate); const prevTotal = rows.length > 0 ? rows[rows.length - 1].total : total; rows.push({ year: y, pretax: Math.round(pretax), aftertax: Math.round(aftertax), roth: Math.round(roth), realEstate: Math.round(realEstate), total: total, yoyGrowth: Math.round(total - prevTotal), pretaxSpend: Math.round(pretaxSpend), aftertaxSpend: Math.round(aftertaxSpend), rothSpend: Math.round(rothSpend), }); if (y === years) break; pretax = pretax * growthFactor; aftertax = aftertax * growthFactor; roth = roth * growthFactor; realEstate = realEstate * growthFactor; pretaxSpend = Math.min(pretaxSpend, pretax * (1 - taxRate)); pretax -= pretaxSpend / (1 - taxRate); pretax = Math.max(0, pretax); aftertaxSpend = Math.min(aftertaxSpend, aftertax); aftertax -= aftertaxSpend; aftertax = Math.max(0, aftertax); rothSpend = Math.min(rothSpend, roth); roth -= rothSpend; roth = Math.max(0, roth); pretaxSpend *= inflationFactor; aftertaxSpend *= inflationFactor; rothSpend *= inflationFactor; } return rows; } function renderTable(rows, tableId) { const tbody = document.querySelector('#' + tableId + ' tbody'); tbody.innerHTML = ''; rows.forEach(function (r) { const growthColor = r.yoyGrowth >= 0 ? 'color:#16a34a' : 'color:#dc2626'; const growthPrefix = r.yoyGrowth >= 0 ? '+$' : '-$'; const growthStr = r.year === 0 ? '—' : '' + growthPrefix + fmt(Math.abs(r.yoyGrowth)) + ''; const tr = document.createElement('tr'); tr.innerHTML = '' + r.year + '' + '$' + fmt(r.pretax) + '' + '$' + fmt(r.aftertax) + '' + '$' + fmt(r.roth) + '' + '$' + fmt(r.realEstate) + '' + '$' + fmt(r.total) + '' + '' + growthStr + '' + '$' + fmt(r.pretaxSpend) + '' + '$' + fmt(r.aftertaxSpend) + '' + '$' + fmt(r.rothSpend) + ''; tbody.appendChild(tr); }); } function renderChart(rows) { const labels = rows.map(function (r) { return 'Year ' + r.year; }); const datasets = [ { label: 'Pre-tax', data: rows.map(function (r) { return r.pretax; }), borderColor: '#2563eb', backgroundColor: 'rgba(37, 99, 235, 0.08)', fill: true, tension: 0.3, }, { label: 'After-tax', data: rows.map(function (r) { return r.aftertax; }), borderColor: '#16a34a', backgroundColor: 'rgba(22, 163, 74, 0.08)', fill: true, tension: 0.3, }, { label: 'Roth', data: rows.map(function (r) { return r.roth; }), borderColor: '#dc2626', backgroundColor: 'rgba(220, 38, 38, 0.08)', fill: true, tension: 0.3, }, { label: 'Real Estate', data: rows.map(function (r) { return r.realEstate; }), borderColor: '#f59e0b', backgroundColor: 'rgba(245, 158, 11, 0.08)', fill: true, tension: 0.3, }, { label: 'Total Net Worth', data: rows.map(function (r) { return r.total; }), borderColor: '#7c3aed', backgroundColor: 'transparent', borderWidth: 2.5, tension: 0.3, }, ]; if (chartInstance) { chartInstance.destroy(); } const ctx = document.getElementById('projection-chart').getContext('2d'); chartInstance = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false, }, plugins: { tooltip: { callbacks: { label: function (context) { return context.dataset.label + ': $' + fmt(context.parsed.y); }, }, }, legend: { position: 'bottom', labels: { usePointStyle: true, pointStyle: 'circle', padding: 16, font: { size: 12 }, }, }, }, scales: { x: { grid: { display: false }, ticks: { maxTicksLimit: 11 }, }, y: { ticks: { callback: function (v) { return '$' + fmt(v); }, }, }, }, }, }); }