declare const Plotly: {
newPlot(
el: string | HTMLElement,
data: object[],
layout: object,
config?: object
): void;
};
interface GraphColors {
bgColor: string;
fgColor: string;
gridColor: string;
lineColor: string;
}
function formatMoney(num: number): string {
return num.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, "$&,");
}
function getInputValue(id: string): number {
const el = document.getElementById(id) as HTMLInputElement | null;
return el ? parseFloat(el.value) : NaN;
}
function getThemeColors(): GraphColors {
const dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
return dark
? { bgColor: "#1a1d27", fgColor: "#e8eaf0", gridColor: "#2d3141", lineColor: "#3d4258" }
: { bgColor: "#ffffff", fgColor: "#1a1d23", gridColor: "#e2e6ea", lineColor: "#c8cdd5" };
}
function buildAxisLayout(title: string, colors: GraphColors): object {
const { bgColor, fgColor, gridColor, lineColor } = colors;
return {
title: { text: title, font: { color: fgColor, size: 13 } },
paper_bgcolor: bgColor,
plot_bgcolor: bgColor,
font: { color: fgColor, family: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" },
margin: { t: 40, r: 16, b: 48, l: 60 },
yaxis: { gridcolor: gridColor, zerolinecolor: gridColor, linecolor: lineColor },
xaxis: { title: { text: "Months", font: { size: 11 } }, gridcolor: gridColor, zerolinecolor: gridColor, linecolor: lineColor },
};
}
class Data {
balance: number;
contribution: number;
returnRate: number;
inflationRate: number;
constructor(balance: number, contribution: number, returnRate: number, inflationRate: number) {
this.balance = balance;
this.contribution = contribution;
this.returnRate = returnRate;
this.inflationRate = inflationRate;
}
summaryRow(): string {
return `
| $${formatMoney(this.balance)} |
$${formatMoney(this.contribution)} |
${this.returnRate.toFixed(2)}% |
${this.inflationRate.toFixed(2)}% |
`;
}
adjustedRate(): number {
return (1 + this.returnRate / 100) / (1 + this.inflationRate / 100) - 1;
}
}
function showResults(data: Data, monthsArr: number[], balanceArr: number[], interestArr: number[]): void {
const infoTbody = document.querySelector("#infoTable tbody") as HTMLTableSectionElement;
infoTbody.innerHTML = data.summaryRow();
document.querySelectorAll(".table-section").forEach((el) => (el.style.display = "block"));
const graphCard = document.getElementById("graphCard") as HTMLElement;
graphCard.style.display = "block";
const colors = getThemeColors();
const balanceTrace = { x: monthsArr, y: balanceArr, type: "scatter", line: { color: "#4f6ef7", width: 2 } };
const interestTrace = { x: monthsArr, y: interestArr, type: "scatter", line: { color: "#22c55e", width: 2 } };
Plotly.newPlot("balChartContainer", [balanceTrace], buildAxisLayout("Total Balance", colors), { responsive: true });
Plotly.newPlot("intChartContainer", [interestTrace], buildAxisLayout("Accrued Interest", colors), { responsive: true });
}
function runCalculation(data: Data, stopCondition: (balance: number, month: number) => boolean): void {
document.querySelectorAll("tbody").forEach((el) => (el.innerHTML = ""));
const resultsTbody = document.querySelector(".resultsTable tbody") as HTMLTableSectionElement;
const adjustedRate = data.adjustedRate();
const monthlyContr = data.contribution;
const monthsArr: number[] = [];
const balanceArr: number[] = [];
const interestArr: number[] = [];
let pVal = data.balance;
let i = 0;
while (!stopCondition(pVal, i)) {
const month = i + 1;
const interest = pVal * (adjustedRate / 12);
const newBalance = pVal + interest + monthlyContr;
const row = document.createElement("tr");
row.innerHTML = `${month} | $${formatMoney(interest)} | $${formatMoney(monthlyContr)} | $${formatMoney(newBalance)} | `;
resultsTbody.appendChild(row);
if (month === 1 || month % 12 === 0) {
interestArr.push(Math.round(interest * 100) / 100);
balanceArr.push(Math.round(newBalance * 100) / 100);
monthsArr.push(month);
}
pVal = newBalance;
i++;
}
showResults(data, monthsArr, balanceArr, interestArr);
}
function retirementYears(): void {
const data = new Data(
getInputValue("begBalance"),
getInputValue("monthlyContr"),
getInputValue("returnRate"),
getInputValue("inflationRate")
);
const years = getInputValue("years");
const totalMonths = years * 12;
runCalculation(data, (_bal, i) => i >= totalMonths);
document.getElementById("graphCard")?.scrollIntoView({ behavior: "smooth" });
}
function retirementMoney(): void {
const data = new Data(
getInputValue("begBalance"),
getInputValue("monthlyContr"),
getInputValue("returnRate"),
getInputValue("inflationRate")
);
const target = getInputValue("money");
runCalculation(data, (bal) => bal >= target);
document.getElementById("graphCard")?.scrollIntoView({ behavior: "smooth" });
}
(window as Window & typeof globalThis & { retirementYears: () => void; retirementMoney: () => void }).retirementYears = retirementYears;
(window as Window & typeof globalThis & { retirementYears: () => void; retirementMoney: () => void }).retirementMoney = retirementMoney;