var YCharts = function(){
    var that={};
    
    // used to load the TEXT_RULER obj/module if it exists in global space
    if(typeof(TEXT_RULER) == 'object'){
        that.textRuler = TEXT_RULER;
    } else {
        that.textRuler = null;
    }
    
    // Used to load the NUMBER_FORMATTER obj/module if it exists in global space
    if(typeof(NUMBER_FORMATTER) == 'object'){
        that.numberFormatter = NUMBER_FORMATTER;
    } else {
        that.numberFormatter = null;
    }

    // Used to load the DATE_FORMATTER obj/module if it exists in global space
    if(typeof(DATE_FORMATTER) == 'object'){
        that.dateFormatter = DATE_FORMATTER;
    } else {
        that.dateFormatter = null;
    }
    
    that.charts=[];
    that.chartOptions = {};
    that.chartData = {};
    that.yAxesTypeMap = {};

    // Time, in milliseconds of each frequency
    that.frequencyLengths = {
        "daily":  24 * 60 * 60 * 1000,
        "weekly":  7 * 24 * 60 * 60 * 1000,
        "monthly":  30 * 24 * 60 * 60 * 1000,
        "quarterly":  90 * 24 * 60 * 60 * 1000,
        "yearly": 365.2425 * 24 * 60 * 60 * 1000
    };

    // Map of app. size of time units in milliseconds
    that.timeUnitSize = {
        "second": 1000,
        "minute": 60 * 1000,
        "hour": 60 * 60 * 1000,
        "day": 24 * 60 * 60 * 1000,
        "month": 30 * 24 * 60 * 60 * 1000,
        "year": 365.2425 * 24 * 60 * 60 * 1000
    };
    
    /*
     *  A special helper structure for tooltips that define the styles for the object labels
     *  The purpose for this helper was to facilitate a specialized truncate measuring tool
     *  that determines the actually width of a tooltip container based on its font-styles
     */
    that.tooltipStyles = function(){
        var self = {};
        
        self.styles = {
            'fontSize':'11px',
            'fontFamily':'Arial',
            'width':'210px', 
            'fontWeight':'bold'
        };
        
        self.styleMap = {
            'fontFamily':'font-family',
            'fontWeight':'font-weight',
            'fontSize':'font-size'
        };
        
        /*
         *  Take the styles defined above and create a special string that can be inserted inline later 
         *  when we are generating the actual html.
         */
        self.styleString = function(){
            var stylestr = '';
            var styleKey = '';
            for(var style in self.styles){
                //make sure some tard didn't prototype a function as a property on obj
                if(self.styles.hasOwnProperty(style)){
                    styleKey = (self.styleMap[style])? self.styleMap[style]:style;
                    styleValue = self.styles[style];
                    stylestr += styleKey+': '+styleValue+'; ';
                }
            }
            // chop off trailing '; '
            return stylestr.slice(0,-2);
        }();
        return self;
    }();
    
    // If you change any of the below options, it will effect our embedding! Must fix embeds options
    // accordingly in compile_embeds.py unless we want to propagate these changes to our embed partners
    that.standardOptions = {
        // Note: because of a quirk with how flot aligns ticks to the edges of the chart, 
        // when autoscaleMargin != null (our default) and flot is automatically making the ticks, 
        // ticks should be set to 2 less than the actual number you want to see. So set ticks: 4, 
        // if you want to 6 ticks on the y-axis
        xaxis: {
            mode: "time",
            font: {size: 11, family: "arial"}, 
            ticks: 5, 
            tickColor: '#D9D9D9',
            tickSize: null,
            minTickSize: [1, "day"],
            tickFormatter: null,
            labelWidth: 36,
            labelHeight: 12,
            autoscaleMargin: 0},
        yaxis: {
            mode: null,
            font: {size: 11, family: "arial"}, 
            ticks: null, 
            color: '#333333',
            tickColor: '#D9D9D9',
            tickSize: null, 
            labelWidth: 36,
            labelHeight: 12,
            autoscaleMargin: 0.02,
            alignTicksWithAxis: 1},
        xaxes : [{}],
        yaxes : [
            {position: "right", tickFormatter: null, tickLength : null},
            {position: "right", tickFormatter: null, tickLength: 0},
            {position: "right", tickFormatter: null, tickLength: 0},
            {position: "right", tickFormatter: null, tickLength: 0},
            {position: "right", tickFormatter: null, tickLength: 0},
            {position: "right", tickFormatter: null, tickLength: 0}
            ],
        legend: {show: false, position: "nw"},
        grid: {
            color: "#333333",
            borderWidth: 1,
            borderColor: "#808080",
            backgroundColor: "#EBF2FE", 
            hoverable: true, 
            clickable: false,
            markings:null
        },
        colors: ["#FF7900", "#0082E9", "#E83535", "#008700", "#7F33BA", 
            "#1C1CB5", "#8CC63F", "#4A4FCF", "#B8292F", "#AF98EA"],
        series: {
            lines: {show: true, lineWidth: 2},
            points: {show: true, radius: 0, fill: true},
            shadowSize: 0
        },
        crosshair: {mode: "x", color: "#808080", lineWidth: 1}
    };
    
    /*
     * An array dump of recession cycles from - http://www.nber.org/cycles/cyclesmain.html
     * A script that takes the list of dates converted into a python structure and then dump out as json
     * can be found here - https://gist.github.com/d8ac996a332bf6332015
     */
    that.recessionCycles = [[-3552854400000, -3505507200000], [-3447619200000, -3426624000000],
    [-3305664000000, -3221510400000], [-3174163200000, -3126816000000], [-3037392000000, -2866579200000],
    [-2771884800000, -2671920000000], [-2614118400000, -2579817600000], [-2508883200000, -2482617600000],
    [-2429827200000, -2385244800000], [-2337897600000, -2290550400000], [-2227478400000, -2180131200000],
    [-2124921600000, -2064441600000], [-1977782400000, -1943481600000], [-1893456000000, -1830384000000],
    [-1798761600000, -1738368000000], [-1622678400000, -1604361600000], [-1577923200000, -1530662400000],
    [-1472860800000, -1435968000000], [-1364947200000, -1330732800000], [-1275523200000, -1162512000000],
    [-1031011200000, -996796800000], [-786240000000, -765331200000], [-667958400000, -639100800000],
    [-520819200000, -494553600000], [-391910400000, -370915200000], [-307756800000, -281318400000],
    [-2678400000, 26265600000], [120960000000, 162864000000], [315532800000, 331257600000],
    [362793600000, 404956800000], [646790400000, 667785600000], [983404800000, 1004572800000], 
    [1196467200000, 1243814400000]];
    

    that.updateTooltipTimeout = null;

    that.addChart = function(chartID, options) {
        var chartOptions = $.extend(true, {}, options);
        that.chartOptions[chartID] = chartOptions;
        that.chartData[chartID] = [];

        if(typeof(that.chartOptions[chartID].format) == 'undefined') {
            that.chartOptions[chartID].format = 'none';
        }
        if(typeof(that.chartOptions[chartID].outliers) == 'undefined') {
            that.chartOptions[chartID].outliers = 'false';
        }
        if(typeof(that.chartOptions[chartID].recessions) == 'undefined') {
            that.chartOptions[chartID].recessions = 'false';
        }
        if(typeof(that.chartOptions[chartID].tooltipStyle) == 'undefined') {
            that.chartOptions[chartID].tooltipStyle = "box";
        }
        if(typeof(that.chartOptions[chartID].tooltipBar) == 'undefined') {
            that.chartOptions[chartID].tooltipBar = "";
        }
        if(typeof(that.chartOptions[chartID].clickURL) == 'undefined') {
            that.chartOptions[chartID].clickURL = null;
        }
        if(typeof(that.chartOptions[chartID].quarterlyFormat) == 'undefined' || 
            !(that.chartOptions[chartID].quarterlyFormat in {'quarter':'', 'month':''})) {
            that.chartOptions[chartID].quarterlyFormat = 'quarter';
        }
        if(typeof(that.chartOptions[chartID].allAxesOnRight) == 'undefined') {
            that.chartOptions[chartID].allAxesOnRight = false;
        }
        if(typeof(that.chartOptions[chartID].xNumTicks) == 'undefined') {
            that.chartOptions[chartID].xNumTicks = that.standardOptions.xaxis.ticks;
        }
        if(typeof(that.chartOptions[chartID].yNumTicks) == 'undefined') {
            that.chartOptions[chartID].yNumTicks = that.standardOptions.yaxis.ticks;
        }
        if(typeof(that.chartOptions[chartID].xTickLength) == 'undefined') {
            that.chartOptions[chartID].xTickLength = that.standardOptions.xaxis.tickLength;
        }
        if(typeof(that.chartOptions[chartID].yTickLength) == 'undefined') {
            that.chartOptions[chartID].yTickLength = that.standardOptions.yaxis.tickLength;
        }
        if(typeof(that.chartOptions[chartID].xLabelWidth) == 'undefined') {
            that.chartOptions[chartID].xLabelWidth = that.standardOptions.xaxis.labelWidth;
        }
        if(typeof(that.chartOptions[chartID].yLabelWidth) == 'undefined') {
            that.chartOptions[chartID].yLabelWidth = that.standardOptions.yaxis.labelWidth;
        }
        if(typeof(that.chartOptions[chartID].xLabelHeight) == 'undefined') {
            that.chartOptions[chartID].xLabelHeight = that.standardOptions.xaxis.labelHeight;
        }
        if(typeof(that.chartOptions[chartID].yLabelHeight) == 'undefined') {
            that.chartOptions[chartID].yLabelHeight = that.standardOptions.yaxis.labelHeight;
        }
        if(typeof(that.chartOptions[chartID].xFont) == 'undefined') {
            that.chartOptions[chartID].xFont = that.standardOptions.xaxis.font;
        }
        if(typeof(that.chartOptions[chartID].yFont) == 'undefined') {
            that.chartOptions[chartID].yFont = that.standardOptions.yaxis.font;
        }
        if(typeof(that.chartOptions[chartID].showLegend) == 'undefined') {
            that.chartOptions[chartID].showLegend = that.standardOptions.legend.show;
        }
        if(typeof(that.chartOptions[chartID].lineWidth) == 'undefined') {
            that.chartOptions[chartID].lineWidth = that.standardOptions.series.lines.lineWidth;
        }
        if(typeof(that.chartOptions[chartID].pointRadius) == 'undefined') {
            that.chartOptions[chartID].pointRadius = that.standardOptions.series.points.radius;
        }
        if(typeof(that.chartOptions[chartID].colors) == 'undefined') {
            that.chartOptions[chartID].colors = that.standardOptions.colors;
        }
        if(typeof(that.chartOptions[chartID].gridMarkingsColor) == 'undefined') {
            that.chartOptions[chartID].gridMarkingsColor = '#E2E2E2';
        }
        if(typeof(that.chartOptions[chartID].gridBorderColor) == 'undefined') {
            that.chartOptions[chartID].gridBorderColor = that.standardOptions.grid.borderColor;
        }
        if(typeof(that.chartOptions[chartID].gridBorderWidth) == 'undefined') {
            that.chartOptions[chartID].gridBorderWidth = that.standardOptions.grid.borderWidth;
        }
        if(typeof(that.chartOptions[chartID].gridBackgroundColor) == 'undefined') {
            that.chartOptions[chartID].gridBackgroundColor = that.standardOptions.grid.backgroundColor;
        }
    };
    
    that.deleteChart = function(chartID) {
        if(!(chartID in that.charts)) {
            return;
        }
        
        that.clearChart(chartID);
        
        delete that.charts[chartID];
        delete that.chartOptions[chartID];
        delete that.chartData[chartID];
        delete that.yAxesTypeMap[chartID];
    };
    
    that.clearChart=function(chartID) {
        that.removeDataTooltip(chartID);
        $('#'+chartID).html('');
        $('#'+chartID).unbind("plotclick");
        $('#'+chartID).unbind("plothover");
        that.chartOptions[chartID].format = 'none';
    };
    
    that.addChartOption=function(chartID, optionName, optionVal) {
        that.chartOptions[chartID][optionName] = optionVal;
    };

    that.addData = function(chartID, data) {
        that.chartData[chartID].push(data);
    };

    that.deleteData = function(chartID, data) {
        that.chartData[chartID] = [];
    };

    that.getDataSet = function(chartID) {
        return that.chartData[chartID];
    };
    
    that.chartLine = function(chartID) {
        that.clearChart(chartID);
        that.chartOptions[chartID].format = 'real';
        var dataSet = that.getDataSet(chartID);
        if(dataSet.length == 0) {
            return;
        }

        var options = that.formBasicChartOptions(chartID, 'real');
        var quarterlyFormat = that.chartOptions[chartID].quarterlyFormat;

        that.formProcessedDataSet(chartID);

        // If we don't support multiple axes, then put the first y-axes on the left!
        if(!that.chartOptions[chartID].allAxesOnRight) {
            options.yaxes[0].position = 'left';
        }
        
        // If we have quarterly data we want to display as quarterly, use our special
        // quarterly formatter ... otherwise use the default Flot tick generator wich
        // a custom formatter
        if(that.hasOnlyQuarterlyData(chartID) && quarterlyFormat != "month") {
            var xNumTicks = that.chartOptions[chartID].xNumTicks;
            options.xaxis.ticks = that.formQuarterlyDateTicks(chartID, dataSet, xNumTicks);
        }
        else {
            options.xaxis.tickFormatter = that.xAxisTimeFormatter;
        }

        // Form the tick formats for the y-axes.
        // We assume that data type of the first data sequence,
        // is the same as ALL the others
        var tickFormatter;
        for(type in that.yAxesTypeMap[chartID]) {
            tickFormatter = that.getNumberTickFormatter(type);
            axis = that.yAxesTypeMap[chartID][type];
            options.yaxes[axis - 1].tickFormatter = tickFormatter;
        }

        // Now prepare an array of data series, ready to push into flot
        var i, len, series = [], seriesCounter = 0;
        var fillColor;
        for(i=0, len=dataSet.length; i < len; i++){
            data = dataSet[i];
            fillColor = null;
            if(options.colors.length > seriesCounter) {
                fillColor = options.colors[seriesCounter];
            }
            series.push({label: data.label, data: data.processedData, yaxis: data.axis, 
                points: {fillColor: fillColor}});
            seriesCounter++;
        }

        that.drawChart(chartID, series, options);
        
        if(options.grid.hoverable) {
            that.resetDataTooltip(chartID);

            that.setupLineEvents(chartID);
        }
    };
    
    that.drawChart = function(chartID, series, options) {
        that.charts[chartID] = $.plot($('#'+chartID), series, options);
    };

    that.chartScatter = function(chartID) {
        that.clearChart(chartID);
        that.chartOptions[chartID].format = 'scatter';
        var dataSet = that.getDataSet(chartID)[0];
        var chartData = dataSet['scatter_data'];
        var calcMetaData = dataSet['calc_metadata'];
        var compCalcMetaData = dataSet['comp_calc_metadata'];
        
        var options = that.formBasicChartOptions(chartID, 'scatter');
        var quarterlyFormat = that.chartOptions[chartID].quarterlyFormat;
        
        var yTickFormatter = that.getNumberTickFormatter(calcMetaData['type']);
        options.yaxes[0].position = 'left';
        options.yaxis.autoscaleMargin = 0.05;
        options.yaxes[0].tickFormatter = yTickFormatter;

        var xTickFormatter = that.getNumberTickFormatter(compCalcMetaData['type']);
        options.xaxis.mode = null;
        options.xaxis.autoscaleMargin = 0.05;
        options.xaxis.tickFormatter = xTickFormatter;

        var i, len, x, y, series = [], dataItem;
        var outliers = that.chartOptions[chartID].outliers;

        if(outliers == 'false') {
            var xVals = [], yVals = [], xAvg, yAvg, xStats, yStats, xMin, xMax, yMin, yMax;
            for(i=0, len=chartData.length; i < len; i++){
                dataItem = chartData[i].data[0];
                xVals.push(dataItem[0]);
                yVals.push(dataItem[1]);
            }
            xStats = that.arrayStats(xVals);
            yStats = that.arrayStats(yVals);

            xMin = (-1.5 * xStats.stdDev) + xStats.average;
            xMax = (1.5 * xStats.stdDev) + xStats.average;
            yMin = (-1.5 * yStats.stdDev) + yStats.average;
            yMax = (1.5 * yStats.stdDev) + yStats.average;
        }

        // For scatter plots we give each point the same color
        for(i=0, len=chartData.length; i < len; i++){
            dataItem = chartData[i].data[0];
            if(outliers == 'false') {
                x = dataItem[0];
                y = dataItem[1];
                if(x < xMin || x > xMax) {
                    continue;
                }
                if(y < yMin || y > yMax) {
                    continue;
                }
            }
            series.push({label: chartData[i].label, data: chartData[i].data, color: '#000', 
                points: {fillColor: '#000'}});
        }

        that.charts[chartID] = $.plot($('#'+chartID), series, options);
        that.setupScatterEvents(chartID);
    };

    // Process the data
    that.formProcessedDataSet = function(chartID) {
        var dataSet = that.getDataSet(chartID);
        
        var startTime, data, processedData;
        var i, len, j, jLen;
        for(i=0, len=dataSet.length; i < len; i++){
            dataSet[i].processedData = $.extend(true, [], dataSet[i].raw_data);
        }
        
        var currentAxis = 1, numYAxes;
        that.yAxesTypeMap[chartID] = {};
        for (i=0, len = dataSet.length; i < len; i++){
            data = dataSet[i];
            if (typeof that.yAxesTypeMap[chartID][data['type']] != 'undefined') {
                dataSet[i].axis = that.yAxesTypeMap[chartID][data['type']];
            }
            else {
                dataSet[i].axis = currentAxis;
                that.yAxesTypeMap[chartID][data['type']] = currentAxis;
                currentAxis++;
            }
        }

        numYAxes = currentAxis;
    };

    that.formBasicChartOptions = function(chartID, chartType) {
        var options = $.extend(true, {}, that.standardOptions);
        var chartOptions = $.extend(true, {}, that.chartOptions[chartID]);
        
        // If clickURL is defined or we're on a scatter chart set grid to clickable
        if(chartOptions.clickURL || chartType == 'scatter') {
            options.grid.clickable = true;
        }

        // If we're on a scatter chart turn off crosshair
        if(chartType == 'scatter') {
            options.crosshair = {mode: null};
        }
        
        options.xaxis.ticks = chartOptions.xNumTicks;
        if(chartOptions.xNumTicks == 0) {
            options.grid.labelMargin = 0;
            options.grid.axisMargin = 0;
        }
        options.yaxis.ticks = chartOptions.yNumTicks;
        if(chartOptions.yNumTicks == 0) {
            options.grid.labelMargin = 0;
            options.grid.axisMargin = 0;
        }
        options.xaxis.tickLength = chartOptions.xTickLength;
        options.yaxis.tickLength = chartOptions.yTickLength;
        options.xaxis.labelWidth = chartOptions.xLabelWidth;
        options.yaxis.labelWidth = chartOptions.yLabelWidth;
        options.xaxis.labelHeight = chartOptions.xLabelHeight;
        options.yaxis.labelHeight = chartOptions.yLabelHeight;
        options.xaxis.font = chartOptions.xFont;
        options.yaxis.font = chartOptions.xFont;
        options.legend.show = chartOptions.showLegend;
        options.series.lines.lineWidth = chartOptions.lineWidth;
        if(chartOptions.pointRadius == 0) {
            options.series.points.show = false;
        }
        
        if(chartOptions.recessions == 'true'){
            options.grid.markings = function(){
                var markings = [];
                var totalPoints = that.recessionCycles.length;
                
                for(var ctr=0; ctr < totalPoints; ctr++){
                    markings.push({
                        xaxis: {
                            from: that.recessionCycles[ctr][0], 
                            to: that.recessionCycles[ctr][1]
                        }, 
                        color:chartOptions.gridMarkingsColor
                    });
                }
                return markings;
            }();
        }
        
        options.series.points.radius = chartOptions.pointRadius;
        options.colors = chartOptions.colors;
        options.grid.backgroundColor = chartOptions.gridBackgroundColor;
        options.grid.borderColor = chartOptions.gridBorderColor;
        options.grid.borderWidth = chartOptions.gridBorderWidth;
        options.grid.markingsColor = chartOptions.gridMarkingsColor;
        
        return options;
    };

    that.formQuarterlyDateTicks = function(chartID, dataSet, numTicks) {
        var i, len, j, jLen, uniqueTimeMap = Object();
        for(i=0, len=dataSet.length; i < len; i++){
            for(j=0, jLen = dataSet[i].processedData.length; j < jLen; j++) {
                uniqueTimeMap[dataSet[i]['processedData'][j][0]] = 1;
            }
        }

        var uniqueTimeSequence = [];
        // obj iteration
        for(i in uniqueTimeMap) {
            uniqueTimeSequence.push(parseInt(i, 10));
        }
        
        uniqueTimeSequence.sort(function(a,b){return a - b;});

        var tickInterval, tickEnd;
        if(uniqueTimeSequence.length <= numTicks) {
            tickInterval = 1;
            tickEnd = uniqueTimeSequence.length - 1;
        }
        else {
            tickInterval = Math.ceil(uniqueTimeSequence.length / numTicks);
            tickEnd = Math.ceil(uniqueTimeSequence.length - (tickInterval / 3));
            tickInterval = Math.ceil(tickEnd / numTicks);
        }
        
        var quarterlyFormat = that.chartOptions[chartID].quarterlyFormat;
        var dateFormatter = that.getDateTickFormatter("quarterly", quarterlyFormat);
        
        var ticks = [];
        var numItems = 1;
        var itemCount = 1;
        for(i=0, len=uniqueTimeSequence.length; i < len; i++){
            if(itemCount > tickEnd) {
                break;
            }
            if(numItems == tickInterval || itemCount == 1) {
                numItems = 1;
                ticks.push([uniqueTimeSequence[i], dateFormatter(uniqueTimeSequence[i])]);
            }
            else {
                numItems += 1;
            }
            itemCount++;
        }
        return ticks;
    };

    that.hasOnlyQuarterlyData = function(chartID) {
        var dataSet = that.getDataSet(chartID);
        var i, len, j, jLen;
        for(i=0, len=dataSet.length; i < len; i++) {
            if(dataSet[i].frequency != "quarterly") {
                return false;
            }
        }
        return true;
    };

    that.setupLineEvents = function(chartID) {
        if(that.chartOptions[chartID].clickURL) {
            $('#'+chartID).bind("plotclick", that.lineClickHandler);
        }

        $('#'+chartID).bind("plothover", function (event, pos, item) {
            var hoverFunc = function() {
                that.lineHoverHandler(chartID, pos, item);
            };
            if(!that.updateTooltipTimeout) {
                that.updateTooltipTimeout = setTimeout(hoverFunc, 33);
            }
        });

        $('#'+chartID).bind("mouseout", function(e) {
            if(!$(e.relatedTarget).hasClass('ychartsTooltipPoint') &&
                !$(e.relatedTarget).hasClass('ychartsTooltipBox')) {
                // Cancel the timeout if it's there
                if(that.updateTooltipTimeout) {
                    clearTimeout(that.updateTooltipTimeout);
                    that.updateTooltipTimeout = null;
                }

                // Remove the tooltip if it's there
                that.resetDataTooltip(chartID);

                // Unhighlight any highlighted chart points
                if(typeof(that.charts[chartID]) != 'undefined') {
                    that.charts[chartID].unhighlight();
                }
            }
        });
    };

    that.lineClickHandler= function(event, pos, item) {
        var chartID = $(this).attr('id');
        if(that.chartOptions[chartID].clickURL) {
            window.location = that.chartOptions[chartID].clickURL;
        }
    };

    that.lineHoverHandler= function(chartID, pos, item) {
        that.updateTooltipTimeout = null;

        var xPos = pos.x;
        var yPos = pos.y;
        var xPagePos = pos.pageX;
        var yPagePos = pos.pageY;

        var dataSet = that.getDataSet(chartID);
        var dataSeries;
        var numItems = 0;

        // For each series, find the closest point at or before the mouse's x position
        var closestXMap = new Object();
        var lastXMap = new Object();
        var i, len, j, jLen, k;
        
        for(i=0, len=dataSet.length; i < len; i++){
            i = parseInt(i, 10);
            dataSeries = dataSet[i].processedData;
            for(j=0, jLen=dataSeries.length; j < jLen; j++){
                j = parseInt(j, 10);
                // look for the first data point that's greater than or 
                // equal to our x position and go one back
                if(dataSeries[j][0] >= xPos) {
                    // If we find a point greater than our mouse position,
                    // we find the first previous point that's non-NULL
                    if(dataSeries[j][0] > xPos) {
                        for(k=j-1; k >= 0; k--) {
                            if(dataSeries[k][1] != null) {
                                break;
                            }
                        }
                    }
                    else {
                        k = j;
                    }
                    
                    // If we were able to find a valid x position, save it!
                    if(k >= 0) {
                        closestXMap[i] = k;
                        numItems++;
                    }
                    break;
                }
                
                // If we're at the end of the series, save the last point!
                if((j + 1) == dataSeries.length && dataSeries[j][1] != null) {
                    closestXMap[i] = j;
                    numItems++;
                    break;
                }
            }
        }

        // If there are no items to put in the tooltip, clear the current tooltip and stop
        if(numItems == 0) {
            that.resetDataTooltip(chartID);
            return;
        }

        // Unhighlight all points on the chart
        that.charts[chartID].unhighlight();

        var tooltipData = [];
        var data, dataPoint, dataFrequency;
        var quarterlyFormat = that.chartOptions[chartID].quarterlyFormat;
        var x, xFormat, xFormatter, xFormatted;
        var y, yFormat, yFormatter, yFormatted;

        for(i=0, len=dataSet.length; i < len; i++){
            i = parseInt(i, 10);

            var tooltipDataItem = {};

            // If the data series does not have a point to display in the tooltip, skip it
            // just store the label and move on !!!
            if(typeof closestXMap[i] == "undefined") {
                tooltipDataItem["label"] = that.formLabel(dataSet[i].object_label, 
                    dataSet[i].object_short_label, dataSet[i].calc_label, dataSet[i].calc_short_label);
                tooltipDataItem["short_label"] = that.formShortLabel(dataSet[i].object_label,
                    dataSet[i].object_short_label, dataSet[i].calc_label, dataSet[i].calc_short_label);
                tooltipDataItem["val1"] = '';
                tooltipDataItem["val2"] = '';
                tooltipDataItem["series_id"] = dataSet[i].series_id;
                tooltipData.push(tooltipDataItem);
                continue;
            }
            
            // Highlight the point on the chart we're hovering off
            that.charts[chartID].highlight(i, closestXMap[i]);
            
            data = dataSet[i];
            dataPoint = data.processedData[closestXMap[i]];
            dataFrequency = data.frequency;

            // Add the labels of the data
            tooltipDataItem["label"] = that.formLabel(dataSet[i].object_label, 
                dataSet[i].object_short_label, dataSet[i].calc_label, dataSet[i].calc_short_label);
            tooltipDataItem["short_label"] = that.formShortLabel(dataSet[i].object_label,
                dataSet[i].object_short_label, dataSet[i].calc_label, dataSet[i].calc_short_label);

            // Add the formatted x value
            x = dataPoint[0];
            // If the latest value is greater than the last formatted, display tooltip date as daily
            if(data.last_formatted != null && x > data.last_formatted) {
                dataFrequency = 'daily';
            }
            xFormatter = that.getDateFormatter(dataFrequency, quarterlyFormat);
            xFormatted = xFormatter(x);
            tooltipDataItem["val1"] = xFormatted;

            // Add the formatted y value
            y = dataPoint[1];
            if(that.chartOptions[chartID].format == 'indexed') {
                yFormatter = that.percentFormatter;
            }
            else {
                yFormatter = that.getNumberFormatter(data.type);
            }
            yFormatted = yFormatter(y);
            tooltipDataItem["val2"] = yFormatted;
            
            // Add delete key
            if(typeof data["series_id"] != "undefined") {
                tooltipDataItem["series_id"] = data["series_id"];
            }
            else {
                tooltipDataItem["series_id"] = "";
            }
            tooltipData.push(tooltipDataItem);
        }

        that.removeDataTooltip(chartID);
        that.showDataTooltip(chartID, xPagePos, yPagePos, "", tooltipData);
    };
    
    that.setupScatterEvents = function(chartID) {
        $('#'+chartID).bind("plotclick", that.scatterClickHandler);
        
        $('#'+chartID).bind("plothover", function (event, pos, item) {
            that.scatterHoverHandler(chartID, pos, item);
        });

        $('#'+chartID).bind("mouseout", function(e) {
            if(!$(e.relatedTarget).hasClass('ychartsTooltipPoint')) {
                $('.ychartsTooltipWrap').remove();
                $('.statRow').removeClass('hover');
                if(typeof(that.charts[chartID]) != 'undefined') {
                    that.charts[chartID].unhighlight();
                }
            }
        });
    };

    that.scatterClickHandler= function(event, pos, item) {
        var chartID = $(this).attr('id');
        if(item) {
            var dataSet = that.getDataSet(chartID)[0];
            var x = item.datapoint[0];
            var y = item.datapoint[1];
            for(var i=0, len=dataSet.scatter_data.length; i < len; i++){
                scatterTuple = dataSet.scatter_data[i].data[0];
                if(x == scatterTuple[0] && y == scatterTuple[1]) {
                    var url = dataSet.scatter_data[i].url;
                    // Hack to propogate outlier state
                    url += '#outliers=' + that.chartOptions[chartID].outliers;
                    window.location = url;
                }
            }
        }
        else if(that.chartOptions[chartID].clickURL) {
            if(that.chartOptions[chartID].clickDestination == 'currentWindow') {
                window.location = that.chartOptions[chartID].clickURL;
            }
            else {
                window.open(that.chartOptions[chartID].clickURL);
            }
        }
    };

    that.scatterHoverHandler = function(chartID, pos, item) {
        if(item) {
            var xPagePos = item.pageX;
            var yPagePos = item.pageY;

            var dataSet = that.getDataSet(chartID)[0];
            var calcMetaData = dataSet['calc_metadata'];
            var compCalcMetaData = dataSet['comp_calc_metadata'];
            var x = item.datapoint[0];
            var y = item.datapoint[1];
            var label = item.series.label;
            var quarterlyFormat = that.chartOptions[chartID].quarterlyFormat;
            var fmtX = that.getNumberFormatter(compCalcMetaData['type'])(x);
            var colorX = that.chartOptions[chartID].colors[1];
            var fmtY = that.getNumberFormatter(calcMetaData['type'])(y);
            var colorY = that.chartOptions[chartID].colors[0];

            var tooltipTitle = item.series.label;
            var tooltipData = [];
            
            var tooltipDataItem = {}, tooltipDataItem2 = {};
            tooltipDataItem["label"] =  calcMetaData['label'];
            tooltipDataItem["short_label"] =  calcMetaData['short_label'];
            tooltipDataItem["val1"] = "";
            tooltipDataItem["val2"] = fmtY;
            tooltipData.push(tooltipDataItem);
            
            tooltipDataItem2["label"] = compCalcMetaData['label'];
            tooltipDataItem2["short_label"] = compCalcMetaData['short_label'];
            tooltipDataItem2["val1"] = "";
            tooltipDataItem2["val2"] = fmtX;
            tooltipData.push(tooltipDataItem2);
            
            that.removeDataTooltip(chartID);
            that.showDataTooltip(chartID, xPagePos, yPagePos, tooltipTitle, tooltipData);
        } else {
            that.removeDataTooltip(chartID);
            that.charts[chartID].unhighlight();
            $('.statRow').removeClass('hover');
        }
    };

    that.formShortLabel = function(objectLabel, objectShortLabel, calcLabel, calcShortLabel) {
        var labelItems = [objectShortLabel, calcShortLabel];
        return labelItems.join(' ');
    };
    
    that.formLabel = function(objectLabel, objectShortLabel, calcLabel, calcShortLabel) {
        var labelItems;
        if(calcLabel == '' && calcShortLabel == '') {
            labelItems = [objectLabel, calcLabel];
        }
        else {
            labelItems = [objectShortLabel, calcLabel];
        }
        return labelItems.join(' ');
    };

    that.showDataTooltip = function(chartID, xPos, yPos, tooltipTitle, tooltipData) {
        var tooltipStyle = that.chartOptions[chartID].tooltipStyle;
        if(tooltipStyle == "box") {
            that.showDataTooltipBox(chartID, xPos, yPos, tooltipTitle, tooltipData);
        }
        else if(tooltipStyle == "pointBox") {
            that.showDataTooltipPointBox(chartID, xPos, yPos, tooltipTitle, tooltipData);
        }
        else if(tooltipStyle == "bar") {
            that.showDataTooltipBar(chartID, xPos, yPos, tooltipTitle, tooltipData);
        }
    };
    
    that.showDataTooltipBox = function(chartID, xPos, yPos, tooltipTitle, tooltipData) {
        var contents = that.showDataTooltipBaseBoxContents(chartID, tooltipTitle, tooltipData);
        that.showDataTooltipBaseBox(chartID, xPos, yPos, contents, false);
    };

    that.showDataTooltipPointBox = function(chartID, xPos, yPos, tooltipTitle, tooltipData) {
        var contents = that.showDataTooltipBaseBoxContents(chartID, tooltipTitle, tooltipData);
        that.showDataTooltipBaseBox(chartID, xPos, yPos, contents, true);
    };

    that.showDataTooltipBaseBoxContents = function(chartID, tooltipTitle, tooltipData) {
        // Form the content of the tooltip
        var contents = '<table class="ychartsTooltipData">';

        // Add an overall title if we have one
        if(tooltipTitle != '') {
            contents += '<tr><td class="ychartsTitle" colspan="2">' + tooltipTitle + '</td></tr>';
        }
        
        var lastVal1 = '', tooltipDataItem;
        for(i=0, len=tooltipData.length; i < len; i++) {
            tooltipDataItem = tooltipData[i];
            if(lastVal1 != tooltipDataItem.val1) {
                contents += '<tr><td class="ychartsLabel" colspan="2">' + tooltipDataItem.val1 + '</td></tr>';
                lastVal1 = tooltipDataItem.val1;
            }

            var colorVal2 = that.chartOptions[chartID].colors[i];
            if(tooltipDataItem.val2 != '') {
                contents += '<tr>';
                contents += '<td class="ychartsLabel" style="color:'+colorVal2+'">' + 
                    tooltipDataItem.short_label + '</td>';
                contents += '<td class="ychartsData" style="color:'+colorVal2+'">' + 
                    tooltipDataItem.val2 + '</td>';
                contents += '</tr>';
            }
        }
        contents += '</table>';
        return contents;
    };

    that.showDataTooltipBaseBox = function(chartID, xPos, yPos, contents, includeTipPoint) {
        // Form the actual tooltip and display it
        var tip = $('<div id="' + '" class="ychartsTooltipWrap">' +
           '<div class="ychartsTooltipBox">' + contents + '</div>' + 
           '</div>').appendTo("body");

        var tipWidth = tip.outerWidth();
        var tipHeight = tip.outerHeight();
        var tipPoint, tipPointWidth, tipPointHeight;
        var pointRadius = that.chartOptions[chartID].pointRadius;
        
        if(includeTipPoint) {
            tipPoint = $('<div class="ychartsTooltipPoint"></div>');
            tipPointWidth = 14;
            tipPointHeight = 18;
        }
        else {
            tipPoint = null;
            tipPointWidth = 0;
            tipPointHeight = 0;
        }

        var x = xPos;
        var y = yPos - 5;

        // If a body is given a width in css, we must adjust for the fact that the window 
        // can be larger than the body since the tooltip is positioned relative to the body.
        var $body = $("body");
        var windowWidth = $(window).width();
        var bodyWidth = $body.width();
        var bodyAdjustment = 0;  //< default to 0 unless the css styles on body indicates it needs an offset
        if($body.css('position') == 'relative' && bodyWidth < windowWidth){
           var bodyOffset = bodyWidth - windowWidth;
           bodyAdjustment = Math.floor(bodyOffset / 2);
           x = x + bodyAdjustment;
        }
        
        // Form the left and top position of box and point.
        var tipTop = y - (tipHeight + tipPointHeight) - pointRadius;
        var tipLeft = x - (tipWidth / 2);
        var tipRight = tipLeft + tipWidth;

        var plotOffset = that.charts[chartID].offset();
        var plotLeft = plotOffset.left + bodyAdjustment;
        var plotRight = plotLeft + that.charts[chartID].width();
        if(tipLeft < plotLeft) {
            tipLeft = Math.floor(plotLeft - (tipPointWidth / 2));
        }
        else if(tipRight > plotRight) {
            tipLeft = Math.floor(plotRight - tipWidth + (tipPointWidth / 2));
        }

        var tipPointLeft = Math.floor(x - tipLeft - tipPointWidth / 2);
        
        tip.css({
            top: tipTop,
            left: tipLeft,
            position: 'absolute',
            display: 'none'
        });

        // If we are over the tooltip, remove the tooltip so that we can get to the
        // chart points underneath it.
        tip.mouseover(function() {
            setTimeout(function() {tip.hide();}, 150);
        });

        if(includeTipPoint) {
            tipPoint.css({
               marginLeft:  tipPointLeft
            });
            tipPoint.appendTo(tip);
            
            // However, if the mouse is over tooltip point, stop the mouseover
            // event propagation so the tooltip itself does not get this event and
            // remove itself.
            tipPoint.mouseover(function(e) {
                e.stopPropagation();
            });
        }

        tip.show();
    };

    that.showDataTooltipBar = function(chartID, xPos, yPos, tooltipTitle, tooltipData) {
        var contents = that.showDataTooltipBarContents(chartID, tooltipTitle, tooltipData);
        var barID = that.chartOptions[chartID].tooltipBar;
        $('#' + barID).html(contents);
    };
    
    that.showDataTooltipBarContents = function(chartID, tooltipTitle, tooltipData) {
        var contents = '', label, val1, val2;
        contents += '<div id="ychartsBigChartKeySect1" class="ychartsBigChartKeySect">';
        for(i=0, len=tooltipData.length; i < len; i++) {
            if(i == 3) {
                contents += '</div><div id="ychartsBigChartKeySect2" class="ychartsBigChartKeySect">';
            }
            label = (tooltipData[i].label != '' ? tooltipData[i].label : '&nbsp;');
            
            // check if a textRuler was instantiated/included in init, if so use it to measure label
            // else go old school and just check char length and truncate using slice
            if(that.textRuler){
                label = that.textRuler.truncate(label, that.tooltipStyles.styles);
            } else {
                if(label.length > 27) {
                    label = label.slice(0, 27) + '...';
                }
            }
            val1 = (tooltipData[i].val1 != '' ? tooltipData[i].val1 : '&nbsp;');
            val2 = (tooltipData[i].val2 != '' ? tooltipData[i].val2 : '&nbsp;');
            contents += '<div class="ychartsKeyLineBox ychartsKeyLine' + String(i + 1) + '">';
            contents += '<div class="ychartsKeyLineCo" style="'+that.tooltipStyles.styleString+'">' + label + '</div>';
            contents += '<div class="ychartsKeyLineDate">' + val1 + '</div>';
            contents += '<div class="ychartsKeyLineVal">' + val2 + '</div>';
            if(i > 0) {
                contents += '<a class="closeX" href="#" data-series-id="' +  
                   tooltipData[i]["series_id"] + '"></a>';
            }
            contents += '</div>';
        }
        contents += '</div>';
        if(tooltipData.length <= 3) {
            contents += '<div id="ychartsBigChartKeySect2" class="ychartsBigChartKeySect">&nbsp;</div>';
        }
        return contents;
    };
    
    that.resetDataTooltip = function(chartID) {
        var tooltipStyle = that.chartOptions[chartID].tooltipStyle;
        if(tooltipStyle == "box") {
            that.removeDataTooltipBox(chartID);
        }
        else if(tooltipStyle == "pointBox") {
            that.removeDataTooltipPointBox(chartID);
        }
        else if(tooltipStyle == "bar") {
            that.resetDataTooltipBar(chartID);
        }
    };
    
    that.resetDataTooltipBar = function(chartID) {
        var barID = that.chartOptions[chartID].tooltipBar;
        $('#' + barID).html('');

        // First check to see if the data set is not empty,
        // if is not, reset the bar with the last item of each data series
        var dataSet = that.getDataSet(chartID);
        var maxLength = 0;
        for (i=0, len = dataSet.length; i < len; i++) {
            if(dataSet[i].processedData.length > maxLength) {
                maxLength = dataSet[i].processedData;
            }
        }
        if(maxLength == 0) {
            return;
        }

        var chart = that.charts[chartID];
        if(chart) {
            var x = chart.getXAxes();
            var y = chart.getYAxes();
            var pos = {
                x : x.max,
                y : y.max,
                pageX : 0,
                pageY : 0
            };
            that.lineHoverHandler(chartID, pos);
        }
    };

    that.removeDataTooltip = function(chartID) {
        var tooltipStyle = that.chartOptions[chartID].tooltipStyle;
        if(tooltipStyle == "box") {
            that.removeDataTooltipBox(chartID);
        }
        else if(tooltipStyle == "pointBox") {
            that.removeDataTooltipPointBox(chartID);
        }
        else if(tooltipStyle == "bar") {
            that.removeDataTooltipBar(chartID);
        }
    };
    
    that.removeDataTooltipBox = function(chartID) {
        $('.ychartsTooltipWrap').remove();
    };

    that.removeDataTooltipPointBox = function(chartID) {
        $('.ychartsTooltipWrap').remove();
    };

    that.removeDataTooltipBar = function(chartID) {
        var barID = that.chartOptions[chartID].tooltipBar;
        $('#' + barID).html('');
    };

    that.getNumberFormatter = function(type) {
        return that.numberFormatter.getFormatter(type);
    };
    
    that.getNumberTickFormatter = function(type) {
        var formatter = that.numberFormatter.getFormatter(type);
        var tickFormatter = function(val, axis) {
            return formatter(val);
        };
        return tickFormatter;
    };

    that.getDateFormatter = function(type, quarterlyFormat) {
        return that.dateFormatter.getFormatter(type, quarterlyFormat);
    };
    
    that.getDateTickFormatter = function(type, quarterlyFormat) {
        var formatter = that.dateFormatter.getFormatter(type, quarterlyFormat);
        var tickFormatter = function(val, axis) {
            return formatter(val);
        };
        return tickFormatter;
    };
    
    that.xAxisTimeFormatter = function(val, axis) {
        var d = new Date(val);

        var t = axis.tickSize[0] * that.timeUnitSize[axis.tickSize[1]];
        var span = axis.max - axis.min;
        var suffix = " %p";
        
        if (t < that.timeUnitSize.minute)
            fmt = "%h:%M:%S" + suffix;
        else if (t < that.timeUnitSize.day) {
            if (span < 2 * that.timeUnitSize.day)
                fmt = "%h:%M" + suffix;
            else
                fmt = "%b %d %h:%M" + suffix;
        }
        else if (t < that.timeUnitSize.month) {
            fmt = "%b %d";
        }
        else if (t < that.timeUnitSize.year) {
            if(span < that.timeUnitSize.year) {
                fmt = "%b";
            }
            fmt = "%b %y";
        }
        else {
            fmt = "%y";
        }

        return $.plot.formatDate(d, fmt);
    };

    that.arrayStats= function(a) {
        var i, len, average = 0, variance = 0;
        for(i=0, len=a.length; i < len; i++){
            average += a[i];
        }
        average = average/a.length;
        
        for(i=0, len=a.length; i < len; i++){
            variance += Math.pow(a[i] - average, 2);
        }
        variance = variance/(a.length-1);
        stdDev = Math.sqrt(variance);
        min = Math.min.apply({},a);
        max = Math.max.apply({},a);
        return {
            'average' : average, 
            'min' : min, 
            'max' : max, 
            'variance' : variance, 
            'stdDev' : stdDev
        };
    };

    that.arrayAverage = function(a) {
        var i, len, average = 0, variance = 0;
        for(i=0, len=a.length; i < len; i++){
            average += a[i];
        }
        average = average/a.length;
        return average;
    };
    
    that.arrayMedian = function(a) {
        if (a.length == 0) {
            return null;
        }
        a.sort(function (a,b){return a - b;});
        var mid = Math.floor(a.length / 2);
        if ((a.length % 2) == 1) {
            return a[mid];
        }
        else {
            return (a[mid - 1] + a[mid]) / 2;
        }
    };

    that.arrayMin = function(a) {
        return Math.min.apply({},a);
    };
    
    that.arrayMax = function(a) {
        return Math.max.apply({},a);
    };

    return that;
}();

