/** * @license Highcharts JS v5.0.3 (2016-11-18) * Data module * * (c) 2012-2016 Torstein Honsi * * License: www.highcharts.com/license */ (function(factory) { if (typeof module === 'object' && module.exports) { module.exports = factory; } else { factory(Highcharts); } }(function(Highcharts) { (function(Highcharts) { /** * Data module * * (c) 2012-2016 Torstein Honsi * * License: www.highcharts.com/license */ /* global jQuery */ 'use strict'; // Utilities var win = Highcharts.win, doc = win.document, each = Highcharts.each, pick = Highcharts.pick, inArray = Highcharts.inArray, isNumber = Highcharts.isNumber, splat = Highcharts.splat, SeriesBuilder; // 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) || []; this.firstRowAsNames = pick(options.firstRowAsNames, true); this.decimalRegex = options.decimalPoint && new RegExp('^(-?[0-9]+)' + options.decimalPoint + '([0-9]+)$'); // This is a two-dimensional array holding the raw, trimmed string values // with the same organisation as the columns array. It makes it possible // for example to revert from interpreted timestamps to string-based // categories. this.rawColumns = []; // 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, options = this.options, xColumns = [], getValueCount = function(type) { return (Highcharts.seriesTypes[type || 'line'].prototype.pointArrayMap || [0]).length; }, getPointArrayMap = function(type) { return Highcharts.seriesTypes[type || 'line'].prototype.pointArrayMap; }, globalType = chartOptions && chartOptions.chart && chartOptions.chart.type, individualCounts = [], seriesBuilders = [], seriesIndex = 0, i; each((chartOptions && chartOptions.series) || [], function(series) { individualCounts.push(getValueCount(series.type || globalType)); }); // Collect the x-column indexes from seriesMapping each((options && options.seriesMapping) || [], function(mapping) { xColumns.push(mapping.x || 0); }); // If there are no defined series with x-columns, use the first column as x column if (xColumns.length === 0) { xColumns.push(0); } // Loop all seriesMappings and constructs SeriesBuilders from // the mapping options. each((options && options.seriesMapping) || [], function(mapping) { var builder = new SeriesBuilder(), name, numberOfValueColumnsNeeded = individualCounts[seriesIndex] || getValueCount(globalType), seriesArr = (chartOptions && chartOptions.series) || [], series = seriesArr[seriesIndex] || {}, pointArrayMap = getPointArrayMap(series.type || globalType) || ['y']; // Add an x reader from the x property or from an undefined column // if the property is not set. It will then be auto populated later. builder.addColumnReader(mapping.x, 'x'); // Add all column mappings for (name in mapping) { if (mapping.hasOwnProperty(name) && name !== 'x') { builder.addColumnReader(mapping[name], name); } } // Add missing columns for (i = 0; i < numberOfValueColumnsNeeded; i++) { if (!builder.hasReader(pointArrayMap[i])) { //builder.addNextColumnReader(pointArrayMap[i]); // Create and add a column reader for the next free column index builder.addColumnReader(undefined, pointArrayMap[i]); } } seriesBuilders.push(builder); seriesIndex++; }); var globalPointArrayMap = getPointArrayMap(globalType); if (globalPointArrayMap === undefined) { globalPointArrayMap = ['y']; } this.valueCount = { global: getValueCount(globalType), xColumns: xColumns, individual: individualCounts, seriesBuilders: seriesBuilders, globalPointArrayMap: globalPointArrayMap }; }, /** * When the data is parsed into columns, either by CSV, table, GS or direct input, * continue with other operations. */ dataFound: function() { if (this.options.switchRowsAndColumns) { this.columns = this.rowsToColumns(this.columns); } // Interpret the info about series and columns this.getColumnDistribution(); // Interpret the values into right types this.parseTypes(); // Handle columns if a handleColumns callback is given if (this.parsed() !== false) { // 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, itemDelimiter, lines, activeRowNo = 0; if (csv) { lines = csv .replace(/\r\n/g, '\n') // Unix .replace(/\r/g, '\n') // Mac .split(options.lineDelimiter || '\n'); itemDelimiter = options.itemDelimiter || (csv.indexOf('\t') !== -1 ? '\t' : ','); 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(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; if (table) { if (typeof table === 'string') { table = doc.getElementById(table); } each(table.getElementsByTagName('tr'), function(tr, rowNo) { if (rowNo >= startRow && rowNo <= endRow) { each(tr.children, function(item, colNo) { if ((item.tagName === 'TD' || item.tagName === 'TH') && colNo >= startColumn && colNo <= endColumn) { if (!columns[colNo - startColumn]) { columns[colNo - startColumn] = []; } columns[colNo - startColumn][rowNo - startRow] = item.innerHTML; } }); } }); this.dataFound(); // continue } }, /** */ 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.ajax({ dataType: 'json', url: 'https://spreadsheets.google.com/feeds/cells/' + googleSpreadsheetKey + '/' + (options.googleSpreadsheetWorksheet || 'od6') + '/public/values?alt=json-in-script&callback=?', error: options.error, success: 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; } } // Insert null for empty spreadsheet cells (#5298) each(columns, function(column) { for (i = 0; i < column.length; i++) { if (column[i] === undefined) { column[i] = null; } } }); self.dataFound(); } }); } }, /** * Trim a string from whitespace */ trim: function(str, inside) { if (typeof str === 'string') { str = str.replace(/^\s+|\s+$/g, ''); // Clear white space insdie the string, like thousands separators if (inside && /^[0-9\s]+$/.test(str)) { str = str.replace(/\s/g, ''); } if (this.decimalRegex) { str = str.replace(this.decimalRegex, '$1.$2'); } } return str; }, /** * Parse numeric cells in to number types and date types in to true dates. */ parseTypes: function() { var columns = this.columns, col = columns.length; while (col--) { this.parseColumn(columns[col], col); } }, /** * Parse a single column. Set properties like .isDatetime and .isNumeric. */ parseColumn: function(column, col) { var rawColumns = this.rawColumns, columns = this.columns, row = column.length, val, floatVal, trimVal, trimInsideVal, firstRowAsNames = this.firstRowAsNames, isXColumn = inArray(col, this.valueCount.xColumns) !== -1, dateVal, backup = [], diff, chartOptions = this.chartOptions, descending, columnTypes = this.options.columnTypes || [], columnType = columnTypes[col], forceCategory = isXColumn && ((chartOptions && chartOptions.xAxis && splat(chartOptions.xAxis)[0].type === 'category') || columnType === 'string'); if (!rawColumns[col]) { rawColumns[col] = []; } while (row--) { val = backup[row] || column[row]; trimVal = this.trim(val); trimInsideVal = this.trim(val, true); floatVal = parseFloat(trimInsideVal); // Set it the first time if (rawColumns[col][row] === undefined) { rawColumns[col][row] = trimVal; } // Disable number or date parsing by setting the X axis type to category if (forceCategory || (row === 0 && firstRowAsNames)) { column[row] = trimVal; } else if (+trimInsideVal === floatVal) { // is numeric column[row] = floatVal; // If the number is greater than milliseconds in a year, assume datetime if (floatVal > 365 * 24 * 3600 * 1000 && columnType !== 'float') { column.isDatetime = true; } else { column.isNumeric = true; } if (column[row + 1] !== undefined) { descending = floatVal > column[row + 1]; } // String, continue to determine if it is a date string or really a string } else { dateVal = this.parseDate(val); // Only allow parsing of dates if this column is an x-column if (isXColumn && isNumber(dateVal) && columnType !== 'float') { // is date backup[row] = val; column[row] = dateVal; column.isDatetime = true; // Check if the dates are uniformly descending or ascending. If they // are not, chances are that they are a different time format, so check // for alternative. if (column[row + 1] !== undefined) { diff = dateVal > column[row + 1]; if (diff !== descending && descending !== undefined) { if (this.alternativeFormat) { this.dateFormat = this.alternativeFormat; row = column.length; this.alternativeFormat = this.dateFormats[this.dateFormat].alternative; } else { column.unsorted = true; } } descending = diff; } } else { // string column[row] = trimVal === '' ? null : trimVal; if (row !== 0 && (column.isDatetime || column.isNumeric)) { column.mixed = true; } } } } // If strings are intermixed with numbers or dates in a parsed column, it is an indication // that parsing went wrong or the data was not intended to display as numbers or dates and // parsing is too aggressive. Fall back to categories. Demonstrated in the // highcharts/demo/column-drilldown sample. if (isXColumn && column.mixed) { columns[col] = rawColumns[col]; } // If the 0 column is date or number and descending, reverse all columns. if (isXColumn && descending && this.options.sort) { for (col = 0; col < columns.length; col++) { columns[col].reverse(); if (firstRowAsNames) { columns[col].unshift(columns[col].pop()); } } } }, /** * A collection of available date formats, extendable from the outside to support * custom date formats. */ 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]); } }, 'dd/mm/YYYY': { regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{4})$/, parser: function(match) { return Date.UTC(+match[3], match[2] - 1, +match[1]); }, alternative: 'mm/dd/YYYY' // different format with the same regex }, 'mm/dd/YYYY': { regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{4})$/, parser: function(match) { return Date.UTC(+match[3], match[1] - 1, +match[2]); } }, 'dd/mm/YY': { regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{2})$/, parser: function(match) { return Date.UTC(+match[3] + 2000, match[2] - 1, +match[1]); }, alternative: 'mm/dd/YY' // different format with the same regex }, 'mm/dd/YY': { regex: /^([0-9]{1,2})[\-\/\.]([0-9]{1,2})[\-\/\.]([0-9]{2})$/, parser: function(match) { return Date.UTC(+match[3] + 2000, match[1] - 1, +match[2]); } } }, /** * Parse a date and return it as a number. Overridable through options.parseDate. */ parseDate: function(val) { var parseDate = this.options.parseDate, ret, key, format, dateFormat = this.options.dateFormat || this.dateFormat, match; if (parseDate) { ret = parseDate(val); } else if (typeof val === 'string') { // Auto-detect the date format the first time if (!dateFormat) { for (key in this.dateFormats) { format = this.dateFormats[key]; match = val.match(format.regex); if (match) { this.dateFormat = dateFormat = key; this.alternativeFormat = format.alternative; ret = format.parser(match); break; } } // Next time, use the one previously found } else { format = this.dateFormats[dateFormat]; match = val.match(format.regex); if (match) { ret = format.parser(match); } } // Fall back to Date.parse if (!match) { match = Date.parse(val); // External tools like Date.js and MooTools extend Date object and // returns a date. if (typeof match === 'object' && match !== null && match.getTime) { ret = match.getTime() - match.getTimezoneOffset() * 60000; // Timestamp } else if (isNumber(match)) { ret = match - (new Date(match)).getTimezoneOffset() * 60000; } } } 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) { return this.options.parsed.call(this, this.columns); } }, getFreeIndexes: function(numberOfColumns, seriesBuilders) { var s, i, freeIndexes = [], freeIndexValues = [], referencedIndexes; // Add all columns as free for (i = 0; i < numberOfColumns; i = i + 1) { freeIndexes.push(true); } // Loop all defined builders and remove their referenced columns for (s = 0; s < seriesBuilders.length; s = s + 1) { referencedIndexes = seriesBuilders[s].getReferencedColumnIndexes(); for (i = 0; i < referencedIndexes.length; i = i + 1) { freeIndexes[referencedIndexes[i]] = false; } } // Collect the values for the free indexes for (i = 0; i < freeIndexes.length; i = i + 1) { if (freeIndexes[i]) { freeIndexValues.push(i); } } return freeIndexValues; }, /** * If a complete callback function is provided in the options, interpret the * columns into a Highcharts options object. */ complete: function() { var columns = this.columns, xColumns = [], type, options = this.options, series, data, i, j, r, seriesIndex, chartOptions, allSeriesBuilders = [], builder, freeIndexes, typeCol, index; xColumns.length = columns.length; if (options.complete || options.afterComplete) { // Get the names and shift the top row for (i = 0; i < columns.length; i++) { if (this.firstRowAsNames) { columns[i].name = columns[i].shift(); } } // Use the next columns for series series = []; freeIndexes = this.getFreeIndexes(columns.length, this.valueCount.seriesBuilders); // Populate defined series for (seriesIndex = 0; seriesIndex < this.valueCount.seriesBuilders.length; seriesIndex++) { builder = this.valueCount.seriesBuilders[seriesIndex]; // If the builder can be populated with remaining columns, then add it to allBuilders if (builder.populateColumns(freeIndexes)) { allSeriesBuilders.push(builder); } } // Populate dynamic series while (freeIndexes.length > 0) { builder = new SeriesBuilder(); builder.addColumnReader(0, 'x'); // Mark index as used (not free) index = inArray(0, freeIndexes); if (index !== -1) { freeIndexes.splice(index, 1); } for (i = 0; i < this.valueCount.global; i++) { // Create and add a column reader for the next free column index builder.addColumnReader(undefined, this.valueCount.globalPointArrayMap[i]); } // If the builder can be populated with remaining columns, then add it to allBuilders if (builder.populateColumns(freeIndexes)) { allSeriesBuilders.push(builder); } } // Get the data-type from the first series x column if (allSeriesBuilders.length > 0 && allSeriesBuilders[0].readers.length > 0) { typeCol = columns[allSeriesBuilders[0].readers[0].columnIndex]; if (typeCol !== undefined) { if (typeCol.isDatetime) { type = 'datetime'; } else if (!typeCol.isNumeric) { type = 'category'; } } } // Axis type is category, then the "x" column should be called "name" if (type === 'category') { for (seriesIndex = 0; seriesIndex < allSeriesBuilders.length; seriesIndex++) { builder = allSeriesBuilders[seriesIndex]; for (r = 0; r < builder.readers.length; r++) { if (builder.readers[r].configName === 'x') { builder.readers[r].configName = 'name'; } } } } // Read data for all builders for (seriesIndex = 0; seriesIndex < allSeriesBuilders.length; seriesIndex++) { builder = allSeriesBuilders[seriesIndex]; // Iterate down the cells of each column and add data to the series data = []; for (j = 0; j < columns[0].length; j++) { data[j] = builder.read(columns, j); } // Add the series series[seriesIndex] = { data: data }; if (builder.name) { series[seriesIndex].name = builder.name; } if (type === 'category') { series[seriesIndex].turboThreshold = 0; } } // Do the callback chartOptions = { series: series }; if (type) { chartOptions.xAxis = { type: type }; if (type === 'category') { chartOptions.xAxis.uniqueNames = false; } } if (options.complete) { options.complete(chartOptions); } // The afterComplete hook is used internally to avoid conflict with the externally // available complete option. if (options.afterComplete) { options.afterComplete(chartOptions); } } } }); // 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, { afterComplete: function(dataOptions) { var i, series; // Merge series configs if (userOptions.hasOwnProperty('series')) { if (typeof userOptions.series === 'object') { i = Math.max(userOptions.series.length, dataOptions.series.length); while (i--) { series = userOptions.series[i] || {}; userOptions.series[i] = Highcharts.merge(series, dataOptions.series[i]); } } else { // Allow merging in dataOptions.series (#2856) delete userOptions.series; } } // Do the merge userOptions = Highcharts.merge(dataOptions, userOptions); proceed.call(chart, userOptions, callback); } }), userOptions); } else { proceed.call(chart, userOptions, callback); } }); /** * Creates a new SeriesBuilder. A SeriesBuilder consists of a number * of ColumnReaders that reads columns and give them a name. * Ex: A series builder can be constructed to read column 3 as 'x' and * column 7 and 8 as 'y1' and 'y2'. * The output would then be points/rows of the form {x: 11, y1: 22, y2: 33} * * The name of the builder is taken from the second column. In the above * example it would be the column with index 7. * @constructor */ SeriesBuilder = function() { this.readers = []; this.pointIsArray = true; }; /** * Populates readers with column indexes. A reader can be added without * a specific index and for those readers the index is taken sequentially * from the free columns (this is handled by the ColumnCursor instance). * @returns {boolean} */ SeriesBuilder.prototype.populateColumns = function(freeIndexes) { var builder = this, enoughColumns = true; // Loop each reader and give it an index if its missing. // The freeIndexes.shift() will return undefined if there // are no more columns. each(builder.readers, function(reader) { if (reader.columnIndex === undefined) { reader.columnIndex = freeIndexes.shift(); } }); // Now, all readers should have columns mapped. If not // then return false to signal that this series should // not be added. each(builder.readers, function(reader) { if (reader.columnIndex === undefined) { enoughColumns = false; } }); return enoughColumns; }; /** * Reads a row from the dataset and returns a point or array depending * on the names of the readers. * @param columns * @param rowIndex * @returns {Array | Object} */ SeriesBuilder.prototype.read = function(columns, rowIndex) { var builder = this, pointIsArray = builder.pointIsArray, point = pointIsArray ? [] : {}, columnIndexes; // Loop each reader and ask it to read its value. // Then, build an array or point based on the readers names. each(builder.readers, function(reader) { var value = columns[reader.columnIndex][rowIndex]; if (pointIsArray) { point.push(value); } else { point[reader.configName] = value; } }); // The name comes from the first column (excluding the x column) if (this.name === undefined && builder.readers.length >= 2) { columnIndexes = builder.getReferencedColumnIndexes(); if (columnIndexes.length >= 2) { // remove the first one (x col) columnIndexes.shift(); // Sort the remaining columnIndexes.sort(); // Now use the lowest index as name column this.name = columns[columnIndexes.shift()].name; } } return point; }; /** * Creates and adds ColumnReader from the given columnIndex and configName. * ColumnIndex can be undefined and in that case the reader will be given * an index when columns are populated. * @param columnIndex {Number | undefined} * @param configName */ SeriesBuilder.prototype.addColumnReader = function(columnIndex, configName) { this.readers.push({ columnIndex: columnIndex, configName: configName }); if (!(configName === 'x' || configName === 'y' || configName === undefined)) { this.pointIsArray = false; } }; /** * Returns an array of column indexes that the builder will use when * reading data. * @returns {Array} */ SeriesBuilder.prototype.getReferencedColumnIndexes = function() { var i, referencedColumnIndexes = [], columnReader; for (i = 0; i < this.readers.length; i = i + 1) { columnReader = this.readers[i]; if (columnReader.columnIndex !== undefined) { referencedColumnIndexes.push(columnReader.columnIndex); } } return referencedColumnIndexes; }; /** * Returns true if the builder has a reader for the given configName. * @param configName * @returns {boolean} */ SeriesBuilder.prototype.hasReader = function(configName) { var i, columnReader; for (i = 0; i < this.readers.length; i = i + 1) { columnReader = this.readers[i]; if (columnReader.configName === configName) { return true; } } // Else return undefined }; }(Highcharts)); }));