/** * Mortgage Calculator Engine for Sell With Gurvinder * Matches exact mortgagecalculator.org layout & calculations */ (function () { 'use strict'; // Input Elements const homePriceInput = document.getElementById('homePrice'); const downPaymentDollars = document.getElementById('downPaymentDollars'); const downPaymentPercent = document.getElementById('downPaymentPercent'); const downRadioDollar = document.getElementById('downRadioDollar'); const downRadioPct = document.getElementById('downRadioPct'); const loanAmountInput = document.getElementById('loanAmount'); const interestRateInput = document.getElementById('interestRate'); const loanTermSelect = document.getElementById('loanTerm'); const startMonthSelect = document.getElementById('startMonth'); const startYearInput = document.getElementById('startYear'); const propertyTaxInput = document.getElementById('propertyTax'); const pmiRateInput = document.getElementById('pmiRate'); const homeInsuranceInput = document.getElementById('homeInsurance'); const hoaFeeInput = document.getElementById('hoaFee'); const loanTypeSelect = document.getElementById('loanType'); const buyRefiSelect = document.getElementById('buyRefi'); const calculateBtn = document.getElementById('calculateBtn'); // Repayment Summary Elements const sumTotalMonthly = document.getElementById('sumTotalMonthly'); const sumPmiStatus = document.getElementById('sumPmiStatus'); const sumDownDollars = document.getElementById('sumDownDollars'); const sumDownPct = document.getElementById('sumDownPct'); const sumPayoffDate = document.getElementById('sumPayoffDate'); const sumTotalInterest = document.getElementById('sumTotalInterest'); const sumMonthlyTax = document.getElementById('sumMonthlyTax'); const sumTotalTax = document.getElementById('sumTotalTax'); const sumMonthlyIns = document.getElementById('sumMonthlyIns'); const sumTotalIns = document.getElementById('sumTotalIns'); const sumAnnualPayment = document.getElementById('sumAnnualPayment'); const sumTotalPayments = document.getElementById('sumTotalPayments'); // Chart & Visual Elements const topChartContainer = document.getElementById('topChartContainer'); const chartTooltip = document.getElementById('chartTooltip'); // Amortization Table Elements const scheduleTableBody = document.getElementById('scheduleTableBody'); const viewToggleAnnual = document.getElementById('viewToggleAnnual'); const viewToggleMonthly = document.getElementById('viewToggleMonthly'); const exportCsvBtn = document.getElementById('exportCsvBtn'); // State let currentSchedule = []; let isAnnualView = true; // Formatter const fmt = (val) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(val || 0); function calculateMortgage() { if (!homePriceInput || !sumTotalMonthly) return; const homePrice = Math.max(0, parseFloat(homePriceInput.value) || 0); let downDollars = parseFloat(downPaymentDollars.value) || 0; let downPct = parseFloat(downPaymentPercent.value) || 0; // Sync Loan Amount const loanAmount = Math.max(0, homePrice - downDollars); if (loanAmountInput) loanAmountInput.value = Math.round(loanAmount); const annualRate = parseFloat(interestRateInput.value) || 0; const loanTermYears = parseInt(loanTermSelect.value) || 30; const numPayments = loanTermYears * 12; const monthlyRate = (annualRate / 100) / 12; const startM = parseInt(startMonthSelect.value) || 7; const startY = parseInt(startYearInput.value) || new Date().getFullYear(); // Property Tax (annual dollars or percent) let annualTax = parseFloat(propertyTaxInput.value) || 0; const monthlyTax = annualTax / 12; // Home Insurance const annualIns = parseFloat(homeInsuranceInput.value) || 0; const monthlyIns = annualIns / 12; // Loan Type Rules & PMI const loanType = loanTypeSelect ? loanTypeSelect.value : 'conventional'; let pmiRatePct = parseFloat(pmiRateInput.value) || 0; let monthlyPMI = 0; let pmiText = 'not required'; if (loanType === 'va') { pmiText = 'VA (No PMI)'; } else if (loanType === 'usda') { pmiText = 'USDA Guarantee'; monthlyPMI = (loanAmount * 0.0035) / 12; } else if (loanType === 'fha') { monthlyPMI = (loanAmount * 0.0085) / 12; pmiText = `${fmt(monthlyPMI)} / mo`; } else { // Conventional if (downPct < 20) { monthlyPMI = (loanAmount * (pmiRatePct / 100)) / 12; pmiText = `${fmt(monthlyPMI)} / mo`; } } // Monthly HOA const monthlyHoa = parseFloat(hoaFeeInput.value) || 0; // Monthly Principal & Interest (P&I) let monthlyPI = 0; if (monthlyRate > 0 && loanAmount > 0) { monthlyPI = (loanAmount * monthlyRate * Math.pow(1 + monthlyRate, numPayments)) / (Math.pow(1 + monthlyRate, numPayments) - 1); } else if (loanAmount > 0) { monthlyPI = loanAmount / numPayments; } // Total Monthly Payment const totalMonthly = monthlyPI + monthlyTax + monthlyIns + monthlyPMI + monthlyHoa; // Amortization Schedule Calculation let balance = loanAmount; let totalInterestPaid = 0; let schedule = []; let curMonth = startM; let curYear = startY; for (let i = 1; i <= numPayments; i++) { const interestForMonth = balance * monthlyRate; const principalForMonth = Math.min(balance, monthlyPI - interestForMonth); totalInterestPaid += interestForMonth; balance = Math.max(0, balance - principalForMonth); const monthName = new Date(curYear, curMonth - 1).toLocaleString('default', { month: 'short' }); schedule.push({ num: i, month: monthName, year: curYear, dateStr: `${monthName}, ${curYear}`, principal: principalForMonth, interest: interestForMonth, taxesAndFees: monthlyTax + monthlyIns + monthlyPMI + monthlyHoa, totalInterest: totalInterestPaid, balance: balance }); curMonth++; if (curMonth > 12) { curMonth = 1; curYear++; } } currentSchedule = schedule; const totalTaxPaid = monthlyTax * numPayments; const totalInsPaid = monthlyIns * numPayments; const totalPmiPaid = monthlyPMI * numPayments; const totalHoaPaid = monthlyHoa * numPayments; const totalAllPayments = (monthlyPI * numPayments) + totalTaxPaid + totalInsPaid + totalPmiPaid + totalHoaPaid; const annualPaymentAmount = totalMonthly * 12; const payoffMonthName = new Date(curYear, curMonth - 2).toLocaleString('default', { month: 'short' }); const payoffDateStr = `${payoffMonthName}, ${curYear}`; // Update Repayment Summary Grid sumTotalMonthly.textContent = fmt(totalMonthly); sumPmiStatus.textContent = pmiText; sumDownDollars.textContent = fmt(downDollars); sumDownPct.textContent = `${downPct.toFixed(2)}%`; sumPayoffDate.textContent = payoffDateStr; sumTotalInterest.textContent = fmt(totalInterestPaid); sumMonthlyTax.textContent = fmt(monthlyTax); sumTotalTax.textContent = fmt(totalTaxPaid); sumMonthlyIns.textContent = fmt(monthlyIns); sumTotalIns.textContent = fmt(totalInsPaid); sumAnnualPayment.textContent = fmt(annualPaymentAmount); sumTotalPayments.textContent = fmt(totalAllPayments); // Render Top Amortization Stacked Bar Chart renderTopAmortizationChart(loanAmount, schedule, loanTermYears, startY); // Render Amortization Table renderAmortizationTable(); } // Render Top Interactive Amortization Chart ("Your Mortgage Payment Information") function renderTopAmortizationChart(initialLoanAmount, schedule, loanTermYears, startYear) { if (!topChartContainer) return; // Aggregate by Year const yearlyMap = {}; schedule.forEach(r => { if (!yearlyMap[r.year]) { yearlyMap[r.year] = { year: r.year, principal: 0, interest: 0, taxesAndFees: 0, endBalance: r.balance }; } yearlyMap[r.year].principal += r.principal; yearlyMap[r.year].interest += r.interest; yearlyMap[r.year].taxesAndFees += r.taxesAndFees; yearlyMap[r.year].endBalance = r.balance; }); const yearlyArray = Object.values(yearlyMap); if (!yearlyArray.length) return; // Determine max values for scaling let maxYearlyPayment = 0; yearlyArray.forEach(y => { const totalYrPay = y.principal + y.interest + y.taxesAndFees; if (totalYrPay > maxYearlyPayment) maxYearlyPayment = totalYrPay; }); const maxBalance = Math.max(initialLoanAmount, yearlyArray[0].endBalance); let barsHtml = ''; const totalYears = yearlyArray.length; // Generate Balance Line points for SVG path let linePoints = []; yearlyArray.forEach((y, idx) => { const xPct = ((idx + 0.5) / totalYears) * 100; const balanceYPct = 100 - ((y.endBalance / maxBalance) * 85); linePoints.push(`${xPct},${balanceYPct}`); const tfHeightPct = (y.taxesAndFees / maxYearlyPayment) * 80; const intHeightPct = (y.interest / maxYearlyPayment) * 80; const prinHeightPct = (y.principal / maxYearlyPayment) * 80; const isKeyYear = idx === 0 || idx === Math.floor(totalYears / 5) || idx === Math.floor(totalYears * 2 / 5) || idx === Math.floor(totalYears * 3 / 5) || idx === Math.floor(totalYears * 4 / 5) || idx === totalYears - 1; barsHtml += `
${isKeyYear ? `${y.year}` : ''}
`; }); topChartContainer.innerHTML = `
Your Mortgage Payment Information
Taxes & Fees Interest Principal Balance
${Math.round(maxBalance / 1000)}k ${Math.round((maxBalance * 0.66) / 1000)}k ${Math.round((maxBalance * 0.33) / 1000)}k 0
${barsHtml} ${yearlyArray.map((y, idx) => { const xPct = ((idx + 0.5) / totalYears) * 100; const balanceYPct = 100 - ((y.endBalance / maxBalance) * 85); return ``; }).join('')}
${Math.round(maxYearlyPayment / 1000)}k ${Math.round((maxYearlyPayment * 0.66) / 1000)}k ${Math.round((maxYearlyPayment * 0.33) / 1000)}k 0
`; // Add Interactive Hover Tooltips document.querySelectorAll('.chart-bar-col').forEach(col => { col.addEventListener('mouseenter', (e) => { const yr = col.getAttribute('data-year'); const tf = parseFloat(col.getAttribute('data-tf')); const int = parseFloat(col.getAttribute('data-int')); const prin = parseFloat(col.getAttribute('data-prin')); const bal = parseFloat(col.getAttribute('data-bal')); chartTooltip.innerHTML = ` ${yr}
Taxes & Fees: ${fmt(tf)}
Interest: ${fmt(int)}
Principal: ${fmt(prin)}
Balance: ${fmt(bal)} `; chartTooltip.style.display = 'block'; }); col.addEventListener('mousemove', (e) => { chartTooltip.style.left = `${e.pageX + 15}px`; chartTooltip.style.top = `${e.pageY - 60}px`; }); col.addEventListener('mouseleave', () => { chartTooltip.style.display = 'none'; }); }); } // Render Amortization Table function renderAmortizationTable() { if (!scheduleTableBody || !currentSchedule.length) return; let rowsHtml = ''; if (isAnnualView) { const annualData = {}; currentSchedule.forEach((row) => { if (!annualData[row.year]) { annualData[row.year] = { year: row.year, principal: 0, interest: 0, endBalance: 0 }; } annualData[row.year].principal += row.principal; annualData[row.year].interest += row.interest; annualData[row.year].endBalance = row.balance; }); let cumInterest = 0; Object.values(annualData).forEach((yr) => { cumInterest += yr.interest; rowsHtml += ` Year ${yr.year} ${fmt(yr.principal)} ${fmt(yr.interest)} ${fmt(cumInterest)} ${fmt(yr.endBalance)} `; }); } else { currentSchedule.forEach((row) => { rowsHtml += ` #${row.num} (${row.dateStr}) ${fmt(row.principal)} ${fmt(row.interest)} ${fmt(row.totalInterest)} ${fmt(row.balance)} `; }); } scheduleTableBody.innerHTML = rowsHtml; } // Sync Input Handlers & Triggers function initEventHandlers() { // Bi-directional Down Payment Sync function syncDownFromDollars() { const price = parseFloat(homePriceInput.value) || 0; const dollars = parseFloat(downPaymentDollars.value) || 0; downPaymentPercent.value = price > 0 ? ((dollars / price) * 100).toFixed(2) : 0; } function syncDownFromPct() { const price = parseFloat(homePriceInput.value) || 0; const pct = parseFloat(downPaymentPercent.value) || 0; downPaymentDollars.value = Math.round((price * pct) / 100); } if (homePriceInput) { homePriceInput.addEventListener('input', () => { syncDownFromPct(); calculateMortgage(); }); } if (downPaymentDollars) { downPaymentDollars.addEventListener('input', () => { syncDownFromDollars(); calculateMortgage(); }); } if (downPaymentPercent) { downPaymentPercent.addEventListener('input', () => { syncDownFromPct(); calculateMortgage(); }); } // Input listeners for real-time recalculation [interestRateInput, loanTermSelect, startMonthSelect, startYearInput, propertyTaxInput, pmiRateInput, homeInsuranceInput, hoaFeeInput, loanTypeSelect, buyRefiSelect].forEach(elem => { if (elem) elem.addEventListener('input', calculateMortgage); }); // Calculate Button Click if (calculateBtn) { calculateBtn.addEventListener('click', (e) => { e.preventDefault(); calculateMortgage(); const summaryCard = document.getElementById('summaryCardSection'); if (summaryCard) { summaryCard.scrollIntoView({ behavior: 'smooth' }); } }); } // Schedule View Toggles if (viewToggleAnnual && viewToggleMonthly) { viewToggleAnnual.addEventListener('click', () => { isAnnualView = true; viewToggleAnnual.classList.add('active'); viewToggleMonthly.classList.remove('active'); renderAmortizationTable(); }); viewToggleMonthly.addEventListener('click', () => { isAnnualView = false; viewToggleMonthly.classList.add('active'); viewToggleAnnual.classList.remove('active'); renderAmortizationTable(); }); } // CSV Export if (exportCsvBtn) { exportCsvBtn.addEventListener('click', () => { if (!currentSchedule.length) return; let csv = 'Payment Number,Date,Principal Paid,Interest Paid,Taxes & Fees,Total Interest,Remaining Balance\n'; currentSchedule.forEach((r) => { csv += `${r.num},"${r.dateStr}",${r.principal.toFixed(2)},${r.interest.toFixed(2)},${r.taxesAndFees.toFixed(2)},${r.totalInterest.toFixed(2)},${r.balance.toFixed(2)}\n`; }); const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', `Mortgage_Amortization_Schedule.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); }); } } // DOM Ready document.addEventListener('DOMContentLoaded', () => { initEventHandlers(); calculateMortgage(); }); })();