| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582 | /** * @license Data plugin for Highcharts * * (c) 2012-2013 Torstein Hønsi * Last revision 2013-06-07 * * License: www.highcharts.com/license *//* * The Highcharts Data plugin is a utility to ease parsing of input sources like * CSV, HTML tables or grid views into basic configuration options for use  * directly in the Highcharts constructor. * * Demo: http://jsfiddle.net/highcharts/SnLFj/ * * --- OPTIONS --- * * - columns : Array<Array<Mixed>> * A two-dimensional array representing the input data on tabular form. This input can * be used when the data is already parsed, for example from a grid view component. * Each cell can be a string or number. If not switchRowsAndColumns is set, the columns * are interpreted as series. See also the rows option. * * - complete : Function(chartOptions) * The callback that is evaluated when the data is finished loading, optionally from an  * external source, and parsed. The first argument passed is a finished chart options * object, containing series and an xAxis with categories if applicable. Thise options * can be extended with additional options and passed directly to the chart constructor. * * - csv : String * A comma delimited string to be parsed. Related options are startRow, endRow, startColumn * and endColumn to delimit what part of the table is used. The lineDelimiter and  * itemDelimiter options define the CSV delimiter formats. *  * - endColumn : Integer * In tabular input data, the first row (indexed by 0) to use. Defaults to the last  * column containing data. * * - endRow : Integer * In tabular input data, the last row (indexed by 0) to use. Defaults to the last row * containing data. * * - googleSpreadsheetKey : String  * A Google Spreadsheet key. See https://developers.google.com/gdata/samples/spreadsheet_sample * for general information on GS. * * - googleSpreadsheetWorksheet : String  * The Google Spreadsheet worksheet. The available id's can be read from  * https://spreadsheets.google.com/feeds/worksheets/{key}/public/basic * * - itemDelimiter : String * Item or cell delimiter for parsing CSV. Defaults to ",". * * - lineDelimiter : String * Line delimiter for parsing CSV. Defaults to "\n". * * - parsed : Function * A callback function to access the parsed columns, the two-dimentional input data * array directly, before they are interpreted into series data and categories. * * - parseDate : Function * A callback function to parse string representations of dates into JavaScript timestamps. * Return an integer on success. * * - rows : Array<Array<Mixed>> * The same as the columns input option, but defining rows intead of columns. * * - startColumn : Integer * In tabular input data, the first column (indexed by 0) to use.  * * - startRow : Integer * In tabular input data, the first row (indexed by 0) to use. * * - table : String|HTMLElement * A HTML table or the id of such to be parsed as input data. Related options ara startRow, * endRow, startColumn and endColumn to delimit what part of the table is used. */// JSLint options:/*global jQuery */(function (Highcharts) {			// Utilities	var each = Highcharts.each;			// The Data constructor	var Data = function (dataOptions, chartOptions) {		this.init(dataOptions, chartOptions);	};		// Set the prototype properties	Highcharts.extend(Data.prototype, {			/**	 * Initialize the Data object with the given options	 */	init: function (options, chartOptions) {		this.options = options;		this.chartOptions = chartOptions;		this.columns = options.columns || this.rowsToColumns(options.rows) || [];		// No need to parse or interpret anything		if (this.columns.length) {			this.dataFound();		// Parse and interpret		} else {			// Parse a CSV string if options.csv is given			this.parseCSV();						// Parse a HTML table if options.table is given			this.parseTable();			// Parse a Google Spreadsheet 			this.parseGoogleSpreadsheet();			}	},	/**	 * Get the column distribution. For example, a line series takes a single column for 	 * Y values. A range series takes two columns for low and high values respectively,	 * and an OHLC series takes four columns.	 */	getColumnDistribution: function () {		var chartOptions = this.chartOptions,			getValueCount = function (type) {				return (Highcharts.seriesTypes[type || 'line'].prototype.pointArrayMap || [0]).length;			},			globalType = chartOptions && chartOptions.chart && chartOptions.chart.type,			individualCounts = [];		each((chartOptions && chartOptions.series) || [], function (series) {			individualCounts.push(getValueCount(series.type || globalType));		});		this.valueCount = {			global: getValueCount(globalType),			individual: individualCounts		};	},	dataFound: function () {		// Interpret the values into right types		this.parseTypes();				// Use first row for series names?		this.findHeaderRow();				// Handle columns if a handleColumns callback is given		this.parsed();				// Complete if a complete callback is given		this.complete();			},		/**	 * Parse a CSV input string	 */	parseCSV: function () {		var self = this,			options = this.options,			csv = options.csv,			columns = this.columns,			startRow = options.startRow || 0,			endRow = options.endRow || Number.MAX_VALUE,			startColumn = options.startColumn || 0,			endColumn = options.endColumn || Number.MAX_VALUE,			lines,			activeRowNo = 0;					if (csv) {						lines = csv				.replace(/\r\n/g, "\n") // Unix				.replace(/\r/g, "\n") // Mac				.split(options.lineDelimiter || "\n");						each(lines, function (line, rowNo) {				var trimmed = self.trim(line),					isComment = trimmed.indexOf('#') === 0,					isBlank = trimmed === '',					items;								if (rowNo >= startRow && rowNo <= endRow && !isComment && !isBlank) {					items = line.split(options.itemDelimiter || ',');					each(items, function (item, colNo) {						if (colNo >= startColumn && colNo <= endColumn) {							if (!columns[colNo - startColumn]) {								columns[colNo - startColumn] = [];												}														columns[colNo - startColumn][activeRowNo] = item;						}					});					activeRowNo += 1;				}			});			this.dataFound();		}	},		/**	 * Parse a HTML table	 */	parseTable: function () {		var options = this.options,			table = options.table,			columns = this.columns,			startRow = options.startRow || 0,			endRow = options.endRow || Number.MAX_VALUE,			startColumn = options.startColumn || 0,			endColumn = options.endColumn || Number.MAX_VALUE,			colNo;					if (table) {						if (typeof table === 'string') {				table = document.getElementById(table);			}						each(table.getElementsByTagName('tr'), function (tr, rowNo) {				colNo = 0; 				if (rowNo >= startRow && rowNo <= endRow) {					each(tr.childNodes, function (item) {						if ((item.tagName === 'TD' || item.tagName === 'TH') && colNo >= startColumn && colNo <= endColumn) {							if (!columns[colNo]) {								columns[colNo] = [];												}							columns[colNo][rowNo - startRow] = item.innerHTML;														colNo += 1;						}					});				}			});			this.dataFound(); // continue		}	},	/**	 * TODO: 	 * - switchRowsAndColumns	 */	parseGoogleSpreadsheet: function () {		var self = this,			options = this.options,			googleSpreadsheetKey = options.googleSpreadsheetKey,			columns = this.columns,			startRow = options.startRow || 0,			endRow = options.endRow || Number.MAX_VALUE,			startColumn = options.startColumn || 0,			endColumn = options.endColumn || Number.MAX_VALUE,			gr, // google row			gc; // google column		if (googleSpreadsheetKey) {			jQuery.getJSON('https://spreadsheets.google.com/feeds/cells/' + 				  googleSpreadsheetKey + '/' + (options.googleSpreadsheetWorksheet || 'od6') +					  '/public/values?alt=json-in-script&callback=?',					  function (json) {									// Prepare the data from the spreadsheat				var cells = json.feed.entry,					cell,					cellCount = cells.length,					colCount = 0,					rowCount = 0,					i;							// First, find the total number of columns and rows that 				// are actually filled with data				for (i = 0; i < cellCount; i++) {					cell = cells[i];					colCount = Math.max(colCount, cell.gs$cell.col);					rowCount = Math.max(rowCount, cell.gs$cell.row);							}							// Set up arrays containing the column data				for (i = 0; i < colCount; i++) {					if (i >= startColumn && i <= endColumn) {						// Create new columns with the length of either end-start or rowCount						columns[i - startColumn] = [];						// Setting the length to avoid jslint warning						columns[i - startColumn].length = Math.min(rowCount, endRow - startRow);					}				}								// Loop over the cells and assign the value to the right				// place in the column arrays				for (i = 0; i < cellCount; i++) {					cell = cells[i];					gr = cell.gs$cell.row - 1; // rows start at 1					gc = cell.gs$cell.col - 1; // columns start at 1					// If both row and col falls inside start and end					// set the transposed cell value in the newly created columns					if (gc >= startColumn && gc <= endColumn &&						gr >= startRow && gr <= endRow) {						columns[gc - startColumn][gr - startRow] = cell.content.$t;					}				}				self.dataFound();			});		}	},		/**	 * Find the header row. For now, we just check whether the first row contains	 * numbers or strings. Later we could loop down and find the first row with 	 * numbers.	 */	findHeaderRow: function () {		var headerRow = 0;		each(this.columns, function (column) {			if (typeof column[0] !== 'string') {				headerRow = null;			}		});		this.headerRow = 0;				},		/**	 * Trim a string from whitespace	 */	trim: function (str) {		return typeof str === 'string' ? str.replace(/^\s+|\s+$/g, '') : str;	},		/**	 * Parse numeric cells in to number types and date types in to true dates.	 * @param {Object} columns	 */	parseTypes: function () {		var columns = this.columns,			col = columns.length, 			row,			val,			floatVal,			trimVal,			dateVal;					while (col--) {			row = columns[col].length;			while (row--) {				val = columns[col][row];				floatVal = parseFloat(val);				trimVal = this.trim(val);				/*jslint eqeq: true*/				if (trimVal == floatVal) { // is numeric				/*jslint eqeq: false*/					columns[col][row] = floatVal;										// If the number is greater than milliseconds in a year, assume datetime					if (floatVal > 365 * 24 * 3600 * 1000) {						columns[col].isDatetime = true;					} else {						columns[col].isNumeric = true;					}													} else { // string, continue to determine if it is a date string or really a string					dateVal = this.parseDate(val);										if (col === 0 && typeof dateVal === 'number' && !isNaN(dateVal)) { // is date						columns[col][row] = dateVal;						columns[col].isDatetime = true;										} else { // string						columns[col][row] = trimVal === '' ? null : trimVal;					}				}							}		}	},	//*	dateFormats: {		'YYYY-mm-dd': {			regex: '^([0-9]{4})-([0-9]{2})-([0-9]{2})$',			parser: function (match) {				return Date.UTC(+match[1], match[2] - 1, +match[3]);			}		}	},	// */	/**	 * Parse a date and return it as a number. Overridable through options.parseDate.	 */	parseDate: function (val) {		var parseDate = this.options.parseDate,			ret,			key,			format,			match;		if (parseDate) {			ret = parseDate(val);		}					if (typeof val === 'string') {			for (key in this.dateFormats) {				format = this.dateFormats[key];				match = val.match(format.regex);				if (match) {					ret = format.parser(match);				}			}		}		return ret;	},		/**	 * Reorganize rows into columns	 */	rowsToColumns: function (rows) {		var row,			rowsLength,			col,			colsLength,			columns;		if (rows) {			columns = [];			rowsLength = rows.length;			for (row = 0; row < rowsLength; row++) {				colsLength = rows[row].length;				for (col = 0; col < colsLength; col++) {					if (!columns[col]) {						columns[col] = [];					}					columns[col][row] = rows[row][col];				}			}		}		return columns;	},		/**	 * A hook for working directly on the parsed columns	 */	parsed: function () {		if (this.options.parsed) {			this.options.parsed.call(this, this.columns);		}	},		/**	 * If a complete callback function is provided in the options, interpret the 	 * columns into a Highcharts options object.	 */	complete: function () {				var columns = this.columns,			firstCol,			type,			options = this.options,			valueCount,			series,			data,			i,			j,			seriesIndex;							if (options.complete) {			this.getColumnDistribution();						// Use first column for X data or categories?			if (columns.length > 1) {				firstCol = columns.shift();				if (this.headerRow === 0) {					firstCol.shift(); // remove the first cell				}												if (firstCol.isDatetime) {					type = 'datetime';				} else if (!firstCol.isNumeric) {					type = 'category';				}			}			// Get the names and shift the top row			for (i = 0; i < columns.length; i++) {				if (this.headerRow === 0) {					columns[i].name = columns[i].shift();				}			}						// Use the next columns for series			series = [];			for (i = 0, seriesIndex = 0; i < columns.length; seriesIndex++) {				// This series' value count				valueCount = Highcharts.pick(this.valueCount.individual[seriesIndex], this.valueCount.global);								// Iterate down the cells of each column and add data to the series				data = [];				for (j = 0; j < columns[i].length; j++) {					data[j] = [						firstCol[j], 						columns[i][j] !== undefined ? columns[i][j] : null					];					if (valueCount > 1) {						data[j].push(columns[i + 1][j] !== undefined ? columns[i + 1][j] : null);					}					if (valueCount > 2) {						data[j].push(columns[i + 2][j] !== undefined ? columns[i + 2][j] : null);					}					if (valueCount > 3) {						data[j].push(columns[i + 3][j] !== undefined ? columns[i + 3][j] : null);					}					if (valueCount > 4) {						data[j].push(columns[i + 4][j] !== undefined ? columns[i + 4][j] : null);					}				}				// Add the series				series[seriesIndex] = {					name: columns[i].name,					data: data				};				i += valueCount;			}						// Do the callback			options.complete({				xAxis: {					type: type				},				series: series			});		}	}	});		// Register the Data prototype and data function on Highcharts	Highcharts.Data = Data;	Highcharts.data = function (options, chartOptions) {		return new Data(options, chartOptions);	};	// Extend Chart.init so that the Chart constructor accepts a new configuration	// option group, data.	Highcharts.wrap(Highcharts.Chart.prototype, 'init', function (proceed, userOptions, callback) {		var chart = this;		if (userOptions && userOptions.data) {			Highcharts.data(Highcharts.extend(userOptions.data, {				complete: function (dataOptions) {										// Merge series configs					if (userOptions.series) {						each(userOptions.series, function (series, i) {							userOptions.series[i] = Highcharts.merge(series, dataOptions.series[i]);						});					}					// Do the merge					userOptions = Highcharts.merge(dataOptions, userOptions);					proceed.call(chart, userOptions, callback);				}			}), userOptions);		} else {			proceed.call(chart, userOptions, callback);		}	});}(Highcharts));
 |