/**
 * @license Highcharts JS v5.0.2 (2016-10-26)
 * 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));
}));
