dataTransform.js

import * as d3 from "d3";

/**
 * Data Transform
 *
 * @module
 * @returns {Array}
 */
export default function dataTransform(data) {

	const SINGLE_SERIES = 1;
	const MULTI_SERIES = 2;
	const coordinateKeys = ['x', 'y', 'z'];

	/**
	 * Data Type
	 *
	 * @type {Number}
	 */
	const dataType = data.key !== undefined ? SINGLE_SERIES : MULTI_SERIES;

	/**
	 * Row Key
	 *
	 * @returns {Array}
	 */
	const rowKey = function() {
		if (dataType === SINGLE_SERIES) {
			return d3.values(data)[0];
		}
	}();

	/**
	 * Row Total
	 *
	 * @returns {Array}
	 */
	const rowTotal = function() {
		if (dataType === SINGLE_SERIES) {
			return d3.sum(data.values, (d) => d.value);
		}
	}();

	/**
	 * Row Keys
	 *
	 * @returns {Array}
	 */
	const rowKeys = function() {
		if (dataType === MULTI_SERIES) {
			return data.map((d) => d.key);
		}
	}();

	/**
	 * Row Totals
	 *
	 * @returns {Array}
	 */
	const rowTotals = function() {
		if (dataType === MULTI_SERIES) {
			const ret = {};
			d3.map(data).values().forEach((d) => {
				const rowKey = d.key;
				d.values.forEach((d) => {
					ret[rowKey] = (typeof ret[rowKey] === "undefined") ? 0 : ret[rowKey];
					ret[rowKey] += d.value;
				});
			});
			return ret;
		}
	}();

	/**
	 * Row Totals Max
	 *
	 * @returns {number}
	 */
	const rowTotalsMax = function() {
		if (dataType === MULTI_SERIES) {
			return d3.max(d3.values(rowTotals));
		}
	}();

	/**
	 * Row Value Keys
	 *
	 * @returns {Array}
	 */
	const rowValuesKeys = function() {
		if (dataType === SINGLE_SERIES) {
			return Object.keys(data.values[0]);
		} else {
			return Object.keys(data[0].values[0]);
		}
	}();

	/**
	 * Union Two Arrays
	 *
	 * @private
	 * @param {Array} array1 - First Array.
	 * @param {Array} array2 - First Array.
	 * @returns {Array}
	 */
	const union = function(array1, array2) {
		const ret = [];
		const arr = array1.concat(array2);
		let len = arr.length;
		const assoc = {};

		while (len--) {
			const item = arr[len];

			if (!assoc[item]) {
				ret.unshift(item);
				assoc[item] = true;
			}
		}

		return ret;
	};

	/**
	 * Column Keys
	 *
	 * @returns {Array}
	 */
	const columnKeys = function() {
		if (dataType === SINGLE_SERIES) {
			return d3.values(data.values).map((d) => d.key);
		}

		let ret = [];
		d3.map(data).values().forEach((d) => {
			const tmp = [];
			d.values.forEach((d, i) => {
				tmp[i] = d.key;
			});
			ret = union(tmp, ret);
		});

		return ret;
	}();

	/**
	 * Column Totals
	 *
	 * @returns {Array}
	 */
	const columnTotals = function() {
		if (dataType !== MULTI_SERIES) {
			return;
		}

		let ret = {};
		d3.map(data).values().forEach((d) => {
			d.values.forEach((d) => {
				const columnName = d.key;
				ret[columnName] = (typeof(ret[columnName]) === "undefined" ? 0 : ret[columnName]);
				ret[columnName] += d.value;
			});
		});

		return ret;
	}();

	/**
	 * Column Totals Max
	 *
	 * @returns {Array}
	 */
	const columnTotalsMax = function() {
		if (dataType === MULTI_SERIES) {
			return d3.max(d3.values(columnTotals));
		}
	}();

	/**
	 * Value Min
	 *
	 * @returns {number}
	 */
	const valueMin = function() {
		if (dataType === SINGLE_SERIES) {
			return d3.min(data.values, (d) => +d.value);
		}

		let ret;
		d3.map(data).values().forEach((d) => {
			d.values.forEach((d) => {
				ret = (typeof(ret) === "undefined" ? d.value : d3.min([ret, +d.value]));
			});
		});

		return +ret;
	}();

	/**
	 * Value Max
	 *
	 * @returns {number}
	 */
	const valueMax = function() {
		let ret;

		if (dataType === SINGLE_SERIES) {
			ret = d3.max(data.values, (d) => +d.value);
		} else {
			d3.map(data).values().forEach((d) => {
				d.values.forEach((d) => {
					ret = (typeof ret !== "undefined" ? d3.max([ret, +d.value]) : +d.value);
				});
			});
		}

		return ret;
	}();

	/**
	 * Value Extent
	 *
	 * @returns {Array}
	 */
	const valueExtent = function() {
		return [valueMin, valueMax];
	}();

	/**
	 * Coordinates Min
	 *
	 * @returns {Array}
	 */
	const coordinatesMin = function() {
		let ret = {};

		if (dataType === SINGLE_SERIES) {
			coordinateKeys.forEach((key) => {
				ret[key] = d3.min(data.values, (d) => +d[key]);
			});
			return ret;

		} else {
			d3.map(data).values().forEach((d) => {
				d.values.forEach((d) => {
					coordinateKeys.forEach((key) => {
						ret[key] = (key in ret ? d3.min([ret[key], +d[key]]) : d[key]);
					});
				});
			});
		}

		return ret;
	}();

	/**
	 * Coordinates Max
	 *
	 * @returns {Array}
	 */
	const coordinatesMax = function() {
		let ret = {};

		if (dataType === SINGLE_SERIES) {
			coordinateKeys.forEach((key) => {
				ret[key] = d3.max(data.values, (d) => +d[key]);
			});
			return ret;

		} else {
			d3.map(data).values().forEach((d) => {
				d.values.forEach((d) => {
					coordinateKeys.forEach((key) => {
						ret[key] = (key in ret ? d3.max([ret[key], +d[key]]) : d[key]);
					});
				});
			});
		}

		return ret;
	}();

	/**
	 * Coordinates Extent
	 *
	 * @returns {Array}
	 */
	const coordinatesExtent = function() {
		let ret = {};
		coordinateKeys.forEach(function(key) {
			ret[key] = [coordinatesMin[key], coordinatesMax[key]]
		});

		return ret;
	}();

	/**
	 * How Many Decimal Places?
	 *
	 * @private
	 * @param {number} num - Float.
	 * @returns {number}
	 */
	const decimalPlaces = function(num) {
		const match = ("" + num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);
		if (!match) {
			return 0;
		}

		return Math.max(
			0,
			// Number of digits right of decimal point.
			(match[1] ? match[1].length : 0)
			// Adjust for scientific notation.
			-
			(match[2] ? +match[2] : 0)
		);
	};

	/**
	 * Max Decimal Place
	 *
	 * @returns {number}
	 */
	const maxDecimalPlace = function() {
		let ret = 0;
		if (dataType === MULTI_SERIES) {
			d3.map(data).values().forEach((d) => {
				d.values.forEach((d) => {
					ret = d3.max([ret, decimalPlaces(d.value)])
				});
			});
		}

		// toFixed must be between 0 and 20
		return ret > 20 ? 20 : ret;
	}();


	/**
	 * Thresholds
	 *
	 * @returns {Array}
	 */
	const thresholds = function() {
		const distance = valueMax - valueMin;
		const bands = [0.15, 0.40, 0.55, 0.90];

		return bands.map((v) => Number((valueMin + (v * distance)).toFixed(maxDecimalPlace)));
	}();


	/**
	 * Summary
	 *
	 * @returns {Array}
	 */
	const summary = function() {
		return {
			dataType: dataType,
			rowKey: rowKey,
			rowTotal: rowTotal,
			rowKeys: rowKeys,
			rowTotals: rowTotals,
			rowTotalsMax: rowTotalsMax,
			rowValuesKeys: rowValuesKeys,
			columnKeys: columnKeys,
			columnTotals: columnTotals,
			columnTotalsMax: columnTotalsMax,
			valueMin: valueMin,
			valueMax: valueMax,
			valueExtent: valueExtent,
			coordinatesMin: coordinatesMin,
			coordinatesMax: coordinatesMax,
			coordinatesExtent: coordinatesExtent,
			maxDecimalPlace: maxDecimalPlace,
			thresholds: thresholds
		}
	};

	/**
	 * Rotate Data
	 *
	 * @returns {Array}
	 */
	const rotate = function() {
		const columnKeys = data.map((d) => d.key);
		const rowKeys = data[0].values.map((d) => d.key);

		const rotated = rowKeys.map((rowKey, rowIndex) => {
			const values = columnKeys.map((columnKey, columnIndex) => {
				// Copy the values from the original object
				const values = Object.assign({}, data[columnIndex].values[rowIndex]);
				// Swap the key over
				values.key = columnKey;

				return values;
			});

			return {
				key: rowKey,
				values: values
			};
		});

		return rotated;
	};

	return {
		summary: summary,
		rotate: rotate
	};

}