initial version
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
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); },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user