220 lines
6.1 KiB
JavaScript
220 lines
6.1 KiB
JavaScript
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 ? '—' : '<span style="' + growthColor + '">' + growthPrefix + fmt(Math.abs(r.yoyGrowth)) + '</span>';
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML =
|
|
'<td>' + r.year + '</td>' +
|
|
'<td>$' + fmt(r.pretax) + '</td>' +
|
|
'<td>$' + fmt(r.aftertax) + '</td>' +
|
|
'<td>$' + fmt(r.roth) + '</td>' +
|
|
'<td>$' + fmt(r.realEstate) + '</td>' +
|
|
'<td>$' + fmt(r.total) + '</td>' +
|
|
'<td>' + growthStr + '</td>' +
|
|
'<td>$' + fmt(r.pretaxSpend) + '</td>' +
|
|
'<td>$' + fmt(r.aftertaxSpend) + '</td>' +
|
|
'<td>$' + fmt(r.rothSpend) + '</td>';
|
|
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); },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|