PageSourceSearch

https://strata.group/modules/module541/calculators/js/rent_vs_buy.js?ver=1749448048

js strata.group collected 2026-10-07 22:00:30 UTC 37,319 bytes, 928 lines download raw bytes

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.