import { Chart, registerables } from "https://esm.sh/chart.js@4"; Chart.register(...registerables); function monthlyInterest(apy: number) { return Math.pow(1 + apy, 1 / 12) - 1; } function rawGrowth(base: number, monthlyFunc: (_: number) => number) { const data: number[] = Array(13).fill(base); for (let i = 1; i < data.length; i++) { data[i] = (data[i - 1] * (1 + monthlyFunc(data[i - 1]))); } return data; } const PLAT_HONORS_THRESHOLD = 100_000; const PLAT_THRESHOLD = 50_000; const GOLD_THRESHOLD = 20_000; function bofaInterestRateFromSavings(savings: number) { if (savings >= PLAT_HONORS_THRESHOLD) { return 0.0004; } else if (savings >= PLAT_THRESHOLD) { return 0.0003; } else if (savings >= GOLD_THRESHOLD) { return 0.0002; } else { return 0.0001; } } function bofaAdditionalPoints(basePoints: number, savings: number) { if (savings >= PLAT_HONORS_THRESHOLD) { return basePoints * 0.75; } else if (savings >= PLAT_THRESHOLD) { return basePoints * 0.5; } else if (savings >= GOLD_THRESHOLD) { return basePoints * 0.25; } else { return basePoints; } } function hysaGrowth(base: number, monthly: number) { return rawGrowth(base, () => monthly).map((v) => v - base); } function bofaSavingsGrowth(base: number) { return rawGrowth(base, (savings) => bofaInterestRateFromSavings(savings)).map( (v) => v - base ); } function creditCardGrowth( baseSaved: number, basePoints: number, monthlySpending: number, ) { return bofaSavingsGrowth(baseSaved).map((growth: number) => monthlySpending * bofaAdditionalPoints(basePoints, baseSaved + growth) / 100 ); } function bofaCombined( baseSaved: number, basePoints: number, monthlySpending: number, ) { const savings = bofaSavingsGrowth(baseSaved); return creditCardGrowth(baseSaved, basePoints, monthlySpending).map((v, i) => v + savings[i] ); } function bofaGrossCombined( baseSaved: number, basePoints: number, monthlySpending: number, ) { const savings = bofaSavingsGrowth(baseSaved); return creditCardGrowth(baseSaved, basePoints, monthlySpending).map((v, i) => v + savings[i] - monthlySpending * i ); } function getDatasets( saved: number, hysaPercent: number, monthlySpending: number, avgBasePoints: number, ) { return [ { label: "HYSA", data: hysaGrowth(saved, monthlyInterest(hysaPercent / 100)).map(v => v.toFixed(2)), borderWidth: 1, }, { label: "BofA Savings Interest", data: bofaSavingsGrowth(saved).map(v => v.toFixed(2)), borderWidth: 1, }, { label: "Credit Card Bonus", data: creditCardGrowth(saved, avgBasePoints, monthlySpending).map(v => v.toFixed(2)), borderWidth: 1, }, { label: "BofA Savings Interest + Credit Card Bonus", data: bofaCombined(saved, avgBasePoints, monthlySpending).map(v => v.toFixed(2)), borderWidth: 1, }, { label: "BofA Savings Interest + Credit Card Bonus - Amount spent", data: bofaGrossCombined(saved, avgBasePoints, monthlySpending).map(v => v.toFixed(2)), borderWidth: 1, hidden: true, }, ]; } document.addEventListener("DOMContentLoaded", () => { function getOrInit(id: string, value: number): number { const ele = document.getElementById(id) as HTMLInputElement; if (ele.value) { document.getElementById(id + "Display").textContent = ele.value; return Number(ele.value); } else { document.getElementById(id + "Display").textContent = String(value); ele.value = String(value); return value; } } let saved = getOrInit("saved", 50_000); let hysaPercent = getOrInit("hysaPercent", 3.75); let monthlySpending = getOrInit("monthlySpending", 2_000); let avgBasePoints = getOrInit("avgBasePoints", 1.5); Chart.defaults.color = "#ccc"; Chart.defaults.borderColor = "#9993"; Chart.defaults.font.family = "'M PLUS Code Latin', sans-serif"; Chart.defaults.font.size = 16; const chart = new Chart(document.getElementById('myChart'), { normalized: true, type: 'line', data: { labels: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "Jan"], datasets: getDatasets(saved, hysaPercent, monthlySpending, avgBasePoints), }, options: { scales: { y: { beginAtZero: true, } }, animation: false, } }); function updateChart() { chart.data.datasets = getDatasets(saved, hysaPercent, monthlySpending, avgBasePoints); chart.update(); } document.getElementById("saved")?.addEventListener("input", (ev) => { if (ev.target.value) { saved = ev.target.value; document.getElementById("savedDisplay").textContent = ev.target.value; updateChart(); } }); document.getElementById("monthlySpending")?.addEventListener("input", (ev) => { if (ev.target.value) { monthlySpending = ev.target.value; document.getElementById("monthlySpendingDisplay").textContent = ev.target.value; updateChart(); } }); document.getElementById("avgBasePoints")?.addEventListener("input", (ev) => { if (ev.target.value) { avgBasePoints = ev.target.value; document.getElementById("avgBasePointsDisplay").textContent = ev.target.value; updateChart(); } }); document.getElementById("hysaPercent")?.addEventListener("input", (ev: Event) => { const value = (ev.target as HTMLInputElement).value; if (value) { hysaPercent = Number(value); document.getElementById("hysaPercentDisplay").textContent = value; updateChart(); } }); });