websites/eddie.sh-deno/src/oneshots/bofa-rewards/chart.ts

196 lines
5.5 KiB
TypeScript

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();
}
});
});