Source: scripts/utils/chartHandler.js

const Chart = require('chart.js');

const {timestampToString} = require(__dirname + '/dateHandler.js');

/**
 * Class for creating charts.
 */
class ChartHandler {
	/**
	 * @param {View} view View object.
	 */
	constructor(view) {
		this.view = view;

		this.colors = [
			'rgba(0, 123, 255, 1)',
			'rgba(255, 159, 64, 1)',
			'rgba(40, 167, 69, 1)',
			'rgba(255, 193, 7, 1)',
			'rgba(220, 53, 69, 1)'
		];
		this.colorSeparator = 'rgba(153, 102, 255, 1)';
	}

	/**
	 * Creates a chart in a given canvas to visualize the input data.
	 * 
	 * @param {string} canvas The id of the canvas element which contains the chart.
	 * @param {array} data The input data.
	 */
	createChart(canvas, data) {
		switch (this.view.storage.readPreference('chartType')) {
			case 'bar':
				this.createMonthlySurplusChart(canvas, data);
				break;
			case 'doughnut':
				this.createCategoryChart(canvas, data);
				break;
			case 'line':
				this.createEarnSpendChart(canvas, data);
				break;
			case 'pie':
				this.createTransactionChart(canvas, data);
				break;
			default:
				this.createTransactionChart(canvas, data);
				break;
		}
	}

	/**
	 * Creates a chart containing the transactions (name, amount) as dataset.
	 * 
	 * @param {string} canvas The id of the canvas element which contains the chart.
	 * @param {array} data The input data.
	 */
	createTransactionChart(canvas, data) {
		while (data.length > this.colors.length) { // Ensure that enough colors are available.
			this.colors = this.colors.concat(this.colors);
		}

		// Ensure that two adjacent parts do not have the same color.
		if (this.colors[data.length - 1] === this.colors[0]) {
			this.colors[data.length - 1] = this.colorSeparator;
		}

		let backgroundColors = this.colors.map(c => c.replace('1)', '0.3)')); // Opacity

		new Chart($(canvas), {
			type: 'pie',
			data: {
				labels: data.map(d => d.name),
				datasets: [{
					data: data.map(d => parseFloat(d.amount).toFixed(2)),
					backgroundColor: backgroundColors,
					borderColor: this.colors,
					borderWidth: 1
				}]
			},
			options: {
				tooltips: {
					callbacks: {
						label: (ttI, cD) => {
							let sum = this.view.printNum(cD.datasets[0].data[ttI.index]);
							return `${cD.labels[ttI.index]}: ${sum}`;
						}
					}
				}
			}
		});
	}

	/**
	 * Creates a chart containing the categories (category name, combined amount of
	 * transactions belonging to that category) as dataset.
	 * 
	 * @param {string} canvas The id of the canvas element which contains the chart.
	 * @param {array} data The input data.
	 */
	createCategoryChart(canvas, data) {
		let dataObj = {};
		let cats = data.map(d => d.category).filter((val, ind, self) => self.indexOf(val) === ind);
		
		cats.forEach(c => dataObj[c] = 0);
		data.forEach(d => dataObj[d.category] += parseFloat(d.amount));

		let plotData = [[], []]; // Make sure to keep labels and data in the correct order
		for (const [key, val] of Object.entries(dataObj)) {
			plotData[0].push(key);
			plotData[1].push(val);
		}

		while (plotData[0].length > this.colors.length) { // Ensure that enough colors are available.
			this.colors = this.colors.concat(this.colors);
		}

		// Ensure that two adjacent parts do not have the same color.
		if (this.colors[plotData[0].length - 1] === this.colors[0]) {
			this.colors[plotData[0].length - 1] = this.colorSeparator;
		}

		let backgroundColors = this.colors.map(c => c.replace('1)', '0.3)')); // Opacity

		new Chart($(canvas), {
			type: 'doughnut',
			data: {
				labels: plotData[0],
				datasets: [{
					data: plotData[1].map(d => parseFloat(d).toFixed(2)),
					backgroundColor: backgroundColors,
					borderColor: this.colors,
					borderWidth: 1
				}]
			},
			options: {
				tooltips: {
					callbacks: {
						label: (ttI, cD) => {
							let sum = this.view.printNum(cD.datasets[0].data[ttI.index]);
							return `${cD.labels[ttI.index]}: ${sum}`;
						}
					}
				}
			}
		});
	}

	/**
	 * Creates a chart containing the monthly surpluses for all budgets combined.
	 * 
	 * @param {string} canvas The id of the canvas element which contains the chart.
	 * @param {array} data The input data.
	 */
	createMonthlySurplusChart(canvas, data) {
		let months = this.view.textData['monthNames'];
		let plotData = [];
		
		months.forEach((m, index) => plotData[index] = [0, 0]);
		data.forEach(d => {
			let i = d.type === 'earning' ? 0 : 1;
			plotData[parseInt(timestampToString(d.date).split('.')[1] - 1)][i] += parseFloat(d.amount);
		});

		while (months.length > this.colors.length) { // Ensure that enough colors are available.
			this.colors = this.colors.concat(this.colors);
		}

		// Ensure that two adjacent parts do not have the same color.
		if (this.colors[months.length - 1] === this.colors[0]) {
			this.colors[months.length - 1] = this.colorSeparator;
		}

		let backgroundColors = this.colors.map(c => c.replace('1)', '0.3)')); // Opacity

		new Chart($(canvas), {
			type: 'bar',
			data: {
				labels: months,
				datasets: [{
					data: plotData.map(d => parseFloat(d[0] - d[1]).toFixed(2)),
					backgroundColor: backgroundColors,
					borderColor: this.colors,
					borderWidth: 1
				}]
			},
			options: {
				tooltips: {
					callbacks: {
						label: (ttI, cD) => {
							let sum = this.view.printNum(cD.datasets[0].data[ttI.index]);
							return `${cD.labels[ttI.index]}: ${sum}`;
						}
					}
				},
				legend: false
			}
		});
	}

	/**
	 * Creates a chart containing the earnings and spendings per day.
	 * 
	 * @param {string} canvas The id of the canvas element which contains the chart.
	 * @param {array} data The input data.
	 */
	createEarnSpendChart(canvas, data) {
		let dataObj = {};

		data.map(d => timestampToString((new Date(d.date))))
			.filter((val, ind, self) => self.indexOf(val) === ind)
			.forEach(d => dataObj[d] = [0, 0]);
		
		data.forEach(d => dataObj[timestampToString(new Date(d.date))][d.type === 'earning' ? 0 : 1] += parseFloat(d.amount));

		let plotData = [[], []]; // Make sure to keep labels and data in the correct order
		
		let labels = Object.keys(dataObj).sort((a, b) => {
			return a.split('.').reverse().join('.') < b.split('.').reverse().join('.') ? -1 : 1;
		});
		
		labels.forEach(key => {
			plotData[0].push(dataObj[key][0]);
			plotData[1].push(dataObj[key][1]);
		});

		new Chart($(canvas), {
			type: 'line',
			data: {
				labels: labels,
				datasets: [{
					data: plotData[0].map(d => parseFloat(d).toFixed(2)),
					backgroundColor: 'rgba(40, 167, 69, 0.3)',
					borderColor: 'rgba(40, 167, 69, 1)',
					borderWidth: 1,
					label: this.view.textData['earnings']
				}, {
					data: plotData[1].map(d => parseFloat(d).toFixed(2)),
					backgroundColor: 'rgba(220, 53, 69, 0.3)',
					borderColor: 'rgba(220, 53, 69, 1)',
					borderWidth: 1,
					label: this.view.textData['spendings']
				}]
			},
			options: {
				tooltips: {
					callbacks: {
						label: (ttI, cD) => {
							let sum = this.view.printNum(cD.datasets[ttI.datasetIndex].data[ttI.index]);
							return `${cD.labels[ttI.index]}: ${sum}`;
						}
					}
				}
			}
		});
	}
}

module.exports = ChartHandler;