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 handling

FAQ

Common questions about this project.