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
$0.00
Monthly Payment
$0.00
Loan Amount
$0.00
Over 0 Payments
$0.00
Total Interest
-
Pay-off Date
Amortization Schedule
Monthly Amortization Schedule
| Payment # |
Date |
Principal |
Interest |
PMI |
Tax |
Insurance |
Balance |
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';
}
});