Mortgage Calculator with Charts body { font-family: Arial, sans-serif; background: #f8f9fa; margin: 0; padding: 0; color: #2a3a4b; } .container { max-width: 900px; background: #fff; margin: 40px auto; padding: 30px 40px 40px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); } h1 { text-align: center; margin-bottom: 25px; } label { display: block; margin: 15px 0 5px; font-weight: bold; } input[type="number"] { width: 100%; padding: 8px; font-size: 1em; border: 1px solid #ccc; border-radius: 6px; box-sizing: border-box; transition: border-color 0.3s; } input[type="number"]:focus { border-color: #007bff; outline: none; } button { margin-top: 20px; width: 100%; padding: 12px; background: #007bff; color: #fff; border: none; border-radius: 6px; font-size: 1.1em; cursor: pointer; transition: background 0.2s; } button:hover, button:focus { background: #0056b3; outline: none; } .summary-box { margin: 30px 0 10px; padding: 20px 0 10px; border-radius: 8px; background: #f9fbfd; border: 1px solid #e0e0e0; text-align: center; } .monthly-payment { color: #2196f3; font-size: 2.3em; font-weight: bold; } .monthly-payment-desc { font-size: 1.15em; color: #444; margin-bottom: 10px; } .after-desc { color: #888; font-size: 1em; margin-bottom: 18px; } .summary-row { display: flex; justify-content: space-around; border-top: 1px solid #e0e0e0; padding-top: 18px; flex-wrap: wrap; } .summary-item { flex: 1 1 150px; text-align: center; margin-bottom: 10px; } .summary-item strong { display: block; font-size: 1.3em; color: #222; } .summary-item span { font-size: 1em; color: #666; } .chart-container { background: #fff; padding: 18px 10px 10px; border-radius: 8px; margin-top: 20px; } #amortChart, #pieChart { max-width: 100%; } .cost-breakdown { font-weight: bold; font-size: 1.15em; margin-top: 18px; cursor: pointer; user-select: none; text-align: center; color: #FFFFFF; } .cost-breakdown:hover, .cost-breakdown:focus { text-decoration: underline; outline: none; } table { width: 100%; border-collapse: collapse; margin-top: 25px; font-size: 0.9em; } th, td { padding: 8px 12px; border: 1px solid #ddd; text-align: right; } th { background-color: #007bff; color: white; text-align: center; } caption { caption-side: top; font-weight: bold; font-size: 1.2em; margin-bottom: 10px; color: #2a3a4b; } @media (max-width: 600px) { .container { padding: 15px 20px 30px; } .summary-row { flex-direction: column; align-items: center; } .summary-item { margin-bottom: 15px; } th, td { font-size: 0.8em; padding: 6px 8px; } }

Mortgage Calculator

const form = document.getElementById('mortgageForm'); const summaryBox = document.getElementById('summaryBox'); const chartSection = document.getElementById('chartSection'); const pieSection = document.getElementById('pieSection'); const costBreakdownBtn = document.getElementById('costBreakdown'); const amortizationScheduleSection = document.getElementById('amortizationScheduleSection'); const amortizationTable = document.getElementById('amortizationTable').getElementsByTagName('tbody')[0]; const toggleScheduleViewBtn = document.getElementById('toggleScheduleView'); let amortChart = null; let pieChart = null; let monthlySchedule = []; let yearlySchedule = []; let showingYearly = false; function formatCurrency(value) { return value.toLocaleString('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 2, }); } function calculateAmortization( loanAmount, monthlyRate, numPayments, monthlyPayment, pmiMonthly, taxMonthly, insMonthly, startDate ) { let balance = loanAmount; let schedule = []; let pmiEndDate = null; let currentDate = new Date(startDate); for (let i = 1; i <= numPayments; i++) { const interest = balance * monthlyRate; let principal = monthlyPayment - interest; if (balance < principal) principal = balance; // last payment adjustment balance -= principal; let pmi = 0; if ((loanAmount - balance) / loanAmount 0) { pmi = pmiMonthly; } else if (!pmiEndDate && pmiMonthly > 0) { pmiEndDate = new Date(currentDate); } schedule.push({ month: i, date: new Date(currentDate), principal, interest, pmi, tax: taxMonthly, insurance: insMonthly, balance: Math.max(balance, 0), }); currentDate.setMonth(currentDate.getMonth() + 1); } return { schedule, pmiEndDate }; } // Aggregate amortization schedule by year for bar chart and yearly table function aggregateByYear(schedule) { const yearly = {}; schedule.forEach(entry => { const year = entry.date.getFullYear(); if (!yearly[year]) { yearly[year] = { principal: 0, interest: 0, tax: 0, insurance: 0, pmi: 0, balance: entry.balance, count: 0, date: entry.date }; } yearly[year].principal += entry.principal; yearly[year].interest += entry.interest; yearly[year].tax += entry.tax; yearly[year].insurance += entry.insurance; yearly[year].pmi += entry.pmi; yearly[year].balance = entry.balance; yearly[year].count++; }); // Convert to sorted array return Object.keys(yearly).sort().map(year => ({ year, principal: yearly[year].principal, interest: yearly[year].interest, tax: yearly[year].tax, insurance: yearly[year].insurance, pmi: yearly[year].pmi, balance: yearly[year].balance, date: yearly[year].date, count: yearly[year].count })); } function renderAmortizationTable(schedule, yearly = false) { amortizationTable.innerHTML = ''; if (yearly) { // --- Add totals for yearly schedule --- let totalPrincipal = 0, totalInterest = 0, totalPMI = 0, totalTax = 0, totalInsurance = 0; schedule.forEach(entry => { const row = amortizationTable.insertRow(); row.insertCell().textContent = entry.year; row.insertCell().textContent = ''; // Date empty for yearly row.insertCell().textContent = formatCurrency(entry.principal); row.insertCell().textContent = formatCurrency(entry.interest); row.insertCell().textContent = formatCurrency(entry.pmi); row.insertCell().textContent = formatCurrency(entry.tax); row.insertCell().textContent = formatCurrency(entry.insurance); row.insertCell().textContent = formatCurrency(entry.balance); totalPrincipal += entry.principal; totalInterest += entry.interest; totalPMI += entry.pmi; totalTax += entry.tax; totalInsurance += entry.insurance; }); // Add totals row at the end const totalRow = amortizationTable.insertRow(); totalRow.style.fontWeight = 'bold'; totalRow.style.backgroundColor = '#e9ecef'; totalRow.insertCell().textContent = 'Total'; totalRow.insertCell().textContent = ''; // Empty for date column totalRow.insertCell().textContent = formatCurrency(totalPrincipal); totalRow.insertCell().textContent = formatCurrency(totalInterest); totalRow.insertCell().textContent = formatCurrency(totalPMI); totalRow.insertCell().textContent = formatCurrency(totalTax); totalRow.insertCell().textContent = formatCurrency(totalInsurance); totalRow.insertCell().textContent = ''; // Balance total not applicable } else { // Monthly schedule rows let totalPrincipal = 0, totalInterest = 0, totalPMI = 0, totalTax = 0, totalInsurance = 0; schedule.forEach(entry => { const row = amortizationTable.insertRow(); row.insertCell().textContent = entry.month; row.insertCell().textContent = entry.date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); row.insertCell().textContent = formatCurrency(entry.principal); row.insertCell().textContent = formatCurrency(entry.interest); row.insertCell().textContent = formatCurrency(entry.pmi); row.insertCell().textContent = formatCurrency(entry.tax); row.insertCell().textContent = formatCurrency(entry.insurance); row.insertCell().textContent = formatCurrency(entry.balance); totalPrincipal += entry.principal; totalInterest += entry.interest; totalPMI += entry.pmi; totalTax += entry.tax; totalInsurance += entry.insurance; }); // Add totals row at the end const totalRow = amortizationTable.insertRow(); totalRow.style.fontWeight = 'bold'; totalRow.style.backgroundColor = '#e9ecef'; totalRow.insertCell().textContent = 'Total'; totalRow.insertCell().textContent = ''; // Empty for date column totalRow.insertCell().textContent = formatCurrency(totalPrincipal); totalRow.insertCell().textContent = formatCurrency(totalInterest); totalRow.insertCell().textContent = formatCurrency(totalPMI); totalRow.insertCell().textContent = formatCurrency(totalTax); totalRow.insertCell().textContent = formatCurrency(totalInsurance); totalRow.insertCell().textContent = ''; // Balance total not applicable } } form.addEventListener('submit', (e) => { e.preventDefault(); const homePrice = parseFloat(form.homePrice.value) || 0; const downPayment = parseFloat(form.downPayment.value) || 0; const interestRate = parseFloat(form.interestRate.value) || 0; const loanTerm = parseInt(form.loanTerm.value) || 0; const pmiRate = parseFloat(form.pmiRate.value) || 0; const tax = parseFloat(form.tax.value) || 0; const insurance = parseFloat(form.insurance.value) || 0; if (homePrice <= 0 || loanTerm <= 0) { alert('Please enter valid Home Price and Loan Term.'); return; } const loanAmount = homePrice - downPayment; const monthlyRate = interestRate / 12 / 100; const numPayments = loanTerm * 12; const monthlyPayment = monthlyRate === 0 ? loanAmount / numPayments : loanAmount * (monthlyRate * Math.pow(1 + monthlyRate, numPayments)) / (Math.pow(1 + monthlyRate, numPayments) - 1); const pmiMonthly = downPayment / homePrice 0 ? (loanAmount * (pmiRate / 100)) / 12 : 0; const taxMonthly = tax / 12; const insMonthly = insurance / 12; const startDate = new Date(); const { schedule, pmiEndDate } = calculateAmortization( loanAmount, monthlyRate, numPayments, monthlyPayment, pmiMonthly, taxMonthly, insMonthly, startDate ); monthlySchedule = schedule; yearlySchedule = aggregateByYear(schedule); const totalMonthlyPayment = monthlyPayment + pmiMonthly + taxMonthly + insMonthly; document.getElementById('monthlyPayment').innerText = formatCurrency(totalMonthlyPayment); document.getElementById('sumLoanAmount').innerText = formatCurrency(loanAmount); document.getElementById('sumTotalPayments').innerText = formatCurrency( monthlyPayment * numPayments + (pmiMonthly * numPayments) + tax + insurance ); document.getElementById('sumTotalInterest').innerText = formatCurrency( schedule.reduce((sum, p) => sum + p.interest, 0) ); document.getElementById('numPayments').innerText = numPayments; const payoffDate = new Date(startDate); payoffDate.setMonth(payoffDate.getMonth() + numPayments); document.getElementById('sumPayoffDate').innerText = payoffDate.toLocaleString('en-US', { month: 'short', year: 'numeric', }); const afterPMIDesc = document.getElementById('afterPMIDesc'); if (pmiMonthly > 0 && pmiEndDate) { afterPMIDesc.innerHTML = `(${formatCurrency( monthlyPayment + taxMonthly + insMonthly )} After ${pmiEndDate.toLocaleString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })})`; } else { afterPMIDesc.innerHTML = ''; } summaryBox.style.display = 'block'; chartSection.style.display = 'block'; costBreakdownBtn.style.display = 'block'; amortizationScheduleSection.style.display = 'block'; // Render amortization table monthly by default showingYearly = false; toggleScheduleViewBtn.textContent = 'Show Yearly Schedule'; renderAmortizationTable(monthlySchedule, false); // Amortization Bar Chart (yearly) const labels = yearlySchedule.map(y => y.year); const principalData = yearlySchedule.map(y => y.principal); const interestData = yearlySchedule.map(y => y.interest); const taxInsPmiData = yearlySchedule.map(y => y.tax + y.insurance + y.pmi); const balanceData = yearlySchedule.map(y => y.balance); if (amortChart) amortChart.destroy(); const ctx = document.getElementById('amortChart').getContext('2d'); amortChart = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [ { label: 'Principal', data: principalData, backgroundColor: '#2196f3', }, { label: 'Interest', data: interestData, backgroundColor: '#f44336', }, { label: 'Tax + Insurance + PMI', data: taxInsPmiData, backgroundColor: '#4caf50', } ] }, options: { responsive: true, interaction: { mode: 'index', intersect: false, }, stacked: true, plugins: { legend: { position: 'top' }, tooltip: { callbacks: { label: ctx => formatCurrency(ctx.raw) } } }, scales: { y: { beginAtZero: true, ticks: { callback: val => formatCurrency(val) } } } } }); // Pie Chart for Cost Breakdown if (pieChart) pieChart.destroy(); const pieCtx = document.getElementById('pieChart').getContext('2d'); pieChart = new Chart(pieCtx, { type: 'pie', data: { labels: ['Principal', 'Interest', 'PMI', 'Tax', 'Insurance'], datasets: [{ data: [ yearlySchedule.reduce((sum, y) => sum + y.principal, 0), yearlySchedule.reduce((sum, y) => sum + y.interest, 0), yearlySchedule.reduce((sum, y) => sum + y.pmi, 0), yearlySchedule.reduce((sum, y) => sum + y.tax, 0), yearlySchedule.reduce((sum, y) => sum + y.insurance, 0), ], backgroundColor: [ '#2196f3', '#f44336', '#ff9800', '#4caf50', '#9c27b0' ] }] }, options: { responsive: true, plugins: { legend: { position: 'bottom' }, tooltip: { callbacks: { label: ctx => { const label = ctx.label || ''; const value = ctx.raw || 0; return `${label}: ${formatCurrency(value)}`; } } } } } }); }); costBreakdownBtn.addEventListener('click', () => { const expanded = costBreakdownBtn.getAttribute('aria-expanded') === 'true'; costBreakdownBtn.setAttribute('aria-expanded', !expanded); pieSection.style.display = expanded ? 'none' : 'block'; costBreakdownBtn.innerText = expanded ? 'Cost Breakdown ▼' : 'Cost Breakdown ▲'; }); toggleScheduleViewBtn.addEventListener('click', () => { showingYearly = !showingYearly; if (showingYearly) { renderAmortizationTable(yearlySchedule, true); toggleScheduleViewBtn.textContent = 'Show Monthly Schedule'; } else { renderAmortizationTable(monthlySchedule, false); toggleScheduleViewBtn.textContent = 'Show Yearly Schedule'; } });