Expense Tracker
Data-driven UI. Manage transactions, visualize spending, and track budgets.
Data Model
Transactions with amounts, categories, and dates.
// Transaction shape:
// {
// id: "uuid",
// type: "expense" | "income",
// amount: 45.99,
// category: "food",
// description: "Lunch with team",
// date: "2024-01-15",
// createdAt: 1705305600000,
// }
// App state:
let transactions = [];
let currentMonth = new Date().toISOString().slice(0, 7); // "2024-01"
// Categories with icons and colors:
const CATEGORIES = {
food: { icon: "🍕", color: "#f97316", label: "Food & Dining" },
transport: { icon: "🚗", color: "#3b82f6", label: "Transport" },
housing: { icon: "🏠", color: "#8b5cf6", label: "Housing" },
utilities: { icon: "💡", color: "#eab308", label: "Utilities" },
entertainment: { icon: "🎬", color: "#ec4899", label: "Entertainment" },
shopping: { icon: "🛒", color: "#14b8a6", label: "Shopping" },
health: { icon: "💊", color: "#ef4444", label: "Health" },
salary: { icon: "💰", color: "#22c55e", label: "Salary" },
freelance: { icon: "💻", color: "#06b6d4", label: "Freelance" },
other: { icon: "📦", color: "#6b7280", label: "Other" },
};
// Budget limits per category (monthly):
let budgets = {
food: 500,
transport: 200,
entertainment: 150,
shopping: 300,
};Transactions
Add, edit, and delete income and expenses.
// Add transaction form:
// <form class="transaction-form">
// <select name="type"><option>expense</option><option>income</option></select>
// <input name="amount" type="number" step="0.01" min="0" required>
// <select name="category">...categories...</select>
// <input name="description" type="text" placeholder="Description">
// <input name="date" type="date">
// <button type="submit">Add</button>
// </form>
const form = document.querySelector(".transaction-form");
form.addEventListener("submit", (e) => {
e.preventDefault();
const formData = new FormData(form);
const transaction = {
id: crypto.randomUUID(),
type: formData.get("type"),
amount: parseFloat(formData.get("amount")),
category: formData.get("category"),
description: formData.get("description").trim(),
date: formData.get("date") || new Date().toISOString().slice(0, 10),
createdAt: Date.now(),
};
// Validate:
if (transaction.amount <= 0 || isNaN(transaction.amount)) {
showError("Enter a valid amount");
return;
}
transactions.push(transaction);
form.reset();
save();
render();
});FormData extracts all form values cleanly. Validate server-side too in a real app.
1 / 2
Summaries
Calculate totals, balances, and budget status.
// Get transactions for a specific month:
function getMonthTransactions(month) {
return transactions.filter(t => t.date.startsWith(month));
}
// Calculate summaries:
function getSummary(month) {
const monthTx = getMonthTransactions(month);
const income = monthTx
.filter(t => t.type === "income")
.reduce((sum, t) => sum + t.amount, 0);
const expenses = monthTx
.filter(t => t.type === "expense")
.reduce((sum, t) => sum + t.amount, 0);
return {
income,
expenses,
balance: income - expenses,
transactionCount: monthTx.length,
};
}
// Spending by category:
function getCategoryBreakdown(month) {
const monthTx = getMonthTransactions(month)
.filter(t => t.type === "expense");
const breakdown = {};
for (const t of monthTx) {
breakdown[t.category] = (breakdown[t.category] || 0) + t.amount;
}
// Sort by amount descending:
return Object.entries(breakdown)
.map(([category, amount]) => ({ category, amount }))
.sort((a, b) => b.amount - a.amount);
}
// Budget alerts:
function getBudgetStatus(month) {
const breakdown = getCategoryBreakdown(month);
return Object.entries(budgets).map(([category, limit]) => {
const spent = breakdown.find(b => b.category === category)?.amount || 0;
const percentage = Math.round((spent / limit) * 100);
return {
category,
limit,
spent,
percentage,
status: percentage >= 100 ? "over" : percentage >= 80 ? "warning" : "ok",
};
});
}
// Render summary cards:
function renderSummary() {
const { income, expenses, balance } = getSummary(currentMonth);
document.querySelector(".summary").innerHTML = `
<div class="card income">
<h3>Income</h3>
<p>+$${income.toFixed(2)}</p>
</div>
<div class="card expenses">
<h3>Expenses</h3>
<p>-$${expenses.toFixed(2)}</p>
</div>
<div class="card balance ${balance >= 0 ? "positive" : "negative"}">
<h3>Balance</h3>
<p>${balance >= 0 ? "+" : ""}$${balance.toFixed(2)}</p>
</div>
`;
}Visualization
Charts with pure CSS and Canvas — no libraries needed.
// CSS-only bar chart:
function renderBarChart(data) {
const max = Math.max(...data.map(d => d.amount));
const chartEl = document.querySelector(".bar-chart");
chartEl.innerHTML = data.map(d => {
const cat = CATEGORIES[d.category];
const height = (d.amount / max) * 100;
return `
<div class="bar-group">
<div class="bar" style="height:${height}%; background:${cat.color}">
<span class="bar-label">$${d.amount.toFixed(0)}</span>
</div>
<span class="bar-category">${cat.icon}</span>
</div>
`;
}).join("");
}
// Donut chart with Canvas:
function renderDonutChart(data) {
const canvas = document.querySelector(".donut-chart");
const ctx = canvas.getContext("2d");
const total = data.reduce((s, d) => s + d.amount, 0);
const centerX = canvas.width / 2;
const centerY = canvas.height / 2;
const radius = 80;
let startAngle = -Math.PI / 2; // start from top
data.forEach(d => {
const sliceAngle = (d.amount / total) * Math.PI * 2;
const cat = CATEGORIES[d.category];
ctx.beginPath();
ctx.arc(centerX, centerY, radius, startAngle, startAngle + sliceAngle);
ctx.arc(centerX, centerY, radius * 0.6, startAngle + sliceAngle, startAngle, true);
ctx.closePath();
ctx.fillStyle = cat.color;
ctx.fill();
startAngle += sliceAngle;
});
// Center text:
ctx.fillStyle = "#fff";
ctx.font = "bold 20px system-ui";
ctx.textAlign = "center";
ctx.fillText(`$${total.toFixed(0)}`, centerX, centerY + 7);
}
// Budget progress bars:
function renderBudgets() {
const statuses = getBudgetStatus(currentMonth);
const budgetEl = document.querySelector(".budgets");
budgetEl.innerHTML = statuses.map(b => {
const cat = CATEGORIES[b.category];
const width = Math.min(b.percentage, 100);
return `
<div class="budget-item ${b.status}">
<div class="budget-header">
<span>${cat.icon} ${cat.label}</span>
<span>$${b.spent.toFixed(0)} / $${b.limit}</span>
</div>
<div class="budget-bar">
<div class="budget-fill" style="width:${width}%"></div>
</div>
</div>
`;
}).join("");
}Export & Import
Download data as CSV or JSON. Import from file.
// Export as CSV:
function exportCSV() {
const headers = "Date,Type,Category,Description,Amount\n";
const rows = transactions.map(t =>
`${t.date},${t.type},${t.category},"${t.description.replace(/"/g, '""')}",${t.amount}`
).join("\n");
downloadFile(headers + rows, "expenses.csv", "text/csv");
}
// Export as JSON:
function exportJSON() {
const data = JSON.stringify({ transactions, budgets }, null, 2);
downloadFile(data, "expenses.json", "application/json");
}
function downloadFile(content, filename, mimeType) {
const blob = new Blob([content], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
// Import from JSON file:
const importInput = document.querySelector("#import-file");
importInput.addEventListener("change", async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
const text = await file.text();
const data = JSON.parse(text);
// Validate structure:
if (!Array.isArray(data.transactions)) {
throw new Error("Invalid file format");
}
// Merge or replace:
const action = confirm("Merge with existing data? (Cancel to replace)");
if (action) {
// Merge — avoid duplicates by id:
const existingIds = new Set(transactions.map(t => t.id));
const newTx = data.transactions.filter(t => !existingIds.has(t.id));
transactions.push(...newTx);
} else {
transactions = data.transactions;
}
save();
render();
} catch (error) {
showError("Failed to import: " + error.message);
}
importInput.value = ""; // reset file input
});
// Key patterns in this project:
// ✓ Complex data aggregation (reduce, filter, group)
// ✓ Date-based filtering and formatting
// ✓ Canvas API for charts
// ✓ CSS-only charts (bar, progress)
// ✓ File export (Blob + download)
// ✓ File import (FileReader + validation)
// ✓ Budget tracking with alerts
// ✓ FormData API for clean form handlingFAQ
Common questions about this project.