PageSourceSearch

https://emortgages.com/wp-content/plugins/wwd-calculators/calculators/js/mortgage_payoff.js?ver=1766021637

js emortgages.com collected 2026-10-08 19:42:25 UTC 37,726 bytes, 1,037 lines download raw bytes

1window.calculatorHandlers = window.calculatorHandlers || {};
2window.calculatorOutputs = window.calculatorOutputs || {};
3window.calculatorHandlers.mortgage_payoff = 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 loanType = $form.find('input[name="loan_type"]:checked').val();
19  var currentMortgageBalance = parseFloat($form.find("#current_mortgage_balance").val().replace(/,/g, ""));
20  var yearsLeft = parseFloat($form.find("#years_left").val());
21  var monthsLeft = parseFloat($form.find("#months_left").val());
22  var loanTerm = loanType === "new" ? parseInt($form.find('input[name="loan_term"]:checked').val()) : yearsLeft + monthsLeft / 12;
23  var interestRate = parseFloat($form.find("#interest_rate").val().replace(/,/g, "")) / 100;
24  var extraPaymentAmount = parseFloat($form.find("#extra_payment_amount").val().replace(/,/g, "")) || 0;
25  var extraPaymentFrequency = $form.find('input[name="extra_payment_frequency"]:checked').val();
26
27  // Perform calculations
28  var loanAmount = currentMortgageBalance;
29
30  switch (loanType) {
31    case "new":
32      var numberOfPayments = loanTerm * 12;
33      break;
34    case "existing":
35      var numberOfPayments = yearsLeft * 12 + monthsLeft;
36      break;
37  }
38
39  // Calculate monthly principal and interest
40  var monthlyInterestRate = interestRate / 12;
41  var monthlyPandI = (loanAmount * monthlyInterestRate * Math.pow(1 + monthlyInterestRate, numberOfPayments)) / (Math.pow(1 + monthlyInterestRate, numberOfPayments) - 1);
42
43  // Calculate amortization schedule
44  var amortizationSchedule = [];
45  var remainingBalance = loanAmount;
46  var remainingBalanceWithExtra = loanAmount;
47  var totalPrincipal = 0;
48  var totalInterest = 0;
49  var totalPrincipalWithExtra = 0;
50  var totalInterestWithExtra = 0;
51  var loanTermMonthsWithExtra = 0;
52
53  for (var month = 1; month <= numberOfPayments; month++) {
54    // Regular payment calculations
55    var monthlyInterest = remainingBalance * monthlyInterestRate;
56    var principalPayment = monthlyPandI - monthlyInterest;
57    remainingBalance = Math.max(0, remainingBalance - principalPayment);
58    totalPrincipal += principalPayment;
59    totalInterest += monthlyInterest;
60
61    // Extra payment calculations
62    var extraPaymentThisMonth = 0;
63
64    switch (extraPaymentFrequency) {
65      case "monthly":
66        extraPaymentThisMonth = extraPaymentAmount;
67        break;
68      case "quarterly":
69        if (month % 3 === 0) extraPaymentThisMonth = extraPaymentAmount;
70        break;
71      case "semiannually":
72        if (month % 6 === 0) extraPaymentThisMonth = extraPaymentAmount;
73        break;
74      case "semi-annually":
75        if (month % 6 === 0) extraPaymentThisMonth = extraPaymentAmount;
76        break;
77      case "annually":
78        if (month % 12 === 0) extraPaymentThisMonth = extraPaymentAmount;
79        break;
80    }
81
82    var monthlyInterestWithExtra = remainingBalanceWithExtra * monthlyInterestRate;
83    var principalPaymentWithExtra = Math.min(remainingBalanceWithExtra, monthlyPandI - monthlyInterestWithExtra + extraPaymentThisMonth);
84    remainingBalanceWithExtra = Math.max(0, remainingBalanceWithExtra - principalPaymentWithExtra);
85    totalPrincipalWithExtra += principalPaymentWithExtra;
86    totalInterestWithExtra += monthlyInterestWithExtra;
87    var paymentWithExtra = principalPaymentWithExtra + monthlyInterestWithExtra;
88
89    amortizationSchedule.push({
90      month: month,
91      payment: monthlyPandI,
92      principalPayment: principalPayment,
93      interestPayment: monthlyInterest,
94      remainingBalance: remainingBalance,
95      totalPrincipalPaid: totalPrincipal,
96      totalInterestPaid: totalInterest,
97      extraPayment: extraPaymentThisMonth,
98      paymentWithExtra: paymentWithExtra,
99      principalPaymentWithExtra: principalPaymentWithExtra,
100      interestPaymentWithExtra: monthlyInterestWithExtra,
101      remainingBalanceWithExtra: remainingBalanceWithExtra,
102      totalPrincipalPaidWithExtra: totalPrincipalWithExtra,
103      totalInterestPaidWithExtra: totalInterestWithExtra,
104      totalSaved: totalInterest - totalInterestWithExtra,
105    });
106
107    if (remainingBalanceWithExtra <= 0 && loanTermMonthsWithExtra === 0) {
108      loanTermMonthsWithExtra = month;
109    }
110  }
111
112  // Prepare result object
113  var result = {
114    loanType: loanType,
115    loanAmount: loanAmount,
116    interestRate: interestRate,
117    loanTermMonths: numberOfPayments,
118    loanTermMonthsWithExtra: loanTermMonthsWithExtra,
119    monthlyPandI: monthlyPandI,
120    totalPrincipal: totalPrincipal,
121    totalPrincipalWithExtra: totalPrincipalWithExtra,
122    totalInterest: totalInterest,
123    totalInterestWithExtra: totalInterestWithExtra,
124    amortizationSchedule: amortizationSchedule,
125  };
126
127  // Update the UI with the results
128  updateUI(result, $container);
129
130  // Clear any previous errors
131  show_error("");
132
133  function updateUI(result, $container) {
134    // Add to window.wwd_results
135    window.wwd_results = window.wwd_results || {};
136
137    for (let key in result) {
138      if (!Array.isArray(result[key])) {
139        window.wwd_results[key] = result[key];
140      }
141    }
142    // time saved text
143    var timeSaved = result.loanTermMonths - result.loanTermMonthsWithExtra;
144    var years = Math.floor(timeSaved / 12);
145    var months = timeSaved % 12;
146    var timeSavedText = "";
147    var timeSavedFullText = "";
148    if (years > 0) {
149      timeSavedText += years + " Year" + (years > 1 ? "s" : "");
150      timeSavedFullText += years + " year" + (years > 1 ? "s" : "");
151    }
152    if (months > 0) {
153      timeSavedText += (years > 0 ? " & " : "") + months + " Month" + (months > 1 ? "s" : "");
154      timeSavedFullText += (years > 0 ? " and " : "") + months + " month" + (months > 1 ? "s" : "");
155    }
156    timeSavedFullText += " earlier";
157
158    var paidOffTimeframeText = "";
159    payoffYears = Math.floor(result.loanTermMonthsWithExtra / 12);
160    payoffMonths = result.loanTermMonthsWithExtra % 12;
161    if (payoffYears > 0) {
162      paidOffTimeframeText += payoffYears + " year" + (payoffYears > 1 ? "s" : "");
163    }
164    if (payoffMonths > 0) {
165      paidOffTimeframeText += (payoffYears > 0 ? " and " : "") + payoffMonths + " month" + (payoffMonths > 1 ? "s" : "");
166    }
167
168    // Summary tab
169    $container.find("#out-estimated-term-savings, .out-time-saved").text(timeSavedText);
170    $container.find("#out-time-saved-text").text(timeSavedFullText);
171
172    $container.find(".out-interest-savings").text("$" + wwd_moneyFormat(result.totalInterest - result.totalInterestWithExtra, 0));
173    $container.find(".out-extra-amount").text("$" + wwd_moneyFormat(extraPaymentAmount, 0));
174    $container.find(".out-total-term-with-extra").text(paidOffTimeframeText);
175
176    updateInterestOverTimeChart(result, $container.find("#out-interest-over-time-chart"));
177
178    // Full Report tab
179    // Update Full Report tab elements
180    $container.find(".out-interest-savings").text("$" + wwd_moneyFormat(result.totalInterest - result.totalInterestWithExtra, 0));
181    $container.find(".out-total-interest-original").text("$" + wwd_moneyFormat(result.totalInterest, 0));
182    $container.find(".out-total-interest-with-extra").text("$" + wwd_moneyFormat(result.totalInterestWithExtra, 0));
183    $container.find(".out-principal-interest-original").text("$" + wwd_moneyFormat(result.totalInterest + result.loanAmount, 0));
184    $container.find(".out-principal-interest-with-extra").text("$" + wwd_moneyFormat(result.totalInterestWithExtra + result.loanAmount, 0));
185
186    // Calculate and update payoff dates
187    const startDate = new Date();
188    const originalPayoffDate = new Date(startDate.getFullYear(), startDate.getMonth() + result.loanTermMonths, 1);
189    const newPayoffDate = new Date(startDate.getFullYear(), startDate.getMonth() + result.loanTermMonthsWithExtra, 1);
190
191    $container.find(".out-payoff-date-original").text(originalPayoffDate.toLocaleString("default", { month: "long", year: "numeric" }));
192    $container.find(".out-payoff-date-with-extra").text(newPayoffDate.toLocaleString("default", { month: "long", year: "numeric" }));
193
194    // Update Full Report tab elements for principal and interest payments
195    $container.find("#out-full-report-principal-payments").text("$" + wwd_moneyFormat(result.loanAmount, 0));
196    $container.find("#out-full-report-interest-payments").text("$" + wwd_moneyFormat(result.totalInterestWithExtra, 0));
197
198    // Update amortization chart
199    updateAmortizationChart(result, $container.find("#out-payoff-amortization-chart"));
200
201    // Update principal vs interest chart
202    updatePrincipalVsInterestChart(result, $container.find("#out-payoff-principal-vs-interest-chart"));
203
204    // Update amortization schedule
205    updateAmortizationSchedule(result, $container.find("#out-payoff-amortization-schedule-table"));
206
207    // Populate PDF input details
208    populatePDFInputDetails();
209  }
210
211  // Function to populate PDF input details
212  function populatePDFInputDetails() {
213    const pdfInputDetails = $container.find("#pdf-input-details-grid");
214
215    if (pdfInputDetails.length === 0) {
216      console.log("PDF Input Details: grid element not found");
217      return;
218    }
219
220    // Convert loan type to display format
221    const loanTypeMap = {
222      new: "New Loan",
223      existing: "Existing Loan",
224    };
225
226    // Convert extra payment frequency to display format
227    const frequencyMap = {
228      monthly: "Monthly",
229      annually: "Annually",
230      one_time: "One Time",
231    };
232
233    const inputDetails = [
234      { label: "Loan Type", value: loanTypeMap[loanType] || loanType },
235      { label: "Current Mortgage Balance", value: `$${wwd_moneyFormat(currentMortgageBalance, 0)}` },
236      { label: "Interest Rate", value: `${wwd_moneyFormat(interestRate * 100, 2)}%` },
237      { label: "Loan Term", value: `${loanTerm} years` },
238      { label: "Extra Payment Amount", value: `$${wwd_moneyFormat(extraPaymentAmount, 0)}` },
239      { label: "Extra Payment Frequency", value: frequencyMap[extraPaymentFrequency] || extraPaymentFrequency },
240    ];
241
242    // Add conditional fields based on loan type
243    if (loanType === "existing") {
244      inputDetails.splice(2, 0, { label: "Years Left", value: `${yearsLeft} years` });
245      inputDetails.splice(3, 0, { label: "Months Left", value: `${monthsLeft} months` });
246    }
247
248    let html = "";
249    inputDetails.forEach((detail) => {
250      html += `
251        <div>
252          <span class="block text-sm text-gray-600 mb-1">${detail.label}</span>
253          <span class="block text-lg font-bold">${detail.value}</span>
254        </div>
255      `;
256    });
257
258    pdfInputDetails.html(html);
259    console.log("PDF Input Details: populated successfully");
260  }
261
262  function updateInterestOverTimeChart(result, $container) {
263    const ctx = $container.get(0).getContext("2d");
264
265    // Generate data for 30 years
266    const months = result.amortizationSchedule.map((entry) => entry.month);
267    const interestPaid = result.amortizationSchedule.map((entry) => entry.totalInterestPaid);
268    const interestPaidWithExtra = result.amortizationSchedule.map((entry) => entry.totalInterestPaidWithExtra);
269    const totalSaved = result.amortizationSchedule.map((entry) => entry.totalSaved);
270    const paidOffMonth = result.loanTermMonthsWithExtra;
271
272    const maxValue = Math.max(...interestPaid, ...interestPaidWithExtra);
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    const breakEvenPlugin = {
280      id: "breakEvenLine",
281      afterDraw: (chart, args, options) => {
282        const {
283          ctx,
284          chartArea: { top, bottom, left, right, width, height },
285          scales: { x, y },
286        } = chart;
287        const breakEvenMonth = paidOffMonth;
288
289        ctx.save();
290
291        // Draw vertical line
292        const xPos = x.getPixelForValue(breakEvenMonth);
293
294        ctx.strokeStyle = "rgba(0, 0, 0, 0.1)";
295        ctx.setLineDash([5, 5]);
296        ctx.beginPath();
297        ctx.moveTo(xPos, top + 5);
298        ctx.lineTo(xPos, bottom);
299        ctx.stroke();
300
301        ctx.font = "14px var(--wwd-font-family)";
302        ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
303        ctx.textAlign = "center";
304        // ctx.fillText("EXTRA PAYMENTS:", xPos, top + 20);
305        // ctx.fillText("MORTGAGE PAID OFF", xPos, top + 40);
306
307        ctx.restore();
308      },
309    };
310
311    let chart = Chart.getChart(ctx);
312
313    if (chart) {
314      // Update existing chart
315      chart.data.labels = months;
316      chart.data.datasets[0].data = interestPaid;
317      chart.data.datasets[1].data = interestPaidWithExtra;
318      chart.data.datasets[2].data = totalSaved;
319      chart.options.plugins.breakEvenLine = { paidOffMonth };
320
321      // Recalculate y-axis scale
322      const maxValue = Math.max(...interestPaid, ...interestPaidWithExtra);
323      const tickSpacing = Math.pow(10, Math.floor(Math.log10(maxValue)) - 1);
324      const roundedMaxValue = Math.ceil(maxValue / tickSpacing) * tickSpacing;
325      const midValue = roundedMaxValue / 2;
326
327      // Update y-axis configuration
328      chart.options.scales.y.max = roundedMaxValue;
329      chart.options.scales.y.ticks.callback = function (value, index, values) {
330        if (value === 0 || value === roundedMaxValue || (value > midValue && !this.midPointTicked)) {
331          this.midPointTicked = value > midValue;
332          if (value >= 1000) {
333            return "$" + (value / 1000).toFixed(0) + "K";
334          } else {
335            return "$" + value.toLocaleString();
336          }
337        }
338        return "";
339      };
340
341      chart.options.scales.y.grid.color = function (context) {
342        if (context.tick.value === roundedMaxValue || (context.tick.value > midValue && !this.midPointLineDrawn)) {
343          this.midPointLineDrawn = context.tick.value > midValue;
344          return "rgba(0, 0, 0, 0.1)";
345        }
346        return "rgba(0, 0, 0, 0)";
347      };
348
349      // Reset the midPointTicked and midPointLineDrawn flags
350      chart.options.scales.y.ticks.midPointTicked = false;
351      chart.options.scales.y.grid.midPointLineDrawn = false;
352
353      chart.update();
354    } else {
355      chart = new Chart(ctx, {
356        type: "line",
357        data: {
358          labels: months,
359          datasets: [
360            {
361              label: "Interest Paid",
362              data: interestPaid,
363              borderColor: WWD_Calculators.colors.payoff_calc_regular_payment_color,
364              backgroundColor: WWD_Calculators.colors.payoff_calc_regular_payment_color,
365              pointRadius: 0,
366              borderWidth: 2,
367            },
368            {
369              label: "With Extra Payment",
370              data: interestPaidWithExtra,
371              borderColor: WWD_Calculators.colors.payoff_calc_extra_payment_color,
372              backgroundColor: WWD_Calculators.colors.payoff_calc_extra_payment_color,
373              pointRadius: 0,
374              borderWidth: 2,
375            },
376            {
377              label: "Total Saved",
378              data: totalSaved,
379              borderColor: WWD_Calculators.colors.payoff_calc_total_saved_color,
380              backgroundColor: WWD_Calculators.colors.payoff_calc_total_saved_color,
381              pointRadius: 0,
382              borderWidth: 2,
383            },
384          ],
385        },
386        options: {
387          responsive: true,
388          maintainAspectRatio: false,
389          scales: {
390            x: {
391              title: {
392                display: false,
393                text: "YEAR",
394              },
395              ticks: {
396                callback: function (value, index, values) {
397                  if (value % 2 === 0 && value !== 0) {
398                    const currentDate = new Date();
399                    const futureDate = new Date(currentDate.setMonth(currentDate.getMonth() + value));
400                    return futureDate.getFullYear();
401                  }
402                  return "";
403                },
404              },
405              grid: {
406                display: true,
407                drawBorder: true,
408                drawOnChartArea: true,
409                drawTicks: true,
410                color: function (context) {
411                  return "rgba(0, 0, 0, 0)";
412                },
413              },
414            },
415            y: {
416              title: {
417                display: false,
418                text: "INTEREST PAID",
419              },
420              ticks: {
421                callback: function (value, index, values) {
422                  if (value === 0 || value === roundedMaxValue || (value > midValue && !midPointTicked)) {
423                    midPointTicked = value > midValue;
424                    if (value >= 1000) {
425                      return "$" + (value / 1000).toFixed(0) + "K";
426                    } else {
427                      return "$" + value.toLocaleString();
428                    }
429                  }
430                  return "";
431                },
432              },
433              min: 0,
434              max: roundedMaxValue,
435              grid: {
436                drawBorder: false,
437                color: function (context) {
438                  if (context.tick.value === roundedMaxValue || (context.tick.value > midValue && !midPointLineDrawn)) {
439                    midPointLineDrawn = context.tick.value > midValue;
440                    return "rgba(0, 0, 0, 0.1)";
441                  }
442                  return "rgba(0, 0, 0, 0)";
443                },
444              },
445              border: {
446                display: false,
447              },
448            },
449          },
450          plugins: {
451            tooltip: {
452              enabled: false, // Disable default tooltip
453              external: customTooltip, // Use our custom tooltip
454              mode: "index",
455              intersect: false,
456            },
457            legend: {
458              display: true,
459              labels: {
460                color: "rgb(255, 99, 132, 0)",
461                boxWidth: 0,
462              },
463            },
464            breakEvenLine: {},
465          },
466          onHover: (event, chartElement) => {
467            if (chartElement.length) {
468              const datasetIndex = chartElement[0].datasetIndex;
469              const index = chartElement[0].index;
470              updateChartTooltip(index, datasetIndex);
471            }
472          },
473        },
474        plugins: [
475          breakEvenPlugin,
476          {
477            id: "tooltipLine",
478            beforeDraw: (chart) => {
479              if (chart.tooltip._active && chart.tooltip._active.length) {
480                const activePoint = chart.tooltip._active[0];
481                const { ctx } = chart;
482                const { x } = activePoint.element;
483                const topY = chart.scales.y.top;
484                const bottomY = chart.scales.y.bottom;
485
486                // Draw vertical line
487                ctx.save();
488                ctx.beginPath();
489                ctx.moveTo(x, topY);
490                ctx.lineTo(x, bottomY);
491                ctx.lineWidth = 1;
492                ctx.strokeStyle = "rgba(0, 0, 0, 0.5)";
493                ctx.stroke();
494                ctx.restore();
495              }
496            },
497          },
498          {
499            id: "customHover",
500            beforeEvent: (chart, args, pluginOptions) => {
501              const event = args.event;
502              if (event.type === "mousemove") {
503                const canvasPosition = Chart.helpers.getRelativePosition(event, chart);
504                const dataX = chart.scales.x.getValueForPixel(canvasPosition.x);
505                if (dataX >= 0 && dataX <= chart.data.labels.length - 1) {
506                  updateChartTooltip(Math.round(dataX));
507                }
508              }
509            },
510          },
511        ],
512      });
513    }
514
515    window.mortgagePayoffChart = chart;
516
517    $container.css("min-height", "300px");
518
519    function updateChartTooltip(index, datasetIndex = 0) {
520      const dataPoint = chart.data.datasets[datasetIndex].data[index];
521      const position = chart.getDatasetMeta(datasetIndex).data[index].getCenterPoint();
522
523      chart.tooltip.setActiveElements(
524        [
525          { datasetIndex: 0, index: index },
526          { datasetIndex: 1, index: index },
527        ],
528        {
529          x: position.x,
530          y: position.y,
531        }
532      );
533
534      chart.draw();
535    }
536
537    function customTooltip(context) {
538      // Make sure we have tooltip items
539      if (context.tooltip.dataPoints.length === 0) return;
540
541      const monthsFromNow = parseInt(context.tooltip.dataPoints[0].label) - 1;
542      const currentDate = new Date();
543      const futureDate = new Date(currentDate.setMonth(currentDate.getMonth() + monthsFromNow));
544
545      const monthYear = futureDate.toLocaleString("default", { month: "long", year: "numeric" });
546      const totalInterest = context.tooltip.dataPoints[0].raw;
547      const totalInterestWithExtra = context.tooltip.dataPoints[1].raw;
548      const totalSaved = context.tooltip.dataPoints[2]?.raw || 0;
549
550      // Create tooltip element
551      let tooltipEl = document.getElementById("chartjs-tooltip");
552
553      if (!tooltipEl) {
554        tooltipEl = document.createElement("div");
555        tooltipEl.id = "chartjs-tooltip";
556        tooltipEl.classList.add("chartjs-tooltip");
557        tooltipEl.innerHTML = "<div class='bg-white shadow-md p-2'></div>";
558        document.body.appendChild(tooltipEl);
559      }
560
561      // Hide tooltip if mouse is outside the chart
562      const chartArea = chart.chartArea;
563      const mouseX = context.tooltip.x;
564      const mouseY = context.tooltip.y;
565
566      if (mouseX < chartArea.left || mouseX > chartArea.right || mouseY < chartArea.top || mouseY > chartArea.bottom) {
567        tooltipEl.style.opacity = 0;
568        return;
569      }
570
571      // Set content
572      const tableBody = `<div class="text-sm text-gray-700 font-bold mb-1">${monthYear}</div>
573    <div class="flex items-center mb-0.5">
574      <span class="w-3 h-3 rounded-full bg-[var(--wwd-payoff-calc-regular-payment-color)] mr-2"></span>
575      <span class="text-sm mr-2">Regular Payments</span>
576      <span class="text-sm text-gray-600 font-bold">${new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(totalInterest)}</span>
577    </div>
578    <div class="flex items-center mb-0.5">
579      <span class="w-3 h-3 rounded-full bg-[var(--wwd-payoff-calc-extra-payment-color)] mr-2"></span>
580      <span class="text-sm mr-2">With Extra Payment</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(totalInterestWithExtra)}</span>
582    </div>
583    <div class="flex items-center">
584      <span class="w-3 h-3 rounded-full bg-[var(--wwd-payoff-calc-total-saved-color)] mr-2"></span>
585      <span class="text-sm mr-2">Total Saved</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(totalSaved)}</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
611  function updateAmortizationChart(result, $container) {
612    // Destroy existing chart if it exists
613    if (window.amortizationChart) {
614      window.amortizationChart.destroy();
615    }
616
617    const schedule = result.amortizationSchedule.slice(0, result.loanTermMonthsWithExtra);
618    const labels = schedule.map((entry) => entry.month);
619    const principalData = schedule.map((entry) => entry.totalPrincipalPaidWithExtra);
620    const interestData = schedule.map((entry) => entry.totalInterestPaidWithExtra);
621    const remainingBalanceData = schedule.map((entry) => entry.remainingBalanceWithExtra);
622
623    const principalColor = WWD_Calculators.colors.principal_color;
624    const interestColor = WWD_Calculators.colors.interest_color;
625    const remainingBalanceColor = WWD_Calculators.colors.remaining_balance_color;
626
627    const data = {
628      labels: labels,
629      datasets: [
630        {
631          label: "Principal",
632          data: principalData,
633          borderColor: principalColor,
634          backgroundColor: principalColor,
635          fill: false,
636          tension: 0.1,
637        },
638        {
639          label: "Interest",
640          data: interestData,
641          borderColor: interestColor,
642          backgroundColor: interestColor,
643          fill: false,
644          tension: 0.1,
645        },
646        {
647          label: "Remaining",
648          data: remainingBalanceData,
649          borderColor: remainingBalanceColor,
650          backgroundColor: remainingBalanceColor,
651          fill: false,
652          tension: 0.1,
653        },
654      ],
655    };
656
657    const config = {
658      type: "line",
659      data: data,
660      options: {
661        responsive: true,
662        plugins: {
663          legend: {
664            position: "top",
665          },
666          title: {
667            display: false,
668            text: "Amortization Schedule",
669          },
670          tooltip: {
671            mode: "index",
672            intersect: false,
673            callbacks: {
674              title: function (context) {
675                return "Month " + context[0].label;
676              },
677              label: function (context) {
678                let label = context.dataset.label || "";
679                if (label) {
680                  label += ": ";
681                }
682                if (context.parsed.y !== null) {
683                  label += new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(context.parsed.y);
684                }
685                return label;
686              },
687            },
688          },
689        },
690        scales: {
691          x: {
692            title: {
693              display: true,
694              text: "Months",
695            },
696            ticks: {
697              callback: function (value, index, values) {
698                return value % 100 === 0 ? value + " mo" : "";
699              },
700            },
701          },
702          y: {
703            title: {
704              display: true,
705              text: "Amount",
706            },
707            ticks: {
708              callback: function (value, index, values) {
709                return "$" + value.toLocaleString();
710              },
711            },
712          },
713        },
714        elements: {
715          line: {
716            borderWidth: 1.5, // Make lines thinner
717          },
718          point: {
719            radius: 0,
720            hitRadius: 5,
721            hoverRadius: 3,
722          },
723        },
724      },
725    };
726
727    window.amortizationChart = new Chart($container[0], config);
728  }
729
730  function updatePrincipalVsInterestChart(result, $container) {
731    // Destroy existing chart if it exists
732    if (window.principalVsInterestChart) {
733      window.principalVsInterestChart.destroy();
734    }
735
736    const schedule = result.amortizationSchedule.slice(0, result.loanTermMonthsWithExtra);
737    const labels = schedule.map((entry) => entry.month);
738    const principalData = schedule.map((entry) => entry.principalPaymentWithExtra);
739    const interestData = schedule.map((entry) => entry.interestPaymentWithExtra);
740
741    const principalColor = WWD_Calculators.colors.principal_color;
742    const interestColor = WWD_Calculators.colors.interest_color;
743
744    const data = {
745      labels: labels,
746      datasets: [
747        {
748          label: "Principal",
749          data: principalData,
750          backgroundColor: principalColor,
751          fill: true,
752        },
753        {
754          label: "Interest",
755          data: interestData,
756          backgroundColor: interestColor,
757          fill: true,
758        },
759      ],
760    };
761
762    const config = {
763      type: "line",
764      data: data,
765      options: {
766        responsive: true,
767        plugins: {
768          legend: {
769            position: "top",
770          },
771          title: {
772            display: false,
773            text: "Principal vs Interest",
774          },
775          tooltip: {
776            mode: "index",
777            intersect: false,
778            callbacks: {
779              title: function (context) {
780                return "Month " + context[0].label;
781              },
782              label: function (context) {
783                let label = context.dataset.label || "";
784                if (label) {
785                  label += ": ";
786                }
787                if (context.parsed.y !== null) {
788                  label += new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(context.parsed.y);
789                }
790                return label;
791              },
792            },
793          },
794        },
795        scales: {
796          x: {
797            stacked: true,
798            title: {
799              display: true,
800              text: "Months",
801            },
802            ticks: {
803              callback: function (value, index, values) {
804                return value % 100 === 0 ? value + "mo" : "";
805              },
806            },
807          },
808          y: {
809            stacked: true,
810            title: {
811              display: true,
812              text: "Amount",
813            },
814            ticks: {
815              callback: function (value, index, values) {
816                return "$" + value.toLocaleString();
817              },
818            },
819          },
820        },
821        elements: {
822          line: {
823            tension: 0.4,
824          },
825          point: {
826            radius: 0,
827          },
828        },
829      },
830    };
831
832    window.principalVsInterestChart = new Chart($container[0], config);
833  }
834
835  function updateAmortizationSchedule(result, $container) {
836    const schedule = result.amortizationSchedule;
837    const itemsPerPage = 12;
838    let currentPage = 1;
839
840    function renderTable(page) {
841      const startIndex = (page - 1) * itemsPerPage;
842      const endIndex = startIndex + itemsPerPage;
843      const pageItems = schedule.slice(startIndex, endIndex);
844
845      let tableHtml = `
846      <div class="overflow-x-auto w-full wwd-amortization-table">
847        <table class="w-full text-sm text-left">
848          <thead>
849            <tr class="bg-gray-100">
850              <th class="px-4 py-2 text-[var(--wwd-heading-color)]">Month</th>
851              <th class="px-4 py-2 text-[var(--wwd-heading-color)]">Interest</th>
852              <th class="px-4 py-2 text-[var(--wwd-heading-color)]">Principal</th>
853              <th class="px-4 py-2 text-[var(--wwd-heading-color)]">Extra</th>
854              <th class="px-4 py-2 text-[var(--wwd-heading-color)]">Remaining</th>
855            </tr>
856          </thead>
857          <tbody>
858    `;
859
860      pageItems.forEach((item, index) => {
861        const rowClass = index % 2 === 0 ? "bg-white" : "bg-gray-50";
862        tableHtml += `
863        <tr class="${rowClass}">
864          <td class="px-4 py-2 text-[var(--wwd-body-color)]">${item.month}</td>
865          <td class="px-4 py-2 text-[var(--wwd-body-color)]">$${wwd_moneyFormat(item.interestPaymentWithExtra, 0)}</td>
866          <td class="px-4 py-2 text-[var(--wwd-body-color)]">$${wwd_moneyFormat(item.principalPayment, 0)}</td>
867          <td class="px-4 py-2 text-[var(--wwd-body-color)]">$${wwd_moneyFormat(item.extraPayment, 0)}</td>
868          <td class="px-4 py-2 text-[var(--wwd-body-color)]">$${wwd_moneyFormat(item.remainingBalanceWithExtra, 0)}</td>
869        </tr>
870      `;
871      });
872
873      tableHtml += `
874          </tbody>
875        </table>
876      </div>
877    `;
878
879      $container.html(tableHtml);
880
881      // Add pagination
882      const totalPages = Math.ceil(schedule.length / itemsPerPage);
883      let paginationHtml = `
884        <div class="flex items-center justify-center mt-4 space-x-2">
885          <button class="pagination-prev px-3 py-1 rounded border ${page === 1 ? "opacity-50 cursor-not-allowed" : ""}">&lt;</button>
886      `;
887
888      for (let i = 1; i <= totalPages; i++) {
889        if (i === 1 || i === totalPages || (i >= page - 2 && i <= page + 2)) {
890          paginationHtml += `
891            <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>
892          `;
893        } else if (i === page - 3 || i === page + 3) {
894          paginationHtml += `<span class="px-1">...</span>`;
895        }
896      }
897
898      paginationHtml += `
899          <button class="pagination-next px-3 py-1 rounded border text-xs sm:text-sm ${page === totalPages ? "opacity-50 cursor-not-allowed" : ""}">&gt;</button>
900        </div>
901      `;
902
903      $container.append(paginationHtml);
904
905      // Add event listeners for pagination
906      $container.find(".pagination-prev").on("click", function () {
907        if (currentPage > 1) {
908          currentPage--;
909          renderTable(currentPage);
910        }
911      });
912
913      $container.find(".pagination-next").on("click", function () {
914        if (currentPage < totalPages) {
915          currentPage++;
916          renderTable(currentPage);
917        }
918      });
919
920      $container.find(".pagination-page").on("click", function () {
921        currentPage = parseInt(jQuery(this).text());
922        renderTable(currentPage);
923      });
924    }
925
926    renderTable(currentPage);
927  }
928};
929
930function initMortgagePayoffCalculator() {
931  var hasMortgagePayoffCalculator = jQuery(".wwd-calculator-form[data-type='mortgage_payoff']").length > 0;
932
933  if (!hasMortgagePayoffCalculator) {
934    return;
935  }
936
937  function updateFieldsVisibility() {
938    const $form = jQuery('.wwd-calculator-form[data-type="mortgage_payoff"]');
939    const $wrapper = $form.closest(".wwd-calculator-wrapper");
940
941    const loanType = $form.find('input[name="loan_type"]:checked').val();
942
943    // Update visibility based on loan type
944    $wrapper.find("[data-loan-type]").each(function () {
945      const $element = jQuery(this);
946      const elementLoanType = $element.attr("data-loan-type");
947
948      if (elementLoanType === loanType || elementLoanType === undefined) {
949        $element.show();
950      } else {
951        $element.hide();
952      }
953    });
954
955    // Trigger recalculation
956    $form.trigger("submit");
957  }
958
959  // Add event listeners
960  jQuery(document).ready(function ($) {
961    const $form = $('.wwd-calculator-form[data-type="mortgage_payoff"]');
962
963    $form.on("change", 'input[name="loan_type"]', function () {
964      updateFieldsVisibility();
965    });
966
967    // Initial update
968    updateFieldsVisibility();
969  });
970
971  jQuery(document).ready(function ($) {
972    $("#down_payment_percentage").on("input", function () {
973      lastUpdated = "percentage";
974      updateDownPaymentAmount();
975      var $range = $('input[data-type="range-input"][data-target="down_payment_percentage"]');
976      var val = parseFloat($(this).val());
977      var min = parseFloat($range.attr("min"));
978      var max = parseFloat($range.attr("max"));
979
980      // Calculate and update progress
981      var progress = Math.min(((val - min) / (max - min)) * 100, 100);
982      $range.val(val);
983      $range[0].style.setProperty("--range-progress", `${progress}%`);
984    });
985
986    $("#down_payment_amount").on("input", function () {
987      lastUpdated = "amount";
988      updateDownPaymentPercentage();
989    });
990
991    $('input[data-type="range-input"][data-target="home_value"]').on("input", function () {
992      $("#home_value")
993        .val(wwd_moneyFormat($(this).val(), 0))
994        .trigger("input");
995    });
996
997    $('input[data-type="range-input"][data-target="down_payment_percentage"]').on("input", function () {
998      $("#down_payment_percentage").val($(this).val()).trigger("input");
999    });
1000
1001    $("#interest_rate").on("input", function () {
1002      var $range = $('input[data-type="range-input"][data-target="interest_rate"]');
1003      var val = parseFloat($(this).val());
1004      var min = parseFloat($range.attr("min"));
1005      var max = parseFloat($range.attr("max"));
1006
1007      // Calculate and update progress
1008      var progress = Math.min(((val - min) / (max - min)) * 100, 100);
1009
1010      $range.val(val);
1011      $range[0].style.setProperty("--range-progress", `${progress}%`);
1012    });
1013
1014    // Trigger the change event on page load to set initial state
1015    $('input[name="loan_type"]:checked').trigger("change");
1016  });
1017
1018  window.addEventListener("wwdTabSwitch", handleMortgagePayoffCalculatorSubmit);
1019
1020  function handleMortgagePayoffCalculatorSubmit() {
1021    // destroy existing chart if it exists
1022    if (window.mortgagePayoffChart) {
1023      window.mortgagePayoffChart.destroy();
1024    }
1025    // hide the tooltip if it exists
1026    var tooltip = document.getElementById("chartjs-tooltip");
1027    if (tooltip) {
1028      tooltip.style.opacity = 0;
1029    }
1030    setTimeout(function () {
1031      window.wwd_submitForms("mortgage_payoff");
1032    }, 100);
1033  }
1034}
1035
1036// Initialize the conventional mortgage calculator
1037initMortgagePayoffCalculator();

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.