File "ch.LOAN.gpl.js"

Full Path: /home/adniftyx/public_html/wp-content/plugins/fc-loan-calculator/src/js/charting/ch.LOAN.gpl.js
File size: 11.47 KB
MIME-type: text/plain
Charset: utf-8

/**
 * @preserve Copyright 2024 Pine Grove Software, LLC
 * AccurateCalculators.com
 * pine-grove.com
 * ch.LOAN.gpl.js
 */


// localization conventions
import { Locales } from '../common/locales.gpl.js';

// string constants
import { LoanCalculatorStrings as loanStrings } from '../strings/strs.LOAN.gpl.js';

// global constants
import { Globals } from '../common/globals.gpl.js';

// utility functions
import { Utils } from '../common/utils.gpl.js';

const
	ROW_TYPE = 2,
	DATE_STR = 4,
	CF = 5, // cash flow
	INT = 8,
	PRIN = 9,
	BAL = 11,
	YEAR = 13,
	THOUSANDS = 1000;

// color constants for charts
const
	// Primary Color (Green) based off: --ac-theme-primary-color: #28a745;
	PRIMARY_COLOR = 'rgba(40, 167, 69, 1)',
	// Complementary Shade (Lighter Green)
	SECONDARY_COLOR = 'rgba(102, 201, 122, 1)';

export class LoanCharts {

	static annual_int = [];
	static annual_prin = [];
	static annual_pmt = [];
	static running_int = [];
	static running_prin = [];
	static running_pmt = [];
	static bal = [];
	static interest = 0;
	static prin = 0;
	static payments = 0;
	static category = [];
	static kStr = Locales.moneyConventions.ccy_r === '' ? 'k' : ' k';

	static async importChartJSLibrary () {
		if (this.chartModule) {
			// Chart.js is already loaded.
			return true;
		}

		try {
			// Dynamically import the script
			this.chartModule = await import('../chartjs/chart.esm.js'); // chart module
			// Store the Chart constructor
			this.Chart = this.chartModule.default; // chart instance
			// Chart.js has been successfully imported.
			return true;
		} catch (error) {
			// eslint-disable-next-line no-console
			console.error('Failed to import Chart.js:', error);
			return false;
		}
	}


	// reset data arrays
	static clear () {
		this.L = 0;
		this.annual_int = [];
		this.annual_prin = [];
		this.annual_pmt = [];
		this.running_int = [];
		this.running_prin = [];
		this.running_pmt = [];
		this.bal = [];
		this.category = [];
	}


	static initAnnualTotalChart () {

		// stacked bar showing annual totals, line show annual payments
		let annualBarChartData = {
			labels: this.category, // year labels for x-axis
			datasets: [{
				type: 'line',
				// 'Payment',
				label: loanStrings.strs.s221,
				borderWidth: 1, // width in pixels
				borderColor: 'rgba(51,51,51,0.5)', // line color
				pointBackgroundColor: 'rgba(0,0,0,0.75)',
				data: this.annual_pmt // annual total payment
			}, {
				type: 'bar',
				// 'Principal',
				label: loanStrings.strs.s223,
				backgroundColor: PRIMARY_COLOR,
				data: this.annual_prin // annual principal totals
			}, {
				type: 'bar',
				// 'Interest',
				label: loanStrings.strs.s222,
				backgroundColor: SECONDARY_COLOR,
				data: this.annual_int // annual interest totals
			}]
		};

		// get a canvas to draw on
		var ctx = document.getElementById('canvas1').getContext('2d');

		// allocate and initialize a chart
		this.annualTotals = new this.Chart(ctx, {
			data: annualBarChartData,
			options: {
				plugins: {
					title: {
						display: true,
						text: this.chart0Title
					}
				},
				tooltips: {
					mode: 'label',
					callbacks: {
						label: function (tooltipItems) {
							return Utils.formatNumericValueWithSym(tooltipItems.yLabel, Locales.moneyConventions, 0);
						}
					}
				},
				responsive: true,
				scales: {
					x: {
						stacked: true
					},
					y: {
						stacked: true,
						beginAtZero: true,
						ticks: {
							callback: function (label) {
								return Utils.formatNumericValueWithSym(label / THOUSANDS, Locales.moneyConventions, 0) + LoanCharts.kStr;
							}
						}
					}
				}
			}
		});

	} // initAnnualTotalChart


	static initAccumulatedTotalChart () {
		// stacked bar showing annual totals, lines show annual payments and balance
		let runningBarChartData = {
			labels: this.category, // years along the x-axis
			datasets: [{
				type: 'line',
				// 'Payment',
				label: loanStrings.strs.s221,
				yAxisID: 'y-axis-0',
				borderWidth: 1, // width in pixels
				borderColor: 'rgba(51,51,51,0.5)', // line color
				pointBackgroundColor: 'rgba(0,0,0,0.75)',
				data: this.running_pmt
			}, {
				type: 'line',
				yAxisID: 'y-axis-1',
				// 'Balance',
				label: loanStrings.strs.s224,
				borderWidth: 1, // width in pixels
				borderColor: 'rgba(151,187,205,0.5)', // line color
				pointBackgroundColor: 'rgba(0,0,0,0.75)',
				data: this.bal
			}, {
				yAxisID: 'y-axis-0',
				// 'Principal',
				label: loanStrings.strs.s223,
				backgroundColor: PRIMARY_COLOR,
				data: this.running_prin
			}, {
				yAxisID: 'y-axis-0',
				// 'Interest',
				label: loanStrings.strs.s222,
				backgroundColor: SECONDARY_COLOR,
				data: this.running_int
			}]
		};

		// get a canvas to draw on
		var ctx = document.getElementById('canvas2').getContext('2d');

		// allocate and initialize a chart
		this.runningTotals = new this.Chart(ctx, {
			type: 'bar',
			data: runningBarChartData,
			options: {
				plugins: {
					title: {
						display: true,
						text: this.chart1Title
					}
				},
				tooltips: {
					mode: 'label',
					callbacks: {
						label: function (tooltipItems) {
							return Utils.formatNumericValueWithSym(tooltipItems.yLabel, Locales.moneyConventions, 0);
						}
					}
				},
				responsive: true,
				scales: {
					x: {
						stacked: true
					},
					'y-axis-0': {
						stacked: true,
						beginAtZero: true,
						position: 'left',
						ticks: {
							beginAtZero: true,
							suggestedMax: this.running_pmt[this.running_pmt.length - 1],
							callback: function (label) {
								return Utils.formatNumericValueWithSym(label / THOUSANDS, Locales.moneyConventions, 0) + LoanCharts.kStr;
							}
						}
					},
					'y-axis-1': {
						position: 'right',
						ticks: {
							callback: function (label) {
								return Utils.formatNumericValueWithSym(label / THOUSANDS, Locales.moneyConventions, 0) + LoanCharts.kStr;
							}
						}
					}
				}
			}
		});

	} // initAccumulatedTotalChart


	/**
	 * Tooltip:
	 * 1. Calculate and Round Percentage: exactPercentage is the raw, unrounded percentage. roundedPercentage is rounded to the nearest tenth.
	 * 2. Track Cumulative Percentage: cumulativePercentage adds up the roundedPercentage values for each tooltip item.
	 * 3. Check for Last Element: If this is the last dataset item, calculate the discrepancy between cumulativePercentage and 100.
	 * 4. Apply Adjustment: Add the discrepancy to the last segment’s percentage to ensure the total equals exactly 100%.
	 */
	static initPIPieChart () {
		let pieDataArray = [this.prin, this.interest];
		// 'Total Principal', 'Total Interest'
		let pieLabelArray = [loanStrings.strs.s226, loanStrings.strs.s227];
		let pieColorArray = [PRIMARY_COLOR, SECONDARY_COLOR];
		let cumulativePercentage = 0; // Add this outside the label function if needed across multiple calls

		var config = {
			type: 'pie',
			data: {
				datasets: [{
					data: pieDataArray,
					backgroundColor: pieColorArray
				}],
				labels: pieLabelArray
			},
			options: {
				responsive: true,
				plugins: {
					title: {
						display: true,
						text: this.chart2Title
					},
					tooltip: {
						callbacks: {
							label: function (context) {
								let label1 = context.dataset.label || '';
								let label2 = '';

								if (context.parsed.y !== null) {
									label1 += ' ' + Utils.formatNumericValueWithSym(context.parsed, Locales.moneyConventions, 0);

									if (LoanCharts.payments > 0) {
										// Calculate exact percentage for this slice
										let exactPercentage = (context.parsed / LoanCharts.payments * 100);
										let roundedPercentage = Math.round(exactPercentage * 10) / 10; // Round to nearest tenth

										// Track cumulative percentage
										cumulativePercentage += roundedPercentage;

										// Check if this is the last element by comparing with total dataset count
										if (context.datasetIndex === context.chart.data.datasets[0].data.length - 1) {
											// Make final adjustment if cumulative percentage is off
											let discrepancy = 100 - cumulativePercentage;

											roundedPercentage += discrepancy; // Adjust last segment
										}

										label2 = ' ' + Utils.formatNumericValueWithSym(roundedPercentage, Locales.rateConventions, 1);
									}
								}
								return [label1, label2];
							}
						}
					}
				}
			}
		};

		// get a canvas to draw on
		var ctx = document.getElementById('canvas3').getContext('2d');

		// allocate and initialize a chart
		this.totalsPie = new this.Chart(ctx, config);

	} // initPIPieChart


	// initialize data structures needed for conventional loan charts
	static createLoanCharts (schedule) {
		const ELEVEN = 11;
		const THREE = 3;
		var L, i, j, transaction, bal;

		// init data structures
		j = 0;
		L = schedule.length - 1;
		// [KT] 11/13/2016 - check array is populated
		if (schedule.length > 0) {
			this.years = schedule[L][YEAR] - schedule[0][YEAR] + 1;

			for (i = 0; i <= L; i += 1) {
				transaction = schedule[i];
				if (transaction[ROW_TYPE] === Globals.ROW_TYPES.ANNUAL_TOTALS) {
					// if annual payments and year has no payment (when loan is originated), do not include.
					this.annual_pmt.push(Math.round(transaction[CF]));
					this.annual_int.push(Math.round(transaction[INT]));
					this.annual_prin.push(Math.round(transaction[PRIN]));
					// if less than or equal to 11 years or divisible by 3, show calendar year label
					if ((this.years <= ELEVEN) || (j % THREE === 0) || j === 0) {
						this.category.push(transaction[YEAR]);
					} else {
						this.category.push('');
					}
					j += 1;
				} else if (transaction[ROW_TYPE] === Globals.ROW_TYPES.RUNNING_TOTALS) {
					this.running_pmt.push(Math.round(transaction[CF]));
					this.running_int.push(Math.round(transaction[INT]));
					this.running_prin.push(Math.round(transaction[PRIN]));
					this.bal.push(bal);
				} else {
					// balance from a normal transaction
					bal = Math.round(transaction[BAL]);
				}
			} // for

			// init data for pie chart
			transaction = schedule[L];
			this.interest = transaction[INT];
			this.prin = transaction[PRIN];
			this.payments = transaction[CF];

			transaction = schedule[0];
			this.strDate = transaction[DATE_STR];  // EQ.dateMath.customDateStrFromDate(transaction.dTransDate);
			// last transaction date
			transaction = schedule[L - 2];
			this.strDate2 = transaction[DATE_STR];

			// Annual Principal and Interest Totals
			this.chart0Title = loanStrings.strs.s229;
			// Accumulated Principal and Interest with Remaining Balance
			this.chart1Title = loanStrings.strs.s230;
			// Total Principal and Interest
			this.chart2Title = loanStrings.strs.s231;
		} // L > 0

		this.initAnnualTotalChart();
		this.initAccumulatedTotalChart();
		this.initPIPieChart();
	}

	static async showCharts (schedule) {
		const isLibraryLoaded = await this.importChartJSLibrary();

		if (isLibraryLoaded) {
			this.createLoanCharts(schedule);
		} else {
			// eslint-disable-next-line no-console
			console.error('Cannot create charts without Chart.js library.');
		}
	}

	static destroy () {
		this.annualTotals.destroy();
		this.annualTotals = null;
		this.runningTotals.destroy();
		this.runningTotals = null;
		this.totalsPie.destroy();
		this.totalsPie = null;
		this.clear();
	}

};  // LoanCharts