krz/401k
A 401k projection web app.
clone: git clone https://gitbay.org/krz/401k.git
1"use strict";
2function formatMoney(num) {
3 return num.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, "$&,");
4}
5function getInputValue(id) {
6 const el = document.getElementById(id);
7 return el ? parseFloat(el.value) : NaN;
8}
9function getThemeColors() {
10 const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
11 return dark
12 ? { bgColor: "#1a1d27", fgColor: "#e8eaf0", gridColor: "#2d3141", lineColor: "#3d4258" }
13 : { bgColor: "#ffffff", fgColor: "#1a1d23", gridColor: "#e2e6ea", lineColor: "#c8cdd5" };
14}
15function buildAxisLayout(title, colors) {
16 const { bgColor, fgColor, gridColor, lineColor } = colors;
17 return {
18 title: { text: title, font: { color: fgColor, size: 13 } },
19 paper_bgcolor: bgColor,
20 plot_bgcolor: bgColor,
21 font: { color: fgColor, family: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" },
22 margin: { t: 40, r: 16, b: 48, l: 60 },
23 yaxis: { gridcolor: gridColor, zerolinecolor: gridColor, linecolor: lineColor },
24 xaxis: { title: { text: "Months", font: { size: 11 } }, gridcolor: gridColor, zerolinecolor: gridColor, linecolor: lineColor },
25 };
26}
27class Data {
28 constructor(balance, contribution, returnRate, inflationRate) {
29 this.balance = balance;
30 this.contribution = contribution;
31 this.returnRate = returnRate;
32 this.inflationRate = inflationRate;
33 }
34 summaryRow() {
35 return `<tr>
36 <td>$${formatMoney(this.balance)}</td>
37 <td>$${formatMoney(this.contribution)}</td>
38 <td>${this.returnRate.toFixed(2)}%</td>
39 <td>${this.inflationRate.toFixed(2)}%</td>
40 </tr>`;
41 }
42 adjustedRate() {
43 return (1 + this.returnRate / 100) / (1 + this.inflationRate / 100) - 1;
44 }
45}
46function showResults(data, monthsArr, balanceArr, interestArr) {
47 const infoTbody = document.querySelector("#infoTable tbody");
48 infoTbody.innerHTML = data.summaryRow();
49 document.querySelectorAll(".table-section").forEach((el) => (el.style.display = "block"));
50 const graphCard = document.getElementById("graphCard");
51 graphCard.style.display = "block";
52 const colors = getThemeColors();
53 const balanceTrace = { x: monthsArr, y: balanceArr, type: "scatter", line: { color: "#4f6ef7", width: 2 } };
54 const interestTrace = { x: monthsArr, y: interestArr, type: "scatter", line: { color: "#22c55e", width: 2 } };
55 Plotly.newPlot("balChartContainer", [balanceTrace], buildAxisLayout("Total Balance", colors), { responsive: true });
56 Plotly.newPlot("intChartContainer", [interestTrace], buildAxisLayout("Accrued Interest", colors), { responsive: true });
57}
58function runCalculation(data, stopCondition) {
59 document.querySelectorAll("tbody").forEach((el) => (el.innerHTML = ""));
60 const resultsTbody = document.querySelector(".resultsTable tbody");
61 const adjustedRate = data.adjustedRate();
62 const monthlyContr = data.contribution;
63 const monthsArr = [];
64 const balanceArr = [];
65 const interestArr = [];
66 let pVal = data.balance;
67 let i = 0;
68 while (!stopCondition(pVal, i)) {
69 const month = i + 1;
70 const interest = pVal * (adjustedRate / 12);
71 const newBalance = pVal + interest + monthlyContr;
72 const row = document.createElement("tr");
73 row.innerHTML = `<td>${month}</td><td>$${formatMoney(interest)}</td><td>$${formatMoney(monthlyContr)}</td><td>$${formatMoney(newBalance)}</td>`;
74 resultsTbody.appendChild(row);
75 if (month === 1 || month % 12 === 0) {
76 interestArr.push(Math.round(interest * 100) / 100);
77 balanceArr.push(Math.round(newBalance * 100) / 100);
78 monthsArr.push(month);
79 }
80 pVal = newBalance;
81 i++;
82 }
83 showResults(data, monthsArr, balanceArr, interestArr);
84}
85function retirementYears() {
86 var _a;
87 const data = new Data(getInputValue("begBalance"), getInputValue("monthlyContr"), getInputValue("returnRate"), getInputValue("inflationRate"));
88 const years = getInputValue("years");
89 const totalMonths = years * 12;
90 runCalculation(data, (_bal, i) => i >= totalMonths);
91 (_a = document.getElementById("graphCard")) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
92}
93function retirementMoney() {
94 var _a;
95 const data = new Data(getInputValue("begBalance"), getInputValue("monthlyContr"), getInputValue("returnRate"), getInputValue("inflationRate"));
96 const target = getInputValue("money");
97 runCalculation(data, (bal) => bal >= target);
98 (_a = document.getElementById("graphCard")) === null || _a === void 0 ? void 0 : _a.scrollIntoView({ behavior: "smooth" });
99}
100window.retirementYears = retirementYears;
101window.retirementMoney = retirementMoney;