1window.calculatorHandlers = window.calculatorHandlers || {}; 2window.calculatorOutputs = window.calculatorOutputs || {}; 3window.calculatorHandlers.rent_vs_buy = function ($form, $container, response = null) { 4 function show_error(text) { 5 let errorContainer = $container.find(".out-error-message"); 6 let resultsContainer = $container.find(".wwd-calculator-results"); 7 8 if (text === "") { 9 resultsContainer.show(); 10 errorContainer.hide(); 11 } else { 12 resultsContainer.hide(); 13 errorContainer.text(text).show(); 14 } 15 } 16 17 // Get input values 18 var homeValue = parseFloat($form.find("#home_value").val().replace(/,/g, "")); 19 var downPaymentAmount = parseFloat($form.find("#down_payment_amount").val().replace(/,/g, "")); 20 var downPaymentPercentage = parseFloat($form.find("#down_payment_percentage").val()) / 100; 21 var monthlyRent = parseFloat($form.find("#monthly_rent").val().replace(/,/g, "")); 22 var interestRate = parseFloat($form.find("#interest_rate").val()) / 100; 23 var loanTerm = parseInt($form.find('input[name="loan_term"]:checked').val()); 24 var homeownersInsurance = parseFloat($form.find("#homeowners_insurance").val().replace(/,/g, "")); 25 var hoaDues = parseFloat($form.find("#hoa_dues").val().replace(/,/g, "")); 26 var propertyTax = parseFloat($form.find("#property_tax_percentage").val()) / 100; 27 var propertyTaxAmount = parseFloat($form.find("#property_tax_amount").val()) / 100; 28 var homeRepairs = parseFloat($form.find("#home_repairs").val()) / 100; 29 var renovations = parseFloat($form.find("#renovations").val()) / 100; 30 var closingCostsPercentage = parseFloat($form.find("#closing_costs_percentage").val()) / 100; 31 var sellingClosingCostsPercentage = parseFloat($form.find("#selling_closing_costs_percentage").val()) / 100; 32 var yearlyRentIncrease = parseFloat($form.find("#yearly_rent_increase").val()) / 100; 33 var homeAppreciation = parseFloat($form.find("#home_appreciation").val()) / 100; 34 var rentalInsurance = parseFloat($form.find("#rental_insurance").val().replace(/,/g, "")); 35 36 // Calculate loan amount 37 var loanAmount = homeValue - downPaymentAmount; 38 39 // Calculate monthly mortgage payment 40 var monthlyInterestRate = interestRate / 12; 41 var numberOfPayments = loanTerm * 12; 42 var monthlyMortgage = (loanAmount * monthlyInterestRate * Math.pow(1 + monthlyInterestRate, numberOfPayments)) / (Math.pow(1 + monthlyInterestRate, numberOfPayments) - 1); 43 44 // Calculate LTV ratio 45 var ltvRatio = (loanAmount / homeValue) * 100; 46 47 // Calculate PMI rate based on LTV ratio and loan term 48 function getPMIRate(ltv, loanTerm) { 49 if (ltv <= 80) return 0; 50 if (ltv > 97) return 0; // Assuming no PMI above 97% LTV 51 52 if (loanTerm <= 20) { 53 if (ltv <= 85) return 0.06; 54 if (ltv <= 90) return 0.12; 55 if (ltv <= 95) return 0.25; 56 return 0.35; 57 } else { 58 if (ltv <= 85) return 0.12; 59 if (ltv <= 90) return 0.25; 60 if (ltv <= 95) return 0.3; 61 return 0.35; 62 } 63 } 64 65 // Calculate monthly PMI 66 var annualPMIRate = getPMIRate(ltvRatio, loanTerm); 67 var yearlyPMI = (loanAmount * annualPMIRate) / 100; 68 var monthlyPMI = (loanAmount * annualPMIRate) / 100 / 12; 69 70 // Calculate monthly property tax 71 var monthlyPropertyTax = propertyTaxAmount / 12; 72 73 // Calculate monthly home repairs and renovations 74 var monthlyHomeRepairs = (homeValue * homeRepairs) / 12; 75 var monthlyRenovations = (homeValue * renovations) / 12; 76 77 // Calculate the closing costs 78 var closingCosts = homeValue * closingCostsPercentage; 79 80 // Calculate total monthly cost of buying 81 var monthlyBuyCost = monthlyMortgage + monthlyPropertyTax + homeownersInsurance / 12 + hoaDues + monthlyHomeRepairs + monthlyRenovations + monthlyPMI; 82 83 // Initialize array to store yearly data 84 var yearlyData = []; 85 86 // Calculate data for each year 87 var rentTotalCost = 0; 88 var buyTotalCost = 0; 89 var remainingMortgageBalance = loanAmount; 90 var currentHomeValue = homeValue; 91 92 var monthlyInterestRate = interestRate / 12; 93 94 for (var month = 1; month <= numberOfPayments; month++) { 95 var monthlyInterest = remainingMortgageBalance * monthlyInterestRate; 96 var principalPayment = monthlyMortgage - monthlyInterest; 97 remainingMortgageBalance = Math.max(0, remainingMortgageBalance - principalPayment); 98 99 if (month % 12 === 0) { 100 currentHomeValue *= 1 + homeAppreciation; 101 } 102 103 // Recalculate monthly costs based on current home value 104 var monthlyPropertyTax = (currentHomeValue * propertyTax) / 12; 105 var monthlyHomeRepairs = (currentHomeValue * homeRepairs) / 12; 106 var monthlyRenovations = (currentHomeValue * renovations) / 12; 107 108 // Update monthly buy cost 109 monthlyBuyCost = monthlyMortgage + monthlyPropertyTax + homeownersInsurance / 12 + hoaDues + monthlyHomeRepairs + monthlyRenovations + monthlyPMI; 110 111 if (month % 12 === 0) { 112 var year = month / 12; 113 var rentMonthlyCost = monthlyRent * Math.pow(1 + yearlyRentIncrease, year - 1); 114 var rentYearlyCost = rentMonthlyCost * 12 + rentalInsurance; 115 116 // Add security deposit for the first year of renting 117 if (year === 1) { 118 rentYearlyCost += monthlyRent; // Assuming one month's rent as security deposit 119 } 120 121 rentTotalCost += rentYearlyCost; 122 123 var buyYearlyCost = monthlyBuyCost * 12; 124 125 // Add down payment and closing costs for the first year of buying 126 if (year === 1) { 127 buyYearlyCost += downPaymentAmount + closingCosts; 128 } 129 130 // Calculate selling costs 131 var sellingCosts = currentHomeValue * sellingClosingCostsPercentage; 132 133 // Calculate total cost if selling this year 134 var totalCostIfSelling = buyTotalCost + buyYearlyCost + sellingCosts - (currentHomeValue - remainingMortgageBalance); 135 136 buyTotalCost += buyYearlyCost; 137 138 yearlyData.push({ 139 year: year, 140 rentMonthlyCost: rentMonthlyCost,
141 buyMonthlyCost: monthlyBuyCost, 142 rentYearlyCost: rentYearlyCost, 143 buyYearlyCost: buyYearlyCost, 144 rentTotalCost: rentTotalCost, 145 buyTotalCost: totalCostIfSelling, 146 totalSaved: rentTotalCost - totalCostIfSelling, 147 totalMonthlySaved: (rentTotalCost - totalCostIfSelling) / month, 148 currentHomeValue: currentHomeValue, 149 currentHomeEquity: currentHomeValue - remainingMortgageBalance, 150 remainingMortgageBalance: remainingMortgageBalance, 151 }); 152 } 153 } 154 155 // Calculate break-even point 156 var breakEvenYear = yearlyData.findIndex((data) => data.buyTotalCost < data.rentTotalCost); 157 if (breakEvenYear === -1) { 158 breakEvenYear = "Never"; 159 } else { 160 breakEvenYear += 1; // Add 1 because array index starts at 0 161 } 162 163 // Prepare result object 164 var result = { 165 homeValue: homeValue, 166 homeAppreciation: homeAppreciation, 167 downPaymentAmount: downPaymentAmount, 168 downPaymentPercentage: downPaymentPercentage, 169 monthlyRent: monthlyRent, 170 interestRate: interestRate, 171 loanTerm: loanTerm, 172 homeownersInsurance: homeownersInsurance, 173 hoaDues: hoaDues, 174 propertyTax: propertyTax, 175 propertyTaxAmount: homeValue * propertyTax, 176 homeRepairsPercentage: homeRepairs, 177 homeRepairsAmount: homeValue * homeRepairs, 178 renovationsPercentage: renovations, 179 renovationsAmount: homeValue * renovations, 180 closingCostsPercentage: closingCostsPercentage, 181 sellingClosingCostsPercentage: sellingClosingCostsPercentage, 182 closingCostsAmount: closingCosts, 183 yearlyRentIncrease: yearlyRentIncrease, 184 rentalInsurance: rentalInsurance, 185 loanAmount: loanAmount, 186 monthlyMortgage: monthlyMortgage, 187 yearlyPMI: yearlyPMI, 188 yearlyData: yearlyData, 189 breakEvenYear: breakEvenYear, 190 }; 191 192 // Update the UI with the results 193 updateUI(result, $container); 194 195 // Clear any previous errors 196 show_error(""); 197 198 function updateUI(result, $container) { 199 // Add to window.wwd_results 200 window.wwd_results = window.wwd_results || {}; 201 202 for (let key in result) { 203 if (!Array.isArray(result[key])) { 204 // Format number values with commas and appropriate decimal places 205 if (typeof result[key] === "number") { 206 if (key.toLowerCase().includes("rate") || key.toLowerCase().includes("percentage")) { 207 // Format percentages with 3 decimal places 208 window.wwd_results[key] = wwd_moneyFormat(result[key] * 100, 3) + "%"; 209 } else { 210 // Format other numbers with 2 decimal places 211 window.wwd_results[key] = wwd_moneyFormat(result[key], 2); 212 } 213 } else { 214 window.wwd_results[key] = result[key]; 215 } 216 } 217 } 218 219 // if there is a breakeven point we add class to #wwd-calcresults 220 if (result.breakEvenYear !== "Never") { 221 $container.addClass("breakeven-point"); 222 $container.removeClass("breakeven-point-never"); 223 } else { 224 $container.removeClass("breakeven-point"); 225 $container.addClass("breakeven-point-never"); 226 } 227 228 var breakevenText = ""; 229 var breakevenTextLower = ""; 230 if (result.breakEvenYear !== "Never") { 231 var yearText = result.breakEvenYear === 1 ? "Year" : "Years"; 232 breakevenText = `${result.breakEvenYear} ${yearText}`; 233 breakevenTextLower = breakevenText.toLowerCase(); 234 } 235 236 // Summary tab 237 $container.find("#out-breakeven-text").text(breakevenText); 238 $container.find(".out-term-years").text(result.loanTerm); 239 240 // Update rent vs buy chart 241 updateRentVsBuyChart(result, $container.find("#out-rent-vs-buy-chart"), "rent-v-buy-chart"); 242 243 // Update Estimated Costs section 244 $container.find("#out-full-report-homeowners-insurance").text(result.homeownersInsurance.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 245 $container.find("#out-full-report-hoa-dues").text(result.hoaDues.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 246 $container.find("#out-full-report-property-tax-amount").text(result.propertyTaxAmount.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 247 $container.find("#out-full-report-home-repairs-amount").text(result.homeRepairsAmount.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 248 $container.find("#out-full-report-renovations-amount").text(result.renovationsAmount.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 249 $container.find("#out-full-report-closing-costs-amount").text(result.closingCostsAmount.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 250 $container.find("#out-full-report-yearly-rent-increase").text((result.yearlyRentIncrease * 100).toFixed(2)); 251 $container.find("#out-full-report-rental-insurance").text(result.rentalInsurance.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 252 $container.find("#out-full-report-monthly-mortgage").text(result.monthlyMortgage.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 253 $container.find("#out-full-report-yearly-pmi").text(result.yearlyPMI.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 254 255 // Update How We Calculate the Costs section 256 $container.find("#out-home-appreciation-rate").text((result.homeAppreciation * 100).toFixed(2)); 257 $container.find("#out-rent-increase-rate").text((result.yearlyRentIncrease * 100).toFixed(2)); 258 $container.find("#out-renters-insurance-cost").text(result.rentalInsurance.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 259 $container.find("#out-home-repairs-rate").text((result.homeRepairsPercentage * 100).toFixed(2)); 260 $container.find("#out-renovations-rate").text((result.renovationsPercentage * 100).toFixed(2)); 261 $container.find("#out-selling-costs-rate").text((result.sellingClosingCostsPercentage * 100).toFixed(2)); 262 } 263 function updateRentVsBuyChart(result, $chart, chartId) { 264 // Assuming you have included Chart.js in your project 265 const ctx = document.getElementById("rentVsBuyChart").getContext("2d"); 266 267 // Generate data for 30 years 268 const years = result.yearlyData.map((data) => data.year); 269 const rentData = result.yearlyData.map((data) => data.rentTotalCost); 270 const buyData = result.yearlyData.map((data) => data.buyTotalCost); 271 272 const maxValue = Math.max(...rentData, ...buyData); 273 const tickSpacing = Math.pow(10, Math.floor(Math.log10(maxValue)) - 1);
274 const roundedMaxValue = Math.ceil(maxValue / tickSpacing) * tickSpacing; 275 const midValue = roundedMaxValue / 2; 276 let midPointTicked = false; 277 let midPointLineDrawn = false; 278 279 updateHTMLValues(result.breakEvenYear === "Never" ? years.length - 1 : result.breakEvenYear); 280 281 const breakEvenPlugin = { 282 id: "breakEvenLine", 283 afterDraw: (chart, args, options) => { 284 const { 285 ctx, 286 chartArea: { top, bottom, left, right, width, height }, 287 scales: { x, y }, 288 } = chart; 289 const breakEvenYear = chart.options.plugins.breakEvenLine.breakEvenYear; 290 291 // Only draw the break-even line if there is a valid break-even year 292 if (breakEvenYear !== "Never" && breakEvenYear >= 0) { 293 ctx.save(); 294 295 // Draw vertical line 296 const xPos = x.getPixelForValue(breakEvenYear); 297 298 ctx.strokeStyle = "rgba(0, 0, 0, 0.1)"; 299 ctx.setLineDash([5, 5]); 300 ctx.beginPath(); 301 ctx.moveTo(xPos, top + 5); 302 ctx.lineTo(xPos, bottom); 303 ctx.stroke(); 304 305 // Draw "Break Even" text 306 ctx.font = "14px var(--wwd-font-family)"; 307 ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; 308 ctx.textAlign = "center"; 309 ctx.fillText("BREAK EVEN", xPos, top + 20); 310 311 ctx.restore(); 312 } 313 }, 314 }; 315 316 let chart = Chart.getChart(ctx); 317 318 if (chart) { 319 // update the chart 320 chart.data.labels = years; 321 chart.data.datasets[0].data = rentData; 322 chart.data.datasets[1].data = buyData; 323 324 // Recalculate y-axis scale 325 const maxValue = Math.max(...rentData, ...buyData); 326 const tickSpacing = Math.pow(10, Math.floor(Math.log10(maxValue)) - 1); 327 const roundedMaxValue = Math.ceil(maxValue / tickSpacing) * tickSpacing; 328 const midValue = roundedMaxValue / 2; 329 330 // Update y-axis configuration 331 chart.options.scales.y.max = roundedMaxValue; 332 chart.options.scales.y.ticks.callback = function (value, index, values) { 333 if (value === 0 || value === roundedMaxValue || (value > midValue && !this.midPointTicked)) {
334 this.midPointTicked = value > midValue; 335 if (value >= 1000) { 336 return "$" + (value / 1000).toFixed(0) + "K"; 337 } else { 338 return "$" + value.toLocaleString(); 339 } 340 } 341 return ""; 342 }; 343 344 chart.options.scales.y.grid.color = function (context) { 345 if (context.tick.value === roundedMaxValue || (context.tick.value > midValue && !this.midPointLineDrawn)) { 346 this.midPointLineDrawn = context.tick.value > midValue; 347 return "rgba(0, 0, 0, 0.1)"; 348 } 349 return "rgba(0, 0, 0, 0)"; 350 }; 351 352 // Reset the midPointTicked and midPointLineDrawn flags 353 chart.options.scales.y.ticks.midPointTicked = false; 354 chart.options.scales.y.grid.midPointLineDrawn = false; 355 356 // Update break-even line plugin 357 chart.options.plugins.breakEvenLine.breakEvenYear = result.breakEvenYear - 1; 358 359 chart.update(); 360 } else { 361 chart = new Chart(ctx, { 362 type: "line", 363 data: { 364 labels: years, 365 datasets: [ 366 { 367 label: "Rent", 368 data: rentData, 369 borderColor: WWD_Calculators.colors.rent_vs_buy_calc_rent_color, 370 backgroundColor: WWD_Calculators.colors.rent_vs_buy_calc_rent_color, 371 pointRadius: 0, 372 borderWidth: 2, 373 }, 374 { 375 label: "Buy", 376 data: buyData, 377 borderColor: WWD_Calculators.colors.rent_vs_buy_calc_buy_color, 378 backgroundColor: WWD_Calculators.colors.rent_vs_buy_calc_buy_color, 379 pointRadius: 0, 380 borderWidth: 2, 381 }, 382 ], 383 }, 384 options: { 385 responsive: true, 386 maintainAspectRatio: false, 387 scales: { 388 x: { 389 title: { 390 display: false, 391 text: "YEARS", 392 }, 393 ticks: { 394 callback: function (value, index, values) { 395 return (value + 1) % 2 === 0 ? value + 1 : ""; 396 }, 397 }, 398 grid: { 399 display: true, 400 drawBorder: true, 401 drawOnChartArea: true, 402 drawTicks: true, 403 color: function (context) { 404 return "rgba(0, 0, 0, 0)"; 405 }, 406 }, 407 }, 408 y: { 409 title: { 410 display: false, 411 text: "TOTAL COST", 412 }, 413 ticks: { 414 callback: function (value, index, values) { 415 if (value === 0 || value === roundedMaxValue || (value > midValue && !midPointTicked)) {
416 midPointTicked = value > midValue; 417 if (value >= 1000) { 418 return "$" + (value / 1000).toFixed(0) + "K"; 419 } else { 420 return "$" + value.toLocaleString(); 421 } 422 } 423 return ""; 424 }, 425 }, 426 min: 0, 427 max: roundedMaxValue, 428 grid: { 429 drawBorder: false, 430 color: function (context) { 431 if (context.tick.value === roundedMaxValue || (context.tick.value > midValue && !midPointLineDrawn)) { 432 midPointLineDrawn = context.tick.value > midValue; 433 return "rgba(0, 0, 0, 0.1)"; 434 } 435 return "rgba(0, 0, 0, 0)"; 436 }, 437 }, 438 border: { 439 display: false, 440 }, 441 }, 442 }, 443 plugins: { 444 tooltip: { 445 enabled: false, // Disable default tooltip 446 external: customTooltip, // Use our custom tooltip 447 mode: "index", 448 intersect: false, 449 }, 450 legend: { 451 display: true, 452 labels: { 453 color: "rgb(255, 99, 132, 0)", 454 boxWidth: 0, 455 }, 456 }, 457 breakEvenLine: { 458 breakEvenYear: result.breakEvenYear - 1, 459 }, 460 }, 461 onHover: (event, chartElement) => { 462 if (chartElement.length) { 463 const datasetIndex = chartElement[0].datasetIndex; 464 const index = chartElement[0].index; 465 updateChartTooltip(index, datasetIndex); 466 } 467 }, 468 }, 469 plugins: [ 470 breakEvenPlugin, 471 { 472 id: "tooltipLine", 473 beforeDraw: (chart) => { 474 if (chart.tooltip._active && chart.tooltip._active.length) { 475 const activePoint = chart.tooltip._active[0]; 476 const { ctx } = chart; 477 const { x } = activePoint.element; 478 const topY = chart.scales.y.top; 479 const bottomY = chart.scales.y.bottom; 480 481 // Draw vertical line 482 ctx.save(); 483 ctx.beginPath(); 484 ctx.moveTo(x, topY); 485 ctx.lineTo(x, bottomY); 486 ctx.lineWidth = 1; 487 ctx.strokeStyle = "rgba(0, 0, 0, 0.5)"; 488 ctx.stroke(); 489 ctx.restore(); 490 } 491 }, 492 }, 493 { 494 id: "customHover", 495 beforeEvent: (chart, args, pluginOptions) => { 496 const event = args.event; 497 if (event.type === "mousemove") { 498 const canvasPosition = Chart.helpers.getRelativePosition(event, chart); 499 const dataX = chart.scales.x.getValueForPixel(canvasPosition.x); 500 if (dataX >= 0 && dataX <= chart.data.labels.length - 1) { 501 updateChartTooltip(Math.round(dataX)); 502 } 503 } 504 }, 505 }, 506 ], 507 }); 508 } 509 510 window.rentVsBuyChart = chart; 511 512 // if the chart has been destroyed return 513 if (!chart) { 514 return; 515 } 516 517 function updateChartTooltip(index, datasetIndex = 0) { 518 const dataPoint = chart.data.datasets[datasetIndex].data[index]; 519 const position = chart.getDatasetMeta(datasetIndex).data[index].getCenterPoint(); 520 521 chart.tooltip.setActiveElements( 522 [ 523 { datasetIndex: 0, index: index }, 524 { datasetIndex: 1, index: index }, 525 ], 526 { 527 x: position.x, 528 y: position.y, 529 } 530 ); 531 532 chart.draw(); 533 } 534 535 const yearSlider = document.getElementById("yearSlider"); 536 yearSlider.min = 1; 537 yearSlider.max = years.length; 538 yearSlider.value = result.breakEvenYear === "Never" ? years.length : result.breakEvenYear; 539 540 // Add event listener to the slider 541 document.getElementById("yearSlider").addEventListener("input", function (e) { 542 const year = parseInt(e.target.value); 543 updateChartTooltip(year - 1); 544 updateHTMLValues(year - 1); 545 }); 546 547 // trigger input event on yearSlider to update the HTML values 548 yearSlider.dispatchEvent(new Event("input")); 549 550 // Implement the custom tooltip function 551 function customTooltip(context) { 552 // Make sure we have tooltip items 553 if (context.tooltip.dataPoints.length === 0) return; 554 555 const year = context.tooltip.dataPoints[0].label; 556 const rentValue = context.tooltip.dataPoints[0].raw; 557 const buyValue = context.tooltip.dataPoints[1].raw; 558 559 // Create tooltip element 560 let tooltipEl = document.getElementById("chartjs-tooltip"); 561 562 if (!tooltipEl) { 563 tooltipEl = document.createElement("div"); 564 tooltipEl.id = "chartjs-tooltip"; 565 tooltipEl.classList.add("chartjs-tooltip"); 566 tooltipEl.innerHTML = "<div class='bg-white shadow-md p-2'></div>"; 567 document.body.appendChild(tooltipEl); 568 } 569 570 // Hide tooltip if no point active 571 if (context.tooltip.opacity === 0) { 572 tooltipEl.style.opacity = 0; 573 return; 574 } 575 576 // Set content 577 const tableBody = `<div class="text-sm text-gray-700 font-bold mb-1">Year ${year}</div> 578 <div class="flex items-center mb-0.5">
579 <span class="w-3 h-3 rounded-full bg-[var(--wwd-rent-vs-buy-calc-rent-color)] mr-2"></span> 580 <span class="text-sm mr-2">Rent</span> 581 <span class="text-sm text-gray-600 font-bold">${new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(rentValue)}</span> 582 </div> 583 <div class="flex items-center"> 584 <span class="w-3 h-3 rounded-full bg-[var(--wwd-rent-vs-buy-calc-buy-color)] mr-2"></span> 585 <span class="text-sm mr-2">Buy</span> 586 <span class="text-sm text-gray-600 font-bold">${new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(buyValue)}</span> 587 </div>`; 588 589 const tableRoot = tooltipEl.querySelector("div"); 590 tableRoot.innerHTML = tableBody; 591 592 // Position tooltip 593 const position = context.chart.canvas.getBoundingClientRect(); 594 const chartWidth = position.width; 595 const cursorX = context.tooltip.caretX; 596 597 if (cursorX > chartWidth / 2) { 598 tooltipEl.style.left = position.left + window.pageXOffset + context.tooltip.caretX - tooltipEl.offsetWidth - 20 + "px"; 599 } else { 600 tooltipEl.style.left = 20 + position.left + window.pageXOffset + context.tooltip.caretX + "px"; 601 } 602 603 tooltipEl.style.opacity = 1; 604 tooltipEl.style.position = "absolute"; 605 tooltipEl.style.top = position.top - 150 + window.pageYOffset + context.tooltip.caretY + "px"; 606 tooltipEl.style.font = context.tooltip.options.bodyFont.string; 607 tooltipEl.style.padding = context.tooltip.options.padding + "px " + context.tooltip.options.padding + "px"; 608 } 609 610 function updateHTMLValues(index) { 611 const yearData = result.yearlyData[index]; 612 const MAX_EQUITY = yearData.currentHomeValue; 613 614 function updateEquityBars(rentEquity, buyEquity) { 615 const rentPercentage = Math.min((rentEquity / MAX_EQUITY) * 100, 100); 616 const buyPercentage = Math.min((buyEquity / MAX_EQUITY) * 100, 100); 617 618 $(".rent-equity-bar").css("width", rentPercentage + "%"); 619 $(".buy-equity-bar").css("width", buyPercentage + "%"); 620 621 $(".rent-equity-value").text("$" + rentEquity.toLocaleString()); 622 $(".buy-equity-value").text("$" + (Math.round(buyEquity) / 1000).toFixed(0) + "K"); 623 } 624 625 const rentEquity = 0; 626 const buyEquity = yearData.currentHomeEquity; 627 updateEquityBars(rentEquity, buyEquity); 628 629 // add class to container based on if buy is cheaper than rent 630 if (yearData.buyTotalCost < yearData.rentTotalCost) { 631 $container.addClass("buy-cheaper"); 632 $container.removeClass("rent-cheaper"); 633 } else { 634 $container.removeClass("buy-cheaper"); 635 $container.addClass("rent-cheaper"); 636 } 637 638 // Update Equity 639 $container.find(".out-current-equity-buy").text("$" + yearData.currentHomeEquity.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 640 641 // Update Home Value and Mortgage Payment 642 $container.find(".out-current-home-value").text("$" + yearData.currentHomeValue.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 643 $container.find(".out-mortgage-payment").text(result.monthlyMortgage.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 644 645 // Update Monthly Cost 646 $container.find(".out-current-monthly-rent").text("$" + yearData.rentMonthlyCost.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 647 $container.find(".out-current-monthly-buy").text("$" + yearData.buyMonthlyCost.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 648 $container.find(".out-current-monthly-difference").text( 649 "$" + 650 Math.abs(yearData.rentMonthlyCost - yearData.buyMonthlyCost) 651 .toFixed(0) 652 .replace(/\B(?=(\d{3})+(?!\d))/g, ",") 653 ); 654 655 // Update Yearly Cost 656 $container.find(".out-current-yearly-rent").text("$" + yearData.rentYearlyCost.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 657 $container.find(".out-current-yearly-buy").text("$" + yearData.buyYearlyCost.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 658 $container.find(".out-current-yearly-difference").text( 659 "$" + 660 Math.abs(yearData.rentYearlyCost - yearData.buyYearlyCost) 661 .toFixed(0) 662 .replace(/\B(?=(\d{3})+(?!\d))/g, ",") 663 ); 664 665 // Update Total Cost 666 $container.find(".out-current-total-rent").text("$" + yearData.rentTotalCost.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 667 $container.find(".out-current-total-buy").text("$" + yearData.buyTotalCost.toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ",")); 668 $container.find(".out-current-total-difference").text( 669 "$" + 670 Math.abs(yearData.rentTotalCost - yearData.buyTotalCost) 671 .toFixed(0) 672 .replace(/\B(?=(\d{3})+(?!\d))/g, ",") 673 ); 674 675 // Update additional elements 676 $container.find(".out-current-year-text").text(index + 1 + (index + 1 === 1 ? " year" : " years")); 677 $container.find(".out-current-monthly-savings").text( 678 "$" + 679 Math.abs(yearData.totalMonthlySaved) 680 .toFixed(0) 681 .replace(/\B(?=(\d{3})+(?!\d))/g, ",") 682 ); 683 $container.find(".out-current-total-savings").text( 684 "$" + 685 Math.abs(yearData.totalSaved) 686 .toFixed(0) 687 .replace(/\B(?=(\d{3})+(?!\d))/g, ",") 688 ); 689 } 690 } 691}; 692 693function initRentVsBuyCalculator() { 694 var hasRentVsBuyCalculator = jQuery(".wwd-calculator-form[data-type='rent_vs_buy']").length > 0; 695 696 if (!hasRentVsBuyCalculator) { 697 return; 698 } 699 700 let lastUpdated = "amount"; 701 702 function updateDownPaymentAmount() { 703 var homeValue = parseFloat($("#home_value").val().replace(/,/g, "")) || 0; 704 var percentage = parseFloat($("#down_payment_percentage").val()) || 0; 705 var amount = homeValue * (percentage / 100); 706 $("#down_payment_amount").val(wwd_moneyFormat(amount, 0)); 707 } 708 709 function updateDownPaymentPercentage() { 710 var homeValue = parseFloat($("#home_value").val().replace(/,/g, "")) || 1; 711 var amount = parseFloat($("#down_payment_amount").val().replace(/,/g, "")) || 0; 712 var percentage = (amount / homeValue) * 100; 713 $("#down_payment_percentage").val(Math.min(percentage, 100).toFixed(2)); 714 $('input[data-type="range-input"][data-target="down_payment_percentage"]').val(Math.min(percentage, 80)); 715 // update progress 716 var $range = $('input[data-type="range-input"][data-target="down_payment_percentage"]'); 717 var val = parseFloat($("#down_payment_percentage").val()); 718 var min = parseFloat($range.attr("min")); 719 var max = parseFloat($range.attr("max")); 720 var progress = Math.min(((val - min) / (max - min)) * 100, 100); 721 $range.val(val); 722 $range[0].style.setProperty("--range-progress", `${progress}%`); 723 } 724 725 function updateHomeValue() { 726 var homeValue = parseFloat($("#home_value").val().replace(/,/g, "")) || 0; 727 const $range = $('input[data-type="range-input"][data-target="home_value"]'); 728 var min = parseFloat($range.attr("min")); 729 var max = parseFloat($range.attr("max")); 730 var progress = Math.min(((homeValue - min) / (max - min)) * 100, 100); 731 $range.val(homeValue); 732 $range[0].style.setProperty("--range-progress", `${progress}%`); 733 if (lastUpdated === "amount") { 734 updateDownPaymentPercentage(); 735 } else { 736 updateDownPaymentAmount(); 737 } 738 updatePropertyTaxAmount(); 739 updateHomeownersInsuranceAmount(); 740 } 741 742 let propertyTaxLastUpdated = "percentage"; 743 744 function updatePropertyTaxAmount() { 745 var homeValue = parseFloat($("#home_value").val().replace(/,/g, "")) || 0; 746 var taxPercentage = parseFloat($("#property_tax_percentage").val()) || 0; 747 var taxAmount = homeValue * (taxPercentage / 100); 748 $("#property_tax_amount").val(wwd_moneyFormat(taxAmount, 0)); 749 } 750 751 function updatePropertyTaxPercentage() { 752 var homeValue = parseFloat($("#home_value").val().replace(/,/g, "")) || 1; 753 var taxAmount = parseFloat($("#property_tax_amount").val().replace(/,/g, "")) || 0; 754 var percentage = (taxAmount / homeValue) * 100; 755 $("#property_tax_percentage").val(percentage.toFixed(2)); 756 $('input[data-type="range-input"][data-target="property_tax_percentage"]').val(percentage); 757 updatePropertyTaxRangeProgress(); 758 } 759 760 function updatePropertyTaxRangeProgress() { 761 var $range = $('input[data-type="range-input"][data-target="property_tax_percentage"]'); 762 var val = parseFloat($("#property_tax_percentage").val()); 763 var min = parseFloat($range.attr("min")); 764 var max = parseFloat($range.attr("max")); 765 var progress = Math.min(((val - min) / (max - min)) * 100, 100); 766 $range.val(val); 767 $range[0].style.setProperty("--range-progress", `${progress}%`); 768 } 769 770 function updateHomeownersInsuranceAmount() { 771 var homeValue = parseFloat($("#home_value").val().replace(/,/g, "")) || 0; 772 var insurancePercentage = parseFloat($("#homeowners_insurance_percentage").val()) || 0; 773 var insuranceAmount = homeValue * (insurancePercentage / 100); 774 $("#homeowners_insurance").val(wwd_moneyFormat(insuranceAmount, 0)); 775 } 776 777 function updateHomeownersInsurancePercentage() { 778 var homeValue = parseFloat($("#home_value").val().replace(/,/g, "")) || 1; 779 var insuranceAmount = parseFloat($("#homeowners_insurance").val().replace(/,/g, "")) || 0; 780 var percentage = (insuranceAmount / homeValue) * 100; 781 $("#homeowners_insurance_percentage").val(percentage.toFixed(2)); 782 $('input[data-type="range-input"][data-target="homeowners_insurance_percentage"]').val(percentage); 783 updateHomeownersInsuranceRangeProgress(); 784 } 785 786 function updateHomeownersInsuranceRangeProgress() { 787 var $range = $('input[data-type="range-input"][data-target="homeowners_insurance_percentage"]'); 788 var val = parseFloat($("#homeowners_insurance_percentage").val()); 789 var min = parseFloat($range.attr("min")); 790 var max = parseFloat($range.attr("max")); 791 var progress = Math.min(((val - min) / (max - min)) * 100, 100); 792 $range.val(val); 793 $range[0].style.setProperty("--range-progress", `${progress}%`); 794 } 795 796 jQuery(document).ready(function ($) { 797 $("#down_payment_percentage").on("input", function () { 798 lastUpdated = "percentage"; 799 updateDownPaymentAmount(); 800 var $range = $('input[data-type="range-input"][data-target="down_payment_percentage"]'); 801 var val = parseFloat($(this).val()); 802 var min = parseFloat($range.attr("min")); 803 var max = parseFloat($range.attr("max")); 804 805 // Calculate and update progress 806 var progress = Math.min(((val - min) / (max - min)) * 100, 100); 807 $range.val(val); 808 $range[0].style.setProperty("--range-progress", `${progress}%`); 809 }); 810 811 $("#down_payment_amount").on("input", function () { 812 lastUpdated = "amount"; 813 updateDownPaymentPercentage(); 814 }); 815 816 $("#home_value").on("input", updateHomeValue); 817 818 $("#property_tax_percentage").on("input", function () { 819 propertyTaxLastUpdated = "percentage"; 820 updatePropertyTaxAmount(); 821 updatePropertyTaxRangeProgress(); 822 }); 823 824 $("#property_tax_amount").on("input", function () { 825 propertyTaxLastUpdated = "amount"; 826 updatePropertyTaxPercentage(); 827 }); 828 829 $('input[data-type="range-input"][data-target="property_tax_percentage"]').on("input", function () { 830 propertyTaxLastUpdated = "percentage"; 831 $("#property_tax_percentage").val($(this).val()).trigger("input"); 832 }); 833 834 // Initialize property tax 835 updatePropertyTaxAmount(); 836 837 $("#homeowners_insurance_percentage").on("input", function () { 838 homeownersInsuranceLastUpdated = "percentage"; 839 updateHomeownersInsuranceAmount(); 840 updateHomeownersInsuranceRangeProgress(); 841 }); 842 843 $("#homeowners_insurance").on("input", function () { 844 homeownersInsuranceLastUpdated = "amount"; 845 updateHomeownersInsurancePercentage(); 846 }); 847 848 $('input[data-type="range-input"][data-target="homeowners_insurance_percentage"]').on("input", function () { 849 homeownersInsuranceLastUpdated = "percentage"; 850 $("#homeowners_insurance_percentage").val($(this).val()).trigger("input"); 851 }); 852 853 // Initialize homeowners insurance 854 updateHomeownersInsuranceAmount(); 855 856 $('input[data-type="range-input"][data-target="home_value"]').on("input", function () { 857 $("#home_value") 858 .val(wwd_moneyFormat($(this).val(), 0)) 859 .trigger("input"); 860 }); 861 862 $('input[data-type="range-input"][data-target="down_payment_percentage"]').on("input", function () { 863 $("#down_payment_percentage").val($(this).val()).trigger("input"); 864 }); 865 866 // Initialize 867 updateDownPaymentPercentage(); 868 updateHomeValue(); 869 870 $("#interest_rate").on("input", function () { 871 var $range = $('input[data-type="range-input"][data-target="interest_rate"]'); 872 var val = parseFloat($(this).val()); 873 var min = parseFloat($range.attr("min")); 874 var max = parseFloat($range.attr("max")); 875 876 // Calculate and update progress 877 var progress = Math.min(((val - min) / (max - min)) * 100, 100); 878 879 $range.val(val); 880 $range[0].style.setProperty("--range-progress", `${progress}%`); 881 }); 882 883 jQuery(".wwd-calculator-wrapper.rent_vs_buy-calculator").each(function () { 884 $ = jQuery; 885 const $wrapper = $(this); 886 const $interestRateInput = $wrapper.find('input[name="interest_rate"]'); 887 const $interestRateRange = $wrapper.find('input[data-type="range-input"][data-target="interest_rate"]'); 888 const $creditScoreInputs = $wrapper.find('input[name="credit_score"]'); 889 890 $creditScoreInputs.on("change", function () { 891 const selectedCreditScore = $(this).val(); 892 updateInterestRate(selectedCreditScore); 893 }); 894 895 function updateInterestRate(creditScore) { 896 if (WWD_Calculators && WWD_Calculators.credit_score_rates) { 897 const rateKey = `rent_vs_buy_credit_score_${creditScore}`; 898 const newRate = WWD_Calculators.credit_score_rates[rateKey]; 899 900 if (newRate) { 901 $interestRateRange.val(newRate); 902 $interestRateInput.val(newRate); 903 $interestRateInput.trigger("change"); 904 } 905 } 906 } 907 908 // Initialize interest rate based on default credit score 909 const defaultCreditScore = $creditScoreInputs.filter(":checked").val(); 910 911 console.log("defaultCreditScore", defaultCreditScore); 912 updateInterestRate(defaultCreditScore); 913 }); 914 }); 915 916 window.addEventListener("wwdTabSwitch", handleRentVsBuyCalculatorSubmit); 917 918 function handleRentVsBuyCalculatorSubmit() { 919 // hide the tooltip if it exists 920 var tooltip = document.getElementById("chartjs-tooltip"); 921 if (tooltip) { 922 tooltip.style.opacity = 0; 923 } 924 } 925} 926 927// Initialize the conventional mortgage calculator 928initRentVsBuyCalculator();
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.