/*

    Graph.js
    --------
   
    A Javascript Object that allows you to draw simple graphs to an
    HTML Canvas Object[1]. 
    
    The idea is to allow you to quickly visualise some data in graphical
    form without resorting to server side dynamic pages.
    
    See: http://www.liquidx.net/canvasgraphjs/

    Requirements
    ------------
    * Mochikit 1.1+ (Base, Async, Color, DOM, Logging)
      <http://www.mochikit.com>    

    Copyright
    ---------
    Copyright 2005,2006 (c) Alastair Tse <alastair^tse.id.au>
    
    Licensed under the BSD License. See end of file for full license terms.
    
    Changes (Ascending Order)
    -------------------------
    
    [11 Dec 2005] 0.5   - Initial Public Release 
    [12 Dec 2005] 0.5.1 - Minor bugfix with drawGrid's color usage. Thanks to 
                          Philippe Marschall.

*/

// Check required components

try {    
    if (typeof(MochiKit.Base) == 'undefined'   ||
        typeof(MochiKit.DOM) == 'undefined'    ||
        typeof(MochiKit.Color) == 'undefined'  ||
        typeof(MochiKit.Format) == 'undefined' ||
        typeof(MochiKit.Logging) == 'undefied')
    {
        throw "";    
    }
} 
catch (e) {    
    throw "canvasGraph depends on MochiKit.Base, MochiKit.DOM, MochiKit.Color, MochiKit.Logging and Mochikit.Format";
}

// utility function that may disappear when I get a better one!

function nearestMagnitude(x) {
    var magn = x;
    if ((x < 1) && (x > 0)) {
        // found float
        var xstr = x + "";
        var dp = 0;
        for (var i = 2; i < xstr.length; i++) {
            if (xstr.charAt(i) != "0") {
                dp = i - 2;
                break;
            }
        }
        magn = Math.pow(10, -1 * dp);
    }
    else {
        // round to magnitude
        var xstr = parseInt(x) + "";
        var n = Math.pow(10, xstr.length - 1);
        magn = Math.round(x/n) * n;
    }
    return magn
}

function get_mouse_position(e) {
    var posx = e.layerX;
    var posy = e.layerY;
    /*
    if (e.layerX || e.layerY) {
        posx = e.;
        posy = e.layerY;
    }
    */
    return new Array(posx, posy);
}

function lightenColor(c) {
   return c.blendedColor(Color.whiteColor(), 0.5);
}

function transparentColor(c) {
   return c.blendedColor(Color.whiteColor()).colorWithAlpha(0.5);
}

function fillColor(c) {
    return c.blendedColor(Color.whiteColor()).colorWithAlpha(0.5).toRGBString();
};

function strokeColor(c) {
    return c.blendedColor(Color.whiteColor()).colorWithAlpha(0.7).toRGBString();
};

function colorScheme() {
   return [Color.redColor(), Color.orangeColor(),
   	   Color.yellowColor(), Color.greenColor(), 
	   Color.cyanColor(), Color.blueColor(),
	   Color.purpleColor(), Color.magentaColor(),
	   Color.brownColor(), Color.grayColor()];
}

CanvasGraph = function(canvasName) {
    this.element = $(canvasName);
    if (isUndefinedOrNull(this.element)) {
        error("CanvasGraph() - canvas not found");
        return null;
    }
        
    if (!CanvasGraph.isSupported(canvasName)) 
        return null;
        
    // TODO: make container creation automatic?
    this.container = this.element.parentNode;
    this.context = this.element.getContext("2d");
    this.height = this.element.height;
    this.width = this.element.width;

    // public attributes

    this.padding = {top: 20, left: 30, bottom: 30, right: 20};
    
    this.xlabels = {};
    this.ylabels = {};

    this.yticks = [];
    this.xticks = [];

    this.autoAxis = true;
    this.xOriginIsZero = true;
    this.yOriginIsZero = true;    
    
    this.barChartWidth = 0.75;    
    this.fontSize = 10;
    this.xtickSeparation = 50;    
    this.ytickSeparation = 50;

    this.labelColor = Color.blackColor();
    this.labelWidth = 50;

    // private attributes - we won't deliberately hide this, but don't touch
    //                      these unless you know what you're doing.
        
    this.xvmin = 0;
    this.xvmax = 0;
    this.yvmin = 0;
    this.yvmax = 0;
    
    this.values = new Array();

    // finally, some initialising
    this.lastDrawState = new Array();
    
    // make sure style is set properly on the graph object
    updateNodeAttributes(this.container, {"style":{
        "position": "relative",
        "width": this.element.width + "px"
    }});
    updateNodeAttributes(this.element, {"style":{
        "position": "relative",
        "zIndex": 1
    }});
    
    /* event handler stuff that is currently disabled
    
    this.mousedown = false;
    this.startx = 0;
    this.starty = 0;
    this.endx = 0;
    this.endy = 0;
    this.labelbox = DIV({"style": {
        "backgroundColor": "rgba(255, 255, 200, 0.8)",
        "border": "1px solid black",
        "fontSize": 8 + "px",
        "position": "absolute",
        "padding": 0,
        "margin": 0,
        "zIndex": 11
        }}, "");
    hideElement(this.labelbox);
    appendChildNodes(this.container, [this.labelbox]);
    addToCallStack(this.element, "onmousedown", bind(this._onmousedown, this));
    addToCallStack(this.element, "onmouseup", bind(this._onmouseup, this));    
    addToCallStack(this.element, "onclick", bind(this._onclick, this));
    addToCallStack(this.element, "ondblclick", bind(this._ondblclick, this));
    addToCallStack(this.element, "onmousemove", bind(this._onmousemove, this));
    
    */
};


CanvasGraph.isSupported = function(canvas_name) {
    // checks if canvas drawing is supported
    var canvas = null;
    if (isUndefinedOrNull(canvas_name)) {
        canvas = createDOM("canvas");
    }
    else {
        canvas = $(canvas_name);
    }
    
    try {
        var context = canvas.getContext("2d");
    }
    catch (e) {
        return false;
    }
    return true;
};

CanvasGraph.prototype.setDataset = function(name, dataset) {
    this.values[name] = dataset;
    
    var all = map(itemgetter(1), items(this.values));
    var allValues = new Array();
    for (var i = 0; i < all.length; i++) {
    	allValues = concat(allValues, all[i]);
    }
    
    if (this.xOriginIsZero)
        this.xvmin = 0;
    else 
    	this.xvmin = listMin(map(itemgetter(0), allValues));

    if (this.yOriginIsZero)    
        this.yvmin = 0;
    else
        this.yvmin = listMin(map(itemgetter(1), allValues));

    this.xvmax = listMax(map(itemgetter(0), allValues));
    this.yvmax = listMax(map(itemgetter(1), allValues));

    this._calcScale();
};

CanvasGraph.prototype.setDatasetFromTable = function(name, tableElement, xcol, ycol) {

    if (isUndefinedOrNull(xcol))
        xcol = 0;
    if (isUndefinedOrNull(ycol))
        ycol = 1;
        
    var rows = tableElement.tBodies[0].rows;
    var data = new Array();
    if (!isUndefinedOrNull(rows)) {
        for (var i = 0; i < rows.length; i++) {
            data.push([strip(scrapeText(rows[i].cells[xcol])), 
                       parseFloat(strip(scrapeText(rows[i].cells[ycol])))]);
        }
        this.setDataset(name, data);
        return true;
    }

    return false;
};


// --------------------------------------------------------------
// -- Draw Pie Charts 
// --------------------------------------------------------------

CanvasGraph.prototype.drawPieChart = function(dataset) {
    var context = this.context;
    var values = this.values[dataset];
    var ytotal = 0;
    for (var i = 0; i < values.length; i++) {
    	ytotal += values[i][1];
    }
    
    this._calcPadding();
    this._calcScale();
    
    var radius = objMin((this.ymax - this.ymin), (this.xmax - this.xmin)) / 2;
    var centerx = (this.xmax - this.xmin) / 2 + this.xmin
    var centery = (this.ymax - this.ymin) / 2 + this.ymin

    var colors = map(lightenColor, colorScheme());
    var threeD = true;    

    var counter = 0.0;
    context.save();
    for (var i = 0; i < values.length; i++) {
        var fraction = values[i][1] / ytotal;
        context.beginPath();
        context.moveTo(centerx, centery);
        context.arc(centerx, centery, radius, 
                   counter * Math.PI * 2 - Math.PI * 0.5,
                   (counter + fraction) * Math.PI * 2 - Math.PI * 0.5,
                   false);
        context.lineTo(centerx, centery);
        context.closePath();
        
        context.fillStyle = colors[i%colors.length].toRGBString();
        context.fill();
        
        // draw label
        var sliceMiddle = (counter + fraction/2)
        var labelx = centerx + Math.sin(sliceMiddle * Math.PI * 2) * (radius + 10);
        var labely = centery - Math.cos(sliceMiddle * Math.PI * 2) * (radius + 10);
        
        var attrib = {"position": "absolute",
                      "zIndex": 11,
                      "width": this.labelWidth + "px",
                      "fontSize": this.fontSize + "px",
                      "overflow": "hidden"};
                      
        if (this.labelColor != null)
            attrib["color"] = this.labelColor.toHexString();
        else
            attrib["color"] = colors[i%colors.length].toHexString();
        
        if (sliceMiddle <= 0.25) {
            // text on top and align left
            attrib["textAlign"] = "left";
            attrib["verticalAlign"] = "top";
            attrib["left"] = labelx + "px";
            attrib["top"] = (labely - this.fontSize) + "px";
        }
        else if (sliceMiddle > 0.25 && sliceMiddle <= 0.5) {
            // text on bottom and align left
            attrib["textAlign"] = "left";
            attrib["verticalAlign"] = "bottom";     
            attrib["left"] = labelx + "px";
            attrib["top"] = labely + "px";
                   
        }
        else if (sliceMiddle > 0.5 && sliceMiddle <= 0.75) {
            // text on bottom and align right
            attrib["textAlign"] = "right";
            attrib["verticalAlign"] = "bottom"; 
            attrib["left"] = (labelx  - this.labelWidth) + "px";
            attrib["top"] = labely + "px";
        }
        else {
            // text on top and align right
            attrib["textAlign"] = "right";
            attrib["verticalAlign"] = "bottom";  
            attrib["left"] = (labelx  - this.labelWidth) + "px";
            attrib["top"] = (labely - this.fontSize) + "px";
        }
        
        var labelText = this.xlabels[values[i][0]];
        if (isUndefinedOrNull(labelText))
            labelText = this.ylabels[values[i][1]];
        
        if (isUndefinedOrNull(labelText))
            labelText = values[i][0];
        labelText += " (" + numberFormatter("#%")(fraction) + ")"
        
        var label = DIV({"style": attrib}, labelText);
        this.xticks.push(label);
        appendChildNodes(this.container, label);
        
        counter += fraction;
    }
    context.restore();
    
};

// --------------------------------------------------------------
// -- Draw Line Plot
// --------------------------------------------------------------

CanvasGraph.prototype.drawLinePlot = function(name_color_map) {

    this._calcPadding();
    this._calcScale();

    var context = this.context;
    var name_colors = items(name_color_map);
    
    for (var c = 0; c < name_colors.length; c++) {
        var values = this.values[name_colors[c][0]];
        if (isUndefinedOrNull(values))
            continue;
        
        // draw the line
        for (var i = 1; i < values.length; i++) {
            var startx = Math.round(this.xmin + values[i-1][0] * this.xvscale);
            var starty = Math.round(this.ymax - values[i-1][1] * this.yvscale);
            var endx = Math.round(this.xmin + values[i][0] * this.xvscale);
            var endy = Math.round(this.ymax - values[i][1] * this.yvscale);
                
            context.save();
            context.lineWidth = 1.0;	    
            context.strokeStyle = strokeColor(name_colors[c][1]);
            context.fillStyle = Color.transparentColor().toRGBString();

            context.beginPath();
            context.moveTo(startx, starty);
            context.lineTo(endx, endy);
            context.closePath();
            context.stroke();
	    
            // fill between y value and axis
            context.beginPath();
            context.moveTo(startx, starty);
            context.lineTo(endx, endy);
            context.lineTo(endx, this.ymax);
            context.lineTo(startx, this.ymax);            
            context.closePath();
            context.fillStyle =  fillColor(name_colors[c][1]);
    	    context.lineWidth = 0;
            context.fill();
            context.restore();
        }
    }
    
    this.lastDrawState.push({"drawLinePlot":name_color_map});

    this.drawAxis();
};

// --------------------------------------------------------------
// -- Draw Line Plot
// --------------------------------------------------------------

CanvasGraph.prototype.drawBarChart = function(name_color_map)  {

    this._calcPadding();
    this._calcScale();

    var context = this.context;
    var name_colors = items(name_color_map);
    var barWidth = 10 * this.xvscale;
    var sets = name_colors.length;
    
    // check if there are missing keys
    for (var i = 0; i < name_colors.length; i++) {
        if (isUndefinedOrNull(this.values[name_colors[i][0]])) {
            error("CanvasGraph.drawBarChart() - given name ", 
                  name_colors[i][0], "does not have dataset associated.");
        }
    }

    // populate the reverse lookup values
    var key_lookup = new Array();
    for (var i = 0; i < name_colors.length; i++) {
        var name = name_colors[i][0];
        var values = this.values[name];
        for (var j = 0; j < values.length; j++) {
            var xv = values[j][0];
            if (isUndefinedOrNull(key_lookup[xv]))
                key_lookup[xv] = [[values[j][1], name]];
            else 
                key_lookup[xv].push([values[j][1], name]);
        }
    }
    

    // draw each bar    
    var xvalues = keys(key_lookup).sort();
    var minxdelta = 100000;
    for (var i = 1; i < xvalues.length; i++) {
        minxdelta = objMin(Math.abs(xvalues[i] - xvalues[i-1]), minxdelta);
    }
    
    var barWidth = ((minxdelta / sets) * this.xvscale *
                       this.barChartWidth)
        
    for (var i = 0; i < xvalues.length; i++) {
        var xv = xvalues[i];
        var yvalues = key_lookup[xv];
        for (var j = 0; j < yvalues.length; j++) {
            var setColor = name_color_map[yvalues[j][1]];
            var barX = (this.xpx(xv) + j * barWidth - (barWidth * sets/2));
            var barY = this.ypx(yvalues[j][0]);
            
            context.save();
            context.rect(barX, barY, barWidth - 1, this.ymax - barY);
            context.fillStyle = fillColor(setColor);
            context.fill();
            
            context.rect(barX, barY, barWidth - 1, this.ymax - barY);
            context.strokeStyle = setColor.toRGBString();
            context.lineWidth = 0.5;            
            context.stroke();
            
            context.restore();
        }
    }

    this.lastDrawState.push({"drawBarChart": name_color_map});

    this.drawAxis(0, (minxdelta * this.xvscale)/2);

};

CanvasGraph.prototype.drawGrid = function(spacing, gridColor) {
    var context = this.context;

    
    context.save()
    if (isUndefinedOrNull(gridColor)) {
        context.strokeStyle = fillColor(Color.lightGrayColor());
    }
    else {
        context.strokeStyle = gridColor.toRGBString();
    }

    context.lineWidth = 0.5;

    for (x = 0; x < this.width; x += spacing) {
        context.save();
        context.beginPath();                
        context.moveTo(x, 0);
        context.lineTo(x, this.height);
        context.closePath();
        context.stroke();
        context.restore();
    }
    
    for (y = 0; y < this.height; y += spacing) {
        context.save();
        context.moveTo(0, y);
        context.lineTo(this.width, y);
        context.closePath();
        context.stroke();
        context.restore();
    }    
    
    context.restore();
    
    this.lastDrawState.push({"drawGrid": spacing});

};

CanvasGraph.prototype.clear = function () {
    var context = this.context;
    context.clearRect(0, 0, this.width, this.height);

    hideElement(this.labelbox);
    for (var i = 0; i < this.xticks.length; i++) {
        removeElement(this.xticks[i]);
    }        
    for (var i = 0; i < this.yticks.length; i++) {
        removeElement(this.yticks[i]);
    }            
    this.xticks = new Array();
    this.yticks = new Array();

};

CanvasGraph.prototype.redraw = function() {
    // Rough redraw to restore draw state
    var states = clone(this.lastDrawState);
    this.lastDrawState = new Array();
    
    if (states.length > 0) {
        this.clear();
    }

    for (var i = 0; i < states.length; i++) {
        if (states[i]["drawGrid"]) {
            this.drawGrid(states[i]["drawGrid"]);
        }
        else if (states[i]["drawBarChart"]) {
            this.drawBarChart(states[i]["drawBarChart"]);
        }
        else if (states[i]["drawLinePlot"]) {
            this.drawLinePlot(states[i]["drawLinePlot"]);
        }
    }
};


/* 

 Internal Functions

*/

// translate from x-value to x-pixel
CanvasGraph.prototype.xpx = function(xv) {
    return this.xmin + (xv - this.xvmin)*this.xvscale;
};

CanvasGraph.prototype.ypx = function(yv) {
    return this.ymax - (yv - this.yvmin)*this.yvscale;
};

CanvasGraph.prototype._calcPadding = function() {
    this.xmin = this.padding.left;
    this.xmax = this.width - this.padding.right;
    this.ymin = this.padding.top;
    this.ymax = this.height - this.padding.bottom;
};

CanvasGraph.prototype._calcScale = function() {
    this.xvrange = this.xvmax - this.xvmin;
    if (this.xvrange > 0)
        this.xvscale = (this.xmax - this.xmin)/this.xvrange;
    else
        this.xvscale = 1;
        
    this.yvrange = this.yvmax - this.yvmin;
    if (this.yvrange > 0) 
        this.yvscale = (this.ymax - this.ymin)/this.yvrange;
    else
        this.yvscale = 1;
};

CanvasGraph.prototype._calcAxis = function() {
    /* 
        Finds the nearest pretty looking steps for the x and y axis.
    */
    
    var xsteps = (this.width / this.xtickSeparation);
    var ysteps = (this.height / this.ytickSeparation);
    
    xsteps = (this.xvmax - this.xvmin) / xsteps;
    ysteps = (this.yvmax - this.yvmin) / ysteps;
    
    xsteps = nearestMagnitude(xsteps);
    ysteps = nearestMagnitude(ysteps);
    
    return new Array(xsteps, ysteps);    
};

CanvasGraph.prototype.drawAxis = function(xaxis_off, yaxis_off, xsteps, ysteps) {

    context = this.context;

    if (isUndefinedOrNull(xsteps) || isUndefinedOrNull(ysteps)) {
        steps = this._calcAxis();
        if (isUndefinedOrNull(xsteps)) 
            xsteps = steps[0];
        if (isUndefinedOrNull(ysteps))
            ysteps = steps[1];
    }

    if (isUndefinedOrNull(xaxis_off)) {
        xaxis_off = 0;
    }
    if (isUndefinedOrNull(yaxis_off)) {
        yaxis_off = 0;
    }

    context.save();
        context.lineWidth = 1.0;
        context.strokeStyle = Color.blackColor().toRGBString();
        
        context.beginPath();       
        context.moveTo(this.xmin - yaxis_off, this.ymin);
        context.lineTo(this.xmin - yaxis_off, this.ymax);        
        context.closePath();
        context.stroke();
        
        context.beginPath();
        context.moveTo(this.xmin - yaxis_off, this.ymax);        
        context.lineTo(this.xmax + yaxis_off, this.ymax);
        context.closePath();
        context.stroke();
    context.restore();
    
    this.drawTicks(xaxis_off, yaxis_off, xsteps, ysteps);
};

CanvasGraph.prototype.drawTicks = function (xaxis_off, yaxis_off, xsteps, ysteps) {

    var context = this.context;
    
    if (isUndefinedOrNull(xaxis_off)) {
        xaxis_off = 0;
    }
    
    if (isUndefinedOrNull(yaxis_off)) {
        yaxis_off = 0;
    }
    
    context.save();
    context.lineWidth = 0.5;
    context.strokeStyle = Color.blackColor().toRGBString();

    // horiztonal ticks
    for (var xv = this.xvmin; xv <= this.xvmax; xv += xsteps) {
        x = this.xpx(xv);
        y = this.ymax + 7;
               
        context.beginPath();
        context.moveTo(x, this.ymax + 1);
        context.lineTo(x, this.ymax + 5);
        context.closePath();
        context.stroke();

        var label = null;        
        if (!isUndefinedOrNull(this.xlabels[xv]))
            label = DIV({}, this.xlabels[xv]);
        else
            label = DIV({}, xv);
        
        updateNodeAttributes(label, {"style":
            {"position": "absolute",
             "textAlign": "center",
             "width": this.xtickSeparation + "px",
             "top": y + "px",
             "left": Math.ceil(x-(this.xtickSeparation/2)) + "px",
             "fontSize": this.fontSize + "px",
             "zIndex": 10,
             "color": "black",
             "overflow": "hidden"
        }});
        
        appendChildNodes(this.container, label);
    }
    
    // vertical ticks
    
    for (var yv = this.yvmin; yv <= this.yvmax; yv += ysteps) {
        y = this.ypx(yv);
        x = this.xmin - 7
        context.beginPath();
        context.moveTo(this.xmin - 5 - yaxis_off, y);
        context.lineTo(this.xmin - 1 - yaxis_off, y);
        context.closePath();
        context.stroke();
        
        var label = null;
        if (!isUndefinedOrNull(this.ylabels[yv]))
            label = DIV({}, this.ylabels[yv]);
        else
            label = DIV({}, yv);
            
        updateNodeAttributes(label, {"style":
            {"position": "absolute",
             "top": Math.ceil(y - this.fontSize/2) + "px",
             "left": Math.ceil(x - this.padding.left - yaxis_off) + "px",
             "fontSize": this.fontSize + "px",
             "zIndex": 10,
             "color": "black",
             "textAlign": "right",
             "width": this.padding.left + "px",
             "overflow": "hidden"
        }});
        appendChildNodes(this.container, label);
    }    
    
    context.restore();
};

/*

 Copyright (c) 2005, 2006 Alastair Tse <alastair@tse.id.au>

 All rights reserved.

 Redistribution and use in source and binary forms, with or without modification, are
 permitted provided that the following conditions are met:
 
  * Redistributions of source code must retain the above copyright notice, this list of
 conditions and the following disclaimer. * Redistributions in binary form must reproduce
 the above copyright notice, this list of conditions and the following disclaimer in the
 documentation and/or other materials provided with the distribution. * Neither the name
 of the <ORGANIZATION> nor the names of its contributors may be used to endorse or
 promote products derived from this software without specific prior written permission.
 
  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
 EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
 OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
*/ 
