// Copyright 2008 by mSpoke http://www.mspoke.com
// Written by Paul Ogilvie pogilvie@mspoke.com

dojo.provide("dojox.charting.plot2d.BoxAndWhisker");

dojo.require("dojox.charting.plot2d.common");
dojo.require("dojox.charting.plot2d.Base");

dojo.require("dojox.lang.utils");
dojo.require("dojox.lang.functional");
dojo.require("dojox.lang.functional.reversed");

(function(){
	var df = dojox.lang.functional, du = dojox.lang.utils,
	    dc = dojox.charting.plot2d.common,
	    purgeGroup = df.lambda("item.purgeGroup()");
	
	dojo.declare("dojox.charting.plot2d.BoxAndWhisker", dojox.charting.plot2d.Base, {
		defaultParams: {
		    hAxis: "x",		// use a horizontal axis named "x"
		    vAxis: "y",		// use a vertical axis named "y"
		    gap:    15,
		    markers: true	// draw markers
		},
		optionalParams: {},	// no optional parameters
		    
		    
		    
		constructor: function(chart, kwArgs){
		    this.opt = dojo.clone(this.defaultParams);
		    du.updateWithObject(this.opt, kwArgs);
		    this.series = [];
		    this.hAxis = this.opt.hAxis;
		    this.vAxis = this.opt.vAxis;
		    this.center = this.opt.center;
 		},
		    
		calculateAxes: function(dim){
		    var stats = dojo.clone(dc.defaultStats);
		    for (var i = 0; i < this.series.length; ++i) {
                        var run = this.series[i]; 
			stats.hmin = Math.min(stats.vmin, run.center);
			stats.hmax = Math.max(stats.vmax, run.center);
			if (run.data.constructor == Array) {
			    dojo.forEach(run.data, function(val, i){
				stats.vmin = Math.min(stats.vmin, val);
				stats.vmax = Math.max(stats.vmax, val);
			    });
			} else {
			    stats.vmin = Math.min(stats.vmin, run.data.lwhisker);
			    stats.vmin = Math.min(stats.vmin, run.data.lbox);
			    dojo.forEach(run.data.outliers, function(val, i){
				stats.vmin = Math.min(stats.vmin, val);
				stats.vmax = Math.max(stats.vmax, val);
                            });
			}			
                    }
		    stats.hmax += 0.5;
		    stats.hmin -= 0.5;
		    var vpad = 0.05*(stats.vmax - stats.vmin);
		    stats.vmin -= vpad;
		    stats.vmax += vpad;
		    this._calc(dim, stats);
		    return this;
		},
		    
		    
	       render: function(dim, offsets){
		    this.dirty = this.isDirty();
		    if(this.dirty){
			dojo.forEach(this.series, purgeGroup);
			this.cleanGroup();
			var s = this.group;
			df.forEachRev(this.series, function(item){ item.cleanGroup(s); });
		    }
		    var t = this.chart.theme, stroke, fill, color, marker, events = this.events();
		    for(var i = this.series.length - 1; i >= 0; --i){
			var run = this.series[i];
			if(!this.dirty && !run.dirty){ continue; }
			run.cleanGroup();
			var s = run.group;
			if(!run.fill || !run.stroke){
			    // need autogenerated color
			    color = run.dyn.color = new dojo.Color(t.next("color"));
			}
			stroke = run.stroke ? run.stroke : dc.augmentStroke(t.series.stroke, color);
			fill = run.fill ? run.fill : dc.augmentFill(t.series.fill, color);
			
			var s = run.group, lowerLine, upperLine, medianLine,
			    ht = this._hScaler.scaler.getTransformerFromModel(this._hScaler),
			    vt = this._vScaler.scaler.getTransformerFromModel(this._vScaler),
			    gap = this.opt.gap < this._hScaler.bounds.scale / 3 ? this.opt.gap : 0,
			    baseline = this._vScaler.bounds.lower,
			    baselineHeight = vt(baseline);
			
			var width;
			if (typeof run.width != undefined) {
			    width = ht(run.width + this._hScaler.bounds.lower);
			} else {
			    width = this._hScaler.bounds.scale - 2 * gap;
			}
			
			var lw, lq, med, uq, uw, outliers;
			if (run.data.constructor == Array) {
			    
			    // Sort the points
			    run.data.sort(function (x, y) { return x - y; });
			    
			    // Identify  25, 50, 75,  percentages
			    var p = run.data.length;
			    var p4 = Math.floor((p + 3) / 2) / 2;
			    med = (run.data[Math.floor((p + 1) / 2) - 1] + run.data[Math.ceil((p + 1) / 2) - 1]) / 2;
			    lq = (run.data[Math.floor(p4) - 1] + run.data[Math.ceil(p4) - 1]) / 2;
			    uq = (run.data[Math.floor(p + 1 - p4) - 1] + run.data[Math.ceil(p + 1 - p4) - 1]) / 2;
			    // Compute inter-quartile range * 1.5
			    var iqr15 = (uq - lq) * 1.5;
			    lw = lq - iqr15;
			    uw = uq + iqr15;
			    
			    // locate whisker endpoints
			    var lwp = 0, uwp = p - 1;
			    var j;
			    for (j = Math.ceil(p4) - 1; j >= 0; j--) {
				if (lw > run.data[j]) {
				    lwp = j+1;
				    break;
				}
			    }
			    lw = run.data[lwp];
			    for (j = Math.floor(p + 1 - p4); j < p; j++) {
				if (uw < run.data[j]) {
				    uwp = j-1;
				    break;
				}
			    }
			    uw = run.data[uwp];
			    if(this.opt.markers){
				outliers = new Array();
				if (lwp > 0) {
				    outliers = outliers.concat(run.data.slice(0, lwp));
				}
				if (uwp < p) {
				    outliers = outliers.concat(run.data.slice(uwp + 1));
				}
			    }
			} else {
			    lw = run.data.lwhisker;
			    lq = run.data.lbox;
			    med = run.data.median;
			    uq = run.data.ubox;
			    uw = run.data.uwhisker;
			    outliers = run.data.outliers;
			}


			var center = offsets.l + ht(run.center),
			    yoff = dim.height - offsets.b;
			
			// Draw quartile box
			var rect = {x: center - width / 2,
				    y: yoff - (uq > baseline ? vt(uq) : baselineHeight),
				    width: width,
				    height: (uq > baseline ? vt(uq) : baselineHeight) - (lq > baseline ? vt(lq) : baselineHeight)},
			    quartileBox = s.createRect(rect).setFill(fill).setStroke(stroke);
			    
                            lowerLine = s.createRect({x: center - width/2, 
                                                      y: yoff - (lw > baseline ? vt(lw) : baselineHeight),
                                                      width: width,
                                                      height: 1}).setStroke(stroke);

                            medianLine = s.createRect({x: center - width/2 - 1, 
                                                      y: yoff - (med > baseline ? vt(med) : baselineHeight),
                                                      width: width + 2,
                                                      height: 1}).setStroke(stroke);
			    
                            upperLine = s.createRect({x: center - width/2, 
                                                      y: yoff - (uw > baseline ? vt(uw) : baselineHeight),
                                                      width: width,
                                                      height: 1}).setStroke(stroke);
			    			    
			    var lc = s.createLine({x1: center, y1: yoff - (lq > baseline ? vt(lq) : baselineHeight),
						   x2: center, y2: yoff - (lw > baseline ? vt(lw) : baselineHeight)}).setStroke(stroke);
			    var uc = s.createLine({x1: center, y1: yoff - (uw > baseline ? vt(uw) : baselineHeight),
                                       x2: center, y2: yoff - (uq > baseline ? vt(uq) : baselineHeight)}).setStroke(stroke);
			    
			    if(events){
				var o = {
				    element: "bar",
				    index:   i,
				    run:     run,
				    plot:    this,
				    hAxis:   this.hAxis || null,
				    vAxis:   this.vAxis || null,
				    shape:   quartileBox,
				    x:       "[" + lq + ", " + uq + "]",
				    y:       "[" + lq + ", " + uq + "]"
				};
				this._connectEvents(quartileBox, o);
				o = {
				    element: "column",
				    index:   i,
				    run:     run,
				    plot:    this,
				    hAxis:   this.hAxis || null,
				    vAxis:   this.vAxis || null,
				    shape:   lowerLine,
				    x:       lw,
				    y:       lw
				}
				this._connectEvents(lowerLine, o);
				o = {
				    element: "column",
				    index:   i,
				    run:     run,
				    plot:    this,
				    hAxis:   this.hAxis || null,
				    vAxis:   this.vAxis || null,
				    shape:   medianLine,
				    x:       med,
				    y:       med
				}
				this._connectEvents(medianLine, o);
				o = {
				    element: "column",
				    index:   i,
				    run:     run,
				    plot:    this,
				    hAxis:   this.hAxis || null,
				    vAxis:   this.vAxis || null,
				    shape:   upperLine,
				    x:       uw,
				    y:       uw
				}
				this._connectEvents(upperLine, o);
			    }

			    if(this.opt.markers){
				var markers = new Array(outliers.length);
				marker = run.dyn.marker = run.marker ? run.marker : t.next("marker");
				var jitters = new Array(outliers.length);
				dojo.forEach(outliers, function(y, k){
					var xp = (center + (Math.random() * width) - width / 2);
					jitters[k] = xp;
					var yp = yoff - (y > baseline ? vt(y) : baselineHeight);
					var path = "M" + xp + " " + yp + " " + marker;
					markers[k] = s.createPath(path).setStroke(stroke).setFill(stroke.color);
				    }, this);
				if(events){
				    dojo.forEach(markers, function(s, k){
					    var o = {
						element: "marker",
						index:   k,
						run:     run,
						plot:    this,
						hAxis:   this.hAxis || null,
						vAxis:   this.vAxis || null,
						shape:   s,
						outline: markers[k] || null,
						shadow:  null,
						cx:      jitters[k],
						cy:      yoff - (outliers[k] > baseline ? vt(outliers[k]) : baselineHeight)
					    };
					    o.x = outliers[k];
					    o.y = outliers[k];
					    this._connectEvents(s, o);
					}, this);
				}
			    }

			    run.dirty = false;
			}
		    this.dirty = false;
		    return this;
		}
	    });
})();
