Experiment about making graphs of route trips
Experiment about making graphs of route trips

--- a/include/common-transit.inc.php
+++ b/include/common-transit.inc.php
@@ -33,5 +33,24 @@
 		return 'weekday';
 	}
 }
+function midnight_seconds()
+{
+	// from http://www.perturb.org/display/Perlfunc__Seconds_Since_Midnight.html
+	if (isset($_SESSION['time'])) {
+		$time = strtotime($_SESSION['time']);
+		return (date("G", $time) * 3600) + (date("i", $time) * 60) + date("s", $time);
+	}
+	return (date("G") * 3600) + (date("i") * 60) + date("s");
+}
+function midnight_seconds_to_time($seconds)
+{
+	if ($seconds > 0) {
+		$midnight = mktime(0, 0, 0, date("n") , date("j") , date("Y"));
+		return date("h:ia", $midnight + $seconds);
+	}
+	else {
+		return "";
+	}
+}
+?>
 
-?>

--- a/include/db/route-dao.inc.php
+++ b/include/db/route-dao.inc.php
@@ -1,6 +1,7 @@
 <?php
 
 function getRoute($routeID) {
+		global $conn;
         $query = "Select * from routes where route_id = '$routeID' LIMIT 1";
         debug($query,"database");
 	$result = pg_query($conn, $query);

--- a/include/db/trip-dao.inc.php
+++ b/include/db/trip-dao.inc.php
@@ -159,10 +159,10 @@
 	return pg_fetch_all($result);
 }
 
-function viaPointNames($tripid, $stop_sequence = "")
+function viaPoints($tripid, $stop_sequence = "")
 {
 	global $conn;
-	$query = "SELECT stop_name
+	$query = "SELECT stops.stop_id, stop_name, arrival_time
 FROM stop_times join stops on stops.stop_id = stop_times.stop_id
 WHERE stop_times.trip_id = '$tripid'
 ".($stop_sequence != "" ? "AND stop_sequence > '$stop_sequence'" : "").
@@ -173,7 +173,14 @@
 		databaseError(pg_result_error($result));
 		return Array();
 	}
-	$pointNames = pg_fetch_all($result);
-	return r_implode(", ", $pointNames);
+	return pg_fetch_all($result);
+}
+function viaPointNames($tripid, $stop_sequence = "")
+{
+	$viaPointNames = Array();
+	foreach(viaPoints($tripid, $stop_sequence) as $point) {
+		$viaPointNames[] = $point['stop_name'];
+	}
+	return r_implode(", ", $viaPointNames);
 }
 ?>

--- /dev/null
+++ b/js/flotr/flotr-0.2.0-alpha.js
@@ -1,1 +1,2 @@
-
+//Flotr 0.2.0-alpha Copyright (c) 2009 Bas Wenneker, <http://solutoire.com>, MIT License.
+var Flotr={version:"0.2.0-alpha",author:"Bas Wenneker",website:"http://www.solutoire.com",_registeredTypes:{lines:"drawSeriesLines",points:"drawSeriesPoints",bars:"drawSeriesBars",candles:"drawSeriesCandles",pie:"drawSeriesPie"},register:function(A,B){Flotr._registeredTypes[A]=B+""},draw:function(B,D,A,C){C=C||Flotr.Graph;return new C(B,D,A)},getSeries:function(A){return A.collect(function(C){var B,C=(C.data)?Object.clone(C):{data:C};for(B=C.data.length-1;B>-1;--B){C.data[B][1]=(C.data[B][1]===null?null:parseFloat(C.data[B][1]))}return C})},merge:function(D,B){var A=B||{};for(var C in D){A[C]=(D[C]!=null&&typeof (D[C])=="object"&&!(D[C].constructor==Array||D[C].constructor==RegExp)&&!Object.isElement(D[C]))?Flotr.merge(D[C],B[C]):A[C]=D[C]}return A},getTickSize:function(E,D,A,B){var H=(A-D)/E;var G=Flotr.getMagnitude(H);var C=H/G;var F=10;if(C<1.5){F=1}else{if(C<2.25){F=2}else{if(C<3){F=((B==0)?2:2.5)}else{if(C<7.5){F=5}}}}return F*G},defaultTickFormatter:function(A){return A+""},defaultTrackFormatter:function(A){return"("+A.x+", "+A.y+")"},defaultPieLabelFormatter:function(A){return(A.fraction*100).toFixed(2)+"%"},getMagnitude:function(A){return Math.pow(10,Math.floor(Math.log(A)/Math.LN10))},toPixel:function(A){return Math.floor(A)+0.5},toRad:function(A){return -A*(Math.PI/180)},parseColor:function(D){if(D instanceof Flotr.Color){return D}var A,C=Flotr.Color;if((A=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(D))){return new C(parseInt(A[1]),parseInt(A[2]),parseInt(A[3]))}if((A=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(D))){return new C(parseInt(A[1]),parseInt(A[2]),parseInt(A[3]),parseFloat(A[4]))}if((A=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(D))){return new C(parseFloat(A[1])*2.55,parseFloat(A[2])*2.55,parseFloat(A[3])*2.55)}if((A=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(D))){return new C(parseFloat(A[1])*2.55,parseFloat(A[2])*2.55,parseFloat(A[3])*2.55,parseFloat(A[4]))}if((A=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(D))){return new C(parseInt(A[1],16),parseInt(A[2],16),parseInt(A[3],16))}if((A=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(D))){return new C(parseInt(A[1]+A[1],16),parseInt(A[2]+A[2],16),parseInt(A[3]+A[3],16))}var B=D.strip().toLowerCase();if(B=="transparent"){return new C(255,255,255,0)}return((A=C.lookupColors[B]))?new C(A[0],A[1],A[2]):false},extractColor:function(B){var A;do{A=B.getStyle("background-color").toLowerCase();if(!(A==""||A=="transparent")){break}B=B.up(0)}while(!B.nodeName.match(/^body$/i));return(A=="rgba(0, 0, 0, 0)")?"transparent":A}};Flotr.Graph=Class.create({initialize:function(B,C,A){this.el=$(B);if(!this.el){throw"The target container doesn't exist"}this.data=C;this.series=Flotr.getSeries(C);this.setOptions(A);this.lastMousePos={pageX:null,pageY:null};this.selection={first:{x:-1,y:-1},second:{x:-1,y:-1}};this.prevSelection=null;this.selectionInterval=null;this.ignoreClick=false;this.prevHit=null;this.constructCanvas();this.initEvents();this.findDataRanges();this.calculateTicks(this.axes.x);this.calculateTicks(this.axes.x2);this.calculateTicks(this.axes.y);this.calculateTicks(this.axes.y2);this.calculateSpacing();this.draw();this.insertLegend();if(this.options.spreadsheet.show){this.constructTabs()}},setOptions:function(B){var P={colors:["#00A8F0","#C0D800","#CB4B4B","#4DA74D","#9440ED"],title:null,subtitle:null,legend:{show:true,noColumns:1,labelFormatter:Prototype.K,labelBoxBorderColor:"#CCCCCC",labelBoxWidth:14,labelBoxHeight:10,labelBoxMargin:5,container:null,position:"nw",margin:5,backgroundColor:null,backgroundOpacity:0.85},xaxis:{ticks:null,showLabels:true,labelsAngle:0,title:null,titleAngle:0,noTicks:5,tickFormatter:Flotr.defaultTickFormatter,tickDecimals:null,min:null,max:null,autoscaleMargin:0,color:null},x2axis:{},yaxis:{ticks:null,showLabels:true,labelsAngle:0,title:null,titleAngle:90,noTicks:5,tickFormatter:Flotr.defaultTickFormatter,tickDecimals:null,min:null,max:null,autoscaleMargin:0,color:null},y2axis:{titleAngle:270},points:{show:false,radius:3,lineWidth:2,fill:true,fillColor:"#FFFFFF",fillOpacity:0.4},lines:{show:false,lineWidth:2,fill:false,fillColor:null,fillOpacity:0.4},bars:{show:false,lineWidth:2,barWidth:1,fill:true,fillColor:null,fillOpacity:0.4,horizontal:false,stacked:false},candles:{show:false,lineWidth:1,wickLineWidth:1,candleWidth:0.6,fill:true,upFillColor:"#00A8F0",downFillColor:"#CB4B4B",fillOpacity:0.5,barcharts:false},pie:{show:false,lineWidth:1,fill:true,fillColor:null,fillOpacity:0.6,explode:6,sizeRatio:0.6,startAngle:Math.PI/4,labelFormatter:Flotr.defaultPieLabelFormatter,pie3D:false,pie3DviewAngle:(Math.PI/2*0.8),pie3DspliceThickness:20},grid:{color:"#545454",backgroundColor:null,tickColor:"#DDDDDD",labelMargin:3,verticalLines:true,horizontalLines:true,outlineWidth:2},selection:{mode:null,color:"#B6D9FF",fps:20},mouse:{track:false,position:"se",relative:false,trackFormatter:Flotr.defaultTrackFormatter,margin:5,lineColor:"#FF3F19",trackDecimals:1,sensibility:2,radius:3},shadowSize:4,defaultType:"lines",HtmlText:true,fontSize:7.5,spreadsheet:{show:false,tabGraphLabel:"Graph",tabDataLabel:"Data",toolbarDownload:"Download CSV",toolbarSelectAll:"Select all"}};P.x2axis=Object.extend(Object.clone(P.xaxis),P.x2axis);P.y2axis=Object.extend(Object.clone(P.yaxis),P.y2axis);this.options=Flotr.merge((B||{}),P);this.axes={x:{options:this.options.xaxis,n:1},x2:{options:this.options.x2axis,n:2},y:{options:this.options.yaxis,n:1},y2:{options:this.options.y2axis,n:2}};var H=[],C=[],K=this.series.length,N=this.series.length,D=this.options.colors,A=[],G=0,M,J,I,O,E;for(J=N-1;J>-1;--J){M=this.series[J].color;if(M!=null){--N;if(Object.isNumber(M)){H.push(M)}else{A.push(Flotr.parseColor(M))}}}for(J=H.length-1;J>-1;--J){N=Math.max(N,H[J]+1)}for(J=0;C.length<N;){M=(D.length==J)?new Flotr.Color(100,100,100):Flotr.parseColor(D[J]);var F=G%2==1?-1:1;var L=1+F*Math.ceil(G/2)*0.2;M.scale(L,L,L);C.push(M);if(++J>=D.length){J=0;++G}}for(J=0,I=0;J<K;++J){O=this.series[J];if(O.color==null){O.color=C[I++].toString()}else{if(Object.isNumber(O.color)){O.color=C[O.color].toString()}}if(!O.xaxis){O.xaxis=this.axes.x}if(O.xaxis==1){O.xaxis=this.axes.x}else{if(O.xaxis==2){O.xaxis=this.axes.x2}}if(!O.yaxis){O.yaxis=this.axes.y}if(O.yaxis==1){O.yaxis=this.axes.y}else{if(O.yaxis==2){O.yaxis=this.axes.y2}}O.lines=Object.extend(Object.clone(this.options.lines),O.lines);O.points=Object.extend(Object.clone(this.options.points),O.points);O.bars=Object.extend(Object.clone(this.options.bars),O.bars);O.candles=Object.extend(Object.clone(this.options.candles),O.candles);O.pie=Object.extend(Object.clone(this.options.pie),O.pie);O.mouse=Object.extend(Object.clone(this.options.mouse),O.mouse);if(O.shadowSize==null){O.shadowSize=this.options.shadowSize}}},constructCanvas:function(){var C=this.el,B,D,A;this.canvas=C.select(".flotr-canvas")[0];this.overlay=C.select(".flotr-overlay")[0];C.childElements().invoke("remove");C.setStyle({position:"relative",cursor:"default"});this.canvasWidth=C.getWidth();this.canvasHeight=C.getHeight();B={width:this.canvasWidth,height:this.canvasHeight};if(this.canvasWidth<=0||this.canvasHeight<=0){throw"Invalid dimensions for plot, width = "+this.canvasWidth+", height = "+this.canvasHeight}if(!this.canvas){D=this.canvas=new Element("canvas",B);D.className="flotr-canvas";D=D.writeAttribute("style","position:absolute;left:0px;top:0px;")}else{D=this.canvas.writeAttribute(B)}C.insert(D);if(Prototype.Browser.IE){D=window.G_vmlCanvasManager.initElement(D)}this.ctx=D.getContext("2d");if(!this.overlay){A=this.overlay=new Element("canvas",B);A.className="flotr-overlay";A=A.writeAttribute("style","position:absolute;left:0px;top:0px;")}else{A=this.overlay.writeAttribute(B)}C.insert(A);if(Prototype.Browser.IE){A=window.G_vmlCanvasManager.initElement(A)}this.octx=A.getContext("2d");if(window.CanvasText){CanvasText.enable(this.ctx);CanvasText.enable(this.octx);this.textEnabled=true}},getTextDimensions:function(F,C,B,D){if(!F){return{width:0,height:0}}if(!this.options.HtmlText&&this.textEnabled){var E=this.ctx.getTextBounds(F,C);return{width:E.width+2,height:E.height+6}}else{var A=this.el.insert('<div style="position:absolute;top:-10000px;'+B+'" class="'+D+' flotr-dummy-div">'+F+"</div>").select(".flotr-dummy-div")[0];dim=A.getDimensions();A.remove();return dim}},loadDataGrid:function(){if(this.seriesData){return this.seriesData}var A=this.series;var B=[];for(i=0;i<A.length;++i){A[i].data.each(function(D){var C=D[0],F=D[1];if(r=B.find(function(G){return G[0]==C})){r[i+1]=F}else{var E=[];E[0]=C;E[i+1]=F;B.push(E)}})}B=B.sortBy(function(C){return C[0]});return this.seriesData=B},showTab:function(B,C){var A="canvas, .flotr-labels, .flotr-legend, .flotr-legend-bg, .flotr-title, .flotr-subtitle";switch(B){case"graph":this.datagrid.up().hide();this.el.select(A).invoke("show");this.tabs.data.removeClassName("selected");this.tabs.graph.addClassName("selected");break;case"data":this.constructDataGrid();this.datagrid.up().show();this.el.select(A).invoke("hide");this.tabs.data.addClassName("selected");this.tabs.graph.removeClassName("selected");break}},constructTabs:function(){var A=new Element("div",{className:"flotr-tabs-group",style:"position:absolute;left:0px;top:"+this.canvasHeight+"px;width:"+this.canvasWidth+"px;"});this.el.insert({bottom:A});this.tabs={graph:new Element("div",{className:"flotr-tab selected",style:"float:left;"}).update(this.options.spreadsheet.tabGraphLabel),data:new Element("div",{className:"flotr-tab",style:"float:left;"}).update(this.options.spreadsheet.tabDataLabel)};A.insert(this.tabs.graph).insert(this.tabs.data);this.el.setStyle({height:this.canvasHeight+this.tabs.data.getHeight()+2+"px"});this.tabs.graph.observe("click",(function(){this.showTab("graph")}).bind(this));this.tabs.data.observe("click",(function(){this.showTab("data")}).bind(this))},constructDataGrid:function(){if(this.datagrid){return this.datagrid}var D,B,L=this.series,J=this.loadDataGrid();var K=this.datagrid=new Element("table",{className:"flotr-datagrid",style:"height:100px;"});var C=["<colgroup><col />"];var F=['<tr class="first-row">'];F.push("<th>&nbsp;</th>");for(D=0;D<L.length;++D){F.push('<th scope="col">'+(L[D].label||String.fromCharCode(65+D))+"</th>");C.push("<col />")}F.push("</tr>");for(B=0;B<J.length;++B){F.push("<tr>");for(D=0;D<L.length+1;++D){var M="td";var G=(J[B][D]!=null?Math.round(J[B][D]*100000)/100000:"");if(D==0){M="th";var I;if(this.options.xaxis.ticks){var E=this.options.xaxis.ticks.find(function(N){return N[0]==J[B][D]});if(E){I=E[1]}}else{I=this.options.xaxis.tickFormatter(G)}if(I){G=I}}F.push("<"+M+(M=="th"?' scope="row"':"")+">"+G+"</"+M+">")}F.push("</tr>")}C.push("</colgroup>");K.update(C.join("")+F.join(""));if(!Prototype.Browser.IE){K.select("td").each(function(N){N.observe("mouseover",function(O){N=O.element();var P=N.previousSiblings();K.select("th[scope=col]")[P.length-1].addClassName("hover");K.select("colgroup col")[P.length].addClassName("hover")});N.observe("mouseout",function(){K.select("colgroup col.hover, th.hover").each(function(O){O.removeClassName("hover")})})})}var H=new Element("div",{className:"flotr-datagrid-toolbar"}).insert(new Element("button",{type:"button",className:"flotr-datagrid-toolbar-button"}).update(this.options.spreadsheet.toolbarDownload).observe("click",this.downloadCSV.bind(this))).insert(new Element("button",{type:"button",className:"flotr-datagrid-toolbar-button"}).update(this.options.spreadsheet.toolbarSelectAll).observe("click",this.selectAllData.bind(this)));var A=new Element("div",{className:"flotr-datagrid-container",style:"left:0px;top:0px;width:"+this.canvasWidth+"px;height:"+this.canvasHeight+"px;overflow:auto;"});A.insert(H);K.wrap(A.hide());this.el.insert(A);return K},selectAllData:function(){if(this.tabs){var B,A,E,D,C=this.constructDataGrid();this.showTab("data");(function(){if((E=C.ownerDocument)&&(D=E.defaultView)&&D.getSelection&&E.createRange&&(B=window.getSelection())&&B.removeAllRanges){A=E.createRange();A.selectNode(C);B.removeAllRanges();B.addRange(A)}else{if(document.body&&document.body.createTextRange&&(A=document.body.createTextRange())){A.moveToElementText(C);A.select()}}}).defer();return true}else{return false}},downloadCSV:function(){var D,A='"x"',C=this.series,E=this.loadDataGrid();for(D=0;D<C.length;++D){A+='%09"'+(C[D].label||String.fromCharCode(65+D))+'"'}A+="%0D%0A";for(D=0;D<E.length;++D){if(this.options.xaxis.ticks){var B=this.options.xaxis.ticks.find(function(F){return F[0]==E[D][0]});if(B){E[D][0]=B[1]}}else{E[D][0]=this.options.xaxis.tickFormatter(E[D][0])}A+=E[D].join("%09")+"%0D%0A"}if(Prototype.Browser.IE){A=A.gsub("%09","\t").gsub("%0A","\n").gsub("%0D","\r");window.open().document.write(A)}else{window.open("data:text/csv,"+A)}},initEvents:function(){this.overlay.stopObserving();this.overlay.observe("mousedown",this.mouseDownHandler.bind(this));this.overlay.observe("mousemove",this.mouseMoveHandler.bind(this));this.overlay.observe("click",this.clickHandler.bind(this))},findDataRanges:function(){var J=this.series,G=this.axes;G.x.datamin=0;G.x.datamax=0;G.x2.datamin=0;G.x2.datamax=0;G.y.datamin=0;G.y.datamax=0;G.y2.datamin=0;G.y2.datamax=0;if(J.length>0){var C,A,D,H,F,B,I,E;for(C=0;C<J.length;++C){B=J[C].data,I=J[C].xaxis,E=J[C].yaxis;if(B.length>0&&!J[C].hide){if(!I.used){I.datamin=I.datamax=B[0][0]}if(!E.used){E.datamin=E.datamax=B[0][1]}I.used=true;E.used=true;for(D=B.length-1;D>-1;--D){H=B[D][0];if(H<I.datamin){I.datamin=H}else{if(H>I.datamax){I.datamax=H}}for(A=1;A<B[D].length;A++){F=B[D][A];if(F<E.datamin){E.datamin=F}else{if(F>E.datamax){E.datamax=F}}}}}}}this.findXAxesValues();this.calculateRange(G.x);this.extendXRangeIfNeededByBar(G.x);if(G.x2.used){this.calculateRange(G.x2);this.extendXRangeIfNeededByBar(G.x2)}this.calculateRange(G.y);this.extendYRangeIfNeededByBar(G.y);if(G.y2.used){this.calculateRange(G.y2);this.extendYRangeIfNeededByBar(G.y2)}},calculateRange:function(D){var F=D.options,C=F.min!=null?F.min:D.datamin,A=F.max!=null?F.max:D.datamax,E;if(A-C==0){var B=(A==0)?1:0.01;C-=B;A+=B}D.tickSize=Flotr.getTickSize(F.noTicks,C,A,F.tickDecimals);if(F.min==null){E=F.autoscaleMargin;if(E!=0){C-=D.tickSize*E;if(C<0&&D.datamin>=0){C=0}C=D.tickSize*Math.floor(C/D.tickSize)}}if(F.max==null){E=F.autoscaleMargin;if(E!=0){A+=D.tickSize*E;if(A>0&&D.datamax<=0){A=0}A=D.tickSize*Math.ceil(A/D.tickSize)}}D.min=C;D.max=A},extendXRangeIfNeededByBar:function(A){if(A.options.max==null){var D=A.max,B,I,F,E,H=[],C=null;for(B=0;B<this.series.length;++B){I=this.series[B];F=I.bars;E=I.candles;if(I.axis==A&&(F.show||E.show)){if(!F.horizontal&&(F.barWidth+A.datamax>D)||(E.candleWidth+A.datamax>D)){D=A.max+I.bars.barWidth}if(F.stacked&&F.horizontal){for(j=0;j<I.data.length;j++){if(I.bars.show&&I.bars.stacked){var G=I.data[j][0];H[G]=(H[G]||0)+I.data[j][1];C=I}}for(j=0;j<H.length;j++){D=Math.max(H[j],D)}}}}A.lastSerie=C;A.max=D}},extendYRangeIfNeededByBar:function(A){if(A.options.max==null){var D=A.max,B,I,F,E,H=[],C=null;for(B=0;B<this.series.length;++B){I=this.series[B];F=I.bars;E=I.candles;if(I.yaxis==A&&F.show&&!I.hide){if(F.horizontal&&(F.barWidth+A.datamax>D)||(E.candleWidth+A.datamax>D)){D=A.max+F.barWidth}if(F.stacked&&!F.horizontal){for(j=0;j<I.data.length;j++){if(I.bars.show&&I.bars.stacked){var G=I.data[j][0];H[G]=(H[G]||0)+I.data[j][1];C=I}}for(j=0;j<H.length;j++){D=Math.max(H[j],D)}}}}A.lastSerie=C;A.max=D}},findXAxesValues:function(){for(i=this.series.length-1;i>-1;--i){s=this.series[i];s.xaxis.values=s.xaxis.values||[];for(j=s.data.length-1;j>-1;--j){s.xaxis.values[s.data[j][0]]={}}}},calculateTicks:function(D){var B=D.options,E,H;D.ticks=[];if(B.ticks){var G=B.ticks,I,F;if(Object.isFunction(G)){G=G({min:D.min,max:D.max})}for(E=0;E<G.length;++E){I=G[E];if(typeof (I)=="object"){H=I[0];F=(I.length>1)?I[1]:B.tickFormatter(H)}else{H=I;F=B.tickFormatter(H)}D.ticks[E]={v:H,label:F}}}else{var A=D.tickSize*Math.ceil(D.min/D.tickSize),C;for(E=0;A+E*D.tickSize<=D.max;++E){H=A+E*D.tickSize;C=B.tickDecimals;if(C==null){C=1-Math.floor(Math.log(D.tickSize)/Math.LN10)}if(C<0){C=0}H=H.toFixed(C);D.ticks.push({v:H,label:B.tickFormatter(H)})}}},calculateSpacing:function(){var L=this.axes,N=this.options,H=this.series,D=N.grid.labelMargin,M=L.x,A=L.x2,J=L.y,K=L.y2,F=2,G,E,C,I;[M,A,J,K].each(function(P){var O="";if(P.options.showLabels){for(G=0;G<P.ticks.length;++G){C=P.ticks[G].label.length;if(C>O.length){O=P.ticks[G].label}}}P.maxLabel=this.getTextDimensions(O,{size:N.fontSize,angle:Flotr.toRad(P.options.labelsAngle)},"font-size:smaller;","flotr-grid-label");P.titleSize=this.getTextDimensions(P.options.title,{size:N.fontSize*1.2,angle:Flotr.toRad(P.options.titleAngle)},"font-weight:bold;","flotr-axis-title")},this);I=this.getTextDimensions(N.title,{size:N.fontSize*1.5},"font-size:1em;font-weight:bold;","flotr-title");this.titleHeight=I.height;I=this.getTextDimensions(N.subtitle,{size:N.fontSize},"font-size:smaller;","flotr-subtitle");this.subtitleHeight=I.height;if(N.show){F=Math.max(F,N.points.radius+N.points.lineWidth/2)}for(E=0;E<N.length;++E){if(H[E].points.show){F=Math.max(F,H[E].points.radius+H[E].points.lineWidth/2)}}var B=this.plotOffset={left:0,right:0,top:0,bottom:0};B.left=B.right=B.top=B.bottom=F;B.bottom+=(M.options.showLabels?(M.maxLabel.height+D):0)+(M.options.title?(M.titleSize.height+D):0);B.top+=(A.options.showLabels?(A.maxLabel.height+D):0)+(A.options.title?(A.titleSize.height+D):0)+this.subtitleHeight+this.titleHeight;B.left+=(J.options.showLabels?(J.maxLabel.width+D):0)+(J.options.title?(J.titleSize.width+D):0);B.right+=(K.options.showLabels?(K.maxLabel.width+D):0)+(K.options.title?(K.titleSize.width+D):0);B.top=Math.floor(B.top);this.plotWidth=this.canvasWidth-B.left-B.right;this.plotHeight=this.canvasHeight-B.bottom-B.top;M.scale=this.plotWidth/(M.max-M.min);A.scale=this.plotWidth/(A.max-A.min);J.scale=this.plotHeight/(J.max-J.min);K.scale=this.plotHeight/(K.max-K.min)},draw:function(){this.drawGrid();this.drawLabels();this.drawTitles();if(this.series.length){this.el.fire("flotr:beforedraw",[this.series,this]);for(var A=0;A<this.series.length;A++){if(!this.series[A].hide){this.drawSeries(this.series[A])}}}this.el.fire("flotr:afterdraw",[this.series,this])},tHoz:function(A,B){B=B||this.axes.x;return(A-B.min)*B.scale},tVert:function(B,A){A=A||this.axes.y;return this.plotHeight-(B-A.min)*A.scale},drawGrid:function(){var B,E=this.options,A=this.ctx;if(E.grid.verticalLines||E.grid.horizontalLines){this.el.fire("flotr:beforegrid",[this.axes.x,this.axes.y,E,this])}A.save();A.translate(this.plotOffset.left,this.plotOffset.top);if(E.grid.backgroundColor!=null){A.fillStyle=E.grid.backgroundColor;A.fillRect(0,0,this.plotWidth,this.plotHeight)}A.lineWidth=1;A.strokeStyle=E.grid.tickColor;A.beginPath();if(E.grid.verticalLines){for(var D=0;D<this.axes.x.ticks.length;++D){B=this.axes.x.ticks[D].v;if((B==this.axes.x.min||B==this.axes.x.max)&&E.grid.outlineWidth!=0){continue}A.moveTo(Math.floor(this.tHoz(B))+A.lineWidth/2,0);A.lineTo(Math.floor(this.tHoz(B))+A.lineWidth/2,this.plotHeight)}}if(E.grid.horizontalLines){for(var C=0;C<this.axes.y.ticks.length;++C){B=this.axes.y.ticks[C].v;if((B==this.axes.y.min||B==this.axes.y.max)&&E.grid.outlineWidth!=0){continue}A.moveTo(0,Math.floor(this.tVert(B))+A.lineWidth/2);A.lineTo(this.plotWidth,Math.floor(this.tVert(B))+A.lineWidth/2)}}A.stroke();if(E.grid.outlineWidth!=0){A.lineWidth=E.grid.outlineWidth;A.strokeStyle=E.grid.color;A.lineJoin="round";A.strokeRect(0,0,this.plotWidth,this.plotHeight)}A.restore();if(E.grid.verticalLines||E.grid.horizontalLines){this.el.fire("flotr:aftergrid",[this.axes.x,this.axes.y,E,this])}},drawLabels:function(){var C=0,D,B,E,F,G,J=this.options,I=this.ctx,H=this.axes;for(E=0;E<H.x.ticks.length;++E){if(H.x.ticks[E].label){++C}}B=this.plotWidth/C;if(!J.HtmlText&&this.textEnabled){var A={size:J.fontSize,adjustAlign:true};D=H.x;A.color=D.options.color||J.grid.color;for(E=0;E<D.ticks.length&&D.options.showLabels&&D.used;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}A.angle=Flotr.toRad(D.options.labelsAngle);A.halign="c";A.valign="t";I.drawText(G.label,this.plotOffset.left+this.tHoz(G.v,D),this.plotOffset.top+this.plotHeight+J.grid.labelMargin,A)}D=H.x2;A.color=D.options.color||J.grid.color;for(E=0;E<D.ticks.length&&D.options.showLabels&&D.used;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}A.angle=Flotr.toRad(D.options.labelsAngle);A.halign="c";A.valign="b";I.drawText(G.label,this.plotOffset.left+this.tHoz(G.v,D),this.plotOffset.top+J.grid.labelMargin,A)}D=H.y;A.color=D.options.color||J.grid.color;for(E=0;E<D.ticks.length&&D.options.showLabels&&D.used;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}A.angle=Flotr.toRad(D.options.labelsAngle);A.halign="r";A.valign="m";I.drawText(G.label,this.plotOffset.left-J.grid.labelMargin,this.plotOffset.top+this.tVert(G.v,D),A)}D=H.y2;A.color=D.options.color||J.grid.color;for(E=0;E<D.ticks.length&&D.options.showLabels&&D.used;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}A.angle=Flotr.toRad(D.options.labelsAngle);A.halign="l";A.valign="m";I.drawText(G.label,this.plotOffset.left+this.plotWidth+J.grid.labelMargin,this.plotOffset.top+this.tVert(G.v,D),A);I.save();I.strokeStyle=A.color;I.beginPath();I.moveTo(this.plotOffset.left+this.plotWidth-8,this.plotOffset.top+this.tVert(G.v,D));I.lineTo(this.plotOffset.left+this.plotWidth,this.plotOffset.top+this.tVert(G.v,D));I.stroke();I.restore()}}else{if(H.x.options.showLabels||H.x2.options.showLabels||H.y.options.showLabels||H.y2.options.showLabels){F=['<div style="font-size:smaller;color:'+J.grid.color+';" class="flotr-labels">'];D=H.x;if(D.options.showLabels){for(E=0;E<D.ticks.length;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}F.push('<div style="position:absolute;top:'+(this.plotOffset.top+this.plotHeight+J.grid.labelMargin)+"px;left:"+(this.plotOffset.left+this.tHoz(G.v,D)-B/2)+"px;width:"+B+"px;text-align:center;"+(D.options.color?("color:"+D.options.color+";"):"")+'" class="flotr-grid-label">'+G.label+"</div>")}}D=H.x2;if(D.options.showLabels&&D.used){for(E=0;E<D.ticks.length;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}F.push('<div style="position:absolute;top:'+(this.plotOffset.top-J.grid.labelMargin-D.maxLabel.height)+"px;left:"+(this.plotOffset.left+this.tHoz(G.v,D)-B/2)+"px;width:"+B+"px;text-align:center;"+(D.options.color?("color:"+D.options.color+";"):"")+'" class="flotr-grid-label">'+G.label+"</div>")}}D=H.y;if(D.options.showLabels){for(E=0;E<D.ticks.length;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}F.push('<div style="position:absolute;top:'+(this.plotOffset.top+this.tVert(G.v,D)-D.maxLabel.height/2)+"px;left:0;width:"+(this.plotOffset.left-J.grid.labelMargin)+"px;text-align:right;"+(D.options.color?("color:"+D.options.color+";"):"")+'" class="flotr-grid-label">'+G.label+"</div>")}}D=H.y2;if(D.options.showLabels&&D.used){I.save();I.strokeStyle=D.options.color||J.grid.color;I.beginPath();for(E=0;E<D.ticks.length;++E){G=D.ticks[E];if(!G.label||G.label.length==0){continue}F.push('<div style="position:absolute;top:'+(this.plotOffset.top+this.tVert(G.v,D)-D.maxLabel.height/2)+"px;right:0;width:"+(this.plotOffset.right-J.grid.labelMargin)+"px;text-align:left;"+(D.options.color?("color:"+D.options.color+";"):"")+'" class="flotr-grid-label">'+G.label+"</div>");I.moveTo(this.plotOffset.left+this.plotWidth-8,this.plotOffset.top+this.tVert(G.v,D));I.lineTo(this.plotOffset.left+this.plotWidth,this.plotOffset.top+this.tVert(G.v,D))}I.stroke();I.restore()}F.push("</div>");this.el.insert(F.join(""))}}},drawTitles:function(){var D,C=this.options,F=C.grid.labelMargin,B=this.ctx,A=this.axes;if(!C.HtmlText&&this.textEnabled){var E={size:C.fontSize,color:C.grid.color,halign:"c"};if(C.subtitle){B.drawText(C.subtitle,this.plotOffset.left+this.plotWidth/2,this.titleHeight+this.subtitleHeight-2,E)}E.weight=1.5;E.size*=1.5;if(C.title){B.drawText(C.title,this.plotOffset.left+this.plotWidth/2,this.titleHeight-2,E)}E.weight=1.8;E.size*=0.8;E.adjustAlign=true;if(A.x.options.title&&A.x.used){E.halign="c";E.valign="t";E.angle=Flotr.toRad(A.x.options.titleAngle);B.drawText(A.x.options.title,this.plotOffset.left+this.plotWidth/2,this.plotOffset.top+A.x.maxLabel.height+this.plotHeight+2*F,E)}if(A.x2.options.title&&A.x2.used){E.halign="c";E.valign="b";E.angle=Flotr.toRad(A.x2.options.titleAngle);B.drawText(A.x2.options.title,this.plotOffset.left+this.plotWidth/2,this.plotOffset.top-A.x2.maxLabel.height-2*F,E)}if(A.y.options.title&&A.y.used){E.halign="r";E.valign="m";E.angle=Flotr.toRad(A.y.options.titleAngle);B.drawText(A.y.options.title,this.plotOffset.left-A.y.maxLabel.width-2*F,this.plotOffset.top+this.plotHeight/2,E)}if(A.y2.options.title&&A.y2.used){E.halign="l";E.valign="m";E.angle=Flotr.toRad(A.y2.options.titleAngle);B.drawText(A.y2.options.title,this.plotOffset.left+this.plotWidth+A.y2.maxLabel.width+2*F,this.plotOffset.top+this.plotHeight/2,E)}}else{D=['<div style="color:'+C.grid.color+';" class="flotr-titles">'];if(C.title){D.push('<div style="position:absolute;top:0;left:'+this.plotOffset.left+"px;font-size:1em;font-weight:bold;text-align:center;width:"+this.plotWidth+'px;" class="flotr-title">'+C.title+"</div>")}if(C.subtitle){D.push('<div style="position:absolute;top:'+this.titleHeight+"px;left:"+this.plotOffset.left+"px;font-size:smaller;text-align:center;width:"+this.plotWidth+'px;" class="flotr-subtitle">'+C.subtitle+"</div>")}D.push("</div>");D.push('<div class="flotr-axis-title" style="font-weight:bold;">');if(A.x.options.title&&A.x.used){D.push('<div style="position:absolute;top:'+(this.plotOffset.top+this.plotHeight+C.grid.labelMargin+A.x.titleSize.height)+"px;left:"+this.plotOffset.left+"px;width:"+this.plotWidth+'px;text-align:center;" class="flotr-axis-title">'+A.x.options.title+"</div>")}if(A.x2.options.title&&A.x2.used){D.push('<div style="position:absolute;top:0;left:'+this.plotOffset.left+"px;width:"+this.plotWidth+'px;text-align:center;" class="flotr-axis-title">'+A.x2.options.title+"</div>")}if(A.y.options.title&&A.y.used){D.push('<div style="position:absolute;top:'+(this.plotOffset.top+this.plotHeight/2-A.y.titleSize.height/2)+'px;left:0;text-align:right;" class="flotr-axis-title">'+A.y.options.title+"</div>")}if(A.y2.options.title&&A.y2.used){D.push('<div style="position:absolute;top:'+(this.plotOffset.top+this.plotHeight/2-A.y.titleSize.height/2)+'px;right:0;text-align:right;" class="flotr-axis-title">'+A.y2.options.title+"</div>")}D.push("</div>");this.el.insert(D.join(""))}},drawSeries:function(A){A=A||this.series;var C=false;for(var B in Flotr._registeredTypes){if(A[B]&&A[B].show){this[Flotr._registeredTypes[B]](A);C=true}}if(!C){this[Flotr._registeredTypes[this.options.defaultType]](A)}},plotLine:function(I,F){var O=this.ctx,A=I.xaxis,K=I.yaxis,J=this.tHoz.bind(this),M=this.tVert.bind(this),H=I.data;if(H.length<2){return }var E=J(H[0][0],A),D=M(H[0][1],K)+F;O.beginPath();O.moveTo(E,D);for(var G=0;G<H.length-1;++G){var C=H[G][0],N=H[G][1],B=H[G+1][0],L=H[G+1][1];if(N===null||L===null){continue}if(N<=L&&N<K.min){if(L<K.min){continue}C=(K.min-N)/(L-N)*(B-C)+C;N=K.min}else{if(L<=N&&L<K.min){if(N<K.min){continue}B=(K.min-N)/(L-N)*(B-C)+C;L=K.min}}if(N>=L&&N>K.max){if(L>K.max){continue}C=(K.max-N)/(L-N)*(B-C)+C;N=K.max}else{if(L>=N&&L>K.max){if(N>K.max){continue}B=(K.max-N)/(L-N)*(B-C)+C;L=K.max}}if(C<=B&&C<A.min){if(B<A.min){continue}N=(A.min-C)/(B-C)*(L-N)+N;C=A.min}else{if(B<=C&&B<A.min){if(C<A.min){continue}L=(A.min-C)/(B-C)*(L-N)+N;B=A.min}}if(C>=B&&C>A.max){if(B>A.max){continue}N=(A.max-C)/(B-C)*(L-N)+N;C=A.max}else{if(B>=C&&B>A.max){if(C>A.max){continue}L=(A.max-C)/(B-C)*(L-N)+N;B=A.max}}if(E!=J(C,A)||D!=M(N,K)+F){O.moveTo(J(C,A),M(N,K)+F)}E=J(B,A);D=M(L,K)+F;O.lineTo(E,D)}O.stroke()},plotLineArea:function(J,D){var S=J.data;if(S.length<2){return }var L,G=0,N=this.ctx,Q=J.xaxis,B=J.yaxis,E=this.tHoz.bind(this),M=this.tVert.bind(this),H=Math.min(Math.max(0,B.min),B.max),F=true;N.beginPath();for(var O=0;O<S.length-1;++O){var R=S[O][0],C=S[O][1],P=S[O+1][0],A=S[O+1][1];if(R<=P&&R<Q.min){if(P<Q.min){continue}C=(Q.min-R)/(P-R)*(A-C)+C;R=Q.min}else{if(P<=R&&P<Q.min){if(R<Q.min){continue}A=(Q.min-R)/(P-R)*(A-C)+C;P=Q.min}}if(R>=P&&R>Q.max){if(P>Q.max){continue}C=(Q.max-R)/(P-R)*(A-C)+C;R=Q.max}else{if(P>=R&&P>Q.max){if(R>Q.max){continue}A=(Q.max-R)/(P-R)*(A-C)+C;P=Q.max}}if(F){N.moveTo(E(R,Q),M(H,B)+D);F=false}if(C>=B.max&&A>=B.max){N.lineTo(E(R,Q),M(B.max,B)+D);N.lineTo(E(P,Q),M(B.max,B)+D);continue}else{if(C<=B.min&&A<=B.min){N.lineTo(E(R,Q),M(B.min,B)+D);N.lineTo(E(P,Q),M(B.min,B)+D);continue}}var I=R,K=P;if(C<=A&&C<B.min&&A>=B.min){R=(B.min-C)/(A-C)*(P-R)+R;C=B.min}else{if(A<=C&&A<B.min&&C>=B.min){P=(B.min-C)/(A-C)*(P-R)+R;A=B.min}}if(C>=A&&C>B.max&&A<=B.max){R=(B.max-C)/(A-C)*(P-R)+R;C=B.max}else{if(A>=C&&A>B.max&&C<=B.max){P=(B.max-C)/(A-C)*(P-R)+R;A=B.max}}if(R!=I){L=(C<=B.min)?L=B.min:B.max;N.lineTo(E(I,Q),M(L,B)+D);N.lineTo(E(R,Q),M(L,B)+D)}N.lineTo(E(R,Q),M(C,B)+D);N.lineTo(E(P,Q),M(A,B)+D);if(P!=K){L=(A<=B.min)?B.min:B.max;N.lineTo(E(K,Q),M(L,B)+D);N.lineTo(E(P,Q),M(L,B)+D)}G=Math.max(P,K)}N.lineTo(E(G,Q),M(H,B)+D);N.closePath();N.fill()},drawSeriesLines:function(C){C=C||this.series;var B=this.ctx;B.save();B.translate(this.plotOffset.left,this.plotOffset.top);B.lineJoin="round";var D=C.lines.lineWidth;var A=C.shadowSize;if(A>0){B.lineWidth=A/2;var E=D/2+B.lineWidth/2;B.strokeStyle="rgba(0,0,0,0.1)";this.plotLine(C,E+A/2);B.strokeStyle="rgba(0,0,0,0.2)";this.plotLine(C,E);if(C.lines.fill){B.fillStyle="rgba(0,0,0,0.05)";this.plotLineArea(C,E+A/2)}}B.lineWidth=D;B.strokeStyle=C.color;if(C.lines.fill){B.fillStyle=C.lines.fillColor!=null?C.lines.fillColor:Flotr.parseColor(C.color).scale(null,null,null,C.lines.fillOpacity).toString();this.plotLineArea(C,0)}this.plotLine(C,0);B.restore()},drawSeriesPoints:function(C){var B=this.ctx;B.save();B.translate(this.plotOffset.left,this.plotOffset.top);var D=C.lines.lineWidth;var A=C.shadowSize;if(A>0){B.lineWidth=A/2;B.strokeStyle="rgba(0,0,0,0.1)";this.plotPointShadows(C,A/2+B.lineWidth/2,C.points.radius);B.strokeStyle="rgba(0,0,0,0.2)";this.plotPointShadows(C,B.lineWidth/2,C.points.radius)}B.lineWidth=C.points.lineWidth;B.strokeStyle=C.color;B.fillStyle=C.points.fillColor!=null?C.points.fillColor:C.color;this.plotPoints(C,C.points.radius,C.points.fill);B.restore()},plotPoints:function(C,E,I){var A=C.xaxis,F=C.yaxis,J=this.ctx,D,B=C.data;for(D=B.length-1;D>-1;--D){var H=B[D][0],G=B[D][1];if(H<A.min||H>A.max||G<F.min||G>F.max){continue}J.beginPath();J.arc(this.tHoz(H,A),this.tVert(G,F),E,0,2*Math.PI,true);if(I){J.fill()}J.stroke()}},plotPointShadows:function(D,B,F){var A=D.xaxis,G=D.yaxis,J=this.ctx,E,C=D.data;for(E=C.length-1;E>-1;--E){var I=C[E][0],H=C[E][1];if(I<A.min||I>A.max||H<G.min||H>G.max){continue}J.beginPath();J.arc(this.tHoz(I,A),this.tVert(H,G)+B,F,0,Math.PI,false);J.stroke()}},drawSeriesBars:function(B){var A=this.ctx,D=B.bars.barWidth,C=Math.min(B.bars.lineWidth,D);A.save();A.translate(this.plotOffset.left,this.plotOffset.top);A.lineJoin="miter";A.lineWidth=C;A.strokeStyle=B.color;this.plotBarsShadows(B,D,0,B.bars.fill);if(B.bars.fill){A.fillStyle=B.bars.fillColor!=null?B.bars.fillColor:Flotr.parseColor(B.color).scale(null,null,null,B.bars.fillOpacity).toString()}this.plotBars(B,D,0,B.bars.fill);A.restore()},plotBars:function(K,N,D,Q){var U=K.data;if(U.length<1){return }var S=K.xaxis,B=K.yaxis,P=this.ctx,F=this.tHoz.bind(this),O=this.tVert.bind(this);for(var R=0;R<U.length;R++){var J=U[R][0],I=U[R][1];var E=true,L=true,A=true;var H=0;if(K.bars.stacked){S.values.each(function(W,V){if(V==J){H=W.stack||0;W.stack=H+I}})}if(K.bars.horizontal){var C=H,T=J+H,G=I,M=I+N}else{var C=J,T=J+N,G=H,M=I+H}if(T<S.min||C>S.max||M<B.min||G>B.max){continue}if(C<S.min){C=S.min;E=false}if(T>S.max){T=S.max;if(S.lastSerie!=K&&K.bars.horizontal){L=false}}if(G<B.min){G=B.min}if(M>B.max){M=B.max;if(B.lastSerie!=K&&!K.bars.horizontal){L=false}}if(Q){P.beginPath();P.moveTo(F(C,S),O(G,B)+D);P.lineTo(F(C,S),O(M,B)+D);P.lineTo(F(T,S),O(M,B)+D);P.lineTo(F(T,S),O(G,B)+D);P.fill()}if(K.bars.lineWidth!=0&&(E||A||L)){P.beginPath();P.moveTo(F(C,S),O(G,B)+D);P[E?"lineTo":"moveTo"](F(C,S),O(M,B)+D);P[L?"lineTo":"moveTo"](F(T,S),O(M,B)+D);P[A?"lineTo":"moveTo"](F(T,S),O(G,B)+D);P.stroke()}}},plotBarsShadows:function(I,K,C){var T=I.data;if(T.length<1){return }var R=I.xaxis,A=I.yaxis,P=this.ctx,D=this.tHoz.bind(this),M=this.tVert.bind(this),N=this.options.shadowSize;for(var Q=0;Q<T.length;Q++){var H=T[Q][0],G=T[Q][1];var E=0;if(I.bars.stacked){R.values.each(function(V,U){if(U==H){E=V.stackShadow||0;V.stackShadow=E+G}})}if(I.bars.horizontal){var B=E,S=H+E,F=G,J=G+K}else{var B=H,S=H+K,F=E,J=G+E}if(S<R.min||B>R.max||J<A.min||F>A.max){continue}if(B<R.min){B=R.min}if(S>R.max){S=R.max}if(F<A.min){F=A.min}if(J>A.max){J=A.max}var O=D(S,R)-D(B,R)-((D(S,R)+N<=this.plotWidth)?0:N);var L=Math.max(0,M(F,A)-M(J,A)-((M(F,A)+N<=this.plotHeight)?0:N));P.fillStyle="rgba(0,0,0,0.05)";P.fillRect(Math.min(D(B,R)+N,this.plotWidth),Math.min(M(J,A)+N,this.plotWidth),O,L)}},drawSeriesCandles:function(B){var A=this.ctx,C=B.candles.candleWidth;A.save();A.translate(this.plotOffset.left,this.plotOffset.top);A.lineJoin="miter";A.lineWidth=B.candles.lineWidth;this.plotCandlesShadows(B,C/2);this.plotCandles(B,C/2);A.restore()},plotCandles:function(K,D){var W=K.data;if(W.length<1){return }var T=K.xaxis,B=K.yaxis,P=this.ctx,E=this.tHoz.bind(this),O=this.tVert.bind(this);for(var S=0;S<W.length;S++){var U=W[S],J=U[0],L=U[1],I=U[2],X=U[3],N=U[4];var C=J,V=J+K.candles.candleWidth,G=Math.max(B.min,X),M=Math.min(B.max,I),A=Math.max(B.min,Math.min(L,N)),R=Math.min(B.max,Math.max(L,N));if(V<T.min||C>T.max||M<B.min||G>B.max){continue}var Q=K.candles[L>N?"downFillColor":"upFillColor"];if(K.candles.fill&&!K.candles.barcharts){P.fillStyle=Flotr.parseColor(Q).scale(null,null,null,K.candles.fillOpacity).toString();P.fillRect(E(C,T),O(R,B)+D,E(V,T)-E(C,T),O(A,B)-O(R,B))}if(K.candles.lineWidth||K.candles.wickLineWidth){var J,H,F=(K.candles.wickLineWidth%2)/2;J=Math.floor(E((C+V)/2),T)+F;P.save();P.strokeStyle=Q;P.lineWidth=K.candles.wickLineWidth;P.lineCap="butt";if(K.candles.barcharts){P.beginPath();P.moveTo(J,Math.floor(O(M,B)+D));P.lineTo(J,Math.floor(O(G,B)+D));H=Math.floor(O(L,B)+D)+0.5;P.moveTo(Math.floor(E(C,T))+F,H);P.lineTo(J,H);H=Math.floor(O(N,B)+D)+0.5;P.moveTo(Math.floor(E(V,T))+F,H);P.lineTo(J,H)}else{P.strokeRect(E(C,T),O(R,B)+D,E(V,T)-E(C,T),O(A,B)-O(R,B));P.beginPath();P.moveTo(J,Math.floor(O(R,B)+D));P.lineTo(J,Math.floor(O(M,B)+D));P.moveTo(J,Math.floor(O(A,B)+D));P.lineTo(J,Math.floor(O(G,B)+D))}P.stroke();P.restore()}}},plotCandlesShadows:function(H,C){var T=H.data;if(T.length<1||H.candles.barcharts){return }var Q=H.xaxis,A=H.yaxis,D=this.tHoz.bind(this),M=this.tVert.bind(this),N=this.options.shadowSize;for(var P=0;P<T.length;P++){var R=T[P],G=R[0],I=R[1],F=R[2],U=R[3],K=R[4];var B=G,S=G+H.candles.candleWidth,E=Math.max(A.min,Math.min(I,K)),J=Math.min(A.max,Math.max(I,K));if(S<Q.min||B>Q.max||J<A.min||E>A.max){continue}var O=D(S,Q)-D(B,Q)-((D(S,Q)+N<=this.plotWidth)?0:N);var L=Math.max(0,M(E,A)-M(J,A)-((M(E,A)+N<=this.plotHeight)?0:N));this.ctx.fillStyle="rgba(0,0,0,0.05)";this.ctx.fillRect(Math.min(D(B,Q)+N,this.plotWidth),Math.min(M(J,A)+N,this.plotWidth),O,L)}},drawSeriesPie:function(G){if(!this.options.pie.drawn){var K=this.ctx,C=this.options,E=G.pie.lineWidth,I=G.shadowSize,R=G.data,D=(Math.min(this.canvasWidth,this.canvasHeight)*G.pie.sizeRatio)/2,H=[];var L=1;var P=Math.sin(G.pie.viewAngle)*G.pie.spliceThickness/L;var M={size:C.fontSize*1.2,color:C.grid.color,weight:1.5};var Q={x:(this.canvasWidth+this.plotOffset.left)/2,y:(this.canvasHeight-this.plotOffset.bottom)/2};var O=this.series.collect(function(T,S){if(T.pie.show){return{name:(T.label||T.data[0][1]),value:[S,T.data[0][1]],explode:T.pie.explode}}});var B=O.pluck("value").pluck(1).inject(0,function(S,T){return S+T});var F=0,N=G.pie.startAngle,J=0;var A=O.collect(function(S){N+=F;J=parseFloat(S.value[1]);F=J/B;return{name:S.name,fraction:F,x:S.value[0],y:J,explode:S.explode,startAngle:2*N*Math.PI,endAngle:2*(N+F)*Math.PI}});K.save();if(I>0){A.each(function(V){var S=(V.startAngle+V.endAngle)/2;var T=Q.x+Math.cos(S)*V.explode+I;var U=Q.y+Math.sin(S)*V.explode+I;this.plotSlice(T,U,D,V.startAngle,V.endAngle,false,L);K.fillStyle="rgba(0,0,0,0.1)";K.fill()},this)}if(C.HtmlText){H=['<div style="color:'+this.options.grid.color+'" class="flotr-labels">']}A.each(function(c,X){var W=(c.startAngle+c.endAngle)/2;var V=C.colors[X];var Y=Q.x+Math.cos(W)*c.explode;var U=Q.y+Math.sin(W)*c.explode;this.plotSlice(Y,U,D,c.startAngle,c.endAngle,false,L);if(G.pie.fill){K.fillStyle=Flotr.parseColor(V).scale(null,null,null,G.pie.fillOpacity).toString();K.fill()}K.lineWidth=E;K.strokeStyle=V;K.stroke();var b=C.pie.labelFormatter(c);var S=(Math.cos(W)<0);var a=Y+Math.cos(W)*(G.pie.explode+D);var Z=U+Math.sin(W)*(G.pie.explode+D);if(c.fraction&&b){if(C.HtmlText){var T="position:absolute;top:"+(Z-5)+"px;";if(S){T+="right:"+(this.canvasWidth-a)+"px;text-align:right;"}else{T+="left:"+a+"px;text-align:left;"}H.push('<div style="'+T+'" class="flotr-grid-label">'+b+"</div>")}else{M.halign=S?"r":"l";K.drawText(b,a,Z+M.size/2,M)}}},this);if(C.HtmlText){H.push("</div>");this.el.insert(H.join(""))}K.restore();C.pie.drawn=true}},plotSlice:function(B,H,A,E,D,F,G){var C=this.ctx;G=G||1;C.save();C.scale(1,G);C.beginPath();C.moveTo(B,H);C.arc(B,H,A,E,D,F);C.lineTo(B,H);C.closePath();C.restore()},plotPie:function(){},insertLegend:function(){if(!this.options.legend.show){return }var H=this.series,I=this.plotOffset,B=this.options,b=[],A=false,O=this.ctx,R;var Q=H.findAll(function(c){return(c.label&&!c.hide)}).size();if(Q){if(!B.HtmlText&&this.textEnabled){var T={size:B.fontSize*1.1,color:B.grid.color};var M=B.legend.position,N=B.legend.margin,L=B.legend.labelBoxWidth,Z=B.legend.labelBoxHeight,S=B.legend.labelBoxMargin,W=I.left+N,U=I.top+N;var a=0;for(R=H.length-1;R>-1;--R){if(!H[R].label||H[R].hide){continue}var E=B.legend.labelFormatter(H[R].label);a=Math.max(a,O.measureText(E,T))}var K=Math.round(L+S*3+a),C=Math.round(Q*(S+Z)+S);if(M.charAt(0)=="s"){U=I.top+this.plotHeight-(N+C)}if(M.charAt(1)=="e"){W=I.left+this.plotWidth-(N+K)}var P=Flotr.parseColor(B.legend.backgroundColor||"rgb(240,240,240)").scale(null,null,null,B.legend.backgroundOpacity||0.1).toString();O.fillStyle=P;O.fillRect(W,U,K,C);O.strokeStyle=B.legend.labelBoxBorderColor;O.strokeRect(Flotr.toPixel(W),Flotr.toPixel(U),K,C);var G=W+S;var F=U+S;for(R=0;R<H.length;R++){if(!H[R].label||H[R].hide){continue}var E=B.legend.labelFormatter(H[R].label);O.fillStyle=H[R].color;O.fillRect(G,F,L-1,Z-1);O.strokeStyle=B.legend.labelBoxBorderColor;O.lineWidth=1;O.strokeRect(Math.ceil(G)-1.5,Math.ceil(F)-1.5,L+2,Z+2);O.drawText(E,G+L+S,F+(Z+T.size-O.fontDescent(T))/2,T);F+=Z+S}}else{for(R=0;R<H.length;++R){if(!H[R].label||H[R].hide){continue}if(R%B.legend.noColumns==0){b.push(A?"</tr><tr>":"<tr>");A=true}var E=B.legend.labelFormatter(H[R].label);b.push('<td class="flotr-legend-color-box"><div style="border:1px solid '+B.legend.labelBoxBorderColor+';padding:1px"><div style="width:'+B.legend.labelBoxWidth+"px;height:"+B.legend.labelBoxHeight+"px;background-color:"+H[R].color+'"></div></div></td><td class="flotr-legend-label">'+E+"</td>")}if(A){b.push("</tr>")}if(b.length>0){var V='<table style="font-size:smaller;color:'+B.grid.color+'">'+b.join("")+"</table>";if(B.legend.container!=null){$(B.legend.container).update(V)}else{var D="";var M=B.legend.position,N=B.legend.margin;if(M.charAt(0)=="n"){D+="top:"+(N+I.top)+"px;"}else{if(M.charAt(0)=="s"){D+="bottom:"+(N+I.bottom)+"px;"}}if(M.charAt(1)=="e"){D+="right:"+(N+I.right)+"px;"}else{if(M.charAt(1)=="w"){D+="left:"+(N+I.left)+"px;"}}var J=this.el.insert('<div class="flotr-legend" style="position:absolute;z-index:2;'+D+'">'+V+"</div>").select("div.flotr-legend").first();if(B.legend.backgroundOpacity!=0){var Y=B.legend.backgroundColor;if(Y==null){var X=(B.grid.backgroundColor!=null)?B.grid.backgroundColor:Flotr.extractColor(J);Y=Flotr.parseColor(X).adjust(null,null,null,1).toString()}this.el.insert('<div class="flotr-legend-bg" style="position:absolute;width:'+J.getWidth()+"px;height:"+J.getHeight()+"px;"+D+"background-color:"+Y+';"> </div>').select("div.flotr-legend-bg").first().setStyle({opacity:B.legend.backgroundOpacity})}}}}}},getEventPosition:function(C){var G=this.overlay.cumulativeOffset(),F=(C.pageX-G.left-this.plotOffset.left),E=(C.pageY-G.top-this.plotOffset.top),D=0,B=0;if(C.pageX==null&&C.clientX!=null){var H=document.documentElement,A=document.body;D=C.clientX+(H&&H.scrollLeft||A.scrollLeft||0);B=C.clientY+(H&&H.scrollTop||A.scrollTop||0)}else{D=C.pageX;B=C.pageY}return{x:this.axes.x.min+F/this.axes.x.scale,x2:this.axes.x2.min+F/this.axes.x2.scale,y:this.axes.y.max-E/this.axes.y.scale,y2:this.axes.y2.max-E/this.axes.y2.scale,relX:F,relY:E,absX:D,absY:B}},clickHandler:function(A){if(this.ignoreClick){this.ignoreClick=false;return }this.el.fire("flotr:click",[this.getEventPosition(A),this])},mouseMoveHandler:function(A){var B=this.getEventPosition(A);this.lastMousePos.pageX=B.absX;this.lastMousePos.pageY=B.absY;if(this.selectionInterval==null&&(this.options.mouse.track||this.series.any(function(C){return C.mouse&&C.mouse.track}))){this.hit(B)}this.el.fire("flotr:mousemove",[A,B,this])},mouseDownHandler:function(C){if(C.isRightClick()){C.stop();var B=this.overlay;B.hide();function A(){B.show();$(document).stopObserving("mousemove",A)}$(document).observe("mousemove",A);return }if(!this.options.selection.mode||!C.isLeftClick()){return }this.setSelectionPos(this.selection.first,C);if(this.selectionInterval!=null){clearInterval(this.selectionInterval)}this.lastMousePos.pageX=null;this.selectionInterval=setInterval(this.updateSelection.bind(this),1000/this.options.selection.fps);this.mouseUpHandler=this.mouseUpHandler.bind(this);$(document).observe("mouseup",this.mouseUpHandler)},fireSelectEvent:function(){var A=this.axes,F=this.selection,C=(F.first.x<=F.second.x)?F.first.x:F.second.x,B=(F.first.x<=F.second.x)?F.second.x:F.first.x,E=(F.first.y>=F.second.y)?F.first.y:F.second.y,D=(F.first.y>=F.second.y)?F.second.y:F.first.y;C=A.x.min+C/A.x.scale;B=A.x.min+B/A.x.scale;E=A.y.max-E/A.y.scale;D=A.y.max-D/A.y.scale;this.el.fire("flotr:select",[{x1:C,y1:E,x2:B,y2:D},this])},mouseUpHandler:function(A){$(document).stopObserving("mouseup",this.mouseUpHandler);A.stop();if(this.selectionInterval!=null){clearInterval(this.selectionInterval);this.selectionInterval=null}this.setSelectionPos(this.selection.second,A);this.clearSelection();if(this.selectionIsSane()){this.drawSelection();this.fireSelectEvent();this.ignoreClick=true}},setSelectionPos:function(D,B){var A=this.options,C=$(this.overlay).cumulativeOffset();if(A.selection.mode.indexOf("x")==-1){D.x=(D==this.selection.first)?0:this.plotWidth}else{D.x=B.pageX-C.left-this.plotOffset.left;D.x=Math.min(Math.max(0,D.x),this.plotWidth)}if(A.selection.mode.indexOf("y")==-1){D.y=(D==this.selection.first)?0:this.plotHeight}else{D.y=B.pageY-C.top-this.plotOffset.top;D.y=Math.min(Math.max(0,D.y),this.plotHeight)}},updateSelection:function(){if(this.lastMousePos.pageX==null){return }this.setSelectionPos(this.selection.second,this.lastMousePos);this.clearSelection();if(this.selectionIsSane()){this.drawSelection()}},clearSelection:function(){if(this.prevSelection==null){return }var G=this.prevSelection,E=this.octx,C=this.plotOffset,A=Math.min(G.first.x,G.second.x),F=Math.min(G.first.y,G.second.y),B=Math.abs(G.second.x-G.first.x),D=Math.abs(G.second.y-G.first.y);E.clearRect(A+C.left-E.lineWidth,F+C.top-E.lineWidth,B+E.lineWidth*2,D+E.lineWidth*2);this.prevSelection=null},setSelection:function(G){var B=this.options,H=this.axes.x,A=this.axes.y,F=yaxis.scale,D=xaxis.scale,E=B.selection.mode.indexOf("x")!=-1,C=B.selection.mode.indexOf("y")!=-1;this.clearSelection();this.selection.first.y=E?0:(A.max-G.y1)*F;this.selection.second.y=E?this.plotHeight:(A.max-G.y2)*F;this.selection.first.x=C?0:(G.x1-H.min)*D;this.selection.second.x=C?this.plotWidth:(G.x2-H.min)*D;this.drawSelection();this.fireSelectEvent()},drawSelection:function(){var C=this.prevSelection,F=this.selection,H=this.octx,I=this.options,A=this.plotOffset;if(C!=null&&F.first.x==C.first.x&&F.first.y==C.first.y&&F.second.x==C.second.x&&F.second.y==C.second.y){return }H.strokeStyle=Flotr.parseColor(I.selection.color).scale(null,null,null,0.8).toString();H.lineWidth=1;H.lineJoin="round";H.fillStyle=Flotr.parseColor(I.selection.color).scale(null,null,null,0.4).toString();this.prevSelection={first:{x:F.first.x,y:F.first.y},second:{x:F.second.x,y:F.second.y}};var E=Math.min(F.first.x,F.second.x),D=Math.min(F.first.y,F.second.y),G=Math.abs(F.second.x-F.first.x),B=Math.abs(F.second.y-F.first.y);H.fillRect(E+A.left,D+A.top,G,B);H.strokeRect(E+A.left,D+A.top,G,B)},selectionIsSane:function(){var A=this.selection;return Math.abs(A.second.x-A.first.x)>=5&&Math.abs(A.second.y-A.first.y)>=5},clearHit:function(){if(this.prevHit){var B=this.options,A=this.plotOffset,C=this.prevHit;this.octx.clearRect(this.tHoz(C.x)+A.left-B.points.radius*2,this.tVert(C.y)+A.top-B.points.radius*2,B.points.radius*3+B.points.lineWidth*3,B.points.radius*3+B.points.lineWidth*3);this.prevHit=null}},hit:function(I){var G=this.series,C=this.options,R=this.prevHit,H=this.plotOffset,D=this.octx,S,A,M,Q,L={dist:Number.MAX_VALUE,x:null,y:null,relX:I.relX,relY:I.relY,absX:I.absX,absY:I.absY,mouse:null};for(Q=0;Q<G.length;Q++){s=G[Q];if(!s.mouse.track){continue}S=s.data;A=(s.xaxis.scale*s.mouse.sensibility);M=(s.yaxis.scale*s.mouse.sensibility);for(var P=0,B,E;P<S.length;P++){if(S[P][1]===null){continue}B=Math.pow(s.xaxis.scale*(S[P][0]-I.x),2);E=Math.pow(s.yaxis.scale*(S[P][1]-I.y),2);if(B<A&&E<M&&Math.sqrt(B+E)<L.dist){L.dist=Math.sqrt(B+E);L.x=S[P][0];L.y=S[P][1];L.mouse=s.mouse}}}if(L.mouse&&L.mouse.track&&!R||(R&&(L.x!=R.x||L.y!=R.y))){var K=this.mouseTrack||this.el.select(".flotr-mouse-value")[0],F="",J=C.mouse.position,N=C.mouse.margin,O="opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;";if(!C.mouse.relative){if(J.charAt(0)=="n"){F+="top:"+(N+H.top)+"px;"}else{if(J.charAt(0)=="s"){F+="bottom:"+(N+H.bottom)+"px;"}}if(J.charAt(1)=="e"){F+="right:"+(N+H.right)+"px;"}else{if(J.charAt(1)=="w"){F+="left:"+(N+H.left)+"px;"}}}else{if(J.charAt(0)=="n"){F+="bottom:"+(N-H.top-this.tVert(L.y)+this.canvasHeight)+"px;"}else{if(J.charAt(0)=="s"){F+="top:"+(N+H.top+this.tVert(L.y))+"px;"}}if(J.charAt(1)=="e"){F+="left:"+(N+H.left+this.tHoz(L.x))+"px;"}else{if(J.charAt(1)=="w"){F+="right:"+(N-H.left-this.tHoz(L.x)+this.canvasWidth)+"px;"}}}O+=F;if(!K){this.el.insert('<div class="flotr-mouse-value" style="'+O+'"></div>');K=this.mouseTrack=this.el.select(".flotr-mouse-value").first()}else{this.mouseTrack=K.setStyle(O)}if(L.x!==null&&L.y!==null){K.show();this.clearHit();if(L.mouse.lineColor!=null){D.save();D.translate(H.left,H.top);D.lineWidth=C.points.lineWidth;D.strokeStyle=L.mouse.lineColor;D.fillStyle="#ffffff";D.beginPath();D.arc(this.tHoz(L.x),this.tVert(L.y),C.mouse.radius,0,2*Math.PI,true);D.fill();D.stroke();D.restore()}this.prevHit=L;var T=L.mouse.trackDecimals;if(T==null||T<0){T=0}K.innerHTML=L.mouse.trackFormatter({x:L.x.toFixed(T),y:L.y.toFixed(T)});K.fire("flotr:hit",[L,this])}else{if(R){K.hide();this.clearHit()}}}},saveImage:function(D,C,A,B){var E=null;switch(D){case"jpeg":case"jpg":E=Canvas2Image.saveAsJPEG(this.canvas,B,C,A);break;default:case"png":E=Canvas2Image.saveAsPNG(this.canvas,B,C,A);break;case"bmp":E=Canvas2Image.saveAsBMP(this.canvas,B,C,A);break}if(Object.isElement(E)&&B){this.restoreCanvas();this.canvas.hide();this.overlay.hide();this.el.insert(E.setStyle({position:"absolute"}))}},restoreCanvas:function(){this.canvas.show();this.overlay.show();this.el.select("img").invoke("remove")}});Flotr.Color=Class.create({initialize:function(E,D,B,C){this.rgba=["r","g","b","a"];var A=4;while(-1<--A){this[this.rgba[A]]=arguments[A]||((A==3)?1:0)}this.normalize()},adjust:function(D,C,E,B){var A=4;while(-1<--A){if(arguments[A]!=null){this[this.rgba[A]]+=arguments[A]}}return this.normalize()},clone:function(){return new Flotr.Color(this.r,this.b,this.g,this.a)},limit:function(B,A,C){return Math.max(Math.min(B,C),A)},normalize:function(){var A=this.limit;this.r=A(parseInt(this.r),0,255);this.g=A(parseInt(this.g),0,255);this.b=A(parseInt(this.b),0,255);this.a=A(this.a,0,1);return this},scale:function(D,C,E,B){var A=4;while(-1<--A){if(arguments[A]!=null){this[this.rgba[A]]*=arguments[A]}}return this.normalize()},distance:function(B){if(!B){return }B=new Flotr.parseColor(B);var C=0;var A=3;while(-1<--A){C+=Math.abs(this[this.rgba[A]]-B[this.rgba[A]])}return C},toString:function(){return(this.a>=1)?"rgb("+[this.r,this.g,this.b].join(",")+")":"rgba("+[this.r,this.g,this.b,this.a].join(",")+")"}});Flotr.Color.lookupColors={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]};Flotr.Date={format:function(F,E){if(!F){return }var A=function(H){H=H.toString();return H.length==1?"0"+H:H};var D=[];var C=false;for(var B=0;B<E.length;++B){var G=E.charAt(B);if(C){switch(G){case"h":G=F.getUTCHours().toString();break;case"H":G=A(F.getUTCHours());break;case"M":G=A(F.getUTCMinutes());break;case"S":G=A(F.getUTCSeconds());break;case"d":G=F.getUTCDate().toString();break;case"m":G=(F.getUTCMonth()+1).toString();break;case"y":G=F.getUTCFullYear().toString();break;case"b":G=Flotr.Date.monthNames[F.getUTCMonth()];break}D.push(G);C=false}else{if(G=="%"){C=true}else{D.push(G)}}}return D.join("")},timeUnits:{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},spec:[[1,"second"],[2,"second"],[5,"second"],[10,"second"],[30,"second"],[1,"minute"],[2,"minute"],[5,"minute"],[10,"minute"],[30,"minute"],[1,"hour"],[2,"hour"],[4,"hour"],[8,"hour"],[12,"hour"],[1,"day"],[2,"day"],[3,"day"],[0.25,"month"],[0.5,"month"],[1,"month"],[2,"month"],[3,"month"],[6,"month"],[1,"year"]],monthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]};

--- /dev/null
+++ b/js/flotr/flotr.debug-0.2.0-alpha_radar1.js
@@ -1,1 +1,3349 @@
+//Flotr 0.2.0-alpha Copyright (c) 2009 Bas Wenneker, <http://solutoire.com>, MIT License.

+//

+//Radar chart added by Ryan Simmons

+//
+/* $Id: flotr.js 82 2009-01-12 19:19:31Z fabien.menager $ */

+

+var Flotr = {

+	version: '0.2.0-alpha',

+	author: 'Bas Wenneker',

+	website: 'http://www.solutoire.com',

+	/**

+	 * An object of the default registered graph types. Use Flotr.register(type, functionName)

+	 * to add your own type.

+	 */

+	_registeredTypes:{

+		'lines': 'drawSeriesLines',

+		'points': 'drawSeriesPoints',

+		'bars': 'drawSeriesBars',

+		'candles': 'drawSeriesCandles',

+		'pie': 'drawSeriesPie',

+		'radar':'drawSeriesRadar'

+	},

+	/**

+	 * Can be used to register your own chart type. Default types are 'lines', 'points' and 'bars'.

+	 * This is still experimental.

+	 * @todo Test and confirm.

+	 * @param {String} type - type of chart, like 'pies', 'bars' etc.

+	 * @param {String} functionName - Name of the draw function, like 'drawSeriesPies', 'drawSeriesBars' etc.

+	 */

+	register: function(type, functionName){

+		Flotr._registeredTypes[type] = functionName+'';	

+	},

+	/**

+	 * Draws the graph. This function is here for backwards compatibility with Flotr version 0.1.0alpha.

+	 * You could also draw graphs by directly calling Flotr.Graph(element, data, options).

+	 * @param {Element} el - element to insert the graph into

+	 * @param {Object} data - an array or object of dataseries

+	 * @param {Object} options - an object containing options

+	 * @param {Class} _GraphKlass_ - (optional) Class to pass the arguments to, defaults to Flotr.Graph

+	 * @return {Class} returns a new graph object and of course draws the graph.

+	 */

+	draw: function(el, data, options, _GraphKlass_){	

+		_GraphKlass_ = _GraphKlass_ || Flotr.Graph;

+		return new _GraphKlass_(el, data, options);

+	},

+	/**

+	 * Collects dataseries from input and parses the series into the right format. It returns an Array 

+	 * of Objects each having at least the 'data' key set.

+	 * @param {Array/Object} data - Object or array of dataseries

+	 * @return {Array} Array of Objects parsed into the right format ({(...,) data: [[x1,y1], [x2,y2], ...] (, ...)})

+	 */

+	getSeries: function(data){

+		return data.collect(function(serie){

+			var i, serie = (serie.data) ? Object.clone(serie) : {'data': serie};

+			for (i = serie.data.length-1; i > -1; --i) {

+				serie.data[i][1] = (serie.data[i][1] === null ? null : parseFloat(serie.data[i][1])); 

+			}

+			return serie;

+		});

+	},

+	/**

+	 * Recursively merges two objects.

+	 * @param {Object} src - source object (likely the object with the least properties)

+	 * @param {Object} dest - destination object (optional, object with the most properties)

+	 * @return {Object} recursively merged Object

+	 */

+	merge: function(src, dest){

+		var result = dest || {};

+		for(var i in src){

+			result[i] = (src[i] != null && typeof(src[i]) == 'object' && !(src[i].constructor == Array || src[i].constructor == RegExp) && !Object.isElement(src[i])) ? Flotr.merge(src[i], dest[i]) : result[i] = src[i];		

+		}

+		return result;

+	},	

+	/**

+	 * Function calculates the ticksize and returns it.

+	 * @param {Integer} noTicks - number of ticks

+	 * @param {Integer} min - lower bound integer value for the current axis

+	 * @param {Integer} max - upper bound integer value for the current axis

+	 * @param {Integer} decimals - number of decimals for the ticks

+	 * @return {Integer} returns the ticksize in pixels

+	 */

+	getTickSize: function(noTicks, min, max, decimals){

+		var delta = (max - min) / noTicks;	

+		var magn = Flotr.getMagnitude(delta);

+		

+		// Norm is between 1.0 and 10.0.

+		var norm = delta / magn;

+		

+		var tickSize = 10;

+		if(norm < 1.5) tickSize = 1;

+		else if(norm < 2.25) tickSize = 2;

+		else if(norm < 3) tickSize = ((decimals == 0) ? 2 : 2.5);

+		else if(norm < 7.5) tickSize = 5;

+		

+		return tickSize * magn;

+	},

+	/**

+	 * Default tick formatter.

+	 * @param {String/Integer} val - tick value integer

+	 * @return {String} formatted tick string

+	 */

+	defaultTickFormatter: function(val){

+		return val+'';

+	},

+	/**

+	 * Formats the mouse tracker values.

+	 * @param {Object} obj - Track value Object {x:..,y:..}

+	 * @return {String} Formatted track string

+	 */

+	defaultTrackFormatter: function(obj){

+		return '('+obj.x+', '+obj.y+')';

+	}, 

+	defaultPieLabelFormatter: function(slice) {

+	  return (slice.fraction*100).toFixed(2)+'%';

+	},

+	/**

+	 * Returns the magnitude of the input value.

+	 * @param {Integer/Float} x - integer or float value

+	 * @return {Integer/Float} returns the magnitude of the input value

+	 */

+	getMagnitude: function(x){

+		return Math.pow(10, Math.floor(Math.log(x) / Math.LN10));

+	},

+	toPixel: function(val){

+		return Math.floor(val)+0.5;//((val-Math.round(val) < 0.4) ? (Math.floor(val)-0.5) : val);

+	},

+	toRad: function(angle){

+		return -angle * (Math.PI/180);

+	},

+	/**

+	 * Parses a color string and returns a corresponding Color.

+	 * @param {String} str - string thats representing a color

+	 * @return {Color} returns a Color object or false

+	 */

+	parseColor: function(str){

+		if (str instanceof Flotr.Color) return str;

+		

+		var result, Color = Flotr.Color;

+

+		// rgb(num,num,num)

+		if((result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(str)))

+			return new Color(parseInt(result[1]), parseInt(result[2]), parseInt(result[3]));

+	

+		// rgba(num,num,num,num)

+		if((result = /rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)))

+			return new Color(parseInt(result[1]), parseInt(result[2]), parseInt(result[3]), parseFloat(result[4]));

+			

+		// rgb(num%,num%,num%)

+		if((result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(str)))

+			return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55);

+	

+		// rgba(num%,num%,num%,num)

+		if((result = /rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(str)))

+			return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4]));

+			

+		// #a0b1c2

+		if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(str)))

+			return new Color(parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16));

+	

+		// #fff

+		if((result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(str)))

+			return new Color(parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16));

+

+		// Otherwise, we're most likely dealing with a named color.

+		var name = str.strip().toLowerCase();

+		if(name == 'transparent'){

+			return new Color(255, 255, 255, 0);

+		}

+		return ((result = Color.lookupColors[name])) ? new Color(result[0], result[1], result[2]) : false;

+	},

+	/**

+	 * Extracts the background-color of the passed element.

+	 * @param {Element} element

+	 * @return {String} color string

+	 */

+	extractColor: function(element){

+		var color;

+		// Loop until we find an element with a background color and stop when we hit the body element. 

+		do {

+			color = element.getStyle('background-color').toLowerCase();

+			if(!(color == '' || color == 'transparent')) break;

+			element = element.up(0);

+		} while(!element.nodeName.match(/^body$/i));

+

+		// Catch Safari's way of signaling transparent.

+		return (color == 'rgba(0, 0, 0, 0)') ? 'transparent' : color;

+	}

+};

+/**

+ * Flotr Graph class that plots a graph on creation.

+

+ */

+Flotr.Graph = Class.create({

+	/**

+	 * Flotr Graph constructor.

+	 * @param {Element} el - element to insert the graph into

+	 * @param {Object} data - an array or object of dataseries

+ 	 * @param {Object} options - an object containing options

+	 */

+	initialize: function(el, data, options){

+		this.el = $(el);

+		

+		if (!this.el) throw 'The target container doesn\'t exist';

+		

+		this.data = data;

+		this.series = Flotr.getSeries(data);

+		this.setOptions(options);

+		

+		// Initialize some variables

+		this.lastMousePos = { pageX: null, pageY: null };

+		this.selection = { first: { x: -1, y: -1}, second: { x: -1, y: -1} };

+		this.prevSelection = null;

+		this.selectionInterval = null;

+		this.ignoreClick = false;   

+		this.prevHit = null;

+    

+		// Create and prepare canvas.

+		this.constructCanvas();

+		

+		// Add event handlers for mouse tracking, clicking and selection

+		this.initEvents();

+		

+		this.findDataRanges();

+		this.calculateTicks(this.axes.x);

+		this.calculateTicks(this.axes.x2);

+		this.calculateTicks(this.axes.y);

+		this.calculateTicks(this.axes.y2);

+		

+		this.calculateSpacing();

+		this.draw();

+		this.insertLegend();

+    

+		// Graph and Data tabs

+		if (this.options.spreadsheet.show) 

+		this.constructTabs();

+	},

+	/**

+	 * Sets options and initializes some variables and color specific values, used by the constructor. 

+	 * @param {Object} opts - options object

+	 */

+  setOptions: function(opts){

+    var options = {

+      colors: ['#00A8F0', '#C0D800', '#CB4B4B', '#4DA74D', '#9440ED'], //=> The default colorscheme. When there are > 5 series, additional colors are generated.

+      title: null,

+      subtitle: null,

+      legend: {

+        show: true,            // => setting to true will show the legend, hide otherwise

+        noColumns: 1,          // => number of colums in legend table // @todo: doesn't work for HtmlText = false

+        labelFormatter: Prototype.K, // => fn: string -> string

+        labelBoxBorderColor: '#CCCCCC', // => border color for the little label boxes

+        labelBoxWidth: 14,

+        labelBoxHeight: 10,

+        labelBoxMargin: 5,

+        container: null,       // => container (as jQuery object) to put legend in, null means default on top of graph

+        position: 'nw',        // => position of default legend container within plot

+        margin: 5,             // => distance from grid edge to default legend container within plot

+        backgroundColor: null, // => null means auto-detect

+        backgroundOpacity: 0.85// => set to 0 to avoid background, set to 1 for a solid background

+      },

+      xaxis: {

+        ticks: null,           // => format: either [1, 3] or [[1, 'a'], 3]

+        showLabels: true,      // => setting to true will show the axis ticks labels, hide otherwise

+        labelsAngle: 0,        // => Labels' angle, in degrees

+        title: null,           // => axis title

+        titleAngle: 0,         // => axis title's angle, in degrees

+        noTicks: 5,            // => number of ticks for automagically generated ticks

+        tickFormatter: Flotr.defaultTickFormatter, // => fn: number -> string

+        tickDecimals: null,    // => no. of decimals, null means auto

+        min: null,             // => min. value to show, null means set automatically

+        max: null,             // => max. value to show, null means set automatically

+        autoscaleMargin: 0,    // => margin in % to add if auto-setting min/max

+        color: null

+      },

+      x2axis: {},

+      yaxis: {

+        ticks: null,           // => format: either [1, 3] or [[1, 'a'], 3]

+        showLabels: true,      // => setting to true will show the axis ticks labels, hide otherwise

+        labelsAngle: 0,        // => Labels' angle, in degrees

+        title: null,           // => axis title

+        titleAngle: 90,        // => axis title's angle, in degrees

+        noTicks: 5,            // => number of ticks for automagically generated ticks

+        tickFormatter: Flotr.defaultTickFormatter, // => fn: number -> string

+        tickDecimals: null,    // => no. of decimals, null means auto

+        min: null,             // => min. value to show, null means set automatically

+        max: null,             // => max. value to show, null means set automatically

+        autoscaleMargin: 0,    // => margin in % to add if auto-setting min/max

+        color: null

+      },

+      y2axis: {

+      	titleAngle: 270

+      },

+      points: {

+        show: false,           // => setting to true will show points, false will hide

+        radius: 3,             // => point radius (pixels)

+        lineWidth: 2,          // => line width in pixels

+        fill: true,            // => true to fill the points with a color, false for (transparent) no fill

+        fillColor: '#FFFFFF',  // => fill color

+        fillOpacity: 0.4

+      },

+      lines: {

+        show: false,           // => setting to true will show lines, false will hide

+        lineWidth: 2,          // => line width in pixels

+        fill: false,           // => true to fill the area from the line to the x axis, false for (transparent) no fill

+        fillColor: null,       // => fill color

+        fillOpacity: 0.4       // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill

+      },

+      radar: {

+        show: false,           // => setting to true will show radar chart, false will hide

+        lineWidth: 2,          // => line width in pixels

+        fill: false,           // => true to fill the area from the line to the x axis, false for (transparent) no fill

+        fillColor: null,       // => fill color

+        fillOpacity: 0.4       // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill

+      },

+      bars: {

+        show: false,           // => setting to true will show bars, false will hide

+        lineWidth: 2,          // => in pixels

+        barWidth: 1,           // => in units of the x axis

+        fill: true,            // => true to fill the area from the line to the x axis, false for (transparent) no fill

+        fillColor: null,       // => fill color

+        fillOpacity: 0.4,      // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill

+        horizontal: false,

+        stacked: false

+      },

+      candles: {

+        show: false,           // => setting to true will show candle sticks, false will hide

+        lineWidth: 1,          // => in pixels

+        wickLineWidth: 1,      // => in pixels

+        candleWidth: 0.6,      // => in units of the x axis

+        fill: true,            // => true to fill the area from the line to the x axis, false for (transparent) no fill

+        upFillColor: '#00A8F0',// => up sticks fill color

+        downFillColor: '#CB4B4B',// => down sticks fill color

+        fillOpacity: 0.5,      // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill

+        barcharts: false       // => draw as barcharts (not standard bars but financial barcharts)

+      },

+      pie: {

+        show: false,           // => setting to true will show bars, false will hide

+        lineWidth: 1,          // => in pixels

+        fill: true,            // => true to fill the area from the line to the x axis, false for (transparent) no fill

+        fillColor: null,       // => fill color

+        fillOpacity: 0.6,      // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill

+        explode: 6,

+        sizeRatio: 0.6,

+        startAngle: Math.PI/4,

+        labelFormatter: Flotr.defaultPieLabelFormatter,

+        pie3D: false,

+        pie3DviewAngle: (Math.PI/2 * 0.8),

+        pie3DspliceThickness: 20

+      },

+      grid: {

+        color: '#545454',      // => primary color used for outline and labels

+        backgroundColor: null, // => null for transparent, else color

+        tickColor: '#DDDDDD',  // => color used for the ticks

+        labelMargin: 3,        // => margin in pixels

+        verticalLines: true,   // => whether to show gridlines in vertical direction

+        horizontalLines: true, // => whether to show gridlines in horizontal direction

+        outlineWidth: 2        // => width of the grid outline/border in pixels

+      },

+      selection: {

+        mode: null,            // => one of null, 'x', 'y' or 'xy'

+        color: '#B6D9FF',      // => selection box color

+        fps: 20                // => frames-per-second

+      },

+      mouse: {

+        track: false,          // => true to track the mouse, no tracking otherwise

+        position: 'se',        // => position of the value box (default south-east)

+        relative: false,       // => next to the mouse cursor

+        trackFormatter: Flotr.defaultTrackFormatter, // => formats the values in the value box

+        margin: 5,             // => margin in pixels of the valuebox

+        lineColor: '#FF3F19',  // => line color of points that are drawn when mouse comes near a value of a series

+        trackDecimals: 1,      // => decimals for the track values

+        sensibility: 2,        // => the lower this number, the more precise you have to aim to show a value

+        radius: 3              // => radius of the track point

+      },

+      radarChartMode: false, // => true to render radar grid / and setup scaling for radar chart

+      shadowSize: 4,           // => size of the 'fake' shadow

+      defaultType: 'lines',    // => default series type

+      HtmlText: true,          // => wether to draw the text using HTML or on the canvas

+      fontSize: 7.5,             // => canvas' text font size

+      spreadsheet: {

+      	show: false,           // => show the data grid using two tabs

+      	tabGraphLabel: 'Graph',

+      	tabDataLabel: 'Data',

+      	toolbarDownload: 'Download CSV', // @todo: add language support

+      	toolbarSelectAll: 'Select all'

+      }

+    }

+    

+    options.x2axis = Object.extend(Object.clone(options.xaxis), options.x2axis);

+    options.y2axis = Object.extend(Object.clone(options.yaxis), options.y2axis);

+    this.options = Flotr.merge((opts || {}), options);

+    

+    this.axes = {

+      x:  {options: this.options.xaxis,  n: 1}, 

+      x2: {options: this.options.x2axis, n: 2}, 

+      y:  {options: this.options.yaxis,  n: 1}, 

+      y2: {options: this.options.y2axis, n: 2}

+    };

+		

+		// Initialize some variables used throughout this function.

+		var assignedColors = [],

+		    colors = [],

+		    ln = this.series.length,

+		    neededColors = this.series.length,

+		    oc = this.options.colors, 

+		    usedColors = [],

+		    variation = 0,

+		    c, i, j, s, tooClose;

+

+		// Collect user-defined colors from series.

+		for(i = neededColors - 1; i > -1; --i){

+			c = this.series[i].color;

+			if(c != null){

+				--neededColors;

+				if(Object.isNumber(c)) assignedColors.push(c);

+				else usedColors.push(Flotr.parseColor(c));

+			}

+		}

+		

+		// Calculate the number of colors that need to be generated.

+		for(i = assignedColors.length - 1; i > -1; --i)

+			neededColors = Math.max(neededColors, assignedColors[i] + 1);

+

+		// Generate needed number of colors.

+		for(i = 0; colors.length < neededColors;){

+			c = (oc.length == i) ? new Flotr.Color(100, 100, 100) : Flotr.parseColor(oc[i]);

+			

+			// Make sure each serie gets a different color.

+			var sign = variation % 2 == 1 ? -1 : 1;

+			var factor = 1 + sign * Math.ceil(variation / 2) * 0.2;

+			c.scale(factor, factor, factor);

+

+			/**

+			 * @todo if we're getting too close to something else, we should probably skip this one

+			 */

+			colors.push(c);

+			

+			if(++i >= oc.length){

+				i = 0;

+				++variation;

+			}

+		}

+	

+		// Fill the options with the generated colors.

+		for(i = 0, j = 0; i < ln; ++i){

+			s = this.series[i];

+

+			// Assign the color.

+			if(s.color == null){

+				s.color = colors[j++].toString();

+			}else if(Object.isNumber(s.color)){

+				s.color = colors[s.color].toString();

+			}

+			

+      if (!s.xaxis) s.xaxis = this.axes.x;

+           if (s.xaxis == 1) s.xaxis = this.axes.x;

+      else if (s.xaxis == 2) s.xaxis = this.axes.x2;

+  

+      if (!s.yaxis) s.yaxis = this.axes.y;

+           if (s.yaxis == 1) s.yaxis = this.axes.y;

+      else if (s.yaxis == 2) s.yaxis = this.axes.y2;

+			

+			// Apply missing options to the series.

+			s.lines   = Object.extend(Object.clone(this.options.lines), s.lines);

+			s.points  = Object.extend(Object.clone(this.options.points), s.points);

+			s.bars    = Object.extend(Object.clone(this.options.bars), s.bars);

+			s.candles = Object.extend(Object.clone(this.options.candles), s.candles);

+			s.pie     = Object.extend(Object.clone(this.options.pie), s.pie);

+			s.radar   = Object.extend(Object.clone(this.options.radar), s.radar);

+			s.mouse   = Object.extend(Object.clone(this.options.mouse), s.mouse);

+			

+			if(s.shadowSize == null) s.shadowSize = this.options.shadowSize;

+		}

+	},

+	/**

+	 * Initializes the canvas and it's overlay canvas element. When the browser is IE, this makes use 

+	 * of excanvas. The overlay canvas is inserted for displaying interactions. After the canvas elements

+	 * are created, the elements are inserted into the container element.

+	 */

+	constructCanvas: function(){

+		var el = this.el,

+			size, c, oc;

+		

+  	this.canvas = el.select('.flotr-canvas')[0];

+		this.overlay = el.select('.flotr-overlay')[0];

+		

+		el.childElements().invoke('remove');

+

+		// For positioning labels and overlay.

+		el.setStyle({position:'relative', cursor:'default'});

+

+		this.canvasWidth = el.getWidth();

+		this.canvasHeight = el.getHeight();

+		size = {'width': this.canvasWidth, 'height': this.canvasHeight};

+

+		if(this.canvasWidth <= 0 || this.canvasHeight <= 0){

+			throw 'Invalid dimensions for plot, width = ' + this.canvasWidth + ', height = ' + this.canvasHeight;

+		}

+

+		// Insert main canvas.

+		if (!this.canvas) {

+			c = this.canvas = new Element('canvas', size);

+			c.className = 'flotr-canvas';

+			c = c.writeAttribute('style', 'position:absolute;left:0px;top:0px;');

+		} else {

+			c = this.canvas.writeAttribute(size);

+		}

+		el.insert(c);

+		

+		if(Prototype.Browser.IE){

+			c = window.G_vmlCanvasManager.initElement(c);

+		}

+		this.ctx = c.getContext('2d');

+    

+		// Insert overlay canvas for interactive features.

+		if (!this.overlay) {

+			oc = this.overlay = new Element('canvas', size);

+			oc.className = 'flotr-overlay';

+			oc = oc.writeAttribute('style', 'position:absolute;left:0px;top:0px;');

+		} else {

+			oc = this.overlay.writeAttribute(size);

+		}

+		el.insert(oc);

+		

+		if(Prototype.Browser.IE){

+			oc = window.G_vmlCanvasManager.initElement(oc);

+		}

+		this.octx = oc.getContext('2d');

+

+		// Enable text functions

+		if (window.CanvasText) {

+		  CanvasText.enable(this.ctx);

+		  CanvasText.enable(this.octx);

+		  this.textEnabled = true;

+		}

+	},

+  getTextDimensions: function(text, canvasStyle, HtmlStyle, className) {

+    if (!text) return {width:0, height:0};

+    

+    if (!this.options.HtmlText && this.textEnabled) {

+      var bounds = this.ctx.getTextBounds(text, canvasStyle);

+      return {

+        width: bounds.width+2, 

+        height: bounds.height+6

+      };

+    }

+    else {

+      var dummyDiv = this.el.insert('<div style="position:absolute;top:-10000px;'+HtmlStyle+'" class="'+className+' flotr-dummy-div">' + text + '</div>').select(".flotr-dummy-div")[0];

+      dim = dummyDiv.getDimensions();

+      dummyDiv.remove();

+      return dim;

+    }

+  },

+	loadDataGrid: function(){

+    if (this.seriesData) return this.seriesData;

+

+		var s = this.series;

+		var dg = [];

+

+    /* The data grid is a 2 dimensions array. There is a row for each X value.

+     * Each row contains the x value and the corresponding y value for each serie ('undefined' if there isn't one)

+    **/

+		for(i = 0; i < s.length; ++i){

+			s[i].data.each(function(v) {

+				var x = v[0],

+				    y = v[1];

+				if (r = dg.find(function(row) {return row[0] == x})) {

+					r[i+1] = y;

+				}

+				else {

+					var newRow = [];

+					newRow[0] = x;

+					newRow[i+1] = y

+					dg.push(newRow);

+				}

+			});

+		}

+		

+    // The data grid is sorted by x value

+		dg = dg.sortBy(function(v) {

+			return v[0];

+		});

+		return this.seriesData = dg;

+	},

+	

+	// @todo: make a tab manager (Flotr.Tabs)

+  showTab: function(tabName, onComplete){

+    var elementsClassNames = 'canvas, .flotr-labels, .flotr-legend, .flotr-legend-bg, .flotr-title, .flotr-subtitle';

+    switch(tabName) {

+      case 'graph':

+        this.datagrid.up().hide();

+        this.el.select(elementsClassNames).invoke('show');

+        this.tabs.data.removeClassName('selected');

+        this.tabs.graph.addClassName('selected');

+      break;

+      case 'data':

+        this.constructDataGrid();

+        this.datagrid.up().show();

+        this.el.select(elementsClassNames).invoke('hide');

+        this.tabs.data.addClassName('selected');

+        this.tabs.graph.removeClassName('selected');

+      break;

+    }

+  },

+  constructTabs: function(){

+    var tabsContainer = new Element('div', {className:'flotr-tabs-group', style:'position:absolute;left:0px;top:'+this.canvasHeight+'px;width:'+this.canvasWidth+'px;'});

+    this.el.insert({bottom: tabsContainer});

+    this.tabs = {

+    	graph: new Element('div', {className:'flotr-tab selected', style:'float:left;'}).update(this.options.spreadsheet.tabGraphLabel),

+    	data: new Element('div', {className:'flotr-tab', style:'float:left;'}).update(this.options.spreadsheet.tabDataLabel)

+    }

+    

+    tabsContainer.insert(this.tabs.graph).insert(this.tabs.data);

+    

+    this.el.setStyle({height: this.canvasHeight+this.tabs.data.getHeight()+2+'px'});

+

+    this.tabs.graph.observe('click', (function() {this.showTab('graph')}).bind(this));

+    this.tabs.data.observe('click', (function() {this.showTab('data')}).bind(this));

+  },

+  

+  // @todo: make a spreadsheet manager (Flotr.Spreadsheet)

+	constructDataGrid: function(){

+    // If the data grid has already been built, nothing to do here

+    if (this.datagrid) return this.datagrid;

+    

+		var i, j, 

+        s = this.series,

+        datagrid = this.loadDataGrid();

+

+		var t = this.datagrid = new Element('table', {className:'flotr-datagrid', style:'height:100px;'});

+		var colgroup = ['<colgroup><col />'];

+		

+		// First row : series' labels

+		var html = ['<tr class="first-row">'];

+		html.push('<th>&nbsp;</th>');

+		for (i = 0; i < s.length; ++i) {

+			html.push('<th scope="col">'+(s[i].label || String.fromCharCode(65+i))+'</th>');

+			colgroup.push('<col />');

+		}

+		html.push('</tr>');

+		

+		// Data rows

+		for (j = 0; j < datagrid.length; ++j) {

+			html.push('<tr>');

+			for (i = 0; i < s.length+1; ++i) {

+        var tag = 'td';

+        var content = (datagrid[j][i] != null ? Math.round(datagrid[j][i]*100000)/100000 : '');

+        

+        if (i == 0) {

+          tag = 'th';

+          var label;

+          if(this.options.xaxis.ticks) {

+            var tick = this.options.xaxis.ticks.find(function (x) { return x[0] == datagrid[j][i] });

+            if (tick) label = tick[1];

+          } 

+          else {

+            label = this.options.xaxis.tickFormatter(content);

+          }

+          

+          if (label) content = label;

+        }

+

+				html.push('<'+tag+(tag=='th'?' scope="row"':'')+'>'+content+'</'+tag+'>');

+			}

+			html.push('</tr>');

+		}

+		colgroup.push('</colgroup>');

+    t.update(colgroup.join('')+html.join(''));

+    

+    if (!Prototype.Browser.IE) {

+      t.select('td').each(function(td) {

+      	td.observe('mouseover', function(e){

+      		td = e.element();

+      		var siblings = td.previousSiblings();

+      		

+      		t.select('th[scope=col]')[siblings.length-1].addClassName('hover');

+      		t.select('colgroup col')[siblings.length].addClassName('hover');

+      	});

+      	

+      	td.observe('mouseout', function(){

+      		t.select('colgroup col.hover, th.hover').each(function(e){e.removeClassName('hover')});

+      	});

+      });

+    }

+    

+		var toolbar = new Element('div', {className: 'flotr-datagrid-toolbar'}).

+	    insert(new Element('button', {type:'button', className:'flotr-datagrid-toolbar-button'}).update(this.options.spreadsheet.toolbarDownload).observe('click', this.downloadCSV.bind(this))).

+	    insert(new Element('button', {type:'button', className:'flotr-datagrid-toolbar-button'}).update(this.options.spreadsheet.toolbarSelectAll).observe('click', this.selectAllData.bind(this)));

+		

+		var container = new Element('div', {className:'flotr-datagrid-container', style:'left:0px;top:0px;width:'+this.canvasWidth+'px;height:'+this.canvasHeight+'px;overflow:auto;'});

+		container.insert(toolbar);

+		t.wrap(container.hide());

+		

+		this.el.insert(container);

+    return t;

+  },

+  selectAllData: function(){

+    if (this.tabs) {

+      var selection, range, doc, win, node = this.constructDataGrid();

+  

+      this.showTab('data');

+      

+      // deferred to be able to select the table

+      (function () {

+        if ((doc = node.ownerDocument) && (win = doc.defaultView) && 

+          win.getSelection && doc.createRange && 

+          (selection = window.getSelection()) && 

+          selection.removeAllRanges) {

+           range = doc.createRange();

+           range.selectNode(node);

+           selection.removeAllRanges();

+           selection.addRange(range);

+        }

+        else if (document.body && document.body.createTextRange && 

+          (range = document.body.createTextRange())) {

+           range.moveToElementText(node);

+           range.select();

+        }

+      }).defer();

+      return true;

+    }

+    else return false;

+  },

+  downloadCSV: function(){

+    var i, csv = '"x"',

+        series = this.series,

+        dg = this.loadDataGrid();

+    

+    for (i = 0; i < series.length; ++i) {

+      csv += '%09"'+(series[i].label || String.fromCharCode(65+i))+'"'; // \t

+    }

+    csv += "%0D%0A"; // \r\n

+    

+    for (i = 0; i < dg.length; ++i) {

+      if (this.options.xaxis.ticks) {

+        var tick = this.options.xaxis.ticks.find(function (x) { return x[0] == dg[i][0] });

+        if (tick) dg[i][0] = tick[1];

+      } else {

+        dg[i][0] = this.options.xaxis.tickFormatter(dg[i][0]);

+      }

+      csv += dg[i].join('%09')+"%0D%0A"; // \t and \r\n

+    }

+    if (Prototype.Browser.IE) {

+      csv = csv.gsub('%09', '\t').gsub('%0A', '\n').gsub('%0D', '\r');

+      window.open().document.write(csv);

+    }

+    else {

+      window.open('data:text/csv,'+csv);

+    }

+  },

+	/**

+	 * Initializes event some handlers.

+	 */

+	initEvents: function () {

+  	//@todo: maybe stopObserving with only flotr functions

+  	this.overlay.stopObserving();

+  	this.overlay.observe('mousedown', this.mouseDownHandler.bind(this));

+		this.overlay.observe('mousemove', this.mouseMoveHandler.bind(this));

+		this.overlay.observe('click', this.clickHandler.bind(this));

+	},

+	/**

+	 * Function determines the min and max values for the xaxis and yaxis.

+	 */

+	findDataRanges: function(){

+		var s = this.series, 

+		    a = this.axes;

+		

+		a.x.datamin = 0;  a.x.datamax  = 0;

+		a.x2.datamin = 0; a.x2.datamax = 0;

+		a.y.datamin = 0;  a.y.datamax  = 0;

+		a.y2.datamin = 0; a.y2.datamax = 0;

+		

+		if(s.length > 0){

+			var i, j, h, x, y, data, xaxis, yaxis;

+		

+			// Get datamin, datamax start values 

+			for(i = 0; i < s.length; ++i) {

+				data = s[i].data, 

+				xaxis = s[i].xaxis, 

+				yaxis = s[i].yaxis;

+				

+				if (data.length > 0 && !s[i].hide) {

+					if (!xaxis.used) xaxis.datamin = xaxis.datamax = data[0][0];

+					if (!yaxis.used) yaxis.datamin = yaxis.datamax = data[0][1];

+					xaxis.used = true;

+					yaxis.used = true;

+

+					for(h = data.length - 1; h > -1; --h){

+  					x = data[h][0];

+  			         if(x < xaxis.datamin) xaxis.datamin = x;

+   					else if(x > xaxis.datamax) xaxis.datamax = x;

+  			    

+  					for(j = 1; j < data[h].length; j++){

+  						y = data[h][j];

+  				         if(y < yaxis.datamin) yaxis.datamin = y;

+  	  				else if(y > yaxis.datamax) yaxis.datamax = y;

+  					}

+					}

+				}

+				if (this.options.radarChartMode) {

+					xaxis.datamin = yaxis.datamin = - yaxis.datamax;

+					xaxis.datamax = yaxis.datamax;

+					if (!this.options.radarChartSides) this.options.radarChartSides = data.length;

+				}

+			}

+		}

+		

+		this.findXAxesValues();

+		

+		this.calculateRange(a.x);

+		this.extendXRangeIfNeededByBar(a.x);

+		

+		if (a.x2.used) {

+			this.calculateRange(a.x2);

+		  this.extendXRangeIfNeededByBar(a.x2);

+		}

+		

+		this.calculateRange(a.y);

+		this.extendYRangeIfNeededByBar(a.y);

+		

+		if (a.y2.used) {

+  		this.calculateRange(a.y2);

+  		this.extendYRangeIfNeededByBar(a.y2);

+		}

+	},

+	/**

+	 * Calculates the range of an axis to apply autoscaling.

+	 */

+	calculateRange: function(axis){

+		var o = axis.options,

+		  min = o.min != null ? o.min : axis.datamin,

+			max = o.max != null ? o.max : axis.datamax,

+			margin;

+

+		if(max - min == 0.0){

+			var widen = (max == 0.0) ? 1.0 : 0.01;

+			min -= widen;

+			max += widen;

+		}

+		axis.tickSize = Flotr.getTickSize(o.noTicks, ((this.options.radarChartMode) ? 0 : min), max, o.tickDecimals);

+

+		// Autoscaling.

+		if(o.min == null){

+			// Add a margin.

+			margin = o.autoscaleMargin;

+			if(margin != 0){

+				min -= axis.tickSize * margin;

+				

+				// Make sure we don't go below zero if all values are positive.

+				if(min < 0 && axis.datamin >= 0) min = 0;

+				min = axis.tickSize * Math.floor(min / axis.tickSize);

+			}

+		}

+		if(o.max == null){

+			margin = o.autoscaleMargin;

+			if(margin != 0){

+				max += axis.tickSize * margin;

+				if(max > 0 && axis.datamax <= 0) max = 0;				

+				max = axis.tickSize * Math.ceil(max / axis.tickSize);

+			}

+		}

+		axis.min = min;

+		axis.max = max;

+	},

+	/**

+	 * Bar series autoscaling in x direction.

+	 */

+	extendXRangeIfNeededByBar: function(axis){

+		if(axis.options.max == null){

+			var newmax = axis.max,

+			    i, s, b, c,

+			    stackedSums = [], 

+			    lastSerie = null;

+

+			for(i = 0; i < this.series.length; ++i){

+				s = this.series[i];

+				b = s.bars;

+				c = s.candles;

+				if(s.axis == axis && (b.show || c.show)) {

+					if (!b.horizontal && (b.barWidth + axis.datamax > newmax) || (c.candleWidth + axis.datamax > newmax)){

+						newmax = axis.max + s.bars.barWidth;

+					}

+					if(b.stacked && b.horizontal){

+						for (j = 0; j < s.data.length; j++) {

+							if (s.bars.show && s.bars.stacked) {

+								var x = s.data[j][0];

+								stackedSums[x] = (stackedSums[x] || 0) + s.data[j][1];

+								lastSerie = s;

+							}

+						}

+				    

+						for (j = 0; j < stackedSums.length; j++) {

+				    	newmax = Math.max(stackedSums[j], newmax);

+						}

+					}

+				}

+			}

+			axis.lastSerie = lastSerie;

+			axis.max = newmax;

+		}

+	},

+	/**

+	 * Bar series autoscaling in y direction.

+	 */

+	extendYRangeIfNeededByBar: function(axis){

+		if(axis.options.max == null){

+			var newmax = axis.max,

+				  i, s, b, c,

+				  stackedSums = [],

+				  lastSerie = null;

+									

+			for(i = 0; i < this.series.length; ++i){

+				s = this.series[i];

+				b = s.bars;

+				c = s.candles;

+				if (s.yaxis == axis && b.show && !s.hide) {

+					if (b.horizontal && (b.barWidth + axis.datamax > newmax) || (c.candleWidth + axis.datamax > newmax)){

+						newmax = axis.max + b.barWidth;

+					}

+					if(b.stacked && !b.horizontal){

+						for (j = 0; j < s.data.length; j++) {

+							if (s.bars.show && s.bars.stacked) {

+								var x = s.data[j][0];

+								stackedSums[x] = (stackedSums[x] || 0) + s.data[j][1];

+								lastSerie = s;

+							}

+						}

+						

+						for (j = 0; j < stackedSums.length; j++) {

+							newmax = Math.max(stackedSums[j], newmax);

+						}

+					}

+				}

+			}

+			axis.lastSerie = lastSerie;

+			axis.max = newmax;

+		}

+	},

+	/** 

+	 * Find every values of the x axes

+	 */

+	findXAxesValues: function(){

+		for(i = this.series.length-1; i > -1 ; --i){

+			s = this.series[i];

+			s.xaxis.values = s.xaxis.values || [];

+			for (j = s.data.length-1; j > -1 ; --j){

+				s.xaxis.values[s.data[j][0]] = {};

+			}

+		}

+	},

+	/**

+	 * Calculate axis ticks.

+	 * @param {Object} axis - axis object

+	 * @param {Object} o - axis options

+	 */

+	calculateTicks: function(axis){

+		var o = axis.options, i, v;

+		

+		axis.ticks = [];	

+		if(o.ticks){

+			var ticks = o.ticks, t, label;

+

+			if(Object.isFunction(ticks)){

+				ticks = ticks({min: axis.min, max: axis.max});

+			}

+			

+			// Clean up the user-supplied ticks, copy them over.

+			for(i = 0; i < ticks.length; ++i){

+				t = ticks[i];

+				if(typeof(t) == 'object'){

+					v = t[0];

+					label = (t.length > 1) ? t[1] : o.tickFormatter(v);

+				}else{

+					v = t;

+					label = o.tickFormatter(v);

+				}

+				axis.ticks[i] = { v: v, label: label };

+			}

+		}

+    else {

+			// Round to nearest multiple of tick size.

+			var start = axis.tickSize * Math.ceil(axis.min / axis.tickSize),

+				  decimals;

+			

+			// Then store all possible ticks.

+			for(i = 0; start + i * axis.tickSize <= axis.max; ++i){

+				v = start + i * axis.tickSize;

+				

+				// Round (this is always needed to fix numerical instability).

+				decimals = o.tickDecimals;

+				if(decimals == null) decimals = 1 - Math.floor(Math.log(axis.tickSize) / Math.LN10);

+				if(decimals < 0) decimals = 0;

+				

+				v = v.toFixed(decimals);

+				axis.ticks.push({ v: v, label: o.tickFormatter(v) });

+			}

+		}

+	},

+	/**

+	 * Calculates axis label sizes.

+	 */

+	calculateSpacing: function(){

+		var a = this.axes,

+  			options = this.options,

+  			series = this.series,

+  			margin = options.grid.labelMargin,

+  			x = a.x,

+  			x2 = a.x2,

+  			y = a.y,

+  			y2 = a.y2,

+  			maxOutset = 2,

+  			i, j, l, dim;

+		

+		// Labels width and height

+		[x, x2, y, y2].each(function(axis) {

+			var maxLabel = '';

+			

+		  if (axis.options.showLabels) {

+				for(i = 0; i < axis.ticks.length; ++i){

+					l = axis.ticks[i].label.length;

+					if(l > maxLabel.length){

+						maxLabel = axis.ticks[i].label;

+					}

+				}

+	    }

+		  axis.maxLabel  = this.getTextDimensions(maxLabel, {size:options.fontSize, angle: Flotr.toRad(axis.options.labelsAngle)}, 'font-size:smaller;', 'flotr-grid-label');

+		  axis.titleSize = this.getTextDimensions(axis.options.title, {size: options.fontSize*1.2, angle: Flotr.toRad(axis.options.titleAngle)}, 'font-weight:bold;', 'flotr-axis-title');

+		}, this);

+

+    // Title height

+    dim = this.getTextDimensions(options.title, {size: options.fontSize*1.5}, 'font-size:1em;font-weight:bold;', 'flotr-title');

+    this.titleHeight = dim.height;

+    

+    // Subtitle height

+    dim = this.getTextDimensions(options.subtitle, {size: options.fontSize}, 'font-size:smaller;', 'flotr-subtitle');

+    this.subtitleHeight = dim.height;

+

+		// Grid outline line width.

+		if(options.show){

+			maxOutset = Math.max(maxOutset, options.points.radius + options.points.lineWidth/2);

+		}

+		for(j = 0; j < options.length; ++j){

+			if (series[j].points.show){

+				maxOutset = Math.max(maxOutset, series[j].points.radius + series[j].points.lineWidth/2);

+			}

+		}

+		

+		var p = this.plotOffset = {left: 0, right: 0, top: 0, bottom: 0};

+		p.left = p.right = p.top = p.bottom = maxOutset;

+		

+		p.bottom += (x.options.showLabels ?  (x.maxLabel.height  + margin) : 0) + 

+		            (x.options.title ?       (x.titleSize.height + margin) : 0);

+		

+    p.top    += (x2.options.showLabels ? (x2.maxLabel.height  + margin) : 0) + 

+                (x2.options.title ?      (x2.titleSize.height + margin) : 0) + this.subtitleHeight + this.titleHeight + 

+		this.options.radarChartMode ? (y.options.showLabels ?  (y.maxLabel.height  + margin) : 0) : 0;

+    

+		p.left   += (y.options.showLabels ?  (y.maxLabel.width  + margin) : 0) + 

+                (y.options.title ?       (y.titleSize.width + margin) : 0);

+		

+		p.right  += (y2.options.showLabels ? (y2.maxLabel.width  + margin) : 0) + 

+                (y2.options.title ?      (y2.titleSize.width + margin) : 0) + 

+		this.options.radarChartMode ? (x.options.showLabels ?  (x.maxLabel.width  + margin) : 0) : 0;

+    

+    p.top = Math.floor(p.top); // In order the outline not to be blured

+    

+		this.plotWidth  = this.canvasWidth - p.left - p.right;

+		this.plotHeight = this.canvasHeight - p.bottom - p.top;

+		

+		x.scale  = this.plotWidth / (x.max - x.min);

+		x2.scale = this.plotWidth / (x2.max - x2.min);

+		y.scale  = this.plotHeight / (y.max - y.min);

+		y2.scale = this.plotHeight / (y2.max - y2.min);

+	},

+	/**

+	 * Draws grid, labels and series.

+	 */

+	draw: function() {

+		this.drawGrid();

+		this.drawLabels();

+    this.drawTitles();

+    

+		if(this.series.length){

+			this.el.fire('flotr:beforedraw', [this.series, this]);

+			for(var i = 0; i < this.series.length; i++){

+				if (!this.series[i].hide)

+					this.drawSeries(this.series[i]);

+			}

+		}

+		this.el.fire('flotr:afterdraw', [this.series, this]);

+	},

+	/**

+	 * Translates absolute horizontal x coordinates to relative coordinates.

+	 * @param {Integer} x - absolute integer x coordinate

+	 * @return {Integer} translated relative x coordinate

+	 */

+	tHoz: function(x, axis){

+		axis = axis || this.axes.x;

+		return (x - axis.min) * axis.scale;

+	},

+	/**

+	 * Translates absolute vertical x coordinates to relative coordinates.

+	 * @param {Integer} y - absolute integer y coordinate

+	 * @return {Integer} translated relative y coordinate

+	 */

+	tVert: function(y, axis){

+		axis = axis || this.axes.y;

+		return this.plotHeight - (y - axis.min) * axis.scale;

+	},

+	/**

+	 * Draws a grid for the graph.

+	 */

+	drawGrid: function(){

+		if (this.options.radarChartMode) { // If we are in radar chart mode call drawRadarGrid instead and exit

+			this.drawRadarGrid();

+			return;

+		}

+		var v, o = this.options,

+		    ctx = this.ctx;

+		if(o.grid.verticalLines || o.grid.horizontalLines){

+			this.el.fire('flotr:beforegrid', [this.axes.x, this.axes.y, o, this]);

+		}

+		ctx.save();

+		ctx.translate(this.plotOffset.left, this.plotOffset.top);

+

+		// Draw grid background, if present in options.

+		if(o.grid.backgroundColor != null){

+			ctx.fillStyle = o.grid.backgroundColor;

+			ctx.fillRect(0, 0, this.plotWidth, this.plotHeight);

+		}

+		

+		// Draw grid lines in vertical direction.

+		ctx.lineWidth = 1;

+		ctx.strokeStyle = o.grid.tickColor;

+		ctx.beginPath();

+		if(o.grid.verticalLines){

+			for(var i = 0; i < this.axes.x.ticks.length; ++i){

+				v = this.axes.x.ticks[i].v;

+				// Don't show lines on upper and lower bounds.

+				if ((v == this.axes.x.min || v == this.axes.x.max) && o.grid.outlineWidth != 0)

+					continue;

+	

+				ctx.moveTo(Math.floor(this.tHoz(v)) + ctx.lineWidth/2, 0);

+				ctx.lineTo(Math.floor(this.tHoz(v)) + ctx.lineWidth/2, this.plotHeight);

+			}

+		}

+		

+		// Draw grid lines in horizontal direction.

+		if(o.grid.horizontalLines){

+			for(var j = 0; j < this.axes.y.ticks.length; ++j){

+				v = this.axes.y.ticks[j].v;

+				// Don't show lines on upper and lower bounds.

+				if ((v == this.axes.y.min || v == this.axes.y.max) && o.grid.outlineWidth != 0)

+					continue;

+	

+				ctx.moveTo(0, Math.floor(this.tVert(v)) + ctx.lineWidth/2);

+				ctx.lineTo(this.plotWidth, Math.floor(this.tVert(v)) + ctx.lineWidth/2);

+			}

+		}

+		ctx.stroke();

+		

+		// Draw axis/grid border.

+		if(o.grid.outlineWidth != 0) {

+			ctx.lineWidth = o.grid.outlineWidth;

+			ctx.strokeStyle = o.grid.color;

+			ctx.lineJoin = 'round';

+			ctx.strokeRect(0, 0, this.plotWidth, this.plotHeight);

+		}

+		ctx.restore();

+		if(o.grid.verticalLines || o.grid.horizontalLines){

+			this.el.fire('flotr:aftergrid', [this.axes.x, this.axes.y, o, this]);

+		}

+	},

+	/**

+	 * Draws a grid for the graph.

+	 */

+	drawRadarGrid: function(){

+		

+		var v, o = this.options,

+		    ctx = this.ctx;

+		    

+		var sides = this.options.radarChartSides,

+		    degreesInRadiansForAngle = Math.PI * 2 / sides,

+		    nintyDegrees = Math.PI / 2;

+		

+		if(o.grid.verticalLines || o.grid.horizontalLines){

+			this.el.fire('flotr:beforegrid', [this.axes.x, this.axes.y, o, this]);

+		}

+		ctx.save();

+		ctx.translate(this.plotOffset.left, this.plotOffset.top);

+		ctx.lineJoin = 'round';

+

+		// Draw grid background, if present in options.

+		if(o.grid.backgroundColor != null){

+			ctx.fillStyle = o.grid.backgroundColor;

+			ctx.fillRect(0, 0, this.plotWidth, this.plotHeight);

+		}

+		

+		// Draw grid lines

+		var regPoly = {};

+		regPoly.xaxis = {};

+		regPoly.yaxis = {};

+		regPoly.xaxis.min = regPoly.yaxis.min = this.axes.x.min;

+		regPoly.xaxis.max = regPoly.yaxis.max = this.axes.x.max;

+		regPoly.xaxis.scale = this.plotWidth / (this.axes.x.max - this.axes.x.min);

+		regPoly.yaxis.scale = this.plotHeight / (this.axes.x.max - this.axes.x.min);

+		

+		ctx.lineWidth = 1;

+		ctx.strokeStyle = o.grid.tickColor;

+		

+		if(o.grid.horizontalLines){

+			for(var j = 0; j < this.axes.y.ticks.length; ++j){

+				v = this.axes.y.ticks[j].v;

+				if (v < 0) continue;

+				// Don't show lines on upper and lower bounds.

+				if ((v == this.axes.y.min || v == this.axes.y.max) && o.grid.outlineWidth != 0)

+					continue;

+				regPoly.data = new Array();

+				for (i = 0; i < sides; i++) {

+					angle = nintyDegrees + (degreesInRadiansForAngle * i);

+					regPoly.data[i] = [v * Math.cos(angle), v * Math.sin(angle)]

+				}

+				regPoly.data[sides] = regPoly.data[0];

+				this.plotLine(regPoly,0);

+			}

+		}

+		

+		// Draw axis/grid border.

+		if(o.grid.outlineWidth != 0) {

+			ctx.lineWidth = o.grid.outlineWidth;

+			ctx.strokeStyle = o.grid.color;

+			regPoly.data = new Array();

+			var radius = this.axes.x.max;

+			for (i = 0; i < sides; i++) {

+				angle = nintyDegrees + (degreesInRadiansForAngle * i);

+				regPoly.data[i] = [radius * Math.cos(angle), radius * Math.sin(angle)]

+				}

+				regPoly.data[sides] = regPoly.data[0];

+				this.plotLine(regPoly,0);

+		}

+		

+		ctx.lineWidth = 1;

+		ctx.strokeStyle = o.grid.tickColor;

+		ctx.beginPath();

+		

+		if(o.grid.verticalLines){

+			for(var i = 0; i < sides; ++i){

+				ctx.moveTo(Math.floor(this.tHoz(0)) + ctx.lineWidth/2, 

+						Math.floor(this.tVert(0)) + ctx.lineWidth/2);

+				ctx.lineTo(Math.floor(this.tHoz(regPoly.data[i][0])) + ctx.lineWidth/2, 

+						Math.floor(this.tVert(regPoly.data[i][1])) + ctx.lineWidth/2);

+			}

+		}

+		

+		ctx.stroke();

+		

+		ctx.restore();

+		if(o.grid.verticalLines || o.grid.horizontalLines){

+			this.el.fire('flotr:aftergrid', [this.axes.x, this.axes.y, o, this]);

+		}

+	},

+	/**

+	* Draws labels aroung radar chart

+	*/

+	drawRadarLabels:function(){

+		var ctx = this.ctx,

+			options = this.options,

+			axis = this.axes.x,

+			tick, minY = 0, maxY = 0,

+			xOffset, yOffset;

+		var style = {

+		    size: options.fontSize,

+		    adjustAlign: true

+		  };

+		style.color = axis.options.color || options.grid.color;

+		style.angle = Flotr.toRad(axis.options.labelsAngle);

+		var radius = axis.max * 1,

+		      closeTo = axis.max * 0.1,

+		      sides = this.options.radarChartSides,

+		      degreesInRadiansForAngle = Math.PI * 2 / sides,

+		      nintyDegrees = Math.PI / 2,

+		      posdata = new Array();

+		for (i = 0; i < sides; i++) {

+				angle = nintyDegrees + (degreesInRadiansForAngle * i);

+				posdata[i] = [radius * Math.cos(angle), radius * Math.sin(angle)];

+				if (minY > posdata[i][1]) minY = posdata[i][1];

+				if (maxY < posdata[i][1]) maxY = posdata[i][1];

+				}

+		for (i = 0; i < sides; i++) {

+				tick = axis.ticks[i];

+				if(!tick.label || tick.label.length == 0) continue;

+				yOffset = 0;

+				if (posdata[i][0] > 0) {

+					style.halign = 'l';

+					xOffset = options.grid.labelMargin;

+				} else {

+					style.halign = 'r';

+					xOffset = - options.grid.labelMargin;

+				}

+				style.valign = 'm';

+				

+				if ((posdata[i][1] + closeTo) >= minY && (posdata[i][1] - closeTo) <= minY) {

+					style.valign = 't' ; 

+					style.halign = 'c';

+					yOffset = options.grid.labelMargin; 

+				};

+				if (posdata[i][1] == maxY) {

+					style.valign = 'b' ; 

+					style.halign = 'c';

+					yOffset = - options.grid.labelMargin; 

+				}

+				ctx.drawText(

+					tick.label,

+					this.plotOffset.left + this.tHoz(posdata[i][0]) + xOffset, 

+					this.plotOffset.top + this.tVert(posdata[i][1]) + yOffset,

+					style

+				);

+				}

+		

+	},

+	/**

+	 * Draws labels for x and y axis.

+	 */   

+	drawLabels: function(){		

+		// Construct fixed width label boxes, which can be styled easily. 

+		var noLabels = 0, axis,

+			xBoxWidth, i, html, tick,

+			options = this.options,

+      ctx = this.ctx,

+      a = this.axes;

+		

+		for(i = 0; i < a.x.ticks.length; ++i){

+			if (a.x.ticks[i].label) {

+				++noLabels;

+			}

+		}

+		xBoxWidth = this.plotWidth / noLabels;

+    

+		if (!options.HtmlText && this.textEnabled) {

+		  var style = {

+		    size: options.fontSize,

+        adjustAlign: true

+		  };

+

+		  // Add x labels.

+		  if (options.radarChartMode) {

+			this.drawRadarLabels();} else {

+		  axis = a.x;

+		  style.color = axis.options.color || options.grid.color;

+		  for(i = 0; i < axis.ticks.length && axis.options.showLabels && axis.used; ++i){

+		    tick = axis.ticks[i];

+		    if(!tick.label || tick.label.length == 0) continue;

+        

+        style.angle = Flotr.toRad(axis.options.labelsAngle);

+        style.halign = 'c';

+        style.valign = 't';

+        

+		    ctx.drawText(

+		      tick.label,

+		      this.plotOffset.left + this.tHoz(tick.v, axis), 

+		      this.plotOffset.top + this.plotHeight + options.grid.labelMargin,

+		      style

+		    );

+		  }}

+		  

+		  // Add x2 labels.

+		  axis = a.x2;

+		  style.color = axis.options.color || options.grid.color;

+		  for(i = 0; i < axis.ticks.length && axis.options.showLabels && axis.used; ++i){

+		    tick = axis.ticks[i];

+		    if(!tick.label || tick.label.length == 0) continue;

+        

+        style.angle = Flotr.toRad(axis.options.labelsAngle);

+        style.halign = 'c';

+        style.valign = 'b';

+        

+		    ctx.drawText(

+		      tick.label,

+		      this.plotOffset.left + this.tHoz(tick.v, axis), 

+		      this.plotOffset.top + options.grid.labelMargin,

+		      style

+		    );

+		  }

+		  

+		  // Add y labels.

+		  axis = a.y;

+		  style.color = axis.options.color || options.grid.color;

+		  for(i = 0; i < axis.ticks.length && axis.options.showLabels && axis.used; ++i){

+		    tick = axis.ticks[i];

+		    if (!tick.label || tick.label.length == 0 || (tick.v < 0 && this.options.radarChartMode)) continue;

+        

+        style.angle = Flotr.toRad(axis.options.labelsAngle);

+        style.halign = 'r';

+        style.valign = 'm';

+        

+		    ctx.drawText(

+		      tick.label,

+		      this.plotOffset.left + (this.options.radarChartMode ? this.tHoz(0) : 0) - options.grid.labelMargin, 

+		      this.plotOffset.top + this.tVert(tick.v, axis),

+		      style

+		    );

+		  }

+		  

+		  // Add y2 labels.

+		  axis = a.y2;

+		  style.color = axis.options.color || options.grid.color;

+		  for(i = 0; i < axis.ticks.length && axis.options.showLabels && axis.used; ++i){

+		    tick = axis.ticks[i];

+		    if (!tick.label || tick.label.length == 0) continue;

+        

+        style.angle = Flotr.toRad(axis.options.labelsAngle);

+        style.halign = 'l';

+        style.valign = 'm';

+        

+		    ctx.drawText(

+		      tick.label,

+		      this.plotOffset.left + this.plotWidth + options.grid.labelMargin, 

+		      this.plotOffset.top + this.tVert(tick.v, axis),

+		      style

+		    );

+		    

+				ctx.save();

+				ctx.strokeStyle = style.color;

+				ctx.beginPath();

+				ctx.moveTo(this.plotOffset.left + this.plotWidth - 8, this.plotOffset.top + this.tVert(tick.v, axis));

+				ctx.lineTo(this.plotOffset.left + this.plotWidth,     this.plotOffset.top + this.tVert(tick.v, axis));

+				ctx.stroke();

+				ctx.restore();

+		  }

+		} 

+		else if (a.x.options.showLabels || 

+				     a.x2.options.showLabels || 

+				     a.y.options.showLabels || 

+				     a.y2.options.showLabels) {

+			html = ['<div style="font-size:smaller;color:' + options.grid.color + ';" class="flotr-labels">'];

+			

+			// Add x labels.

+			axis = a.x;

+			if (axis.options.showLabels){

+				for(i = 0; i < axis.ticks.length; ++i){

+					tick = axis.ticks[i];

+					if(!tick.label || tick.label.length == 0) continue;

+					html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.plotHeight + options.grid.labelMargin) + 'px;left:' + (this.plotOffset.left + this.tHoz(tick.v, axis) - xBoxWidth/2) + 'px;width:' + xBoxWidth + 'px;text-align:center;'+(axis.options.color?('color:'+axis.options.color+';'):'')+'" class="flotr-grid-label">' + tick.label + '</div>');

+				}

+			}

+			

+			// Add x2 labels.

+			axis = a.x2;

+			if (axis.options.showLabels && axis.used){

+				for(i = 0; i < axis.ticks.length; ++i){

+					tick = axis.ticks[i];

+					if(!tick.label || tick.label.length == 0) continue;

+					html.push('<div style="position:absolute;top:' + (this.plotOffset.top - options.grid.labelMargin - axis.maxLabel.height) + 'px;left:' + (this.plotOffset.left + this.tHoz(tick.v, axis) - xBoxWidth/2) + 'px;width:' + xBoxWidth + 'px;text-align:center;'+(axis.options.color?('color:'+axis.options.color+';'):'')+'" class="flotr-grid-label">' + tick.label + '</div>');

+				}

+			}

+			

+			// Add y labels.

+			axis = a.y;

+			if (axis.options.showLabels){

+				for(i = 0; i < axis.ticks.length; ++i){

+					tick = axis.ticks[i];

+					if (!tick.label || tick.label.length == 0) continue;

+					html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.tVert(tick.v, axis) - axis.maxLabel.height/2) + 'px;left:0;width:' + (this.plotOffset.left - options.grid.labelMargin) + 'px;text-align:right;'+(axis.options.color?('color:'+axis.options.color+';'):'')+'" class="flotr-grid-label">' + tick.label + '</div>');

+				}

+			}

+			

+			// Add y2 labels.

+			axis = a.y2;

+			if (axis.options.showLabels && axis.used){

+				ctx.save();

+				ctx.strokeStyle = axis.options.color || options.grid.color;

+				ctx.beginPath();

+				

+				for(i = 0; i < axis.ticks.length; ++i){

+					tick = axis.ticks[i];

+					if (!tick.label || tick.label.length == 0) continue;

+					html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.tVert(tick.v, axis) - axis.maxLabel.height/2) + 'px;right:0;width:' + (this.plotOffset.right - options.grid.labelMargin) + 'px;text-align:left;'+(axis.options.color?('color:'+axis.options.color+';'):'')+'" class="flotr-grid-label">' + tick.label + '</div>');

+

+					ctx.moveTo(this.plotOffset.left + this.plotWidth - 8, this.plotOffset.top + this.tVert(tick.v, axis));

+					ctx.lineTo(this.plotOffset.left + this.plotWidth,     this.plotOffset.top + this.tVert(tick.v, axis));

+				}

+				ctx.stroke();

+				ctx.restore();

+			}

+			

+			html.push('</div>');

+			this.el.insert(html.join(''));

+		}

+	},

+  /**

+   * Draws the title and the subtitle

+   */   

+  drawTitles: function(){

+    var html,

+        options = this.options,

+        margin = options.grid.labelMargin,

+        ctx = this.ctx,

+        a = this.axes;

+      

+    if (!options.HtmlText && this.textEnabled) {

+      var style = {

+        size: options.fontSize,

+        color: options.grid.color,

+        halign: 'c'

+      };

+

+      // Add subtitle

+      if (options.subtitle){

+        ctx.drawText(

+          options.subtitle,

+          this.plotOffset.left + this.plotWidth/2, 

+          this.titleHeight + this.subtitleHeight - 2,

+          style

+        );

+      }

+      

+			style.weight = 1.5;

+      style.size *= 1.5;

+      

+      // Add title

+      if (options.title){

+        ctx.drawText(

+          options.title,

+          this.plotOffset.left + this.plotWidth/2, 

+          this.titleHeight - 2,

+          style

+        );

+      }

+      

+      style.weight = 1.8;

+      style.size *= 0.8;

+      style.adjustAlign = true;

+      

+			// Add x axis title

+			if (a.x.options.title && a.x.used){

+				style.halign = 'c';

+				style.valign = 't';

+				style.angle = Flotr.toRad(a.x.options.titleAngle);

+        ctx.drawText(

+          a.x.options.title,

+          this.plotOffset.left + this.plotWidth/2, 

+          this.plotOffset.top + a.x.maxLabel.height + this.plotHeight + 2 * margin,

+          style

+        );

+      }

+			

+			// Add x2 axis title

+			if (a.x2.options.title && a.x2.used){

+				style.halign = 'c';

+				style.valign = 'b';

+				style.angle = Flotr.toRad(a.x2.options.titleAngle);

+        ctx.drawText(

+          a.x2.options.title,

+          this.plotOffset.left + this.plotWidth/2, 

+          this.plotOffset.top - a.x2.maxLabel.height - 2 * margin,

+          style

+        );

+      }

+			

+			// Add y axis title

+			if (a.y.options.title && a.y.used){

+				style.halign = 'r';

+				style.valign = 'm';

+				style.angle = Flotr.toRad(a.y.options.titleAngle);

+        ctx.drawText(

+          a.y.options.title,

+          this.plotOffset.left - a.y.maxLabel.width - 2 * margin, 

+          this.plotOffset.top + this.plotHeight / 2,

+          style

+        );

+      }

+			

+			// Add y2 axis title

+			if (a.y2.options.title && a.y2.used){

+				style.halign = 'l';

+				style.valign = 'm';

+				style.angle = Flotr.toRad(a.y2.options.titleAngle);

+        ctx.drawText(

+          a.y2.options.title,

+          this.plotOffset.left + this.plotWidth + a.y2.maxLabel.width + 2 * margin, 

+          this.plotOffset.top + this.plotHeight / 2,

+          style

+        );

+      }

+    } 

+    else {

+      html = ['<div style="color:'+options.grid.color+';" class="flotr-titles">'];

+      

+      // Add title

+      if (options.title){

+        html.push('<div style="position:absolute;top:0;left:'+this.plotOffset.left+'px;font-size:1em;font-weight:bold;text-align:center;width:'+this.plotWidth+'px;" class="flotr-title">'+options.title+'</div>');

+      }

+      

+      // Add subtitle

+      if (options.subtitle){

+        html.push('<div style="position:absolute;top:'+this.titleHeight+'px;left:'+this.plotOffset.left+'px;font-size:smaller;text-align:center;width:'+this.plotWidth+'px;" class="flotr-subtitle">'+options.subtitle+'</div>');

+      }

+      html.push('</div>');

+      

+      

+      html.push('<div class="flotr-axis-title" style="font-weight:bold;">');

+			// Add x axis title

+			if (a.x.options.title && a.x.used){

+				html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.plotHeight + options.grid.labelMargin + a.x.titleSize.height) + 'px;left:' + this.plotOffset.left + 'px;width:' + this.plotWidth + 'px;text-align:center;" class="flotr-axis-title">' + a.x.options.title + '</div>');

+			}

+			

+			// Add x2 axis title

+			if (a.x2.options.title && a.x2.used){

+				html.push('<div style="position:absolute;top:0;left:' + this.plotOffset.left + 'px;width:' + this.plotWidth + 'px;text-align:center;" class="flotr-axis-title">' + a.x2.options.title + '</div>');

+			}

+			

+			// Add y axis title

+			if (a.y.options.title && a.y.used){

+				html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.plotHeight/2 - a.y.titleSize.height/2) + 'px;left:0;text-align:right;" class="flotr-axis-title">' + a.y.options.title + '</div>');

+			}

+			

+			// Add y2 axis title

+			if (a.y2.options.title && a.y2.used){

+				html.push('<div style="position:absolute;top:' + (this.plotOffset.top + this.plotHeight/2 - a.y.titleSize.height/2) + 'px;right:0;text-align:right;" class="flotr-axis-title">' + a.y2.options.title + '</div>');

+			}

+			html.push('</div>');

+      

+      this.el.insert(html.join(''));

+    }

+  },

+	/**

+	 * Actually draws the graph.

+	 * @param {Object} series - series to draw

+	 */

+	drawSeries: function(series){

+		series = series || this.series;

+		

+		var drawn = false;

+		for(var type in Flotr._registeredTypes){

+			if(series[type] && series[type].show){

+				this[Flotr._registeredTypes[type]](series);

+				drawn = true;

+			}

+		}

+		

+		if(!drawn){

+			this[Flotr._registeredTypes[this.options.defaultType]](series);

+		}

+	},

+	

+	plotLine: function(series, offset){

+		var ctx = this.ctx,

+		    xa = series.xaxis,

+		    ya = series.yaxis,

+  			tHoz = this.tHoz.bind(this),

+  			tVert = this.tVert.bind(this),

+  			data = series.data;

+			

+		if(data.length < 2) return;

+

+		var prevx = tHoz(data[0][0], xa),

+		    prevy = tVert(data[0][1], ya) + offset;

+		ctx.beginPath();

+		ctx.moveTo(prevx, prevy);

+		for(var i = 0; i < data.length - 1; ++i){

+			var x1 = data[i][0],   y1 = data[i][1],

+			    x2 = data[i+1][0], y2 = data[i+1][1];

+

+      // To allow empty values

+      if (y1 === null || y2 === null) continue;

+      

+			/**

+			 * Clip with ymin.

+			 */

+			if(y1 <= y2 && y1 < ya.min){

+				/**

+				 * Line segment is outside the drawing area.

+				 */

+				if(y2 < ya.min) continue;

+				

+				/**

+				 * Compute new intersection point.

+				 */

+				x1 = (ya.min - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y1 = ya.min;

+			}else if(y2 <= y1 && y2 < ya.min){

+				if(y1 < ya.min) continue;

+				x2 = (ya.min - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y2 = ya.min;

+			}

+

+			/**

+			 * Clip with ymax.

+			 */ 

+			if(y1 >= y2 && y1 > ya.max) {

+				if(y2 > ya.max) continue;

+				x1 = (ya.max - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y1 = ya.max;

+			}

+			else if(y2 >= y1 && y2 > ya.max){

+				if(y1 > ya.max) continue;

+				x2 = (ya.max - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y2 = ya.max;

+			}

+

+			/**

+			 * Clip with xmin.

+			 */

+			if(x1 <= x2 && x1 < xa.min){

+				if(x2 < xa.min) continue;

+				y1 = (xa.min - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x1 = xa.min;

+			}else if(x2 <= x1 && x2 < xa.min){

+				if(x1 < xa.min) continue;

+				y2 = (xa.min - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x2 = xa.min;

+			}

+

+			/**

+			 * Clip with xmax.

+			 */

+			if(x1 >= x2 && x1 > xa.max){

+				if (x2 > xa.max) continue;

+				y1 = (xa.max - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x1 = xa.max;

+			}else if(x2 >= x1 && x2 > xa.max){

+				if(x1 > xa.max) continue;

+				y2 = (xa.max - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x2 = xa.max;

+			}

+

+			if(prevx != tHoz(x1, xa) || prevy != tVert(y1, ya) + offset)

+				ctx.moveTo(tHoz(x1, xa), tVert(y1, ya) + offset);

+			

+			prevx = tHoz(x2, xa);

+			prevy = tVert(y2, ya) + offset;

+			ctx.lineTo(prevx, prevy);

+		}

+		ctx.stroke();

+	},

+	/**

+	 * Function used to fill

+	 * @param {Object} data

+	 */

+	plotLineArea: function(series, offset){

+		var data = series.data;

+		if(data.length < 2) return;

+

+		var top, lastX = 0,

+			ctx = this.ctx,

+	    xa = series.xaxis,

+	    ya = series.yaxis,

+			tHoz = this.tHoz.bind(this),

+			tVert = this.tVert.bind(this),

+			bottom = Math.min(Math.max(0, ya.min), ya.max),

+			first = true;

+		

+		ctx.beginPath();

+		for(var i = 0; i < data.length - 1; ++i){

+			

+			var x1 = data[i][0], y1 = data[i][1],

+			    x2 = data[i+1][0], y2 = data[i+1][1];

+			

+			if(x1 <= x2 && x1 < xa.min){

+				if(x2 < xa.min) continue;

+				y1 = (xa.min - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x1 = xa.min;

+			}else if(x2 <= x1 && x2 < xa.min){

+				if(x1 < xa.min) continue;

+				y2 = (xa.min - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x2 = xa.min;

+			}

+								

+			if(x1 >= x2 && x1 > xa.max){

+				if(x2 > xa.max) continue;

+				y1 = (xa.max - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x1 = xa.max;

+			}else if(x2 >= x1 && x2 > xa.max){

+				if (x1 > xa.max) continue;

+				y2 = (xa.max - x1) / (x2 - x1) * (y2 - y1) + y1;

+				x2 = xa.max;

+			}

+

+			if(first){

+				ctx.moveTo(tHoz(x1, xa), tVert(bottom, ya) + offset);

+				first = false;

+			}

+			

+			/**

+			 * Now check the case where both is outside.

+			 */

+			if(y1 >= ya.max && y2 >= ya.max){

+				ctx.lineTo(tHoz(x1, xa), tVert(ya.max, ya) + offset);

+				ctx.lineTo(tHoz(x2, xa), tVert(ya.max, ya) + offset);

+				continue;

+			}else if(y1 <= ya.min && y2 <= ya.min){

+				ctx.lineTo(tHoz(x1, xa), tVert(ya.min, ya) + offset);

+				ctx.lineTo(tHoz(x2, xa), tVert(ya.min, ya) + offset);

+				continue;

+			}

+			

+			/**

+			 * Else it's a bit more complicated, there might

+			 * be two rectangles and two triangles we need to fill

+			 * in; to find these keep track of the current x values.

+			 */

+			var x1old = x1, x2old = x2;

+			

+			/**

+			 * And clip the y values, without shortcutting.

+			 * Clip with ymin.

+			 */

+			if(y1 <= y2 && y1 < ya.min && y2 >= ya.min){

+				x1 = (ya.min - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y1 = ya.min;

+			}else if(y2 <= y1 && y2 < ya.min && y1 >= ya.min){

+				x2 = (ya.min - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y2 = ya.min;

+			}

+

+			/**

+			 * Clip with ymax.

+			 */

+			if(y1 >= y2 && y1 > ya.max && y2 <= ya.max){

+				x1 = (ya.max - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y1 = ya.max;

+			}else if(y2 >= y1 && y2 > ya.max && y1 <= ya.max){

+				x2 = (ya.max - y1) / (y2 - y1) * (x2 - x1) + x1;

+				y2 = ya.max;

+			}

+

+			/**

+			 * If the x value was changed we got a rectangle to fill.

+			 */

+			if(x1 != x1old){

+				top = (y1 <= ya.min) ? top = ya.min : ya.max;

+				ctx.lineTo(tHoz(x1old, xa), tVert(top, ya) + offset);

+				ctx.lineTo(tHoz(x1, xa), tVert(top, ya) + offset);

+			}

+		   	

+			/**

+			 * Fill the triangles.

+			 */

+			ctx.lineTo(tHoz(x1, xa), tVert(y1, ya) + offset);

+			ctx.lineTo(tHoz(x2, xa), tVert(y2, ya) + offset);

+

+			/**

+			 * Fill the other rectangle if it's there.

+			 */

+			if(x2 != x2old){

+				top = (y2 <= ya.min) ? ya.min : ya.max;

+				ctx.lineTo(tHoz(x2old, xa), tVert(top, ya) + offset);

+				ctx.lineTo(tHoz(x2, xa), tVert(top, ya) + offset);

+			}

+

+			lastX = Math.max(x2, x2old);

+		}

+		

+		ctx.lineTo(tHoz(lastX, xa), tVert(bottom, ya) + offset);

+		ctx.closePath();

+		ctx.fill();

+	},

+	/**

+	 * Function: (private) drawSeriesLines

+	 * 

+	 * Function draws lines series in the canvas element.

+	 * 

+	 * Parameters:

+	 * 		series - Series with options.lines.show = true.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	drawSeriesLines: function(series){

+		series = series || this.series;

+		var ctx = this.ctx;

+		ctx.save();

+		ctx.translate(this.plotOffset.left, this.plotOffset.top);

+		ctx.lineJoin = 'round';

+

+		var lw = series.lines.lineWidth;

+		var sw = series.shadowSize;

+

+		if(sw > 0){

+			ctx.lineWidth = sw / 2;

+

+			var offset = lw/2 + ctx.lineWidth/2;

+			

+			ctx.strokeStyle = "rgba(0,0,0,0.1)";

+			this.plotLine(series, offset + sw/2);

+

+			ctx.strokeStyle = "rgba(0,0,0,0.2)";

+			this.plotLine(series, offset);

+

+			if(series.lines.fill) {

+				ctx.fillStyle = "rgba(0,0,0,0.05)";

+				this.plotLineArea(series, offset + sw/2);

+			}

+		}

+

+		ctx.lineWidth = lw;

+		ctx.strokeStyle = series.color;

+		if(series.lines.fill){

+			ctx.fillStyle = series.lines.fillColor != null ? series.lines.fillColor : Flotr.parseColor(series.color).scale(null, null, null, series.lines.fillOpacity).toString();

+			this.plotLineArea(series, 0);

+		}

+

+		this.plotLine(series, 0);

+		ctx.restore();

+	},

+	/**

+	 * Function: drawSeriesPoints

+	 * 

+	 * Function draws point series in the canvas element.

+	 * 

+	 * Parameters:

+	 * 		series - Series with options.points.show = true.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	drawSeriesPoints: function(series) {

+		var ctx = this.ctx;

+		

+		ctx.save();

+		ctx.translate(this.plotOffset.left, this.plotOffset.top);

+

+		var lw = series.lines.lineWidth;

+		var sw = series.shadowSize;

+		

+		if(sw > 0){

+			ctx.lineWidth = sw / 2;

+      

+			ctx.strokeStyle = 'rgba(0,0,0,0.1)';

+			this.plotPointShadows(series, sw/2 + ctx.lineWidth/2, series.points.radius);

+

+			ctx.strokeStyle = 'rgba(0,0,0,0.2)';

+			this.plotPointShadows(series, ctx.lineWidth/2, series.points.radius);

+		}

+

+		ctx.lineWidth = series.points.lineWidth;

+		ctx.strokeStyle = series.color;

+		ctx.fillStyle = series.points.fillColor != null ? series.points.fillColor : series.color;

+		this.plotPoints(series, series.points.radius, series.points.fill);

+		ctx.restore();

+	},

+	plotPoints: function (series, radius, fill) {

+    var xa = series.xaxis,

+        ya = series.yaxis,

+		    ctx = this.ctx, i,

+		    data = series.data;

+			

+		for(i = data.length - 1; i > -1; --i){

+			var x = data[i][0], y = data[i][1];

+			if(x < xa.min || x > xa.max || y < ya.min || y > ya.max)

+				continue;

+			

+			ctx.beginPath();

+			ctx.arc(this.tHoz(x, xa), this.tVert(y, ya), radius, 0, 2 * Math.PI, true);

+			if(fill) ctx.fill();

+			ctx.stroke();

+		}

+	},

+	plotPointShadows: function(series, offset, radius){

+    var xa = series.xaxis,

+        ya = series.yaxis,

+		    ctx = this.ctx, i,

+		    data = series.data;

+			

+		for(i = data.length - 1; i > -1; --i){

+			var x = data[i][0], y = data[i][1];

+			if (x < xa.min || x > xa.max || y < ya.min || y > ya.max)

+				continue;

+			ctx.beginPath();

+			ctx.arc(this.tHoz(x, xa), this.tVert(y, ya) + offset, radius, 0, Math.PI, false);

+			ctx.stroke();

+		}

+	},

+	/**

+	 * Function: drawSeriesBars

+	 * 

+	 * Function draws bar series in the canvas element.

+	 * 

+	 * Parameters:

+	 * 		series - Series with options.bars.show = true.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	drawSeriesBars: function(series) {

+		var ctx = this.ctx,

+			bw = series.bars.barWidth,

+			lw = Math.min(series.bars.lineWidth, bw);

+		

+		ctx.save();

+		ctx.translate(this.plotOffset.left, this.plotOffset.top);

+		ctx.lineJoin = 'miter';

+

+		/**

+		 * @todo linewidth not interpreted the right way.

+		 */

+		ctx.lineWidth = lw;

+		ctx.strokeStyle = series.color;

+    

+		this.plotBarsShadows(series, bw, 0, series.bars.fill);

+

+		if(series.bars.fill){

+			ctx.fillStyle = series.bars.fillColor != null ? series.bars.fillColor : Flotr.parseColor(series.color).scale(null, null, null, series.bars.fillOpacity).toString();

+		}

+    

+		this.plotBars(series, bw, 0, series.bars.fill);

+		ctx.restore();

+	},

+	plotBars: function(series, barWidth, offset, fill){

+		var data = series.data;

+		if(data.length < 1) return;

+		

+    var xa = series.xaxis,

+        ya = series.yaxis,

+  			ctx = this.ctx,

+  			tHoz = this.tHoz.bind(this),

+  			tVert = this.tVert.bind(this);

+

+		for(var i = 0; i < data.length; i++){

+			var x = data[i][0],

+			    y = data[i][1];

+			var drawLeft = true, drawTop = true, drawRight = true;

+			

+			// Stacked bars

+			var stackOffset = 0;

+			if(series.bars.stacked) {

+			  xa.values.each(function(o, v) {

+			    if (v == x) {

+			      stackOffset = o.stack || 0;

+			      o.stack = stackOffset + y;

+			    }

+			  });

+			}

+

+			// @todo: fix horizontal bars support

+			// Horizontal bars

+			if(series.bars.horizontal)

+				var left = stackOffset, right = x + stackOffset, bottom = y, top = y + barWidth;

+			else 

+				var left = x, right = x + barWidth, bottom = stackOffset, top = y + stackOffset;

+

+			if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)

+				continue;

+

+			if(left < xa.min){

+				left = xa.min;

+				drawLeft = false;

+			}

+

+			if(right > xa.max){

+				right = xa.max;

+				if (xa.lastSerie != series && series.bars.horizontal)

+					drawTop = false;

+			}

+

+			if(bottom < ya.min)

+				bottom = ya.min;

+

+			if(top > ya.max){

+				top = ya.max;

+				if (ya.lastSerie != series && !series.bars.horizontal)

+					drawTop = false;

+			}

+      

+			/**

+			 * Fill the bar.

+			 */

+			if(fill){

+				ctx.beginPath();

+				ctx.moveTo(tHoz(left, xa), tVert(bottom, ya) + offset);

+				ctx.lineTo(tHoz(left, xa), tVert(top, ya) + offset);

+				ctx.lineTo(tHoz(right, xa), tVert(top, ya) + offset);

+				ctx.lineTo(tHoz(right, xa), tVert(bottom, ya) + offset);

+				ctx.fill();

+			}

+

+			/**

+			 * Draw bar outline/border.

+			 */

+			if(series.bars.lineWidth != 0 && (drawLeft || drawRight || drawTop)){

+				ctx.beginPath();

+				ctx.moveTo(tHoz(left, xa), tVert(bottom, ya) + offset);

+				

+				ctx[drawLeft ?'lineTo':'moveTo'](tHoz(left, xa), tVert(top, ya) + offset);

+				ctx[drawTop  ?'lineTo':'moveTo'](tHoz(right, xa), tVert(top, ya) + offset);

+				ctx[drawRight?'lineTo':'moveTo'](tHoz(right, xa), tVert(bottom, ya) + offset);

+				         

+				ctx.stroke();

+			}

+		}

+	},

+  plotBarsShadows: function(series, barWidth, offset){

+		var data = series.data;

+    if(data.length < 1) return;

+    

+    var xa = series.xaxis,

+        ya = series.yaxis,

+        ctx = this.ctx,

+        tHoz = this.tHoz.bind(this),

+        tVert = this.tVert.bind(this),

+        sw = this.options.shadowSize;

+

+    for(var i = 0; i < data.length; i++){

+      var x = data[i][0],

+          y = data[i][1];

+      

+      // Stacked bars

+      var stackOffset = 0;

+			if(series.bars.stacked) {

+			  xa.values.each(function(o, v) {

+			    if (v == x) {

+			      stackOffset = o.stackShadow || 0;

+			      o.stackShadow = stackOffset + y;

+			    }

+			  });

+			}

+      

+      // Horizontal bars

+      if(series.bars.horizontal) 

+        var left = stackOffset, right = x + stackOffset, bottom = y, top = y + barWidth;

+      else 

+        var left = x, right = x + barWidth, bottom = stackOffset, top = y + stackOffset;

+

+      if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)

+        continue;

+

+      if(left < xa.min)   left = xa.min;

+      if(right > xa.max)  right = xa.max;

+      if(bottom < ya.min) bottom = ya.min;

+      if(top > ya.max)    top = ya.max;

+      

+      var width =  tHoz(right, xa)-tHoz(left, xa)-((tHoz(right, xa)+sw <= this.plotWidth) ? 0 : sw);

+      var height = Math.max(0, tVert(bottom, ya)-tVert(top, ya)-((tVert(bottom, ya)+sw <= this.plotHeight) ? 0 : sw));

+

+      ctx.fillStyle = 'rgba(0,0,0,0.05)';

+      ctx.fillRect(Math.min(tHoz(left, xa)+sw, this.plotWidth), Math.min(tVert(top, ya)+sw, this.plotWidth), width, height);

+    }

+  },

+	/**

+	 * Function: drawSeriesCandles

+	 * 

+	 * Function draws candles series in the canvas element.

+	 * 

+	 * Parameters:

+	 * 		series - Series with options.candles.show = true.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	drawSeriesCandles: function(series) {

+		var ctx = this.ctx,

+			  bw = series.candles.candleWidth;

+		

+		ctx.save();

+		ctx.translate(this.plotOffset.left, this.plotOffset.top);

+		ctx.lineJoin = 'miter';

+

+		/**

+		 * @todo linewidth not interpreted the right way.

+		 */

+		ctx.lineWidth = series.candles.lineWidth;

+		this.plotCandlesShadows(series, bw/2);

+		this.plotCandles(series, bw/2);

+		

+		ctx.restore();

+	},

+	plotCandles: function(series, offset){

+		var data = series.data;

+		if(data.length < 1) return;

+		

+    var xa = series.xaxis,

+        ya = series.yaxis,

+  			ctx = this.ctx,

+  			tHoz = this.tHoz.bind(this),

+  			tVert = this.tVert.bind(this);

+

+		for(var i = 0; i < data.length; i++){

+      var d     = data[i],

+  		    x     = d[0],

+  		    open  = d[1],

+  		    high  = d[2],

+  		    low   = d[3],

+  		    close = d[4];

+

+			var left    = x,

+			    right   = x + series.candles.candleWidth,

+          bottom  = Math.max(ya.min, low),

+	        top     = Math.min(ya.max, high),

+          bottom2 = Math.max(ya.min, Math.min(open, close)),

+	        top2    = Math.min(ya.max, Math.max(open, close));

+

+			if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)

+				continue;

+

+			var color = series.candles[open>close?'downFillColor':'upFillColor'];

+			/**

+			 * Fill the candle.

+			 */

+			if(series.candles.fill && !series.candles.barcharts){

+				ctx.fillStyle = Flotr.parseColor(color).scale(null, null, null, series.candles.fillOpacity).toString();

+				ctx.fillRect(tHoz(left, xa), tVert(top2, ya) + offset, tHoz(right, xa) - tHoz(left, xa), tVert(bottom2, ya) - tVert(top2, ya));

+			}

+

+			/**

+			 * Draw candle outline/border, high, low.

+			 */

+			if(series.candles.lineWidth || series.candles.wickLineWidth){

+				var x, y, pixelOffset = (series.candles.wickLineWidth % 2) / 2;

+

+				x = Math.floor(tHoz((left + right) / 2), xa) + pixelOffset;

+				

+			  ctx.save();

+			  ctx.strokeStyle = color;

+			  ctx.lineWidth = series.candles.wickLineWidth;

+			  ctx.lineCap = 'butt';

+			  

+				if (series.candles.barcharts) {

+					ctx.beginPath();

+					

+					ctx.moveTo(x, Math.floor(tVert(top, ya) + offset));

+					ctx.lineTo(x, Math.floor(tVert(bottom, ya) + offset));

+					

+					y = Math.floor(tVert(open, ya) + offset)+0.5;

+					ctx.moveTo(Math.floor(tHoz(left, xa))+pixelOffset, y);

+					ctx.lineTo(x, y);

+					

+					y = Math.floor(tVert(close, ya) + offset)+0.5;

+					ctx.moveTo(Math.floor(tHoz(right, xa))+pixelOffset, y);

+					ctx.lineTo(x, y);

+				} 

+				else {

+  				ctx.strokeRect(tHoz(left, xa), tVert(top2, ya) + offset, tHoz(right, xa) - tHoz(left, xa), tVert(bottom2, ya) - tVert(top2, ya));

+

+  				ctx.beginPath();

+  				ctx.moveTo(x, Math.floor(tVert(top2,    ya) + offset));

+  				ctx.lineTo(x, Math.floor(tVert(top,     ya) + offset));

+  				ctx.moveTo(x, Math.floor(tVert(bottom2, ya) + offset));

+  				ctx.lineTo(x, Math.floor(tVert(bottom,  ya) + offset));

+				}

+				

+				ctx.stroke();

+				ctx.restore();

+			}

+		}

+	},

+  plotCandlesShadows: function(series, offset){

+		var data = series.data;

+    if(data.length < 1 || series.candles.barcharts) return;

+    

+    var xa = series.xaxis,

+        ya = series.yaxis,

+        tHoz = this.tHoz.bind(this),

+        tVert = this.tVert.bind(this),

+        sw = this.options.shadowSize;

+

+    for(var i = 0; i < data.length; i++){

+      var d     = data[i],

+      		x     = d[0],

+	        open  = d[1],

+	        high  = d[2],

+	        low   = d[3],

+	        close = d[4];

+      

+			var left   = x,

+	        right  = x + series.candles.candleWidth,

+          bottom = Math.max(ya.min, Math.min(open, close)),

+	        top    = Math.min(ya.max, Math.max(open, close));

+

+      if(right < xa.min || left > xa.max || top < ya.min || bottom > ya.max)

+        continue;

+

+      var width =  tHoz(right, xa)-tHoz(left, xa)-((tHoz(right, xa)+sw <= this.plotWidth) ? 0 : sw);

+      var height = Math.max(0, tVert(bottom, ya)-tVert(top, ya)-((tVert(bottom, ya)+sw <= this.plotHeight) ? 0 : sw));

+

+      this.ctx.fillStyle = 'rgba(0,0,0,0.05)';

+      this.ctx.fillRect(Math.min(tHoz(left, xa)+sw, this.plotWidth), Math.min(tVert(top, ya)+sw, this.plotWidth), width, height);

+    }

+  },

+  /**

+   * Function: drawSeriesRadar

+   * 

+   * Function draws a radar chart on the canvas element.

+   * 

+   * Parameters:

+   *    series - Series with options.radar.show = true.

+   * 

+   * Returns:

+   *    void

+   */

+  drawSeriesRadar: function(series) {

+	var ctx = this.ctx,

+		options = this.options, sides= series.data.length;

+		

+	var degreesInRadiansForAngle = Math.PI * 2 / sides,

+	      nintyDegrees = Math.PI / 2;

+	

+	var poly = {};

+	

+	/* 

+	Draw radar grid

+	

+	poly.xaxis = series.xaxis;

+	poly.yaxis = series.yaxis;

+	ctx.save();

+	ctx.translate(this.plotOffset.left, this.plotOffset.top);

+	ctx.lineJoin = 'round';

+	for (radius = 20; radius <= 100; radius += 20) {

+	poly.data = new Array();

+	for (i = 0; i < sides; i++) {

+		angle = nintyDegrees + (degreesInRadiansForAngle * i);

+		poly.data[i] = [radius * Math.cos(angle), radius * Math.sin(angle)]

+	}

+	poly.data[sides] = poly.data[0];

+	this.plotLine(poly,0);}

+	

+	var outside = poly.data;

+	for (i = 0; i < sides; i++) {

+		poly.data = new Array();

+		poly.data[0] = [0,0];

+		poly.data[1] = outside[i];

+		this.plotLine(poly,0);

+	}

+	*/

+	

+	/*

+	Convert Series data into X, Y co-ordinates

+	*/

+	if (!series.dataInRadarFormat) {

+	poly.data = new Array();

+	for (i = 0; i < sides; i++) {

+		angle = nintyDegrees + (degreesInRadiansForAngle * i);

+		poly.data[i] = [series.data[i][1] * Math.cos(angle), series.data[i][1] * Math.sin(angle), series.data[i][0], series.data[i][1]]

+	}

+	poly.data[sides] = poly.data[0];

+	series.data = poly.data;

+	series.lines = series.radar;

+	series.lines.show = false;

+	series.dataInRadarFormat = true;

+	}

+	

+	this.drawSeriesLines(series);

+	

+},

+  

+  

+  /**

+   * Function: drawSeriesPie

+   * 

+   * Function draws a pie in the canvas element.

+   * 

+   * Parameters:

+   *    series - Series with options.pie.show = true.

+   * 

+   * Returns:

+   *    void

+   */

+  drawSeriesPie: function(series) {

+    if (!this.options.pie.drawn) {

+    var ctx = this.ctx,

+        options = this.options,

+        lw = series.pie.lineWidth,

+        sw = series.shadowSize,

+        data = series.data,

+        radius = (Math.min(this.canvasWidth, this.canvasHeight) * series.pie.sizeRatio) / 2,

+        html = [];

+    

+    var vScale = 1;//Math.cos(series.pie.viewAngle);

+    var plotTickness = Math.sin(series.pie.viewAngle)*series.pie.spliceThickness / vScale;

+    

+    var style = {

+      size: options.fontSize*1.2,

+      color: options.grid.color,

+      weight: 1.5

+    };

+    

+    var center = {

+      x: (this.canvasWidth+this.plotOffset.left)/2,

+      y: (this.canvasHeight-this.plotOffset.bottom)/2

+    };

+    

+    // Pie portions

+    var portions = this.series.collect(function(hash, index){

+    	if (hash.pie.show)

+      return {

+        name: (hash.label || hash.data[0][1]),

+        value: [index, hash.data[0][1]],

+        explode: hash.pie.explode

+      };

+    });

+    

+    // Sum of the portions' angles

+    var sum = portions.pluck('value').pluck(1).inject(0, function(acc, n) { return acc + n; });

+    

+    var fraction = 0.0,

+        angle = series.pie.startAngle,

+        value = 0.0;

+    

+    var slices = portions.collect(function(slice){

+      angle += fraction;

+      value = parseFloat(slice.value[1]); // @warning : won't support null values !!

+      fraction = value/sum;

+      return {

+        name:     slice.name,

+        fraction: fraction,

+        x:        slice.value[0],

+        y:        value,

+        explode:  slice.explode,

+        startAngle: 2 * angle * Math.PI,

+        endAngle:   2 * (angle + fraction) * Math.PI

+      };

+    });

+    

+    ctx.save();

+

+    if(sw > 0){

+	    slices.each(function (slice) {

+        var bisection = (slice.startAngle + slice.endAngle) / 2;

+        

+        var xOffset = center.x + Math.cos(bisection) * slice.explode + sw;

+        var yOffset = center.y + Math.sin(bisection) * slice.explode + sw;

+        

+		    this.plotSlice(xOffset, yOffset, radius, slice.startAngle, slice.endAngle, false, vScale);

+

+        ctx.fillStyle = 'rgba(0,0,0,0.1)';

+        ctx.fill();

+      }, this);

+    }

+    

+    if (options.HtmlText) {

+      html = ['<div style="color:' + this.options.grid.color + '" class="flotr-labels">'];

+    }

+    

+    slices.each(function (slice, index) {

+      var bisection = (slice.startAngle + slice.endAngle) / 2;

+      var color = options.colors[index];

+      

+      var xOffset = center.x + Math.cos(bisection) * slice.explode;

+      var yOffset = center.y + Math.sin(bisection) * slice.explode;

+      

+      this.plotSlice(xOffset, yOffset, radius, slice.startAngle, slice.endAngle, false, vScale);

+      

+      if(series.pie.fill){

+        ctx.fillStyle = Flotr.parseColor(color).scale(null, null, null, series.pie.fillOpacity).toString();

+        ctx.fill();

+      }

+      ctx.lineWidth = lw;

+      ctx.strokeStyle = color;

+      ctx.stroke();

+      

+      /*ctx.save();

+      ctx.scale(1, vScale);

+      

+      ctx.moveTo(xOffset, yOffset);

+      ctx.beginPath();

+      ctx.lineTo(xOffset, yOffset+plotTickness);

+      ctx.lineTo(xOffset+Math.cos(slice.startAngle)*radius, yOffset+Math.sin(slice.startAngle)*radius+plotTickness);

+      ctx.lineTo(xOffset+Math.cos(slice.startAngle)*radius, yOffset+Math.sin(slice.startAngle)*radius);

+      ctx.lineTo(xOffset, yOffset);

+      ctx.closePath();

+      ctx.fill();ctx.stroke();

+      

+      ctx.moveTo(xOffset, yOffset);

+      ctx.beginPath();

+      ctx.lineTo(xOffset, yOffset+plotTickness);

+      ctx.lineTo(xOffset+Math.cos(slice.endAngle)*radius, yOffset+Math.sin(slice.endAngle)*radius+plotTickness);

+      ctx.lineTo(xOffset+Math.cos(slice.endAngle)*radius, yOffset+Math.sin(slice.endAngle)*radius);

+      ctx.lineTo(xOffset, yOffset);

+      ctx.closePath();

+      ctx.fill();ctx.stroke();

+      

+      ctx.moveTo(xOffset+Math.cos(slice.startAngle)*radius, yOffset+Math.sin(slice.startAngle)*radius);

+      ctx.beginPath();

+      ctx.lineTo(xOffset+Math.cos(slice.startAngle)*radius, yOffset+Math.sin(slice.startAngle)*radius+plotTickness);

+      ctx.arc(xOffset, yOffset+plotTickness, radius, slice.startAngle, slice.endAngle, false);

+      ctx.lineTo(xOffset+Math.cos(slice.endAngle)*radius, yOffset+Math.sin(slice.endAngle)*radius);

+      ctx.arc(xOffset, yOffset, radius, slice.endAngle, slice.startAngle, true);

+      ctx.closePath();

+      ctx.fill();ctx.stroke();

+      

+      ctx.scale(1, 1/vScale);

+      this.plotSlice(xOffset, yOffset+plotTickness, radius, slice.startAngle, slice.endAngle, false, vScale);

+      ctx.stroke();

+      if(series.pie.fill){

+        ctx.fillStyle = Flotr.parseColor(color).scale(null, null, null, series.pie.fillOpacity).toString();

+        ctx.fill();

+      }

+      

+      ctx.restore();*/

+      

+      var label = options.pie.labelFormatter(slice);

+      

+      var textAlignRight = (Math.cos(bisection) < 0);

+      var distX = xOffset + Math.cos(bisection) * (series.pie.explode + radius);

+      var distY = yOffset + Math.sin(bisection) * (series.pie.explode + radius);

+      

+      if (slice.fraction && label) {

+        if (options.HtmlText) {

+          var divStyle = 'position:absolute;top:' + (distY - 5) + 'px;'; //@todo: change

+          if (textAlignRight) {

+            divStyle += 'right:'+(this.canvasWidth - distX)+'px;text-align:right;';

+          }

+          else {

+            divStyle += 'left:'+distX+'px;text-align:left;';

+          }

+          html.push('<div style="' + divStyle + '" class="flotr-grid-label">' + label + '</div>');

+        }

+        else {

+          style.halign = textAlignRight ? 'r' : 'l';

+          ctx.drawText(

+            label, 

+            distX, 

+            distY + style.size / 2, 

+            style

+          );

+        }

+      }

+    }, this);

+

+    if (options.HtmlText) {

+      html.push('</div>');    

+      this.el.insert(html.join(''));

+    }

+    

+    ctx.restore();

+    options.pie.drawn = true;

+    }

+  },

+  plotSlice: function(x, y, radius, startAngle, endAngle, fill, vScale) {

+    var ctx = this.ctx;

+    vScale = vScale || 1;

+    

+    ctx.save();

+    ctx.scale(1, vScale);

+    ctx.beginPath();

+    ctx.moveTo(x, y);

+    ctx.arc   (x, y, radius, startAngle, endAngle, fill);

+    ctx.lineTo(x, y);

+    ctx.closePath();

+    ctx.restore();

+  },

+  plotPie: function() {}, 

+	/**

+	 * Function: insertLegend

+	 * 

+	 * Function adds a legend div to the canvas container or draws it on the canvas.

+	 * 

+	 * Parameters:

+	 * 		none

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	insertLegend: function(){

+		if(!this.options.legend.show)

+			return;

+			

+		var series = this.series,

+			plotOffset = this.plotOffset,

+			options = this.options,

+			fragments = [],

+			rowStarted = false, 

+			ctx = this.ctx,

+			i;

+			

+		var noLegendItems = series.findAll(function(s) {return (s.label && !s.hide)}).size();

+

+    if (noLegendItems) {

+	    if (!options.HtmlText && this.textEnabled) {

+	      var style = {

+	        size: options.fontSize*1.1,

+	        color: options.grid.color

+	      };

+	      

+	      // @todo: take css into account

+	      //var dummyDiv = this.el.insert('<div class="flotr-legend" style="position:absolute;top:-10000px;"></div>');

+	      

+	      var p = options.legend.position, 

+	          m = options.legend.margin,

+	          lbw = options.legend.labelBoxWidth,

+	          lbh = options.legend.labelBoxHeight,

+	          lbm = options.legend.labelBoxMargin,

+	          offsetX = plotOffset.left + m,

+	          offsetY = plotOffset.top + m;

+	      

+	      // We calculate the labels' max width

+	      var labelMaxWidth = 0;

+	      for(i = series.length - 1; i > -1; --i){

+	        if(!series[i].label || series[i].hide) continue;

+	        var label = options.legend.labelFormatter(series[i].label);	

+	        labelMaxWidth = Math.max(labelMaxWidth, ctx.measureText(label, style));

+	      }

+	      

+	      var legendWidth  = Math.round(lbw + lbm*3 + labelMaxWidth),

+	          legendHeight = Math.round(noLegendItems*(lbm+lbh) + lbm);

+	

+	      if(p.charAt(0) == 's') offsetY = plotOffset.top + this.plotHeight - (m + legendHeight);

+	      if(p.charAt(1) == 'e') offsetX = plotOffset.left + this.plotWidth - (m + legendWidth);

+

+	      // Legend box

+	      var color = Flotr.parseColor(options.legend.backgroundColor || 'rgb(240,240,240)').scale(null, null, null, options.legend.backgroundOpacity || 0.1).toString();

+	      

+	      ctx.fillStyle = color;

+	      ctx.fillRect(offsetX, offsetY, legendWidth, legendHeight);

+	      ctx.strokeStyle = options.legend.labelBoxBorderColor;

+	      ctx.strokeRect(Flotr.toPixel(offsetX), Flotr.toPixel(offsetY), legendWidth, legendHeight);

+	      

+	      // Legend labels

+	      var x = offsetX + lbm;

+	      var y = offsetY + lbm;

+	      for(i = 0; i < series.length; i++){

+	        if(!series[i].label || series[i].hide) continue;

+	        var label = options.legend.labelFormatter(series[i].label);

+

+	        ctx.fillStyle = series[i].color;

+	        ctx.fillRect(x, y, lbw-1, lbh-1);

+	        

+	        ctx.strokeStyle = options.legend.labelBoxBorderColor;

+	        ctx.lineWidth = 1;

+	        ctx.strokeRect(Math.ceil(x)-1.5, Math.ceil(y)-1.5, lbw+2, lbh+2);

+	        

+	        // Legend text

+	        ctx.drawText(

+	          label,

+	          x + lbw + lbm,

+	          y + (lbh + style.size - ctx.fontDescent(style))/2,

+	          style

+	        );

+	        

+	        y += lbh + lbm;

+	      }

+	    }

+	    else {

+	  		for(i = 0; i < series.length; ++i){

+	  			if(!series[i].label || series[i].hide) continue;

+	  			

+	  			if(i % options.legend.noColumns == 0){

+	  				fragments.push(rowStarted ? '</tr><tr>' : '<tr>');

+	  				rowStarted = true;

+	  			}

+	  

+	  			var label = options.legend.labelFormatter(series[i].label);

+	  			

+	  			fragments.push('<td class="flotr-legend-color-box"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:' + options.legend.labelBoxWidth + 'px;height:' + options.legend.labelBoxHeight + 'px;background-color:' + series[i].color + '"></div></div></td>' +

+	  				'<td class="flotr-legend-label">' + label + '</td>');

+	  		}

+	  		if(rowStarted) fragments.push('</tr>');

+	  		

+	  		if(fragments.length > 0){

+	  			var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';

+	  			if(options.legend.container != null){

+	  				$(options.legend.container).update(table);

+	  			}else{

+	  				var pos = '';

+	  				var p = options.legend.position, m = options.legend.margin;

+	  				

+	  				     if(p.charAt(0) == 'n') pos += 'top:' + (m + plotOffset.top) + 'px;';

+	  				else if(p.charAt(0) == 's') pos += 'bottom:' + (m + plotOffset.bottom) + 'px;';					

+	  				     if(p.charAt(1) == 'e') pos += 'right:' + (m + plotOffset.right) + 'px;';

+	  				else if(p.charAt(1) == 'w') pos += 'left:' + (m + plotOffset.left) + 'px;';

+	  				     

+	  				var div = this.el.insert('<div class="flotr-legend" style="position:absolute;z-index:2;' + pos +'">' + table + '</div>').select('div.flotr-legend').first();

+	  				

+	  				if(options.legend.backgroundOpacity != 0.0){

+	  					/**

+	  					 * Put in the transparent background separately to avoid blended labels and

+	  					 * label boxes.

+	  					 */

+	  					var c = options.legend.backgroundColor;

+	  					if(c == null){

+	  						var tmp = (options.grid.backgroundColor != null) ? options.grid.backgroundColor : Flotr.extractColor(div);

+	  						c = Flotr.parseColor(tmp).adjust(null, null, null, 1).toString();

+	  					}

+	  					this.el.insert('<div class="flotr-legend-bg" style="position:absolute;width:' + div.getWidth() + 'px;height:' + div.getHeight() + 'px;' + pos +'background-color:' + c + ';"> </div>').select('div.flotr-legend-bg').first().setStyle({

+	  						'opacity': options.legend.backgroundOpacity

+	  					});						

+	  				}

+	  			}

+	  		}

+	    }

+    }

+	},

+	/**

+	 * Function: getEventPosition

+	 * 

+	 * Calculates the coordinates from a mouse event object.

+	 * 

+	 * Parameters:

+	 * 		event - Mouse Event object.

+	 * 

+	 * Returns:

+	 * 		Object with x and y coordinates of the mouse.

+	 */

+	getEventPosition: function (event){

+		var offset = this.overlay.cumulativeOffset(),

+			rx = (event.pageX - offset.left - this.plotOffset.left),

+			ry = (event.pageY - offset.top - this.plotOffset.top),

+			ax = 0, ay = 0

+			

+		if(event.pageX == null && event.clientX != null){

+			var de = document.documentElement, b = document.body;

+			ax = event.clientX + (de && de.scrollLeft || b.scrollLeft || 0);

+			ay = event.clientY + (de && de.scrollTop || b.scrollTop || 0);

+		}else{

+			ax = event.pageX;

+			ay = event.pageY;

+		}

+		

+		return {

+			x:  this.axes.x.min  + rx / this.axes.x.scale,

+			x2: this.axes.x2.min + rx / this.axes.x2.scale,

+			y:  this.axes.y.max  - ry / this.axes.y.scale,

+			y2: this.axes.y2.max - ry / this.axes.y2.scale,

+			relX: rx,

+			relY: ry,

+			absX: ax,

+			absY: ay

+		};

+	},

+	/**

+	 * Function: clickHandler

+	 * 

+	 * Handler observes the 'click' event and fires the 'flotr:click' event.

+	 * 

+	 * Parameters:

+	 * 		event - 'click' Event object.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	clickHandler: function(event){

+		if(this.ignoreClick){

+			this.ignoreClick = false;

+			return;

+		}

+		this.el.fire('flotr:click', [this.getEventPosition(event), this]);

+	},

+	/**

+	 * Function: mouseMoveHandler

+	 * 

+	 * Handler observes mouse movement over the graph area. Fires the 

+	 * 'flotr:mousemove' event.

+	 * 

+	 * Parameters:

+	 * 		event - 'mousemove' Event object.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	mouseMoveHandler: function(event){

+ 		var pos = this.getEventPosition(event);

+    

+		this.lastMousePos.pageX = pos.absX;

+		this.lastMousePos.pageY = pos.absY;	

+		if(this.selectionInterval == null && (this.options.mouse.track || this.series.any(function(s){return s.mouse && s.mouse.track;}))){	

+			this.hit(pos);

+		}

+    

+		this.el.fire('flotr:mousemove', [event, pos, this]);

+	},

+	/**

+	 * Function: mouseDownHandler

+	 * 

+	 * Handler observes the 'mousedown' event.

+	 * 

+	 * Parameters:

+	 * 		event - 'mousedown' Event object.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	mouseDownHandler: function (event){

+    if(event.isRightClick()) {

+      event.stop();

+      var overlay = this.overlay;

+      overlay.hide();

+      

+      function cancelContextMenu () {

+        overlay.show();

+        $(document).stopObserving('mousemove', cancelContextMenu);

+      }

+      $(document).observe('mousemove', cancelContextMenu);

+      return;

+    }

+    

+		if(!this.options.selection.mode || !event.isLeftClick()) return;

+		

+		this.setSelectionPos(this.selection.first, event);				

+		if(this.selectionInterval != null){

+			clearInterval(this.selectionInterval);

+		}

+		this.lastMousePos.pageX = null;

+		this.selectionInterval = setInterval(this.updateSelection.bind(this), 1000/this.options.selection.fps);

+		

+		this.mouseUpHandler = this.mouseUpHandler.bind(this);

+		$(document).observe('mouseup', this.mouseUpHandler);

+	},

+	/**

+	 * Function: (private) fireSelectEvent

+	 * 

+	 * Fires the 'flotr:select' event when the user made a selection.

+	 * 

+	 * Parameters:

+	 * 		none

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	fireSelectEvent: function(){

+		var a = this.axes, selection = this.selection,

+			x1 = (selection.first.x <= selection.second.x) ? selection.first.x : selection.second.x,

+			x2 = (selection.first.x <= selection.second.x) ? selection.second.x : selection.first.x,

+			y1 = (selection.first.y >= selection.second.y) ? selection.first.y : selection.second.y,

+			y2 = (selection.first.y >= selection.second.y) ? selection.second.y : selection.first.y;

+		

+		x1 = a.x.min + x1 / a.x.scale;

+		x2 = a.x.min + x2 / a.x.scale;

+		y1 = a.y.max - y1 / a.y.scale;

+		y2 = a.y.max - y2 / a.y.scale;

+

+		this.el.fire('flotr:select', [{x1:x1, y1:y1, x2:x2, y2:y2}, this]);

+	},

+	/**

+	 * Function: (private) mouseUpHandler

+	 * 

+	 * Handler observes the mouseup event for the document. 

+	 * 

+	 * Parameters:

+	 * 		event - 'mouseup' Event object.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	mouseUpHandler: function(event){

+    $(document).stopObserving('mouseup', this.mouseUpHandler);

+    event.stop();

+    

+		if(this.selectionInterval != null){

+			clearInterval(this.selectionInterval);

+			this.selectionInterval = null;

+		}

+

+		this.setSelectionPos(this.selection.second, event);

+		this.clearSelection();

+		

+		if(this.selectionIsSane()){

+			this.drawSelection();

+			this.fireSelectEvent();

+			this.ignoreClick = true;

+		}

+	},

+	/**

+	 * Function: setSelectionPos

+	 * 

+	 * Calculates the position of the selection.

+	 * 

+	 * Parameters:

+	 * 		pos - Position object.

+	 * 		event - Event object.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	setSelectionPos: function(pos, event) {

+		var options = this.options,

+		    offset = $(this.overlay).cumulativeOffset();

+		

+		if(options.selection.mode.indexOf('x') == -1){

+			pos.x = (pos == this.selection.first) ? 0 : this.plotWidth;			   

+		}else{

+			pos.x = event.pageX - offset.left - this.plotOffset.left;

+			pos.x = Math.min(Math.max(0, pos.x), this.plotWidth);

+		}

+

+		if (options.selection.mode.indexOf('y') == -1){

+			pos.y = (pos == this.selection.first) ? 0 : this.plotHeight;

+		}else{

+			pos.y = event.pageY - offset.top - this.plotOffset.top;

+			pos.y = Math.min(Math.max(0, pos.y), this.plotHeight);

+		}

+	},

+	/**

+	 * Function: updateSelection

+	 * 

+	 * Updates (draws) the selection box.

+	 * 

+	 * Parameters:

+	 * 		none

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	updateSelection: function(){

+		if(this.lastMousePos.pageX == null) return;

+		

+		this.setSelectionPos(this.selection.second, this.lastMousePos);

+		this.clearSelection();

+		

+		if(this.selectionIsSane()) this.drawSelection();

+	},

+	/**

+	 * Function: clearSelection

+	 * 

+	 * Removes the selection box from the overlay canvas.

+	 * 

+	 * Parameters:

+	 * 		none

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	clearSelection: function() {

+		if(this.prevSelection == null) return;

+			

+		var prevSelection = this.prevSelection,

+			octx = this.octx,

+			plotOffset = this.plotOffset,

+			x = Math.min(prevSelection.first.x, prevSelection.second.x),

+			y = Math.min(prevSelection.first.y, prevSelection.second.y),

+			w = Math.abs(prevSelection.second.x - prevSelection.first.x),

+			h = Math.abs(prevSelection.second.y - prevSelection.first.y);

+		

+		octx.clearRect(x + plotOffset.left - octx.lineWidth,

+		               y + plotOffset.top - octx.lineWidth,

+		               w + octx.lineWidth*2,

+		               h + octx.lineWidth*2);

+		

+		this.prevSelection = null;

+	},

+	/**

+	 * Function: setSelection

+	 * 

+	 * Allows the user the manually select an area.

+	 * 

+	 * Parameters:

+	 * 		area - Object with coordinates to select.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	setSelection: function(area){

+		var options = this.options,

+			xa = this.axes.x,

+			ya = this.axes.y,

+			vertScale = yaxis.scale,

+			hozScale = xaxis.scale,

+			selX = options.selection.mode.indexOf('x') != -1,

+			selY = options.selection.mode.indexOf('y') != -1;

+		

+		this.clearSelection();

+

+		this.selection.first.y  = selX ? 0 : (ya.max - area.y1) * vertScale;

+		this.selection.second.y = selX ? this.plotHeight : (ya.max - area.y2) * vertScale;			

+		this.selection.first.x  = selY ? 0 : (area.x1 - xa.min) * hozScale;

+		this.selection.second.x = selY ? this.plotWidth : (area.x2 - xa.min) * hozScale;

+		

+		this.drawSelection();

+		this.fireSelectEvent();

+	},

+	/**

+	 * Function: (private) drawSelection

+	 * 

+	 * Draws the selection box.

+	 * 

+	 * Parameters:

+	 * 		none

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	drawSelection: function() {

+		var prevSelection = this.prevSelection,

+			selection = this.selection,

+			octx = this.octx,

+			options = this.options,

+			plotOffset = this.plotOffset;

+		

+		if(prevSelection != null &&

+			selection.first.x == prevSelection.first.x &&

+			selection.first.y == prevSelection.first.y && 

+			selection.second.x == prevSelection.second.x &&

+			selection.second.y == prevSelection.second.y)

+			return;

+		

+		octx.strokeStyle = Flotr.parseColor(options.selection.color).scale(null, null, null, 0.8).toString();

+		octx.lineWidth = 1;

+		octx.lineJoin = 'round';

+		octx.fillStyle = Flotr.parseColor(options.selection.color).scale(null, null, null, 0.4).toString();

+

+		this.prevSelection = {

+			first: { x: selection.first.x, y: selection.first.y },

+			second: { x: selection.second.x, y: selection.second.y }

+		};

+

+		var x = Math.min(selection.first.x, selection.second.x),

+		    y = Math.min(selection.first.y, selection.second.y),

+		    w = Math.abs(selection.second.x - selection.first.x),

+		    h = Math.abs(selection.second.y - selection.first.y);

+		

+		octx.fillRect(x + plotOffset.left, y + plotOffset.top, w, h);

+		octx.strokeRect(x + plotOffset.left, y + plotOffset.top, w, h);

+	},

+	/**

+	 * Function: (private) selectionIsSane

+	 * 

+	 * Determines whether or not the selection is sane and should be drawn.

+	 * 

+	 * Parameters:

+	 * 		none

+	 * 

+	 * Returns:

+	 * 		boolean - True when sane, false otherwise.

+	 */

+	selectionIsSane: function(){

+		var selection = this.selection;

+		return Math.abs(selection.second.x - selection.first.x) >= 5 &&

+		       Math.abs(selection.second.y - selection.first.y) >= 5;

+	},

+	/**

+	 * Function: clearHit

+	 * 

+	 * Removes the mouse tracking point from the overlay.

+	 * 

+	 * Parameters:

+	 * 		none

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	clearHit: function(){

+		if(this.prevHit){

+			var options = this.options,

+			    plotOffset = this.plotOffset,

+			    prevHit = this.prevHit;

+					

+			this.octx.clearRect(

+				this.tHoz(prevHit.x) + plotOffset.left - options.points.radius*2,

+				this.tVert(prevHit.y) + plotOffset.top - options.points.radius*2,

+				options.points.radius*3 + options.points.lineWidth*3, 

+				options.points.radius*3 + options.points.lineWidth*3

+			);

+			this.prevHit = null;

+		}		

+	},

+	/**

+	 * Function: hit

+	 * 

+	 * Retrieves the nearest data point from the mouse cursor. If it's within

+	 * a certain range, draw a point on the overlay canvas and display the x and y

+	 * value of the data.

+	 * 

+	 * Parameters:

+	 * 		mouse - Object that holds the relative x and y coordinates of the cursor.

+	 * 

+	 * Returns:

+	 * 		void

+	 */

+	hit: function(mouse){

+		var series = this.series,

+			options = this.options,

+			prevHit = this.prevHit,

+			plotOffset = this.plotOffset,

+			octx = this.octx, 

+			data, xsens, ysens,

+			/**

+			 * Nearest data element.

+			 */

+			i, n = {

+				dist:Number.MAX_VALUE,

+				x:null,

+				y:null,

+				relX:mouse.relX,

+				relY:mouse.relY,

+				absX:mouse.absX,

+				absY:mouse.absY,

+				mouse:null,

+				radarData:null

+			};

+		

+		for(i = 0; i < series.length; i++){

+			s = series[i];

+			if(!s.mouse.track) continue;

+			data = s.data;

+			xsens = (s.xaxis.scale*s.mouse.sensibility);

+			ysens = (s.yaxis.scale*s.mouse.sensibility);

+

+			for(var j = 0, xpow, ypow; j < data.length; j++){

+				if (data[j][1] === null) continue;

+				xpow = Math.pow(s.xaxis.scale*(data[j][0] - mouse.x), 2);

+				ypow = Math.pow(s.yaxis.scale*(data[j][1] - mouse.y), 2);

+				if(xpow < xsens && ypow < ysens && Math.sqrt(xpow+ypow) < n.dist){

+					n.dist = Math.sqrt(xpow+ypow);

+					n.x = data[j][0];

+					n.y = data[j][1];

+					n.radarLabel = data[j][2];

+					n.radarData = data[j][3];

+					n.mouse = s.mouse;

+				}

+			}

+		}

+		

+		if(n.mouse && n.mouse.track && !prevHit || (prevHit && (n.x != prevHit.x || n.y != prevHit.y))){

+			var mt = this.mouseTrack || this.el.select(".flotr-mouse-value")[0],

+			    pos = '', 

+			    p = options.mouse.position, 

+			    m = options.mouse.margin,

+			    elStyle = 'opacity:0.7;background-color:#000;color:#fff;display:none;position:absolute;padding:2px 8px;-moz-border-radius:4px;border-radius:4px;white-space:nowrap;';

+

+			if (!options.mouse.relative) { // absolute to the canvas

+						 if(p.charAt(0) == 'n') pos += 'top:' + (m + plotOffset.top) + 'px;';

+				else if(p.charAt(0) == 's') pos += 'bottom:' + (m + plotOffset.bottom) + 'px;';					

+				     if(p.charAt(1) == 'e') pos += 'right:' + (m + plotOffset.right) + 'px;';

+				else if(p.charAt(1) == 'w') pos += 'left:' + (m + plotOffset.left) + 'px;';

+			}

+			else { // relative to the mouse

+			       if(p.charAt(0) == 'n') pos += 'bottom:' + (m - plotOffset.top - this.tVert(n.y) + this.canvasHeight) + 'px;';

+				else if(p.charAt(0) == 's') pos += 'top:' + (m + plotOffset.top + this.tVert(n.y)) + 'px;';

+				     if(p.charAt(1) == 'e') pos += 'left:' + (m + plotOffset.left + this.tHoz(n.x)) + 'px;';

+				else if(p.charAt(1) == 'w') pos += 'right:' + (m - plotOffset.left - this.tHoz(n.x) + this.canvasWidth) + 'px;';

+			}

+			

+			elStyle += pos;

+				     

+			if(!mt){

+				this.el.insert('<div class="flotr-mouse-value" style="'+elStyle+'"></div>');

+				mt = this.mouseTrack = this.el.select('.flotr-mouse-value').first();

+			}

+			else {

+				this.mouseTrack = mt.setStyle(elStyle);

+			}

+			

+			if(n.x !== null && n.y !== null){

+				mt.show();

+				

+				this.clearHit();

+				if(n.mouse.lineColor != null){

+					octx.save();

+					octx.translate(plotOffset.left, plotOffset.top);

+					octx.lineWidth = options.points.lineWidth;

+					octx.strokeStyle = n.mouse.lineColor;

+					octx.fillStyle = '#ffffff';

+					octx.beginPath();

+					octx.arc(this.tHoz(n.x), this.tVert(n.y), options.mouse.radius, 0, 2 * Math.PI, true);

+					octx.fill();

+					octx.stroke();

+					octx.restore();

+				}

+				this.prevHit = n;

+				

+				var decimals = n.mouse.trackDecimals;

+				if(decimals == null || decimals < 0) decimals = 0;

+				

+				mt.innerHTML = n.mouse.trackFormatter({x: n.x.toFixed(decimals), y: n.y.toFixed(decimals), 

+												radarLabel: n.radarLabel, radarData: n.radarData.toFixed(decimals)});

+				mt.fire('flotr:hit', [n, this]);

+			}

+			else if(prevHit){

+				mt.hide();

+				this.clearHit();

+			}

+		}

+	},

+	saveImage: function (type, width, height, replaceCanvas) {

+		var image = null;

+	  switch (type) {

+	  	case 'jpeg':

+	    case 'jpg': image = Canvas2Image.saveAsJPEG(this.canvas, replaceCanvas, width, height); break;

+      default:

+      case 'png': image = Canvas2Image.saveAsPNG(this.canvas, replaceCanvas, width, height); break;

+      case 'bmp': image = Canvas2Image.saveAsBMP(this.canvas, replaceCanvas, width, height); break;

+	  }

+	  if (Object.isElement(image) && replaceCanvas) {

+	    this.restoreCanvas();

+	    this.canvas.hide();

+	    this.overlay.hide();

+	  	this.el.insert(image.setStyle({position: 'absolute'}));

+	  }

+	},

+	restoreCanvas: function() {

+    this.canvas.show();

+    this.overlay.show();

+    this.el.select('img').invoke('remove');

+	}

+});

+

+Flotr.Color = Class.create({

+	initialize: function(r, g, b, a){

+		this.rgba = ['r','g','b','a'];

+		var x = 4;

+		while(-1<--x){

+			this[this.rgba[x]] = arguments[x] || ((x==3) ? 1.0 : 0);

+		}

+		this.normalize();

+	},

+	

+	adjust: function(rd, gd, bd, ad) {

+		var x = 4;

+		while(-1<--x){

+			if(arguments[x] != null)

+				this[this.rgba[x]] += arguments[x];

+		}

+		return this.normalize();

+	},

+	

+	clone: function(){

+		return new Flotr.Color(this.r, this.b, this.g, this.a);

+	},

+	

+	limit: function(val,minVal,maxVal){

+		return Math.max(Math.min(val, maxVal), minVal);

+	},

+	

+	normalize: function(){

+		var limit = this.limit;

+		this.r = limit(parseInt(this.r), 0, 255);

+		this.g = limit(parseInt(this.g), 0, 255);

+		this.b = limit(parseInt(this.b), 0, 255);

+		this.a = limit(this.a, 0, 1);

+		return this;

+	},

+	

+	scale: function(rf, gf, bf, af){

+		var x = 4;

+		while(-1<--x){

+			if(arguments[x] != null)

+				this[this.rgba[x]] *= arguments[x];

+		}

+		return this.normalize();

+	},

+	

+	distance: function(color){

+		if (!color) return;

+		color = new Flotr.parseColor(color);

+	  var dist = 0;

+		var x = 3;

+		while(-1<--x){

+			dist += Math.abs(this[this.rgba[x]] - color[this.rgba[x]]);

+		}

+		return dist;

+	},

+	

+	toString: function(){

+		return (this.a >= 1.0) ? 'rgb('+[this.r,this.g,this.b].join(',')+')' : 'rgba('+[this.r,this.g,this.b,this.a].join(',')+')';

+	}

+});

+

+Flotr.Color.lookupColors = {

+	aqua:[0,255,255],

+	azure:[240,255,255],

+	beige:[245,245,220],

+	black:[0,0,0],

+	blue:[0,0,255],

+	brown:[165,42,42],

+	cyan:[0,255,255],

+	darkblue:[0,0,139],

+	darkcyan:[0,139,139],

+	darkgrey:[169,169,169],

+	darkgreen:[0,100,0],

+	darkkhaki:[189,183,107],

+	darkmagenta:[139,0,139],

+	darkolivegreen:[85,107,47],

+	darkorange:[255,140,0],

+	darkorchid:[153,50,204],

+	darkred:[139,0,0],

+	darksalmon:[233,150,122],

+	darkviolet:[148,0,211],

+	fuchsia:[255,0,255],

+	gold:[255,215,0],

+	green:[0,128,0],

+	indigo:[75,0,130],

+	khaki:[240,230,140],

+	lightblue:[173,216,230],

+	lightcyan:[224,255,255],

+	lightgreen:[144,238,144],

+	lightgrey:[211,211,211],

+	lightpink:[255,182,193],

+	lightyellow:[255,255,224],

+	lime:[0,255,0],

+	magenta:[255,0,255],

+	maroon:[128,0,0],

+	navy:[0,0,128],

+	olive:[128,128,0],

+	orange:[255,165,0],

+	pink:[255,192,203],

+	purple:[128,0,128],

+	violet:[128,0,128],

+	red:[255,0,0],

+	silver:[192,192,192],

+	white:[255,255,255],

+	yellow:[255,255,0]

+};

+

+// not used yet

+Flotr.Date = {

+  format: function(d, format) {

+		if (!d) return;

+

+    var leftPad = function(n) {

+      n = n.toString();

+      return n.length == 1 ? "0" + n : n;

+    };

+    

+    var r = [];

+    var escape = false;

+    

+    for (var i = 0; i < format.length; ++i) {

+      var c = format.charAt(i);

+      

+      if (escape) {

+        switch (c) {

+	        case 'h': c = d.getUTCHours().toString(); break;

+	        case 'H': c = leftPad(d.getUTCHours()); break;

+	        case 'M': c = leftPad(d.getUTCMinutes()); break;

+	        case 'S': c = leftPad(d.getUTCSeconds()); break;

+	        case 'd': c = d.getUTCDate().toString(); break;

+	        case 'm': c = (d.getUTCMonth() + 1).toString(); break;

+	        case 'y': c = d.getUTCFullYear().toString(); break;

+	        case 'b': c = Flotr.Date.monthNames[d.getUTCMonth()]; break;

+        }

+        r.push(c);

+        escape = false;

+      }

+      else {

+        if (c == "%")

+          escape = true;

+        else

+          r.push(c);

+      }

+    }

+    return r.join("");

+  },

+  timeUnits: {

+    "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

+  },

+  // the allowed tick sizes, after 1 year we use an integer algorithm

+  spec: [

+    [1, "second"], [2, "second"], [5, "second"], [10, "second"], [30, "second"], 

+    [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"], [30, "minute"], 

+    [1, "hour"], [2, "hour"], [4, "hour"], [8, "hour"], [12, "hour"],

+    [1, "day"], [2, "day"], [3, "day"],

+    [0.25, "month"], [0.5, "month"], [1, "month"], [2, "month"], [3, "month"], [6, "month"],

+    [1, "year"]

+  ],

+  monthNames: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]

+};

 

--- /dev/null
+++ b/js/flotr/lib/base64.js
@@ -1,1 +1,113 @@
-
+/* Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp>

+ * Version: 1.0

+ * LastModified: Dec 25 1999

+ * This library is free.  You can redistribute it and/or modify it.

+ */

+

+/*

+ * Interfaces:

+ * b64 = base64encode(data);

+ * data = base64decode(b64);

+ */

+

+(function() {

+

+var base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

+var base64DecodeChars = [

+    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,

+    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,

+    -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63,

+    52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1,

+    -1,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14,

+    15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1,

+    -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,

+    41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1];

+

+function base64encode(str) {

+    var out, i, len;

+    var c1, c2, c3;

+

+    len = str.length;

+    i = 0;

+    out = "";

+    while(i < len) {

+	c1 = str.charCodeAt(i++) & 0xff;

+	if(i == len)

+	{

+	    out += base64EncodeChars.charAt(c1 >> 2);

+	    out += base64EncodeChars.charAt((c1 & 0x3) << 4);

+	    out += "==";

+	    break;

+	}

+	c2 = str.charCodeAt(i++);

+	if(i == len)

+	{

+	    out += base64EncodeChars.charAt(c1 >> 2);

+	    out += base64EncodeChars.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));

+	    out += base64EncodeChars.charAt((c2 & 0xF) << 2);

+	    out += "=";

+	    break;

+	}

+	c3 = str.charCodeAt(i++);

+	out += base64EncodeChars.charAt(c1 >> 2);

+	out += base64EncodeChars.charAt(((c1 & 0x3)<< 4) | ((c2 & 0xF0) >> 4));

+	out += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >>6));

+	out += base64EncodeChars.charAt(c3 & 0x3F);

+    }

+    return out;

+}

+

+function base64decode(str) {

+    var c1, c2, c3, c4;

+    var i, len, out;

+

+    len = str.length;

+    i = 0;

+    out = "";

+    while(i < len) {

+	/* c1 */

+	do {

+	    c1 = base64DecodeChars[str.charCodeAt(i++) & 0xff];

+	} while(i < len && c1 == -1);

+	if(c1 == -1)

+	    break;

+

+	/* c2 */

+	do {

+	    c2 = base64DecodeChars[str.charCodeAt(i++) & 0xff];

+	} while(i < len && c2 == -1);

+	if(c2 == -1)

+	    break;

+

+	out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4));

+

+	/* c3 */

+	do {

+	    c3 = str.charCodeAt(i++) & 0xff;

+	    if(c3 == 61)

+		return out;

+	    c3 = base64DecodeChars[c3];

+	} while(i < len && c3 == -1);

+	if(c3 == -1)

+	    break;

+

+	out += String.fromCharCode(((c2 & 0XF) << 4) | ((c3 & 0x3C) >> 2));

+

+	/* c4 */

+	do {

+	    c4 = str.charCodeAt(i++) & 0xff;

+	    if(c4 == 61)

+		return out;

+	    c4 = base64DecodeChars[c4];

+	} while(i < len && c4 == -1);

+	if(c4 == -1)

+	    break;

+	out += String.fromCharCode(((c3 & 0x03) << 6) | c4);

+    }

+    return out;

+}

+

+if (!window.btoa) window.btoa = base64encode;

+if (!window.atob) window.atob = base64decode;

+

+})();

--- /dev/null
+++ b/js/flotr/lib/canvas2image.js
@@ -1,1 +1,230 @@
-
+/*

+ * Canvas2Image v0.1

+ * Copyright (c) 2008 Jacob Seidelin, cupboy@gmail.com

+ * MIT License [http://www.opensource.org/licenses/mit-license.php]

+ */

+

+var Canvas2Image = (function() {

+	// check if we have canvas support

+	var oCanvas = document.createElement("canvas");

+  

+	// no canvas, bail out.

+	if (!oCanvas.getContext) {

+		return {

+			saveAsBMP : function(){},

+			saveAsPNG : function(){},

+			saveAsJPEG : function(){}

+		}

+	}

+

+	var bHasImageData = !!(oCanvas.getContext("2d").getImageData);

+	var bHasDataURL = !!(oCanvas.toDataURL);

+	var bHasBase64 = !!(window.btoa);

+

+	var strDownloadMime = "image/octet-stream";

+

+	// ok, we're good

+	var readCanvasData = function(oCanvas) {

+		var iWidth = parseInt(oCanvas.width);

+		var iHeight = parseInt(oCanvas.height);

+		return oCanvas.getContext("2d").getImageData(0,0,iWidth,iHeight);

+	}

+

+	// base64 encodes either a string or an array of charcodes

+	var encodeData = function(data) {

+		var strData = "";

+		if (typeof data == "string") {

+			strData = data;

+		} else {

+			var aData = data;

+			for (var i = 0; i < aData.length; i++) {

+				strData += String.fromCharCode(aData[i]);

+			}

+		}

+		return btoa(strData);

+	}

+

+	// creates a base64 encoded string containing BMP data

+	// takes an imagedata object as argument

+	var createBMP = function(oData) {

+		var aHeader = [];

+	

+		var iWidth = oData.width;

+		var iHeight = oData.height;

+

+		aHeader.push(0x42); // magic 1

+		aHeader.push(0x4D); 

+	

+		var iFileSize = iWidth*iHeight*3 + 54; // total header size = 54 bytes

+		aHeader.push(iFileSize % 256); iFileSize = Math.floor(iFileSize / 256);

+		aHeader.push(iFileSize % 256); iFileSize = Math.floor(iFileSize / 256);

+		aHeader.push(iFileSize % 256); iFileSize = Math.floor(iFileSize / 256);

+		aHeader.push(iFileSize % 256);

+

+		aHeader.push(0); // reserved

+		aHeader.push(0);

+		aHeader.push(0); // reserved

+		aHeader.push(0);

+

+		aHeader.push(54); // data offset

+		aHeader.push(0);

+		aHeader.push(0);

+		aHeader.push(0);

+

+		var aInfoHeader = [];

+		aInfoHeader.push(40); // info header size

+		aInfoHeader.push(0);

+		aInfoHeader.push(0);

+		aInfoHeader.push(0);

+

+		var iImageWidth = iWidth;

+		aInfoHeader.push(iImageWidth % 256); iImageWidth = Math.floor(iImageWidth / 256);

+		aInfoHeader.push(iImageWidth % 256); iImageWidth = Math.floor(iImageWidth / 256);

+		aInfoHeader.push(iImageWidth % 256); iImageWidth = Math.floor(iImageWidth / 256);

+		aInfoHeader.push(iImageWidth % 256);

+	

+		var iImageHeight = iHeight;

+		aInfoHeader.push(iImageHeight % 256); iImageHeight = Math.floor(iImageHeight / 256);

+		aInfoHeader.push(iImageHeight % 256); iImageHeight = Math.floor(iImageHeight / 256);

+		aInfoHeader.push(iImageHeight % 256); iImageHeight = Math.floor(iImageHeight / 256);

+		aInfoHeader.push(iImageHeight % 256);

+	

+		aInfoHeader.push(1); // num of planes

+		aInfoHeader.push(0);

+	

+		aInfoHeader.push(24); // num of bits per pixel

+		aInfoHeader.push(0);

+	

+		aInfoHeader.push(0); // compression = none

+		aInfoHeader.push(0);

+		aInfoHeader.push(0);

+		aInfoHeader.push(0);

+	

+		var iDataSize = iWidth*iHeight*3; 

+		aInfoHeader.push(iDataSize % 256); iDataSize = Math.floor(iDataSize / 256);

+		aInfoHeader.push(iDataSize % 256); iDataSize = Math.floor(iDataSize / 256);

+		aInfoHeader.push(iDataSize % 256); iDataSize = Math.floor(iDataSize / 256);

+		aInfoHeader.push(iDataSize % 256); 

+	

+		for (var i = 0; i < 16; i++) {

+			aInfoHeader.push(0);	// these bytes not used

+		}

+	

+		var iPadding = (4 - ((iWidth * 3) % 4)) % 4;

+

+		var aImgData = oData.data;

+

+		var strPixelData = "";

+		var y = iHeight;

+		do {

+			var iOffsetY = iWidth*(y-1)*4;

+			var strPixelRow = "";

+			for (var x=0;x<iWidth;x++) {

+				var iOffsetX = 4*x;

+

+				strPixelRow += String.fromCharCode(aImgData[iOffsetY+iOffsetX+2]);

+				strPixelRow += String.fromCharCode(aImgData[iOffsetY+iOffsetX+1]);

+				strPixelRow += String.fromCharCode(aImgData[iOffsetY+iOffsetX]);

+			}

+			for (var c=0;c<iPadding;c++) {

+				strPixelRow += String.fromCharCode(0);

+			}

+			strPixelData += strPixelRow;

+		} while (--y);

+

+		return encodeData(aHeader.concat(aInfoHeader)) + encodeData(strPixelData);

+	}

+

+	// sends the generated file to the client

+	var saveFile = function(strData) {

+    if (!window.open(strData)) {

+      document.location.href = strData;

+    }

+	}

+

+	var makeDataURI = function(strData, strMime) {

+		return "data:" + strMime + ";base64," + strData;

+	}

+

+	// generates a <img> object containing the imagedata

+	var makeImageObject = function(strSource) {

+		var oImgElement = document.createElement("img");

+		oImgElement.src = strSource;

+		return oImgElement;

+	}

+

+	var scaleCanvas = function(oCanvas, iWidth, iHeight) {

+		if (iWidth && iHeight) {

+			var oSaveCanvas = document.createElement("canvas");

+			

+			oSaveCanvas.width = iWidth;

+			oSaveCanvas.height = iHeight;

+			oSaveCanvas.style.width = iWidth+"px";

+			oSaveCanvas.style.height = iHeight+"px";

+

+			var oSaveCtx = oSaveCanvas.getContext("2d");

+

+			oSaveCtx.drawImage(oCanvas, 0, 0, oCanvas.width, oCanvas.height, 0, 0, iWidth, iWidth);

+			

+			return oSaveCanvas;

+		}

+		return oCanvas;

+	}

+

+	return {

+		saveAsPNG : function(oCanvas, bReturnImg, iWidth, iHeight) {

+			if (!bHasDataURL) {

+				return false;

+			}

+			var oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight);

+			var strData = oScaledCanvas.toDataURL("image/png");

+			if (bReturnImg) {

+				return makeImageObject(strData);

+			} else {

+				saveFile(strData.replace("image/png", strDownloadMime));

+			}

+			return true;

+		},

+

+		saveAsJPEG : function(oCanvas, bReturnImg, iWidth, iHeight) {

+			if (!bHasDataURL) {

+				return false;

+			}

+

+			var oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight);

+			var strMime = "image/jpeg";

+			var strData = oScaledCanvas.toDataURL(strMime);

+	

+			// check if browser actually supports jpeg by looking for the mime type in the data uri.

+			// if not, return false

+			if (strData.indexOf(strMime) != 5) {

+				return false;

+			}

+

+			if (bReturnImg) {

+				return makeImageObject(strData);

+			} else {

+				saveFile(strData.replace(strMime, strDownloadMime));

+			}

+			return true;

+		},

+

+		saveAsBMP : function(oCanvas, bReturnImg, iWidth, iHeight) {

+			if (!(bHasImageData && bHasBase64)) {

+				return false;

+			}

+

+			var oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight);

+

+			var oData = readCanvasData(oScaledCanvas);

+			var strImgData = createBMP(oData);

+			if (bReturnImg) {

+				return makeImageObject(makeDataURI(strImgData, "image/bmp"));

+			} else {

+				saveFile(makeDataURI(strImgData, strDownloadMime));

+			}

+			return true;

+		}

+	};

+

+})();

--- /dev/null
+++ b/js/flotr/lib/canvastext.js
@@ -1,1 +1,397 @@
-
+/**

+ * This code is released to the public domain by Jim Studt, 2007.

+ * He may keep some sort of up to date copy at http://www.federated.com/~jim/canvastext/


+ * A partial support for accentuated letters as been added too.

+ */

+var CanvasText = {

+	/** The letters definition. It is a list of letters, 

+	 * with their width, and the coordinates of points compositing them.

+	 * The syntax for the points is : [x, y], null value means "pen up"

+	 */

+  letters: {

+		'\n':{ width: -1, points: [] },

+    ' ': { width: 10, points: [] },

+    '!': { width: 10, points: [[5,21],[5,7],null,[5,2],[4,1],[5,0],[6,1],[5,2]] },

+    '"': { width: 16, points: [[4,21],[4,14],null,[12,21],[12,14]] },

+    '#': { width: 21, points: [[11,25],[4,-7],null,[17,25],[10,-7],null,[4,12],[18,12],null,[3,6],[17,6]] },

+    '$': { width: 20, points: [[8,25],[8,-4],null,[12,25],[12,-4],null,[17,18],[15,20],[12,21],[8,21],[5,20],[3,18],[3,16],[4,14],[5,13],[7,12],[13,10],[15,9],[16,8],[17,6],[17,3],[15,1],[12,0],[8,0],[5,1],[3,3]] },

+    '%': { width: 24, points: [[21,21],[3,0],null,[8,21],[10,19],[10,17],[9,15],[7,14],[5,14],[3,16],[3,18],[4,20],[6,21],[8,21],null,[17,7],[15,6],[14,4],[14,2],[16,0],[18,0],[20,1],[21,3],[21,5],[19,7],[17,7]] },

+    '&': { width: 26, points: [[23,12],[23,13],[22,14],[21,14],[20,13],[19,11],[17,6],[15,3],[13,1],[11,0],[7,0],[5,1],[4,2],[3,4],[3,6],[4,8],[5,9],[12,13],[13,14],[14,16],[14,18],[13,20],[11,21],[9,20],[8,18],[8,16],[9,13],[11,10],[16,3],[18,1],[20,0],[22,0],[23,1],[23,2]] },

+    '\'':{ width: 10, points: [[5,19],[4,20],[5,21],[6,20],[6,18],[5,16],[4,15]] },

+    '(': { width: 14, points: [[11,25],[9,23],[7,20],[5,16],[4,11],[4,7],[5,2],[7,-2],[9,-5],[11,-7]] },

+    ')': { width: 14, points: [[3,25],[5,23],[7,20],[9,16],[10,11],[10,7],[9,2],[7,-2],[5,-5],[3,-7]] },

+    '*': { width: 16, points: [[8,21],[8,9],null,[3,18],[13,12],null,[13,18],[3,12]] },

+    '+': { width: 26, points: [[13,18],[13,0],null,[4,9],[22,9]] },

+    ',': { width: 10, points: [[6,1],[5,0],[4,1],[5,2],[6,1],[6,-1],[5,-3],[4,-4]] },

+    '-': { width: 26, points: [[4,9],[22,9]] },

+    '.': { width: 10, points: [[5,2],[4,1],[5,0],[6,1],[5,2]] },

+    '/': { width: 22, points: [[20,25],[2,-7]] },

+    '0': { width: 20, points: [[9,21],[6,20],[4,17],[3,12],[3,9],[4,4],[6,1],[9,0],[11,0],[14,1],[16,4],[17,9],[17,12],[16,17],[14,20],[11,21],[9,21]] },

+    '1': { width: 20, points: [[6,17],[8,18],[11,21],[11,0]] },

+    '2': { width: 20, points: [[4,16],[4,17],[5,19],[6,20],[8,21],[12,21],[14,20],[15,19],[16,17],[16,15],[15,13],[13,10],[3,0],[17,0]] },

+    '3': { width: 20, points: [[5,21],[16,21],[10,13],[13,13],[15,12],[16,11],[17,8],[17,6],[16,3],[14,1],[11,0],[8,0],[5,1],[4,2],[3,4]] },

+    '4': { width: 20, points: [[13,21],[3,7],[18,7],null,[13,21],[13,0]] },

+    '5': { width: 20, points: [[15,21],[5,21],[4,12],[5,13],[8,14],[11,14],[14,13],[16,11],[17,8],[17,6],[16,3],[14,1],[11,0],[8,0],[5,1],[4,2],[3,4]] },

+    '6': { width: 20, points: [[16,18],[15,20],[12,21],[10,21],[7,20],[5,17],[4,12],[4,7],[5,3],[7,1],[10,0],[11,0],[14,1],[16,3],[17,6],[17,7],[16,10],[14,12],[11,13],[10,13],[7,12],[5,10],[4,7]] },

+    '7': { width: 20, points: [[17,21],[7,0],null,[3,21],[17,21]] },

+    '8': { width: 20, points: [[8,21],[5,20],[4,18],[4,16],[5,14],[7,13],[11,12],[14,11],[16,9],[17,7],[17,4],[16,2],[15,1],[12,0],[8,0],[5,1],[4,2],[3,4],[3,7],[4,9],[6,11],[9,12],[13,13],[15,14],[16,16],[16,18],[15,20],[12,21],[8,21]] },

+    '9': { width: 20, points: [[16,14],[15,11],[13,9],[10,8],[9,8],[6,9],[4,11],[3,14],[3,15],[4,18],[6,20],[9,21],[10,21],[13,20],[15,18],[16,14],[16,9],[15,4],[13,1],[10,0],[8,0],[5,1],[4,3]] },

+    ':': { width: 10, points: [[5,14],[4,13],[5,12],[6,13],[5,14],null,[5,2],[4,1],[5,0],[6,1],[5,2]] },

+    ';': { width: 10, points: [[5,14],[4,13],[5,12],[6,13],[5,14],null,[6,1],[5,0],[4,1],[5,2],[6,1],[6,-1],[5,-3],[4,-4]] },

+    '<': { width: 24, points: [[20,18],[4,9],[20,0]] },

+    '=': { width: 26, points: [[4,12],[22,12],null,[4,6],[22,6]] },

+    '>': { width: 24, points: [[4,18],[20,9],[4,0]] },

+    '?': { width: 18, points: [[3,16],[3,17],[4,19],[5,20],[7,21],[11,21],[13,20],[14,19],[15,17],[15,15],[14,13],[13,12],[9,10],[9,7],null,[9,2],[8,1],[9,0],[10,1],[9,2]] },

+    '@': { width: 27, points: [[18,13],[17,15],[15,16],[12,16],[10,15],[9,14],[8,11],[8,8],[9,6],[11,5],[14,5],[16,6],[17,8],null,[12,16],[10,14],[9,11],[9,8],[10,6],[11,5],null,[18,16],[17,8],[17,6],[19,5],[21,5],[23,7],[24,10],[24,12],[23,15],[22,17],[20,19],[18,20],[15,21],[12,21],[9,20],[7,19],[5,17],[4,15],[3,12],[3,9],[4,6],[5,4],[7,2],[9,1],[12,0],[15,0],[18,1],[20,2],[21,3],null,[19,16],[18,8],[18,6],[19,5]] },

+    'A': { width: 18, points: [[9,21],[1,0],null,[9,21],[17,0],null,[4,7],[14,7]] },

+    'B': { width: 21, points: [[4,21],[4,0],null,[4,21],[13,21],[16,20],[17,19],[18,17],[18,15],[17,13],[16,12],[13,11],null,[4,11],[13,11],[16,10],[17,9],[18,7],[18,4],[17,2],[16,1],[13,0],[4,0]] },

+    'C': { width: 21, points: [[18,16],[17,18],[15,20],[13,21],[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5]] },

+    'D': { width: 21, points: [[4,21],[4,0],null,[4,21],[11,21],[14,20],[16,18],[17,16],[18,13],[18,8],[17,5],[16,3],[14,1],[11,0],[4,0]] },

+    'E': { width: 19, points: [[4,21],[4,0],null,[4,21],[17,21],null,[4,11],[12,11],null,[4,0],[17,0]] },

+    'F': { width: 18, points: [[4,21],[4,0],null,[4,21],[17,21],null,[4,11],[12,11]] },

+    'G': { width: 21, points: [[18,16],[17,18],[15,20],[13,21],[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[18,8],null,[13,8],[18,8]] },

+    'H': { width: 22, points: [[4,21],[4,0],null,[18,21],[18,0],null,[4,11],[18,11]] },

+    'I': { width: 8,  points: [[4,21],[4,0]] },

+    'J': { width: 16, points: [[12,21],[12,5],[11,2],[10,1],[8,0],[6,0],[4,1],[3,2],[2,5],[2,7]] },

+    'K': { width: 21, points: [[4,21],[4,0],null,[18,21],[4,7],null,[9,12],[18,0]] },

+    'L': { width: 17, points: [[4,21],[4,0],null,[4,0],[16,0]] },

+    'M': { width: 24, points: [[4,21],[4,0],null,[4,21],[12,0],null,[20,21],[12,0],null,[20,21],[20,0]] },

+    'N': { width: 22, points: [[4,21],[4,0],null,[4,21],[18,0],null,[18,21],[18,0]] },

+    'O': { width: 22, points: [[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[19,8],[19,13],[18,16],[17,18],[15,20],[13,21],[9,21]] },

+    'P': { width: 21, points: [[4,21],[4,0],null,[4,21],[13,21],[16,20],[17,19],[18,17],[18,14],[17,12],[16,11],[13,10],[4,10]] },

+    'Q': { width: 22, points: [[9,21],[7,20],[5,18],[4,16],[3,13],[3,8],[4,5],[5,3],[7,1],[9,0],[13,0],[15,1],[17,3],[18,5],[19,8],[19,13],[18,16],[17,18],[15,20],[13,21],[9,21],null,[12,4],[18,-2]] },

+    'R': { width: 21, points: [[4,21],[4,0],null,[4,21],[13,21],[16,20],[17,19],[18,17],[18,15],[17,13],[16,12],[13,11],[4,11],null,[11,11],[18,0]] },

+    'S': { width: 20, points: [[17,18],[15,20],[12,21],[8,21],[5,20],[3,18],[3,16],[4,14],[5,13],[7,12],[13,10],[15,9],[16,8],[17,6],[17,3],[15,1],[12,0],[8,0],[5,1],[3,3]] },

+    'T': { width: 16, points: [[8,21],[8,0],null,[1,21],[15,21]] },

+    'U': { width: 22, points: [[4,21],[4,6],[5,3],[7,1],[10,0],[12,0],[15,1],[17,3],[18,6],[18,21]] },

+    'V': { width: 18, points: [[1,21],[9,0],null,[17,21],[9,0]] },

+    'W': { width: 24, points: [[2,21],[7,0],null,[12,21],[7,0],null,[12,21],[17,0],null,[22,21],[17,0]] },

+    'X': { width: 20, points: [[3,21],[17,0],null,[17,21],[3,0]] },

+    'Y': { width: 18, points: [[1,21],[9,11],[9,0],null,[17,21],[9,11]] },

+    'Z': { width: 20, points: [[17,21],[3,0],null,[3,21],[17,21],null,[3,0],[17,0]] },

+    '[': { width: 14, points: [[4,25],[4,-7],null,[5,25],[5,-7],null,[4,25],[11,25],null,[4,-7],[11,-7]] },

+    '\\':{ width: 14, points: [[0,21],[14,-3]] },

+    ']': { width: 14, points: [[9,25],[9,-7],null,[10,25],[10,-7],null,[3,25],[10,25],null,[3,-7],[10,-7]] },

+    '^': { width: 14, points: [[3,10],[8,18],[13,10]] },

+    '_': { width: 16, points: [[0,-2],[16,-2]] },

+    '`': { width: 10, points: [[6,21],[5,20],[4,18],[4,16],[5,15],[6,16],[5,17]] },

+    'a': { width: 19, points: [[15,14],[15,0],null,[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },

+    'b': { width: 19, points: [[4,21],[4,0],null,[4,11],[6,13],[8,14],[11,14],[13,13],[15,11],[16,8],[16,6],[15,3],[13,1],[11,0],[8,0],[6,1],[4,3]] },

+    'c': { width: 18, points: [[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },

+    'd': { width: 19, points: [[15,21],[15,0],null,[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },

+    'e': { width: 18, points: [[3,8],[15,8],[15,10],[14,12],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },

+    'f': { width: 12, points: [[10,21],[8,21],[6,20],[5,17],[5,0],null,[2,14],[9,14]] },

+    'g': { width: 19, points: [[15,14],[15,-2],[14,-5],[13,-6],[11,-7],[8,-7],[6,-6],null,[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },

+    'h': { width: 19, points: [[4,21],[4,0],null,[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0]] },

+    'i': { width: 8,  points: [[3,21],[4,20],[5,21],[4,22],[3,21],null,[4,14],[4,0]] },

+    'j': { width: 10, points: [[5,21],[6,20],[7,21],[6,22],[5,21],null,[6,14],[6,-3],[5,-6],[3,-7],[1,-7]] },

+    'k': { width: 17, points: [[4,21],[4,0],null,[14,14],[4,4],null,[8,8],[15,0]] },

+    'l': { width: 8,  points: [[4,21],[4,0]] },

+    'm': { width: 30, points: [[4,14],[4,0],null,[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0],null,[15,10],[18,13],[20,14],[23,14],[25,13],[26,10],[26,0]] },

+    'n': { width: 19, points: [[4,14],[4,0],null,[4,10],[7,13],[9,14],[12,14],[14,13],[15,10],[15,0]] },

+    'o': { width: 19, points: [[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3],[16,6],[16,8],[15,11],[13,13],[11,14],[8,14]] },

+    'p': { width: 19, points: [[4,14],[4,-7],null,[4,11],[6,13],[8,14],[11,14],[13,13],[15,11],[16,8],[16,6],[15,3],[13,1],[11,0],[8,0],[6,1],[4,3]] },

+    'q': { width: 19, points: [[15,14],[15,-7],null,[15,11],[13,13],[11,14],[8,14],[6,13],[4,11],[3,8],[3,6],[4,3],[6,1],[8,0],[11,0],[13,1],[15,3]] },

+    'r': { width: 13, points: [[4,14],[4,0],null,[4,8],[5,11],[7,13],[9,14],[12,14]] },

+    's': { width: 17, points: [[14,11],[13,13],[10,14],[7,14],[4,13],[3,11],[4,9],[6,8],[11,7],[13,6],[14,4],[14,3],[13,1],[10,0],[7,0],[4,1],[3,3]] },

+    't': { width: 12, points: [[5,21],[5,4],[6,1],[8,0],[10,0],null,[2,14],[9,14]] },

+    'u': { width: 19, points: [[4,14],[4,4],[5,1],[7,0],[10,0],[12,1],[15,4],null,[15,14],[15,0]] },

+    'v': { width: 16, points: [[2,14],[8,0],null,[14,14],[8,0]] },

+    'w': { width: 22, points: [[3,14],[7,0],null,[11,14],[7,0],null,[11,14],[15,0],null,[19,14],[15,0]] },

+    'x': { width: 17, points: [[3,14],[14,0],null,[14,14],[3,0]] },

+    'y': { width: 16, points: [[2,14],[8,0],null,[14,14],[8,0],[6,-4],[4,-6],[2,-7],[1,-7]] },

+    'z': { width: 17, points: [[14,14],[3,0],null,[3,14],[14,14],null,[3,0],[14,0]] },

+    '{': { width: 14, points: [[9,25],[7,24],[6,23],[5,21],[5,19],[6,17],[7,16],[8,14],[8,12],[6,10],null,[7,24],[6,22],[6,20],[7,18],[8,17],[9,15],[9,13],[8,11],[4,9],[8,7],[9,5],[9,3],[8,1],[7,0],[6,-2],[6,-4],[7,-6],null,[6,8],[8,6],[8,4],[7,2],[6,1],[5,-1],[5,-3],[6,-5],[7,-6],[9,-7]] },

+    '|': { width: 8,  points: [[4,25],[4,-7]] },

+    '}': { width: 14, points: [[5,25],[7,24],[8,23],[9,21],[9,19],[8,17],[7,16],[6,14],[6,12],[8,10],null,[7,24],[8,22],[8,20],[7,18],[6,17],[5,15],[5,13],[6,11],[10,9],[6,7],[5,5],[5,3],[6,1],[7,0],[8,-2],[8,-4],[7,-6],null,[8,8],[6,6],[6,4],[7,2],[8,1],[9,-1],[9,-3],[8,-5],[7,-6],[5,-7]] },

+    '~': { width: 24, points: [[3,6],[3,8],[4,11],[6,12],[8,12],[10,11],[14,8],[16,7],[18,7],[20,8],[21,10],null,[3,8],[4,10],[6,11],[8,11],[10,10],[14,7],[16,6],[18,6],[20,7],[21,10],[21,12]] },

















+  },

+  

+  specialchars: {

+  	'pi': { width: 19, points: [[6,14],[6,0],null,[14,14],[14,0],null,[2,13],[6,16],[13,13],[17,16]] }

+  },

+  

+  /** Diacritics, used to draw accentuated letters */

+  diacritics: {



+    '`': { entity: 'grave', points: [[7,22],[12,19]] },

+    '^': { entity: 'circ',  points: [[5.5,19],[9.5,23],[12.5,19]] },


+    '~': { entity: 'tilde', points: [[4,18],[7,22],[10,18],[13,22]] }

+  },

+  

+  /** The default font styling */

+  style: {

+    size: 8,          // font height in pixels

+    font: null,       // not yet implemented

+    color: '#000000', // 

+    weight: 1,        // float, 1 for 'normal'

+    halign: 'l',      // l: left, r: right, c: center

+    valign: 'b',      // t: top, m: middle, b: bottom 

+    adjustAlign: false, // modifies the alignments if the angle is different from 0 to make the spin point always at the good position

+    angle: 0,         // in radians, anticlockwise

+    tracking: 1,      // space between the letters, float, 1 for 'normal'

+    boundingBoxColor: '#ff0000', //null // color of the bounding box (null to hide), can be used for debug and font drawing

+    originPointColor: '#000000' //null // color of the bounding box (null to hide), can be used for debug and font drawing

+  },

+  

+  debug: false,

+  _bufferLexemes: {},

+  

+  /** Get the letter data corresponding to a char

+   * @param {String} ch - The char

+   */

+  letter: function(ch) {

+    return CanvasText.letters[ch];

+  },

+  

+  parseLexemes: function(str) {

+    if (CanvasText._bufferLexemes[str]) 

+      return CanvasText._bufferLexemes[str];

+    

+  	var i, c, matches = str.match(/&[A-Za-z]{2,5};|\s|./g);

+  	var result = [], chars = [];

+  	for (i = 0; i < matches.length; i++) {

+  		c = matches[i];

+  		if (c.length == 1) 

+  			chars.push(c);

+  		else {

+  			var entity = c.substring(1, c.length-1);

+  			if (CanvasText.specialchars[entity]) 

+  				chars.push(entity);

+  			else

+  				chars = chars.concat(c.toArray());

+  		}

+  	}

+  	for (i = 0; i < chars.length; i++) {

+  		c = chars[i];

+  		if (c = CanvasText.letters[c] || CanvasText.specialchars[c])

+  		  result.push(c);

+  	}

+  	return CanvasText._bufferLexemes[str] = result.compact();

+  },

+

+  /** Get the font ascent for a given style

+   * @param {Object} style - The reference style

+   */

+  ascent: function(style) {

+  	style = style || {};

+    return (style.size || CanvasText.style.size);

+  },

+  

+  /** Get the font descent for a given style 

+   * @param {Object} style - The reference style

+   * */

+  descent: function(style) {

+  	style = style || {};

+    return 7.0*(style.size || CanvasText.style.size)/25.0;

+  },

+  

+  /** Measure the text horizontal size 

+   * @param {String} str - The text

+   * @param {Object} style - Text style

+   * */

+  measure: function(str, style) {

+    if (!str) return;

+    style = style || {};

+    

+    var i, width, lexemes = CanvasText.parseLexemes(str),

+        total = 0;

+

+    for (i = lexemes.length-1; i > -1; --i) {

+    	c = lexemes[i];

+    	width = (c.diacritic) ? CanvasText.letter(c.letter).width : c.width;

+      total += width * (style.tracking || CanvasText.style.tracking) * (style.size || CanvasText.style.size) / 25.0;

+    }

+    return total;

+  },

+  

+  getDimensions: function(str, style) {

+    var width = CanvasText.measure(str, style),

+        height = style.size || CanvasText.style.size,

+        angle = style.angle || CanvasText.style.angle;

+

+    if (style.angle == 0) return {width: width, height: height};

+    return {

+      width:  Math.abs(Math.cos(angle) * width) + Math.abs(Math.sin(angle) * height),

+      height: Math.abs(Math.sin(angle) * width) + Math.abs(Math.cos(angle) * height)

+    }

+  },

+  

+  getBestAlign: function(angle, style) {

+    angle += CanvasText.getAngleFromAlign(style.halign, style.valign);

+    var a = {h:'c', v:'m'};

+    if (Math.round(Math.cos(angle)*1000)/1000 != 0) 

+      a.h = (Math.cos(angle) > 0 ? 'r' : 'l');

+    

+    if (Math.round(Math.sin(angle)*1000)/1000 != 0) 

+      a.v = (Math.sin(angle) > 0 ? 't' : 'b');

+    return a;

+  },

+  

+  getAngleFromAlign: function(halign, valign) {

+    var pi = Math.PI, table = {

+      'rm': 0,

+      'rt': pi/4,

+      'ct': pi/2,

+      'lt': 3*(pi/4),

+      'lm': pi,

+      'lb': -3*(pi/4),

+      'cb': -pi/2,

+      'rb': -pi/4,

+      'cm': 0

+    }

+    return table[halign+valign];

+  },

+  

+  /** Draws serie of points at given coordinates 

+   * @param {Canvas context} ctx - The canvas context

+   * @param {Array} points - The points to draw

+   * @param {Number} x - The X coordinate

+   * @param {Number} y - The Y coordinate

+   * @param {Number} mag - The scale 

+   */

+  drawPoints: function (ctx, points, x, y, mag, offset) {

+    var i, a, penUp = true, needStroke = 0;

+    offset = offset || {x:0, y:0};

+    

+    ctx.beginPath();

+    for (i = 0; i < points.length; i++) {

+      a = points[i];

+      if (!a) {

+        penUp = true;

+        continue;

+      }

+      if (penUp) {

+        ctx.moveTo(x + a[0]*mag + offset.x, y - a[1]*mag + offset.y);

+        penUp = false;

+      }

+      else {

+        ctx.lineTo(x + a[0]*mag + offset.x, y - a[1]*mag + offset.y);

+      }

+    }

+    ctx.stroke();

+  },

+  

+  /** Draws a text at given coordinates and with a given style

+   * @param {Canvas context} ctx - The canvas context

+   * @param {String} str - The text to draw

+   * @param {Number} xOrig - The X coordinate

+   * @param {Number} yOrig - The Y coordinate

+   * @param {Object} style - The font style

+   */

+  draw: function(ctx, str, xOrig, yOrig, style) {

+    if (!str) return;

+    style = style || CanvasText.style;

+    style.halign = style.halign || CanvasText.style.halign;

+    style.valign = style.valign || CanvasText.style.valign;

+    style.angle = style.angle || CanvasText.style.angle;

+    style.size = style.size || CanvasText.style.size;

+    style.adjustAlign = style.adjustAlign || CanvasText.style.adjustAlign;

+    

+    var i, c, total = 0,

+        mag = style.size / 25.0,

+        x = 0, y = 0,

+        lexemes = CanvasText.parseLexemes(str);

+    

+    var offset = {x:0, y:0}, 

+        measure = CanvasText.measure(str, style),

+        align;

+        

+    if (style.adjustAlign) {

+      align = CanvasText.getBestAlign(style.angle, style);

+      style.halign = align.h;

+      style.valign = align.v;

+    }

+        

+    switch (style.halign) {

+      case 'l': break;

+      case 'c': offset.x = -measure / 2; break;

+      case 'r': offset.x = -measure; break;

+    }

+    

+    switch (style.valign) {

+      case 'b': break;

+      case 'm': offset.y = style.size / 2; break;

+      case 't': offset.y = style.size; break;

+    }

+    

+    ctx.save();

+    ctx.translate(xOrig, yOrig);

+    ctx.rotate(style.angle);

+    ctx.lineCap = "round";

+    ctx.lineWidth = 2.0 * mag * (style.weight || CanvasText.style.weight);

+    ctx.strokeStyle = style.color || CanvasText.style.color;

+    

+    for (i = 0; i < lexemes.length; i++) {

+    	c = lexemes[i];

+      if (c.width == -1) {

+        x = 0;

+        y = style.size * 1.4;

+        continue;

+      }

+    

+      var points = c.points,

+          width = c.width;

+          

+      if (c.diacritic) {

+        var dia = CanvasText.diacritics[c.diacritic];

+        var char = CanvasText.letter(c.letter);

+

+        CanvasText.drawPoints(ctx, dia.points, x, y - (c.letter.toUpperCase() == c.letter ? 3 : 0), mag, offset);

+        points = char.points;

+        width = char.width;

+      }

+

+      CanvasText.drawPoints(ctx, points, x, y, mag, offset);

+      

+      if (CanvasText.debug) {

+      	ctx.save();

+        ctx.lineJoin = "miter";

+        ctx.lineWidth = 0.5;

+        ctx.strokeStyle = (style.boundingBoxColor || CanvasText.style.boundingBoxColor);

+      	ctx.strokeRect(x+offset.x, y+offset.y, width*mag, -style.size);

+        

+        ctx.fillStyle = (style.originPointColor || CanvasText.style.originPointColor);

+        ctx.beginPath();

+        ctx.arc(0, 0, 1.5, 0, Math.PI*2, true);

+        ctx.fill();

+        

+      	ctx.restore();

+      }

+      

+      x += width*mag*(style.tracking || CanvasText.style.tracking);

+    }

+    ctx.restore();

+    return total;

+  },

+  

+  /** Enables the text function for a Canvas context

+   * @param {Canvas context} ctx - The canvas context

+   */

+  enable: function(ctx) {

+    ctx.drawText    = function(text, x, y, style) { return CanvasText.draw(ctx, text, x, y, style); };

+    ctx.measureText = function(text, style) { return CanvasText.measure(text, style); };

+    ctx.getTextBounds = function(text, style) { return CanvasText.getDimensions(text, style); };

+    ctx.fontAscent  = function(style) { return CanvasText.ascent(style); };

+    ctx.fontDescent = function(style) { return CanvasText.descent(style); };

+  }

+};

--- /dev/null
+++ b/js/flotr/lib/excanvas.js
@@ -1,1 +1,885 @@
-
+// Copyright 2006 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+
+// Known Issues:
+//
+// * Patterns are not implemented.
+// * Radial gradient are not implemented. The VML version of these look very
+//   different from the canvas one.
+// * Clipping paths are not implemented.
+// * Coordsize. The width and height attribute have higher priority than the
+//   width and height style values which isn't correct.
+// * Painting mode isn't implemented.
+// * Canvas width/height should is using content-box by default. IE in
+//   Quirks mode will draw the canvas using border-box. Either change your
+//   doctype to HTML5
+//   (http://www.whatwg.org/specs/web-apps/current-work/#the-doctype)
+//   or use Box Sizing Behavior from WebFX
+//   (http://webfx.eae.net/dhtml/boxsizing/boxsizing.html)
+// * Non uniform scaling does not correctly scale strokes.
+// * Optimize. There is always room for speed improvements.
+
+// Only add this code if we do not already have a canvas implementation
+if (!document.createElement('canvas').getContext) {
+
+(function() {
+
+  // alias some functions to make (compiled) code shorter
+  var m = Math;
+  var mr = m.round;
+  var ms = m.sin;
+  var mc = m.cos;
+  var abs = m.abs;
+  var sqrt = m.sqrt;
+
+  // this is used for sub pixel precision
+  var Z = 10;
+  var Z2 = Z / 2;
+
+  /**
+   * This funtion is assigned to the <canvas> elements as element.getContext().
+   * @this {HTMLElement}
+   * @return {CanvasRenderingContext2D_}
+   */
+  function getContext() {
+    return this.context_ ||
+        (this.context_ = new CanvasRenderingContext2D_(this));
+  }
+
+  var slice = Array.prototype.slice;
+
+  /**
+   * Binds a function to an object. The returned function will always use the
+   * passed in {@code obj} as {@code this}.
+   *
+   * Example:
+   *
+   *   g = bind(f, obj, a, b)
+   *   g(c, d) // will do f.call(obj, a, b, c, d)
+   *
+   * @param {Function} f The function to bind the object to
+   * @param {Object} obj The object that should act as this when the function
+   *     is called
+   * @param {*} var_args Rest arguments that will be used as the initial
+   *     arguments when the function is called
+   * @return {Function} A new function that has bound this
+   */
+  function bind(f, obj, var_args) {
+    var a = slice.call(arguments, 2);
+    return function() {
+      return f.apply(obj, a.concat(slice.call(arguments)));
+    };
+  }
+
+  var G_vmlCanvasManager_ = {
+    init: function(opt_doc) {
+      if (/MSIE/.test(navigator.userAgent) && !window.opera) {
+        var doc = opt_doc || document;
+        // Create a dummy element so that IE will allow canvas elements to be
+        // recognized.
+        doc.createElement('canvas');
+        doc.attachEvent('onreadystatechange', bind(this.init_, this, doc));
+      }
+    },
+
+    init_: function(doc) {
+      // create xmlns
+      if (!doc.namespaces['g_vml_']) {
+        doc.namespaces.add('g_vml_', 'urn:schemas-microsoft-com:vml',
+                           '#default#VML');
+
+      }
+      if (!doc.namespaces['g_o_']) {
+        doc.namespaces.add('g_o_', 'urn:schemas-microsoft-com:office:office',
+                           '#default#VML');
+      }
+
+      // Setup default CSS.  Only add one style sheet per document
+      if (!doc.styleSheets['ex_canvas_']) {
+        var ss = doc.createStyleSheet();
+        ss.owningElement.id = 'ex_canvas_';
+        ss.cssText = 'canvas{display:inline-block;overflow:hidden;' +
+            // default size is 300x150 in Gecko and Opera
+            'text-align:left;width:300px;height:150px}' +
+            'g_vml_\\:*{behavior:url(#default#VML)}' +
+            'g_o_\\:*{behavior:url(#default#VML)}';
+
+      }
+
+      // find all canvas elements
+      var els = doc.getElementsByTagName('canvas');
+      for (var i = 0; i < els.length; i++) {
+        this.initElement(els[i]);
+      }
+    },
+
+    /**
+     * Public initializes a canvas element so that it can be used as canvas
+     * element from now on. This is called automatically before the page is
+     * loaded but if you are creating elements using createElement you need to
+     * make sure this is called on the element.
+     * @param {HTMLElement} el The canvas element to initialize.
+     * @return {HTMLElement} the element that was created.
+     */
+    initElement: function(el) {
+      if (!el.getContext) {
+
+        el.getContext = getContext;
+
+        // Remove fallback content. There is no way to hide text nodes so we
+        // just remove all childNodes. We could hide all elements and remove
+        // text nodes but who really cares about the fallback content.
+        el.innerHTML = '';
+
+        // do not use inline function because that will leak memory
+        el.attachEvent('onpropertychange', onPropertyChange);
+        el.attachEvent('onresize', onResize);
+
+        var attrs = el.attributes;
+        if (attrs.width && attrs.width.specified) {
+          // TODO: use runtimeStyle and coordsize
+          // el.getContext().setWidth_(attrs.width.nodeValue);
+          el.style.width = attrs.width.nodeValue + 'px';
+        } else {
+          el.width = el.clientWidth;
+        }
+        if (attrs.height && attrs.height.specified) {
+          // TODO: use runtimeStyle and coordsize
+          // el.getContext().setHeight_(attrs.height.nodeValue);
+          el.style.height = attrs.height.nodeValue + 'px';
+        } else {
+          el.height = el.clientHeight;
+        }
+        //el.getContext().setCoordsize_()
+      }
+      return el;
+    }
+  };
+
+  function onPropertyChange(e) {
+    var el = e.srcElement;
+
+    switch (e.propertyName) {
+      case 'width':
+        el.style.width = el.attributes.width.nodeValue + 'px';
+        el.getContext().clearRect();
+        break;
+      case 'height':
+        el.style.height = el.attributes.height.nodeValue + 'px';
+        el.getContext().clearRect();
+        break;
+    }
+  }
+
+  function onResize(e) {
+    var el = e.srcElement;
+    if (el.firstChild) {
+      el.firstChild.style.width =  el.clientWidth + 'px';
+      el.firstChild.style.height = el.clientHeight + 'px';
+    }
+  }
+
+  G_vmlCanvasManager_.init();
+
+  // precompute "00" to "FF"
+  var dec2hex = [];
+  for (var i = 0; i < 16; i++) {
+    for (var j = 0; j < 16; j++) {
+      dec2hex[i * 16 + j] = i.toString(16) + j.toString(16);
+    }
+  }
+
+  function createMatrixIdentity() {
+    return [
+      [1, 0, 0],
+      [0, 1, 0],
+      [0, 0, 1]
+    ];
+  }
+
+  function matrixMultiply(m1, m2) {
+    var result = createMatrixIdentity();
+
+    for (var x = 0; x < 3; x++) {
+      for (var y = 0; y < 3; y++) {
+        var sum = 0;
+
+        for (var z = 0; z < 3; z++) {
+          sum += m1[x][z] * m2[z][y];
+        }
+
+        result[x][y] = sum;
+      }
+    }
+    return result;
+  }
+
+  function copyState(o1, o2) {
+    o2.fillStyle     = o1.fillStyle;
+    o2.lineCap       = o1.lineCap;
+    o2.lineJoin      = o1.lineJoin;
+    o2.lineWidth     = o1.lineWidth;
+    o2.miterLimit    = o1.miterLimit;
+    o2.shadowBlur    = o1.shadowBlur;
+    o2.shadowColor   = o1.shadowColor;
+    o2.shadowOffsetX = o1.shadowOffsetX;
+    o2.shadowOffsetY = o1.shadowOffsetY;
+    o2.strokeStyle   = o1.strokeStyle;
+    o2.globalAlpha   = o1.globalAlpha;
+    o2.arcScaleX_    = o1.arcScaleX_;
+    o2.arcScaleY_    = o1.arcScaleY_;
+    o2.lineScale_    = o1.lineScale_;
+  }
+
+  function processStyle(styleString) {
+    var str, alpha = 1;
+
+    styleString = String(styleString);
+    if (styleString.substring(0, 3) == 'rgb') {
+      var start = styleString.indexOf('(', 3);
+      var end = styleString.indexOf(')', start + 1);
+      var guts = styleString.substring(start + 1, end).split(',');
+
+      str = '#';
+      for (var i = 0; i < 3; i++) {
+        str += dec2hex[Number(guts[i])];
+      }
+
+      if (guts.length == 4 && styleString.substr(3, 1) == 'a') {
+        alpha = guts[3];
+      }
+    } else {
+      str = styleString;
+    }
+
+    return {color: str, alpha: alpha};
+  }
+
+  function processLineCap(lineCap) {
+    switch (lineCap) {
+      case 'butt':
+        return 'flat';
+      case 'round':
+        return 'round';
+      case 'square':
+      default:
+        return 'square';
+    }
+  }
+
+  /**
+   * This class implements CanvasRenderingContext2D interface as described by
+   * the WHATWG.
+   * @param {HTMLElement} surfaceElement The element that the 2D context should
+   * be associated with
+   */
+  function CanvasRenderingContext2D_(surfaceElement) {
+    this.m_ = createMatrixIdentity();
+
+    this.mStack_ = [];
+    this.aStack_ = [];
+    this.currentPath_ = [];
+
+    // Canvas context properties
+    this.strokeStyle = '#000';
+    this.fillStyle = '#000';
+
+    this.lineWidth = 1;
+    this.lineJoin = 'miter';
+    this.lineCap = 'butt';
+    this.miterLimit = Z * 1;
+    this.globalAlpha = 1;
+    this.canvas = surfaceElement;
+
+    var el = surfaceElement.ownerDocument.createElement('div');
+    el.style.width =  surfaceElement.clientWidth + 'px';
+    el.style.height = surfaceElement.clientHeight + 'px';
+    el.style.overflow = 'hidden';
+    el.style.position = 'absolute';
+    surfaceElement.appendChild(el);
+
+    this.element_ = el;
+    this.arcScaleX_ = 1;
+    this.arcScaleY_ = 1;
+    this.lineScale_ = 1;
+  }
+
+  var contextPrototype = CanvasRenderingContext2D_.prototype;
+  contextPrototype.clearRect = function() {
+    this.element_.innerHTML = '';
+  };
+
+  contextPrototype.beginPath = function() {
+    // TODO: Branch current matrix so that save/restore has no effect
+    //       as per safari docs.
+    this.currentPath_ = [];
+  };
+
+  contextPrototype.moveTo = function(aX, aY) {
+    var p = this.getCoords_(aX, aY);
+    this.currentPath_.push({type: 'moveTo', x: p.x, y: p.y});
+    this.currentX_ = p.x;
+    this.currentY_ = p.y;
+  };
+
+  contextPrototype.lineTo = function(aX, aY) {
+    var p = this.getCoords_(aX, aY);
+    this.currentPath_.push({type: 'lineTo', x: p.x, y: p.y});
+
+    this.currentX_ = p.x;
+    this.currentY_ = p.y;
+  };
+
+  contextPrototype.bezierCurveTo = function(aCP1x, aCP1y,
+                                            aCP2x, aCP2y,
+                                            aX, aY) {
+    var p = this.getCoords_(aX, aY);
+    var cp1 = this.getCoords_(aCP1x, aCP1y);
+    var cp2 = this.getCoords_(aCP2x, aCP2y);
+    bezierCurveTo(this, cp1, cp2, p);
+  };
+
+  // Helper function that takes the already fixed cordinates.
+  function bezierCurveTo(self, cp1, cp2, p) {
+    self.currentPath_.push({
+      type: 'bezierCurveTo',
+      cp1x: cp1.x,
+      cp1y: cp1.y,
+      cp2x: cp2.x,
+      cp2y: cp2.y,
+      x: p.x,
+      y: p.y
+    });
+    self.currentX_ = p.x;
+    self.currentY_ = p.y;
+  }
+
+  contextPrototype.quadraticCurveTo = function(aCPx, aCPy, aX, aY) {
+    // the following is lifted almost directly from
+    // http://developer.mozilla.org/en/docs/Canvas_tutorial:Drawing_shapes
+
+    var cp = this.getCoords_(aCPx, aCPy);
+    var p = this.getCoords_(aX, aY);
+
+    var cp1 = {
+      x: this.currentX_ + 2.0 / 3.0 * (cp.x - this.currentX_),
+      y: this.currentY_ + 2.0 / 3.0 * (cp.y - this.currentY_)
+    };
+    var cp2 = {
+      x: cp1.x + (p.x - this.currentX_) / 3.0,
+      y: cp1.y + (p.y - this.currentY_) / 3.0
+    };
+
+    bezierCurveTo(this, cp1, cp2, p);
+  };
+
+  contextPrototype.arc = function(aX, aY, aRadius,
+                                  aStartAngle, aEndAngle, aClockwise) {
+    aRadius *= Z;
+    var arcType = aClockwise ? 'at' : 'wa';
+
+    var xStart = aX + mc(aStartAngle) * aRadius - Z2;
+    var yStart = aY + ms(aStartAngle) * aRadius - Z2;
+
+    var xEnd = aX + mc(aEndAngle) * aRadius - Z2;
+    var yEnd = aY + ms(aEndAngle) * aRadius - Z2;
+
+    // IE won't render arches drawn counter clockwise if xStart == xEnd.
+    if (xStart == xEnd && !aClockwise) {
+      xStart += 0.125; // Offset xStart by 1/80 of a pixel. Use something
+                       // that can be represented in binary
+    }
+
+    var p = this.getCoords_(aX, aY);
+    var pStart = this.getCoords_(xStart, yStart);
+    var pEnd = this.getCoords_(xEnd, yEnd);
+
+    this.currentPath_.push({type: arcType,
+                           x: p.x,
+                           y: p.y,
+                           radius: aRadius,
+                           xStart: pStart.x,
+                           yStart: pStart.y,
+                           xEnd: pEnd.x,
+                           yEnd: pEnd.y});
+
+  };
+
+  contextPrototype.rect = function(aX, aY, aWidth, aHeight) {
+    this.moveTo(aX, aY);
+    this.lineTo(aX + aWidth, aY);
+    this.lineTo(aX + aWidth, aY + aHeight);
+    this.lineTo(aX, aY + aHeight);
+    this.closePath();
+  };
+
+  contextPrototype.strokeRect = function(aX, aY, aWidth, aHeight) {
+    var oldPath = this.currentPath_;
+    this.beginPath();
+
+    this.moveTo(aX, aY);
+    this.lineTo(aX + aWidth, aY);
+    this.lineTo(aX + aWidth, aY + aHeight);
+    this.lineTo(aX, aY + aHeight);
+    this.closePath();
+    this.stroke();
+
+    this.currentPath_ = oldPath;
+  };
+
+  contextPrototype.fillRect = function(aX, aY, aWidth, aHeight) {
+    var oldPath = this.currentPath_;
+    this.beginPath();
+
+    this.moveTo(aX, aY);
+    this.lineTo(aX + aWidth, aY);
+    this.lineTo(aX + aWidth, aY + aHeight);
+    this.lineTo(aX, aY + aHeight);
+    this.closePath();
+    this.fill();
+
+    this.currentPath_ = oldPath;
+  };
+
+  contextPrototype.createLinearGradient = function(aX0, aY0, aX1, aY1) {
+    var gradient = new CanvasGradient_('gradient');
+    gradient.x0_ = aX0;
+    gradient.y0_ = aY0;
+    gradient.x1_ = aX1;
+    gradient.y1_ = aY1;
+    return gradient;
+  };
+
+  contextPrototype.createRadialGradient = function(aX0, aY0, aR0,
+                                                   aX1, aY1, aR1) {
+    var gradient = new CanvasGradient_('gradientradial');
+    gradient.x0_ = aX0;
+    gradient.y0_ = aY0;
+    gradient.r0_ = aR0;
+    gradient.x1_ = aX1;
+    gradient.y1_ = aY1;
+    gradient.r1_ = aR1;
+    return gradient;
+  };
+
+  contextPrototype.drawImage = function(image, var_args) {
+    var dx, dy, dw, dh, sx, sy, sw, sh;
+
+    // to find the original width we overide the width and height
+    var oldRuntimeWidth = image.runtimeStyle.width;
+    var oldRuntimeHeight = image.runtimeStyle.height;
+    image.runtimeStyle.width = 'auto';
+    image.runtimeStyle.height = 'auto';
+
+    // get the original size
+    var w = image.width;
+    var h = image.height;
+
+    // and remove overides
+    image.runtimeStyle.width = oldRuntimeWidth;
+    image.runtimeStyle.height = oldRuntimeHeight;
+
+    if (arguments.length == 3) {
+      dx = arguments[1];
+      dy = arguments[2];
+      sx = sy = 0;
+      sw = dw = w;
+      sh = dh = h;
+    } else if (arguments.length == 5) {
+      dx = arguments[1];
+      dy = arguments[2];
+      dw = arguments[3];
+      dh = arguments[4];
+      sx = sy = 0;
+      sw = w;
+      sh = h;
+    } else if (arguments.length == 9) {
+      sx = arguments[1];
+      sy = arguments[2];
+      sw = arguments[3];
+      sh = arguments[4];
+      dx = arguments[5];
+      dy = arguments[6];
+      dw = arguments[7];
+      dh = arguments[8];
+    } else {
+      throw Error('Invalid number of arguments');
+    }
+
+    var d = this.getCoords_(dx, dy);
+
+    var w2 = sw / 2;
+    var h2 = sh / 2;
+
+    var vmlStr = [];
+
+    var W = 10;
+    var H = 10;
+
+    // For some reason that I've now forgotten, using divs didn't work
+    vmlStr.push(' <g_vml_:group',
+                ' coordsize="', Z * W, ',', Z * H, '"',
+                ' coordorigin="0,0"' ,
+                ' style="width:', W, 'px;height:', H, 'px;position:absolute;');
+
+    // If filters are necessary (rotation exists), create them
+    // filters are bog-slow, so only create them if abbsolutely necessary
+    // The following check doesn't account for skews (which don't exist
+    // in the canvas spec (yet) anyway.
+
+    if (this.m_[0][0] != 1 || this.m_[0][1]) {
+      var filter = [];
+
+      // Note the 12/21 reversal
+      filter.push('M11=', this.m_[0][0], ',',
+                  'M12=', this.m_[1][0], ',',
+                  'M21=', this.m_[0][1], ',',
+                  'M22=', this.m_[1][1], ',',
+                  'Dx=', mr(d.x / Z), ',',
+                  'Dy=', mr(d.y / Z), '');
+
+      // Bounding box calculation (need to minimize displayed area so that
+      // filters don't waste time on unused pixels.
+      var max = d;
+      var c2 = this.getCoords_(dx + dw, dy);
+      var c3 = this.getCoords_(dx, dy + dh);
+      var c4 = this.getCoords_(dx + dw, dy + dh);
+
+      max.x = m.max(max.x, c2.x, c3.x, c4.x);
+      max.y = m.max(max.y, c2.y, c3.y, c4.y);
+
+      vmlStr.push('padding:0 ', mr(max.x / Z), 'px ', mr(max.y / Z),
+                  'px 0;filter:progid:DXImageTransform.Microsoft.Matrix(',
+                  filter.join(''), ", sizingmethod='clip');")
+    } else {
+      vmlStr.push('top:', mr(d.y / Z), 'px;left:', mr(d.x / Z), 'px;');
+    }
+
+    vmlStr.push(' ">' ,
+                '<g_vml_:image src="', image.src, '"',
+                ' style="width:', Z * dw, 'px;',
+                ' height:', Z * dh, 'px;"',
+                ' cropleft="', sx / w, '"',
+                ' croptop="', sy / h, '"',
+                ' cropright="', (w - sx - sw) / w, '"',
+                ' cropbottom="', (h - sy - sh) / h, '"',
+                ' />',
+                '</g_vml_:group>');
+
+    this.element_.insertAdjacentHTML('BeforeEnd',
+                                    vmlStr.join(''));
+  };
+
+  contextPrototype.stroke = function(aFill) {
+    var lineStr = [];
+    var lineOpen = false;
+    var a = processStyle(aFill ? this.fillStyle : this.strokeStyle);
+    var color = a.color;
+    var opacity = a.alpha * this.globalAlpha;
+
+    var W = 10;
+    var H = 10;
+
+    lineStr.push('<g_vml_:shape',
+                 ' filled="', !!aFill, '"',
+                 ' style="position:absolute;width:', W, 'px;height:', H, 'px;"',
+                 ' coordorigin="0 0" coordsize="', Z * W, ' ', Z * H, '"',
+                 ' stroked="', !aFill, '"',
+                 ' path="');
+
+    var newSeq = false;
+    var min = {x: null, y: null};
+    var max = {x: null, y: null};
+
+    for (var i = 0; i < this.currentPath_.length; i++) {
+      var p = this.currentPath_[i];
+      var c;
+
+      switch (p.type) {
+        case 'moveTo':
+          c = p;
+          lineStr.push(' m ', mr(p.x), ',', mr(p.y));
+          break;
+        case 'lineTo':
+          lineStr.push(' l ', mr(p.x), ',', mr(p.y));
+          break;
+        case 'close':
+          lineStr.push(' x ');
+          p = null;
+          break;
+        case 'bezierCurveTo':
+          lineStr.push(' c ',
+                       mr(p.cp1x), ',', mr(p.cp1y), ',',
+                       mr(p.cp2x), ',', mr(p.cp2y), ',',
+                       mr(p.x), ',', mr(p.y));
+          break;
+        case 'at':
+        case 'wa':
+          lineStr.push(' ', p.type, ' ',
+                       mr(p.x - this.arcScaleX_ * p.radius), ',',
+                       mr(p.y - this.arcScaleY_ * p.radius), ' ',
+                       mr(p.x + this.arcScaleX_ * p.radius), ',',
+                       mr(p.y + this.arcScaleY_ * p.radius), ' ',
+                       mr(p.xStart), ',', mr(p.yStart), ' ',
+                       mr(p.xEnd), ',', mr(p.yEnd));
+          break;
+      }
+
+
+      // TODO: Following is broken for curves due to
+      //       move to proper paths.
+
+      // Figure out dimensions so we can do gradient fills
+      // properly
+      if (p) {
+        if (min.x == null || p.x < min.x) {
+          min.x = p.x;
+        }
+        if (max.x == null || p.x > max.x) {
+          max.x = p.x;
+        }
+        if (min.y == null || p.y < min.y) {
+          min.y = p.y;
+        }
+        if (max.y == null || p.y > max.y) {
+          max.y = p.y;
+        }
+      }
+    }
+    lineStr.push(' ">');
+
+    if (!aFill) {
+      var lineWidth = this.lineScale_ * this.lineWidth;
+
+      // VML cannot correctly render a line if the width is less than 1px.
+      // In that case, we dilute the color to make the line look thinner.
+      if (lineWidth < 1) {
+        opacity *= lineWidth;
+      }
+
+      lineStr.push(
+        '<g_vml_:stroke',
+        ' opacity="', opacity, '"',
+        ' joinstyle="', this.lineJoin, '"',
+        ' miterlimit="', this.miterLimit, '"',
+        ' endcap="', processLineCap(this.lineCap), '"',
+        ' weight="', lineWidth, 'px"',
+        ' color="', color, '" />'
+      );
+    } else if (typeof this.fillStyle == 'object') {
+      var fillStyle = this.fillStyle;
+      var angle = 0;
+      var focus = {x: 0, y: 0};
+
+      // additional offset
+      var shift = 0;
+      // scale factor for offset
+      var expansion = 1;
+
+      if (fillStyle.type_ == 'gradient') {
+        var x0 = fillStyle.x0_ / this.arcScaleX_;
+        var y0 = fillStyle.y0_ / this.arcScaleY_;
+        var x1 = fillStyle.x1_ / this.arcScaleX_;
+        var y1 = fillStyle.y1_ / this.arcScaleY_;
+        var p0 = this.getCoords_(x0, y0);
+        var p1 = this.getCoords_(x1, y1);
+        var dx = p1.x - p0.x;
+        var dy = p1.y - p0.y;
+        angle = Math.atan2(dx, dy) * 180 / Math.PI;
+
+        // The angle should be a non-negative number.
+        if (angle < 0) {
+          angle += 360;
+        }
+
+        // Very small angles produce an unexpected result because they are
+        // converted to a scientific notation string.
+        if (angle < 1e-6) {
+          angle = 0;
+        }
+      } else {
+        var p0 = this.getCoords_(fillStyle.x0_, fillStyle.y0_);
+        var width  = max.x - min.x;
+        var height = max.y - min.y;
+        focus = {
+          x: (p0.x - min.x) / width,
+          y: (p0.y - min.y) / height
+        };
+
+        width  /= this.arcScaleX_ * Z;
+        height /= this.arcScaleY_ * Z;
+        var dimension = m.max(width, height);
+        shift = 2 * fillStyle.r0_ / dimension;
+        expansion = 2 * fillStyle.r1_ / dimension - shift;
+      }
+
+      // We need to sort the color stops in ascending order by offset,
+      // otherwise IE won't interpret it correctly.
+      var stops = fillStyle.colors_;
+      stops.sort(function(cs1, cs2) {
+        return cs1.offset - cs2.offset;
+      });
+
+      var length = stops.length;
+      var color1 = stops[0].color;
+      var color2 = stops[length - 1].color;
+      var opacity1 = stops[0].alpha * this.globalAlpha;
+      var opacity2 = stops[length - 1].alpha * this.globalAlpha;
+
+      var colors = [];
+      for (var i = 0; i < length; i++) {
+        var stop = stops[i];
+        colors.push(stop.offset * expansion + shift + ' ' + stop.color);
+      }
+
+      // When colors attribute is used, the meanings of opacity and o:opacity2
+      // are reversed.
+      lineStr.push('<g_vml_:fill type="', fillStyle.type_, '"',
+                   ' method="none" focus="100%"',
+                   ' color="', color1, '"',
+                   ' color2="', color2, '"',
+                   ' colors="', colors.join(','), '"',
+                   ' opacity="', opacity2, '"',
+                   ' g_o_:opacity2="', opacity1, '"',
+                   ' angle="', angle, '"',
+                   ' focusposition="', focus.x, ',', focus.y, '" />');
+    } else {
+      lineStr.push('<g_vml_:fill color="', color, '" opacity="', opacity,
+                   '" />');
+    }
+
+    lineStr.push('</g_vml_:shape>');
+
+    this.element_.insertAdjacentHTML('beforeEnd', lineStr.join(''));
+  };
+
+  contextPrototype.fill = function() {
+    this.stroke(true);
+  }
+
+  contextPrototype.closePath = function() {
+    this.currentPath_.push({type: 'close'});
+  };
+
+  /**
+   * @private
+   */
+  contextPrototype.getCoords_ = function(aX, aY) {
+    var m = this.m_;
+    return {
+      x: Z * (aX * m[0][0] + aY * m[1][0] + m[2][0]) - Z2,
+      y: Z * (aX * m[0][1] + aY * m[1][1] + m[2][1]) - Z2
+    }
+  };
+
+  contextPrototype.save = function() {
+    var o = {};
+    copyState(this, o);
+    this.aStack_.push(o);
+    this.mStack_.push(this.m_);
+    this.m_ = matrixMultiply(createMatrixIdentity(), this.m_);
+  };
+
+  contextPrototype.restore = function() {
+    copyState(this.aStack_.pop(), this);
+    this.m_ = this.mStack_.pop();
+  };
+
+  contextPrototype.translate = function(aX, aY) {
+    var m1 = [
+      [1,  0,  0],
+      [0,  1,  0],
+      [aX, aY, 1]
+    ];
+
+    this.m_ = matrixMultiply(m1, this.m_);
+  };
+
+  contextPrototype.rotate = function(aRot) {
+    var c = mc(aRot);
+    var s = ms(aRot);
+
+    var m1 = [
+      [c,  s, 0],
+      [-s, c, 0],
+      [0,  0, 1]
+    ];
+
+    this.m_ = matrixMultiply(m1, this.m_);
+  };
+
+  contextPrototype.scale = function(aX, aY) {
+    this.arcScaleX_ *= aX;
+    this.arcScaleY_ *= aY;
+    var m1 = [
+      [aX, 0,  0],
+      [0,  aY, 0],
+      [0,  0,  1]
+    ];
+
+    var m = this.m_ = matrixMultiply(m1, this.m_);
+
+    // Get the line scale.
+    // Determinant of this.m_ means how much the area is enlarged by the
+    // transformation. So its square root can be used as a scale factor
+    // for width.
+    var det = m[0][0] * m[1][1] - m[0][1] * m[1][0];
+    this.lineScale_ = sqrt(abs(det));
+  };
+
+  /******** STUBS ********/
+  contextPrototype.clip = function() {
+    // TODO: Implement
+  };
+
+  contextPrototype.arcTo = function() {
+    // TODO: Implement
+  };
+
+  contextPrototype.createPattern = function() {
+    return new CanvasPattern_;
+  };
+
+  // Gradient / Pattern Stubs
+  function CanvasGradient_(aType) {
+    this.type_ = aType;
+    this.x0_ = 0;
+    this.y0_ = 0;
+    this.r0_ = 0;
+    this.x1_ = 0;
+    this.y1_ = 0;
+    this.r1_ = 0;
+    this.colors_ = [];
+  }
+
+  CanvasGradient_.prototype.addColorStop = function(aOffset, aColor) {
+    aColor = processStyle(aColor);
+    this.colors_.push({offset: aOffset,
+                       color: aColor.color,
+                       alpha: aColor.alpha});
+  };
+
+  function CanvasPattern_() {}
+
+  // set up externs
+  G_vmlCanvasManager = G_vmlCanvasManager_;
+  CanvasRenderingContext2D = CanvasRenderingContext2D_;
+  CanvasGradient = CanvasGradient_;
+  CanvasPattern = CanvasPattern_;
+
+})();
+
+} // if
+

--- /dev/null
+++ b/js/flotr/lib/prototype-1.6.0.2.js
@@ -1,1 +1,4221 @@
-
+/*  Prototype JavaScript framework, version 1.6.0.2

+ *  (c) 2005-2008 Sam Stephenson

+ *

+ *  Prototype is freely distributable under the terms of an MIT-style license.

+ *  For details, see the Prototype web site: http://www.prototypejs.org/

+ *

+ *--------------------------------------------------------------------------*/

+

+var Prototype = {

+  Version: '1.6.0.2',

+

+  Browser: {

+    IE:     !!(window.attachEvent && !window.opera),

+    Opera:  !!window.opera,

+    WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1,

+    Gecko:  navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,

+    MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/)

+  },

+

+  BrowserFeatures: {

+    XPath: !!document.evaluate,

+    ElementExtensions: !!window.HTMLElement,

+    SpecificElementExtensions:

+      document.createElement('div').__proto__ &&

+      document.createElement('div').__proto__ !==

+        document.createElement('form').__proto__

+  },

+

+  ScriptFragment: '<script[^>]*>([\\S\\s]*?)<\/script>',

+  JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/,

+

+  emptyFunction: function() { },

+  K: function(x) { return x }

+};

+

+if (Prototype.Browser.MobileSafari)

+  Prototype.BrowserFeatures.SpecificElementExtensions = false;

+

+

+/* Based on Alex Arnell's inheritance implementation. */

+var Class = {

+  create: function() {

+    var parent = null, properties = $A(arguments);

+    if (Object.isFunction(properties[0]))

+      parent = properties.shift();

+

+    function klass() {

+      this.initialize.apply(this, arguments);

+    }

+

+    Object.extend(klass, Class.Methods);

+    klass.superclass = parent;

+    klass.subclasses = [];

+

+    if (parent) {

+      var subclass = function() { };

+      subclass.prototype = parent.prototype;

+      klass.prototype = new subclass;

+      parent.subclasses.push(klass);

+    }

+

+    for (var i = 0; i < properties.length; i++)

+      klass.addMethods(properties[i]);

+

+    if (!klass.prototype.initialize)

+      klass.prototype.initialize = Prototype.emptyFunction;

+

+    klass.prototype.constructor = klass;

+

+    return klass;

+  }

+};

+

+Class.Methods = {

+  addMethods: function(source) {

+    var ancestor   = this.superclass && this.superclass.prototype;

+    var properties = Object.keys(source);

+

+    if (!Object.keys({ toString: true }).length)

+      properties.push("toString", "valueOf");

+

+    for (var i = 0, length = properties.length; i < length; i++) {

+      var property = properties[i], value = source[property];

+      if (ancestor && Object.isFunction(value) &&

+          value.argumentNames().first() == "$super") {

+        var method = value, value = Object.extend((function(m) {

+          return function() { return ancestor[m].apply(this, arguments) };

+        })(property).wrap(method), {

+          valueOf:  function() { return method },

+          toString: function() { return method.toString() }

+        });

+      }

+      this.prototype[property] = value;

+    }

+

+    return this;

+  }

+};

+

+var Abstract = { };

+

+Object.extend = function(destination, source) {

+  for (var property in source)

+    destination[property] = source[property];

+  return destination;

+};

+

+Object.extend(Object, {

+  inspect: function(object) {

+    try {

+      if (Object.isUndefined(object)) return 'undefined';

+      if (object === null) return 'null';

+      return object.inspect ? object.inspect() : String(object);

+    } catch (e) {

+      if (e instanceof RangeError) return '...';

+      throw e;

+    }

+  },

+

+  toJSON: function(object) {

+    var type = typeof object;

+    switch (type) {

+      case 'undefined':

+      case 'function':

+      case 'unknown': return;

+      case 'boolean': return object.toString();

+    }

+

+    if (object === null) return 'null';

+    if (object.toJSON) return object.toJSON();

+    if (Object.isElement(object)) return;

+

+    var results = [];

+    for (var property in object) {

+      var value = Object.toJSON(object[property]);

+      if (!Object.isUndefined(value))

+        results.push(property.toJSON() + ': ' + value);

+    }

+

+    return '{' + results.join(', ') + '}';

+  },

+

+  toQueryString: function(object) {

+    return $H(object).toQueryString();

+  },

+

+  toHTML: function(object) {

+    return object && object.toHTML ? object.toHTML() : String.interpret(object);

+  },

+

+  keys: function(object) {

+    var keys = [];

+    for (var property in object)

+      keys.push(property);

+    return keys;

+  },

+

+  values: function(object) {

+    var values = [];

+    for (var property in object)

+      values.push(object[property]);

+    return values;

+  },

+

+  clone: function(object) {

+    return Object.extend({ }, object);

+  },

+

+  isElement: function(object) {

+    return object && object.nodeType == 1;

+  },

+

+  isArray: function(object) {

+    return object != null && typeof object == "object" &&

+      'splice' in object && 'join' in object;

+  },

+

+  isHash: function(object) {

+    return object instanceof Hash;

+  },

+

+  isFunction: function(object) {

+    return typeof object == "function";

+  },

+

+  isString: function(object) {

+    return typeof object == "string";

+  },

+

+  isNumber: function(object) {

+    return typeof object == "number";

+  },

+

+  isUndefined: function(object) {

+    return typeof object == "undefined";

+  }

+});

+

+Object.extend(Function.prototype, {

+  argumentNames: function() {

+    var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip");

+    return names.length == 1 && !names[0] ? [] : names;

+  },

+

+  bind: function() {

+    if (arguments.length < 2 && Object.isUndefined(arguments[0])) return this;

+    var __method = this, args = $A(arguments), object = args.shift();

+    return function() {

+      return __method.apply(object, args.concat($A(arguments)));

+    }

+  },

+

+  bindAsEventListener: function() {

+    var __method = this, args = $A(arguments), object = args.shift();

+    return function(event) {

+      return __method.apply(object, [event || window.event].concat(args));

+    }

+  },

+

+  curry: function() {

+    if (!arguments.length) return this;

+    var __method = this, args = $A(arguments);

+    return function() {

+      return __method.apply(this, args.concat($A(arguments)));

+    }

+  },

+

+  delay: function() {

+    var __method = this, args = $A(arguments), timeout = args.shift() * 1000;

+    return window.setTimeout(function() {

+      return __method.apply(__method, args);

+    }, timeout);

+  },

+

+  wrap: function(wrapper) {

+    var __method = this;

+    return function() {

+      return wrapper.apply(this, [__method.bind(this)].concat($A(arguments)));

+    }

+  },

+

+  methodize: function() {

+    if (this._methodized) return this._methodized;

+    var __method = this;

+    return this._methodized = function() {

+      return __method.apply(null, [this].concat($A(arguments)));

+    };

+  }

+});

+

+Function.prototype.defer = Function.prototype.delay.curry(0.01);

+

+Date.prototype.toJSON = function() {

+  return '"' + this.getUTCFullYear() + '-' +

+    (this.getUTCMonth() + 1).toPaddedString(2) + '-' +

+    this.getUTCDate().toPaddedString(2) + 'T' +

+    this.getUTCHours().toPaddedString(2) + ':' +

+    this.getUTCMinutes().toPaddedString(2) + ':' +

+    this.getUTCSeconds().toPaddedString(2) + 'Z"';

+};

+

+var Try = {

+  these: function() {

+    var returnValue;

+

+    for (var i = 0, length = arguments.length; i < length; i++) {

+      var lambda = arguments[i];

+      try {

+        returnValue = lambda();

+        break;

+      } catch (e) { }

+    }

+

+    return returnValue;

+  }

+};

+

+RegExp.prototype.match = RegExp.prototype.test;

+

+RegExp.escape = function(str) {

+  return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');

+};

+

+/*--------------------------------------------------------------------------*/

+

+var PeriodicalExecuter = Class.create({

+  initialize: function(callback, frequency) {

+    this.callback = callback;

+    this.frequency = frequency;

+    this.currentlyExecuting = false;

+

+    this.registerCallback();

+  },

+

+  registerCallback: function() {

+    this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);

+  },

+

+  execute: function() {

+    this.callback(this);

+  },

+

+  stop: function() {

+    if (!this.timer) return;

+    clearInterval(this.timer);

+    this.timer = null;

+  },

+

+  onTimerEvent: function() {

+    if (!this.currentlyExecuting) {

+      try {

+        this.currentlyExecuting = true;

+        this.execute();

+      } finally {

+        this.currentlyExecuting = false;

+      }

+    }

+  }

+});

+Object.extend(String, {

+  interpret: function(value) {

+    return value == null ? '' : String(value);

+  },

+  specialChar: {

+    '\b': '\\b',

+    '\t': '\\t',

+    '\n': '\\n',

+    '\f': '\\f',

+    '\r': '\\r',

+    '\\': '\\\\'

+  }

+});

+

+Object.extend(String.prototype, {

+  gsub: function(pattern, replacement) {

+    var result = '', source = this, match;

+    replacement = arguments.callee.prepareReplacement(replacement);

+

+    while (source.length > 0) {

+      if (match = source.match(pattern)) {

+        result += source.slice(0, match.index);

+        result += String.interpret(replacement(match));

+        source  = source.slice(match.index + match[0].length);

+      } else {

+        result += source, source = '';

+      }

+    }

+    return result;

+  },

+

+  sub: function(pattern, replacement, count) {

+    replacement = this.gsub.prepareReplacement(replacement);

+    count = Object.isUndefined(count) ? 1 : count;

+

+    return this.gsub(pattern, function(match) {

+      if (--count < 0) return match[0];

+      return replacement(match);

+    });

+  },

+

+  scan: function(pattern, iterator) {

+    this.gsub(pattern, iterator);

+    return String(this);

+  },

+

+  truncate: function(length, truncation) {

+    length = length || 30;

+    truncation = Object.isUndefined(truncation) ? '...' : truncation;

+    return this.length > length ?

+      this.slice(0, length - truncation.length) + truncation : String(this);

+  },

+

+  strip: function() {

+    return this.replace(/^\s+/, '').replace(/\s+$/, '');

+  },

+

+  stripTags: function() {

+    return this.replace(/<\/?[^>]+>/gi, '');

+  },

+

+  stripScripts: function() {

+    return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), '');

+  },

+

+  extractScripts: function() {

+    var matchAll = new RegExp(Prototype.ScriptFragment, 'img');

+    var matchOne = new RegExp(Prototype.ScriptFragment, 'im');

+    return (this.match(matchAll) || []).map(function(scriptTag) {

+      return (scriptTag.match(matchOne) || ['', ''])[1];

+    });

+  },

+

+  evalScripts: function() {

+    return this.extractScripts().map(function(script) { return eval(script) });

+  },

+

+  escapeHTML: function() {

+    var self = arguments.callee;

+    self.text.data = this;

+    return self.div.innerHTML;

+  },

+

+  unescapeHTML: function() {

+    var div = new Element('div');

+    div.innerHTML = this.stripTags();

+    return div.childNodes[0] ? (div.childNodes.length > 1 ?

+      $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) :

+      div.childNodes[0].nodeValue) : '';

+  },

+

+  toQueryParams: function(separator) {

+    var match = this.strip().match(/([^?#]*)(#.*)?$/);

+    if (!match) return { };

+

+    return match[1].split(separator || '&').inject({ }, function(hash, pair) {

+      if ((pair = pair.split('='))[0]) {

+        var key = decodeURIComponent(pair.shift());

+        var value = pair.length > 1 ? pair.join('=') : pair[0];

+        if (value != undefined) value = decodeURIComponent(value);

+

+        if (key in hash) {

+          if (!Object.isArray(hash[key])) hash[key] = [hash[key]];

+          hash[key].push(value);

+        }

+        else hash[key] = value;

+      }

+      return hash;

+    });

+  },

+

+  toArray: function() {

+    return this.split('');

+  },

+

+  succ: function() {

+    return this.slice(0, this.length - 1) +

+      String.fromCharCode(this.charCodeAt(this.length - 1) + 1);

+  },

+

+  times: function(count) {

+    return count < 1 ? '' : new Array(count + 1).join(this);

+  },

+

+  camelize: function() {

+    var parts = this.split('-'), len = parts.length;

+    if (len == 1) return parts[0];

+

+    var camelized = this.charAt(0) == '-'

+      ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1)

+      : parts[0];

+

+    for (var i = 1; i < len; i++)

+      camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1);

+

+    return camelized;

+  },

+

+  capitalize: function() {

+    return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase();

+  },

+

+  underscore: function() {

+    return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase();

+  },

+

+  dasherize: function() {

+    return this.gsub(/_/,'-');

+  },

+

+  inspect: function(useDoubleQuotes) {

+    var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) {

+      var character = String.specialChar[match[0]];

+      return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16);

+    });

+    if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"';

+    return "'" + escapedString.replace(/'/g, '\\\'') + "'";

+  },

+

+  toJSON: function() {

+    return this.inspect(true);

+  },

+

+  unfilterJSON: function(filter) {

+    return this.sub(filter || Prototype.JSONFilter, '#{1}');

+  },

+

+  isJSON: function() {

+    var str = this;

+    if (str.blank()) return false;

+    str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, '');

+    return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);

+  },

+

+  evalJSON: function(sanitize) {

+    var json = this.unfilterJSON();

+    try {

+      if (!sanitize || json.isJSON()) return eval('(' + json + ')');

+    } catch (e) { }

+    throw new SyntaxError('Badly formed JSON string: ' + this.inspect());

+  },

+

+  include: function(pattern) {

+    return this.indexOf(pattern) > -1;

+  },

+

+  startsWith: function(pattern) {

+    return this.indexOf(pattern) === 0;

+  },

+

+  endsWith: function(pattern) {

+    var d = this.length - pattern.length;

+    return d >= 0 && this.lastIndexOf(pattern) === d;

+  },

+

+  empty: function() {

+    return this == '';

+  },

+

+  blank: function() {

+    return /^\s*$/.test(this);

+  },

+

+  interpolate: function(object, pattern) {

+    return new Template(this, pattern).evaluate(object);

+  }

+});

+

+if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, {

+  escapeHTML: function() {

+    return this.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');

+  },

+  unescapeHTML: function() {

+    return this.replace(/&amp;/g,'&').replace(/&lt;/g,'<').replace(/&gt;/g,'>');

+  }

+});

+

+String.prototype.gsub.prepareReplacement = function(replacement) {

+  if (Object.isFunction(replacement)) return replacement;

+  var template = new Template(replacement);

+  return function(match) { return template.evaluate(match) };

+};

+

+String.prototype.parseQuery = String.prototype.toQueryParams;

+

+Object.extend(String.prototype.escapeHTML, {

+  div:  document.createElement('div'),

+  text: document.createTextNode('')

+});

+

+with (String.prototype.escapeHTML) div.appendChild(text);

+

+var Template = Class.create({

+  initialize: function(template, pattern) {

+    this.template = template.toString();

+    this.pattern = pattern || Template.Pattern;

+  },

+

+  evaluate: function(object) {

+    if (Object.isFunction(object.toTemplateReplacements))

+      object = object.toTemplateReplacements();

+

+    return this.template.gsub(this.pattern, function(match) {

+      if (object == null) return '';

+

+      var before = match[1] || '';

+      if (before == '\\') return match[2];

+

+      var ctx = object, expr = match[3];

+      var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;

+      match = pattern.exec(expr);

+      if (match == null) return before;

+

+      while (match != null) {

+        var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1];

+        ctx = ctx[comp];

+        if (null == ctx || '' == match[3]) break;

+        expr = expr.substring('[' == match[3] ? match[1].length : match[0].length);

+        match = pattern.exec(expr);

+      }

+

+      return before + String.interpret(ctx);

+    });

+  }

+});

+Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/;

+

+var $break = { };

+

+var Enumerable = {

+  each: function(iterator, context) {

+    var index = 0;

+    iterator = iterator.bind(context);

+    try {

+      this._each(function(value) {

+        iterator(value, index++);

+      });

+    } catch (e) {

+      if (e != $break) throw e;

+    }

+    return this;

+  },

+

+  eachSlice: function(number, iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var index = -number, slices = [], array = this.toArray();

+    while ((index += number) < array.length)

+      slices.push(array.slice(index, index+number));

+    return slices.collect(iterator, context);

+  },

+

+  all: function(iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var result = true;

+    this.each(function(value, index) {

+      result = result && !!iterator(value, index);

+      if (!result) throw $break;

+    });

+    return result;

+  },

+

+  any: function(iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var result = false;

+    this.each(function(value, index) {

+      if (result = !!iterator(value, index))

+        throw $break;

+    });

+    return result;

+  },

+

+  collect: function(iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var results = [];

+    this.each(function(value, index) {

+      results.push(iterator(value, index));

+    });

+    return results;

+  },

+

+  detect: function(iterator, context) {

+    iterator = iterator.bind(context);

+    var result;

+    this.each(function(value, index) {

+      if (iterator(value, index)) {

+        result = value;

+        throw $break;

+      }

+    });

+    return result;

+  },

+

+  findAll: function(iterator, context) {

+    iterator = iterator.bind(context);

+    var results = [];

+    this.each(function(value, index) {

+      if (iterator(value, index))

+        results.push(value);

+    });

+    return results;

+  },

+

+  grep: function(filter, iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var results = [];

+

+    if (Object.isString(filter))

+      filter = new RegExp(filter);

+

+    this.each(function(value, index) {

+      if (filter.match(value))

+        results.push(iterator(value, index));

+    });

+    return results;

+  },

+

+  include: function(object) {

+    if (Object.isFunction(this.indexOf))

+      if (this.indexOf(object) != -1) return true;

+

+    var found = false;

+    this.each(function(value) {

+      if (value == object) {

+        found = true;

+        throw $break;

+      }

+    });

+    return found;

+  },

+

+  inGroupsOf: function(number, fillWith) {

+    fillWith = Object.isUndefined(fillWith) ? null : fillWith;

+    return this.eachSlice(number, function(slice) {

+      while(slice.length < number) slice.push(fillWith);

+      return slice;

+    });

+  },

+

+  inject: function(memo, iterator, context) {

+    iterator = iterator.bind(context);

+    this.each(function(value, index) {

+      memo = iterator(memo, value, index);

+    });

+    return memo;

+  },

+

+  invoke: function(method) {

+    var args = $A(arguments).slice(1);

+    return this.map(function(value) {

+      return value[method].apply(value, args);

+    });

+  },

+

+  max: function(iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var result;

+    this.each(function(value, index) {

+      value = iterator(value, index);

+      if (result == null || value >= result)

+        result = value;

+    });

+    return result;

+  },

+

+  min: function(iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var result;

+    this.each(function(value, index) {

+      value = iterator(value, index);

+      if (result == null || value < result)

+        result = value;

+    });

+    return result;

+  },

+

+  partition: function(iterator, context) {

+    iterator = iterator ? iterator.bind(context) : Prototype.K;

+    var trues = [], falses = [];

+    this.each(function(value, index) {

+      (iterator(value, index) ?

+        trues : falses).push(value);

+    });

+    return [trues, falses];

+  },

+

+  pluck: function(property) {

+    var results = [];

+    this.each(function(value) {

+      results.push(value[property]);

+    });

+    return results;

+  },

+

+  reject: function(iterator, context) {

+    iterator = iterator.bind(context);

+    var results = [];

+    this.each(function(value, index) {

+      if (!iterator(value, index))

+        results.push(value);

+    });

+    return results;

+  },

+

+  sortBy: function(iterator, context) {

+    iterator = iterator.bind(context);

+    return this.map(function(value, index) {

+      return {value: value, criteria: iterator(value, index)};

+    }).sort(function(left, right) {

+      var a = left.criteria, b = right.criteria;

+      return a < b ? -1 : a > b ? 1 : 0;

+    }).pluck('value');

+  },

+

+  toArray: function() {

+    return this.map();

+  },

+

+  zip: function() {

+    var iterator = Prototype.K, args = $A(arguments);

+    if (Object.isFunction(args.last()))

+      iterator = args.pop();

+

+    var collections = [this].concat(args).map($A);

+    return this.map(function(value, index) {

+      return iterator(collections.pluck(index));

+    });

+  },

+

+  size: function() {

+    return this.toArray().length;

+  },

+

+  inspect: function() {

+    return '#<Enumerable:' + this.toArray().inspect() + '>';

+  }

+};

+

+Object.extend(Enumerable, {

+  map:     Enumerable.collect,

+  find:    Enumerable.detect,

+  select:  Enumerable.findAll,

+  filter:  Enumerable.findAll,

+  member:  Enumerable.include,

+  entries: Enumerable.toArray,

+  every:   Enumerable.all,

+  some:    Enumerable.any

+});

+function $A(iterable) {

+  if (!iterable) return [];

+  if (iterable.toArray) return iterable.toArray();

+  var length = iterable.length || 0, results = new Array(length);

+  while (length--) results[length] = iterable[length];

+  return results;

+}

+

+if (Prototype.Browser.WebKit) {

+  $A = function(iterable) {

+    if (!iterable) return [];

+    if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') &&

+        iterable.toArray) return iterable.toArray();

+    var length = iterable.length || 0, results = new Array(length);

+    while (length--) results[length] = iterable[length];

+    return results;

+  };

+}

+

+Array.from = $A;

+

+Object.extend(Array.prototype, Enumerable);

+

+if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse;

+

+Object.extend(Array.prototype, {

+  _each: function(iterator) {

+    for (var i = 0, length = this.length; i < length; i++)

+      iterator(this[i]);

+  },

+

+  clear: function() {

+    this.length = 0;

+    return this;

+  },

+

+  first: function() {

+    return this[0];

+  },

+

+  last: function() {

+    return this[this.length - 1];

+  },

+

+  compact: function() {

+    return this.select(function(value) {

+      return value != null;

+    });

+  },

+

+  flatten: function() {

+    return this.inject([], function(array, value) {

+      return array.concat(Object.isArray(value) ?

+        value.flatten() : [value]);

+    });

+  },

+

+  without: function() {

+    var values = $A(arguments);

+    return this.select(function(value) {

+      return !values.include(value);

+    });

+  },

+

+  reverse: function(inline) {

+    return (inline !== false ? this : this.toArray())._reverse();

+  },

+

+  reduce: function() {

+    return this.length > 1 ? this : this[0];

+  },

+

+  uniq: function(sorted) {

+    return this.inject([], function(array, value, index) {

+      if (0 == index || (sorted ? array.last() != value : !array.include(value)))

+        array.push(value);

+      return array;

+    });

+  },

+

+  intersect: function(array) {

+    return this.uniq().findAll(function(item) {

+      return array.detect(function(value) { return item === value });

+    });

+  },

+

+  clone: function() {

+    return [].concat(this);

+  },

+

+  size: function() {

+    return this.length;

+  },

+

+  inspect: function() {

+    return '[' + this.map(Object.inspect).join(', ') + ']';

+  },

+

+  toJSON: function() {

+    var results = [];

+    this.each(function(object) {

+      var value = Object.toJSON(object);

+      if (!Object.isUndefined(value)) results.push(value);

+    });

+    return '[' + results.join(', ') + ']';

+  }

+});

+

+// use native browser JS 1.6 implementation if available

+if (Object.isFunction(Array.prototype.forEach))

+  Array.prototype._each = Array.prototype.forEach;

+

+if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) {

+  i || (i = 0);

+  var length = this.length;

+  if (i < 0) i = length + i;

+  for (; i < length; i++)

+    if (this[i] === item) return i;

+  return -1;

+};

+

+if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) {

+  i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1;

+  var n = this.slice(0, i).reverse().indexOf(item);

+  return (n < 0) ? n : i - n - 1;

+};

+

+Array.prototype.toArray = Array.prototype.clone;

+

+function $w(string) {

+  if (!Object.isString(string)) return [];

+  string = string.strip();

+  return string ? string.split(/\s+/) : [];

+}

+

+if (Prototype.Browser.Opera){

+  Array.prototype.concat = function() {

+    var array = [];

+    for (var i = 0, length = this.length; i < length; i++) array.push(this[i]);

+    for (var i = 0, length = arguments.length; i < length; i++) {

+      if (Object.isArray(arguments[i])) {

+        for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++)

+          array.push(arguments[i][j]);

+      } else {

+        array.push(arguments[i]);

+      }

+    }

+    return array;

+  };

+}

+Object.extend(Number.prototype, {

+  toColorPart: function() {

+    return this.toPaddedString(2, 16);

+  },

+

+  succ: function() {

+    return this + 1;

+  },

+

+  times: function(iterator) {

+    $R(0, this, true).each(iterator);

+    return this;

+  },

+

+  toPaddedString: function(length, radix) {

+    var string = this.toString(radix || 10);

+    return '0'.times(length - string.length) + string;

+  },

+

+  toJSON: function() {

+    return isFinite(this) ? this.toString() : 'null';

+  }

+});

+

+$w('abs round ceil floor').each(function(method){

+  Number.prototype[method] = Math[method].methodize();

+});

+function $H(object) {

+  return new Hash(object);

+};

+

+var Hash = Class.create(Enumerable, (function() {

+

+  function toQueryPair(key, value) {

+    if (Object.isUndefined(value)) return key;

+    return key + '=' + encodeURIComponent(String.interpret(value));

+  }

+

+  return {

+    initialize: function(object) {

+      this._object = Object.isHash(object) ? object.toObject() : Object.clone(object);

+    },

+

+    _each: function(iterator) {

+      for (var key in this._object) {

+        var value = this._object[key], pair = [key, value];

+        pair.key = key;

+        pair.value = value;

+        iterator(pair);

+      }

+    },

+

+    set: function(key, value) {

+      return this._object[key] = value;

+    },

+

+    get: function(key) {

+      return this._object[key];

+    },

+

+    unset: function(key) {

+      var value = this._object[key];

+      delete this._object[key];

+      return value;

+    },

+

+    toObject: function() {

+      return Object.clone(this._object);

+    },

+

+    keys: function() {

+      return this.pluck('key');

+    },

+

+    values: function() {

+      return this.pluck('value');

+    },

+

+    index: function(value) {

+      var match = this.detect(function(pair) {

+        return pair.value === value;

+      });

+      return match && match.key;

+    },

+

+    merge: function(object) {

+      return this.clone().update(object);

+    },

+

+    update: function(object) {

+      return new Hash(object).inject(this, function(result, pair) {

+        result.set(pair.key, pair.value);

+        return result;

+      });

+    },

+

+    toQueryString: function() {

+      return this.map(function(pair) {

+        var key = encodeURIComponent(pair.key), values = pair.value;

+

+        if (values && typeof values == 'object') {

+          if (Object.isArray(values))

+            return values.map(toQueryPair.curry(key)).join('&');

+        }

+        return toQueryPair(key, values);

+      }).join('&');

+    },

+

+    inspect: function() {

+      return '#<Hash:{' + this.map(function(pair) {

+        return pair.map(Object.inspect).join(': ');

+      }).join(', ') + '}>';

+    },

+

+    toJSON: function() {

+      return Object.toJSON(this.toObject());

+    },

+

+    clone: function() {

+      return new Hash(this);

+    }

+  }

+})());

+

+Hash.prototype.toTemplateReplacements = Hash.prototype.toObject;

+Hash.from = $H;

+var ObjectRange = Class.create(Enumerable, {

+  initialize: function(start, end, exclusive) {

+    this.start = start;

+    this.end = end;

+    this.exclusive = exclusive;

+  },

+

+  _each: function(iterator) {

+    var value = this.start;

+    while (this.include(value)) {

+      iterator(value);

+      value = value.succ();

+    }

+  },

+

+  include: function(value) {

+    if (value < this.start)

+      return false;

+    if (this.exclusive)

+      return value < this.end;

+    return value <= this.end;

+  }

+});

+

+var $R = function(start, end, exclusive) {

+  return new ObjectRange(start, end, exclusive);

+};

+

+var Ajax = {

+  getTransport: function() {

+    return Try.these(

+      function() {return new XMLHttpRequest()},

+      function() {return new ActiveXObject('Msxml2.XMLHTTP')},

+      function() {return new ActiveXObject('Microsoft.XMLHTTP')}

+    ) || false;

+  },

+

+  activeRequestCount: 0

+};

+

+Ajax.Responders = {

+  responders: [],

+

+  _each: function(iterator) {

+    this.responders._each(iterator);

+  },

+

+  register: function(responder) {

+    if (!this.include(responder))

+      this.responders.push(responder);

+  },

+

+  unregister: function(responder) {

+    this.responders = this.responders.without(responder);

+  },

+

+  dispatch: function(callback, request, transport, json) {

+    this.each(function(responder) {

+      if (Object.isFunction(responder[callback])) {

+        try {

+          responder[callback].apply(responder, [request, transport, json]);

+        } catch (e) { }

+      }

+    });

+  }

+};

+

+Object.extend(Ajax.Responders, Enumerable);

+

+Ajax.Responders.register({

+  onCreate:   function() { Ajax.activeRequestCount++ },

+  onComplete: function() { Ajax.activeRequestCount-- }

+});

+

+Ajax.Base = Class.create({

+  initialize: function(options) {

+    this.options = {

+      method:       'post',

+      asynchronous: true,

+      contentType:  'application/x-www-form-urlencoded',

+      encoding:     'UTF-8',

+      parameters:   '',

+      evalJSON:     true,

+      evalJS:       true

+    };

+    Object.extend(this.options, options || { });

+

+    this.options.method = this.options.method.toLowerCase();

+

+    if (Object.isString(this.options.parameters))

+      this.options.parameters = this.options.parameters.toQueryParams();

+    else if (Object.isHash(this.options.parameters))

+      this.options.parameters = this.options.parameters.toObject();

+  }

+});

+

+Ajax.Request = Class.create(Ajax.Base, {

+  _complete: false,

+

+  initialize: function($super, url, options) {

+    $super(options);

+    this.transport = Ajax.getTransport();

+    this.request(url);

+  },

+

+  request: function(url) {

+    this.url = url;

+    this.method = this.options.method;

+    var params = Object.clone(this.options.parameters);

+

+    if (!['get', 'post'].include(this.method)) {

+      // simulate other verbs over post

+      params['_method'] = this.method;

+      this.method = 'post';

+    }

+

+    this.parameters = params;

+

+    if (params = Object.toQueryString(params)) {

+      // when GET, append parameters to URL

+      if (this.method == 'get')

+        this.url += (this.url.include('?') ? '&' : '?') + params;

+      else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent))

+        params += '&_=';

+    }

+

+    try {

+      var response = new Ajax.Response(this);

+      if (this.options.onCreate) this.options.onCreate(response);

+      Ajax.Responders.dispatch('onCreate', this, response);

+

+      this.transport.open(this.method.toUpperCase(), this.url,

+        this.options.asynchronous);

+

+      if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1);

+

+      this.transport.onreadystatechange = this.onStateChange.bind(this);

+      this.setRequestHeaders();

+

+      this.body = this.method == 'post' ? (this.options.postBody || params) : null;

+      this.transport.send(this.body);

+

+      /* Force Firefox to handle ready state 4 for synchronous requests */

+      if (!this.options.asynchronous && this.transport.overrideMimeType)

+        this.onStateChange();

+

+    }

+    catch (e) {

+      this.dispatchException(e);

+    }

+  },

+

+  onStateChange: function() {

+    var readyState = this.transport.readyState;

+    if (readyState > 1 && !((readyState == 4) && this._complete))

+      this.respondToReadyState(this.transport.readyState);

+  },

+

+  setRequestHeaders: function() {

+    var headers = {

+      'X-Requested-With': 'XMLHttpRequest',

+      'X-Prototype-Version': Prototype.Version,

+      'Accept': 'text/javascript, text/html, application/xml, text/xml, */*'

+    };

+

+    if (this.method == 'post') {

+      headers['Content-type'] = this.options.contentType +

+        (this.options.encoding ? '; charset=' + this.options.encoding : '');

+

+      /* Force "Connection: close" for older Mozilla browsers to work

+       * around a bug where XMLHttpRequest sends an incorrect

+       * Content-length header. See Mozilla Bugzilla #246651.

+       */

+      if (this.transport.overrideMimeType &&

+          (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005)

+            headers['Connection'] = 'close';

+    }

+

+    // user-defined headers

+    if (typeof this.options.requestHeaders == 'object') {

+      var extras = this.options.requestHeaders;

+

+      if (Object.isFunction(extras.push))

+        for (var i = 0, length = extras.length; i < length; i += 2)

+          headers[extras[i]] = extras[i+1];

+      else

+        $H(extras).each(function(pair) { headers[pair.key] = pair.value });

+    }

+

+    for (var name in headers)

+      this.transport.setRequestHeader(name, headers[name]);

+  },

+

+  success: function() {

+    var status = this.getStatus();

+    return !status || (status >= 200 && status < 300);

+  },

+

+  getStatus: function() {

+    try {

+      return this.transport.status || 0;

+    } catch (e) { return 0 }

+  },

+

+  respondToReadyState: function(readyState) {

+    var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this);

+

+    if (state == 'Complete') {

+      try {

+        this._complete = true;

+        (this.options['on' + response.status]

+         || this.options['on' + (this.success() ? 'Success' : 'Failure')]

+         || Prototype.emptyFunction)(response, response.headerJSON);

+      } catch (e) {

+        this.dispatchException(e);

+      }

+

+      var contentType = response.getHeader('Content-type');

+      if (this.options.evalJS == 'force'

+          || (this.options.evalJS && this.isSameOrigin() && contentType

+          && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i)))

+        this.evalResponse();

+    }

+

+    try {

+      (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON);

+      Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON);

+    } catch (e) {

+      this.dispatchException(e);

+    }

+

+    if (state == 'Complete') {

+      // avoid memory leak in MSIE: clean up

+      this.transport.onreadystatechange = Prototype.emptyFunction;

+    }

+  },

+

+  isSameOrigin: function() {

+    var m = this.url.match(/^\s*https?:\/\/[^\/]*/);

+    return !m || (m[0] == '#{protocol}//#{domain}#{port}'.interpolate({

+      protocol: location.protocol,

+      domain: document.domain,

+      port: location.port ? ':' + location.port : ''

+    }));

+  },

+

+  getHeader: function(name) {

+    try {

+      return this.transport.getResponseHeader(name) || null;

+    } catch (e) { return null }

+  },

+

+  evalResponse: function() {

+    try {

+      return eval((this.transport.responseText || '').unfilterJSON());

+    } catch (e) {

+      this.dispatchException(e);

+    }

+  },

+

+  dispatchException: function(exception) {

+    (this.options.onException || Prototype.emptyFunction)(this, exception);

+    Ajax.Responders.dispatch('onException', this, exception);

+  }

+});

+

+Ajax.Request.Events =

+  ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];

+

+Ajax.Response = Class.create({

+  initialize: function(request){

+    this.request = request;

+    var transport  = this.transport  = request.transport,

+        readyState = this.readyState = transport.readyState;

+

+    if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) {

+      this.status       = this.getStatus();

+      this.statusText   = this.getStatusText();

+      this.responseText = String.interpret(transport.responseText);

+      this.headerJSON   = this._getHeaderJSON();

+    }

+

+    if(readyState == 4) {

+      var xml = transport.responseXML;

+      this.responseXML  = Object.isUndefined(xml) ? null : xml;

+      this.responseJSON = this._getResponseJSON();

+    }

+  },

+

+  status:      0,

+  statusText: '',

+

+  getStatus: Ajax.Request.prototype.getStatus,

+

+  getStatusText: function() {

+    try {

+      return this.transport.statusText || '';

+    } catch (e) { return '' }

+  },

+

+  getHeader: Ajax.Request.prototype.getHeader,

+

+  getAllHeaders: function() {

+    try {

+      return this.getAllResponseHeaders();

+    } catch (e) { return null }

+  },

+

+  getResponseHeader: function(name) {

+    return this.transport.getResponseHeader(name);

+  },

+

+  getAllResponseHeaders: function() {

+    return this.transport.getAllResponseHeaders();

+  },

+

+  _getHeaderJSON: function() {

+    var json = this.getHeader('X-JSON');

+    if (!json) return null;

+    json = decodeURIComponent(escape(json));

+    try {

+      return json.evalJSON(this.request.options.sanitizeJSON ||

+        !this.request.isSameOrigin());

+    } catch (e) {

+      this.request.dispatchException(e);

+    }

+  },

+

+  _getResponseJSON: function() {

+    var options = this.request.options;

+    if (!options.evalJSON || (options.evalJSON != 'force' &&

+      !(this.getHeader('Content-type') || '').include('application/json')) ||

+        this.responseText.blank())

+          return null;

+    try {

+      return this.responseText.evalJSON(options.sanitizeJSON ||

+        !this.request.isSameOrigin());

+    } catch (e) {

+      this.request.dispatchException(e);

+    }

+  }

+});

+

+Ajax.Updater = Class.create(Ajax.Request, {

+  initialize: function($super, container, url, options) {

+    this.container = {

+      success: (container.success || container),

+      failure: (container.failure || (container.success ? null : container))

+    };

+

+    options = Object.clone(options);

+    var onComplete = options.onComplete;

+    options.onComplete = (function(response, json) {

+      this.updateContent(response.responseText);

+      if (Object.isFunction(onComplete)) onComplete(response, json);

+    }).bind(this);

+

+    $super(url, options);

+  },

+

+  updateContent: function(responseText) {

+    var receiver = this.container[this.success() ? 'success' : 'failure'],

+        options = this.options;

+

+    if (!options.evalScripts) responseText = responseText.stripScripts();

+

+    if (receiver = $(receiver)) {

+      if (options.insertion) {

+        if (Object.isString(options.insertion)) {

+          var insertion = { }; insertion[options.insertion] = responseText;

+          receiver.insert(insertion);

+        }

+        else options.insertion(receiver, responseText);

+      }

+      else receiver.update(responseText);

+    }

+  }

+});

+

+Ajax.PeriodicalUpdater = Class.create(Ajax.Base, {

+  initialize: function($super, container, url, options) {

+    $super(options);

+    this.onComplete = this.options.onComplete;

+

+    this.frequency = (this.options.frequency || 2);

+    this.decay = (this.options.decay || 1);

+

+    this.updater = { };

+    this.container = container;

+    this.url = url;

+

+    this.start();

+  },

+

+  start: function() {

+    this.options.onComplete = this.updateComplete.bind(this);

+    this.onTimerEvent();

+  },

+

+  stop: function() {

+    this.updater.options.onComplete = undefined;

+    clearTimeout(this.timer);

+    (this.onComplete || Prototype.emptyFunction).apply(this, arguments);

+  },

+

+  updateComplete: function(response) {

+    if (this.options.decay) {

+      this.decay = (response.responseText == this.lastText ?

+        this.decay * this.options.decay : 1);

+

+      this.lastText = response.responseText;

+    }

+    this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency);

+  },

+

+  onTimerEvent: function() {

+    this.updater = new Ajax.Updater(this.container, this.url, this.options);

+  }

+});

+function $(element) {

+  if (arguments.length > 1) {

+    for (var i = 0, elements = [], length = arguments.length; i < length; i++)

+      elements.push($(arguments[i]));

+    return elements;

+  }

+  if (Object.isString(element))

+    element = document.getElementById(element);

+  return Element.extend(element);

+}

+

+if (Prototype.BrowserFeatures.XPath) {

+  document._getElementsByXPath = function(expression, parentElement) {

+    var results = [];

+    var query = document.evaluate(expression, $(parentElement) || document,

+      null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

+    for (var i = 0, length = query.snapshotLength; i < length; i++)

+      results.push(Element.extend(query.snapshotItem(i)));

+    return results;

+  };

+}

+

+/*--------------------------------------------------------------------------*/

+

+if (!window.Node) var Node = { };

+

+if (!Node.ELEMENT_NODE) {

+  // DOM level 2 ECMAScript Language Binding

+  Object.extend(Node, {

+    ELEMENT_NODE: 1,

+    ATTRIBUTE_NODE: 2,

+    TEXT_NODE: 3,

+    CDATA_SECTION_NODE: 4,

+    ENTITY_REFERENCE_NODE: 5,

+    ENTITY_NODE: 6,

+    PROCESSING_INSTRUCTION_NODE: 7,

+    COMMENT_NODE: 8,

+    DOCUMENT_NODE: 9,

+    DOCUMENT_TYPE_NODE: 10,

+    DOCUMENT_FRAGMENT_NODE: 11,

+    NOTATION_NODE: 12

+  });

+}

+

+(function() {

+  var element = this.Element;

+  this.Element = function(tagName, attributes) {

+    attributes = attributes || { };

+    tagName = tagName.toLowerCase();

+    var cache = Element.cache;

+    if (Prototype.Browser.IE && attributes.name) {

+      tagName = '<' + tagName + ' name="' + attributes.name + '">';

+      delete attributes.name;

+      return Element.writeAttribute(document.createElement(tagName), attributes);

+    }

+    if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName));

+    return Element.writeAttribute(cache[tagName].cloneNode(false), attributes);

+  };

+  Object.extend(this.Element, element || { });

+}).call(window);

+

+Element.cache = { };

+

+Element.Methods = {

+  visible: function(element) {

+    return $(element).style.display != 'none';

+  },

+

+  toggle: function(element) {

+    element = $(element);

+    Element[Element.visible(element) ? 'hide' : 'show'](element);

+    return element;

+  },

+

+  hide: function(element) {

+    $(element).style.display = 'none';

+    return element;

+  },

+

+  show: function(element) {

+    $(element).style.display = '';

+    return element;

+  },

+

+  remove: function(element) {

+    element = $(element);

+    element.parentNode.removeChild(element);

+    return element;

+  },

+

+  update: function(element, content) {

+    element = $(element);

+    if (content && content.toElement) content = content.toElement();

+    if (Object.isElement(content)) return element.update().insert(content);

+    content = Object.toHTML(content);

+    element.innerHTML = content.stripScripts();

+    content.evalScripts.bind(content).defer();

+    return element;

+  },

+

+  replace: function(element, content) {

+    element = $(element);

+    if (content && content.toElement) content = content.toElement();

+    else if (!Object.isElement(content)) {

+      content = Object.toHTML(content);

+      var range = element.ownerDocument.createRange();

+      range.selectNode(element);

+      content.evalScripts.bind(content).defer();

+      content = range.createContextualFragment(content.stripScripts());

+    }

+    element.parentNode.replaceChild(content, element);

+    return element;

+  },

+

+  insert: function(element, insertions) {

+    element = $(element);

+

+    if (Object.isString(insertions) || Object.isNumber(insertions) ||

+        Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML)))

+          insertions = {bottom:insertions};

+

+    var content, insert, tagName, childNodes;

+

+    for (var position in insertions) {

+      content  = insertions[position];

+      position = position.toLowerCase();

+      insert = Element._insertionTranslations[position];

+

+      if (content && content.toElement) content = content.toElement();

+      if (Object.isElement(content)) {

+        insert(element, content);

+        continue;

+      }

+

+      content = Object.toHTML(content);

+

+      tagName = ((position == 'before' || position == 'after')

+        ? element.parentNode : element).tagName.toUpperCase();

+

+      childNodes = Element._getContentFromAnonymousElement(tagName, content.stripScripts());

+

+      if (position == 'top' || position == 'after') childNodes.reverse();

+      childNodes.each(insert.curry(element));

+

+      content.evalScripts.bind(content).defer();

+    }

+

+    return element;

+  },

+

+  wrap: function(element, wrapper, attributes) {

+    element = $(element);

+    if (Object.isElement(wrapper))

+      $(wrapper).writeAttribute(attributes || { });

+    else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes);

+    else wrapper = new Element('div', wrapper);

+    if (element.parentNode)

+      element.parentNode.replaceChild(wrapper, element);

+    wrapper.appendChild(element);

+    return wrapper;

+  },

+

+  inspect: function(element) {

+    element = $(element);

+    var result = '<' + element.tagName.toLowerCase();

+    $H({'id': 'id', 'className': 'class'}).each(function(pair) {

+      var property = pair.first(), attribute = pair.last();

+      var value = (element[property] || '').toString();

+      if (value) result += ' ' + attribute + '=' + value.inspect(true);

+    });

+    return result + '>';

+  },

+

+  recursivelyCollect: function(element, property) {

+    element = $(element);

+    var elements = [];

+    while (element = element[property])

+      if (element.nodeType == 1)

+        elements.push(Element.extend(element));

+    return elements;

+  },

+

+  ancestors: function(element) {

+    return $(element).recursivelyCollect('parentNode');

+  },

+

+  descendants: function(element) {

+    return $(element).select("*");

+  },

+

+  firstDescendant: function(element) {

+    element = $(element).firstChild;

+    while (element && element.nodeType != 1) element = element.nextSibling;

+    return $(element);

+  },

+

+  immediateDescendants: function(element) {

+    if (!(element = $(element).firstChild)) return [];

+    while (element && element.nodeType != 1) element = element.nextSibling;

+    if (element) return [element].concat($(element).nextSiblings());

+    return [];

+  },

+

+  previousSiblings: function(element) {

+    return $(element).recursivelyCollect('previousSibling');

+  },

+

+  nextSiblings: function(element) {

+    return $(element).recursivelyCollect('nextSibling');

+  },

+

+  siblings: function(element) {

+    element = $(element);

+    return element.previousSiblings().reverse().concat(element.nextSiblings());

+  },

+

+  match: function(element, selector) {

+    if (Object.isString(selector))

+      selector = new Selector(selector);

+    return selector.match($(element));

+  },

+

+  up: function(element, expression, index) {

+    element = $(element);

+    if (arguments.length == 1) return $(element.parentNode);

+    var ancestors = element.ancestors();

+    return Object.isNumber(expression) ? ancestors[expression] :

+      Selector.findElement(ancestors, expression, index);

+  },

+

+  down: function(element, expression, index) {

+    element = $(element);

+    if (arguments.length == 1) return element.firstDescendant();

+    return Object.isNumber(expression) ? element.descendants()[expression] :

+      element.select(expression)[index || 0];

+  },

+

+  previous: function(element, expression, index) {

+    element = $(element);

+    if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element));

+    var previousSiblings = element.previousSiblings();

+    return Object.isNumber(expression) ? previousSiblings[expression] :

+      Selector.findElement(previousSiblings, expression, index);

+  },

+

+  next: function(element, expression, index) {

+    element = $(element);

+    if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element));

+    var nextSiblings = element.nextSiblings();

+    return Object.isNumber(expression) ? nextSiblings[expression] :

+      Selector.findElement(nextSiblings, expression, index);

+  },

+

+  select: function() {

+    var args = $A(arguments), element = $(args.shift());

+    return Selector.findChildElements(element, args);

+  },

+

+  adjacent: function() {

+    var args = $A(arguments), element = $(args.shift());

+    return Selector.findChildElements(element.parentNode, args).without(element);

+  },

+

+  identify: function(element) {

+    element = $(element);

+    var id = element.readAttribute('id'), self = arguments.callee;

+    if (id) return id;

+    do { id = 'anonymous_element_' + self.counter++ } while ($(id));

+    element.writeAttribute('id', id);

+    return id;

+  },

+

+  readAttribute: function(element, name) {

+    element = $(element);

+    if (Prototype.Browser.IE) {

+      var t = Element._attributeTranslations.read;

+      if (t.values[name]) return t.values[name](element, name);

+      if (t.names[name]) name = t.names[name];

+      if (name.include(':')) {

+        return (!element.attributes || !element.attributes[name]) ? null :

+         element.attributes[name].value;

+      }

+    }

+    return element.getAttribute(name);

+  },

+

+  writeAttribute: function(element, name, value) {

+    element = $(element);

+    var attributes = { }, t = Element._attributeTranslations.write;

+

+    if (typeof name == 'object') attributes = name;

+    else attributes[name] = Object.isUndefined(value) ? true : value;

+

+    for (var attr in attributes) {

+      name = t.names[attr] || attr;

+      value = attributes[attr];

+      if (t.values[attr]) name = t.values[attr](element, value);

+      if (value === false || value === null)

+        element.removeAttribute(name);

+      else if (value === true)

+        element.setAttribute(name, name);

+      else element.setAttribute(name, value);

+    }

+    return element;

+  },

+

+  getHeight: function(element) {

+    return $(element).getDimensions().height;

+  },

+

+  getWidth: function(element) {

+    return $(element).getDimensions().width;

+  },

+

+  classNames: function(element) {

+    return new Element.ClassNames(element);

+  },

+

+  hasClassName: function(element, className) {

+    if (!(element = $(element))) return;

+    var elementClassName = element.className;

+    return (elementClassName.length > 0 && (elementClassName == className ||

+      new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName)));

+  },

+

+  addClassName: function(element, className) {

+    if (!(element = $(element))) return;

+    if (!element.hasClassName(className))

+      element.className += (element.className ? ' ' : '') + className;

+    return element;

+  },

+

+  removeClassName: function(element, className) {

+    if (!(element = $(element))) return;

+    element.className = element.className.replace(

+      new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip();

+    return element;

+  },

+

+  toggleClassName: function(element, className) {

+    if (!(element = $(element))) return;

+    return element[element.hasClassName(className) ?

+      'removeClassName' : 'addClassName'](className);

+  },

+

+  // removes whitespace-only text node children

+  cleanWhitespace: function(element) {

+    element = $(element);

+    var node = element.firstChild;

+    while (node) {

+      var nextNode = node.nextSibling;

+      if (node.nodeType == 3 && !/\S/.test(node.nodeValue))

+        element.removeChild(node);

+      node = nextNode;

+    }

+    return element;

+  },

+

+  empty: function(element) {

+    return $(element).innerHTML.blank();

+  },

+

+  descendantOf: function(element, ancestor) {

+    element = $(element), ancestor = $(ancestor);

+    var originalAncestor = ancestor;

+

+    if (element.compareDocumentPosition)

+      return (element.compareDocumentPosition(ancestor) & 8) === 8;

+

+    if (element.sourceIndex && !Prototype.Browser.Opera) {

+      var e = element.sourceIndex, a = ancestor.sourceIndex,

+       nextAncestor = ancestor.nextSibling;

+      if (!nextAncestor) {

+        do { ancestor = ancestor.parentNode; }

+        while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode);

+      }

+      if (nextAncestor && nextAncestor.sourceIndex)

+       return (e > a && e < nextAncestor.sourceIndex);

+    }

+

+    while (element = element.parentNode)

+      if (element == originalAncestor) return true;

+    return false;

+  },

+

+  scrollTo: function(element) {

+    element = $(element);

+    var pos = element.cumulativeOffset();

+    window.scrollTo(pos[0], pos[1]);

+    return element;

+  },

+

+  getStyle: function(element, style) {

+    element = $(element);

+    style = style == 'float' ? 'cssFloat' : style.camelize();

+    var value = element.style[style];

+    if (!value) {

+      var css = document.defaultView.getComputedStyle(element, null);

+      value = css ? css[style] : null;

+    }

+    if (style == 'opacity') return value ? parseFloat(value) : 1.0;

+    return value == 'auto' ? null : value;

+  },

+

+  getOpacity: function(element) {

+    return $(element).getStyle('opacity');

+  },

+

+  setStyle: function(element, styles) {

+    element = $(element);

+    var elementStyle = element.style, match;

+    if (Object.isString(styles)) {

+      element.style.cssText += ';' + styles;

+      return styles.include('opacity') ?

+        element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element;

+    }

+    for (var property in styles)

+      if (property == 'opacity') element.setOpacity(styles[property]);

+      else

+        elementStyle[(property == 'float' || property == 'cssFloat') ?

+          (Object.isUndefined(elementStyle.styleFloat) ? 'cssFloat' : 'styleFloat') :

+            property] = styles[property];

+

+    return element;

+  },

+

+  setOpacity: function(element, value) {

+    element = $(element);

+    element.style.opacity = (value == 1 || value === '') ? '' :

+      (value < 0.00001) ? 0 : value;

+    return element;

+  },

+

+  getDimensions: function(element) {

+    element = $(element);

+    var display = $(element).getStyle('display');

+    if (display != 'none' && display != null) // Safari bug

+      return {width: element.offsetWidth, height: element.offsetHeight};

+

+    // All *Width and *Height properties give 0 on elements with display none,

+    // so enable the element temporarily

+    var els = element.style;

+    var originalVisibility = els.visibility;

+    var originalPosition = els.position;

+    var originalDisplay = els.display;

+    els.visibility = 'hidden';

+    els.position = 'absolute';

+    els.display = 'block';

+    var originalWidth = element.clientWidth;

+    var originalHeight = element.clientHeight;

+    els.display = originalDisplay;

+    els.position = originalPosition;

+    els.visibility = originalVisibility;

+    return {width: originalWidth, height: originalHeight};

+  },

+

+  makePositioned: function(element) {

+    element = $(element);

+    var pos = Element.getStyle(element, 'position');

+    if (pos == 'static' || !pos) {

+      element._madePositioned = true;

+      element.style.position = 'relative';

+      // Opera returns the offset relative to the positioning context, when an

+      // element is position relative but top and left have not been defined

+      if (window.opera) {

+        element.style.top = 0;

+        element.style.left = 0;

+      }

+    }

+    return element;

+  },

+

+  undoPositioned: function(element) {

+    element = $(element);

+    if (element._madePositioned) {

+      element._madePositioned = undefined;

+      element.style.position =

+        element.style.top =

+        element.style.left =

+        element.style.bottom =

+        element.style.right = '';

+    }

+    return element;

+  },

+

+  makeClipping: function(element) {

+    element = $(element);

+    if (element._overflow) return element;

+    element._overflow = Element.getStyle(element, 'overflow') || 'auto';

+    if (element._overflow !== 'hidden')

+      element.style.overflow = 'hidden';

+    return element;

+  },

+

+  undoClipping: function(element) {

+    element = $(element);

+    if (!element._overflow) return element;

+    element.style.overflow = element._overflow == 'auto' ? '' : element._overflow;

+    element._overflow = null;

+    return element;

+  },

+

+  cumulativeOffset: function(element) {

+    var valueT = 0, valueL = 0;

+    do {

+      valueT += element.offsetTop  || 0;

+      valueL += element.offsetLeft || 0;

+      element = element.offsetParent;

+    } while (element);

+    return Element._returnOffset(valueL, valueT);

+  },

+

+  positionedOffset: function(element) {

+    var valueT = 0, valueL = 0;

+    do {

+      valueT += element.offsetTop  || 0;

+      valueL += element.offsetLeft || 0;

+      element = element.offsetParent;

+      if (element) {

+        if (element.tagName == 'BODY') break;

+        var p = Element.getStyle(element, 'position');

+        if (p !== 'static') break;

+      }

+    } while (element);

+    return Element._returnOffset(valueL, valueT);

+  },

+

+  absolutize: function(element) {

+    element = $(element);

+    if (element.getStyle('position') == 'absolute') return;

+    // Position.prepare(); // To be done manually by Scripty when it needs it.

+

+    var offsets = element.positionedOffset();

+    var top     = offsets[1];

+    var left    = offsets[0];

+    var width   = element.clientWidth;

+    var height  = element.clientHeight;

+

+    element._originalLeft   = left - parseFloat(element.style.left  || 0);

+    element._originalTop    = top  - parseFloat(element.style.top || 0);

+    element._originalWidth  = element.style.width;

+    element._originalHeight = element.style.height;

+

+    element.style.position = 'absolute';

+    element.style.top    = top + 'px';

+    element.style.left   = left + 'px';

+    element.style.width  = width + 'px';

+    element.style.height = height + 'px';

+    return element;

+  },

+

+  relativize: function(element) {

+    element = $(element);

+    if (element.getStyle('position') == 'relative') return;

+    // Position.prepare(); // To be done manually by Scripty when it needs it.

+

+    element.style.position = 'relative';

+    var top  = parseFloat(element.style.top  || 0) - (element._originalTop || 0);

+    var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0);

+

+    element.style.top    = top + 'px';

+    element.style.left   = left + 'px';

+    element.style.height = element._originalHeight;

+    element.style.width  = element._originalWidth;

+    return element;

+  },

+

+  cumulativeScrollOffset: function(element) {

+    var valueT = 0, valueL = 0;

+    do {

+      valueT += element.scrollTop  || 0;

+      valueL += element.scrollLeft || 0;

+      element = element.parentNode;

+    } while (element);

+    return Element._returnOffset(valueL, valueT);

+  },

+

+  getOffsetParent: function(element) {

+    if (element.offsetParent) return $(element.offsetParent);

+    if (element == document.body) return $(element);

+

+    while ((element = element.parentNode) && element != document.body)

+      if (Element.getStyle(element, 'position') != 'static')

+        return $(element);

+

+    return $(document.body);

+  },

+

+  viewportOffset: function(forElement) {

+    var valueT = 0, valueL = 0;

+

+    var element = forElement;

+    do {

+      valueT += element.offsetTop  || 0;

+      valueL += element.offsetLeft || 0;

+

+      // Safari fix

+      if (element.offsetParent == document.body &&

+        Element.getStyle(element, 'position') == 'absolute') break;

+

+    } while (element = element.offsetParent);

+

+    element = forElement;

+    do {

+      if (!Prototype.Browser.Opera || element.tagName == 'BODY') {

+        valueT -= element.scrollTop  || 0;

+        valueL -= element.scrollLeft || 0;

+      }

+    } while (element = element.parentNode);

+

+    return Element._returnOffset(valueL, valueT);

+  },

+

+  clonePosition: function(element, source) {

+    var options = Object.extend({

+      setLeft:    true,

+      setTop:     true,

+      setWidth:   true,

+      setHeight:  true,

+      offsetTop:  0,

+      offsetLeft: 0

+    }, arguments[2] || { });

+

+    // find page position of source

+    source = $(source);

+    var p = source.viewportOffset();

+

+    // find coordinate system to use

+    element = $(element);

+    var delta = [0, 0];

+    var parent = null;

+    // delta [0,0] will do fine with position: fixed elements,

+    // position:absolute needs offsetParent deltas

+    if (Element.getStyle(element, 'position') == 'absolute') {

+      parent = element.getOffsetParent();

+      delta = parent.viewportOffset();

+    }

+

+    // correct by body offsets (fixes Safari)

+    if (parent == document.body) {

+      delta[0] -= document.body.offsetLeft;

+      delta[1] -= document.body.offsetTop;

+    }

+

+    // set position

+    if (options.setLeft)   element.style.left  = (p[0] - delta[0] + options.offsetLeft) + 'px';

+    if (options.setTop)    element.style.top   = (p[1] - delta[1] + options.offsetTop) + 'px';

+    if (options.setWidth)  element.style.width = source.offsetWidth + 'px';

+    if (options.setHeight) element.style.height = source.offsetHeight + 'px';

+    return element;

+  }

+};

+

+Element.Methods.identify.counter = 1;

+

+Object.extend(Element.Methods, {

+  getElementsBySelector: Element.Methods.select,

+  childElements: Element.Methods.immediateDescendants

+});

+

+Element._attributeTranslations = {

+  write: {

+    names: {

+      className: 'class',

+      htmlFor:   'for'

+    },

+    values: { }

+  }

+};

+

+if (Prototype.Browser.Opera) {

+  Element.Methods.getStyle = Element.Methods.getStyle.wrap(

+    function(proceed, element, style) {

+      switch (style) {

+        case 'left': case 'top': case 'right': case 'bottom':

+          if (proceed(element, 'position') === 'static') return null;

+        case 'height': case 'width':

+          // returns '0px' for hidden elements; we want it to return null

+          if (!Element.visible(element)) return null;

+

+          // returns the border-box dimensions rather than the content-box

+          // dimensions, so we subtract padding and borders from the value

+          var dim = parseInt(proceed(element, style), 10);

+

+          if (dim !== element['offset' + style.capitalize()])

+            return dim + 'px';

+

+          var properties;

+          if (style === 'height') {

+            properties = ['border-top-width', 'padding-top',

+             'padding-bottom', 'border-bottom-width'];

+          }

+          else {

+            properties = ['border-left-width', 'padding-left',

+             'padding-right', 'border-right-width'];

+          }

+          return properties.inject(dim, function(memo, property) {

+            var val = proceed(element, property);

+            return val === null ? memo : memo - parseInt(val, 10);

+          }) + 'px';

+        default: return proceed(element, style);

+      }

+    }

+  );

+

+  Element.Methods.readAttribute = Element.Methods.readAttribute.wrap(

+    function(proceed, element, attribute) {

+      if (attribute === 'title') return element.title;

+      return proceed(element, attribute);

+    }

+  );

+}

+

+else if (Prototype.Browser.IE) {

+  // IE doesn't report offsets correctly for static elements, so we change them

+  // to "relative" to get the values, then change them back.

+  Element.Methods.getOffsetParent = Element.Methods.getOffsetParent.wrap(

+    function(proceed, element) {

+      element = $(element);

+      var position = element.getStyle('position');

+      if (position !== 'static') return proceed(element);

+      element.setStyle({ position: 'relative' });

+      var value = proceed(element);

+      element.setStyle({ position: position });

+      return value;

+    }

+  );

+

+  $w('positionedOffset viewportOffset').each(function(method) {

+    Element.Methods[method] = Element.Methods[method].wrap(

+      function(proceed, element) {

+        element = $(element);

+        var position = element.getStyle('position');

+        if (position !== 'static') return proceed(element);

+        // Trigger hasLayout on the offset parent so that IE6 reports

+        // accurate offsetTop and offsetLeft values for position: fixed.

+        var offsetParent = element.getOffsetParent();

+        if (offsetParent && offsetParent.getStyle('position') === 'fixed')

+          offsetParent.setStyle({ zoom: 1 });

+        element.setStyle({ position: 'relative' });

+        var value = proceed(element);

+        element.setStyle({ position: position });

+        return value;

+      }

+    );

+  });

+

+  Element.Methods.getStyle = function(element, style) {

+    element = $(element);

+    style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize();

+    var value = element.style[style];

+    if (!value && element.currentStyle) value = element.currentStyle[style];

+

+    if (style == 'opacity') {

+      if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/))

+        if (value[1]) return parseFloat(value[1]) / 100;

+      return 1.0;

+    }

+

+    if (value == 'auto') {

+      if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none'))

+        return element['offset' + style.capitalize()] + 'px';

+      return null;

+    }

+    return value;

+  };

+

+  Element.Methods.setOpacity = function(element, value) {

+    function stripAlpha(filter){

+      return filter.replace(/alpha\([^\)]*\)/gi,'');

+    }

+    element = $(element);

+    var currentStyle = element.currentStyle;

+    if ((currentStyle && !currentStyle.hasLayout) ||

+      (!currentStyle && element.style.zoom == 'normal'))

+        element.style.zoom = 1;

+

+    var filter = element.getStyle('filter'), style = element.style;

+    if (value == 1 || value === '') {

+      (filter = stripAlpha(filter)) ?

+        style.filter = filter : style.removeAttribute('filter');

+      return element;

+    } else if (value < 0.00001) value = 0;

+    style.filter = stripAlpha(filter) +

+      'alpha(opacity=' + (value * 100) + ')';

+    return element;

+  };

+

+  Element._attributeTranslations = {

+    read: {

+      names: {

+        'class': 'className',

+        'for':   'htmlFor'

+      },

+      values: {

+        _getAttr: function(element, attribute) {

+          return element.getAttribute(attribute, 2);

+        },

+        _getAttrNode: function(element, attribute) {

+          var node = element.getAttributeNode(attribute);

+          return node ? node.value : "";

+        },

+        _getEv: function(element, attribute) {

+          attribute = element.getAttribute(attribute);

+          return attribute ? attribute.toString().slice(23, -2) : null;

+        },

+        _flag: function(element, attribute) {

+          return $(element).hasAttribute(attribute) ? attribute : null;

+        },

+        style: function(element) {

+          return element.style.cssText.toLowerCase();

+        },

+        title: function(element) {

+          return element.title;

+        }

+      }

+    }

+  };

+

+  Element._attributeTranslations.write = {

+    names: Object.extend({

+      cellpadding: 'cellPadding',

+      cellspacing: 'cellSpacing'

+    }, Element._attributeTranslations.read.names),

+    values: {

+      checked: function(element, value) {

+        element.checked = !!value;

+      },

+

+      style: function(element, value) {

+        element.style.cssText = value ? value : '';

+      }

+    }

+  };

+

+  Element._attributeTranslations.has = {};

+

+  $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' +

+      'encType maxLength readOnly longDesc').each(function(attr) {

+    Element._attributeTranslations.write.names[attr.toLowerCase()] = attr;

+    Element._attributeTranslations.has[attr.toLowerCase()] = attr;

+  });

+

+  (function(v) {

+    Object.extend(v, {

+      href:        v._getAttr,

+      src:         v._getAttr,

+      type:        v._getAttr,

+      action:      v._getAttrNode,

+      disabled:    v._flag,

+      checked:     v._flag,

+      readonly:    v._flag,

+      multiple:    v._flag,

+      onload:      v._getEv,

+      onunload:    v._getEv,

+      onclick:     v._getEv,

+      ondblclick:  v._getEv,

+      onmousedown: v._getEv,

+      onmouseup:   v._getEv,

+      onmouseover: v._getEv,

+      onmousemove: v._getEv,

+      onmouseout:  v._getEv,

+      onfocus:     v._getEv,

+      onblur:      v._getEv,

+      onkeypress:  v._getEv,

+      onkeydown:   v._getEv,

+      onkeyup:     v._getEv,

+      onsubmit:    v._getEv,

+      onreset:     v._getEv,

+      onselect:    v._getEv,

+      onchange:    v._getEv

+    });

+  })(Element._attributeTranslations.read.values);

+}

+

+else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) {

+  Element.Methods.setOpacity = function(element, value) {

+    element = $(element);

+    element.style.opacity = (value == 1) ? 0.999999 :

+      (value === '') ? '' : (value < 0.00001) ? 0 : value;

+    return element;

+  };

+}

+

+else if (Prototype.Browser.WebKit) {

+  Element.Methods.setOpacity = function(element, value) {

+    element = $(element);

+    element.style.opacity = (value == 1 || value === '') ? '' :

+      (value < 0.00001) ? 0 : value;

+

+    if (value == 1)

+      if(element.tagName == 'IMG' && element.width) {

+        element.width++; element.width--;

+      } else try {

+        var n = document.createTextNode(' ');

+        element.appendChild(n);

+        element.removeChild(n);

+      } catch (e) { }

+

+    return element;

+  };

+

+  // Safari returns margins on body which is incorrect if the child is absolutely

+  // positioned.  For performance reasons, redefine Element#cumulativeOffset for

+  // KHTML/WebKit only.

+  Element.Methods.cumulativeOffset = function(element) {

+    var valueT = 0, valueL = 0;

+    do {

+      valueT += element.offsetTop  || 0;

+      valueL += element.offsetLeft || 0;

+      if (element.offsetParent == document.body)

+        if (Element.getStyle(element, 'position') == 'absolute') break;

+

+      element = element.offsetParent;

+    } while (element);

+

+    return Element._returnOffset(valueL, valueT);

+  };

+}

+

+if (Prototype.Browser.IE || Prototype.Browser.Opera) {

+  // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements

+  Element.Methods.update = function(element, content) {

+    element = $(element);

+

+    if (content && content.toElement) content = content.toElement();

+    if (Object.isElement(content)) return element.update().insert(content);

+

+    content = Object.toHTML(content);

+    var tagName = element.tagName.toUpperCase();

+

+    if (tagName in Element._insertionTranslations.tags) {

+      $A(element.childNodes).each(function(node) { element.removeChild(node) });

+      Element._getContentFromAnonymousElement(tagName, content.stripScripts())

+        .each(function(node) { element.appendChild(node) });

+    }

+    else element.innerHTML = content.stripScripts();

+

+    content.evalScripts.bind(content).defer();

+    return element;

+  };

+}

+

+if ('outerHTML' in document.createElement('div')) {

+  Element.Methods.replace = function(element, content) {

+    element = $(element);

+

+    if (content && content.toElement) content = content.toElement();

+    if (Object.isElement(content)) {

+      element.parentNode.replaceChild(content, element);

+      return element;

+    }

+

+    content = Object.toHTML(content);

+    var parent = element.parentNode, tagName = parent.tagName.toUpperCase();

+

+    if (Element._insertionTranslations.tags[tagName]) {

+      var nextSibling = element.next();

+      var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts());

+      parent.removeChild(element);

+      if (nextSibling)

+        fragments.each(function(node) { parent.insertBefore(node, nextSibling) });

+      else

+        fragments.each(function(node) { parent.appendChild(node) });

+    }

+    else element.outerHTML = content.stripScripts();

+

+    content.evalScripts.bind(content).defer();

+    return element;

+  };

+}

+

+Element._returnOffset = function(l, t) {

+  var result = [l, t];

+  result.left = l;

+  result.top = t;

+  return result;

+};

+

+Element._getContentFromAnonymousElement = function(tagName, html) {

+  var div = new Element('div'), t = Element._insertionTranslations.tags[tagName];

+  if (t) {

+    div.innerHTML = t[0] + html + t[1];

+    t[2].times(function() { div = div.firstChild });

+  } else div.innerHTML = html;

+  return $A(div.childNodes);

+};

+

+Element._insertionTranslations = {

+  before: function(element, node) {

+    element.parentNode.insertBefore(node, element);

+  },

+  top: function(element, node) {

+    element.insertBefore(node, element.firstChild);

+  },

+  bottom: function(element, node) {

+    element.appendChild(node);

+  },

+  after: function(element, node) {

+    element.parentNode.insertBefore(node, element.nextSibling);

+  },

+  tags: {

+    TABLE:  ['<table>',                '</table>',                   1],

+    TBODY:  ['<table><tbody>',         '</tbody></table>',           2],

+    TR:     ['<table><tbody><tr>',     '</tr></tbody></table>',      3],

+    TD:     ['<table><tbody><tr><td>', '</td></tr></tbody></table>', 4],

+    SELECT: ['<select>',               '</select>',                  1]

+  }

+};

+

+(function() {

+  Object.extend(this.tags, {

+    THEAD: this.tags.TBODY,

+    TFOOT: this.tags.TBODY,

+    TH:    this.tags.TD

+  });

+}).call(Element._insertionTranslations);

+

+Element.Methods.Simulated = {

+  hasAttribute: function(element, attribute) {

+    attribute = Element._attributeTranslations.has[attribute] || attribute;

+    var node = $(element).getAttributeNode(attribute);

+    return node && node.specified;

+  }

+};

+

+Element.Methods.ByTag = { };

+

+Object.extend(Element, Element.Methods);

+

+if (!Prototype.BrowserFeatures.ElementExtensions &&

+    document.createElement('div').__proto__) {

+  window.HTMLElement = { };

+  window.HTMLElement.prototype = document.createElement('div').__proto__;

+  Prototype.BrowserFeatures.ElementExtensions = true;

+}

+

+Element.extend = (function() {

+  if (Prototype.BrowserFeatures.SpecificElementExtensions)

+    return Prototype.K;

+

+  var Methods = { }, ByTag = Element.Methods.ByTag;

+

+  var extend = Object.extend(function(element) {

+    if (!element || element._extendedByPrototype ||

+        element.nodeType != 1 || element == window) return element;

+

+    var methods = Object.clone(Methods),

+      tagName = element.tagName, property, value;

+

+    // extend methods for specific tags

+    if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]);

+

+    for (property in methods) {

+      value = methods[property];

+      if (Object.isFunction(value) && !(property in element))

+        element[property] = value.methodize();

+    }

+

+    element._extendedByPrototype = Prototype.emptyFunction;

+    return element;

+

+  }, {

+    refresh: function() {

+      // extend methods for all tags (Safari doesn't need this)

+      if (!Prototype.BrowserFeatures.ElementExtensions) {

+        Object.extend(Methods, Element.Methods);

+        Object.extend(Methods, Element.Methods.Simulated);

+      }

+    }

+  });

+

+  extend.refresh();

+  return extend;

+})();

+

+Element.hasAttribute = function(element, attribute) {

+  if (element.hasAttribute) return element.hasAttribute(attribute);

+  return Element.Methods.Simulated.hasAttribute(element, attribute);

+};

+

+Element.addMethods = function(methods) {

+  var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag;

+

+  if (!methods) {

+    Object.extend(Form, Form.Methods);

+    Object.extend(Form.Element, Form.Element.Methods);

+    Object.extend(Element.Methods.ByTag, {

+      "FORM":     Object.clone(Form.Methods),

+      "INPUT":    Object.clone(Form.Element.Methods),

+      "SELECT":   Object.clone(Form.Element.Methods),

+      "TEXTAREA": Object.clone(Form.Element.Methods)

+    });

+  }

+

+  if (arguments.length == 2) {

+    var tagName = methods;

+    methods = arguments[1];

+  }

+

+  if (!tagName) Object.extend(Element.Methods, methods || { });

+  else {

+    if (Object.isArray(tagName)) tagName.each(extend);

+    else extend(tagName);

+  }

+

+  function extend(tagName) {

+    tagName = tagName.toUpperCase();

+    if (!Element.Methods.ByTag[tagName])

+      Element.Methods.ByTag[tagName] = { };

+    Object.extend(Element.Methods.ByTag[tagName], methods);

+  }

+

+  function copy(methods, destination, onlyIfAbsent) {

+    onlyIfAbsent = onlyIfAbsent || false;

+    for (var property in methods) {

+      var value = methods[property];

+      if (!Object.isFunction(value)) continue;

+      if (!onlyIfAbsent || !(property in destination))

+        destination[property] = value.methodize();

+    }

+  }

+

+  function findDOMClass(tagName) {

+    var klass;

+    var trans = {

+      "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph",

+      "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList",

+      "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading",

+      "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote",

+      "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION":

+      "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD":

+      "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR":

+      "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET":

+      "FrameSet", "IFRAME": "IFrame"

+    };

+    if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element';

+    if (window[klass]) return window[klass];

+    klass = 'HTML' + tagName + 'Element';

+    if (window[klass]) return window[klass];

+    klass = 'HTML' + tagName.capitalize() + 'Element';

+    if (window[klass]) return window[klass];

+

+    window[klass] = { };

+    window[klass].prototype = document.createElement(tagName).__proto__;

+    return window[klass];

+  }

+

+  if (F.ElementExtensions) {

+    copy(Element.Methods, HTMLElement.prototype);

+    copy(Element.Methods.Simulated, HTMLElement.prototype, true);

+  }

+

+  if (F.SpecificElementExtensions) {

+    for (var tag in Element.Methods.ByTag) {

+      var klass = findDOMClass(tag);

+      if (Object.isUndefined(klass)) continue;

+      copy(T[tag], klass.prototype);

+    }

+  }

+

+  Object.extend(Element, Element.Methods);

+  delete Element.ByTag;

+

+  if (Element.extend.refresh) Element.extend.refresh();

+  Element.cache = { };

+};

+

+document.viewport = {

+  getDimensions: function() {

+    var dimensions = { };

+    var B = Prototype.Browser;

+    $w('width height').each(function(d) {

+      var D = d.capitalize();

+      dimensions[d] = (B.WebKit && !document.evaluate) ? self['inner' + D] :

+        (B.Opera) ? document.body['client' + D] : document.documentElement['client' + D];

+    });

+    return dimensions;

+  },

+

+  getWidth: function() {

+    return this.getDimensions().width;

+  },

+

+  getHeight: function() {

+    return this.getDimensions().height;

+  },

+

+  getScrollOffsets: function() {

+    return Element._returnOffset(

+      window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft,

+      window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop);

+  }

+};

+/* Portions of the Selector class are derived from Jack Slocum’s DomQuery,

+ * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style

+ * license.  Please see http://www.yui-ext.com/ for more information. */

+

+var Selector = Class.create({

+  initialize: function(expression) {

+    this.expression = expression.strip();

+    this.compileMatcher();

+  },

+

+  shouldUseXPath: function() {

+    if (!Prototype.BrowserFeatures.XPath) return false;

+

+    var e = this.expression;

+

+    // Safari 3 chokes on :*-of-type and :empty

+    if (Prototype.Browser.WebKit &&

+     (e.include("-of-type") || e.include(":empty")))

+      return false;

+

+    // XPath can't do namespaced attributes, nor can it read

+    // the "checked" property from DOM nodes

+    if ((/(\[[\w-]*?:|:checked)/).test(this.expression))

+      return false;

+

+    return true;

+  },

+

+  compileMatcher: function() {

+    if (this.shouldUseXPath())

+      return this.compileXPathMatcher();

+

+    var e = this.expression, ps = Selector.patterns, h = Selector.handlers,

+        c = Selector.criteria, le, p, m;

+

+    if (Selector._cache[e]) {

+      this.matcher = Selector._cache[e];

+      return;

+    }

+

+    this.matcher = ["this.matcher = function(root) {",

+                    "var r = root, h = Selector.handlers, c = false, n;"];

+

+    while (e && le != e && (/\S/).test(e)) {

+      le = e;

+      for (var i in ps) {

+        p = ps[i];

+        if (m = e.match(p)) {

+          this.matcher.push(Object.isFunction(c[i]) ? c[i](m) :

+    	      new Template(c[i]).evaluate(m));

+          e = e.replace(m[0], '');

+          break;

+        }

+      }

+    }

+

+    this.matcher.push("return h.unique(n);\n}");

+    eval(this.matcher.join('\n'));

+    Selector._cache[this.expression] = this.matcher;

+  },

+

+  compileXPathMatcher: function() {

+    var e = this.expression, ps = Selector.patterns,

+        x = Selector.xpath, le, m;

+

+    if (Selector._cache[e]) {

+      this.xpath = Selector._cache[e]; return;

+    }

+

+    this.matcher = ['.//*'];

+    while (e && le != e && (/\S/).test(e)) {

+      le = e;

+      for (var i in ps) {

+        if (m = e.match(ps[i])) {

+          this.matcher.push(Object.isFunction(x[i]) ? x[i](m) :

+            new Template(x[i]).evaluate(m));

+          e = e.replace(m[0], '');

+          break;

+        }

+      }

+    }

+

+    this.xpath = this.matcher.join('');

+    Selector._cache[this.expression] = this.xpath;

+  },

+

+  findElements: function(root) {

+    root = root || document;

+    if (this.xpath) return document._getElementsByXPath(this.xpath, root);

+    return this.matcher(root);

+  },

+

+  match: function(element) {

+    this.tokens = [];

+

+    var e = this.expression, ps = Selector.patterns, as = Selector.assertions;

+    var le, p, m;

+

+    while (e && le !== e && (/\S/).test(e)) {

+      le = e;

+      for (var i in ps) {

+        p = ps[i];

+        if (m = e.match(p)) {

+          // use the Selector.assertions methods unless the selector

+          // is too complex.

+          if (as[i]) {

+            this.tokens.push([i, Object.clone(m)]);

+            e = e.replace(m[0], '');

+          } else {

+            // reluctantly do a document-wide search

+            // and look for a match in the array

+            return this.findElements(document).include(element);

+          }

+        }

+      }

+    }

+

+    var match = true, name, matches;

+    for (var i = 0, token; token = this.tokens[i]; i++) {

+      name = token[0], matches = token[1];

+      if (!Selector.assertions[name](element, matches)) {

+        match = false; break;

+      }

+    }

+

+    return match;

+  },

+

+  toString: function() {

+    return this.expression;

+  },

+

+  inspect: function() {

+    return "#<Selector:" + this.expression.inspect() + ">";

+  }

+});

+

+Object.extend(Selector, {

+  _cache: { },

+

+  xpath: {

+    descendant:   "//*",

+    child:        "/*",

+    adjacent:     "/following-sibling::*[1]",

+    laterSibling: '/following-sibling::*',

+    tagName:      function(m) {

+      if (m[1] == '*') return '';

+      return "[local-name()='" + m[1].toLowerCase() +

+             "' or local-name()='" + m[1].toUpperCase() + "']";

+    },

+    className:    "[contains(concat(' ', @class, ' '), ' #{1} ')]",

+    id:           "[@id='#{1}']",

+    attrPresence: function(m) {

+      m[1] = m[1].toLowerCase();

+      return new Template("[@#{1}]").evaluate(m);

+    },

+    attr: function(m) {

+      m[1] = m[1].toLowerCase();

+      m[3] = m[5] || m[6];

+      return new Template(Selector.xpath.operators[m[2]]).evaluate(m);

+    },

+    pseudo: function(m) {

+      var h = Selector.xpath.pseudos[m[1]];

+      if (!h) return '';

+      if (Object.isFunction(h)) return h(m);

+      return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m);

+    },

+    operators: {

+      '=':  "[@#{1}='#{3}']",

+      '!=': "[@#{1}!='#{3}']",

+      '^=': "[starts-with(@#{1}, '#{3}')]",

+      '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']",

+      '*=': "[contains(@#{1}, '#{3}')]",

+      '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]",

+      '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]"

+    },

+    pseudos: {

+      'first-child': '[not(preceding-sibling::*)]',

+      'last-child':  '[not(following-sibling::*)]',

+      'only-child':  '[not(preceding-sibling::* or following-sibling::*)]',

+      'empty':       "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]",

+      'checked':     "[@checked]",

+      'disabled':    "[@disabled]",

+      'enabled':     "[not(@disabled)]",

+      'not': function(m) {

+        var e = m[6], p = Selector.patterns,

+            x = Selector.xpath, le, v;

+

+        var exclusion = [];

+        while (e && le != e && (/\S/).test(e)) {

+          le = e;

+          for (var i in p) {

+            if (m = e.match(p[i])) {

+              v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m);

+              exclusion.push("(" + v.substring(1, v.length - 1) + ")");

+              e = e.replace(m[0], '');

+              break;

+            }

+          }

+        }

+        return "[not(" + exclusion.join(" and ") + ")]";

+      },

+      'nth-child':      function(m) {

+        return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m);

+      },

+      'nth-last-child': function(m) {

+        return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m);

+      },

+      'nth-of-type':    function(m) {

+        return Selector.xpath.pseudos.nth("position() ", m);

+      },

+      'nth-last-of-type': function(m) {

+        return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m);

+      },

+      'first-of-type':  function(m) {

+        m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m);

+      },

+      'last-of-type':   function(m) {

+        m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m);

+      },

+      'only-of-type':   function(m) {

+        var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m);

+      },

+      nth: function(fragment, m) {

+        var mm, formula = m[6], predicate;

+        if (formula == 'even') formula = '2n+0';

+        if (formula == 'odd')  formula = '2n+1';

+        if (mm = formula.match(/^(\d+)$/)) // digit only

+          return '[' + fragment + "= " + mm[1] + ']';

+        if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b

+          if (mm[1] == "-") mm[1] = -1;

+          var a = mm[1] ? Number(mm[1]) : 1;

+          var b = mm[2] ? Number(mm[2]) : 0;

+          predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " +

+          "((#{fragment} - #{b}) div #{a} >= 0)]";

+          return new Template(predicate).evaluate({

+            fragment: fragment, a: a, b: b });

+        }

+      }

+    }

+  },

+

+  criteria: {

+    tagName:      'n = h.tagName(n, r, "#{1}", c);      c = false;',

+    className:    'n = h.className(n, r, "#{1}", c);    c = false;',

+    id:           'n = h.id(n, r, "#{1}", c);           c = false;',

+    attrPresence: 'n = h.attrPresence(n, r, "#{1}", c); c = false;',

+    attr: function(m) {

+      m[3] = (m[5] || m[6]);

+      return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(m);

+    },

+    pseudo: function(m) {

+      if (m[6]) m[6] = m[6].replace(/"/g, '\\"');

+      return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m);

+    },

+    descendant:   'c = "descendant";',

+    child:        'c = "child";',

+    adjacent:     'c = "adjacent";',

+    laterSibling: 'c = "laterSibling";'

+  },

+

+  patterns: {

+    // combinators must be listed first

+    // (and descendant needs to be last combinator)

+    laterSibling: /^\s*~\s*/,

+    child:        /^\s*>\s*/,

+    adjacent:     /^\s*\+\s*/,

+    descendant:   /^\s/,

+

+    // selectors follow

+    tagName:      /^\s*(\*|[\w\-]+)(\b|$)?/,

+    id:           /^#([\w\-\*]+)(\b|$)/,

+    className:    /^\.([\w\-\*]+)(\b|$)/,

+    pseudo:

+/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/,

+    attrPresence: /^\[([\w]+)\]/,

+    attr:         /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/

+  },

+

+  // for Selector.match and Element#match

+  assertions: {

+    tagName: function(element, matches) {

+      return matches[1].toUpperCase() == element.tagName.toUpperCase();

+    },

+

+    className: function(element, matches) {

+      return Element.hasClassName(element, matches[1]);

+    },

+

+    id: function(element, matches) {

+      return element.id === matches[1];

+    },

+

+    attrPresence: function(element, matches) {

+      return Element.hasAttribute(element, matches[1]);

+    },

+

+    attr: function(element, matches) {

+      var nodeValue = Element.readAttribute(element, matches[1]);

+      return nodeValue && Selector.operators[matches[2]](nodeValue, matches[5] || matches[6]);

+    }

+  },

+

+  handlers: {

+    // UTILITY FUNCTIONS

+    // joins two collections

+    concat: function(a, b) {

+      for (var i = 0, node; node = b[i]; i++)

+        a.push(node);

+      return a;

+    },

+

+    // marks an array of nodes for counting

+    mark: function(nodes) {

+      var _true = Prototype.emptyFunction;

+      for (var i = 0, node; node = nodes[i]; i++)

+        node._countedByPrototype = _true;

+      return nodes;

+    },

+

+    unmark: function(nodes) {

+      for (var i = 0, node; node = nodes[i]; i++)

+        node._countedByPrototype = undefined;

+      return nodes;

+    },

+

+    // mark each child node with its position (for nth calls)

+    // "ofType" flag indicates whether we're indexing for nth-of-type

+    // rather than nth-child

+    index: function(parentNode, reverse, ofType) {

+      parentNode._countedByPrototype = Prototype.emptyFunction;

+      if (reverse) {

+        for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) {

+          var node = nodes[i];

+          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;

+        }

+      } else {

+        for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++)

+          if (node.nodeType == 1 && (!ofType || node._countedByPrototype)) node.nodeIndex = j++;

+      }

+    },

+

+    // filters out duplicates and extends all nodes

+    unique: function(nodes) {

+      if (nodes.length == 0) return nodes;

+      var results = [], n;

+      for (var i = 0, l = nodes.length; i < l; i++)

+        if (!(n = nodes[i])._countedByPrototype) {

+          n._countedByPrototype = Prototype.emptyFunction;

+          results.push(Element.extend(n));

+        }

+      return Selector.handlers.unmark(results);

+    },

+

+    // COMBINATOR FUNCTIONS

+    descendant: function(nodes) {

+      var h = Selector.handlers;

+      for (var i = 0, results = [], node; node = nodes[i]; i++)

+        h.concat(results, node.getElementsByTagName('*'));

+      return results;

+    },

+

+    child: function(nodes) {

+      var h = Selector.handlers;

+      for (var i = 0, results = [], node; node = nodes[i]; i++) {

+        for (var j = 0, child; child = node.childNodes[j]; j++)

+          if (child.nodeType == 1 && child.tagName != '!') results.push(child);

+      }

+      return results;

+    },

+

+    adjacent: function(nodes) {

+      for (var i = 0, results = [], node; node = nodes[i]; i++) {

+        var next = this.nextElementSibling(node);

+        if (next) results.push(next);

+      }

+      return results;

+    },

+

+    laterSibling: function(nodes) {

+      var h = Selector.handlers;

+      for (var i = 0, results = [], node; node = nodes[i]; i++)

+        h.concat(results, Element.nextSiblings(node));

+      return results;

+    },

+

+    nextElementSibling: function(node) {

+      while (node = node.nextSibling)

+	      if (node.nodeType == 1) return node;

+      return null;

+    },

+

+    previousElementSibling: function(node) {

+      while (node = node.previousSibling)

+        if (node.nodeType == 1) return node;

+      return null;

+    },

+

+    // TOKEN FUNCTIONS

+    tagName: function(nodes, root, tagName, combinator) {

+      var uTagName = tagName.toUpperCase();

+      var results = [], h = Selector.handlers;

+      if (nodes) {

+        if (combinator) {

+          // fastlane for ordinary descendant combinators

+          if (combinator == "descendant") {

+            for (var i = 0, node; node = nodes[i]; i++)

+              h.concat(results, node.getElementsByTagName(tagName));

+            return results;

+          } else nodes = this[combinator](nodes);

+          if (tagName == "*") return nodes;

+        }

+        for (var i = 0, node; node = nodes[i]; i++)

+          if (node.tagName.toUpperCase() === uTagName) results.push(node);

+        return results;

+      } else return root.getElementsByTagName(tagName);

+    },

+

+    id: function(nodes, root, id, combinator) {

+      var targetNode = $(id), h = Selector.handlers;

+      if (!targetNode) return [];

+      if (!nodes && root == document) return [targetNode];

+      if (nodes) {

+        if (combinator) {

+          if (combinator == 'child') {

+            for (var i = 0, node; node = nodes[i]; i++)

+              if (targetNode.parentNode == node) return [targetNode];

+          } else if (combinator == 'descendant') {

+            for (var i = 0, node; node = nodes[i]; i++)

+              if (Element.descendantOf(targetNode, node)) return [targetNode];

+          } else if (combinator == 'adjacent') {

+            for (var i = 0, node; node = nodes[i]; i++)

+              if (Selector.handlers.previousElementSibling(targetNode) == node)

+                return [targetNode];

+          } else nodes = h[combinator](nodes);

+        }

+        for (var i = 0, node; node = nodes[i]; i++)

+          if (node == targetNode) return [targetNode];

+        return [];

+      }

+      return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : [];

+    },

+

+    className: function(nodes, root, className, combinator) {

+      if (nodes && combinator) nodes = this[combinator](nodes);

+      return Selector.handlers.byClassName(nodes, root, className);

+    },

+

+    byClassName: function(nodes, root, className) {

+      if (!nodes) nodes = Selector.handlers.descendant([root]);

+      var needle = ' ' + className + ' ';

+      for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) {

+        nodeClassName = node.className;

+        if (nodeClassName.length == 0) continue;

+        if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle))

+          results.push(node);

+      }

+      return results;

+    },

+

+    attrPresence: function(nodes, root, attr, combinator) {

+      if (!nodes) nodes = root.getElementsByTagName("*");

+      if (nodes && combinator) nodes = this[combinator](nodes);

+      var results = [];

+      for (var i = 0, node; node = nodes[i]; i++)

+        if (Element.hasAttribute(node, attr)) results.push(node);

+      return results;

+    },

+

+    attr: function(nodes, root, attr, value, operator, combinator) {

+      if (!nodes) nodes = root.getElementsByTagName("*");

+      if (nodes && combinator) nodes = this[combinator](nodes);

+      var handler = Selector.operators[operator], results = [];

+      for (var i = 0, node; node = nodes[i]; i++) {

+        var nodeValue = Element.readAttribute(node, attr);

+        if (nodeValue === null) continue;

+        if (handler(nodeValue, value)) results.push(node);

+      }

+      return results;

+    },

+

+    pseudo: function(nodes, name, value, root, combinator) {

+      if (nodes && combinator) nodes = this[combinator](nodes);

+      if (!nodes) nodes = root.getElementsByTagName("*");

+      return Selector.pseudos[name](nodes, value, root);

+    }

+  },

+

+  pseudos: {

+    'first-child': function(nodes, value, root) {

+      for (var i = 0, results = [], node; node = nodes[i]; i++) {

+        if (Selector.handlers.previousElementSibling(node)) continue;

+          results.push(node);

+      }

+      return results;

+    },

+    'last-child': function(nodes, value, root) {

+      for (var i = 0, results = [], node; node = nodes[i]; i++) {

+        if (Selector.handlers.nextElementSibling(node)) continue;

+          results.push(node);

+      }

+      return results;

+    },

+    'only-child': function(nodes, value, root) {

+      var h = Selector.handlers;

+      for (var i = 0, results = [], node; node = nodes[i]; i++)

+        if (!h.previousElementSibling(node) && !h.nextElementSibling(node))

+          results.push(node);

+      return results;

+    },

+    'nth-child':        function(nodes, formula, root) {

+      return Selector.pseudos.nth(nodes, formula, root);

+    },

+    'nth-last-child':   function(nodes, formula, root) {

+      return Selector.pseudos.nth(nodes, formula, root, true);

+    },

+    'nth-of-type':      function(nodes, formula, root) {

+      return Selector.pseudos.nth(nodes, formula, root, false, true);

+    },

+    'nth-last-of-type': function(nodes, formula, root) {

+      return Selector.pseudos.nth(nodes, formula, root, true, true);

+    },

+    'first-of-type':    function(nodes, formula, root) {

+      return Selector.pseudos.nth(nodes, "1", root, false, true);

+    },

+    'last-of-type':     function(nodes, formula, root) {

+      return Selector.pseudos.nth(nodes, "1", root, true, true);

+    },

+    'only-of-type':     function(nodes, formula, root) {

+      var p = Selector.pseudos;

+      return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root);

+    },

+

+    // handles the an+b logic

+    getIndices: function(a, b, total) {

+      if (a == 0) return b > 0 ? [b] : [];

+      return $R(1, total).inject([], function(memo, i) {

+        if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i);

+        return memo;

+      });

+    },

+

+    // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type

+    nth: function(nodes, formula, root, reverse, ofType) {

+      if (nodes.length == 0) return [];

+      if (formula == 'even') formula = '2n+0';

+      if (formula == 'odd')  formula = '2n+1';

+      var h = Selector.handlers, results = [], indexed = [], m;

+      h.mark(nodes);

+      for (var i = 0, node; node = nodes[i]; i++) {

+        if (!node.parentNode._countedByPrototype) {

+          h.index(node.parentNode, reverse, ofType);

+          indexed.push(node.parentNode);

+        }

+      }

+      if (formula.match(/^\d+$/)) { // just a number

+        formula = Number(formula);

+        for (var i = 0, node; node = nodes[i]; i++)

+          if (node.nodeIndex == formula) results.push(node);

+      } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b

+        if (m[1] == "-") m[1] = -1;

+        var a = m[1] ? Number(m[1]) : 1;

+        var b = m[2] ? Number(m[2]) : 0;

+        var indices = Selector.pseudos.getIndices(a, b, nodes.length);

+        for (var i = 0, node, l = indices.length; node = nodes[i]; i++) {

+          for (var j = 0; j < l; j++)

+            if (node.nodeIndex == indices[j]) results.push(node);

+        }

+      }

+      h.unmark(nodes);

+      h.unmark(indexed);

+      return results;

+    },

+

+    'empty': function(nodes, value, root) {

+      for (var i = 0, results = [], node; node = nodes[i]; i++) {

+        // IE treats comments as element nodes

+        if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue;

+        results.push(node);

+      }

+      return results;

+    },

+

+    'not': function(nodes, selector, root) {

+      var h = Selector.handlers, selectorType, m;

+      var exclusions = new Selector(selector).findElements(root);

+      h.mark(exclusions);

+      for (var i = 0, results = [], node; node = nodes[i]; i++)

+        if (!node._countedByPrototype) results.push(node);

+      h.unmark(exclusions);

+      return results;

+    },

+

+    'enabled': function(nodes, value, root) {

+      for (var i = 0, results = [], node; node = nodes[i]; i++)

+        if (!node.disabled) results.push(node);

+      return results;

+    },

+

+    'disabled': function(nodes, value, root) {

+      for (var i = 0, results = [], node; node = nodes[i]; i++)

+        if (node.disabled) results.push(node);

+      return results;

+    },

+

+    'checked': function(nodes, value, root) {

+      for (var i = 0, results = [], node; node = nodes[i]; i++)

+        if (node.checked) results.push(node);

+      return results;

+    }

+  },

+

+  operators: {

+    '=':  function(nv, v) { return nv == v; },

+    '!=': function(nv, v) { return nv != v; },

+    '^=': function(nv, v) { return nv.startsWith(v); },

+    '$=': function(nv, v) { return nv.endsWith(v); },

+    '*=': function(nv, v) { return nv.include(v); },

+    '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); },

+    '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); }

+  },

+

+  split: function(expression) {

+    var expressions = [];

+    expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {

+      expressions.push(m[1].strip());

+    });

+    return expressions;

+  },

+

+  matchElements: function(elements, expression) {

+    var matches = $$(expression), h = Selector.handlers;

+    h.mark(matches);

+    for (var i = 0, results = [], element; element = elements[i]; i++)

+      if (element._countedByPrototype) results.push(element);

+    h.unmark(matches);

+    return results;

+  },

+

+  findElement: function(elements, expression, index) {

+    if (Object.isNumber(expression)) {

+      index = expression; expression = false;

+    }

+    return Selector.matchElements(elements, expression || '*')[index || 0];

+  },

+

+  findChildElements: function(element, expressions) {

+    expressions = Selector.split(expressions.join(','));

+    var results = [], h = Selector.handlers;

+    for (var i = 0, l = expressions.length, selector; i < l; i++) {

+      selector = new Selector(expressions[i].strip());

+      h.concat(results, selector.findElements(element));

+    }

+    return (l > 1) ? h.unique(results) : results;

+  }

+});

+

+if (Prototype.Browser.IE) {

+  Object.extend(Selector.handlers, {

+    // IE returns comment nodes on getElementsByTagName("*").

+    // Filter them out.

+    concat: function(a, b) {

+      for (var i = 0, node; node = b[i]; i++)

+        if (node.tagName !== "!") a.push(node);

+      return a;

+    },

+

+    // IE improperly serializes _countedByPrototype in (inner|outer)HTML.

+    unmark: function(nodes) {

+      for (var i = 0, node; node = nodes[i]; i++)

+        node.removeAttribute('_countedByPrototype');

+      return nodes;

+    }

+  });

+}

+

+function $$() {

+  return Selector.findChildElements(document, $A(arguments));

+}

+var Form = {

+  reset: function(form) {

+    $(form).reset();

+    return form;

+  },

+

+  serializeElements: function(elements, options) {

+    if (typeof options != 'object') options = { hash: !!options };

+    else if (Object.isUndefined(options.hash)) options.hash = true;

+    var key, value, submitted = false, submit = options.submit;

+

+    var data = elements.inject({ }, function(result, element) {

+      if (!element.disabled && element.name) {

+        key = element.name; value = $(element).getValue();

+        if (value != null && (element.type != 'submit' || (!submitted &&

+            submit !== false && (!submit || key == submit) && (submitted = true)))) {

+          if (key in result) {

+            // a key is already present; construct an array of values

+            if (!Object.isArray(result[key])) result[key] = [result[key]];

+            result[key].push(value);

+          }

+          else result[key] = value;

+        }

+      }

+      return result;

+    });

+

+    return options.hash ? data : Object.toQueryString(data);

+  }

+};

+

+Form.Methods = {

+  serialize: function(form, options) {

+    return Form.serializeElements(Form.getElements(form), options);

+  },

+

+  getElements: function(form) {

+    return $A($(form).getElementsByTagName('*')).inject([],

+      function(elements, child) {

+        if (Form.Element.Serializers[child.tagName.toLowerCase()])

+          elements.push(Element.extend(child));

+        return elements;

+      }

+    );

+  },

+

+  getInputs: function(form, typeName, name) {

+    form = $(form);

+    var inputs = form.getElementsByTagName('input');

+

+    if (!typeName && !name) return $A(inputs).map(Element.extend);

+

+    for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) {

+      var input = inputs[i];

+      if ((typeName && input.type != typeName) || (name && input.name != name))

+        continue;

+      matchingInputs.push(Element.extend(input));

+    }

+

+    return matchingInputs;

+  },

+

+  disable: function(form) {

+    form = $(form);

+    Form.getElements(form).invoke('disable');

+    return form;

+  },

+

+  enable: function(form) {

+    form = $(form);

+    Form.getElements(form).invoke('enable');

+    return form;

+  },

+

+  findFirstElement: function(form) {

+    var elements = $(form).getElements().findAll(function(element) {

+      return 'hidden' != element.type && !element.disabled;

+    });

+    var firstByIndex = elements.findAll(function(element) {

+      return element.hasAttribute('tabIndex') && element.tabIndex >= 0;

+    }).sortBy(function(element) { return element.tabIndex }).first();

+

+    return firstByIndex ? firstByIndex : elements.find(function(element) {

+      return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase());

+    });

+  },

+

+  focusFirstElement: function(form) {

+    form = $(form);

+    form.findFirstElement().activate();

+    return form;

+  },

+

+  request: function(form, options) {

+    form = $(form), options = Object.clone(options || { });

+

+    var params = options.parameters, action = form.readAttribute('action') || '';

+    if (action.blank()) action = window.location.href;

+    options.parameters = form.serialize(true);

+

+    if (params) {

+      if (Object.isString(params)) params = params.toQueryParams();

+      Object.extend(options.parameters, params);

+    }

+

+    if (form.hasAttribute('method') && !options.method)

+      options.method = form.method;

+

+    return new Ajax.Request(action, options);

+  }

+};

+

+/*--------------------------------------------------------------------------*/

+

+Form.Element = {

+  focus: function(element) {

+    $(element).focus();

+    return element;

+  },

+

+  select: function(element) {

+    $(element).select();

+    return element;

+  }

+};

+

+Form.Element.Methods = {

+  serialize: function(element) {

+    element = $(element);

+    if (!element.disabled && element.name) {

+      var value = element.getValue();

+      if (value != undefined) {

+        var pair = { };

+        pair[element.name] = value;

+        return Object.toQueryString(pair);

+      }

+    }

+    return '';

+  },

+

+  getValue: function(element) {

+    element = $(element);

+    var method = element.tagName.toLowerCase();

+    return Form.Element.Serializers[method](element);

+  },

+

+  setValue: function(element, value) {

+    element = $(element);

+    var method = element.tagName.toLowerCase();

+    Form.Element.Serializers[method](element, value);

+    return element;

+  },

+

+  clear: function(element) {

+    $(element).value = '';

+    return element;

+  },

+

+  present: function(element) {

+    return $(element).value != '';

+  },

+

+  activate: function(element) {

+    element = $(element);

+    try {

+      element.focus();

+      if (element.select && (element.tagName.toLowerCase() != 'input' ||

+          !['button', 'reset', 'submit'].include(element.type)))

+        element.select();

+    } catch (e) { }

+    return element;

+  },

+

+  disable: function(element) {

+    element = $(element);

+    element.blur();

+    element.disabled = true;

+    return element;

+  },

+

+  enable: function(element) {

+    element = $(element);

+    element.disabled = false;

+    return element;

+  }

+};

+

+/*--------------------------------------------------------------------------*/

+

+var Field = Form.Element;

+var $F = Form.Element.Methods.getValue;

+

+/*--------------------------------------------------------------------------*/

+

+Form.Element.Serializers = {

+  input: function(element, value) {

+    switch (element.type.toLowerCase()) {

+      case 'checkbox':

+      case 'radio':

+        return Form.Element.Serializers.inputSelector(element, value);

+      default:

+        return Form.Element.Serializers.textarea(element, value);

+    }

+  },

+

+  inputSelector: function(element, value) {

+    if (Object.isUndefined(value)) return element.checked ? element.value : null;

+    else element.checked = !!value;

+  },

+

+  textarea: function(element, value) {

+    if (Object.isUndefined(value)) return element.value;

+    else element.value = value;

+  },

+

+  select: function(element, index) {

+    if (Object.isUndefined(index))

+      return this[element.type == 'select-one' ?

+        'selectOne' : 'selectMany'](element);

+    else {

+      var opt, value, single = !Object.isArray(index);

+      for (var i = 0, length = element.length; i < length; i++) {

+        opt = element.options[i];

+        value = this.optionValue(opt);

+        if (single) {

+          if (value == index) {

+            opt.selected = true;

+            return;

+          }

+        }

+        else opt.selected = index.include(value);

+      }

+    }

+  },

+

+  selectOne: function(element) {

+    var index = element.selectedIndex;

+    return index >= 0 ? this.optionValue(element.options[index]) : null;

+  },

+

+  selectMany: function(element) {

+    var values, length = element.length;

+    if (!length) return null;

+

+    for (var i = 0, values = []; i < length; i++) {

+      var opt = element.options[i];

+      if (opt.selected) values.push(this.optionValue(opt));

+    }

+    return values;

+  },

+

+  optionValue: function(opt) {

+    // extend element because hasAttribute may not be native

+    return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text;

+  }

+};

+

+/*--------------------------------------------------------------------------*/

+

+Abstract.TimedObserver = Class.create(PeriodicalExecuter, {

+  initialize: function($super, element, frequency, callback) {

+    $super(callback, frequency);

+    this.element   = $(element);

+    this.lastValue = this.getValue();

+  },

+

+  execute: function() {

+    var value = this.getValue();

+    if (Object.isString(this.lastValue) && Object.isString(value) ?

+        this.lastValue != value : String(this.lastValue) != String(value)) {

+      this.callback(this.element, value);

+      this.lastValue = value;

+    }

+  }

+});

+

+Form.Element.Observer = Class.create(Abstract.TimedObserver, {

+  getValue: function() {

+    return Form.Element.getValue(this.element);

+  }

+});

+

+Form.Observer = Class.create(Abstract.TimedObserver, {

+  getValue: function() {

+    return Form.serialize(this.element);

+  }

+});

+

+/*--------------------------------------------------------------------------*/

+

+Abstract.EventObserver = Class.create({

+  initialize: function(element, callback) {

+    this.element  = $(element);

+    this.callback = callback;

+

+    this.lastValue = this.getValue();

+    if (this.element.tagName.toLowerCase() == 'form')

+      this.registerFormCallbacks();

+    else

+      this.registerCallback(this.element);

+  },

+

+  onElementEvent: function() {

+    var value = this.getValue();

+    if (this.lastValue != value) {

+      this.callback(this.element, value);

+      this.lastValue = value;

+    }

+  },

+

+  registerFormCallbacks: function() {

+    Form.getElements(this.element).each(this.registerCallback, this);

+  },

+

+  registerCallback: function(element) {

+    if (element.type) {

+      switch (element.type.toLowerCase()) {

+        case 'checkbox':

+        case 'radio':

+          Event.observe(element, 'click', this.onElementEvent.bind(this));

+          break;

+        default:

+          Event.observe(element, 'change', this.onElementEvent.bind(this));

+          break;

+      }

+    }

+  }

+});

+

+Form.Element.EventObserver = Class.create(Abstract.EventObserver, {

+  getValue: function() {

+    return Form.Element.getValue(this.element);

+  }

+});

+

+Form.EventObserver = Class.create(Abstract.EventObserver, {

+  getValue: function() {

+    return Form.serialize(this.element);

+  }

+});

+if (!window.Event) var Event = { };

+

+Object.extend(Event, {

+  KEY_BACKSPACE: 8,

+  KEY_TAB:       9,

+  KEY_RETURN:   13,

+  KEY_ESC:      27,

+  KEY_LEFT:     37,

+  KEY_UP:       38,

+  KEY_RIGHT:    39,

+  KEY_DOWN:     40,

+  KEY_DELETE:   46,

+  KEY_HOME:     36,

+  KEY_END:      35,

+  KEY_PAGEUP:   33,

+  KEY_PAGEDOWN: 34,

+  KEY_INSERT:   45,

+

+  cache: { },

+

+  relatedTarget: function(event) {

+    var element;

+    switch(event.type) {

+      case 'mouseover': element = event.fromElement; break;

+      case 'mouseout':  element = event.toElement;   break;

+      default: return null;

+    }

+    return Element.extend(element);

+  }

+});

+

+Event.Methods = (function() {

+  var isButton;

+

+  if (Prototype.Browser.IE) {

+    var buttonMap = { 0: 1, 1: 4, 2: 2 };

+    isButton = function(event, code) {

+      return event.button == buttonMap[code];

+    };

+

+  } else if (Prototype.Browser.WebKit) {

+    isButton = function(event, code) {

+      switch (code) {

+        case 0: return event.which == 1 && !event.metaKey;

+        case 1: return event.which == 1 && event.metaKey;

+        default: return false;

+      }

+    };

+

+  } else {

+    isButton = function(event, code) {

+      return event.which ? (event.which === code + 1) : (event.button === code);

+    };

+  }

+

+  return {

+    isLeftClick:   function(event) { return isButton(event, 0) },

+    isMiddleClick: function(event) { return isButton(event, 1) },

+    isRightClick:  function(event) { return isButton(event, 2) },

+

+    element: function(event) {

+      var node = Event.extend(event).target;

+      return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node);

+    },

+

+    findElement: function(event, expression) {

+      var element = Event.element(event);

+      if (!expression) return element;

+      var elements = [element].concat(element.ancestors());

+      return Selector.findElement(elements, expression, 0);

+    },

+

+    pointer: function(event) {

+      return {

+        x: event.pageX || (event.clientX +

+          (document.documentElement.scrollLeft || document.body.scrollLeft)),

+        y: event.pageY || (event.clientY +

+          (document.documentElement.scrollTop || document.body.scrollTop))

+      };

+    },

+

+    pointerX: function(event) { return Event.pointer(event).x },

+    pointerY: function(event) { return Event.pointer(event).y },

+

+    stop: function(event) {

+      Event.extend(event);

+      event.preventDefault();

+      event.stopPropagation();

+      event.stopped = true;

+    }

+  };

+})();

+

+Event.extend = (function() {

+  var methods = Object.keys(Event.Methods).inject({ }, function(m, name) {

+    m[name] = Event.Methods[name].methodize();

+    return m;

+  });

+

+  if (Prototype.Browser.IE) {

+    Object.extend(methods, {

+      stopPropagation: function() { this.cancelBubble = true },

+      preventDefault:  function() { this.returnValue = false },

+      inspect: function() { return "[object Event]" }

+    });

+

+    return function(event) {

+      if (!event) return false;

+      if (event._extendedByPrototype) return event;

+

+      event._extendedByPrototype = Prototype.emptyFunction;

+      var pointer = Event.pointer(event);

+      Object.extend(event, {

+        target: event.srcElement,

+        relatedTarget: Event.relatedTarget(event),

+        pageX:  pointer.x,

+        pageY:  pointer.y

+      });

+      return Object.extend(event, methods);

+    };

+

+  } else {

+    Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__;

+    Object.extend(Event.prototype, methods);

+    return Prototype.K;

+  }

+})();

+

+Object.extend(Event, (function() {

+  var cache = Event.cache;

+

+  function getEventID(element) {

+    if (element._prototypeEventID) return element._prototypeEventID[0];

+    arguments.callee.id = arguments.callee.id || 1;

+    return element._prototypeEventID = [++arguments.callee.id];

+  }

+

+  function getDOMEventName(eventName) {

+    if (eventName && eventName.include(':')) return "dataavailable";

+    return eventName;

+  }

+

+  function getCacheForID(id) {

+    return cache[id] = cache[id] || { };

+  }

+

+  function getWrappersForEventName(id, eventName) {

+    var c = getCacheForID(id);

+    return c[eventName] = c[eventName] || [];

+  }

+

+  function createWrapper(element, eventName, handler) {

+    var id = getEventID(element);

+    var c = getWrappersForEventName(id, eventName);

+    if (c.pluck("handler").include(handler)) return false;

+

+    var wrapper = function(event) {

+      if (!Event || !Event.extend ||

+        (event.eventName && event.eventName != eventName))

+          return false;

+

+      Event.extend(event);

+      handler.call(element, event);

+    };

+

+    wrapper.handler = handler;

+    c.push(wrapper);

+    return wrapper;

+  }

+

+  function findWrapper(id, eventName, handler) {

+    var c = getWrappersForEventName(id, eventName);

+    return c.find(function(wrapper) { return wrapper.handler == handler });

+  }

+

+  function destroyWrapper(id, eventName, handler) {

+    var c = getCacheForID(id);

+    if (!c[eventName]) return false;

+    c[eventName] = c[eventName].without(findWrapper(id, eventName, handler));

+  }

+

+  function destroyCache() {

+    for (var id in cache)

+      for (var eventName in cache[id])

+        cache[id][eventName] = null;

+  }

+

+  if (window.attachEvent) {

+    window.attachEvent("onunload", destroyCache);

+  }

+

+  return {

+    observe: function(element, eventName, handler) {

+      element = $(element);

+      var name = getDOMEventName(eventName);

+

+      var wrapper = createWrapper(element, eventName, handler);

+      if (!wrapper) return element;

+

+      if (element.addEventListener) {

+        element.addEventListener(name, wrapper, false);

+      } else {

+        element.attachEvent("on" + name, wrapper);

+      }

+

+      return element;

+    },

+

+    stopObserving: function(element, eventName, handler) {

+      element = $(element);

+      var id = getEventID(element), name = getDOMEventName(eventName);

+

+      if (!handler && eventName) {

+        getWrappersForEventName(id, eventName).each(function(wrapper) {

+          element.stopObserving(eventName, wrapper.handler);

+        });

+        return element;

+

+      } else if (!eventName) {

+        Object.keys(getCacheForID(id)).each(function(eventName) {

+          element.stopObserving(eventName);

+        });

+        return element;

+      }

+

+      var wrapper = findWrapper(id, eventName, handler);

+      if (!wrapper) return element;

+

+      if (element.removeEventListener) {

+        element.removeEventListener(name, wrapper, false);

+      } else {

+        element.detachEvent("on" + name, wrapper);

+      }

+

+      destroyWrapper(id, eventName, handler);

+

+      return element;

+    },

+

+    fire: function(element, eventName, memo) {

+      element = $(element);

+      if (element == document && document.createEvent && !element.dispatchEvent)

+        element = document.documentElement;

+

+      var event;

+      if (document.createEvent) {

+        event = document.createEvent("HTMLEvents");

+        event.initEvent("dataavailable", true, true);

+      } else {

+        event = document.createEventObject();

+        event.eventType = "ondataavailable";

+      }

+

+      event.eventName = eventName;

+      event.memo = memo || { };

+

+      if (document.createEvent) {

+        element.dispatchEvent(event);

+      } else {

+        element.fireEvent(event.eventType, event);

+      }

+

+      return Event.extend(event);

+    }

+  };

+})());

+

+Object.extend(Event, Event.Methods);

+

+Element.addMethods({

+  fire:          Event.fire,

+  observe:       Event.observe,

+  stopObserving: Event.stopObserving

+});

+

+Object.extend(document, {

+  fire:          Element.Methods.fire.methodize(),

+  observe:       Element.Methods.observe.methodize(),

+  stopObserving: Element.Methods.stopObserving.methodize(),

+  loaded:        false

+});

+

+(function() {

+  /* Support for the DOMContentLoaded event is based on work by Dan Webb,

+     Matthias Miller, Dean Edwards and John Resig. */

+

+  var timer;

+

+  function fireContentLoadedEvent() {

+    if (document.loaded) return;

+    if (timer) window.clearInterval(timer);

+    document.fire("dom:loaded");

+    document.loaded = true;

+  }

+

+  if (document.addEventListener) {

+    if (Prototype.Browser.WebKit) {

+      timer = window.setInterval(function() {

+        if (/loaded|complete/.test(document.readyState))

+          fireContentLoadedEvent();

+      }, 0);

+

+      Event.observe(window, "load", fireContentLoadedEvent);

+

+    } else {

+      document.addEventListener("DOMContentLoaded",

+        fireContentLoadedEvent, false);

+    }

+

+  } else {

+    document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");

+    $("__onDOMContentLoaded").onreadystatechange = function() {

+      if (this.readyState == "complete") {

+        this.onreadystatechange = null;

+        fireContentLoadedEvent();

+      }

+    };

+  }

+})();

+/*------------------------------- DEPRECATED -------------------------------*/

+

+Hash.toQueryString = Object.toQueryString;

+

+var Toggle = { display: Element.toggle };

+

+Element.Methods.childOf = Element.Methods.descendantOf;

+

+var Insertion = {

+  Before: function(element, content) {

+    return Element.insert(element, {before:content});

+  },

+

+  Top: function(element, content) {

+    return Element.insert(element, {top:content});

+  },

+

+  Bottom: function(element, content) {

+    return Element.insert(element, {bottom:content});

+  },

+

+  After: function(element, content) {

+    return Element.insert(element, {after:content});

+  }

+};

+

+var $continue = new Error('"throw $continue" is deprecated, use "return" instead');

+

+// This should be moved to script.aculo.us; notice the deprecated methods

+// further below, that map to the newer Element methods.

+var Position = {

+  // set to true if needed, warning: firefox performance problems

+  // NOT neeeded for page scrolling, only if draggable contained in

+  // scrollable elements

+  includeScrollOffsets: false,

+

+  // must be called before calling withinIncludingScrolloffset, every time the

+  // page is scrolled

+  prepare: function() {

+    this.deltaX =  window.pageXOffset

+                || document.documentElement.scrollLeft

+                || document.body.scrollLeft

+                || 0;

+    this.deltaY =  window.pageYOffset

+                || document.documentElement.scrollTop

+                || document.body.scrollTop

+                || 0;

+  },

+

+  // caches x/y coordinate pair to use with overlap

+  within: function(element, x, y) {

+    if (this.includeScrollOffsets)

+      return this.withinIncludingScrolloffsets(element, x, y);

+    this.xcomp = x;

+    this.ycomp = y;

+    this.offset = Element.cumulativeOffset(element);

+

+    return (y >= this.offset[1] &&

+            y <  this.offset[1] + element.offsetHeight &&

+            x >= this.offset[0] &&

+            x <  this.offset[0] + element.offsetWidth);

+  },

+

+  withinIncludingScrolloffsets: function(element, x, y) {

+    var offsetcache = Element.cumulativeScrollOffset(element);

+

+    this.xcomp = x + offsetcache[0] - this.deltaX;

+    this.ycomp = y + offsetcache[1] - this.deltaY;

+    this.offset = Element.cumulativeOffset(element);

+

+    return (this.ycomp >= this.offset[1] &&

+            this.ycomp <  this.offset[1] + element.offsetHeight &&

+            this.xcomp >= this.offset[0] &&

+            this.xcomp <  this.offset[0] + element.offsetWidth);

+  },

+

+  // within must be called directly before

+  overlap: function(mode, element) {

+    if (!mode) return 0;

+    if (mode == 'vertical')

+      return ((this.offset[1] + element.offsetHeight) - this.ycomp) /

+        element.offsetHeight;

+    if (mode == 'horizontal')

+      return ((this.offset[0] + element.offsetWidth) - this.xcomp) /

+        element.offsetWidth;

+  },

+

+  // Deprecation layer -- use newer Element methods now (1.5.2).

+

+  cumulativeOffset: Element.Methods.cumulativeOffset,

+

+  positionedOffset: Element.Methods.positionedOffset,

+

+  absolutize: function(element) {

+    Position.prepare();

+    return Element.absolutize(element);

+  },

+

+  relativize: function(element) {

+    Position.prepare();

+    return Element.relativize(element);

+  },

+

+  realOffset: Element.Methods.cumulativeScrollOffset,

+

+  offsetParent: Element.Methods.getOffsetParent,

+

+  page: Element.Methods.viewportOffset,

+

+  clone: function(source, target, options) {

+    options = options || { };

+    return Element.clonePosition(target, source, options);

+  }

+};

+

+/*--------------------------------------------------------------------------*/

+

+if (!document.getElementsByClassName) document.getElementsByClassName = function(instanceMethods){

+  function iter(name) {

+    return name.blank() ? null : "[contains(concat(' ', @class, ' '), ' " + name + " ')]";

+  }

+

+  instanceMethods.getElementsByClassName = Prototype.BrowserFeatures.XPath ?

+  function(element, className) {

+    className = className.toString().strip();

+    var cond = /\s/.test(className) ? $w(className).map(iter).join('') : iter(className);

+    return cond ? document._getElementsByXPath('.//*' + cond, element) : [];

+  } : function(element, className) {

+    className = className.toString().strip();

+    var elements = [], classNames = (/\s/.test(className) ? $w(className) : null);

+    if (!classNames && !className) return elements;

+

+    var nodes = $(element).getElementsByTagName('*');

+    className = ' ' + className + ' ';

+

+    for (var i = 0, child, cn; child = nodes[i]; i++) {

+      if (child.className && (cn = ' ' + child.className + ' ') && (cn.include(className) ||

+          (classNames && classNames.all(function(name) {

+            return !name.toString().blank() && cn.include(' ' + name + ' ');

+          }))))

+        elements.push(Element.extend(child));

+    }

+    return elements;

+  };

+

+  return function(className, parentElement) {

+    return $(parentElement || document.body).getElementsByClassName(className);

+  };

+}(Element.Methods);

+

+/*--------------------------------------------------------------------------*/

+

+Element.ClassNames = Class.create();

+Element.ClassNames.prototype = {

+  initialize: function(element) {

+    this.element = $(element);

+  },

+

+  _each: function(iterator) {

+    this.element.className.split(/\s+/).select(function(name) {

+      return name.length > 0;

+    })._each(iterator);

+  },

+

+  set: function(className) {

+    this.element.className = className;

+  },

+

+  add: function(classNameToAdd) {

+    if (this.include(classNameToAdd)) return;

+    this.set($A(this).concat(classNameToAdd).join(' '));

+  },

+

+  remove: function(classNameToRemove) {

+    if (!this.include(classNameToRemove)) return;

+    this.set($A(this).without(classNameToRemove).join(' '));

+  },

+

+  toString: function() {

+    return $A(this).join(' ');

+  }

+};

+

+Object.extend(Element.ClassNames.prototype, Enumerable);

+

+/*--------------------------------------------------------------------------*/

+

+Element.addMethods();

symlink:b/labs/about.php (new)
--- /dev/null
+++ b/labs/about.php
@@ -1,1 +1,1 @@
-
+../about.php

symlink:b/labs/feedback.php (new)
--- /dev/null
+++ b/labs/feedback.php
@@ -1,1 +1,1 @@
-
+../feedback.php

--- a/labs/index.php
+++ b/labs/index.php
@@ -6,6 +6,8 @@
 		<li data-role="list-divider" > Experimental Features </li>
 		<li><a href="mywaybalance.php"><h3>MyWay Balance for mobile</h3>
 		<p>Mobile viewer for MyWay balance. Warning! No HTTPS security.</p></a></li>
+		<li><a href="networkstats.php"><h3>Network/Route Statistics</h3>
+		<p>Statistical analysis of network/routes</p></a></li>
 		<li>More coming soon!</li>
             </ul>
 	    </div>

--- /dev/null
+++ b/labs/networkstats.php
@@ -1,1 +1,148 @@
+<?php
+include ('../include/common.inc.php');
+include_header("Network/Route Statistics", "networkstats")
+?>
+<script type="text/javascript" src="js/flotr/lib/prototype-1.6.0.2.js"></script>
 
+		<!--[if IE]>
+
+			<script type="text/javascript" src="js/flotr/lib/excanvas.js"></script>
+
+			<script type="text/javascript" src="js/flotr/lib/base64.js"></script>
+
+		<![endif]-->
+
+		<script type="text/javascript" src="js/flotr/lib/canvas2image.js"></script>
+
+		<script type="text/javascript" src="js/flotr/lib/canvastext.js"></script>
+
+		<script type="text/javascript" src="js/flotr/flotr.debug-0.2.0-alpha_radar1.js"></script>
+		<form method="get" action="networkstats.php">
+			<select id="routeid" name="routeid">
+				<?php
+				foreach (getRoutes() as $route) {
+				echo "<option value=\"{$route['route_id']}\">{$route['route_short_name']} {$route['route_long_name']}</option>";
+				}
+				?>
+			</select>
+			<input type="submit" value="View"/>
+		</form>
+
+<?php
+// middle of graph = 6am
+$adjustFactor = 0;
+$routeid = ($_REQUEST['routeid'] ? filter_var($_REQUEST['routeid'], FILTER_SANITIZE_NUMBER_INT) : 0);
+$route = getRoute($routeid);
+echo "<h1>{$route['route_short_name']} {$route['route_long_name']}</h1>";
+foreach (getRouteTrips($routeid) as $key => $trip) {
+	$dLabel[$key] = $trip['arrival_time'];
+	if ($key == 0) {
+		$time = strtotime($trip['arrival_time']);
+		$adjustFactor = (date("G", $time) * 3600);
+	}
+	$tripStops = viaPoints($trip['trip_id']);
+	foreach ($tripStops as $i => $stop) {
+		if ($key == 0) {
+			$dTicks[$i] = $stop['stop_name'];
+		}
+		$time = strtotime($stop['arrival_time']);
+		$d[$key][$i] = 	(date("G", $time) * 3600) + (date("i", $time) * 60) + date("s", $time) - $adjustFactor;
+
+	}
+}
+
+?>
+<div id="container" style="width:100%;height:900px;"></div>
+<script type="text/javascript">
+
+			/**
+
+			 * Wait till dom's finished loading.
+
+			 */
+
+			document.observe('dom:loaded', function(){
+
+				/**
+
+				 * Fill series d1 and d2.
+
+				 */
+<?php
+foreach ($d as $key => $dataseries) {
+	
+	echo "var d$key =[";
+	foreach ($dataseries as $i => $datapoint) {
+		echo "[$i, $datapoint],";
+	}
+	echo "];\n";
+}
+
+?>
+
+			    
+
+			    var f = Flotr.draw($('container'), 
+
+					[
+						<?php
+foreach ($d as $key => $dataseries) {
+	
+	echo '{data:d'.$key.", label:'{$dLabel[$key]}'".', radar:{fill:false}},'."\n";
+	
+}
+
+?>
+					 ],
+
+					{defaultType: 'radar',
+
+					 radarChartMode: true,
+
+					 HtmlText: false,
+
+					 fontSize: 9,
+
+					 xaxis:{
+
+						ticks: [
+							<?php
+foreach ($dTicks as $key => $tickName) {
+		echo '['.$key.', "'.$tickName.'"],';
+}
+
+?>
+							
+							]},
+
+					 mouse:{ // Setup point tracking
+
+						track: true,
+
+						lineColor: 'black',
+
+						relative: true,
+
+						sensibility: 70,
+
+						trackFormatter: function(obj){
+						var d = new Date();
+						d.setMinutes(0);
+						d.setHours(0);
+d.setTime(d.getTime() + Math.floor(obj.radarData*1000) + <?php echo $adjustFactor*1000 ?>);
+return d.getHours() +':'+ (d.getMinutes().toString().length == 1 ? '0'+ d.getMinutes():  d.getMinutes());
+}}});
+
+			});
+
+		</script>
+
+	    </div>
+	    
+
+
+<?php
+include_footer()
+?>
+        
+

 Binary files a/transitdata.cbrfeed.sql.gz and /dev/null differ