Mortgage Payoff Summary
Based on your inputted interest rate of 6.00%
Payoff details for your mortgage, including the impact of extra payments or bi-weekly payments on interest savings and payoff time.
| Original | With Payoff |
|---|
| Monthly Pay | $0.00 | $0.00 |
| Total Payments | $0.00 | $0.00 |
| Total Interest | $0.00 | $0.00 |
| Remaining Payments | $0.00 | $0.00 |
| Remaining Interest | $0.00 | $0.00 |
| Payoff Time | 0 yrs, 0 mos | 0 yrs, 0 mos |
Loan Balance Over Time
Loan Balance: $0.00
Principal Paid: $0.00
Interest Paid: $0.00
Payoff Schedule Breakdown
Payment details up to the payoff date.
First payment: July 2025 | Payoff date: June 2035
|
|
`);
printWindow.document.close();
printWindow.print();
});exportCsvBtn.addEventListener('click', () => {
const rows = [["Year", "Month", "Principal", "Interest", "Remaining Balance"]];
payoffTableBody.querySelectorAll('details').forEach(detail => {
const year = detail.querySelector('summary .summary-year').textContent;
detail.querySelectorAll('.monthly-table tbody tr').forEach(row => {
const cells = Array.from(row.querySelectorAll('td')).map(td => {
const text = td.textContent.replace(/[^0-9A-Za-z\s.]/g, '');
return `"${text.replace(/"/g, '""')}"`;
});
rows.push([year, ...cells]);
});
});
const csvContent = "data:text/csv;charset=utf-8," + rows.map(e => e.join(",")).join("\n");
const link = document.createElement("a");
link.setAttribute("href", encodeURI(csvContent));
link.setAttribute("download", "payoff_schedule.csv");
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});// --- CORE LOGIC ---
function getFormValues() {
const isKnownTerm = tabKnownTerm.classList.contains('active');
if (isKnownTerm) {
return {
mode: 'known',
loanAmount: parseCurrency(knownLoanAmountInput.value),
loanTerm: parseInt(knownLoanTermSelect.value, 10),
interestRate: parseFloat(knownInterestRateInput.value) || 0,
remainingYears: parseFloat(knownRemainingYearsInput.value) || 0,
remainingMonths: parseFloat(knownRemainingMonthsInput.value) || 0,
extraMonthly: parseCurrency(extraMonthlyInput.value),
extraYearly: parseCurrency(extraYearlyInput.value),
oneTimeAmount: parseCurrency(oneTimeAmountInput.value),
oneTimeDate: oneTimeDateInput.value ? new Date(oneTimeDateInput.value) : new Date(),
biweeklyPayments: biweeklyPaymentsCheckbox.checked
};
} else {
return {
mode: 'unknown',
principalBalance: parseCurrency(unknownPrincipalBalanceInput.value),
monthlyPayment: parseCurrency(unknownMonthlyPaymentInput.value),
interestRate: parseFloat(unknownInterestRateInput.value) || 0,
extraMonthly: parseCurrency(extraMonthlyInput.value),
extraYearly: parseCurrency(extraYearlyInput.value),
oneTimeAmount: parseCurrency(oneTimeAmountInput.value),
oneTimeDate: oneTimeDateInput.value ? new Date(oneTimeDateInput.value) : new Date(),
biweeklyPayments: biweeklyPaymentsCheckbox.checked
};
}
}function calculateAndDisplay() {
loadingSpinner.classList.add('active');
setTimeout(() => {
const form = getFormValues();
let monthlyPayment, principal, remainingPayments, maxPayments;if (form.mode === 'known') {
principal = form.loanAmount;
maxPayments = form.loanTerm * 12;
remainingPayments = Math.min(maxPayments, Math.floor(form.remainingYears * 12 + form.remainingMonths));
const monthlyRate = form.interestRate / 1200;
if (principal > 0 && maxPayments > 0 && monthlyRate > 0) {
monthlyPayment = principal * (monthlyRate * Math.pow(1 + monthlyRate, maxPayments)) / (Math.pow(1 + monthlyRate, maxPayments) - 1);
} else if (principal > 0 && maxPayments > 0) {
monthlyPayment = principal / maxPayments;
} else {
monthlyPayment = 0;
}
monthlyPayment = Number(monthlyPayment.toFixed(2));
} else {
principal = form.principalBalance;
monthlyPayment = form.monthlyPayment;
remainingPayments = estimateRemainingPayments(principal, monthlyPayment, form.interestRate / 1200);
maxPayments = remainingPayments;
}const startDate = new Date();
const endDate = new Date(startDate);
endDate.setMonth(endDate.getMonth() + remainingPayments);const payoffData = generatePayoffData(
principal,
form.interestRate / 1200,
monthlyPayment,
maxPayments,
form.biweeklyPayments ? monthlyPayment / 2 : form.extraMonthly,
form.extraYearly,
form.oneTimeAmount,
form.oneTimeDate,
startDate,
endDate,
form.biweeklyPayments
);const noExtraPayoffData = generatePayoffData(
principal,
form.interestRate / 1200,
monthlyPayment,
maxPayments,
0,
0,
0,
startDate,
startDate,
endDate,
false
);updateUI(form, monthlyPayment, payoffData, noExtraPayoffData);
loadingSpinner.classList.remove('active');
}, 50);
}function estimateRemainingPayments(principal, monthlyPayment, monthlyRate) {
if (principal <= 0 || monthlyPayment <= 0 || monthlyRate < 0) return 0;
if (monthlyRate === 0) return Math.ceil(principal / monthlyPayment);
const n = Math.log(monthlyPayment / (monthlyPayment - principal * monthlyRate)) / Math.log(1 + monthlyRate);
return Math.ceil(n);
}function generatePayoffData(principal, monthlyRate, monthlyPayment, maxPayments, extraMonthly, extraYearly, oneTimeAmount, oneTimeDate, startDate, endDate, isBiweekly) {
if (principal <= 0 || maxPayments <= 0) {
return { yearlyData: {}, monthlyData: [], totalInterest: 0, totalPayments: 0, numPayments: 0, firstPaymentDate: new Date(startDate), lastPaymentDate: new Date(startDate), payoffAmount: 0 };
}let balance = principal;
let totalInterest = 0;
let totalPayments = 0;
const firstPaymentDate = new Date(startDate.getFullYear(), startDate.getMonth() + 1, 1);
let currentDate = new Date(firstPaymentDate);
let lastPaymentDate = currentDate;
const monthlyData = [];
const yearlyData = {};
let oneTimeApplied = false;
let paymentCount = 0;
let biweeklyCounter = 0;while (balance > 0.01 && paymentCount < maxPayments && currentDate <= endDate) {
const year = currentDate.getFullYear();
const isYearStart = currentDate.getMonth() === 0;
const isBiweeklyPayment = isBiweekly && (biweeklyCounter % 2 === 0);const interestPayment = balance * monthlyRate;
let principalPayment = isBiweeklyPayment ? (monthlyPayment / 2) : (monthlyPayment + extraMonthly);if (isYearStart && extraYearly > 0 && currentDate > firstPaymentDate) {
principalPayment += extraYearly;
}if (!oneTimeApplied && oneTimeAmount > 0 && currentDate >= oneTimeDate) {
principalPayment += oneTimeAmount;
oneTimeApplied = true;
}if (principalPayment > balance) {
principalPayment = balance;
}balance -= principalPayment;
totalInterest += interestPayment;
totalPayments += principalPayment + interestPayment;
lastPaymentDate = new Date(currentDate);
paymentCount++;monthlyData.push({
date: new Date(currentDate),
interest: Number(interestPayment.toFixed(2)),
principal: Number(principalPayment.toFixed(2)),
balance: Number(Math.max(balance, 0).toFixed(2))
});if (!yearlyData[year]) {
yearlyData[year] = { months: [], totalPrincipal: 0, totalInterest: 0, endBalance: 0 };
}
yearlyData[year].months.push(monthlyData[monthlyData.length - 1]);
yearlyData[year].totalPrincipal += principalPayment;
yearlyData[year].totalInterest += interestPayment;
yearlyData[year].endBalance = Math.max(balance, 0);if (isBiweekly) {
biweeklyCounter++;
currentDate.setDate(currentDate.getDate() + 14);
if (biweeklyCounter % 26 === 0) {
paymentCount++;
monthlyData.push({
date: new Date(currentDate),
interest: 0,
principal: monthlyPayment,
balance: Number(Math.max(balance - monthlyPayment, 0).toFixed(2))
});
yearlyData[year].months.push(monthlyData[monthlyData.length - 1]);
yearlyData[year].totalPrincipal += monthlyPayment;
yearlyData[year].endBalance = Math.max(balance, 0);
balance -= monthlyPayment;
totalPayments += monthlyPayment;
}
} else {
currentDate.setMonth(currentDate.getMonth() + 1);
}
}Object.keys(yearlyData).forEach(year => {
yearlyData[year].totalPrincipal = Number(yearlyData[year].totalPrincipal.toFixed(2));
yearlyData[year].totalInterest = Number(yearlyData[year].totalInterest.toFixed(2));
yearlyData[year].endBalance = Number(yearlyData[year].endBalance.toFixed(2));
});const payoffAmount = monthlyData.length > 0 ? monthlyData[monthlyData.length - 1].balance : principal;return {
yearlyData,
monthlyData,
totalInterest: Number(totalInterest.toFixed(2)),
totalPayments: Number(totalPayments.toFixed(2)),
numPayments: paymentCount,
firstPaymentDate,
lastPaymentDate,
payoffAmount
};
}function updateUI(form, monthlyPayment, payoffData, noExtraPayoffData) {
const { yearlyData, monthlyData, totalInterest, totalPayments, numPayments, firstPaymentDate, lastPaymentDate, payoffAmount } = payoffData;
const noExtraTotalInterest = noExtraPayoffData.totalInterest;
const noExtraTotalPayments = noExtraPayoffData.totalPayments;
const noExtraNumPayments = noExtraPayoffData.numPayments;
const noExtraPayoffAmount = noExtraPayoffData.payoffAmount;dynamicRateEl.textContent = form.interestRate.toFixed(2);
summaryNumPaymentsEl.textContent = numPayments;
summaryPayoffAmountEl.textContent = formatCurrency(payoffAmount);
summaryTotalInterestEl.textContent = formatCurrency(totalInterest);
summaryInterestSavingsEl.textContent = formatCurrency(Math.max(noExtraTotalInterest - totalInterest, 0));
summaryPayoffTimeEl.textContent = formatTerm(numPayments);const totalPrincipal = Number((form.mode === 'known' ? form.loanAmount : form.principalBalance - (monthlyData[monthlyData.length - 1]?.balance || 0)).toFixed(2));
legendBalanceEl.textContent = formatCurrency(monthlyData[monthlyData.length - 1]?.balance || 0);
legendPrincipalEl.textContent = formatCurrency(totalPrincipal);
legendInterestEl.textContent = formatCurrency(totalInterest);compMonthlyPayOrigEl.textContent = formatCurrency(monthlyPayment);
compMonthlyPayNewEl.textContent = formatCurrency(monthlyPayment + (form.biweeklyPayments ? monthlyPayment / 12 : form.extraMonthly));
compTotalPaymentsOrigEl.textContent = formatCurrency(noExtraTotalPayments);
compTotalPaymentsNewEl.textContent = formatCurrency(totalPayments);
compTotalInterestOrigEl.textContent = formatCurrency(noExtraTotalInterest);
compTotalInterestNewEl.textContent = formatCurrency(totalInterest);
compRemainingPaymentsOrigEl.textContent = formatCurrency(noExtraPayoffAmount + noExtraTotalInterest);
compRemainingPaymentsNewEl.textContent = formatCurrency(payoffAmount + totalInterest);
compRemainingInterestOrigEl.textContent = formatCurrency(noExtraTotalInterest);
compRemainingInterestNewEl.textContent = formatCurrency(totalInterest);
compPayoffTimeOrigEl.textContent = formatTerm(noExtraNumPayments);
compPayoffTimeNewEl.textContent = formatTerm(numPayments);firstPaymentDateEl.textContent = firstPaymentDate.toLocaleString('default', { month: 'long', year: 'numeric' });
lastPaymentDateEl.textContent = lastPaymentDate.toLocaleString('default', { month: 'long', year: 'numeric' });updatePayoffGraph(yearlyData, form.mode === 'known' ? form.loanAmount : form.principalBalance, totalInterest, noExtraPayoffData);
updatePayoffTable(yearlyData);
}function updatePayoffGraph(yearlyData, loanAmount, totalInterest, noExtraPayoffData) {
const svg = document.getElementById('payoff-chart-svg');
svg.innerHTML = '';
if (Object.keys(yearlyData).length === 0 || loanAmount <= 0) {
svg.innerHTML = '
No data to display';
return;
}const allMonths = Object.values(yearlyData).flatMap(y => y.months);
const noExtraMonths = Object.values(noExtraPayoffData.yearlyData).flatMap(y => y.months);
if (allMonths.length === 0) return;const width = 360;
const height = 220, m = { top: 20, right: 20, bottom: 35, left: 50 };
const w = width - m.left - m.right, h = height - m.top - m.bottom;
const maxY = Math.max(loanAmount, totalInterest, noExtraPayoffData.totalInterest, 1);const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
g.setAttribute("transform", `translate(${m.left},${m.top})`);
svg.appendChild(g);const maxMonths = Math.max(allMonths.length, noExtraMonths.length);
const xScale = i => (i / (maxMonths - 1)) * w;
const yScale = v => h - (v / maxY) * h;const createPath = (data, color, isDashed = false) => {
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
let d = `M0,${yScale(data[0])}`;
for (let i = 1; i < data.length; i++) {
if (isFinite(data[i]) && data[i] >= 0) {
d += ` L${xScale(i)},${yScale(data[i])}`;
}
}
path.setAttribute('d', d);
path.setAttribute('class', 'line');
path.style.stroke = color;
path.style.strokeWidth = '2.5';
path.style.opacity = isDashed ? '0.5' : '0.9';
if (isDashed) path.style.strokeDasharray = '5,5';
return path;
};let cumulativePrincipal = 0, cumulativeInterest = 0;
const balanceData = allMonths.map(d => d.balance < 0 ? 0 : d.balance);
const principalData = allMonths.map(d => {
cumulativePrincipal += d.principal;
return Math.min(cumulativePrincipal, maxY);
});
const interestData = allMonths.map(d => {
cumulativeInterest += d.interest;
return Math.min(cumulativeInterest, maxY);
});
const oldBalanceData = noExtraMonths.map(d => d.balance < 0 ? 0 : d.balance);[createPath(oldBalanceData, 'var(--br-chart-balance)', true),
createPath(balanceData, 'var(--br-chart-balance)'),
createPath(principalData, 'var(--br-chart-principal)'),
createPath(interestData, 'var(--br-chart-interest)')].forEach(path => g.appendChild(path));for (let i = 0; i <= 5; i++) {
const y = h * (i / 5);
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute('x1', 0); line.setAttribute('x2', w); line.setAttribute('y1', y); line.setAttribute('y2', y); line.setAttribute('class', 'grid');
g.appendChild(line);
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute('x', -10); text.setAttribute('y', y + 4); text.setAttribute('class', 'axis-label'); text.setAttribute('text-anchor', 'end');
text.textContent = `$${(maxY * (1 - i / 5) / 1000).toFixed(0)}k`;
g.appendChild(text);
}const yearKeys = Object.keys(yearlyData).sort();
const step = Math.max(1, Math.ceil(yearKeys.length / 6));
for (let i = 0; i < yearKeys.length; i += step) {
const year = yearKeys[i];
const monthIndex = allMonths.findIndex(d => d.date.getFullYear() == year);
if (monthIndex < 0) continue;
const x = xScale(monthIndex);
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute('x1', x); line.setAttribute('x2', x); line.setAttribute('y1', 0); line.setAttribute('y2', h); line.setAttribute('class', 'grid');
g.appendChild(line);
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute('x', x); text.setAttribute('y', h + 20); text.setAttribute('class', 'axis-label'); text.setAttribute('text-anchor', 'middle');
text.textContent = year;
g.appendChild(text);
}
}function updatePayoffTable(yearlyData) {
payoffTableBody.innerHTML = '';
Object.keys(yearlyData).sort().forEach(year => {
const yearData = yearlyData[year];
const details = document.createElement('details');
details.innerHTML = `
${year}
${formatCurrency(yearData.totalPrincipal)}
${formatCurrency(yearData.totalInterest)}
${formatCurrency(yearData.endBalance)}
| Month | Principal | Interest | Remaining Balance |
${yearData.months.map(row => `| ${row.date.toLocaleString('default', { month: 'long' })} | ${formatCurrency(row.principal)} | ${formatCurrency(row.interest)} | ${formatCurrency(row.balance)} |
`).join('')}
`;
payoffTableBody.appendChild(details);
});
}calculateAndDisplay();
});