/* 
    PlotKit Layout
    --------------
    
    Handles laying out data on to a virtual canvas square canvas between 0.0 
    and 1.0. If you want to add new chart/plot types such as point plots,
    you need to add them here.
    
    Copyright
    ---------
    Copyright 2005,2006 (c) Alastair Tse <alastair^liquidx.net>
    For use under the BSD license. <http://www.liquidx.net/plotkit>
    
*/

try {    
    if (typeof(PlotKit.Base) == 'undefined')
    {
        throw ""
    }
} 
catch (e) {    
    throw "PlotKit.Layout depends on MochiKit.{Base,Color,DOM,Format} and PlotKit.Base"
}

// --------------------------------------------------------------------
// Start of Layout definition
// --------------------------------------------------------------------

if (typeof(PlotKit.Layout) == 'undefined') {
    PlotKit.Layout = {};
}

PlotKit.Layout.NAME = "PlotKit.Layout";
PlotKit.Layout.VERSION = PlotKit.VERSION;

PlotKit.Layout.__repr__ = function() {
    return "[" + this.NAME + " " + this.VERSION + "]";
};

PlotKit.Layout.toString = function() {
    return this.__repr__();
}

PlotKit.Layout.valid_styles = ["bar", "line", "pie", "point"];

// --------------------------------------------------------------------
// Start of Layout definition
// --------------------------------------------------------------------

PlotKit.Layout = function(style, options) {
    
    this.options = {
        "barWidthFillFraction": 0.75,
        "xOriginIsZero": true,
        "yOriginIsZero": true,
        "xAxis": null, // [xmin, xmax]
        "yAxis": null, // [ymin, ymax]
        "xTicks": null, // [{label: "somelabel", v: value}, ..] (label opt.)
        "yTicks": null, // [{label: "somelabel", v: value}, ..] (label opt.)
        "xNumberOfTicks": 10,
        "yNumberOfTicks": 5,
        "xTickPrecision": 1,
        "yTickPrecision": 3,
        "pieRadius": 0.4
    };

    // valid external options : TODO: input verification
    this.style = style; 
    MochiKit.Base.update(this.options, options ? options : {});

    // externally visible states
    // overriden if xAxis and yAxis are set in options
    if (!MochiKit.Base.isUndefinedOrNull(this.options.xAxis)) {
        this.minxval = this.options.xAxis[0];
        this.maxxval = this.options.xAxis[1];
        this.xscale = this.maxxval - this.minxval; 
    }
    else {
        this.minxval = 0;
        this.maxxval = null;
        this.xscale = null; // val -> pos factor (eg, xval * xscale = xpos)
    }

    if (!MochiKit.Base.isUndefinedOrNull(this.options.yAxis)) {
        this.minyval = this.options.yAxis[0];
        this.maxyval = this.options.yAxis[1];
        this.yscale = this.maxyval - this.maxymin;
    }
    else {
        this.minyval = 0;
        this.maxyval = null;
        this.yscale = null;
    }

    this.bars = new Array();   // array of bars to plot for bar charts
    this.points = new Array(); // array of points to plot for line plots
    this.slices = new Array(); // array of slices to draw for pie charts

    this.xticks = new Array();
    this.yticks = new Array();

    // internal states
    this.datasets = new Array();
    this.minxdelta = 0;
    this.xrange = 1;
    this.yrange = 1;

    this.hitTestCache = {x2maxy: null};
    
};

// --------------------------------------------------------------------
// Dataset Manipulation
// --------------------------------------------------------------------


PlotKit.Layout.prototype.addDataset = function(setname, set_xy) {
    this.datasets[setname] = set_xy;
};

PlotKit.Layout.prototype.removeDataset = function(setname, set_xy) {
    this.datasets[setname] = null;
};

PlotKit.Layout.prototype.addDatasetFromTable = function(name, tableElement, xcol, ycol) {
	var isNil = MochiKit.Base.isUndefinedOrNull;
	var scrapeText = MochiKit.DOM.scrapeText;
	var strip = MochiKit.Format.strip;
	
	if (isNil(xcol))
		xcol = 0;
	if (isNil(ycol))
		ycol = 1;
        
    var rows = tableElement.tBodies[0].rows;
    var data = new Array();
    if (!isNil(rows)) {
        for (var i = 0; i < rows.length; i++) {
            data.push([parseFloat(strip(scrapeText(rows[i].cells[xcol]))),
                       parseFloat(strip(scrapeText(rows[i].cells[ycol])))]);
        }
        this.addDataset(name, data);
        return true;
    }
    return false;
};

// --------------------------------------------------------------------
// Evaluates the layout for the current data and style.
// --------------------------------------------------------------------

PlotKit.Layout.prototype.evaluate = function() {
    this._evaluateLimits();
    this._evaluateScales();
    if (this.style == "bar") {
        this._evaluateBarCharts();
        this._evaluateBarTicks();
    }
    else if (this.style == "line") {
        this._evaluateLineCharts();
        this._evaluateLineTicks();
    }
    else if (this.style == "pie") {
        this._evaluatePieCharts();
        this._evaluatePieTicks();
    }
};



// Given the fractional x, y positions, report the corresponding
// x, y values.
PlotKit.Layout.prototype.hitTest = function(x, y) {
    // TODO: make this more efficient with better datastructures
    //       for this.bars, this.points and this.slices

    var f = MochiKit.Format.twoDigitFloat;

    if ((this.style == "bar") && this.bars && (this.bars.length > 0)) {
        for (var i = 0; i < this.bars.length; i++) {
            var bar = this.bars[i];
            if ((x >= bar.x) && (x <= bar.x + bar.w) 
                && (y >= bar.y) && (y - bar.y <= bar.h))
                return bar;
        }
    }

    else if (this.style == "line") {
        if (this.hitTestCache.x2maxy == null) {
            this._regenerateHitTestCache();
        }

        // 1. find the xvalues that equal or closest to the give x
        var xval = x / this.xscale;
        var xvalues = this.hitTestCache.xvalues;
        var xbefore = null;
        var xafter = null;

        for (var i = 1; i < xvalues.length; i++) {
            if (xvalues[i] > xval) {
                xbefore = xvalues[i-1];
                xafter = xvalues[i];
                break;
            }
        }

        if ((xbefore != null)) {
            var ybefore = this.hitTestCache.x2maxy[xbefore];
            var yafter = this.hitTestCache.x2maxy[xafter];
            var yval = (1.0 - y)/this.yscale;

            // interpolate whether we will fall inside or outside
            var gradient = (yafter - ybefore) / (xafter - xbefore);
            var projmaxy = ybefore + gradient * (xval - xbefore);
            if (projmaxy >= yval) {
                // inside the highest curve (roughly)
                var obj = {xval: xval, yval: yval,
                           xafter: xafter, yafter: yafter,
                           xbefore: xbefore, ybefore: ybefore,
                           yprojected: projmaxy
                };
                return obj;
            }
        }
    }

    else if (this.style == "pie") {
        var dist = Math.sqrt((y-0.5)*(y-0.5) + (x-0.5)*(x-0.5));
        if (dist > this.options.pieRadius)
            return null;

        // TODO: actually doesn't work if we don't know how the Canvas
        //       lays it out, need to fix!
        var angle = Math.atan2(y - 0.5, x - 0.5) - Math.PI/2;
        for (var i = 0; i < this.slices.length; i++) {
            var slice = this.slices[i];
            if (slice.startAngle < angle && slice.endAngle >= angle)
                return slice;
        }
    }

    return null;
};

// Reports valid position rectangle for X value (only valid for bar charts)
PlotKit.Layout.prototype.rectForX = function(x) {
    return null;
};

// Reports valid angles through which X value encloses (only valid for pie charts)
PlotKit.Layout.prototype.angleRangeForX = function(x) {
    return null;
};

// --------------------------------------------------------------------
// START Internal Functions
// --------------------------------------------------------------------

PlotKit.Layout.prototype._evaluateLimits = function() {
    // take all values from all datasets and find max and min
    var map = MochiKit.Base.map;
    var items = MochiKit.Base.items;
    var itemgetter = MochiKit.Base.itemgetter;
    var collapse = PlotKit.Base.collapse;
    var listMin = MochiKit.Base.listMin;
    var listMax = MochiKit.Base.listMax;
    var isNil = MochiKit.Base.isUndefinedOrNull;

    var all = collapse(map(itemgetter(1), items(this.datasets)));

    if (isNil(this.options.xAxis)) {
        if (this.options.xOriginIsZero)
            this.minxval = 0;
        else
            this.minxval = listMin(map(parseFloat, map(itemgetter(0), all)));
    }
    
    if (isNil(this.options.yAxis)) {
        if (this.options.yOriginIsZero)
            this.minyval = 0;
        else
            this.minyval = listMin(map(parseFloat, map(itemgetter(1), all)));
    }

    this.maxxval = listMax(map(parseFloat, map(itemgetter(0), all)));
    this.maxyval = listMax(map(parseFloat, map(itemgetter(1), all)));
};

PlotKit.Layout.prototype._evaluateScales = function() {
    var isNil = MochiKit.Base.isUndefinedOrNull;

    this.xrange = this.maxxval - this.minxval;
    if (this.xrange == 0)
        this.xscale = 1.0;
    else
        this.xscale = 1/this.xrange;

    this.yrange = this.maxyval - this.minyval;
    if (this.yrange == 0)
        this.yscale = 1.0;
    else
        this.yscale = 1/this.yrange;
};

PlotKit.Layout.prototype._uniqueXValues = function() {
    var collapse = PlotKit.Base.collapse;
    var map = MochiKit.Base.map;
    var uniq = PlotKit.Base.uniq;
    var getter = MochiKit.Base.itemgetter;

    var xvalues = map(parseFloat, map(getter(0), collapse(map(getter(1), items(this.datasets)))));
    xvalues.sort(MochiKit.Base.compare);
    return uniq(xvalues);
};

// Create the bars
PlotKit.Layout.prototype._evaluateBarCharts = function() {
    var keys = MochiKit.Base.keys;
    var items = MochiKit.Base.items;

    var setCount = keys(this.datasets).length;

    // work out how far separated values are
    var xdelta = 10000000;
    var xvalues = this._uniqueXValues();
    for (var i = 1; i < xvalues.length; i++) {
        xdelta = Math.min(Math.abs(xvalues[i] - xvalues[i-1]), xdelta);
    }

    var barWidth = 0;
    var barWidthForSet = 0;
    var barMargin = 0;
    if (xvalues.length == 1) {
        // note we have to do something smarter if we only plot one value
        xdelta = 1.0;
        this.xscale = 1.0;
        this.minxval = xvalues[0];
        barWidth = 1.0 * this.options.barWidthFillFraction;
        barWidthForSet = barWidth/setCount;
        barMargin = (1.0 - this.options.barWidthFillFraction)/2;
    }
    else {
        // readjust xscale to fix with bar charts
        this.xscale = (1.0 - xdelta/this.xrange)/this.xrange;
        barWidth = xdelta * this.xscale * this.options.barWidthFillFraction;
        barWidthForSet = barWidth / setCount;
        barMargin = xdelta * this.xscale * (1.0 - this.options.barWidthFillFraction)/2;
    }

    this.minxdelta = xdelta; // need this for tick positions

    // add all the rects
    this.bars = new Array();
    var i = 0;
    for (var setName in this.datasets) {
        var dataset = this.datasets[setName];
        for (var j = 0; j < dataset.length; j++) {
            var item = dataset[j];
            var rect = {
                x: ((parseFloat(item[0]) - this.minxval) * this.xscale) + (i * barWidthForSet) + barMargin,
                y: 1.0 - ((parseFloat(item[1]) - this.minyval) * this.yscale),
                w: barWidthForSet,
                h: ((parseFloat(item[1]) - this.minyval) * this.yscale),
                xval: parseFloat(item[0]),
                yval: parseFloat(item[1]),
                name: setName
            };
            this.bars.push(rect);
        }
        i++;
    }
};


// Create the line charts
PlotKit.Layout.prototype._evaluateLineCharts = function() {
    var keys = MochiKit.Base.keys;
    var items = MochiKit.Base.items;

    var setCount = keys(this.datasets).length;

    // add all the rects
    this.points = new Array();
    var i = 0;
    for (var setName in this.datasets) {
        var dataset = this.datasets[setName];
        dataset.sort(function(a, b) { return compare(parseFloat(a[0]), parseFloat(b[0])); });
        for (var j = 0; j < dataset.length; j++) {
            var item = dataset[j];
            var point = {
                x: ((parseFloat(item[0]) - this.minxval) * this.xscale),
                y: 1.0 - ((parseFloat(item[1]) - this.minyval) * this.yscale),
                xval: parseFloat(item[0]),
                yval: parseFloat(item[1]),
                name: setName
            };
            this.points.push(point);
        }
        i++;
    }
};

// Create the pie charts
PlotKit.Layout.prototype._evaluatePieCharts = function() {
    var items = MochiKit.Base.items;
    var sum = MochiKit.Iter.sum;
    var getter = MochiKit.Base.itemgetter;

    var setCount = keys(this.datasets).length;

    // we plot the y values of the first dataset
    var dataset = items(this.datasets)[0][1];
    var total = sum(map(getter(1), dataset));

    this.slices = new Array();
    var currentAngle = 0.0;
    for (var i = 0; i < dataset.length; i++) {
        var fraction = dataset[i][1] / total;
		var startAngle = currentAngle * Math.PI * 2;
		var endAngle = (currentAngle + fraction) * Math.PI * 2;
			
        var slice = {fraction: fraction,
                     xval: dataset[i][0],
                     yval: dataset[i][1],
                     startAngle: startAngle,
                     endAngle: endAngle
        };
        this.slices.push(slice);
        currentAngle += fraction;
    }
};

PlotKit.Layout.prototype._evaluateLineTicksForXAxis = function() {
    var isNil = MochiKit.Base.isUndefinedOrNull;
    
    if (this.options.xTicks) {
        // we use use specified ticks with optional labels

        this.xticks = new Array();
        var makeTicks = function(tick) {
            var label = tick.label;
            if (isNil(label))
                label = tick.v.toString();
            var pos = this.xscale * (tick.v - this.minxval);
            this.xticks.push([pos, label]);
        };
        MochiKit.Iter.forEach(this.options.xTicks, bind(makeTicks, this));
    }
    else if (this.options.xNumberOfTicks) {
        // we use defined number of ticks as hint to auto generate
        var xvalues = this._uniqueXValues();
        var roughSeparation = this.xrange / this.options.xNumberOfTicks;
        var tickCount = 0;

        this.xticks = new Array();
        for (var i = 0; i <= xvalues.length; i++) {
            if (xvalues[i] >= (tickCount) * roughSeparation) {
                var pos = this.xscale * (xvalues[i] - this.minxval);
                if ((pos > 1.0) || (pos < 0.0))
                    return;
                this.xticks.push([pos, xvalues[i]]);
                tickCount++;
            }
            if (tickCount > this.options.xNumberOfTicks)
                break;
        }
    }
};

PlotKit.Layout.prototype._evaluateLineTicksForYAxis = function() {
    var isNil = MochiKit.Base.isUndefinedOrNull;


    if (this.options.yTicks) {
        this.yticks = new Array();
        var makeTicks = function(tick) {
            var label = tick.label;
            if (isNil(label))
                label = tick.v.toString();
            var pos = 1.0 - (this.yscale * (tick.v + this.minxval));
            if ((pos < 0.0) || (pos > 1.0))
                return;
            this.yticks.push([pos, label]);
        };
        MochiKit.Iter.forEach(this.options.yTicks, bind(makeTicks, this));
    }
    else if (this.options.yNumberOfTicks) {
        // We use the optionally defined number of ticks as a guide        
        this.yticks = new Array();

        // if we get this separation right, we'll have good looking graphs
        var roundInt = PlotKit.Base.roundInterval;
        var prec = this.options.yTickPrecision;
        var roughSeparation = roundInt(this.yrange, 
                                       this.options.yNumberOfTicks,
                                       this.options.yTickPrecision);

        for (var i = 0; i <= this.options.yNumberOfTicks; i++) {
            var yval = this.minyval + (i * roughSeparation);
            var pos = 1.0 - ((yval - this.minyval) * this.yscale);
            this.yticks.push([pos, MochiKit.Format.roundToFixed(yval, 1)]);
        }
    }
};

PlotKit.Layout.prototype._evaluateLineTicks = function() {
    this._evaluateLineTicksForXAxis();
    this._evaluateLineTicksForYAxis();
};

PlotKit.Layout.prototype._evaluateBarTicks = function() {
    this._evaluateLineTicks();
    var centerInBar = function(tick) {
        return [tick[0] + (this.minxdelta * this.xscale)/2, tick[1]];
    };
    this.xticks = MochiKit.Base.map(bind(centerInBar, this), this.xticks);
};

PlotKit.Layout.prototype._evaluatePieTicks = function() {
    var isNil = MochiKit.Base.isUndefinedOrNull;
	var formatter = MochiKit.Format.numberFormatter("#%");

    this.xticks = new Array();
	if (this.options.xTicks) {
		// make a lookup dict for x->slice values
		var lookup = new Array();
		for (var i = 0; i < this.slices.length; i++) {
			lookup[this.slices[i].xval] = this.slices[i];
		}
		
		for (var i =0; i < this.options.xTicks.length; i++) {
			var tick = this.options.xTicks[i];
			var slice = lookup[tick.v]; 
            var label = tick.label;
			if (slice) {
                if (isNil(label))
                    label = tick.v.toString();
				label += " (" + formatter(slice.fraction) + ")";
				this.xticks.push([tick.v, label]);
			}
		}
	}
	else {
		// we make our own labels from all the slices
		for (var i =0; i < this.slices.length; i++) {
			var slice = this.slices[i];
			var label = slice.xval + " (" + formatter(slice.fraction) + ")";
			this.xticks.push([slice.xval, label]);
		}
	}
};

PlotKit.Layout.prototype._regenerateHitTestCache = function() {
    this.hitTestCache.xvalues = this._uniqueXValues();
    this.hitTestCache.xlookup = new Array();
    this.hitTestCache.x2maxy = new Array();

    var listMax = MochiKit.Base.listMax;
    var itemgetter = MochiKit.Base.itemgetter;
    var map = MochiKit.Base.map;

    // generate a lookup table for x values to y values
    var setNames = keys(this.datasets);
    for (var i = 0; i < setNames.length; i++) {
        var dataset = this.datasets[setNames[i]];
        for (var j = 0; j < dataset.length; j++) {
            var xval = dataset[j][0];
            var yval = dataset[j][1];
            if (this.hitTestCache.xlookup[xval])
                this.hitTestCache.xlookup[xval].push([yval, setNames[i]]);
            else 
                this.hitTestCache.xlookup[xval] = [[yval, setNames[i]]];
        }
    }

    for (var x in this.hitTestCache.xlookup) {
        var yvals = this.hitTestCache.xlookup[x];
        this.hitTestCache.x2maxy[x] = listMax(map(itemgetter(0), yvals));
    }


};

// --------------------------------------------------------------------
// END Internal Functions
// --------------------------------------------------------------------

