1window.calculatorOutputs = window.calculatorOutputs || {}; 2window.calculatorHandlers.arm_mortgage_with_pmi_adjustment = function ($form, $container, response = null) { 3 function show_error(text) { 4 let errorContainer = $container.find(".out-error-message"); 5 let resultsContainer = $container.find(".wwd-calculator-results"); 6 7 if (text === "") { 8 resultsContainer.show(); 9 errorContainer.hide(); 10 } else { 11 resultsContainer.hide(); 12 errorContainer.text(text).show(); 13 } 14 } 15 16 // Get input values 17 var arm_type = $form.find('input[name="arm_type"]:checked').val(); 18 var homeValue = parseFloat($form.find("#home_value").val().replace(/,/g, "")); 19 var downPaymentPercentage = parseFloat($form.find("#down_payment_percentage").val().replace(/,/g, "")) / 100; 20 var downPaymentAmount = parseFloat($form.find("#down_payment_amount").val().replace(/,/g, "")); 21 var interestRate = parseFloat($form.find("#interest_rate").val().replace(/,/g, "")) / 100; 22 var loanTerm = parseInt($form.find('input[name="loan_term"]:checked').val().replace(/,/g, "")); 23 var includePMI = $form.find("#include_pmi").is(":checked"); 24 var includeTaxesInsurance = $form.find("#include_taxes_insurance").is(":checked"); 25 var propertyTaxPercentage = parseFloat($form.find("#property_tax_percentage").val().replace(/,/g, "")) / 100; 26 var homeownersInsurance = parseFloat($form.find("#homeowners_insurance").val().replace(/,/g, "")); 27 var homeownersInsurancePercentage = parseFloat($form.find("#homeowners_insurance_percentage").val().replace(/,/g, "")) / 100; 28 var hoaDues = parseFloat($form.find("#hoa_dues").val().replace(/,/g, "")); 29 30 // var homeAppreciationRate = parseFloat($form.find("#home_appreciation").val().replace(/,/g, "")) / 100; 31 var expectedAdjustment = parseFloat($form.find("#expected_adjustment").val().replace(/,/g, "")) / 100; 32 var rateDirection = $form.find('input[name="rate_direction"]:checked').val(); 33 var maxAdjustment = parseFloat($form.find("#max_adjustment").val().replace(/,/g, "")) / 100; 34 var rateFloor = parseFloat($form.find("#rate_floor").val().replace(/,/g, "")) / 100; 35 // Get ARM-specific inputs 36 var monthsBetweenAdjustments = parseInt($form.find("#months_between_adjustments").val()); 37 var monthsBeforeFirstAdjustment = parseInt($form.find("#months_before_first_adju
37stment").val()); 38 39 // Perform calculations 40 var downPayment = downPaymentAmount || homeValue * downPaymentPercentage; 41 var loanAmount = homeValue - downPayment; 42 var monthlyInterestRate = interestRate / 12; 43 var numberOfPayments = loanTerm * 12; 44 45 // Calculate monthly principal and interest 46 var monthlyPandI = (loanAmount * monthlyInterestRate * Math.pow(1 + monthlyInterestRate, numberOfPayments)) / (Math.pow(1 + monthlyInterestRate, numberOfPayments) - 1); 47 48 // Calculate LTV ratio 49 var ltvRatio = (loanAmount / homeValue) * 100; 50 51 // Calculate PMI rate based on LTV ratio and loan term 52 function getPMIRate(ltv, loanTerm) { 53 if (ltv <= 80) return 0; 54 if (ltv > 97) return 0; 55 56 const creditScore = jQuery('input[name="credit_score"]:checked').val(); 57 let ltvRange; 58 59 if (ltv > 97) { 60 ltvRange = "97_plus"; 61 } else if (ltv > 95) { 62 ltvRange = "95_97"; 63 } else if (ltv > 90) { 64 ltvRange = "90_95"; 65 } else if (ltv > 85) { 66 ltvRange = "85_90"; 67 } 68 69 const rateKey = `ltv_${ltvRange}_credit_score_${creditScore}`; 70 71 if (WWD_Calculators && WWD_Calculators.ltv_rate_adjustments && WWD_Calculators.ltv_rate_adjustments[rateKey]) { 72 return parseFloat(WWD_Calculators.ltv_rate_adjustments[rateKey]); 73 } 74 75 return 0; 76 } 77 78 // Calculate monthly PMI 79 var annualPMIRate = getPMIRate(ltvRatio, loanTerm); 80 var monthlyPMI = (loanAmount * annualPMIRate) / 100 / 12; 81 82 var currentHomeValue = homeValue; 83 84 // Calculate monthly taxes and insurance 85 var monthlyPropertyTax = (homeValue * propertyTaxPercentage) / 12; 86 var monthlyHomeownersInsurance = homeownersInsurance / 12; 87 var monthlyTaxesAndHOA = monthlyPropertyTax + hoaDues; 88 89 // Calculate total monthly payment 90 var totalMonthlyPayment = includePMI ? monthlyPandI + monthlyPMI : monthlyPandI; 91 totalMonthlyPayment += includeTaxesInsurance ? monthlyTaxesAndHOA + monthlyHomeownersInsurance : 0; 92 93 // Calculate amortization schedule 94 var amortizationSchedule = []; 95 var remainingBalance = loanAmount; 96 var totalPrincipal = 0; 97 var totalInterest = 0; 98 var currentInterestRate = interestRate; 99 var monthlyInterestRate = currentInterestRate / 12; 100 var currentMonthlyPMI = monthlyPMI; 101 102 for (var month = 1; month <= numberOfPayments; month++) { 103 // Check if it's time for a rate adjustment 104 if (month > monthsBeforeFirstAdjustment && (month - monthsBeforeFirstAdjustment) % monthsBetweenAdjustments === 0) { 105 // Adjust the interest rate 106 if (rateDirection === "up") { 107 currentInterestRate = Math.min(currentInterestRate + expectedAdjustment, interestRate + maxAdjustment); 108 } else { 109 currentInterestRate = Math.max(currentInterestRate - expectedAdjustment, rateFloor); 110 } 111 monthlyInterestRate = currentInterestRate / 12; 112 113 // Recalculate monthly payment 114 var remainingMonths = numberOfPayments - month + 1; 115 monthlyPandI = (remainingBalance * monthlyInterestRate * Math.pow(1 + monthlyInterestRate, remainingMonths)) / (Math.pow(1 + monthlyInterestRate, remainingMonths) - 1); 116 } 117 118 var interestPayment = remainingBalance * monthlyInterestRate; 119 var principalPayment = monthlyPandI - interestPayment; 120 remainingBalance -= principalPayment; 121 122 totalPrincipal += principalPayment; 123 totalInterest += interestPayment; 124 125 if (month % 12 === 1 && month !== 1) { 126 currentHomeValue *= 1; 127 128 // recalculate property tax 129 monthlyPropertyTax = (currentHomeValue * propertyTaxPercentage) / 12; 130 monthlyTaxesAndHOA = monthlyPropertyTax + hoaDues; 131 // recalculate homeowners insurance 132 monthlyHomeownersInsurance = (currentHomeValue * homeownersInsurancePercentage) / 12; 133 } 134 135 var currentLtvRatio = (remainingBalance / homeValue) * 100; 136 137 // Check if PMI should be removed 138 if (currentLtvRatio <= 78 && currentMonthlyPMI > 0) { 139 currentMonthlyPMI = 0; 140 } 141 142 amortizationSchedule.push({ 143 month: month, 144 payment: monthlyPandI, 145 principalPayment: principalPayment, 146 interestPayment: interestPayment, 147 monthlyPMI: currentMonthlyPMI ? currentMonthlyPMI : 0, 148 monthlyPropertyTax: includeTaxesInsurance ? monthlyPropertyTax : 0, 149 monthlyHomeownersInsurance: includeTaxesInsurance ? monthlyHomeownersInsurance : 0, 150 monthlyHOADues: includeTaxesInsurance ? hoaDues : 0, 151 monthlyTaxesHoa: includeTaxesInsurance ? monthlyTaxesAndHOA : 0, 152 remainingBalance: remainingBalance, 153 totalPrincipalPaid: totalPrincipal, 154 totalInterestPaid: totalInterest, 155 interestRate: currentInterestRate * 100,
156 homeValue: currentHomeValue, 157 }); 158 } 159 160 // Prepare result object 161 var result = { 162 armType: arm_type, 163 homeValue: wwd_moneyFormat(homeValue, 0), 164 loanAmount: wwd_moneyFormat(loanAmount, 0), 165 downPayment: wwd_moneyFormat(downPayment, 0), 166 interestRate: wwd_moneyFormat(interestRate * 100, 2) + " %", 167 loanTerm: loanTerm + " years", 168 loanTermMonths: loanTerm * 12, 169 loanTermYears: loanTerm, 170 taxesInsuranceIncluded: includeTaxesInsurance ? "Yes" : "No", 171 pmiIncluded: includePMI ? "Yes" : "No", 172 propertyTax: wwd_moneyFormat(propertyTaxPercentage * 100, 2) + " %", 173 homeownersInsurance: wwd_moneyFormat(homeownersInsurance, 0), 174 mortgageInsurance: wwd_moneyFormat(monthlyPMI, 0), 175 hoaDues: wwd_moneyFormat(hoaDues, 0), 176 monthlyPMI: wwd_moneyFormat(monthlyPMI, 0), 177 monthlyPandI: wwd_moneyFormat(monthlyPandI, 0), 178 monthlyTaxesAndHOA: wwd_moneyFormat(monthlyTaxesAndHOA, 0), 179 monthlyHomeownersInsurance: wwd_moneyFormat(monthlyHomeownersInsurance, 0), 180 totalMonthlyPayment: wwd_moneyFormat(totalMonthlyPayment, 0), 181 amortizationSchedule: amortizationSchedule, 182 totalPrincipal: wwd_moneyFormat(totalPrincipal, 0), 183 totalInterest: wwd_moneyFormat(totalInterest, 0), 184 }; 185 186 // Update the UI with the results 187 updateUI(result, $container); 188 189 // Clear any previous errors 190 show_error(""); 191 192 function updateUI(result, $container) { 193 // Add to window.wwd_results 194 window.wwd_results = window.wwd_results || {}; 195 196 for (let key in result) { 197 if (!Array.isArray(result[key])) { 198 window.wwd_results[key] = result[key]; 199 } 200 } 201 // Summary tab 202 $container.find("#out-p_and_i").text(result.monthlyPandI); 203 $container.find("#out-pmi").text(result.monthlyPMI); 204 $container.find("#out-taxes-hoa").text(result.monthlyTaxesAndHOA); 205 $container.find("#out-homeowners-insurance").text(result.monthlyHomeownersInsurance); 206 $container.find("#out-purchase-price").text(result.homeValue); 207 $container.find("#out-down-payment").text(result.downPayment); 208 $container.find("#out-total-loan-amount").text(result.loanAmount); 209 $container.find(".out-term-years").text(result.loanTermYears); 210 211 // if PMI is not included hide the PMI row. 212 if (result.pmiIncluded === "No") { 213 $container.find(".pmi-out-wrapper").hide(); 214 } else { 215 $container.find(".pmi-out-wrapper").show(); 216 } 217 218 // if taxes and insurance is not included hide the taxes and insurance row. 219 if (result.taxesInsuranceIncluded === "No") { 220 $container.find(".taxes-hoa-out-wrapper").hide(); 221 $container.find(".insurance-out-wrapper").hide(); 222 } else { 223 $container.find(".taxes-hoa-out-wrapper").show(); 224 $container.find(".insurance-out-wrapper").show(); 225 } 226 227 // Convert ARM type to display format 228 const armTypeMap = { 229 "10_1_arm": "10/1 ARM", 230 "7_1_arm": "7/1 ARM", 231 "5_1_arm": "5/1 ARM", 232 "3_1_arm": "3/1 ARM", 233 "10_6_month_arm": "10/6 month ARM", 234 "7_6_month_arm": "7/6 month ARM", 235 "5_6_month_arm": "5/6 month ARM", 236 "3_6_month_arm": "3/6 month ARM", 237 }; 238 const displayArmType = armTypeMap[result.armType]; 239 240 // Full Report tab 241 $container.find("#out-full-report-arm-type").text(displayArmType); 242 $container.find("#out-full-report-estimated-payment").text(result.totalMonthlyPayment); 243 $container.find("#out-full-report-loan-amount").text(result.loanAmount); 244 $container.find("#out-full-report-down-payment").text(result.downPayment); 245 $container.find("#out-full-report-interest-rate").text(result.interestRate); 246 $container.find("#out-full-report-loan-term").text(result.loanTerm); 247 $container.find("#out-full-report-taxes-insurance-included").text(result.taxesInsuranceIncluded); 248 $container.find("#out-full-report-property-tax").text(result.propertyTax + " /yr"); 249 $container.find("#out-full-report-homeowners-insurance").text(result.homeownersInsurance + " /yr"); 250 $container.find("#out-full-report-mortgage-insurance").text(result.monthlyPMI); 251 $container.find("#out-full-report-hoa-dues").text(result.hoaDues + " /mo"); 252 $container.find("#out-full-report-principal-payments").text(result.totalPrincipal); 253 $container.find("#out-full-report-interest-payments").text(result.totalInterest); 254 255 // Update the max value for #yearSlider 256 const termInMonths = parseInt(result.loanTermMonths); 257 $("#yearSlider").attr("max", termInMonths); 258 $("#yearSlider").val(0); 259 $(".out-term-years").text(result.loanTermYears); 260 $("#yearSliderFill").css("width", "0%"); 261 $("#yearSliderThumb").css("left", "0%"); 262 263 // Update payment breakdown chart 264 updatePaymentBreakdownChart(result, $container.find("#out-full-report-payment-breakdown"), "full-report-chart"); 265 updatePaymentBreakdownChart(result, $container.find("#out-payment-breakdown"), "summary-chart"); 266 267 // Update amortization chart 268 updateAmortizationChart(result, $container.find("#out-amortization-chart")); 269 270 // Update principal vs interest chart 271 updatePrincipalVsInterestChart(result, $container.find("#out-principal-vs-interest-chart")); 272 273 // Update amortization schedule 274 updateAmortizationSchedule(result, $container.find("#out-amortization-schedule-table")); 275 276 // Populate PDF input details 277 populatePDFInputDetails(); 278 } 279 280 // Function to populate PDF input details 281 function populatePDFInputDetails() { 282 const pdfInputDetails = $container.find("#pdf-input-details-grid"); 283 if (pdfInputDetails.length === 0) return; 284 285 const inputs = { 286 armType: arm_type, 287 homeValue: homeValue, 288 downPaymentAmount: downPaymentAmount, 289 downPaymentPercentage: downPaymentPercentage * 100,
290 interestRate: interestRate * 100, 291 loanTerm: loanTerm, 292 expectedAdjustment: expectedAdjustment * 100, 293 rateDirection: rateDirection, 294 maxAdjustment: maxAdjustment * 100, 295 rateFloor: rateFloor * 100, 296 includePMI: includePMI, 297 includeTaxesInsurance: includeTaxesInsurance, 298 propertyTaxPercentage: propertyTaxPercentage * 100, 299 homeownersInsurance: homeownersInsurance, 300 hoaDues: hoaDues, 301 }; 302 303 // ARM type display mapping 304 const armTypeDisplayMap = { 305 "10_1_arm": "10/1 ARM", 306 "7_1_arm": "7/1 ARM", 307 "5_1_arm": "5/1 ARM", 308 "3_1_arm": "3/1 ARM", 309 "10_6_month_arm": "10/6 month ARM", 310 "7_6_month_arm": "7/6 month ARM", 311 "5_6_month_arm": "5/6 month ARM", 312 "3_6_month_arm": "3/6 month ARM", 313 }; 314 315 const inputDetails = []; 316 317 // ARM Type 318 inputDetails.push({ 319 label: "ARM Type", 320 value: armTypeDisplayMap[inputs.armType] || inputs.armType, 321 }); 322 323 // Home Value / Purchase Price 324 inputDetails.push({ 325 label: "Purchase Price", 326 value: `$${wwd_moneyFormat(inputs.homeValue, 0)}`, 327 }); 328 329 // Down Payment 330 inputDetails.push({ 331 label: "Down Payment", 332 value: `$${wwd_moneyFormat(inputs.downPaymentAmount, 0)} (${inputs.downPaymentPercentage.toFixed(1)}%)`, 333 }); 334 335 // Initial Interest Rate 336 inputDetails.push({ 337 label: "Initial Interest Rate", 338 value: `${inputs.interestRate.toFixed(3)}%`, 339 }); 340 341 // Loan Term 342 inputDetails.push({ 343 label: "Loan Term", 344 value: `${inputs.loanTerm} years`, 345 }); 346 347 // Expected Adjustment 348 inputDetails.push({ 349 label: "Expected Adjustment", 350 value: `${inputs.expectedAdjustment.toFixed(2)}%`, 351 }); 352 353 // Rate Direction 354 inputDetails.push({ 355 label: "Rate Direction", 356 value: inputs.rateDirection === "up" ? "Increasing" : "Decreasing", 357 }); 358 359 // Max Adjustment 360 inputDetails.push({ 361 label: "Max Adjustment", 362 value: `${inputs.maxAdjustment.toFixed(2)}%`, 363 }); 364 365 // Rate Floor 366 inputDetails.push({ 367 label: "Rate Floor", 368 value: `${inputs.rateFloor.toFixed(2)}%`, 369 }); 370 371 // Include PMI 372 inputDetails.push({ 373 label: "Include PMI", 374 value: inputs.includePMI ? "Yes" : "No", 375 }); 376 377 // Include Taxes & Insurance 378 inputDetails.push({ 379 label: "Include Taxes & Insurance", 380 value: inputs.includeTaxesInsurance ? "Yes" : "No", 381 }); 382 383 // Property Tax 384 inputDetails.push({ 385 label: "Property Tax", 386 value: `${inputs.propertyTaxPercentage.toFixed(2)}%`, 387 }); 388 389 // Homeowners Insurance 390 inputDetails.push({ 391 label: "Homeowners Insurance", 392 value: `$${wwd_moneyFormat(inputs.homeownersInsurance, 0)}`, 393 }); 394 395 // HOA Dues 396 inputDetails.push({ 397 label: "HOA Dues", 398 value: `$${wwd_moneyFormat(inputs.hoaDues, 0)}`, 399 }); 400 401 let html = ""; 402 inputDetails.forEach((detail) => { 403 if (detail.value !== null) { 404 html += ` 405 <div> 406 <span class="block text-sm text-gray-600 mb-1">${detail.label}</span> 407 <span class="block text-lg font-bold">${detail.value}</span> 408 </div> 409 `; 410 } 411 }); 412 413 pdfInputDetails.html(html); 414 console.log("PDF Input Details: populated successfully"); 415 } 416 417 function updatePaymentBreakdownChart(result, $container, chartId) { 418 console.log("Updating chart with ID:", chartId); 419 console.log("Existing charts:", window.paymentBreakdownCharts); 420 421 // Destroy existing chart if it exists 422 if (window.paymentBreakdownCharts && window.paymentBreakdownCharts[chartId]) { 423 console.log("Attempting to destroy chart:", chartId); 424 try { 425 window.paymentBreakdownCharts[chartId].destroy(); 426 console.log("Chart destroyed successfully"); 427 } catch (error) { 428 console.error("Error destroying chart:", error); 429 } 430 delete window.paymentBreakdownCharts[chartId]; 431 } else { 432 console.log("No existing chart found to destroy"); 433 } 434 435 const amortizationSchedule = result.amortizationSchedule; 436 437 console.log(amortizationSchedule); 438 439 function updateChartData(monthIndex) { 440 console.log("Updating chart data for month:", monthIndex); 441 const scheduleItem = amortizationSchedule[monthIndex]; 442 443 if (!scheduleItem) { 444 console.error("No schedule item found for month:", monthIndex); 445 return; 446 } 447 448 // Parse the values, removing currency symbols and commas 449 const interestRate = parseFloat(scheduleItem.interestRate); 450 const principal = parseFloat(scheduleItem.principalPayment); 451 const interest = parseFloat(scheduleItem.interestPayment); 452 const pmi = parseFloat(scheduleItem.monthlyPMI); 453 const taxesAndHoa = parseFloat(scheduleItem.monthlyTaxesHoa); 454 const homeownersInsurance = parseFloat(scheduleItem.monthlyHomeownersInsurance); 455 456 // Update the chart data 457 chart.data.datasets[0].data = [principal, interest, pmi, taxesAndHoa, homeownersInsurance]; 458 459 const totalMonthlyPayment = principal + interest + pmi + taxesAndHoa + homeownersInsurance; 460 // Update the centerTextPlugin options 461 chart.options.plugins.centerText = { 462 text: wwd_moneyFormat(totalMonthlyPayment, 0), 463 }; 464
465 chart.update(); 466 467 // Update output elements 468 const currentDate = new Date(); 469 const futureDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + monthIndex, 1); 470 const year = Math.floor(monthIndex / 12) + 1; 471 const month = (monthIndex % 12) + 1; 472 473 $(".current-interest-rate").text(interestRate.toFixed(3) + "%"); 474 $(".out-current-year").text(year); 475 $(".out-current-month").text(month); 476 $(".current-year-savings").text("$" + wwd_moneyFormat(totalMonthlyPayment, 0)); 477 $(".out-current-month-year").text(futureDate.toLocaleString("default", { month: "long", year: "numeric" })); 478 479 // Update legend output 480 $("#out-monthly-principal").text(wwd_moneyFormat(principal, 0)); 481 $("#out-monthly-interest").text(wwd_moneyFormat(interest, 0)); 482 $("#out-pmi").text(wwd_moneyFormat(pmi, 0)); 483 $("#out-taxes-hoa").text(wwd_moneyFormat(taxesAndHoa, 0)); 484 $("#out-homeowners-insurance").text(wwd_moneyFormat(homeownersInsurance, 0)); 485 486 // Show/hide PMI row based on whether PMI is included 487 if (pmi > 0) { 488 $(".pmi-out-wrapper").show(); 489 } else { 490 $(".pmi-out-wrapper").hide(); 491 } 492 493 // Show/hide taxes and insurance rows based on whether they are included 494 if (taxesAndHoa > 0 || homeownersInsurance > 0) { 495 $(".taxes-hoa-out-wrapper").show(); 496 $(".insurance-out-wrapper").show(); 497 } else { 498 $(".taxes-hoa-out-wrapper").hide(); 499 $(".insurance-out-wrapper").hide(); 500 } 501 502 console.log("Chart data updated successfully"); 503 } 504 505 // Create the data for the chart 506 const data = { 507 labels: ["Principal", "Interest", "PMI", "Taxes and HOA", "Homeowners Insurance"], 508 datasets: [ 509 { 510 data: [0, 0, 0, 0, 0], 511 backgroundColor: [WWD_Calculators.colors.principal_color, WWD_Calculators.colors.interest_color, WWD_Calculators.colors.pmi_color, WWD_Calculators.colors.taxes_hoa_color, WWD_Calculators.colors.property_insurance_color], 512 hoverOffset: 20, 513 }, 514 ], 515 }; 516 517 // Custom plugin for center text 518 const centerTextPlugin = { 519 id: "centerText", 520 options: { 521 text: "0", 522 }, 523 afterDraw: (chart, args, options) => { 524 const width = chart.width; 525 const height = chart.height; 526 const ctx = chart.ctx; 527 ctx.restore(); 528 529 // Title text 530 const titleFontSize = (height / 360).toFixed(2); 531 ctx.font = `${titleFontSize}em "Plus Jakarta Sans"`; 532 ctx.fillStyle = "#000"; 533 ctx.textBaseline = "middle"; 534 ctx.textAlign = "center"; 535 ctx.fillText("Your Estimated", width / 2, height / 2.3 - height / 12); 536 ctx.fillText("Monthly Payment:", width / 2, height / 2.24 - height / 24); 537 538 // Amount text 539 const amountFontSize = (height / 110).toFixed(2); 540 ctx.font = `bold ${amountFontSize}em "Plus Jakarta Sans"`; 541 ctx.fillStyle = "#555"; 542 543 // Render dollar sign 544 const dollarSignFontSize = (height / 240).toFixed(2); 545 ctx.font = `${dollarSignFontSize}em "Plus Jakarta Sans"`; 546 ctx.fillText("$", width / 2.45 - ctx.measureText(result.totalMonthlyPayment).width / 2 - ctx.measureText("$").width / 2, height / 2.1 + height / 24); 547 548 // Render amount 549 ctx.font = `bold ${amountFontSize}em "Plus Jakarta Sans"`; 550 ctx.fontFamily = "Plus Jakarta Sans"; 551 ctx.fillStyle = "#000"; 552 const text = options.text; 553 // const text = result.totalMonthlyPayment.replace(/^\$/, "").trim(); 554 ctx.fillText(text, width / 2.15 + ctx.measureText("$").width / 2, height / 2 + height / 24); 555 556 // ctx.save(); 557 ctx.restore(); 558 }, 559 }; 560 561 // Chart configuration 562 const config = { 563 type: "doughnut", 564 data: data, 565 options: { 566 responsive: true, 567 plugins: { 568 legend: { 569 display: false, 570 }, 571 title: { 572 display: false, 573 }, 574 tooltip: { 575 callbacks: { 576 label: function (context) { 577 let label = context.label || ""; 578 if (label) { 579 label += ": "; 580 } 581 if (context.parsed !== null) { 582 label += "$" + window.wwd_moneyFormat(context.parsed, 0); 583 } 584 return label; 585 }, 586 }, 587 }, 588 }, 589 cutout: "70%", 590 onHover: (event, chartElement) => { 591 event.native.target.style.cursor = chartElement[0] ? "pointer" : "default"; 592 }, 593 layout: { 594 padding: 20, 595 }, 596 }, 597 plugins: [centerTextPlugin], 598 }; 599 600 // Create the chart 601 if (!window.paymentBreakdownCharts) { 602 window.paymentBreakdownCharts = {}; 603 } 604 const chart = new Chart($container[0], config); 605 window.paymentBreakdownCharts[chartId] = chart; 606 console.log("New chart created with ID:", chartId); 607 608 // Initialize the chart with the first month's data 609 updateChartData(0); 610 611 // Remove any existing event listener 612 $("#yearSlider").off("input.updateChartData"); 613 $("#yearSlider").val(0); 614 615 // Add event listener for the year slider with a namespace 616 $("#yearSlider").on("input.updateChartData", function () { 617 const selectedMonth = parseInt($(this).val()); 618 updateChartData(selectedMonth - 1); 619 }); 620 621 // Store the updateChartData function on the chart object 622 chart.updateChartData = updateChartData; 623 } 624 625 function updateAmortizationChart(result, $container) { 626 // Destroy existing chart if it exists 627 if (window.amortizationChart) { 628 window.amortizationChart.destroy(); 629 } 630 631 const schedule = result.amortizationSchedule; 632 const labels = schedule.map((entry) => entry.month); 633 const principalData = schedule.map((entry) => entry.totalPrincipalPaid); 634 const interestData = schedule.map((entry) => entry.totalInterestPaid); 635 const remainingBalanceData = schedule.map((entry) => entry.remainingBalance); 636 637 const principalColor = WWD_Calculators.colors.principal_color; 638 const interestColor = WWD_Calculators.colors.interest_color; 639 const remainingBalanceColor = WWD_Calculators.colors.remaining_balance_color; 640 641 const data = { 642 labels: labels, 643 datasets: [ 644 { 645 label: "Principal", 646 data: principalData, 647 borderColor: principalColor, 648 backgroundColor: principalColor, 649 fill: false, 650 tension: 0.1, 651 }, 652 { 653 label: "Interest", 654 data: interestData, 655 borderColor: interestColor, 656 backgroundColor: interestColor, 657 fill: false, 658 tension: 0.1, 659 }, 660 { 661 label: "Remaining", 662 data: remainingBalanceData, 663 borderColor: remainingBalanceColor, 664 backgroundColor: remainingBalanceColor, 665 fill: false, 666 tension: 0.1, 667 }, 668 ], 669 }; 670 671 const config = { 672 type: "line", 673 data: data, 674 options: { 675 responsive: true, 676 plugins: { 677 legend: { 678 position: "top", 679 }, 680 title: { 681 display: false, 682 text: "Amortization Schedule", 683 }, 684 tooltip: { 685 mode: "index", 686 intersect: false, 687 callbacks: { 688 title: function (context) { 689 return "Month " + context[0].label; 690 }, 691 label: function (context) { 692 let label = context.dataset.label || ""; 693 if (label) { 694 label += ": "; 695 } 696 if (context.parsed.y !== null) { 697 label += new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(context.parsed.y); 698 } 699 return label; 700 }, 701 }, 702 }, 703 }, 704 scales: { 705 x: { 706 title: { 707 display: true, 708 text: "Months", 709 }, 710 ticks: { 711 callback: function (value, index, values) { 712 return value % 100 === 0 ? value + " mo" : ""; 713 }, 714 }, 715 }, 716 y: { 717 title: { 718 display: true, 719 text: "Amount", 720 }, 721 ticks: { 722 callback: function (value, index, values) {
723 return "$" + value.toLocaleString(); 724 }, 725 }, 726 }, 727 }, 728 elements: { 729 line: { 730 borderWidth: 1.5, // Make lines thinner 731 }, 732 point: { 733 radius: 0, 734 hitRadius: 5, 735 hoverRadius: 3, 736 }, 737 }, 738 }, 739 }; 740 741 window.amortizationChart = new Chart($container[0], config); 742 } 743 744 function updatePrincipalVsInterestChart(result, $container) { 745 // Destroy existing chart if it exists 746 if (window.principalVsInterestChart) { 747 window.principalVsInterestChart.destroy(); 748 } 749 750 const schedule = result.amortizationSchedule; 751 const labels = schedule.map((entry) => entry.month); 752 const principalData = schedule.map((entry) => entry.principalPayment); 753 const interestData = schedule.map((entry) => entry.interestPayment); 754 755 const principalColor = WWD_Calculators.colors.principal_color; 756 const interestColor = WWD_Calculators.colors.interest_color; 757 758 const data = { 759 labels: labels, 760 datasets: [ 761 { 762 label: "Principal", 763 data: principalData, 764 backgroundColor: principalColor, 765 fill: true, 766 }, 767 { 768 label: "Interest", 769 data: interestData, 770 backgroundColor: interestColor, 771 fill: true, 772 }, 773 ], 774 }; 775 776 const config = { 777 type: "line", 778 data: data, 779 options: { 780 responsive: true, 781 plugins: { 782 legend: { 783 position: "top", 784 }, 785 title: { 786 display: false, 787 text: "Principal vs Interest", 788 }, 789 tooltip: { 790 mode: "index", 791 intersect: false, 792 callbacks: { 793 title: function (context) { 794 return "Month " + context[0].label; 795 }, 796 label: function (context) { 797 let label = context.dataset.label || ""; 798 if (label) { 799 label += ": "; 800 } 801 if (context.parsed.y !== null) { 802 label += new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(context.parsed.y); 803 } 804 return label; 805 }, 806 }, 807 }, 808 }, 809 scales: { 810 x: { 811 stacked: true, 812 title: { 813 display: true, 814 text: "Months", 815 }, 816 ticks: { 817 callback: function (value, index, values) { 818 return value % 100 === 0 ? value + "mo" : ""; 819 }, 820 }, 821 }, 822 y: { 823 stacked: true, 824 title: { 825 display: true, 826 text: "Amount", 827 }, 828 ticks: { 829 callback: function (value, index, values) { 830 return "$" + value.toLocaleString(); 831 }, 832 }, 833 }, 834 }, 835 elements: { 836 line: { 837 tension: 0.4, 838 }, 839 point: { 840 radius: 0, 841 }, 842 }, 843 }, 844 }; 845 846 window.principalVsInterestChart = new Chart($container[0], config); 847 } 848 849 function updateAmortizationSchedule(result, $container) { 850 const schedule = result.amortizationSchedule; 851 const itemsPerPage = 12;
852 let currentPage = 1; 853 854 function renderTable(page) { 855 const startIndex = (page - 1) * itemsPerPage; 856 const endIndex = startIndex + itemsPerPage; 857 const pageItems = schedule.slice(startIndex, endIndex); 858 859 let tableHtml = ` 860 <div class="overflow-x-auto w-full wwd-amortization-table"> 861 <table class="w-full text-sm text-left"> 862 <thead> 863 <tr class="bg-gray-100"> 864 <th class="px-4 py-2 text-[var(--wwd-heading-color)]">Month</th> 865 <th class="px-4 py-2 text-[var(--wwd-heading-color)]">Interest</th> 866 <th class="px-4 py-2 text-[var(--wwd-heading-color)]">Principal</th> 867 <th class="px-4 py-2 text-[var(--wwd-heading-color)]">Remaining</th> 868 </tr> 869 </thead> 870 <tbody> 871 `; 872 873 pageItems.forEach((item, index) => { 874 const rowClass = index % 2 === 0 ? "bg-white" : "bg-gray-50"; 875 tableHtml += ` 876 <tr class="${rowClass}"> 877 <td class="px-4 py-2 text-[var(--wwd-body-color)]">${item.month}</td> 878 <td class="px-4 py-2 text-[var(--wwd-body-color)]">$${wwd_moneyFormat(item.interestPayment, 0)}</td> 879 <td class="px-4 py-2 text-[var(--wwd-body-color)]">$${wwd_moneyFormat(item.principalPayment, 0)}</td> 880 <td class="px-4 py-2 text-[var(--wwd-body-color)]">$${wwd_moneyFormat(item.remainingBalance, 0)}</td> 881 </tr> 882 `; 883 }); 884 885 tableHtml += ` 886 </tbody> 887 </table> 888 </div> 889 `; 890 891 $container.html(tableHtml); 892 893 // Add pagination 894 const totalPages = Math.ceil(schedule.length / itemsPerPage); 895 let paginationHtml = ` 896 <div class="flex items-center justify-center mt-4 space-x-2"> 897 <button class="pagination-prev px-3 py-1 rounded border ${page === 1 ? "opacity-50 cursor-not-allowed" : ""}"><</button> 898 `; 899 900 for (let i = 1; i <= totalPages; i++) { 901 if (i === 1 || i === totalPages || (i >= page - 2 && i <= page + 2)) { 902 paginationHtml += ` 903 <button class="pagination-page px-1.5 sm:px-3 py-1 rounded border text-xs sm:text-sm ${i === page ? "bg-[var(--wwd-primary-color)] text-white" : ""}">${i}</button> 904 `; 905 } else if (i === page - 3 || i === page + 3) { 906 paginationHtml += `<span class="px-1">...</span>`; 907 } 908 } 909 910 paginationHtml += ` 911 <button class="pagination-next px-3 py-1 rounded border text-xs sm:text-sm ${page === totalPages ? "opacity-50 cursor-not-allowed" : ""}">></button> 912 </div> 913 `; 914 915 $container.append(paginationHtml); 916 917 // Add event listeners for pagination 918 $container.find(".pagination-prev").on("click", function () { 919 if (currentPage > 1) { 920 currentPage--; 921 renderTable(currentPage); 922 } 923 }); 924 925 $container.find(".pagination-next").on("click", function () { 926 if (currentPage < totalPages) { 927 currentPage++; 928 renderTable(currentPage); 929 } 930 }); 931 932 $container.find(".pagination-page").on("click", function () { 933 currentPage = parseInt(jQuery(this).text()); 934 renderTable(currentPage); 935 }); 936 } 937 938 renderTable(currentPage); 939 } 940}; 941 942function initARMMortgageWithPMIAdjustmentCalculator() { 943 var hasARMMortgageWithPMIAdjustmentCalculator = jQuery(".wwd-calculator-form[data-type='arm_mortgage_with_pmi_adjustment']").length > 0; 944 945 if (!hasARMMortgageWithPMIAdjustmentCalculator) { 946 return; 947 } 948 949 jQuery(".wwd-calculator-wrapper.arm_mortgage_with_pmi_adjustment-calculator").each(function () { 950 $ = jQuery; 951 const $wrapper = $(this); 952 const $interestRateInput = $wrapper.find('input[name="interest_rate"]'); 953 const $creditScoreInputs = $wrapper.find('input[name="credit_score"]'); 954 955 $creditScoreInputs.on("change", function () { 956 const selectedCreditScore = $(this).val(); 957 updateInterestRate(selectedCreditScore); 958 }); 959 960 function updateInterestRate(creditScore) { 961 if (WWD_Calculators && WWD_Calculators.credit_score_rates) { 962 const rateKey = `arm_mortgage_credit_score_${creditScore}`; 963 const newRate = WWD_Calculators.credit_score_rates[rateKey]; 964 965 if (newRate) { 966 $interestRateInput.val(newRate); 967 $interestRateInput.trigger("change"); 968 } 969 } 970 } 971 972 // Initialize interest rate based on default credit score 973 const defaultCreditScore = $creditScoreInputs.filter(":checked").val(); 974 updateInterestRate(defaultCreditScore); 975 }); 976 977 let lastUpdated = "amount"; 978 979 function updateDownPaymentAmount() { 980 var homeValue = parseFloat($("#home_value").val().replace(/,/g, "")) || 0; 981 var percentage = parseFloat($("#down_payment_percentage").val()) || 0; 982 var amount = homeValue * (percentage / 100); 983 $("#down_payment_amount").val(wwd_moneyFormat(amount, 0)); 984 } 985 986 function updateDownPaymentPercentage() { 987 var homeValue = parseFloat($("#home_value").val().replace(/,/g, "")) || 1; 988 var amount = parseFloat($("#down_payment_amount").val().replace(/,/g, "")) || 0; 989 var percentage = (amount / homeValue) * 100; 990 $("#down_payment_percentage").val(Math.min(percentage, 100).toFixed(2)); 991 $('input[data-type="range-input"][data-target="down_payment_percentage"]').val(Math.min(percentage, 80)); 992 // update progress 993 var $range = $('input[data-type="range-input"][data-target="down_payment_percentage"]'); 994 var val = parseFloat($("#down_payment_percentage").val()); 995 var min = parseFloat($range.attr("min")); 996 var max = parseFloat($range.attr("max")); 997 var progress = Math.min(((val - min) / (max - min)) * 100, 100); 998 $range.val(val); 999 $range[0].style.setProperty("--range-progress", `${progress}%`); 1000 } 1001 1002 function updateHomeValue() { 1003 var homeValue = parseFloat($("#home_value").val().replace(/,/g, "")) || 0; 1004 const $range = $('input[data-type="range-input"][data-target="home_value"]'); 1005 var min = parseFloat($range.attr("min")); 1006 var max = parseFloat($range.attr("max")); 1007 var progress = Math.min(((homeValue - min) / (max - min)) * 100, 100); 1008 $range.val(homeValue); 1009 $range[0].style.setProperty("--range-progress", `${progress}%`); 1010 if (lastUpdated === "amount") { 1011 updateDownPaymentPercentage(); 1012 } else { 1013 updateDownPaymentAmount(); 1014 } 1015 updatePropertyTaxAmount(); 1016 updateHomeownersInsuranceAmount(); 1017 } 1018 1019 let propertyTaxLastUpdated = "percentage"; 1020 1021 function updatePropertyTaxAmount() { 1022 var homeValue = parseFloat($("#home_value").val().replace(/,/g, "")) || 0; 1023 var taxPercentage = parseFloat($("#property_tax_percentage").val()) || 0; 1024 var taxAmount = homeValue * (taxPercentage / 100); 1025 $("#property_tax_amount").val(wwd_moneyFormat(taxAmount, 0)); 1026 } 1027 1028 function updatePropertyTaxPercentage() { 1029 var homeValue = parseFloat($("#home_value").val().replace(/,/g, "")) || 1; 1030 var taxAmount = parseFloat($("#property_tax_amount").val().replace(/,/g, "")) || 0; 1031 var percentage = (taxAmount / homeValue) * 100; 1032 $("#property_tax_percentage").val(percentage.toFixed(2)); 1033 $('input[data-type="range-input"][data-target="property_tax_percentage"]').val(percentage); 1034 updatePropertyTaxRangeProgress(); 1035 } 1036 1037 function updatePropertyTaxRangeProgress() { 1038 var $range = $('input[data-type="range-input"][data-target="property_tax_percentage"]'); 1039 var val = parseFloat($("#property_tax_percentage").val()); 1040 var min = parseFloat($range.attr("min")); 1041 var max = parseFloat($range.attr("max")); 1042 var progress = Math.min(((val - min) / (max - min)) * 100, 100); 1043 $range.val(val); 1044 $range[0].style.setProperty("--range-progress", `${progress}%`); 1045 } 1046 1047 function updateHomeownersInsuranceAmount() { 1048 var homeValue = parseFloat($("#home_value").val().replace(/,/g, "")) || 0; 1049 var insurancePercentage = parseFloat($("#homeowners_insurance_percentage").val()) || 0; 1050 var insuranceAmount = homeValue * (insurancePercentage / 100); 1051 $("#homeowners_insurance").val(wwd_moneyFormat(insuranceAmount, 0)); 1052 } 1053 1054 function updateHomeownersInsurancePercentage() { 1055 var homeValue = parseFloat($("#home_value").val().replace(/,/g, "")) || 1; 1056 var insuranceAmount = parseFloat($("#homeowners_insurance").val().replace(/,/g, "")) || 0; 1057 var percentage = (insuranceAmount / homeValue) * 100; 1058 $("#homeowners_insurance_percentage").val(percentage.toFixed(2)); 1059 $('input[data-type="range-input"][data-target="homeowners_insurance_percentage"]').val(percentage); 1060 updateHomeownersInsuranceRangeProgress(); 1061 } 1062 1063 function updateHomeownersInsuranceRangeProgress() { 1064 var $range = $('input[data-type="range-input"][data-target="homeowners_insurance_percentage"]'); 1065 var val = parseFloat($("#homeowners_insurance_percentage").val()); 1066 var min = parseFloat($range.attr("min")); 1067 var max = parseFloat($range.attr("max")); 1068 var progress = Math.min(((val - min) / (max - min)) * 100, 100); 1069 $range.val(val); 1070 $range[0].style.setProperty("--range-progress", `${progress}%`); 1071 } 1072 1073 jQuery(document).ready(function ($) { 1074 $("#down_payment_percentage").on("input", function () { 1075 lastUpdated = "percentage"; 1076 updateDownPaymentAmount(); 1077 var $range = $('input[data-type="range-input"][data-target="down_payment_percentage"]'); 1078 var val = parseFloat($(this).val()); 1079 var min = parseFloat($range.attr("min")); 1080 var max = parseFloat($range.attr("max")); 1081 1082 // Calculate and update progress 1083 var progress = Math.min(((val - min) / (max - min)) * 100, 100); 1084 $range.val(val); 1085 $range[0].style.setProperty("--range-progress", `${progress}%`); 1086 }); 1087 1088 $("#down_payment_amount").on("input", function () { 1089 lastUpdated = "amount"; 1090 updateDownPaymentPercentage(); 1091 }); 1092 1093 $("#home_value").on("input", updateHomeValue); 1094 1095 $("#property_tax_percentage").on("input", function () { 1096 propertyTaxLastUpdated = "percentage"; 1097 updatePropertyTaxAmount(); 1098 updatePropertyTaxRangeProgress(); 1099 }); 1100 1101 $("#property_tax_amount").on("input", function () { 1102 propertyTaxLastUpdated = "amount"; 1103 updatePropertyTaxPercentage(); 1104 }); 1105 1106 $('input[data-type="range-input"][data-target="property_tax_percentage"]').on("input", function () { 1107 propertyTaxLastUpdated = "percentage"; 1108 $("#property_tax_percentage").val($(this).val()).trigger("input"); 1109 }); 1110 1111 // Initialize property tax 1112 updatePropertyTaxAmount(); 1113 1114 $("#homeowners_insurance_percentage").on("input", function () { 1115 homeownersInsuranceLastUpdated = "percentage"; 1116 updateHomeownersInsuranceAmount(); 1117 updateHomeownersInsuranceRangeProgress(); 1118 }); 1119 1120 $("#homeowners_insurance").on("input", function () { 1121 homeownersInsuranceLastUpdated = "amount"; 1122 updateHomeownersInsurancePercentage(); 1123 }); 1124 1125 $('input[data-type="range-input"][data-target="homeowners_insurance_percentage"]').on("input", function () { 1126 homeownersInsuranceLastUpdated = "percentage"; 1127 $("#homeowners_insurance_percentage").val($(this).val()).trigger("input"); 1128 }); 1129 1130 // Initialize homeowners insurance 1131 updateHomeownersInsuranceAmount(); 1132 1133 $('input[data-type="range-input"][data-target="home_value"]').on("input", function () { 1134 $("#home_value") 1135 .val(wwd_moneyFormat($(this).val(), 0)) 1136 .trigger("input"); 1137 }); 1138 1139 $('input[data-type="range-input"][data-target="down_payment_percentage"]').on("input", function () { 1140 $("#down_payment_percentage").val($(this).val()).trigger("input"); 1141 }); 1142 1143 // Initialize 1144 updateDownPaymentPercentage(); 1145 updateHomeValue(); 1146 1147 // Function to update hidden fields based on ARM type 1148 function updateARMFields(armType) { 1149 let monthsBetweenAdjustments, monthsBeforeFirstAdjustment; 1150 1151 switch (armType) { 1152 case "10_1_arm": 1153 monthsBetweenAdjustments = 12; 1154 monthsBeforeFirstAdjustment = 120; 1155 break; 1156 case "7_1_arm": 1157 monthsBetweenAdjustments = 12; 1158 monthsBeforeFirstAdjustment = 84; 1159 break; 1160 case "5_1_arm": 1161 monthsBetweenAdjustments = 12; 1162 monthsBeforeFirstAdjustment = 60; 1163 break; 1164 case "3_1_arm": 1165 monthsBetweenAdjustments = 12; 1166 monthsBeforeFirstAdjustment = 36; 1167 break; 1168 case "10_6_month_arm": 1169 monthsBetweenAdjustments = 6; 1170 monthsBeforeFirstAdjustment = 120; 1171 break; 1172 case "7_6_month_arm": 1173 monthsBetweenAdjustments = 6; 1174 monthsBeforeFirstAdjustment = 84; 1175 break; 1176 case "5_6_month_arm": 1177 monthsBetweenAdjustments = 6; 1178 monthsBeforeFirstAdjustment = 60; 1179 break; 1180 case "3_6_month_arm": 1181 monthsBetweenAdjustments = 6; 1182 monthsBeforeFirstAdjustment = 36; 1183 break; 1184 default: 1185 monthsBetweenAdjustments = 12; 1186 monthsBeforeFirstAdjustment = 60; 1187 } 1188 1189 $("#months_between_adjustments").val(monthsBetweenAdjustments);
1190 $("#months_before_first_adjustment").val(monthsBeforeFirstAdjustment); 1191 } 1192 1193 // Listen for changes on the ARM type radio buttons 1194 $('input[name="arm_type"]').on("change", function () { 1195 updateARMFields($(this).val()); 1196 }); 1197 1198 // Initial update based on the default selected ARM type 1199 updateARMFields($('input[name="arm_type"]:checked').val()); 1200 1201 $("#interest_rate").on("input", function () { 1202 var $range = $('input[data-type="range-input"][data-target="interest_rate"]'); 1203 var val = parseFloat($(this).val()); 1204 var min = parseFloat($range.attr("min")); 1205 var max = parseFloat($range.attr("max")); 1206 1207 // Calculate and update progress 1208 var progress = Math.min(((val - min) / (max - min)) * 100, 100); 1209 1210 $range.val(val); 1211 $range[0].style.setProperty("--range-progress", `${progress}%`); 1212 }); 1213 }); 1214 1215 // Specific event listeners for ARM mortgage calculator 1216 window.addEventListener("load", function () { 1217 window.wwd_submitForms("arm_mortgage_with_pmi_adjustment"); 1218 }); 1219 1220 window.addEventListener("wwdTabSwitch", handleARMMortgageWithPMIAdjustmentCalculatorSubmit); 1221 1222 function handleARMMortgageWithPMIAdjustmentCalculatorSubmit() { 1223 setTimeout(function () { 1224 window.wwd_submitForms("arm_mortgage_with_pmi_adjustment"); 1225 }, 100); 1226 } 1227} 1228 1229// Initialize the ARM mortgage calculator 1230initARMMortgageWithPMIAdjustmentCalculator();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.