Raphael.fn.drawGrid = function (x, y, w, h, wv, hv, color) {
    color = color || "#000";
    var path = ["M", Math.round(x) + .5, Math.round(y) + .5, "L", Math.round(x + w) + .5, Math.round(y) + .5, Math.round(x + w) + .5, Math.round(y + h) + .5, Math.round(x) + .5, Math.round(y + h) + .5, Math.round(x) + .5, Math.round(y) + .5],
        rowHeight = h / hv,
        columnWidth = w / wv;
    for (var i = 1; i < hv; i++) {
        path = path.concat(["M", Math.round(x) + .5, Math.round(y + i * rowHeight) + .5, "H", Math.round(x + w) + .5]);
    }
    for (i = 1; i < wv; i++) {
        path = path.concat(["M", Math.round(x + i * columnWidth) + .5, Math.round(y) + .5, "V", Math.round(y + h) + .5]);
    }
    return this.path(path.join(",")).attr({stroke: color});
};

Raphael.fn.minLineChartWidth = 200;

Raphael.fn.lineChartMetrics = function (seriess, masterLabels) {
	var r = this;
	var max = 0;
    var shouldDisplayValuesAsRatios = false;
	for (var i = 0, ii = seriess.length; i < ii; i++) {
		max = Math.max(max, Math.max.apply(Math, seriess[i].values));
        
        if ( seriess[i].shouldDisplayValuesAsRatios ) {
            shouldDisplayValuesAsRatios = true;
        }
	}
    if ( shouldDisplayValuesAsRatios ) {
        max *= 100.0;
    }
	var txt = {font: '12px "Helvetica Neue", Helvetica, sans-serif', fill: "black"};
	var numberOfVerticalGridLines = 4, minMaxValue = 2;
	max = Math.ceil(Math.max(minMaxValue, max));
	max += (max % minMaxValue);
	var maxValueLabelWidth = 0;
	for (var i = 0, ii = numberOfVerticalGridLines; i <= ii; i++) {
        var t = r.text(0, 0, max - i * (max/numberOfVerticalGridLines) + ( shouldDisplayValuesAsRatios ? '%' : '' ));
		t.attr(txt);
		t.attr('text-anchor', 'start');
		maxValueLabelWidth = Math.max(maxValueLabelWidth, t.getBBox().width);
		t.remove();
	}
    var dataPointWidth = 40,
		leftgutter = maxValueLabelWidth + 10,
        bottomgutter = 45,
        topgutter = 20,
		width = leftgutter + dataPointWidth * masterLabels.length;
					
	return {
		max: max,
		txt: txt,
		numberOfVerticalGridLines: numberOfVerticalGridLines,
		minMaxValue: minMaxValue,
		maxValueLabelWidth: maxValueLabelWidth,
		dataPointWidth: dataPointWidth,
		leftgutter: leftgutter,
		bottomgutter: bottomgutter,
		topgutter: topgutter,
		width: width,
		chartElementWidth: width + r.minLineChartWidth,
        shouldDisplayValuesAsRatios : shouldDisplayValuesAsRatios
	};
}

Raphael.fn.lineChart = function (metrics, height, seriess, masterLabels, colors, mouseOverHandler, mouseOutHandler) {
	var r = this;
    function getAnchors(p1x, p1y, p2x, p2y, p3x, p3y) {
        var l1 = (p2x - p1x) / 2,
            l2 = (p3x - p2x) / 2,
            a = Math.atan((p2x - p1x) / Math.abs(p2y - p1y)),
            b = Math.atan((p3x - p2x) / Math.abs(p2y - p3y));
        a = p1y < p2y ? Math.PI - a : a;
        b = p3y < p2y ? Math.PI - b : b;
        var alpha = Math.PI / 2 - ((a + b) % (Math.PI * 2)) / 2,
            dx1 = l1 * Math.sin(alpha + a),
            dy1 = l1 * Math.cos(alpha + a),
            dx2 = l2 * Math.sin(alpha + b),
            dy2 = l2 * Math.cos(alpha + b);
        return {
            x1: p2x - dx1,
            y1: p2y + dy1,
            x2: p2x + dx2,
            y2: p2y + dy2
        };
    }
	var max = metrics.max;
	var txt = metrics.txt;
	var numberOfVerticalGridLines = metrics.numberOfVerticalGridLines, minMaxValue = metrics.minMaxValue;
	var valueLabels = r.set();
	var maxValueLabelWidth = metrics.maxValueLabelWidth;
	for (var i = 0, ii = numberOfVerticalGridLines; i <= ii; i++) {
        var t = r.text(0, 0, max - i * (max/numberOfVerticalGridLines) + ( metrics.shouldDisplayValuesAsRatios ? '%' : '' ));
		t.attr(txt);
		t.attr('text-anchor', 'start');
		valueLabels.push(t);
	}
    var dataPointWidth = metrics.dataPointWidth,
		originX = r.minLineChartWidth * 0.5,
		leftgutter = metrics.leftgutter,
        bottomgutter = metrics.bottomgutter,
        topgutter = metrics.topgutter,
		width = metrics.width,
        txt1 = {font: '10px "Helvetica Neue", Helvetica, sans-serif', fill: "black"},
        X = ( masterLabels.length ? (width - leftgutter) / masterLabels.length : 0 ),
        Y = (height - bottomgutter - topgutter) / ( max * (metrics.shouldDisplayValuesAsRatios ? 0.01 : 1.0) ),
		gridX = originX + leftgutter + X * .5 + .5,
		gridY = topgutter + .5,
		gridW = width - leftgutter - X,
		gridH = height - topgutter - bottomgutter;
	for (var i = 0, ii = valueLabels.length; i < ii; i++) {
		var t = valueLabels[i];
		t.attr({
			x: gridX - t.getBBox().width - 10,
			y: gridY + (gridH/numberOfVerticalGridLines) * i
		});
	}	
    r.drawGrid(gridX, gridY, gridW, gridH, Math.max( 0, masterLabels.length - 1 ), numberOfVerticalGridLines, "hsba(0.0, 0.0, 1.0, 0.1)");
    var label = r.set(),
        is_label_visible = false,
        leave_timer,
        blanket = r.set();
    label.push(r.text(60, 12, "Popup Label").attr(txt));
    label.push(r.text(60, 27, "Popup Sublabel").attr(txt1));
    label.hide();
    var frame = r.popup(100, 100, label, "right").attr({fill: "white", stroke: "#666", "stroke-width": 2}).hide();
	var hoverDots = r.set();
	var colorhue = 0.0;
	for (var j = 0, jj = seriess.length; j < jj; j++) {
		var data = seriess[j].values;
		var labels = seriess[j].labels;
		var colorhsba = (colors ? DirectMail.hsbaFromColor(colors[j]) : {h:colorhue + 0.1*j, s:0.6, b:1.0, a:1.0});
		var color = DirectMail.hsbaDeclarationWithValues(colorhsba);
	    var path = r.path().attr({stroke: color, "stroke-width": 4, "stroke-linejoin": "round"});
	    var p;
	    for (var i = 0, ii = masterLabels.length; i < ii; i++) {
	        var y = Math.round(height - bottomgutter - Y * data[i]),
	            x = originX + Math.round(leftgutter + X * (i + .5));
			if (!j) {
				var t = r.text(x, height, masterLabels[i].label + (masterLabels[i].sublabel ? "\n" + masterLabels[i].sublabel : "")).attr(txt).toBack();
				t.attr( "y", height - bottomgutter + t.getBBox().height/2 + 10 );
			}
	        if (!i) {
	            p = ["M", x, y, "C", x, y];
	        }
	        if (i && i < ii - 1) {
	            var Y0 = Math.round(height - bottomgutter - Y * data[i - 1]),
	                X0 = originX + Math.round(leftgutter + X * (i - .5)),
	                Y2 = Math.round(height - bottomgutter - Y * data[i + 1]),
	                X2 = originX + Math.round(leftgutter + X * (i + 1.5));
	            var a = getAnchors(X0, Y0, x, y, X2, Y2);
	            p = p.concat([a.x1, a.y1, x, y, a.x2, a.y2]);
	        }
	        var dot = r.circle(x, y, 4).attr({fill: "white", stroke: color, "stroke-width": 2});
			var hoverDot = r.circle(x, y, 6).attr({fill: 'white', opacity: 0});
			hoverDots.push(hoverDot);
			var hoverObject = hoverDot;
			if (seriess.length == 1) {
		        blanket.push(r.rect(originX + leftgutter + X * i, 0, X, height - bottomgutter).attr({stroke: "none", fill: "#fff", opacity: 0}));
				hoverObject = blanket[blanket.length - 1];
			}
	        (function (hoverObject, x, y, data, series, lbl, dot, color) {
	            var timer, i = 0;
	            hoverObject.hover(function (event) {
	                clearTimeout(leave_timer);
	                var side = "right";
	                if (x + frame.getBBox().width > width) {
	                    side = "left";
	                }
	                label[0].attr({text: lbl.label});
	                label[1].attr({text: lbl.sublabel, fill: color});
	                var ppp = r.popup(x, y, label, side, 1);
					var popupLabelTranslation = [0, 0];
					if (!is_label_visible) {
						frame.attr({path: ppp.path});
						label.attr({translation: [ppp.dx, ppp.dy]});
					}
					else {
						popupLabelTranslation = [ppp.dx, ppp.dy];
					}
					frame.show().attr({ path: ppp.path });
					label.show().attr({ translation: popupLabelTranslation });
	                dot.attr("r", 6);
	                is_label_visible = true;
					if (mouseOverHandler) {
						mouseOverHandler(event, series, lbl);
					}
	            }, function (event) {
	                dot.attr("r", 4);
	                leave_timer = setTimeout(function () {
	                    frame.hide();
	                    label.hide();
	                    is_label_visible = false;
	                }, 1);
					if (mouseOutHandler) {
						mouseOutHandler(event, series, lbl);
					}
	            });
	        })(hoverObject, x, y, data[i], seriess[j], labels[i], dot, color);
	    }
	    p = p.concat([x, y, x, y]);
	    path.attr({path: p});		
	}

    frame.toFront();
    label.toFront();
	if (seriess.length == 1) {
	    blanket.toFront();
	}
	else {
		blanket.toBack();
		hoverDots.toFront();
	}
};