Project: Budget Tracker
Project: Budget Tracker
Build a personal finance tracker with category management, filtering, charts, and data export. This capstone project combines everything — DOM, events, local storage, date handling, and data visualization with Chart.js.
Features
- Add income and expense transactions
- Category tagging with custom categories
- Monthly balance summary
- Spending breakdown chart (pie chart)
- Filter by category and date range
- Export to CSV
- Responsive layout
Core Data Model
// transaction object shape
{
id: 1234567890, // Date.now()
type: "expense", // "income" | "expense"
amount: 45.99,
category: "Food",
description: "Grocery run",
date: "2026-05-26" // YYYY-MM-DD
}
app.js — Complete Implementation
import Chart from "chart.js/auto";
// ─── State ──────────────────────────────────────────────────
const STORAGE_KEY = "budget-tracker-v1";
let transactions = loadData();
let categoryChart = null;
let activeFilter = { category: "all", month: getCurrentMonth() };
function loadData() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) ?? "[]"); }
catch { return []; }
}
function saveData() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(transactions));
}
function getCurrentMonth() {
return new Date().toISOString().slice(0, 7); // "2026-05"
}
// ─── Categories ─────────────────────────────────────────────
const CATEGORIES = {
income: ["Salary", "Freelance", "Investment", "Gift", "Other Income"],
expense: ["Housing", "Food", "Transport", "Healthcare", "Entertainment",
"Shopping", "Education", "Utilities", "Insurance", "Other"]
};
const CATEGORY_COLORS = {
"Housing": "#e74c3c", "Food": "#f39c12", "Transport": "#3498db",
"Healthcare": "#2ecc71", "Entertainment": "#9b59b6", "Shopping": "#1abc9c",
"Education": "#e67e22", "Utilities": "#34495e", "Insurance": "#e91e63",
"Salary": "#27ae60", "Freelance": "#2980b9", "Investment": "#8e44ad",
"Other": "#95a5a6"
};
// ─── Transactions ────────────────────────────────────────────
function addTransaction(data) {
transactions.unshift({
id: Date.now(),
...data,
amount: parseFloat(data.amount)
});
saveData();
render();
}
function deleteTransaction(id) {
transactions = transactions.filter(t => t.id !== id);
saveData();
render();
}
// ─── Calculations ────────────────────────────────────────────
function getFilteredTransactions() {
return transactions.filter(t => {
const matchMonth = !activeFilter.month || t.date.startsWith(activeFilter.month);
const matchCat = activeFilter.category === "all" || t.category === activeFilter.category;
return matchMonth && matchCat;
});
}
function getStats(filtered = getFilteredTransactions()) {
const income = filtered
.filter(t => t.type === "income")
.reduce((sum, t) => sum + t.amount, 0);
const expenses = filtered
.filter(t => t.type === "expense")
.reduce((sum, t) => sum + t.amount, 0);
return { income, expenses, balance: income - expenses };
}
function getSpendingByCategory(filtered = getFilteredTransactions()) {
return filtered
.filter(t => t.type === "expense")
.reduce((map, t) => {
map[t.category] = (map[t.category] ?? 0) + t.amount;
return map;
}, {});
}
// ─── Rendering ───────────────────────────────────────────────
function formatMoney(amount) {
return new Intl.NumberFormat("en-US", {
style: "currency", currency: "USD"
}).format(amount);
}
function formatDate(dateStr) {
return new Date(dateStr + "T00:00:00").toLocaleDateString("en-US", {
month: "short", day: "numeric"
});
}
function renderSummary(stats) {
document.getElementById("income-total").textContent = formatMoney(stats.income);
document.getElementById("expense-total").textContent = formatMoney(stats.expenses);
const balanceEl = document.getElementById("balance-total");
balanceEl.textContent = formatMoney(stats.balance);
balanceEl.className = `summary-value ${stats.balance >= 0 ? "positive" : "negative"}`;
}
function renderTransactions(filtered) {
const list = document.getElementById("transaction-list");
if (filtered.length === 0) {
list.innerHTML = `<p class="empty">No transactions found.</p>`;
return;
}
list.innerHTML = filtered.map(t => `
<div class="transaction ${t.type}" data-id="${t.id}">
<div class="transaction-info">
<span class="transaction-category" style="background:${CATEGORY_COLORS[t.category] ?? '#95a5a6'}">
${t.category}
</span>
<div>
<div class="transaction-desc">${t.description}</div>
<div class="transaction-date">${formatDate(t.date)}</div>
</div>
</div>
<div class="transaction-right">
<span class="transaction-amount ${t.type}">
${t.type === "income" ? "+" : "-"}${formatMoney(t.amount)}
</span>
<button class="delete-btn" title="Delete">✕</button>
</div>
</div>
`).join("");
}
function renderChart(spendingByCategory) {
const canvas = document.getElementById("spending-chart");
const labels = Object.keys(spendingByCategory);
const values = Object.values(spendingByCategory);
if (labels.length === 0) {
canvas.style.display = "none";
return;
}
canvas.style.display = "block";
if (categoryChart) categoryChart.destroy();
categoryChart = new Chart(canvas, {
type: "doughnut",
data: {
labels,
datasets: [{
data: values,
backgroundColor: labels.map(l => CATEGORY_COLORS[l] ?? "#95a5a6"),
borderWidth: 2,
borderColor: "#fff"
}]
},
options: {
responsive: true,
plugins: {
legend: { position: "right" },
tooltip: {
callbacks: {
label: (ctx) => ` ${ctx.label}: ${formatMoney(ctx.raw)}`
}
}
}
}
});
}
function render() {
const filtered = getFilteredTransactions();
const stats = getStats(filtered);
const spending = getSpendingByCategory(filtered);
renderSummary(stats);
renderTransactions(filtered);
renderChart(spending);
}
// ─── Form Handling ───────────────────────────────────────────
const form = document.getElementById("add-transaction");
const typeSelect = document.getElementById("type");
const categorySelect = document.getElementById("category");
typeSelect.addEventListener("change", () => {
const categories = CATEGORIES[typeSelect.value] ?? [];
categorySelect.innerHTML = categories.map(c => `<option>${c}</option>`).join("");
});
// Initialize category options
typeSelect.dispatchEvent(new Event("change"));
form.addEventListener("submit", (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(form));
if (!data.amount || !data.description || !data.date) return;
addTransaction(data);
form.reset();
typeSelect.dispatchEvent(new Event("change")); // reset categories
document.getElementById("date").value = new Date().toISOString().split("T")[0];
});
// Set today's date as default
document.getElementById("date").value = new Date().toISOString().split("T")[0];
// ─── Delete via delegation ────────────────────────────────────
document.getElementById("transaction-list").addEventListener("click", (e) => {
if (e.target.classList.contains("delete-btn")) {
const id = parseInt(e.target.closest(".transaction").dataset.id);
deleteTransaction(id);
}
});
// ─── Filters ─────────────────────────────────────────────────
document.getElementById("month-filter").addEventListener("change", (e) => {
activeFilter.month = e.target.value;
render();
});
document.getElementById("category-filter").addEventListener("change", (e) => {
activeFilter.category = e.target.value;
render();
});
// Set month filter to current month
document.getElementById("month-filter").value = getCurrentMonth();
// ─── Export to CSV ────────────────────────────────────────────
document.getElementById("export-btn").addEventListener("click", () => {
const filtered = getFilteredTransactions();
const rows = [
["Date", "Type", "Category", "Description", "Amount"],
...filtered.map(t => [t.date, t.type, t.category, t.description, t.amount])
];
const csv = rows.map(row => row.map(cell => `"${cell}"`).join(",")).join("\n");
const blob = new Blob([csv], { type: "text/csv" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `budget-${activeFilter.month}.csv`;
a.click();
URL.revokeObjectURL(url);
});
// ─── Initialize ─────────────────────────────────────────────
render();
Key Concepts Demonstrated
Intl.NumberFormat — browser-native currency formatting, no library needed.
Chart.js integration — dynamically updating a doughnut chart with destroy() before new Chart().
CSV export — Blob + URL.createObjectURL + programmatic anchor click for client-side file downloads.
Computed stats — deriving income, expenses, and balance from a single source of truth (the transactions array).
Date handling — ISO 8601 format (YYYY-MM-DD) for reliable sorting and filtering.
Congratulations on completing the JavaScript Complete 2026 course! You've gone from variables to closures, from the event loop to full applications with APIs and data visualization.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises