Source: Highchart.js

"use strict";

// Import external names locally
var Shared,
	Collection,
	CollectionInit,
	Overload;

Shared = require('./Shared');
Overload = require('./Overload');

/**
 * The constructor.
 *
 * @constructor
 */
var Highchart = function (collection, options) {
	this.init.apply(this, arguments);
};

Highchart.prototype.init = function (collection, options) {
	this._options = options;
	this._selector = window.jQuery(this._options.selector);

	if (!this._selector[0]) {
		throw(this.classIdentifier() + ' "' + collection.name() + '": Chart target element does not exist via selector: ' + this._options.selector);
	}

	this._listeners = {};
	this._collection = collection;

	// Setup the chart
	this._options.series = [];

	// Disable attribution on highcharts
	options.chartOptions = options.chartOptions || {};
	options.chartOptions.credits = false;

	// Set the data for the chart
	var data,
		seriesObj,
		chartData;

	switch (this._options.type) {
		case 'pie':
			// Create chart from data
			this._selector.highcharts(this._options.chartOptions);
			this._chart = this._selector.highcharts();

			// Generate graph data from collection data
			data = this._collection.find();

			seriesObj = {
				allowPointSelect: true,
				cursor: 'pointer',
				dataLabels: {
					enabled: true,
					format: '<b>{point.name}</b>: {y} ({point.percentage:.0f}%)',
					style: {
						color: (window.Highcharts.theme && window.Highcharts.theme.contrastTextColor) || 'black'
					}
				}
			};

			chartData = this.pieDataFromCollectionData(data, this._options.keyField, this._options.valField);

			window.jQuery.extend(seriesObj, this._options.seriesOptions);

			window.jQuery.extend(seriesObj, {
				name: this._options.seriesName,
				data: chartData
			});

			this._chart.addSeries(seriesObj, true, true);
			break;

		case 'line':
		case 'area':
		case 'column':
		case 'bar':
			// Generate graph data from collection data
			chartData = this.seriesDataFromCollectionData(
				this._options.seriesField,
				this._options.keyField,
				this._options.valField,
				this._options.orderBy
			);

			this._options.chartOptions.xAxis = chartData.xAxis;
			this._options.chartOptions.series = chartData.series;

			this._selector.highcharts(this._options.chartOptions);
			this._chart = this._selector.highcharts();
			break;

		default:
			throw(this.classIdentifier() + ' "' + collection.name() + '": Chart type specified is not currently supported by ForerunnerDB: ' + this._options.type);
	}

	// Hook the collection events to auto-update the chart
	this._hookEvents();
};

Shared.addModule('Highchart', Highchart);

Collection = Shared.modules.Collection;
CollectionInit = Collection.prototype.init;

Shared.mixin(Highchart.prototype, 'Mixin.Common');
Shared.mixin(Highchart.prototype, 'Mixin.Events');

/**
 * Gets / sets the current state.
 * @param {String=} val The name of the state to set.
 * @returns {*}
 */
Shared.synthesize(Highchart.prototype, 'state');

/**
 * Generate pie-chart series data from the given collection data array.
 * @param data
 * @param keyField
 * @param valField
 * @returns {Array}
 */
Highchart.prototype.pieDataFromCollectionData = function (data, keyField, valField) {
	var graphData = [],
		i;

	for (i = 0; i < data.length; i++) {
		graphData.push([data[i][keyField], data[i][valField]]);
	}

	return graphData;
};

/**
 * Generate line-chart series data from the given collection data array.
 * @param seriesField
 * @param keyField
 * @param valField
 * @param orderBy
 */
Highchart.prototype.seriesDataFromCollectionData = function (seriesField, keyField, valField, orderBy) {
	var data = this._collection.distinct(seriesField),
		seriesData = [],
		xAxis = {
			categories: []
		},
		seriesName,
		query,
		dataSearch,
		seriesValues,
		i, k;

	// What we WANT to output:
	/*series: [{
		name: 'Responses',
		data: [7.0, 6.9, 9.5, 14.5, 18.2, 21.5, 25.2, 26.5, 23.3, 18.3, 13.9, 9.6]
	}]*/

	// Loop keys
	for (i = 0; i < data.length; i++) {
		seriesName = data[i];
		query = {};
		query[seriesField] = seriesName;

		seriesValues = [];
		dataSearch = this._collection.find(query, {
			orderBy: orderBy
		});

		// Loop the keySearch data and grab the value for each item
		for (k = 0; k < dataSearch.length; k++) {
			xAxis.categories.push(dataSearch[k][keyField]);
			seriesValues.push(dataSearch[k][valField]);
		}

		seriesData.push({
			name: seriesName,
			data: seriesValues
		});
	}

	return {
		xAxis: xAxis,
		series: seriesData
	};
};

/**
 * Hook the events the chart needs to know about from the internal collection.
 * @private
 */
Highchart.prototype._hookEvents = function () {
	var self = this;

	self._collection.on('change', function () { self._changeListener.apply(self, arguments); });

	// If the collection is dropped, clean up after ourselves
	self._collection.on('drop', function () { self.drop.apply(self, arguments); });
};

/**
 * Handles changes to the collection data that the chart is reading from and then
 * updates the data in the chart display.
 * @private
 */
Highchart.prototype._changeListener = function () {
	var self = this;

	// Update the series data on the chart
	if(typeof self._collection !== 'undefined' && self._chart) {
		var data = self._collection.find(),
			i;

		switch (self._options.type) {
			case 'pie':
				self._chart.series[0].setData(
					self.pieDataFromCollectionData(
						data,
						self._options.keyField,
						self._options.valField
					),
					true,
					true
				);
				break;

			case 'bar':
			case 'line':
			case 'area':
			case 'column':
				var seriesData = self.seriesDataFromCollectionData(
					self._options.seriesField,
					self._options.keyField,
					self._options.valField,
					self._options.orderBy
				);

				self._chart.xAxis[0].setCategories(
					seriesData.xAxis.categories
				);

				for (i = 0; i < seriesData.series.length; i++) {
					if (self._chart.series[i]) {
						// Series exists, set it's data
						self._chart.series[i].setData(
							seriesData.series[i].data,
							true,
							true
						);
					} else {
						// Series data does not yet exist, add a new series
						self._chart.addSeries(
							seriesData.series[i],
							true,
							true
						);
					}
				}
				break;

			default:
				break;
		}
	}
};

/**
 * Destroys the chart and all internal references.
 * @returns {Boolean}
 */
Highchart.prototype.drop = function () {
	if (!this.isDropped()) {
		this._state = 'dropped';

		if (this._chart) {
			this._chart.destroy();
		}

		if (this._collection) {
			this._collection.off('change', this._changeListener);
			this._collection.off('drop', this.drop);

			if (this._collection._highcharts) {
				delete this._collection._highcharts[this._options.selector];
			}
		}

		delete this._chart;
		delete this._options;
		delete this._collection;

		this.emit('drop', this);

		return true;
	} else {
		return true;
	}
};

// Extend collection with highchart init
Collection.prototype.init = function () {
	this._highcharts = {};
	CollectionInit.apply(this, arguments);
};

/**
 * Creates a pie chart from the collection.
 * @type {Overload}
 */
Collection.prototype.pieChart = new Overload({
	/**
	 * Chart via options object.
	 * @func pieChart
	 * @memberof Highchart
	 * @param {Object} options The options object.
	 * @returns {*}
	 */
	'object': function (options) {
		options.type = 'pie';

		options.chartOptions = options.chartOptions || {};
		options.chartOptions.chart = options.chartOptions.chart || {};
		options.chartOptions.chart.type = 'pie';

		if (!this._highcharts[options.selector]) {
			// Store new chart in charts array
			this._highcharts[options.selector] = new Highchart(this, options);
		}

		return this._highcharts[options.selector];
	},

	/**
	 * Chart via defined params and an options object.
	 * @func pieChart
	 * @memberof Highchart
	 * @param {String|jQuery} selector The element to render the chart to.
	 * @param {String} keyField The field to use as the data key.
	 * @param {String} valField The field to use as the data value.
	 * @param {String} seriesName The name of the series to display on the chart.
	 * @param {Object} options The options object.
	 */
	'*, string, string, string, ...': function (selector, keyField, valField, seriesName, options) {
		options = options || {};

		options.selector = selector;
		options.keyField = keyField;
		options.valField = valField;
		options.seriesName = seriesName;

		// Call the main chart method
		this.pieChart(options);
	}
});

/**
 * Creates a line chart from the collection.
 * @type {Overload}
 */
Collection.prototype.lineChart = new Overload({
	/**
	 * Chart via options object.
	 * @func lineChart
	 * @memberof Highchart
	 * @param {Object} options The options object.
	 * @returns {*}
	 */
	'object': function (options) {
		options.type = 'line';

		options.chartOptions = options.chartOptions || {};
		options.chartOptions.chart = options.chartOptions.chart || {};
		options.chartOptions.chart.type = 'line';

		if (!this._highcharts[options.selector]) {
			// Store new chart in charts array
			this._highcharts[options.selector] = new Highchart(this, options);
		}

		return this._highcharts[options.selector];
	},

	/**
	 * Chart via defined params and an options object.
	 * @func lineChart
	 * @memberof Highchart
	 * @param {String|jQuery} selector The element to render the chart to.
	 * @param {String} seriesField The name of the series to plot.
	 * @param {String} keyField The field to use as the data key.
	 * @param {String} valField The field to use as the data value.
	 * @param {Object} options The options object.
	 */
	'*, string, string, string, ...': function (selector, seriesField, keyField, valField, options) {
		options = options || {};

		options.seriesField = seriesField;
		options.selector = selector;
		options.keyField = keyField;
		options.valField = valField;

		// Call the main chart method
		this.lineChart(options);
	}
});

/**
 * Creates an area chart from the collection.
 * @type {Overload}
 */
Collection.prototype.areaChart = new Overload({
	/**
	 * Chart via options object.
	 * @func areaChart
	 * @memberof Highchart
	 * @param {Object} options The options object.
	 * @returns {*}
	 */
	'object': function (options) {
		options.type = 'area';

		options.chartOptions = options.chartOptions || {};
		options.chartOptions.chart = options.chartOptions.chart || {};
		options.chartOptions.chart.type = 'area';

		if (!this._highcharts[options.selector]) {
			// Store new chart in charts array
			this._highcharts[options.selector] = new Highchart(this, options);
		}

		return this._highcharts[options.selector];
	},

	/**
	 * Chart via defined params and an options object.
	 * @func areaChart
	 * @memberof Highchart
	 * @param {String|jQuery} selector The element to render the chart to.
	 * @param {String} seriesField The name of the series to plot.
	 * @param {String} keyField The field to use as the data key.
	 * @param {String} valField The field to use as the data value.
	 * @param {Object} options The options object.
	 */
	'*, string, string, string, ...': function (selector, seriesField, keyField, valField, options) {
		options = options || {};

		options.seriesField = seriesField;
		options.selector = selector;
		options.keyField = keyField;
		options.valField = valField;

		// Call the main chart method
		this.areaChart(options);
	}
});

/**
 * Creates a column chart from the collection.
 * @type {Overload}
 */
Collection.prototype.columnChart = new Overload({
	/**
	 * Chart via options object.
	 * @func columnChart
	 * @memberof Highchart
	 * @param {Object} options The options object.
	 * @returns {*}
	 */
	'object': function (options) {
		options.type = 'column';

		options.chartOptions = options.chartOptions || {};
		options.chartOptions.chart = options.chartOptions.chart || {};
		options.chartOptions.chart.type = 'column';

		if (!this._highcharts[options.selector]) {
			// Store new chart in charts array
			this._highcharts[options.selector] = new Highchart(this, options);
		}

		return this._highcharts[options.selector];
	},

	/**
	 * Chart via defined params and an options object.
	 * @func columnChart
	 * @memberof Highchart
	 * @param {String|jQuery} selector The element to render the chart to.
	 * @param {String} seriesField The name of the series to plot.
	 * @param {String} keyField The field to use as the data key.
	 * @param {String} valField The field to use as the data value.
	 * @param {Object} options The options object.
	 */
	'*, string, string, string, ...': function (selector, seriesField, keyField, valField, options) {
		options = options || {};

		options.seriesField = seriesField;
		options.selector = selector;
		options.keyField = keyField;
		options.valField = valField;

		// Call the main chart method
		this.columnChart(options);
	}
});

/**
 * Creates a bar chart from the collection.
 * @type {Overload}
 */
Collection.prototype.barChart = new Overload({
	/**
	 * Chart via options object.
	 * @func barChart
	 * @memberof Highchart
	 * @param {Object} options The options object.
	 * @returns {*}
	 */
	'object': function (options) {
		options.type = 'bar';

		options.chartOptions = options.chartOptions || {};
		options.chartOptions.chart = options.chartOptions.chart || {};
		options.chartOptions.chart.type = 'bar';

		if (!this._highcharts[options.selector]) {
			// Store new chart in charts array
			this._highcharts[options.selector] = new Highchart(this, options);
		}

		return this._highcharts[options.selector];
	},

	/**
	 * Chart via defined params and an options object.
	 * @func barChart
	 * @memberof Highchart
	 * @param {String|jQuery} selector The element to render the chart to.
	 * @param {String} seriesField The name of the series to plot.
	 * @param {String} keyField The field to use as the data key.
	 * @param {String} valField The field to use as the data value.
	 * @param {Object} options The options object.
	 */
	'*, string, string, string, ...': function (selector, seriesField, keyField, valField, options) {
		options = options || {};

		options.seriesField = seriesField;
		options.selector = selector;
		options.keyField = keyField;
		options.valField = valField;

		// Call the main chart method
		this.barChart(options);
	}
});

/**
 * Creates a stacked bar chart from the collection.
 * @type {Overload}
 */
Collection.prototype.stackedBarChart = new Overload({
	/**
	 * Chart via options object.
	 * @func stackedBarChart
	 * @memberof Highchart
	 * @param {Object} options The options object.
	 * @returns {*}
	 */
	'object': function (options) {
		options.type = 'bar';

		options.chartOptions = options.chartOptions || {};
		options.chartOptions.chart = options.chartOptions.chart || {};
		options.chartOptions.chart.type = 'bar';

		options.plotOptions = options.plotOptions || {};
		options.plotOptions.series = options.plotOptions.series || {};
		options.plotOptions.series.stacking = options.plotOptions.series.stacking || 'normal';

		if (!this._highcharts[options.selector]) {
			// Store new chart in charts array
			this._highcharts[options.selector] = new Highchart(this, options);
		}

		return this._highcharts[options.selector];
	},

	/**
	 * Chart via defined params and an options object.
	 * @func stackedBarChart
	 * @memberof Highchart
	 * @param {String|jQuery} selector The element to render the chart to.
	 * @param {String} seriesField The name of the series to plot.
	 * @param {String} keyField The field to use as the data key.
	 * @param {String} valField The field to use as the data value.
	 * @param {Object} options The options object.
	 */
	'*, string, string, string, ...': function (selector, seriesField, keyField, valField, options) {
		options = options || {};

		options.seriesField = seriesField;
		options.selector = selector;
		options.keyField = keyField;
		options.valField = valField;

		// Call the main chart method
		this.stackedBarChart(options);
	}
});

/**
 * Removes a chart from the page by it's selector.
 * @memberof Collection
 * @param {String} selector The chart selector.
 */
Collection.prototype.dropChart = function (selector) {
	if (this._highcharts && this._highcharts[selector]) {
		this._highcharts[selector].drop();
	}
};

Shared.finishModule('Highchart');
module.exports = Highchart;