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(A){return A}};if(Prototype.Browser.MobileSafari){Prototype.BrowserFeatures.SpecificElementExtensions=false}var Class={create:function(){var E=null,D=$A(arguments);if(Object.isFunction(D[0])){E=D.shift()}function A(){this.initialize.apply(this,arguments)}Object.extend(A,Class.Methods);A.superclass=E;A.subclasses=[];if(E){var B=function(){};B.prototype=E.prototype;A.prototype=new B;E.subclasses.push(A)}for(var C=0;C<D.length;C++){A.addMethods(D[C])}if(!A.prototype.initialize){A.prototype.initialize=Prototype.emptyFunction}A.prototype.constructor=A;return A}};Class.Methods={addMethods:function(G){var C=this.superclass&&this.superclass.prototype;var B=Object.keys(G);if(!Object.keys({toString:true}).length){B.push("toString","valueOf")}for(var A=0,D=B.length;A<D;A++){var F=B[A],E=G[F];if(C&&Object.isFunction(E)&&E.argumentNames().first()=="$super"){var H=E,E=Object.extend((function(I){return function(){return C[I].apply(this,arguments)}})(F).wrap(H),{valueOf:function(){return H},toString:function(){return H.toString()}})}this.prototype[F]=E}return this}};var Abstract={};Object.extend=function(A,C){for(var B in C){A[B]=C[B]}return A};Object.extend(Object,{inspect:function(A){try{if(Object.isUndefined(A)){return"undefined"}if(A===null){return"null"}return A.inspect?A.inspect():String(A)}catch(B){if(B instanceof RangeError){return"..."}throw B}},toJSON:function(A){var C=typeof A;switch(C){case"undefined":case"function":case"unknown":return ;case"boolean":return A.toString()}if(A===null){return"null"}if(A.toJSON){return A.toJSON()}if(Object.isElement(A)){return }var B=[];for(var E in A){var D=Object.toJSON(A[E]);if(!Object.isUndefined(D)){B.push(E.toJSON()+": "+D)}}return"{"+B.join(", ")+"}"},toQueryString:function(A){return $H(A).toQueryString()},toHTML:function(A){return A&&A.toHTML?A.toHTML():String.interpret(A)},keys:function(A){var B=[];for(var C in A){B.push(C)}return B},values:function(B){var A=[];for(var C in B){A.push(B[C])}return A},clone:function(A){return Object.extend({},A)},isElement:function(A){return A&&A.nodeType==1},isArray:function(A){return A!=null&&typeof A=="object"&&"splice" in A&&"join" in A},isHash:function(A){return A instanceof Hash},isFunction:function(A){return typeof A=="function"},isString:function(A){return typeof A=="string"},isNumber:function(A){return typeof A=="number"},isUndefined:function(A){return typeof A=="undefined"}});Object.extend(Function.prototype,{argumentNames:function(){var A=this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip");return A.length==1&&!A[0]?[]:A},bind:function(){if(arguments.length<2&&Object.isUndefined(arguments[0])){return this}var A=this,C=$A(arguments),B=C.shift();return function(){return A.apply(B,C.concat($A(arguments)))}},bindAsEventListener:function(){var A=this,C=$A(arguments),B=C.shift();return function(D){return A.apply(B,[D||window.event].concat(C))}},curry:function(){if(!arguments.length){return this}var A=this,B=$A(arguments);return function(){return A.apply(this,B.concat($A(arguments)))}},delay:function(){var A=this,B=$A(arguments),C=B.shift()*1000;return window.setTimeout(function(){return A.apply(A,B)},C)},wrap:function(B){var A=this;return function(){return B.apply(this,[A.bind(this)].concat($A(arguments)))}},methodize:function(){if(this._methodized){return this._methodized}var A=this;return this._methodized=function(){return A.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 C;for(var B=0,D=arguments.length;B<D;B++){var A=arguments[B];try{C=A();break}catch(E){}}return C}};RegExp.prototype.match=RegExp.prototype.test;RegExp.escape=function(A){return String(A).replace(/([.*+?^=!:${}()|[\]\/\\])/g,"\\$1")};var PeriodicalExecuter=Class.create({initialize:function(B,A){this.callback=B;this.frequency=A;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(A){return A==null?"":String(A)},specialChar:{"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r","\\":"\\\\"}});Object.extend(String.prototype,{gsub:function(E,C){var A="",D=this,B;C=arguments.callee.prepareReplacement(C);while(D.length>0){if(B=D.match(E)){A+=D.slice(0,B.index);A+=String.interpret(C(B));D=D.slice(B.index+B[0].length)}else{A+=D,D=""}}return A},sub:function(C,A,B){A=this.gsub.prepareReplacement(A);B=Object.isUndefined(B)?1:B;return this.gsub(C,function(D){if(--B<0){return D[0]}return A(D)})},scan:function(B,A){this.gsub(B,A);return String(this)},truncate:function(B,A){B=B||30;A=Object.isUndefined(A)?"...":A;return this.length>B?this.slice(0,B-A.length)+A: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 B=new RegExp(Prototype.ScriptFragment,"img");var A=new RegExp(Prototype.ScriptFragment,"im");return(this.match(B)||[]).map(function(C){return(C.match(A)||["",""])[1]})},evalScripts:function(){return this.extractScripts().map(function(script){return eval(script)})},escapeHTML:function(){var A=arguments.callee;A.text.data=this;return A.div.innerHTML},unescapeHTML:function(){var A=new Element("div");A.innerHTML=this.stripTags();return A.childNodes[0]?(A.childNodes.length>1?$A(A.childNodes).inject("",function(B,C){return B+C.nodeValue}):A.childNodes[0].nodeValue):""},toQueryParams:function(B){var A=this.strip().match(/([^?#]*)(#.*)?$/);if(!A){return{}}return A[1].split(B||"&").inject({},function(E,F){if((F=F.split("="))[0]){var C=decodeURIComponent(F.shift());var D=F.length>1?F.join("="):F[0];if(D!=undefined){D=decodeURIComponent(D)}if(C in E){if(!Object.isArray(E[C])){E[C]=[E[C]]}E[C].push(D)}else{E[C]=D}}return E})},toArray:function(){return this.split("")},succ:function(){return this.slice(0,this.length-1)+String.fromCharCode(this.charCodeAt(this.length-1)+1)},times:function(A){return A<1?"":new Array(A+1).join(this)},camelize:function(){var D=this.split("-"),A=D.length;if(A==1){return D[0]}var C=this.charAt(0)=="-"?D[0].charAt(0).toUpperCase()+D[0].substring(1):D[0];for(var B=1;B<A;B++){C+=D[B].charAt(0).toUpperCase()+D[B].substring(1)}return C},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(B){var A=this.gsub(/[\x00-\x1f\\]/,function(C){var D=String.specialChar[C[0]];return D?D:"\\u00"+C[0].charCodeAt().toPaddedString(2,16)});if(B){return'"'+A.replace(/"/g,'\\"')+'"'}return"'"+A.replace(/'/g,"\\'")+"'"},toJSON:function(){return this.inspect(true)},unfilterJSON:function(A){return this.sub(A||Prototype.JSONFilter,"#{1}")},isJSON:function(){var A=this;if(A.blank()){return false}A=this.replace(/\\./g,"@").replace(/"[^"\\\n\r]*"/g,"");return(/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(A)},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(A){return this.indexOf(A)>-1},startsWith:function(A){return this.indexOf(A)===0},endsWith:function(A){var B=this.length-A.length;return B>=0&&this.lastIndexOf(A)===B},empty:function(){return this==""},blank:function(){return/^\s*$/.test(this)},interpolate:function(A,B){return new Template(this,B).evaluate(A)}});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(B){if(Object.isFunction(B)){return B}var A=new Template(B);return function(C){return A.evaluate(C)}};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(A,B){this.template=A.toString();this.pattern=B||Template.Pattern},evaluate:function(A){if(Object.isFunction(A.toTemplateReplacements)){A=A.toTemplateReplacements()}return this.template.gsub(this.pattern,function(D){if(A==null){return""}var F=D[1]||"";if(F=="\\"){return D[2]}var B=A,G=D[3];var E=/^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/;D=E.exec(G);if(D==null){return F}while(D!=null){var C=D[1].startsWith("[")?D[2].gsub("\\\\]","]"):D[1];B=B[C];if(null==B||""==D[3]){break}G=G.substring("["==D[3]?D[1].length:D[0].length);D=E.exec(G)}return F+String.interpret(B)})}});Template.Pattern=/(^|.|\r|\n)(#\{(.*?)\})/;var $break={};var Enumerable={each:function(C,B){var A=0;C=C.bind(B);try{this._each(function(E){C(E,A++)})}catch(D){if(D!=$break){throw D}}return this},eachSlice:function(D,C,B){C=C?C.bind(B):Prototype.K;var A=-D,E=[],F=this.toArray();while((A+=D)<F.length){E.push(F.slice(A,A+D))}return E.collect(C,B)},all:function(C,B){C=C?C.bind(B):Prototype.K;var A=true;this.each(function(E,D){A=A&&!!C(E,D);if(!A){throw $break}});return A},any:function(C,B){C=C?C.bind(B):Prototype.K;var A=false;this.each(function(E,D){if(A=!!C(E,D)){throw $break}});return A},collect:function(C,B){C=C?C.bind(B):Prototype.K;var A=[];this.each(function(E,D){A.push(C(E,D))});return A},detect:function(C,B){C=C.bind(B);var A;this.each(function(E,D){if(C(E,D)){A=E;throw $break}});return A},findAll:function(C,B){C=C.bind(B);var A=[];this.each(function(E,D){if(C(E,D)){A.push(E)}});return A},grep:function(D,C,B){C=C?C.bind(B):Prototype.K;var A=[];if(Object.isString(D)){D=new RegExp(D)}this.each(function(F,E){if(D.match(F)){A.push(C(F,E))}});return A},include:function(A){if(Object.isFunction(this.indexOf)){if(this.indexOf(A)!=-1){return true}}var B=false;this.each(function(C){if(C==A){B=true;throw $break}});return B},inGroupsOf:function(B,A){A=Object.isUndefined(A)?null:A;return this.eachSlice(B,function(C){while(C.length<B){C.push(A)}return C})},inject:function(A,C,B){C=C.bind(B);this.each(function(E,D){A=C(A,E,D)});return A},invoke:function(B){var A=$A(arguments).slice(1);return this.map(function(C){return C[B].apply(C,A)})},max:function(C,B){C=C?C.bind(B):Prototype.K;var A;this.each(function(E,D){E=C(E,D);if(A==null||E>=A){A=E}});return A},min:function(C,B){C=C?C.bind(B):Prototype.K;var A;this.each(function(E,D){E=C(E,D);if(A==null||E<A){A=E}});return A},partition:function(D,B){D=D?D.bind(B):Prototype.K;var C=[],A=[];this.each(function(F,E){(D(F,E)?C:A).push(F)});return[C,A]},pluck:function(B){var A=[];this.each(function(C){A.push(C[B])});return A},reject:function(C,B){C=C.bind(B);var A=[];this.each(function(E,D){if(!C(E,D)){A.push(E)}});return A},sortBy:function(B,A){B=B.bind(A);return this.map(function(D,C){return{value:D,criteria:B(D,C)}}).sort(function(F,E){var D=F.criteria,C=E.criteria;return D<C?-1:D>C?1:0}).pluck("value")},toArray:function(){return this.map()},zip:function(){var B=Prototype.K,A=$A(arguments);if(Object.isFunction(A.last())){B=A.pop()}var C=[this].concat(A).map($A);return this.map(function(E,D){return B(C.pluck(D))})},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(C){if(!C){return[]}if(C.toArray){return C.toArray()}var B=C.length||0,A=new Array(B);while(B--){A[B]=C[B]}return A}if(Prototype.Browser.WebKit){$A=function(C){if(!C){return[]}if(!(Object.isFunction(C)&&C=="[object NodeList]")&&C.toArray){return C.toArray()}var B=C.length||0,A=new Array(B);while(B--){A[B]=C[B]}return A}}Array.from=$A;Object.extend(Array.prototype,Enumerable);if(!Array.prototype._reverse){Array.prototype._reverse=Array.prototype.reverse}Object.extend(Array.prototype,{_each:function(B){for(var A=0,C=this.length;A<C;A++){B(this[A])}},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(A){return A!=null})},flatten:function(){return this.inject([],function(B,A){return B.concat(Object.isArray(A)?A.flatten():[A])})},without:function(){var A=$A(arguments);return this.select(function(B){return !A.include(B)})},reverse:function(A){return(A!==false?this:this.toArray())._reverse()},reduce:function(){return this.length>1?this:this[0]},uniq:function(A){return this.inject([],function(D,C,B){if(0==B||(A?D.last()!=C:!D.include(C))){D.push(C)}return D})},intersect:function(A){return this.uniq().findAll(function(B){return A.detect(function(C){return B===C})})},clone:function(){return[].concat(this)},size:function(){return this.length},inspect:function(){return"["+this.map(Object.inspect).join(", ")+"]"},toJSON:function(){var A=[];this.each(function(B){var C=Object.toJSON(B);if(!Object.isUndefined(C)){A.push(C)}});return"["+A.join(", ")+"]"}});if(Object.isFunction(Array.prototype.forEach)){Array.prototype._each=Array.prototype.forEach}if(!Array.prototype.indexOf){Array.prototype.indexOf=function(C,A){A||(A=0);var B=this.length;if(A<0){A=B+A}for(;A<B;A++){if(this[A]===C){return A}}return -1}}if(!Array.prototype.lastIndexOf){Array.prototype.lastIndexOf=function(B,A){A=isNaN(A)?this.length:(A<0?this.length+A:A)+1;var C=this.slice(0,A).reverse().indexOf(B);return(C<0)?C:A-C-1}}Array.prototype.toArray=Array.prototype.clone;function $w(A){if(!Object.isString(A)){return[]}A=A.strip();return A?A.split(/\s+/):[]}if(Prototype.Browser.Opera){Array.prototype.concat=function(){var E=[];for(var B=0,C=this.length;B<C;B++){E.push(this[B])}for(var B=0,C=arguments.length;B<C;B++){if(Object.isArray(arguments[B])){for(var A=0,D=arguments[B].length;A<D;A++){E.push(arguments[B][A])}}else{E.push(arguments[B])}}return E}}Object.extend(Number.prototype,{toColorPart:function(){return this.toPaddedString(2,16)},succ:function(){return this+1},times:function(A){$R(0,this,true).each(A);return this},toPaddedString:function(C,B){var A=this.toString(B||10);return"0".times(C-A.length)+A},toJSON:function(){return isFinite(this)?this.toString():"null"}});$w("abs round ceil floor").each(function(A){Number.prototype[A]=Math[A].methodize()});function $H(A){return new Hash(A)}var Hash=Class.create(Enumerable,(function(){function A(B,C){if(Object.isUndefined(C)){return B}return B+"="+encodeURIComponent(String.interpret(C))}return{initialize:function(B){this._object=Object.isHash(B)?B.toObject():Object.clone(B)},_each:function(C){for(var B in this._object){var D=this._object[B],E=[B,D];E.key=B;E.value=D;C(E)}},set:function(B,C){return this._object[B]=C},get:function(B){return this._object[B]},unset:function(B){var C=this._object[B];delete this._object[B];return C},toObject:function(){return Object.clone(this._object)},keys:function(){return this.pluck("key")},values:function(){return this.pluck("value")},index:function(C){var B=this.detect(function(D){return D.value===C});return B&&B.key},merge:function(B){return this.clone().update(B)},update:function(B){return new Hash(B).inject(this,function(C,D){C.set(D.key,D.value);return C})},toQueryString:function(){return this.map(function(D){var C=encodeURIComponent(D.key),B=D.value;if(B&&typeof B=="object"){if(Object.isArray(B)){return B.map(A.curry(C)).join("&")}}return A(C,B)}).join("&")},inspect:function(){return"#<Hash:{"+this.map(function(B){return B.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(C,A,B){this.start=C;this.end=A;this.exclusive=B},_each:function(A){var B=this.start;while(this.include(B)){A(B);B=B.succ()}},include:function(A){if(A<this.start){return false}if(this.exclusive){return A<this.end}return A<=this.end}});var $R=function(C,A,B){return new ObjectRange(C,A,B)};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(A){this.responders._each(A)},register:function(A){if(!this.include(A)){this.responders.push(A)}},unregister:function(A){this.responders=this.responders.without(A)},dispatch:function(D,B,C,A){this.each(function(E){if(Object.isFunction(E[D])){try{E[D].apply(E,[B,C,A])}catch(F){}}})}};Object.extend(Ajax.Responders,Enumerable);Ajax.Responders.register({onCreate:function(){Ajax.activeRequestCount++},onComplete:function(){Ajax.activeRequestCount--}});Ajax.Base=Class.create({initialize:function(A){this.options={method:"post",asynchronous:true,contentType:"application/x-www-form-urlencoded",encoding:"UTF-8",parameters:"",evalJSON:true,evalJS:true};Object.extend(this.options,A||{});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,B,A){$super(A);this.transport=Ajax.getTransport();this.request(B)},request:function(B){this.url=B;this.method=this.options.method;var D=Object.clone(this.options.parameters);if(!["get","post"].include(this.method)){D._method=this.method;this.method="post"}this.parameters=D;if(D=Object.toQueryString(D)){if(this.method=="get"){this.url+=(this.url.include("?")?"&":"?")+D}else{if(/Konqueror|Safari|KHTML/.test(navigator.userAgent)){D+="&_="}}}try{var A=new Ajax.Response(this);if(this.options.onCreate){this.options.onCreate(A)}Ajax.Responders.dispatch("onCreate",this,A);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||D):null;this.transport.send(this.body);if(!this.options.asynchronous&&this.transport.overrideMimeType){this.onStateChange()}}catch(C){this.dispatchException(C)}},onStateChange:function(){var A=this.transport.readyState;if(A>1&&!((A==4)&&this._complete)){this.respondToReadyState(this.transport.readyState)}},setRequestHeaders:function(){var E={"X-Requested-With":"XMLHttpRequest","X-Prototype-Version":Prototype.Version,Accept:"text/javascript, text/html, application/xml, text/xml, */*"};if(this.method=="post"){E["Content-type"]=this.options.contentType+(this.options.encoding?"; charset="+this.options.encoding:"");if(this.transport.overrideMimeType&&(navigator.userAgent.match(/Gecko\/(\d{4})/)||[0,2005])[1]<2005){E.Connection="close"}}if(typeof this.options.requestHeaders=="object"){var C=this.options.requestHeaders;if(Object.isFunction(C.push)){for(var B=0,D=C.length;B<D;B+=2){E[C[B]]=C[B+1]}}else{$H(C).each(function(F){E[F.key]=F.value})}}for(var A in E){this.transport.setRequestHeader(A,E[A])}},success:function(){var A=this.getStatus();return !A||(A>=200&&A<300)},getStatus:function(){try{return this.transport.status||0}catch(A){return 0}},respondToReadyState:function(A){var C=Ajax.Request.Events[A],B=new Ajax.Response(this);if(C=="Complete"){try{this._complete=true;(this.options["on"+B.status]||this.options["on"+(this.success()?"Success":"Failure")]||Prototype.emptyFunction)(B,B.headerJSON)}catch(D){this.dispatchException(D)}var E=B.getHeader("Content-type");if(this.options.evalJS=="force"||(this.options.evalJS&&this.isSameOrigin()&&E&&E.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))){this.evalResponse()}}try{(this.options["on"+C]||Prototype.emptyFunction)(B,B.headerJSON);Ajax.Responders.dispatch("on"+C,this,B,B.headerJSON)}catch(D){this.dispatchException(D)}if(C=="Complete"){this.transport.onreadystatechange=Prototype.emptyFunction}},isSameOrigin:function(){var A=this.url.match(/^\s*https?:\/\/[^\/]*/);return !A||(A[0]=="#{protocol}//#{domain}#{port}".interpolate({protocol:location.protocol,domain:document.domain,port:location.port?":"+location.port:""}))},getHeader:function(A){try{return this.transport.getResponseHeader(A)||null}catch(B){return null}},evalResponse:function(){try{return eval((this.transport.responseText||"").unfilterJSON())}catch(e){this.dispatchException(e)}},dispatchException:function(A){(this.options.onException||Prototype.emptyFunction)(this,A);Ajax.Responders.dispatch("onException",this,A)}});Ajax.Request.Events=["Uninitialized","Loading","Loaded","Interactive","Complete"];Ajax.Response=Class.create({initialize:function(C){this.request=C;var D=this.transport=C.transport,A=this.readyState=D.readyState;if((A>2&&!Prototype.Browser.IE)||A==4){this.status=this.getStatus();this.statusText=this.getStatusText();this.responseText=String.interpret(D.responseText);this.headerJSON=this._getHeaderJSON()}if(A==4){var B=D.responseXML;this.responseXML=Object.isUndefined(B)?null:B;this.responseJSON=this._getResponseJSON()}},status:0,statusText:"",getStatus:Ajax.Request.prototype.getStatus,getStatusText:function(){try{return this.transport.statusText||""}catch(A){return""}},getHeader:Ajax.Request.prototype.getHeader,getAllHeaders:function(){try{return this.getAllResponseHeaders()}catch(A){return null}},getResponseHeader:function(A){return this.transport.getResponseHeader(A)},getAllResponseHeaders:function(){return this.transport.getAllResponseHeaders()},_getHeaderJSON:function(){var A=this.getHeader("X-JSON");if(!A){return null}A=decodeURIComponent(escape(A));try{return A.evalJSON(this.request.options.sanitizeJSON||!this.request.isSameOrigin())}catch(B){this.request.dispatchException(B)}},_getResponseJSON:function(){var A=this.request.options;if(!A.evalJSON||(A.evalJSON!="force"&&!(this.getHeader("Content-type")||"").include("application/json"))||this.responseText.blank()){return null}try{return this.responseText.evalJSON(A.sanitizeJSON||!this.request.isSameOrigin())}catch(B){this.request.dispatchException(B)}}});Ajax.Updater=Class.create(Ajax.Request,{initialize:function($super,A,C,B){this.container={success:(A.success||A),failure:(A.failure||(A.success?null:A))};B=Object.clone(B);var D=B.onComplete;B.onComplete=(function(E,F){this.updateContent(E.responseText);if(Object.isFunction(D)){D(E,F)}}).bind(this);$super(C,B)},updateContent:function(D){var C=this.container[this.success()?"success":"failure"],A=this.options;if(!A.evalScripts){D=D.stripScripts()}if(C=$(C)){if(A.insertion){if(Object.isString(A.insertion)){var B={};B[A.insertion]=D;C.insert(B)}else{A.insertion(C,D)}}else{C.update(D)}}}});Ajax.PeriodicalUpdater=Class.create(Ajax.Base,{initialize:function($super,A,C,B){$super(B);this.onComplete=this.options.onComplete;this.frequency=(this.options.frequency||2);this.decay=(this.options.decay||1);this.updater={};this.container=A;this.url=C;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(A){if(this.options.decay){this.decay=(A.responseText==this.lastText?this.decay*this.options.decay:1);this.lastText=A.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 $(B){if(arguments.length>1){for(var A=0,D=[],C=arguments.length;A<C;A++){D.push($(arguments[A]))}return D}if(Object.isString(B)){B=document.getElementById(B)}return Element.extend(B)}if(Prototype.BrowserFeatures.XPath){document._getElementsByXPath=function(F,A){var C=[];var E=document.evaluate(F,$(A)||document,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null);for(var B=0,D=E.snapshotLength;B<D;B++){C.push(Element.extend(E.snapshotItem(B)))}return C}}if(!window.Node){var Node={}}if(!Node.ELEMENT_NODE){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 A=this.Element;this.Element=function(D,C){C=C||{};D=D.toLowerCase();var B=Element.cache;if(Prototype.Browser.IE&&C.name){D="<"+D+' name="'+C.name+'">';delete C.name;return Element.writeAttribute(document.createElement(D),C)}if(!B[D]){B[D]=Element.extend(document.createElement(D))}return Element.writeAttribute(B[D].cloneNode(false),C)};Object.extend(this.Element,A||{})}).call(window);Element.cache={};Element.Methods={visible:function(A){return $(A).style.display!="none"},toggle:function(A){A=$(A);Element[Element.visible(A)?"hide":"show"](A);return A},hide:function(A){$(A).style.display="none";return A},show:function(A){$(A).style.display="";return A},remove:function(A){A=$(A);A.parentNode.removeChild(A);return A},update:function(A,B){A=$(A);if(B&&B.toElement){B=B.toElement()}if(Object.isElement(B)){return A.update().insert(B)}B=Object.toHTML(B);A.innerHTML=B.stripScripts();B.evalScripts.bind(B).defer();return A},replace:function(B,C){B=$(B);if(C&&C.toElement){C=C.toElement()}else{if(!Object.isElement(C)){C=Object.toHTML(C);var A=B.ownerDocument.createRange();A.selectNode(B);C.evalScripts.bind(C).defer();C=A.createContextualFragment(C.stripScripts())}}B.parentNode.replaceChild(C,B);return B},insert:function(C,E){C=$(C);if(Object.isString(E)||Object.isNumber(E)||Object.isElement(E)||(E&&(E.toElement||E.toHTML))){E={bottom:E}}var D,F,B,G;for(var A in E){D=E[A];A=A.toLowerCase();F=Element._insertionTranslations[A];if(D&&D.toElement){D=D.toElement()}if(Object.isElement(D)){F(C,D);continue}D=Object.toHTML(D);B=((A=="before"||A=="after")?C.parentNode:C).tagName.toUpperCase();G=Element._getContentFromAnonymousElement(B,D.stripScripts());if(A=="top"||A=="after"){G.reverse()}G.each(F.curry(C));D.evalScripts.bind(D).defer()}return C},wrap:function(B,C,A){B=$(B);if(Object.isElement(C)){$(C).writeAttribute(A||{})}else{if(Object.isString(C)){C=new Element(C,A)}else{C=new Element("div",C)}}if(B.parentNode){B.parentNode.replaceChild(C,B)}C.appendChild(B);return C},inspect:function(B){B=$(B);var A="<"+B.tagName.toLowerCase();$H({id:"id",className:"class"}).each(function(F){var E=F.first(),C=F.last();var D=(B[E]||"").toString();if(D){A+=" "+C+"="+D.inspect(true)}});return A+">"},recursivelyCollect:function(A,C){A=$(A);var B=[];while(A=A[C]){if(A.nodeType==1){B.push(Element.extend(A))}}return B},ancestors:function(A){return $(A).recursivelyCollect("parentNode")},descendants:function(A){return $(A).select("*")},firstDescendant:function(A){A=$(A).firstChild;while(A&&A.nodeType!=1){A=A.nextSibling}return $(A)},immediateDescendants:function(A){if(!(A=$(A).firstChild)){return[]}while(A&&A.nodeType!=1){A=A.nextSibling}if(A){return[A].concat($(A).nextSiblings())}return[]},previousSiblings:function(A){return $(A).recursivelyCollect("previousSibling")},nextSiblings:function(A){return $(A).recursivelyCollect("nextSibling")},siblings:function(A){A=$(A);return A.previousSiblings().reverse().concat(A.nextSiblings())},match:function(B,A){if(Object.isString(A)){A=new Selector(A)}return A.match($(B))},up:function(B,D,A){B=$(B);if(arguments.length==1){return $(B.parentNode)}var C=B.ancestors();return Object.isNumber(D)?C[D]:Selector.findElement(C,D,A)},down:function(B,C,A){B=$(B);if(arguments.length==1){return B.firstDescendant()}return Object.isNumber(C)?B.descendants()[C]:B.select(C)[A||0]},previous:function(B,D,A){B=$(B);if(arguments.length==1){return $(Selector.handlers.previousElementSibling(B))}var C=B.previousSiblings();return Object.isNumber(D)?C[D]:Selector.findElement(C,D,A)},next:function(C,D,B){C=$(C);if(arguments.length==1){return $(Selector.handlers.nextElementSibling(C))}var A=C.nextSiblings();return Object.isNumber(D)?A[D]:Selector.findElement(A,D,B)},select:function(){var A=$A(arguments),B=$(A.shift());return Selector.findChildElements(B,A)},adjacent:function(){var A=$A(arguments),B=$(A.shift());return Selector.findChildElements(B.parentNode,A).without(B)},identify:function(B){B=$(B);var C=B.readAttribute("id"),A=arguments.callee;if(C){return C}do{C="anonymous_element_"+A.counter++}while($(C));B.writeAttribute("id",C);return C},readAttribute:function(C,A){C=$(C);if(Prototype.Browser.IE){var B=Element._attributeTranslations.read;if(B.values[A]){return B.values[A](C,A)}if(B.names[A]){A=B.names[A]}if(A.include(":")){return(!C.attributes||!C.attributes[A])?null:C.attributes[A].value}}return C.getAttribute(A)},writeAttribute:function(E,C,F){E=$(E);var B={},D=Element._attributeTranslations.write;if(typeof C=="object"){B=C}else{B[C]=Object.isUndefined(F)?true:F}for(var A in B){C=D.names[A]||A;F=B[A];if(D.values[A]){C=D.values[A](E,F)}if(F===false||F===null){E.removeAttribute(C)}else{if(F===true){E.setAttribute(C,C)}else{E.setAttribute(C,F)}}}return E},getHeight:function(A){return $(A).getDimensions().height},getWidth:function(A){return $(A).getDimensions().width},classNames:function(A){return new Element.ClassNames(A)},hasClassName:function(A,B){if(!(A=$(A))){return }var C=A.className;return(C.length>0&&(C==B||new RegExp("(^|\\s)"+B+"(\\s|$)").test(C)))},addClassName:function(A,B){if(!(A=$(A))){return }if(!A.hasClassName(B)){A.className+=(A.className?" ":"")+B}return A},removeClassName:function(A,B){if(!(A=$(A))){return }A.className=A.className.replace(new RegExp("(^|\\s+)"+B+"(\\s+|$)")," ").strip();return A},toggleClassName:function(A,B){if(!(A=$(A))){return }return A[A.hasClassName(B)?"removeClassName":"addClassName"](B)},cleanWhitespace:function(B){B=$(B);var C=B.firstChild;while(C){var A=C.nextSibling;if(C.nodeType==3&&!/\S/.test(C.nodeValue)){B.removeChild(C)}C=A}return B},empty:function(A){return $(A).innerHTML.blank()},descendantOf:function(D,C){D=$(D),C=$(C);var F=C;if(D.compareDocumentPosition){return(D.compareDocumentPosition(C)&8)===8}if(D.sourceIndex&&!Prototype.Browser.Opera){var E=D.sourceIndex,B=C.sourceIndex,A=C.nextSibling;if(!A){do{C=C.parentNode}while(!(A=C.nextSibling)&&C.parentNode)}if(A&&A.sourceIndex){return(E>B&&E<A.sourceIndex)}}while(D=D.parentNode){if(D==F){return true}}return false},scrollTo:function(A){A=$(A);var B=A.cumulativeOffset();window.scrollTo(B[0],B[1]);return A},getStyle:function(B,C){B=$(B);C=C=="float"?"cssFloat":C.camelize();var D=B.style[C];if(!D){var A=document.defaultView.getComputedStyle(B,null);D=A?A[C]:null}if(C=="opacity"){return D?parseFloat(D):1}return D=="auto"?null:D},getOpacity:function(A){return $(A).getStyle("opacity")},setStyle:function(B,C){B=$(B);var E=B.style,A;if(Object.isString(C)){B.style.cssText+=";"+C;return C.include("opacity")?B.setOpacity(C.match(/opacity:\s*(\d?\.?\d*)/)[1]):B}for(var D in C){if(D=="opacity"){B.setOpacity(C[D])}else{E[(D=="float"||D=="cssFloat")?(Object.isUndefined(E.styleFloat)?"cssFloat":"styleFloat"):D]=C[D]}}return B},setOpacity:function(A,B){A=$(A);A.style.opacity=(B==1||B==="")?"":(B<0.00001)?0:B;return A},getDimensions:function(C){C=$(C);var G=$(C).getStyle("display");if(G!="none"&&G!=null){return{width:C.offsetWidth,height:C.offsetHeight}}var B=C.style;var F=B.visibility;var D=B.position;var A=B.display;B.visibility="hidden";B.position="absolute";B.display="block";var H=C.clientWidth;var E=C.clientHeight;B.display=A;B.position=D;B.visibility=F;return{width:H,height:E}},makePositioned:function(A){A=$(A);var B=Element.getStyle(A,"position");if(B=="static"||!B){A._madePositioned=true;A.style.position="relative";if(window.opera){A.style.top=0;A.style.left=0}}return A},undoPositioned:function(A){A=$(A);if(A._madePositioned){A._madePositioned=undefined;A.style.position=A.style.top=A.style.left=A.style.bottom=A.style.right=""}return A},makeClipping:function(A){A=$(A);if(A._overflow){return A}A._overflow=Element.getStyle(A,"overflow")||"auto";if(A._overflow!=="hidden"){A.style.overflow="hidden"}return A},undoClipping:function(A){A=$(A);if(!A._overflow){return A}A.style.overflow=A._overflow=="auto"?"":A._overflow;A._overflow=null;return A},cumulativeOffset:function(B){var A=0,C=0;do{A+=B.offsetTop||0;C+=B.offsetLeft||0;B=B.offsetParent}while(B);return Element._returnOffset(C,A)},positionedOffset:function(B){var A=0,D=0;do{A+=B.offsetTop||0;D+=B.offsetLeft||0;B=B.offsetParent;if(B){if(B.tagName=="BODY"){break}var C=Element.getStyle(B,"position");if(C!=="static"){break}}}while(B);return Element._returnOffset(D,A)},absolutize:function(B){B=$(B);if(B.getStyle("position")=="absolute"){return }var D=B.positionedOffset();var F=D[1];var E=D[0];var C=B.clientWidth;var A=B.clientHeight;B._originalLeft=E-parseFloat(B.style.left||0);B._originalTop=F-parseFloat(B.style.top||0);B._originalWidth=B.style.width;B._originalHeight=B.style.height;B.style.position="absolute";B.style.top=F+"px";B.style.left=E+"px";B.style.width=C+"px";B.style.height=A+"px";return B},relativize:function(A){A=$(A);if(A.getStyle("position")=="relative"){return }A.style.position="relative";var C=parseFloat(A.style.top||0)-(A._originalTop||0);var B=parseFloat(A.style.left||0)-(A._originalLeft||0);A.style.top=C+"px";A.style.left=B+"px";A.style.height=A._originalHeight;A.style.width=A._originalWidth;return A},cumulativeScrollOffset:function(B){var A=0,C=0;do{A+=B.scrollTop||0;C+=B.scrollLeft||0;B=B.parentNode}while(B);return Element._returnOffset(C,A)},getOffsetParent:function(A){if(A.offsetParent){return $(A.offsetParent)}if(A==document.body){return $(A)}while((A=A.parentNode)&&A!=document.body){if(Element.getStyle(A,"position")!="static"){return $(A)}}return $(document.body)},viewportOffset:function(D){var A=0,C=0;var B=D;do{A+=B.offsetTop||0;C+=B.offsetLeft||0;if(B.offsetParent==document.body&&Element.getStyle(B,"position")=="absolute"){break}}while(B=B.offsetParent);B=D;do{if(!Prototype.Browser.Opera||B.tagName=="BODY"){A-=B.scrollTop||0;C-=B.scrollLeft||0}}while(B=B.parentNode);return Element._returnOffset(C,A)},clonePosition:function(B,D){var A=Object.extend({setLeft:true,setTop:true,setWidth:true,setHeight:true,offsetTop:0,offsetLeft:0},arguments[2]||{});D=$(D);var E=D.viewportOffset();B=$(B);var F=[0,0];var C=null;if(Element.getStyle(B,"position")=="absolute"){C=B.getOffsetParent();F=C.viewportOffset()}if(C==document.body){F[0]-=document.body.offsetLeft;F[1]-=document.body.offsetTop}if(A.setLeft){B.style.left=(E[0]-F[0]+A.offsetLeft)+"px"}if(A.setTop){B.style.top=(E[1]-F[1]+A.offsetTop)+"px"}if(A.setWidth){B.style.width=D.offsetWidth+"px"}if(A.setHeight){B.style.height=D.offsetHeight+"px"}return B}};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(D,B,C){switch(C){case"left":case"top":case"right":case"bottom":if(D(B,"position")==="static"){return null}case"height":case"width":if(!Element.visible(B)){return null}var E=parseInt(D(B,C),10);if(E!==B["offset"+C.capitalize()]){return E+"px"}var A;if(C==="height"){A=["border-top-width","padding-top","padding-bottom","border-bottom-width"]}else{A=["border-left-width","padding-left","padding-right","border-right-width"]}return A.inject(E,function(F,G){var H=D(B,G);return H===null?F:F-parseInt(H,10)})+"px";default:return D(B,C)}});Element.Methods.readAttribute=Element.Methods.readAttribute.wrap(function(C,A,B){if(B==="title"){return A.title}return C(A,B)})}else{if(Prototype.Browser.IE){Element.Methods.getOffsetParent=Element.Methods.getOffsetParent.wrap(function(C,B){B=$(B);var A=B.getStyle("position");if(A!=="static"){return C(B)}B.setStyle({position:"relative"});var D=C(B);B.setStyle({position:A});return D});$w("positionedOffset viewportOffset").each(function(A){Element.Methods[A]=Element.Methods[A].wrap(function(E,C){C=$(C);var B=C.getStyle("position");if(B!=="static"){return E(C)}var D=C.getOffsetParent();if(D&&D.getStyle("position")==="fixed"){D.setStyle({zoom:1})}C.setStyle({position:"relative"});var F=E(C);C.setStyle({position:B});return F})});Element.Methods.getStyle=function(A,B){A=$(A);B=(B=="float"||B=="cssFloat")?"styleFloat":B.camelize();var C=A.style[B];if(!C&&A.currentStyle){C=A.currentStyle[B]}if(B=="opacity"){if(C=(A.getStyle("filter")||"").match(/alpha\(opacity=(.*)\)/)){if(C[1]){return parseFloat(C[1])/100}}return 1}if(C=="auto"){if((B=="width"||B=="height")&&(A.getStyle("display")!="none")){return A["offset"+B.capitalize()]+"px"}return null}return C};Element.Methods.setOpacity=function(B,E){function F(G){return G.replace(/alpha\([^\)]*\)/gi,"")}B=$(B);var A=B.currentStyle;if((A&&!A.hasLayout)||(!A&&B.style.zoom=="normal")){B.style.zoom=1}var D=B.getStyle("filter"),C=B.style;if(E==1||E===""){(D=F(D))?C.filter=D:C.removeAttribute("filter");return B}else{if(E<0.00001){E=0}}C.filter=F(D)+"alpha(opacity="+(E*100)+")";return B};Element._attributeTranslations={read:{names:{"class":"className","for":"htmlFor"},values:{_getAttr:function(A,B){return A.getAttribute(B,2)},_getAttrNode:function(A,C){var B=A.getAttributeNode(C);return B?B.value:""},_getEv:function(A,B){B=A.getAttribute(B);return B?B.toString().slice(23,-2):null},_flag:function(A,B){return $(A).hasAttribute(B)?B:null},style:function(A){return A.style.cssText.toLowerCase()},title:function(A){return A.title}}}};Element._attributeTranslations.write={names:Object.extend({cellpadding:"cellPadding",cellspacing:"cellSpacing"},Element._attributeTranslations.read.names),values:{checked:function(A,B){A.checked=!!B},style:function(A,B){A.style.cssText=B?B:""}}};Element._attributeTranslations.has={};$w("colSpan rowSpan vAlign dateTime accessKey tabIndex encType maxLength readOnly longDesc").each(function(A){Element._attributeTranslations.write.names[A.toLowerCase()]=A;Element._attributeTranslations.has[A.toLowerCase()]=A});(function(A){Object.extend(A,{href:A._getAttr,src:A._getAttr,type:A._getAttr,action:A._getAttrNode,disabled:A._flag,checked:A._flag,readonly:A._flag,multiple:A._flag,onload:A._getEv,onunload:A._getEv,onclick:A._getEv,ondblclick:A._getEv,onmousedown:A._getEv,onmouseup:A._getEv,onmouseover:A._getEv,onmousemove:A._getEv,onmouseout:A._getEv,onfocus:A._getEv,onblur:A._getEv,onkeypress:A._getEv,onkeydown:A._getEv,onkeyup:A._getEv,onsubmit:A._getEv,onreset:A._getEv,onselect:A._getEv,onchange:A._getEv})})(Element._attributeTranslations.read.values)}else{if(Prototype.Browser.Gecko&&/rv:1\.8\.0/.test(navigator.userAgent)){Element.Methods.setOpacity=function(A,B){A=$(A);A.style.opacity=(B==1)?0.999999:(B==="")?"":(B<0.00001)?0:B;return A}}else{if(Prototype.Browser.WebKit){Element.Methods.setOpacity=function(A,B){A=$(A);A.style.opacity=(B==1||B==="")?"":(B<0.00001)?0:B;if(B==1){if(A.tagName=="IMG"&&A.width){A.width++;A.width--}else{try{var D=document.createTextNode(" ");A.appendChild(D);A.removeChild(D)}catch(C){}}}return A};Element.Methods.cumulativeOffset=function(B){var A=0,C=0;do{A+=B.offsetTop||0;C+=B.offsetLeft||0;if(B.offsetParent==document.body){if(Element.getStyle(B,"position")=="absolute"){break}}B=B.offsetParent}while(B);return Element._returnOffset(C,A)}}}}}if(Prototype.Browser.IE||Prototype.Browser.Opera){Element.Methods.update=function(B,C){B=$(B);if(C&&C.toElement){C=C.toElement()}if(Object.isElement(C)){return B.update().insert(C)}C=Object.toHTML(C);var A=B.tagName.toUpperCase();if(A in Element._insertionTranslations.tags){$A(B.childNodes).each(function(D){B.removeChild(D)});Element._getContentFromAnonymousElement(A,C.stripScripts()).each(function(D){B.appendChild(D)})}else{B.innerHTML=C.stripScripts()}C.evalScripts.bind(C).defer();return B}}if("outerHTML" in document.createElement("div")){Element.Methods.replace=function(C,E){C=$(C);if(E&&E.toElement){E=E.toElement()}if(Object.isElement(E)){C.parentNode.replaceChild(E,C);return C}E=Object.toHTML(E);var D=C.parentNode,B=D.tagName.toUpperCase();if(Element._insertionTranslations.tags[B]){var F=C.next();var A=Element._getContentFromAnonymousElement(B,E.stripScripts());D.removeChild(C);if(F){A.each(function(G){D.insertBefore(G,F)})}else{A.each(function(G){D.appendChild(G)})}}else{C.outerHTML=E.stripScripts()}E.evalScripts.bind(E).defer();return C}}Element._returnOffset=function(B,C){var A=[B,C];A.left=B;A.top=C;return A};Element._getContentFromAnonymousElement=function(C,B){var D=new Element("div"),A=Element._insertionTranslations.tags[C];if(A){D.innerHTML=A[0]+B+A[1];A[2].times(function(){D=D.firstChild})}else{D.innerHTML=B}return $A(D.childNodes)};Element._insertionTranslations={before:function(A,B){A.parentNode.insertBefore(B,A)},top:function(A,B){A.insertBefore(B,A.firstChild)},bottom:function(A,B){A.appendChild(B)},after:function(A,B){A.parentNode.insertBefore(B,A.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(A,C){C=Element._attributeTranslations.has[C]||C;var B=$(A).getAttributeNode(C);return B&&B.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 A={},B=Element.Methods.ByTag;var C=Object.extend(function(F){if(!F||F._extendedByPrototype||F.nodeType!=1||F==window){return F}var D=Object.clone(A),E=F.tagName,H,G;if(B[E]){Object.extend(D,B[E])}for(H in D){G=D[H];if(Object.isFunction(G)&&!(H in F)){F[H]=G.methodize()}}F._extendedByPrototype=Prototype.emptyFunction;return F},{refresh:function(){if(!Prototype.BrowserFeatures.ElementExtensions){Object.extend(A,Element.Methods);Object.extend(A,Element.Methods.Simulated)}}});C.refresh();return C})();Element.hasAttribute=function(A,B){if(A.hasAttribute){return A.hasAttribute(B)}return Element.Methods.Simulated.hasAttribute(A,B)};Element.addMethods=function(C){var I=Prototype.BrowserFeatures,D=Element.Methods.ByTag;if(!C){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 B=C;C=arguments[1]}if(!B){Object.extend(Element.Methods,C||{})}else{if(Object.isArray(B)){B.each(H)}else{H(B)}}function H(F){F=F.toUpperCase();if(!Element.Methods.ByTag[F]){Element.Methods.ByTag[F]={}}Object.extend(Element.Methods.ByTag[F],C)}function A(L,K,F){F=F||false;for(var N in L){var M=L[N];if(!Object.isFunction(M)){continue}if(!F||!(N in K)){K[N]=M.methodize()}}}function E(L){var F;var K={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(K[L]){F="HTML"+K[L]+"Element"}if(window[F]){return window[F]}F="HTML"+L+"Element";if(window[F]){return window[F]}F="HTML"+L.capitalize()+"Element";if(window[F]){return window[F]}window[F]={};window[F].prototype=document.createElement(L).__proto__;return window[F]}if(I.ElementExtensions){A(Element.Methods,HTMLElement.prototype);A(Element.Methods.Simulated,HTMLElement.prototype,true)}if(I.SpecificElementExtensions){for(var J in Element.Methods.ByTag){var G=E(J);if(Object.isUndefined(G)){continue}A(D[J],G.prototype)}}Object.extend(Element,Element.Methods);delete Element.ByTag;if(Element.extend.refresh){Element.extend.refresh()}Element.cache={}};document.viewport={getDimensions:function(){var A={};var C=Prototype.Browser;$w("width height").each(function(E){var B=E.capitalize();A[E]=(C.WebKit&&!document.evaluate)?self["inner"+B]:(C.Opera)?document.body["client"+B]:document.documentElement["client"+B]});return A},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)}};var Selector=Class.create({initialize:function(A){this.expression=A.strip();this.compileMatcher()},shouldUseXPath:function(){if(!Prototype.BrowserFeatures.XPath){return false}var A=this.expression;if(Prototype.Browser.WebKit&&(A.include("-of-type")||A.include(":empty"))){return false}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,F=Selector.patterns,B=Selector.xpath,D,A;if(Selector._cache[E]){this.xpath=Selector._cache[E];return }this.matcher=[".//*"];while(E&&D!=E&&(/\S/).test(E)){D=E;for(var C in F){if(A=E.match(F[C])){this.matcher.push(Object.isFunction(B[C])?B[C](A):new Template(B[C]).evaluate(A));E=E.replace(A[0],"");break}}}this.xpath=this.matcher.join("");Selector._cache[this.expression]=this.xpath},findElements:function(A){A=A||document;if(this.xpath){return document._getElementsByXPath(this.xpath,A)}return this.matcher(A)},match:function(H){this.tokens=[];var L=this.expression,A=Selector.patterns,E=Selector.assertions;var B,D,F;while(L&&B!==L&&(/\S/).test(L)){B=L;for(var I in A){D=A[I];if(F=L.match(D)){if(E[I]){this.tokens.push([I,Object.clone(F)]);L=L.replace(F[0],"")}else{return this.findElements(document).include(H)}}}}var K=true,C,J;for(var I=0,G;G=this.tokens[I];I++){C=G[0],J=G[1];if(!Selector.assertions[C](H,J)){K=false;break}}return K},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(A){if(A[1]=="*"){return""}return"[local-name()='"+A[1].toLowerCase()+"' or local-name()='"+A[1].toUpperCase()+"']"},className:"[contains(concat(' ', @class, ' '), ' #{1} ')]",id:"[@id='#{1}']",attrPresence:function(A){A[1]=A[1].toLowerCase();return new Template("[@#{1}]").evaluate(A)},attr:function(A){A[1]=A[1].toLowerCase();A[3]=A[5]||A[6];return new Template(Selector.xpath.operators[A[2]]).evaluate(A)},pseudo:function(A){var B=Selector.xpath.pseudos[A[1]];if(!B){return""}if(Object.isFunction(B)){return B(A)}return new Template(Selector.xpath.pseudos[A[1]]).evaluate(A)},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(B){var H=B[6],G=Selector.patterns,A=Selector.xpath,E,C;var F=[];while(H&&E!=H&&(/\S/).test(H)){E=H;for(var D in G){if(B=H.match(G[D])){C=Object.isFunction(A[D])?A[D](B):new Template(A[D]).evaluate(B);F.push("("+C.substring(1,C.length-1)+")");H=H.replace(B[0],"");break}}}return"[not("+F.join(" and ")+")]"},"nth-child":function(A){return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ",A)},"nth-last-child":function(A){return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ",A)},"nth-of-type":function(A){return Selector.xpath.pseudos.nth("position() ",A)},"nth-last-of-type":function(A){return Selector.xpath.pseudos.nth("(last() + 1 - position()) ",A)},"first-of-type":function(A){A[6]="1";return Selector.xpath.pseudos["nth-of-type"](A)},"last-of-type":function(A){A[6]="1";return Selector.xpath.pseudos["nth-last-of-type"](A)},"only-of-type":function(A){var B=Selector.xpath.pseudos;return B["first-of-type"](A)+B["last-of-type"](A)},nth:function(E,C){var F,G=C[6],B;if(G=="even"){G="2n+0"}if(G=="odd"){G="2n+1"}if(F=G.match(/^(\d+)$/)){return"["+E+"= "+F[1]+"]"}if(F=G.match(/^(-?\d*)?n(([+-])(\d+))?/)){if(F[1]=="-"){F[1]=-1}var D=F[1]?Number(F[1]):1;var A=F[2]?Number(F[2]):0;B="[((#{fragment} - #{b}) mod #{a} = 0) and ((#{fragment} - #{b}) div #{a} >= 0)]";return new Template(B).evaluate({fragment:E,a:D,b:A})}}}},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(A){A[3]=(A[5]||A[6]);return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}", c); c = false;').evaluate(A)},pseudo:function(A){if(A[6]){A[6]=A[6].replace(/"/g,'\\"')}return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(A)},descendant:'c = "descendant";',child:'c = "child";',adjacent:'c = "adjacent";',laterSibling:'c = "laterSibling";'},patterns:{laterSibling:/^\s*~\s*/,child:/^\s*>\s*/,adjacent:/^\s*\+\s*/,descendant:/^\s/,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|([^'"][^\]]*?)))?\]/},assertions:{tagName:function(A,B){return B[1].toUpperCase()==A.tagName.toUpperCase()},className:function(A,B){return Element.hasClassName(A,B[1])},id:function(A,B){return A.id===B[1]},attrPresence:function(A,B){return Element.hasAttribute(A,B[1])},attr:function(B,C){var A=Element.readAttribute(B,C[1]);return A&&Selector.operators[C[2]](A,C[5]||C[6])}},handlers:{concat:function(B,A){for(var C=0,D;D=A[C];C++){B.push(D)}return B},mark:function(A){var D=Prototype.emptyFunction;for(var B=0,C;C=A[B];B++){C._countedByPrototype=D}return A},unmark:function(A){for(var B=0,C;C=A[B];B++){C._countedByPrototype=undefined}return A},index:function(A,D,G){A._countedByPrototype=Prototype.emptyFunction;if(D){for(var B=A.childNodes,E=B.length-1,C=1;E>=0;E--){var F=B[E];if(F.nodeType==1&&(!G||F._countedByPrototype)){F.nodeIndex=C++}}}else{for(var E=0,C=1,B=A.childNodes;F=B[E];E++){if(F.nodeType==1&&(!G||F._countedByPrototype)){F.nodeIndex=C++}}}},unique:function(B){if(B.length==0){return B}var D=[],E;for(var C=0,A=B.length;C<A;C++){if(!(E=B[C])._countedByPrototype){E._countedByPrototype=Prototype.emptyFunction;D.push(Element.extend(E))}}return Selector.handlers.unmark(D)},descendant:function(A){var D=Selector.handlers;for(var C=0,B=[],E;E=A[C];C++){D.concat(B,E.getElementsByTagName("*"))}return B},child:function(A){var E=Selector.handlers;for(var D=0,C=[],F;F=A[D];D++){for(var B=0,G;G=F.childNodes[B];B++){if(G.nodeType==1&&G.tagName!="!"){C.push(G)}}}return C},adjacent:function(A){for(var C=0,B=[],E;E=A[C];C++){var D=this.nextElementSibling(E);if(D){B.push(D)}}return B},laterSibling:function(A){var D=Selector.handlers;for(var C=0,B=[],E;E=A[C];C++){D.concat(B,Element.nextSiblings(E))}return B},nextElementSibling:function(A){while(A=A.nextSibling){if(A.nodeType==1){return A}}return null},previousElementSibling:function(A){while(A=A.previousSibling){if(A.nodeType==1){return A}}return null},tagName:function(A,H,C,B){var I=C.toUpperCase();var E=[],G=Selector.handlers;if(A){if(B){if(B=="descendant"){for(var F=0,D;D=A[F];F++){G.concat(E,D.getElementsByTagName(C))}return E}else{A=this[B](A)}if(C=="*"){return A}}for(var F=0,D;D=A[F];F++){if(D.tagName.toUpperCase()===I){E.push(D)}}return E}else{return H.getElementsByTagName(C)}},id:function(B,A,H,F){var G=$(H),D=Selector.handlers;if(!G){return[]}if(!B&&A==document){return[G]}if(B){if(F){if(F=="child"){for(var C=0,E;E=B[C];C++){if(G.parentNode==E){return[G]}}}else{if(F=="descendant"){for(var C=0,E;E=B[C];C++){if(Element.descendantOf(G,E)){return[G]}}}else{if(F=="adjacent"){for(var C=0,E;E=B[C];C++){if(Selector.handlers.previousElementSibling(G)==E){return[G]}}}else{B=D[F](B)}}}}for(var C=0,E;E=B[C];C++){if(E==G){return[G]}}return[]}return(G&&Element.descendantOf(G,A))?[G]:[]},className:function(B,A,C,D){if(B&&D){B=this[D](B)}return Selector.handlers.byClassName(B,A,C)},byClassName:function(C,B,F){if(!C){C=Selector.handlers.descendant([B])}var H=" "+F+" ";for(var E=0,D=[],G,A;G=C[E];E++){A=G.className;if(A.length==0){continue}if(A==F||(" "+A+" ").include(H)){D.push(G)}}return D},attrPresence:function(C,B,A,G){if(!C){C=B.getElementsByTagName("*")}if(C&&G){C=this[G](C)}var E=[];for(var D=0,F;F=C[D];D++){if(Element.hasAttribute(F,A)){E.push(F)}}return E},attr:function(A,I,H,J,C,B){if(!A){A=I.getElementsByTagName("*")}if(A&&B){A=this[B](A)}var K=Selector.operators[C],F=[];for(var E=0,D;D=A[E];E++){var G=Element.readAttribute(D,H);if(G===null){continue}if(K(G,J)){F.push(D)}}return F},pseudo:function(B,C,E,A,D){if(B&&D){B=this[D](B)}if(!B){B=A.getElementsByTagName("*")}return Selector.pseudos[C](B,E,A)}},pseudos:{"first-child":function(B,F,A){for(var D=0,C=[],E;E=B[D];D++){if(Selector.handlers.previousElementSibling(E)){continue}C.push(E)}return C},"last-child":function(B,F,A){for(var D=0,C=[],E;E=B[D];D++){if(Selector.handlers.nextElementSibling(E)){continue}C.push(E)}return C},"only-child":function(B,G,A){var E=Selector.handlers;for(var D=0,C=[],F;F=B[D];D++){if(!E.previousElementSibling(F)&&!E.nextElementSibling(F)){C.push(F)}}return C},"nth-child":function(B,C,A){return Selector.pseudos.nth(B,C,A)},"nth-last-child":function(B,C,A){return Selector.pseudos.nth(B,C,A,true)},"nth-of-type":function(B,C,A){return Selector.pseudos.nth(B,C,A,false,true)},"nth-last-of-type":function(B,C,A){return Selector.pseudos.nth(B,C,A,true,true)},"first-of-type":function(B,C,A){return Selector.pseudos.nth(B,"1",A,false,true)},"last-of-type":function(B,C,A){return Selector.pseudos.nth(B,"1",A,true,true)},"only-of-type":function(B,D,A){var C=Selector.pseudos;return C["last-of-type"](C["first-of-type"](B,D,A),D,A)},getIndices:function(B,A,C){if(B==0){return A>0?[A]:[]}return $R(1,C).inject([],function(D,E){if(0==(E-A)%B&&(E-A)/B>=0){D.push(E)}return D})},nth:function(A,L,N,K,C){if(A.length==0){return[]}if(L=="even"){L="2n+0"}if(L=="odd"){L="2n+1"}var J=Selector.handlers,I=[],B=[],E;J.mark(A);for(var H=0,D;D=A[H];H++){if(!D.parentNode._countedByPrototype){J.index(D.parentNode,K,C);B.push(D.parentNode)}}if(L.match(/^\d+$/)){L=Number(L);for(var H=0,D;D=A[H];H++){if(D.nodeIndex==L){I.push(D)}}}else{if(E=L.match(/^(-?\d*)?n(([+-])(\d+))?/)){if(E[1]=="-"){E[1]=-1}var O=E[1]?Number(E[1]):1;var M=E[2]?Number(E[2]):0;var P=Selector.pseudos.getIndices(O,M,A.length);for(var H=0,D,F=P.length;D=A[H];H++){for(var G=0;G<F;G++){if(D.nodeIndex==P[G]){I.push(D)}}}}}J.unmark(A);J.unmark(B);return I},empty:function(B,F,A){for(var D=0,C=[],E;E=B[D];D++){if(E.tagName=="!"||(E.firstChild&&!E.innerHTML.match(/^\s*$/))){continue}C.push(E)}return C},not:function(A,D,I){var G=Selector.handlers,J,C;var H=new Selector(D).findElements(I);G.mark(H);for(var F=0,E=[],B;B=A[F];F++){if(!B._countedByPrototype){E.push(B)}}G.unmark(H);return E},enabled:function(B,F,A){for(var D=0,C=[],E;E=B[D];D++){if(!E.disabled){C.push(E)}}return C},disabled:function(B,F,A){for(var D=0,C=[],E;E=B[D];D++){if(E.disabled){C.push(E)}}return C},checked:function(B,F,A){for(var D=0,C=[],E;E=B[D];D++){if(E.checked){C.push(E)}}return C}},operators:{"=":function(B,A){return B==A},"!=":function(B,A){return B!=A},"^=":function(B,A){return B.startsWith(A)},"$=":function(B,A){return B.endsWith(A)},"*=":function(B,A){return B.include(A)},"~=":function(B,A){return(" "+B+" ").include(" "+A+" ")},"|=":function(B,A){return("-"+B.toUpperCase()+"-").include("-"+A.toUpperCase()+"-")}},split:function(B){var A=[];B.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/,function(C){A.push(C[1].strip())});return A},matchElements:function(F,G){var E=$$(G),D=Selector.handlers;D.mark(E);for(var C=0,B=[],A;A=F[C];C++){if(A._countedByPrototype){B.push(A)}}D.unmark(E);return B},findElement:function(B,C,A){if(Object.isNumber(C)){A=C;C=false}return Selector.matchElements(B,C||"*")[A||0]},findChildElements:function(E,G){G=Selector.split(G.join(","));var D=[],F=Selector.handlers;for(var C=0,B=G.length,A;C<B;C++){A=new Selector(G[C].strip());F.concat(D,A.findElements(E))}return(B>1)?F.unique(D):D}});if(Prototype.Browser.IE){Object.extend(Selector.handlers,{concat:function(B,A){for(var C=0,D;D=A[C];C++){if(D.tagName!=="!"){B.push(D)}}return B},unmark:function(A){for(var B=0,C;C=A[B];B++){C.removeAttribute("_countedByPrototype")}return A}})}function $$(){return Selector.findChildElements(document,$A(arguments))}var Form={reset:function(A){$(A).reset();return A},serializeElements:function(G,B){if(typeof B!="object"){B={hash:!!B}}else{if(Object.isUndefined(B.hash)){B.hash=true}}var C,F,A=false,E=B.submit;var D=G.inject({},function(H,I){if(!I.disabled&&I.name){C=I.name;F=$(I).getValue();if(F!=null&&(I.type!="submit"||(!A&&E!==false&&(!E||C==E)&&(A=true)))){if(C in H){if(!Object.isArray(H[C])){H[C]=[H[C]]}H[C].push(F)}else{H[C]=F}}}return H});return B.hash?D:Object.toQueryString(D)}};Form.Methods={serialize:function(B,A){return Form.serializeElements(Form.getElements(B),A)},getElements:function(A){return $A($(A).getElementsByTagName("*")).inject([],function(B,C){if(Form.Element.Serializers[C.tagName.toLowerCase()]){B.push(Element.extend(C))}return B})},getInputs:function(G,C,D){G=$(G);var A=G.getElementsByTagName("input");if(!C&&!D){return $A(A).map(Element.extend)}for(var E=0,H=[],F=A.length;E<F;E++){var B=A[E];if((C&&B.type!=C)||(D&&B.name!=D)){continue}H.push(Element.extend(B))}return H},disable:function(A){A=$(A);Form.getElements(A).invoke("disable");return A},enable:function(A){A=$(A);Form.getElements(A).invoke("enable");return A},findFirstElement:function(B){var C=$(B).getElements().findAll(function(D){return"hidden"!=D.type&&!D.disabled});var A=C.findAll(function(D){return D.hasAttribute("tabIndex")&&D.tabIndex>=0}).sortBy(function(D){return D.tabIndex}).first();return A?A:C.find(function(D){return["input","select","textarea"].include(D.tagName.toLowerCase())})},focusFirstElement:function(A){A=$(A);A.findFirstElement().activate();return A},request:function(B,A){B=$(B),A=Object.clone(A||{});var D=A.parameters,C=B.readAttribute("action")||"";if(C.blank()){C=window.location.href}A.parameters=B.serialize(true);if(D){if(Object.isString(D)){D=D.toQueryParams()}Object.extend(A.parameters,D)}if(B.hasAttribute("method")&&!A.method){A.method=B.method}return new Ajax.Request(C,A)}};Form.Element={focus:function(A){$(A).focus();return A},select:function(A){$(A).select();return A}};Form.Element.Methods={serialize:function(A){A=$(A);if(!A.disabled&&A.name){var B=A.getValue();if(B!=undefined){var C={};C[A.name]=B;return Object.toQueryString(C)}}return""},getValue:function(A){A=$(A);var B=A.tagName.toLowerCase();return Form.Element.Serializers[B](A)},setValue:function(A,B){A=$(A);var C=A.tagName.toLowerCase();Form.Element.Serializers[C](A,B);return A},clear:function(A){$(A).value="";return A},present:function(A){return $(A).value!=""},activate:function(A){A=$(A);try{A.focus();if(A.select&&(A.tagName.toLowerCase()!="input"||!["button","reset","submit"].include(A.type))){A.select()}}catch(B){}return A},disable:function(A){A=$(A);A.blur();A.disabled=true;return A},enable:function(A){A=$(A);A.disabled=false;return A}};var Field=Form.Element;var $F=Form.Element.Methods.getValue;Form.Element.Serializers={input:function(A,B){switch(A.type.toLowerCase()){case"checkbox":case"radio":return Form.Element.Serializers.inputSelector(A,B);default:return Form.Element.Serializers.textarea(A,B)}},inputSelector:function(A,B){if(Object.isUndefined(B)){return A.checked?A.value:null}else{A.checked=!!B}},textarea:function(A,B){if(Object.isUndefined(B)){return A.value}else{A.value=B}},select:function(D,A){if(Object.isUndefined(A)){return this[D.type=="select-one"?"selectOne":"selectMany"](D)}else{var C,F,G=!Object.isArray(A);for(var B=0,E=D.length;B<E;B++){C=D.options[B];F=this.optionValue(C);if(G){if(F==A){C.selected=true;return }}else{C.selected=A.include(F)}}}},selectOne:function(B){var A=B.selectedIndex;return A>=0?this.optionValue(B.options[A]):null},selectMany:function(D){var A,E=D.length;if(!E){return null}for(var C=0,A=[];C<E;C++){var B=D.options[C];if(B.selected){A.push(this.optionValue(B))}}return A},optionValue:function(A){return Element.extend(A).hasAttribute("value")?A.value:A.text}};Abstract.TimedObserver=Class.create(PeriodicalExecuter,{initialize:function($super,A,B,C){$super(C,B);this.element=$(A);this.lastValue=this.getValue()},execute:function(){var A=this.getValue();if(Object.isString(this.lastValue)&&Object.isString(A)?this.lastValue!=A:String(this.lastValue)!=String(A)){this.callback(this.element,A);this.lastValue=A}}});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(A,B){this.element=$(A);this.callback=B;this.lastValue=this.getValue();if(this.element.tagName.toLowerCase()=="form"){this.registerFormCallbacks()}else{this.registerCallback(this.element)}},onElementEvent:function(){var A=this.getValue();if(this.lastValue!=A){this.callback(this.element,A);this.lastValue=A}},registerFormCallbacks:function(){Form.getElements(this.element).each(this.registerCallback,this)},registerCallback:function(A){if(A.type){switch(A.type.toLowerCase()){case"checkbox":case"radio":Event.observe(A,"click",this.onElementEvent.bind(this));break;default:Event.observe(A,"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(B){var A;switch(B.type){case"mouseover":A=B.fromElement;break;case"mouseout":A=B.toElement;break;default:return null}return Element.extend(A)}});Event.Methods=(function(){var A;if(Prototype.Browser.IE){var B={0:1,1:4,2:2};A=function(D,C){return D.button==B[C]}}else{if(Prototype.Browser.WebKit){A=function(D,C){switch(C){case 0:return D.which==1&&!D.metaKey;case 1:return D.which==1&&D.metaKey;default:return false}}}else{A=function(D,C){return D.which?(D.which===C+1):(D.button===C)}}}return{isLeftClick:function(C){return A(C,0)},isMiddleClick:function(C){return A(C,1)},isRightClick:function(C){return A(C,2)},element:function(D){var C=Event.extend(D).target;return Element.extend(C.nodeType==Node.TEXT_NODE?C.parentNode:C)},findElement:function(D,F){var C=Event.element(D);if(!F){return C}var E=[C].concat(C.ancestors());return Selector.findElement(E,F,0)},pointer:function(C){return{x:C.pageX||(C.clientX+(document.documentElement.scrollLeft||document.body.scrollLeft)),y:C.pageY||(C.clientY+(document.documentElement.scrollTop||document.body.scrollTop))}},pointerX:function(C){return Event.pointer(C).x},pointerY:function(C){return Event.pointer(C).y},stop:function(C){Event.extend(C);C.preventDefault();C.stopPropagation();C.stopped=true}}})();Event.extend=(function(){var A=Object.keys(Event.Methods).inject({},function(B,C){B[C]=Event.Methods[C].methodize();return B});if(Prototype.Browser.IE){Object.extend(A,{stopPropagation:function(){this.cancelBubble=true},preventDefault:function(){this.returnValue=false},inspect:function(){return"[object Event]"}});return function(B){if(!B){return false}if(B._extendedByPrototype){return B}B._extendedByPrototype=Prototype.emptyFunction;var C=Event.pointer(B);Object.extend(B,{target:B.srcElement,relatedTarget:Event.relatedTarget(B),pageX:C.x,pageY:C.y});return Object.extend(B,A)}}else{Event.prototype=Event.prototype||document.createEvent("HTMLEvents").__proto__;Object.extend(Event.prototype,A);return Prototype.K}})();Object.extend(Event,(function(){var B=Event.cache;function C(J){if(J._prototypeEventID){return J._prototypeEventID[0]}arguments.callee.id=arguments.callee.id||1;return J._prototypeEventID=[++arguments.callee.id]}function G(J){if(J&&J.include(":")){return"dataavailable"}return J}function A(J){return B[J]=B[J]||{}}function F(L,J){var K=A(L);return K[J]=K[J]||[]}function H(K,J,L){var O=C(K);var N=F(O,J);if(N.pluck("handler").include(L)){return false}var M=function(P){if(!Event||!Event.extend||(P.eventName&&P.eventName!=J)){return false}Event.extend(P);L.call(K,P)};M.handler=L;N.push(M);return M}function I(M,J,K){var L=F(M,J);return L.find(function(N){return N.handler==K})}function D(M,J,K){var L=A(M);if(!L[J]){return false}L[J]=L[J].without(I(M,J,K))}function E(){for(var K in B){for(var J in B[K]){B[K][J]=null}}}if(window.attachEvent){window.attachEvent("onunload",E)}return{observe:function(L,J,M){L=$(L);var K=G(J);var N=H(L,J,M);if(!N){return L}if(L.addEventListener){L.addEventListener(K,N,false)}else{L.attachEvent("on"+K,N)}return L},stopObserving:function(L,J,M){L=$(L);var O=C(L),K=G(J);if(!M&&J){F(O,J).each(function(P){L.stopObserving(J,P.handler)});return L}else{if(!J){Object.keys(A(O)).each(function(P){L.stopObserving(P)});return L}}var N=I(O,J,M);if(!N){return L}if(L.removeEventListener){L.removeEventListener(K,N,false)}else{L.detachEvent("on"+K,N)}D(O,J,M);return L},fire:function(L,K,J){L=$(L);if(L==document&&document.createEvent&&!L.dispatchEvent){L=document.documentElement}var M;if(document.createEvent){M=document.createEvent("HTMLEvents");M.initEvent("dataavailable",true,true)}else{M=document.createEventObject();M.eventType="ondataavailable"}M.eventName=K;M.memo=J||{};if(document.createEvent){L.dispatchEvent(M)}else{L.fireEvent(M.eventType,M)}return Event.extend(M)}}})());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(){var B;function A(){if(document.loaded){return }if(B){window.clearInterval(B)}document.fire("dom:loaded");document.loaded=true}if(document.addEventListener){if(Prototype.Browser.WebKit){B=window.setInterval(function(){if(/loaded|complete/.test(document.readyState)){A()}},0);Event.observe(window,"load",A)}else{document.addEventListener("DOMContentLoaded",A,false)}}else{document.write("<script id=__onDOMContentLoaded defer src=//:><\/script>");$("__onDOMContentLoaded").onreadystatechange=function(){if(this.readyState=="complete"){this.onreadystatechange=null;A()}}}})();Hash.toQueryString=Object.toQueryString;var Toggle={display:Element.toggle};Element.Methods.childOf=Element.Methods.descendantOf;var Insertion={Before:function(A,B){return Element.insert(A,{before:B})},Top:function(A,B){return Element.insert(A,{top:B})},Bottom:function(A,B){return Element.insert(A,{bottom:B})},After:function(A,B){return Element.insert(A,{after:B})}};var $continue=new Error('"throw $continue" is deprecated, use "return" instead');var Position={includeScrollOffsets:false,prepare:function(){this.deltaX=window.pageXOffset||document.documentElement.scrollLeft||document.body.scrollLeft||0;this.deltaY=window.pageYOffset||document.documentElement.scrollTop||document.body.scrollTop||0},within:function(B,A,C){if(this.includeScrollOffsets){return this.withinIncludingScrolloffsets(B,A,C)}this.xcomp=A;this.ycomp=C;this.offset=Element.cumulativeOffset(B);return(C>=this.offset[1]&&C<this.offset[1]+B.offsetHeight&&A>=this.offset[0]&&A<this.offset[0]+B.offsetWidth)},withinIncludingScrolloffsets:function(B,A,D){var C=Element.cumulativeScrollOffset(B);this.xcomp=A+C[0]-this.deltaX;this.ycomp=D+C[1]-this.deltaY;this.offset=Element.cumulativeOffset(B);return(this.ycomp>=this.offset[1]&&this.ycomp<this.offset[1]+B.offsetHeight&&this.xcomp>=this.offset[0]&&this.xcomp<this.offset[0]+B.offsetWidth)},overlap:function(B,A){if(!B){return 0}if(B=="vertical"){return((this.offset[1]+A.offsetHeight)-this.ycomp)/A.offsetHeight}if(B=="horizontal"){return((this.offset[0]+A.offsetWidth)-this.xcomp)/A.offsetWidth}},cumulativeOffset:Element.Methods.cumulativeOffset,positionedOffset:Element.Methods.positionedOffset,absolutize:function(A){Position.prepare();return Element.absolutize(A)},relativize:function(A){Position.prepare();return Element.relativize(A)},realOffset:Element.Methods.cumulativeScrollOffset,offsetParent:Element.Methods.getOffsetParent,page:Element.Methods.viewportOffset,clone:function(B,C,A){A=A||{};return Element.clonePosition(C,B,A)}};if(!document.getElementsByClassName){document.getElementsByClassName=function(B){function A(C){return C.blank()?null:"[contains(concat(' ', @class, ' '), ' "+C+" ')]"}B.getElementsByClassName=Prototype.BrowserFeatures.XPath?function(C,E){E=E.toString().strip();var D=/\s/.test(E)?$w(E).map(A).join(""):A(E);return D?document._getElementsByXPath(".//*"+D,C):[]}:function(E,F){F=F.toString().strip();var G=[],H=(/\s/.test(F)?$w(F):null);if(!H&&!F){return G}var C=$(E).getElementsByTagName("*");F=" "+F+" ";for(var D=0,J,I;J=C[D];D++){if(J.className&&(I=" "+J.className+" ")&&(I.include(F)||(H&&H.all(function(K){return !K.toString().blank()&&I.include(" "+K+" ")})))){G.push(Element.extend(J))}}return G};return function(D,C){return $(C||document.body).getElementsByClassName(D)}}(Element.Methods)}Element.ClassNames=Class.create();Element.ClassNames.prototype={initialize:function(A){this.element=$(A)},_each:function(A){this.element.className.split(/\s+/).select(function(B){return B.length>0})._each(A)},set:function(A){this.element.className=A},add:function(A){if(this.include(A)){return }this.set($A(this).concat(A).join(" "))},remove:function(A){if(!this.include(A)){return }this.set($A(this).without(A).join(" "))},toString:function(){return $A(this).join(" ")}};Object.extend(Element.ClassNames.prototype,Enumerable);Element.addMethods();/*
moo.fx, simple effects library built with prototype.js (http://prototype.conio.net).
by Valerio Proietti (http://mad4milk.net) MIT-style LICENSE.
for more info (http://moofx.mad4milk.net).
Sunday, March 05, 2006
v 1.2.3
*/
 
moo_fx_js_version = "$Revision: 1.1 $";

var fx = new Object();
//base
fx.Base = function(){};
fx.Base.prototype = {
	setOptions: function(options) {
	this.options = {
		duration: 500,
		onComplete: '',
		transition: fx.sinoidal
	}
	Object.extend(this.options, options || {});
	},

	step: function() {
		var time  = (new Date).getTime();
		if (time >= this.options.duration+this.startTime) {
			this.now = this.to;
			clearInterval (this.timer);
			this.timer = null;
			if (this.options.onComplete) setTimeout(this.options.onComplete.bind(this), 10);
		}
		else {
			var Tpos = (time - this.startTime) / (this.options.duration);
			this.now = this.options.transition(Tpos) * (this.to-this.from) + this.from;
		}
		this.increase();
	},

	custom: function(from, to) {
		if (this.timer != null) return;
		this.from = from;
		this.to = to;
		this.startTime = (new Date).getTime();
		this.timer = setInterval (this.step.bind(this), 13);
	},

	hide: function() {
		this.now = 0;
		this.increase();
	},

	clearTimer: function() {
		clearInterval(this.timer);
		this.timer = null;
	}
}

//stretchers
fx.Layout = Class.create();
fx.Layout.prototype = Object.extend(new fx.Base(), {
	initialize: function(el, options) {
		this.el = $(el);
		this.el.style.overflow = "hidden";
		this.iniWidth = this.el.offsetWidth;
		this.iniHeight = this.el.offsetHeight;
		this.setOptions(options);
	}
});

fx.Height = Class.create();
Object.extend(Object.extend(fx.Height.prototype, fx.Layout.prototype), {	
	increase: function() {
		this.el.style.height = this.now + "px";
	},

	toggle: function() {
		if (this.el.offsetHeight > 0) this.custom(this.el.offsetHeight, 0);
		else this.custom(0, this.el.scrollHeight);
	}
});

fx.Width = Class.create();
Object.extend(Object.extend(fx.Width.prototype, fx.Layout.prototype), {	
	increase: function() {
		this.el.style.width = this.now + "px";
	},

	toggle: function(){
		if (this.el.offsetWidth > 0) this.custom(this.el.offsetWidth, 0);
		else this.custom(0, this.iniWidth);
	}
});

//fader
fx.Opacity = Class.create();
fx.Opacity.prototype = Object.extend(new fx.Base(), {
	initialize: function(el, options) {
		this.el = $(el);
		this.now = 1;
		this.increase();
		this.setOptions(options);
	},

	increase: function() {
		if (this.now == 1 && (/Firefox/.test(navigator.userAgent))) this.now = 0.9999;
		this.setOpacity(this.now);
	},
	
	setOpacity: function(opacity) {
		if (opacity == 0 && this.el.style.visibility != "hidden") this.el.style.visibility = "hidden";
		else if (this.el.style.visibility != "visible") this.el.style.visibility = "visible";
		if (window.ActiveXObject) this.el.style.filter = "alpha(opacity=" + opacity*100 + ")";
		this.el.style.opacity = opacity;
	},

	toggle: function() {
		if (this.now > 0) this.custom(1, 0);
		else this.custom(0, 1);
	}
});

//transitions
fx.sinoidal = function(pos){
	return ((-Math.cos(pos*Math.PI)/2) + 0.5);
	//this transition is from script.aculo.us
}
fx.linear = function(pos){
	return pos;
}
fx.cubic = function(pos){
	return Math.pow(pos, 3);
}
fx.circ = function(pos){
	return Math.sqrt(pos);
}


fx.omgHeight = Class.create();
Object.extend(Object.extend(fx.omgHeight.prototype, fx.Layout.prototype), {	
	nominalHeight: 20,
	increase: function() {
		this.el.style.height = this.now + "px";
	},

	toggle: function() {
		if (this.el.offsetHeight > this.nominalHeight) this.custom(this.el.offsetHeight, this.nominalHeight);
		else this.custom(this.nominalHeight, this.el.scrollHeight);
	}
});
/*
moo.fx pack, effects extensions for moo.fx.
by Valerio Proietti (http://mad4milk.net) MIT-style LICENSE
for more info visit (http://moofx.mad4milk.net).
Friday, April 14, 2006
v 1.2.4
*/
 
moo_fx_pack_js_version = "$Revision: 1.1 $";

//smooth scroll
fx.Scroll = Class.create();
fx.Scroll.prototype = Object.extend(new fx.Base(), {
	initialize: function(options) {
		this.setOptions(options);
	},

	scrollTo: function(el){
		var dest = Position.cumulativeOffset($(el))[1];
		var client = window.innerHeight || document.documentElement.clientHeight;
		var full = document.documentElement.scrollHeight;
		var top = window.pageYOffset || document.body.scrollTop || document.documentElement.scrollTop;
		if (dest+client > full) this.custom(top, dest - client + (full-dest));
		else this.custom(top, dest);
	},

	increase: function(){
		window.scrollTo(0, this.now);
	}
});

//text size modify, now works with pixels too.
fx.Text = Class.create();
fx.Text.prototype = Object.extend(new fx.Base(), {
	initialize: function(el, options) {
		this.el = $(el);
		this.setOptions(options);
		if (!this.options.unit) this.options.unit = "em";
	},

	increase: function() {
		this.el.style.fontSize = this.now + this.options.unit;
	}
});

//composition effect: widht/height/opacity
fx.Combo = Class.create();
fx.Combo.prototype = {
	setOptions: function(options) {
		this.options = {
			opacity: true,
			height: true,
			width: false
		}
		Object.extend(this.options, options || {});
	},

	initialize: function(el, options) {
		this.el = $(el);
		this.setOptions(options);
		if (this.options.opacity) {
			this.o = new fx.Opacity(el, options);
			options.onComplete = null;
		}
		if (this.options.height) {
			this.h = new fx.Height(el, options);
			options.onComplete = null;
		}
		if (this.options.width) this.w = new fx.Width(el, options);
	},
	
	toggle: function() { this.checkExec('toggle'); },

	hide: function(){ this.checkExec('hide'); },
	
	clearTimer: function(){ this.checkExec('clearTimer'); },
	
	checkExec: function(func){
		if (this.o) this.o[func]();
		if (this.h) this.h[func]();
		if (this.w) this.w[func]();
	},
	
	//only if width+height
	resizeTo: function(hto, wto) {
		if (this.h && this.w) {
			this.h.custom(this.el.offsetHeight, this.el.offsetHeight + hto);
			this.w.custom(this.el.offsetWidth, this.el.offsetWidth + wto);
		}
	},

	customSize: function(hto, wto) {
		if (this.h && this.w) {
			this.h.custom(this.el.offsetHeight, hto);
			this.w.custom(this.el.offsetWidth, wto);
		}
	}
}

fx.Accordion = Class.create();
fx.Accordion.prototype = {
	setOptions: function(options) {
		this.options = {
			delay: 100,
			opacity: false
		}
		Object.extend(this.options, options || {});
	},

	initialize: function(togglers, elements, options) {
		this.elements = elements;
		this.setOptions(options);
		var options = options || '';
		this.fxa = [];
		if (options && options.onComplete) options.onFinish = options.onComplete;
		elements.each(function(el, i){
			options.onComplete = function(){
				if (el.offsetHeight > 0) el.style.height = '1%';
				if (options.onFinish) options.onFinish(el);
			}
			this.fxa[i] = new fx.Combo(el, options);
			this.fxa[i].hide();
		}.bind(this));

		togglers.each(function(tog, i){
			if (typeof tog.onclick == 'function') var exClick = tog.onclick;
			tog.onclick = function(){
				if (exClick) exClick();
				this.showThisHideOpen(elements[i]);
			}.bind(this);
		}.bind(this));
	},

	showThisHideOpen: function(toShow){
		this.elements.each(function(el, j){
			if (el.offsetHeight > 0 && el != toShow) this.clearAndToggle(el, j);
			if (el == toShow && toShow.offsetHeight == 0) setTimeout(function(){this.clearAndToggle(toShow, j);}.bind(this), this.options.delay);
		}.bind(this));
	},

	clearAndToggle: function(el, i){
		this.fxa[i].clearTimer();
		this.fxa[i].toggle();
	}
}

var Remember = new Object();
Remember = function(){};
Remember.prototype = {
	initialize: function(el, options){
		this.el = $(el);
		this.days = 365;
		this.options = options;
		this.effect();
		var cookie = this.readCookie();
		if (cookie) {
			this.fx.now = cookie;
			this.fx.increase();
		}
	},

	//cookie functions based on code by Peter-Paul Koch
	setCookie: function(value) {
		var date = new Date();
		date.setTime(date.getTime()+(this.days*24*60*60*1000));
		var expires = "; expires="+date.toGMTString();
		document.cookie = this.el+this.el.id+this.prefix+"="+value+expires+"; path=/";
	},

	readCookie: function() {
		var nameEQ = this.el+this.el.id+this.prefix + "=";
		var ca = document.cookie.split(';');
		for(var i=0;c=ca[i];i++) {
			while (c.charAt(0)==' ') c = c.substring(1,c.length);
			if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
		}
		return false;
	},

	custom: function(from, to){
		if (this.fx.now != to) {
			this.setCookie(to);
			this.fx.custom(from, to);
		}
	}
}

fx.RememberHeight = Class.create();
fx.RememberHeight.prototype = Object.extend(new Remember(), {
	effect: function(){
		this.fx = new fx.Height(this.el, this.options);
		this.prefix = 'height';
	},
	
	toggle: function(){
		if (this.el.offsetHeight == 0) this.setCookie(this.el.scrollHeight);
		else this.setCookie(0);
		this.fx.toggle();
	},
	
	resize: function(to){
		this.setCookie(this.el.offsetHeight+to);
		this.fx.custom(this.el.offsetHeight,this.el.offsetHeight+to);
	},

	hide: function(){
		if (!this.readCookie()) {
			this.fx.hide();
		}
	}
});

fx.RememberText = Class.create();
fx.RememberText.prototype = Object.extend(new Remember(), {
	effect: function(){
		this.fx = new fx.Text(this.el, this.options);
		this.prefix = 'text';
	}
});

//useful for-replacement
Array.prototype.iterate = function(func){
	for(var i=0;i<this.length;i++) func(this[i], i);
}
if (!Array.prototype.each) Array.prototype.each = Array.prototype.iterate;

//Easing Equations (c) 2003 Robert Penner, all rights reserved.
//This work is subject to the terms in http://www.robertpenner.com/easing_terms_of_use.html.

//expo
fx.expoIn = function(pos){
	return Math.pow(2, 10 * (pos - 1));
}
fx.expoOut = function(pos){
	return (-Math.pow(2, -10 * pos) + 1);
}

//quad
fx.quadIn = function(pos){
	return Math.pow(pos, 2);
}
fx.quadOut = function(pos){
	return -(pos)*(pos-2);
}

//circ
fx.circOut = function(pos){
	return Math.sqrt(1 - Math.pow(pos-1,2));
}
fx.circIn = function(pos){
	return -(Math.sqrt(1 - Math.pow(pos, 2)) - 1);
}

//back
fx.backIn = function(pos){
	return (pos)*pos*((2.7)*pos - 1.7);
}
fx.backOut = function(pos){
	return ((pos-1)*(pos-1)*((2.7)*(pos-1) + 1.7) + 1);
}

//sine
fx.sineOut = function(pos){
	return Math.sin(pos * (Math.PI/2));
}
fx.sineIn = function(pos){
	return -Math.cos(pos * (Math.PI/2)) + 1;
}
fx.sineInOut = function(pos){
	return -(Math.cos(Math.PI*pos) - 1)/2;
}// Ideally all "Petromedia" code should live in the PM "namespace", eventually it will...
var PM = {};

// prototype.lite uses $c instead of $A, save madness by aliasing it.
$c = $A;

function KB912945_fix(slot) {
	var elem = document.getElementById("to_be_rewritten_"+slot);
	if(elem) document.write(elem.innerHTML);
}

function $v(el) {
	el = $(el);
	if(el && el.value) {
		return el.value;
	}
	if(el && el.firstChild && el.firstChild.nodeValue) {
		return el.firstChild.nodeValue;
	}
	return ''; // who really wants 'undefined' anyway?
}

// create an element, optionally set an ID & add a child.
function $pn(type, id, child) {
	el = document.createElement(type);
	if(id)
		el.id = id;
	if(child)
		el.appendChild(child);
	return el;
}

// create a new text node. useful to dump things in to $pn.
function $pt(v) { return document.createTextNode(v); };


/* z = element to start with, x = tags (* for all), y = class (false for none), w = the behaviour, v = type of event (default is click) */
function applyBehaviour(z,x,y,w,v){
	if(!v) v = "click";
	if(!z) z = document;
	b = $(z).getElementsByTagName(x);
	$c(b).each(function(a){
		if((y == false) || Element.hasClassName(a,y)) {
			addEvent(a, v, w);
		}
	});
}

// function applyBehaviour2(node,selector,action,event) {
// 	$A($(node ? node : document).getElementsBySelector(selector)).each(function(a) {
// 		$(a).observe((event ? event : 'click'), action)
// 	});
// }

function addEvent(obj, evType, fn){
	return Event.observe(obj, evType, fn);
}

function removeEvent(obj, evType, fn, useCapture){
  if (obj.removeEventListener){
    obj.removeEventListener(evType, fn, useCapture);
    return true;
  } else if (obj.detachEvent){
    var r = obj.detachEvent("on"+evType, fn.bind(obj));
    return r;
  } else {
    alert("Handler could not be removed");
  }
}

function addToggler(toggle, toggleable, doNow, widthMode, nominalHeight) {
	r = function() {
		if($(toggle) && $(toggleable)) {
			if(widthMode) {
				$(toggleable).fx = new fx.Width($(toggleable), {duration: 500});
			} else {
				$(toggleable).fx = new fx.omgHeight($(toggleable), {duration: 500});
			}
			if(nominalHeight || nominalHeight === 0) $(toggleable).fx.nominalHeight = nominalHeight;
			$(toggleable).toggle = function() {
				this.fx.toggle();
				if($(toggle).doToggleStyle) $(toggle).doToggleStyle();
			};
			$(toggle).onclick = function() {
				this.blur();
				$(toggleable).toggle();
				return false;
			};
		}
	};
	
	if(doNow)	r();
	else      	addEvent(window, 'load', r);
}

Element.addMethods({
	getByTagName: function(el, tagName) {
		childNode = new Array();
		childNode[0] = null;
		i = 0;
		$c(el.childNodes).each(function(child){
			if(child.nodeName == tagName) {
				childNode[i] = child;
				i++;
			}
		});
		return childNode;
	},
	
	toggleStyle: function(workingOn, styleA) {
		workingOn = $(workingOn);
		if(Element.hasClassName(workingOn, styleA)) {
			Element.removeClassName(workingOn, styleA);
		} else {
			Element.addClassName(workingOn, styleA);
		}
	},
	
	toggleStyles: function(el, styleA, styleB) {
		el = $(el);
		if(Element.hasClassName(el, styleA)) {
			Element.removeClassName(el, styleA);
			Element.addClassName(el, styleB);
		} else {
			Element.removeClassName(el, styleB);
			Element.addClassName(el, styleA);		
		}
	},

	tabToMe: function(el) {
		inactiveTab = $(el).parentNode.firstChild;
		while(inactiveTab) {
			if(inactiveTab.nodeName.toLowerCase() == el.nodeName.toLowerCase() && inactiveTab.tabInactiveClass) {
				if(Element.hasClassName(inactiveTab, inactiveTab.tabActiveClass) && inactiveTab.onclosetab) {
					inactiveTab.onclosetab();
				}
				b = inactiveTab.className;
				Element.removeClassName(inactiveTab, inactiveTab.tabActiveClass);
				Element.addClassName(inactiveTab, inactiveTab.tabInactiveClass);
			}
			inactiveTab = inactiveTab.nextSibling;
		}		
		Element.removeClassName(el, el.tabInactiveClass);
		Element.addClassName(el, el.tabActiveClass);
	},

	makeSwitchingTab: function(el, onClickHandler, offClickHandler, inactiveClass, activeClass) {
		el.tabInactiveClass = inactiveClass;
		el.tabActiveClass = activeClass;
		el.onclick = function() { this.tabToMe(); onClickHandler(); };

		if(offClickHandler) el.onclosetab = offClickHandler;

		childer = el.firstChild;
		while(childer) {
			if(childer.nodeName.toLowerCase() == 'a') {
				childer.onclick = function() { this.blur(); return false; };
			}
			childer = childer.nextSibling;
		}
	},

	makeElementAjaxy: function (element) {
		element.href = element.href + "&ajax=1";
		element.onclick = function() {
			parent_node = this.parentNode;
			new Ajax.Updater(parent_node, this.href, {method: 'get', evalScripts: true, onComplete: function(){ 
				$(parent_node.getElementsByTagName('a')[0]).makeElementAjaxy(); 
			}});
			Element.update(parent_node, OMGLib.loadingImageSmall);
			return false;
		};
	},
	
	// These two functions let play with the position of relatively positioned elements
	// without destroying the relativeness.

	relativeOffsetTop: function(el) {
		el.absolutize();
		output = el.offsetTop;
		el.relativize();
		return output;
	},	
	setRelativeOffsetTop: function(el, to) {
		el.absolutize();
		el.style.top = to + "px";
		el.relativize();
	},
	
	relativeOffsetTopAndLeft: function(el) {
		el.absolutize();
		outputTop = el.offsetTop;
		outputLeft = el.offsetLeft;
		el.relativize();
		return { top: outputTop, left: outputLeft };
	}
	
});


function createCookie(name,value,days) {
	if (days) {
		var date = new Date();
		date.setTime(date.getTime()+(days*24*60*60*1000));
		var expires = "; expires="+date.toGMTString();
	}
	else var expires = "";
	document.cookie = name+"="+value+expires+"; path=/";
}

function readCookie(name) {
	var nameEQ = name + "=";
	var ca = document.cookie.split(';');
	for(var i=0;i < ca.length;i++) {
		var c = ca[i];
		while (c.charAt(0)==' ') c = c.substring(1,c.length);
		if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
	}
	return null;
}

function eraseCookie(name) {
	createCookie(name,"",-1);
}

function toggleAutoSignin() {
	document.location.replace("/toggleAutoSignIn?sourcePage="+document.URL);
}

function printit(){}

function nav2(a, c) {
	if(a) {
		a = $(a);
		b = a.getElementsByTagName('select')[0];
		if(b) {
			c = $v(b);
		}
	}
	if(c) {
		document.location = c;
	}
	return false;
}

function nav(formname) {
	opt=document.forms[formname].menu.options[document.forms[formname].menu.selectedIndex].value;
	if (opt!='') {		
		document.location=opt;
	}
} 

function popUp(url, name,x,y,tb,loc,st,mn,sc,rs) {	
	popWin=window.open(url,name,'toolbar='+tb+',location='+loc+',directories=0,status='+st+',menubar='+mn+',scrollbars='+sc+',resizable='+rs+',width='+x+',height='+y);
	self.name = "mainWin"; 
	popWin.opener = self;
	return popWin;
}

function isIE(ver) {
	browserVer = parseInt(navigator.appVersion);
	if (Prototype.Browser.IE && (browserVer >= ver)) {
		return true;
	}
	return false;
}
function URLencode(sStr) {
    return escape(sStr).replace(/\+/g, '%2B').replace(/\"/g,'%22').replace(/\'/g, '%27').replace(/\=/g, '%3D').replace(/\@/g, '%40');
}

function resizeIEFix() {
	var t_body = document.getElementsByTagName('body');
	body = t_body[0];
	var appVersion = navigator.appVersion;
	if ((appVersion.indexOf("MSIE") != -1) && (appVersion.indexOf("Macintosh") == -1))
	{
		if (document.getElementsByTagName)
		{
			var html = document.getElementsByTagName('html');
			if (html[0].offsetWidth) { width = html[0].offsetWidth; }
			if (html[0].offsetHeight) { height = html[0].offsetHeight; }
		}
		if (width < 1020) {	
			body.style.width = width - 4;
			if(Element.hasClassName(body, 'feauxframe')) {
				body.style.height = height - 53;
			} else {
				body.style.height = height - 4;				
			}
			body.hasFixedWidth = true;
		} else if(body.hasFixedWidth) {
			body.style.width = '';
			body.style.height = '';
			body.hasFixedWidth = false;
		}
	}
}

function makeJumpToNext(a, b, limit) {
	$(a).onkeyup = function(e) { 
		if((e.keyCode != 0 && e.keyCode != 9) && this.value.length == limit) {
			$(b).focus();
		}
	};
}

function findPos(obj) {
	var curleft = curtop = 0;
	if (obj.offsetParent) {
		curleft = obj.offsetLeft;
		curtop = obj.offsetTop;
		while (obj = obj.offsetParent) {
			curleft += obj.offsetLeft;
			curtop += obj.offsetTop;
		}
	}
	return [curleft,curtop];
}

// Date ISO8601 helpers, useful for sending this to/from JSON.

Date.prototype.setISO8601 = function(dString){

   var regexp = /(\d\d\d\d)(-)?(\d\d)(-)?(\d\d)(T)?(\d\d)(:)?(\d\d)(:)?(\d\d)(\.\d+)?(Z|([+-])(\d\d)(:)?(\d\d))/;

   if (dString.toString().match(new RegExp(regexp))) {
      var d = dString.match(new RegExp(regexp));
      var offset = 0;

      this.setUTCFullYear(parseInt(d[1]));
      this.setUTCMonth(d[3] - 1);
      this.setUTCDate(d[5]);
      this.setUTCHours(d[7]);
      this.setUTCMinutes(d[9]);
      this.setUTCSeconds(d[11]);
      if (d[12])
         this.setUTCMilliseconds(parseFloat(d[12]) * 1000);
      else
         this.setUTCMilliseconds(0);
      if (d[13] != 'Z') {
         offset = (d[15] * 60) + parseInt(d[17]);
         offset *= ((d[14] == '-') ? -1 : 1);
         this.setTime(this.getTime() - offset * 60 * 1000);
      }
   }
   else {
      this.setTime(Date.parse(dString));
   }
   return this;
};


Date.prototype.iso8601 = function() {
	return this.UTCstrftime('%Y-%m-%dT%H:%M:%SZ');
};

Date.dateWithISO8601 = function(isoDate) {
	new_date = new Date();
	new_date.setISO8601(isoDate);
	return new_date;
};

/* Class.create2: A add-on to prototype.js Class.create that makes for neater syntax
 * 2007 Patrick Quinn-Graham
 *
 * This software is hereby released into the public domain. Do with it as
 * you please, but with the understanding that it is provided "AS IS" and 
 * without any warranty of any kind.
 *
 * @DEPRECATED: This has now been rolled in to Prototype as of v1.6, you can
 * now just call Class.create({...});. You should only use this if you require
 * multiple mix-ins at creation time.
 *  
 **/

Class.create2 = function() { // create a new class with as many mix-ins as we want.
	var klass = Class.create();
	$A(arguments).each(function(a){
		Object.extend(klass.prototype, a);
	});
	return klass;
};

/* /Class.create2 */

function clickAndReplaceParent(el, loadingMess, theParentNode) {
	if(!theParentNode) theParentNode = el.parentNode;
	if(loadingMess) Element.update(theParentNode, "<img src=\"" + window.ASSET_BASE + "/global/aqua2spinner12.v09.01.12.gif\" alt=\"Loading...\" height=\"12\" width=\"12\"/>");
	new Ajax.Updater(theParentNode, el.href, {method: 'get', evalScripts: true});
}

PM.global = {
	verify: function(ident_num, page_log) {
		Event.observe(window, 'load', function(){
			verify_x = new Image(1,1);
			verify_x.src = '/verify?i='+ident_num+"&p="+page_log;
		});
	},
	log: function(page_url) {
	//	log_x = new Image(1,1);
	//	log_x.src = '/_do_log?i='+ident_num+'&p='+
	},
	
	newExtendedGraphWithWait: function(container, url, options, wait) {
				
		if($(wait).value == 0) {
			
			$(wait).value = 1;
		
			if(options) {
				if(options.createGraphDIV) {
					newDivOptions = Object.extend({'style': 'width:'+(options.width || '100px')+';height:'+(options.height || '100px')+';font-size:0.9em;'}, options.createDivOptions || {});
				
					parentContainer = container;
					container = new Element('div', newDivOptions);
					parentContainer.appendChild(container);
				}
			}
			
			Element.update(container, "<img style=\"background-color: #f2f2f2; padding: 10px;\" src=\"" + window.ASSET_BASE + "/global/aqua2spinner12.v09.01.12.gif\" alt=\"Loading...\" height=\"12\" width=\"12\"/>");
	
			new Ajax.Request(url, {method: 'get', onSuccess: function(transport) {
				 Element.update(container, "");
				//x = transport.responseJSON;
				x = transport.responseText.evalJSON();
	
				if(x.fileFormat == "com.tankerworld.graph.json") {
					//alert('drawing...');
					using = {};
					if(options.overrides) {
						using = Object.extend(x, options.overrides);
					} else {
						using = x;
					}
					
					f = FlotrExtended.draw(container,x.data,using);
					if(options && options.callback) {
						options.callback(f);
					}
				} else if(x.fileFormat == "com.tankerworld_nograph1.graph.json") {
				
					Element.update(container, "<img src=\"" + window.ASSET_BASE + "/bw/prices/graph1_nodata.png\">");
				
				} else if(x.fileFormat == "com.tankerworld_nograph2.graph.json") {
				
					Element.update(container, "<img src=\"" + window.ASSET_BASE + "/bw/prices/graph2_nodata.png\">");
				
				} else {
					alert("There was an error loading the graph, please try again. (-10)");
				}
			},
			
			onComplete: function() { $(wait).value = 0; }
			
			
			});
			
		}
		
	},
	
	newExtendedGraph: function(container, url, options) {
		
		if(options) {
			if(options.createGraphDIV) {
				newDivOptions = Object.extend({'style': 'width:'+(options.width || '100px')+';height:'+(options.height || '100px')+';font-size:0.9em;'}, options.createDivOptions || {});
			
				parentContainer = container;
				container = new Element('div', newDivOptions);
				parentContainer.appendChild(container);
			}
		}
		
		Element.update(container, "<img style=\"background-color: #f2f2f2; padding: 10px;\" src=\"" + window.ASSET_BASE + "/global/aqua2spinner12.v09.01.12.gif\" alt=\"Loading...\" height=\"12\" width=\"12\"/>");

		new Ajax.Request(url, {method: 'get', onSuccess: function(transport) {
			 Element.update(container, "");
			//x = transport.responseJSON;
			x = transport.responseText.evalJSON();

			if(x.fileFormat == "com.tankerworld.graph.json") {
				//alert('drawing...');
				using = {};
				if(options.overrides) {
					using = Object.extend(x, options.overrides);
				} else {
					using = x;
				}
				
				f = FlotrExtended.draw(container,x.data,using);
				if(options && options.callback) {
					options.callback(f);
				}
			} else if(x.fileFormat == "com.tankerworld_nograph1.graph.json") {
			
				Element.update(container, "<img src=\"" + window.ASSET_BASE + "/bw/prices/graph1_nodata.png\">");
			
			} else if(x.fileFormat == "com.tankerworld_nograph2.graph.json") {
			
				Element.update(container, "<img src=\"" + window.ASSET_BASE + "/bw/prices/graph2_nodata.png\">");
			
			} else if(x.fileFormat == "com.tankerworld_noqualgraph.graph.json") {
				
				Element.update(container, "<img src=\"" + window.ASSET_BASE + "/bw/quality/graph_nodata.png\">");
				
			} else if(x.fileFormat == "com.tankerworld_noqualgraph_sml.graph.json") {
				
				Element.update(container, "<img src=\"" + window.ASSET_BASE + "/bw/quality/graph_nodata_sml.png\">");
				
			} else {
				alert("There was an error loading the graph, please try again. (-10)");
			}
		}});
	},
		
	newGraph: function(container, url, options) {
		
		if(options) {
			if(options.createGraphDIV) {
				newDivOptions = Object.extend({'style': 'width:'+(options.width || '100px')+';height:'+(options.height || '100px')+';font-size:0.9em;'}, options.createDivOptions || {});
			
				parentContainer = container;
				container = new Element('div', newDivOptions);
				parentContainer.appendChild(container);
			}
		}
		
		 Element.update(container, "<img style=\"background-color: #f2f2f2; padding: 10px;\" src=\"" + window.ASSET_BASE + "/global/aqua2spinner12.v09.01.12.gif\" alt=\"Loading...\" height=\"12\" width=\"12\"/>");

		new Ajax.Request(url, {method: 'get', onSuccess: function(transport) {
			 Element.update(container, "");
			//x = transport.responseJSON;
			x = transport.responseText.evalJSON();

			if(x.fileFormat == "com.tankerworld.graph.json") {
				//alert('drawing...');
				using = {};
				if(options.overrides) {
					using = Object.extend(x, options.overrides);
				} else {
					using = x;
				}
				
				f = Flotr.draw(container,x.data,using);
				if(options && options.callback) {
					options.callback(f);
				}
			} else if(x.fileFormat == "com.tankerworld_nograph1.graph.json") {
			
				Element.update(container, "<img src=\"" + window.ASSET_BASE + "/bw/prices/graph1_nodata.png>");
			
			} else if(x.fileFormat == "com.tankerworld_nograph2.graph.json") {
			
				Element.update(container, "<img src=\"" + window.ASSET_BASE + "/bw/prices/graph2_nodata.png>");
			
			} else if(x.fileFormat == "com.tankerworld_noqualgraph.graph.json") {
				
				Element.update(container, "<img src=\"" + window.ASSET_BASE + "/bw/quality/graph_nodata.png\">");
				
			} else if(x.fileFormat == "com.tankerworld_noqualgraph_sml.graph.json") {
				
				Element.update(container, "<img src=\"" + window.ASSET_BASE + "/bw/quality/graph_nodata_sml.png\">");
				
			} else {
				alert("There was an error loading the graph, please try again. (-10)");
			}
		}});
	},
	
	reportForumPost: function(report_script, thread_id, post_seq, report_button) {
		$(report_button).update('Reporting...');
		new Ajax.Request(report_script, {
			method: 'post',
			parameters: { 'thread': thread_id, 'post': post_seq },
			onComplete: function(t) {
				r = t.responseJSON;
				if(r.error) {
					alert("An error occurred trying to report this content. Please reload the page and try again.");
				} else {
					$(report_button).update('Thank you for bringing this to our attention. A member of our team will review this post.');	
				}
			}.bind(this)
		});
	},
	
	reportClassifiedAd: function(report_script, ad_id, report_button) {
		$(report_button).update('Reporting...');
		new Ajax.Request(report_script, {
			method: 'post',
			parameters: { 'ad': ad_id },
			onComplete: function(t) {
				r = t.responseJSON;
				if(r.error) {
					alert("An error occurred trying to report this content. Please reload the page and try again.");
				} else {
					$(report_button).update('Thank you for bringing this to our attention. A member of our team will review this ad.');	
				}
			}.bind(this)
		});
	},
	
	radioHasChosen: function(form_radio_name) {
		anyChecked=false;
		$$('input[name="' + form_radio_name + '"]').each(function(a){
			if($(a).checked){
				anyChecked=true;
			}
		});
		return anyChecked;
	},
	
	countdownTimer: function(countdown_id, targetdate, starttext) {
		
		// Get date objects for the current (user) date and the target date.
		this.localtime = new Date();
		this.targetdate = new Date(targetdate);
		
		// Adjust the local date for the difference between time zones. 
		this.localtime.setSeconds(this.localtime.getSeconds() + (this.localtime.getTimezoneOffset() * 60));
		
		// Check to make sure that the target date has not already passed. 
		var remaining = (this.targetdate-this.localtime)/1000;
		if (remaining > 0){
			setTimeout(function(){PM.global.updateCountdown(countdown_id, starttext); }, 1);
		} 
	},
	
	updateCountdown: function(countdown_id, starttext) {
		
		//Check time difference so we don't go past the target date.
		var remaining = (this.targetdate-this.localtime)/1000;		

		// Increment time by one second
		this.localtime.setSeconds(this.localtime.getSeconds()+1);
		
		if (remaining > 0) {
			
			// Get the time remaining in days/hours/minutes/seconds.
			var days = Math.floor(remaining/(60*60*24));//Days until target date
			var hours = Math.floor((remaining-(days*60*60*24))/(60*60));//Hours until target date
			var minutes = Math.floor((remaining-(days*60*60*24)-(hours*60*60))/60);//Minutes until target date
			var seconds = Math.floor(remaining-(days*60*60*24)-(hours*60*60)-(minutes*60));//Seconds until target date
		
			// Left pad numbers with zeros where needed.
			if (hours.toString().length==1) hours = '0' + hours;
			if (minutes.toString().length==1) minutes = '0' + minutes;
			if (seconds.toString().length==1) seconds = '0' + seconds;
			
			// Format the countdown clock, and call the update function again 1 second later.
			$(countdown_id).update(starttext + days + ' days, ' + hours + ':' + minutes + ':' + seconds);
			setTimeout(function(){PM.global.updateCountdown(countdown_id, starttext);}, 1000);
		}
	}
		
		
}; 

PM.global.BasicTabController = Class.create({
	initialize: function() {
		this.basicTabs = Array();
		this.currentTab = 1;
	},
	registerTab : function(tab) {
		this.basicTabs[this.basicTabs.length] = tab;
	},
	setupTabs : function() {
		this.basicTabs.each(function(t){
			t.setup();
		});
	}
});

PM.global.BasicTab = Class.create({
	initialize: function(aTab, aGrip, aIndex, options) {
		this.tab = aTab;
		this.grip = aGrip;
		this.tabIndex = aIndex;
		Object.extend(this, options || {});
		this.btc.registerTab(this);
	},
	setup: function() {
		$(this.tab).makeSwitchingTab(function() { this.onClick(); }.bind(this), 
	 							     function() { this.offClick(); }.bind(this), 'inactiveTab', 'activeTab');
	},
	hide: function() {
		this.offClick();
		$(this.tab).style.display = 'none';
	},
	show: function() {
		$(this.tab).style.display = 'block';
	},
	onClick: function() {
		this.btc.currentTab = this.tabIndex;
		$(this.grip).style.display = 'block';
		this.onActivate();
	},
	offClick: function() {
		//info("Should hide myself " + this.tab);
		$(this.grip).style.display = 'none';
	},
	clear: function() {
		$(this.grip).innerHTML = "&nbsp;";
	},
	activate: function() {
		$(this.tab).onclick();
	},
	onActivate: function() {
		// by default do nothing
	}
});

/*
 dateSwatch is a "helper" for using the Yahoo! UI calendar widget
 Make sure you have, at least, the yahoo, dom, event, and calendar modules from Y!UI.
*/
dateSwatch = Class.create({
	initialize: function(container, lnk, input, defaultDate, title, inputAsCallBack, align) {
		y = YAHOO;
		if(!(y && y.widget && y.widget.Calendar && y.util && y.util.Event)) {
			alert("This control requires several Yahoo! UI javascript components");
		}
		container = $(container);
		this.container = container;
		this.lnk = $(lnk);
		this.alignLeft = (align == "left");
		if(inputAsCallBack) {
			this.inputField = input;
			this.setAsValue = false;
			//info("inputAsRef: TRUE");
		} else {
			this.inputField = $(input);
			this.setAsValue = true;
		}
		this.calendar = new y.widget.Calendar(container.id + "-calendar", container.id, {title: title, close: true});
		this.setDate(defaultDate);
		this.calendar.selectEvent.subscribe(this.dateChange.bind(this), this.calendar.call, true);
		y.util.Event.addListener(lnk, "click", this.showCal.bind(this), this.calendar, true);	
		
		if (navigator.appName == "Microsoft Internet Explorer") {
			this.scrollCheckInterval = setInterval(this.scrollCheck.bind(this), 10);
			this.oldScrollPos = 0;
		}

	},
	scrollCheck: function() {
		if(this.oldScrollPos != document.body.scrollTop) {
			this.oldScrollPos = document.body.scrollTop;
			if(this.onScrollChange) this.onScrollChange();
		}
	},
	setDate: function(defaultDate) {
		if(defaultDate && defaultDate != "") {
			v = defaultDate.split("-");
			fdate = v[1] + "/" + v[2] + "/" + v[0];
			this.calendar.select(fdate);
			var firstDate = this.calendar.getSelectedDates()[0]; 
			this.calendar.cfg.setProperty("pagedate", v[1] + "/" + v[0]);
		}	
		this.calendar.render();
	},
	dateChange: function(t, args) {
		val = args[0][0];
		val[1] = (val[1] < 10) ? 0 + '' + val[1] : val[1];
		val[2] = (val[2] < 10) ? 0 + '' + val[2] : val[2];
		
		if(this.setAsValue) {
			this.inputField.value = val[0] + "-" + val[1] + "-" + val[2];
		} else {	
			this.inputField(val[0] + "-" + val[1] + "-" + val[2]);
		}			
		this.lnk.firstChild.nodeValue = val[0] + "-" + val[1] + "-" + val[2];
		this.calendar.hide();
		this.onScrollChange = null;
	},
	showCal: function() {
		this.calendar.show();
		lnky = findPos(this.lnk);
		this.container.style.top = (lnky[1] + this.lnk.offsetHeight) + "px";
		
		this.onScrollChange = function () {
			lnky = findPos(this.lnk);
			this.container.style.top = (lnky[1] + this.lnk.offsetHeight - this.oldScrollPos) + "px";
		};
		
		if(this.alignLeft) {
			this.container.style.left = (lnky[0]) + "px";
		} else {
			this.container.style.left = ((lnky[0] + this.lnk.offsetWidth) - this.container.offsetWidth) + "px";	
		}		
		return false;
	}
});

var bwSite = {};
bwSite.verify = PM.global.verify;

if(Prototype.Browser.IE) {
	Event.observe(window, 'load', resizeIEFix);
	Event.observe(window, 'resize', resizeIEFix);
}

bwSite.setupFlipper = function() {
	if(!$('mainMenuBar_grip') || !$('mainMenuBar')) return false;
	addToggler('mainMenuBar_grip', 'mainMenuBar', true, false);
	$('mainMenuBar').fx.setOptions({duration: 500, onComplete:function(){
		// work out if we've collapsed
		if($('mainMenuBar').offsetHeight == 20) {
			Element.addClassName(document.getElementsByTagName('body')[0], 'smallMenu');
			createCookie('smallMenu', 'y', 365);
		} else {
			Element.removeClassName(document.getElementsByTagName('body')[0], 'smallMenu');
			createCookie('smallMenu', 'n', 365);
		}
	}});	
};

document.observe('dom:loaded', bwSite.setupFlipper);


PM.emissionsCalculator = {
	
	calc: function(typeHolder, volHolder, sulphurHolder, resultsHolder) {
		
		carb = 82;
		if ($(typeHolder).value=="IFO") carb = 86.5;

		co2 = Math.round($(volHolder).value*(carb/100)*3.667,1);

		sox = Math.round($(volHolder).value*($(sulphurHolder).value/100)*2.05,1);					

		$(resultsHolder).innerHTML="<div style=\'font-size: 1.0em; font-weight: bold;\'>Your Emissions Are:</div><div style=\'\'>CO<sub>2</sub>&nbsp;&nbsp;<strong>"+co2+"&nbsp;mt</strong><br/>SO<sub>x</sub>&nbsp;&nbsp;<strong>"+sox+"&nbsp;mt</strong></div>";
		
	}
	
};var isIE  = (navigator.appVersion.indexOf("MSIE") != -1) ? true : false;
var isWin = (navigator.appVersion.toLowerCase().indexOf("win") != -1) ? true : false;
var isOpera = (navigator.userAgent.indexOf("Opera") != -1) ? true : false;
jsVersion = 1.1;
// JavaScript helper required to detect Flash Player PlugIn version information
function JSGetSwfVer(i){
	// NS/Opera version >= 3 check for Flash plugin in plugin array
	if (navigator.plugins != null && navigator.plugins.length > 0) {
		if (navigator.plugins["Shockwave Flash 2.0"] || navigator.plugins["Shockwave Flash"]) {
			var swVer2 = navigator.plugins["Shockwave Flash 2.0"] ? " 2.0" : "";
      		var flashDescription = navigator.plugins["Shockwave Flash" + swVer2].description;
			descArray = flashDescription.split(" ");
			tempArrayMajor = descArray[2].split(".");
			versionMajor = tempArrayMajor[0];
			versionMinor = tempArrayMajor[1];
			if ( descArray[3] != "" ) {
				tempArrayMinor = descArray[3].split("r");
			} else {
				tempArrayMinor = descArray[4].split("r");
			}
      		versionRevision = tempArrayMinor[1] > 0 ? tempArrayMinor[1] : 0;
            flashVer = versionMajor + "." + versionMinor + "." + versionRevision;
      	} else {
			flashVer = -1;
		}
	}
	// MSN/WebTV 2.6 supports Flash 4
	else if (navigator.userAgent.toLowerCase().indexOf("webtv/2.6") != -1) flashVer = 4;
	// WebTV 2.5 supports Flash 3
	else if (navigator.userAgent.toLowerCase().indexOf("webtv/2.5") != -1) flashVer = 3;
	// older WebTV supports Flash 2
	else if (navigator.userAgent.toLowerCase().indexOf("webtv") != -1) flashVer = 2;
	// Can't detect in all other cases
	else {
		
		flashVer = -1;
	}
	return flashVer;
} 
// If called with no parameters this function returns a floating point value 
// which should be the version of the Flash Player or 0.0 
// ex: Flash Player 7r14 returns 7.14
// If called with reqMajorVer, reqMinorVer, reqRevision returns true if that version or greater is available
function DetectFlashVer(reqMajorVer, reqMinorVer, reqRevision) 
{
 	reqVer = parseFloat(reqMajorVer + "." + reqRevision);
   	// loop backwards through the versions until we find the newest version	
	for (i=25;i>0;i--) {	
		if (isIE && isWin && !isOpera) {
			versionStr = VBGetSwfVer(i);
		} else {
			versionStr = JSGetSwfVer(i);		
		}
		if (versionStr == -1 ) { 
			return false;
		} else if (versionStr != 0) {
			if(isIE && isWin && !isOpera) {
				tempArray         = versionStr.split(" ");
				tempString        = tempArray[1];
				versionArray      = tempString .split(",");				
			} else {
				versionArray      = versionStr.split(".");
			}
			versionMajor      = versionArray[0];
			versionMinor      = versionArray[1];
			versionRevision   = versionArray[2];
			
			versionString     = versionMajor + "." + versionRevision;   // 7.0r24 == 7.24
			versionNum        = parseFloat(versionString);
        	// is the major.revision >= requested major.revision AND the minor version >= requested minor
			if ( (versionMajor > reqMajorVer) && (versionNum >= reqVer) ) {
				return true;
			} else {
				return ((versionNum >= reqVer && versionMinor >= reqMinorVer) ? true : false );	
			}
		}
	}	
	return (reqVer ? false : 0.0);
}



function writeFlash(data, width, height, bgcolor, returnHTML) {
	res = '<div style="margin: 0 auto; padding: 0 !important; background-color: '+ bgcolor+ '; width: '+width+'px; height: '+height+'px; overflow: hidden;">'
	res = res + '<object style="margin:0;padding:0;" type="application/x-shockwave-flash" data="' + data + '" width="'+ width +'" height="' + height + '">';
	res = res + '<param name="quality" value="high" />';
	res = res + '<param name="bgcolor" value="' + bgcolor + '" />';
	res = res + '<param name="allowScriptAccess" value="sameDomain" />';
	res = res + '<param name="menu" value="false"/>';
	res = res + '<param name="wmode" value="transparent"/>';
	res = res + '<param name="movie" value="' + data + '" />';
	res = res + '</object>';
	res = res + '</div>';
	if(returnHTML) return res;
	
	document.write(res);
}
/*
Copyright (c) 2005 JSON.org
Permission is hereby granted, free of charge, to any person obtaining a copyof this software and associated documentation files (the "Software"), to dealin the Software without restriction, including without limitation the rightsto use, copy, modify, merge, publish, distribute, sublicense, and/or sellcopies of the Software, and to permit persons to whom the Software isfurnished to do so, subject to the following conditions:
The Software shall be used for Good, not Evil.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS ORIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THEAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHERLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THESOFTWARE.
*/
/*    The global object JSON contains two methods.
    JSON.stringify(value) takes a JavaScript value and produces a JSON text.    The value must not be cyclical.
    JSON.parse(text) takes a JSON text and produces a JavaScript value. It will    throw a 'JSONError' exception if there is an error.*/var JSON = {    copyright: '(c)2005 JSON.org',    license: 'http://www.crockford.com/JSON/license.html',/*    Stringify a JavaScript value, producing a JSON text.*/    stringify: function (v) {        var a = [];/*    Emit a string.*/        function e(s) {            a[a.length] = s;        }/*    Convert a value.*/        function g(x) {            var c, i, l, v;            switch (typeof x) {            case 'object':                if (x) {                    if (x instanceof Array) {                        e('[');                        l = a.length;                        for (i = 0; i < x.length; i += 1) {                            v = x[i];                            if (typeof v != 'undefined' &&                                    typeof v != 'function') {                                if (l < a.length) {                                    e(',');                                }                                g(v);                            }                        }                        e(']');                        return;                    } else if (typeof x.valueOf == 'function') {                        e('{');                        l = a.length;                        for (i in x) {                            v = x[i];                            if (typeof v != 'undefined' &&                                    typeof v != 'function' &&                                    (!v || typeof v != 'object' ||                                        typeof v.valueOf == 'function')) {                                if (l < a.length) {                                    e(',');                                }                                g(i);                                e(':');                                g(v);                            }                        }                        return e('}');                    }                }                e('null');                return;            case 'number':                e(isFinite(x) ? +x : 'null');                return;            case 'string':                l = x.length;                e('"');                for (i = 0; i < l; i += 1) {                    c = x.charAt(i);                    if (c >= ' ') {                        if (c == '\\' || c == '"') {                            e('\\');                        }                        e(c);                    } else {                        switch (c) {                        case '\b':                            e('\\b');                            break;                        case '\f':                            e('\\f');                            break;                        case '\n':                            e('\\n');                            break;                        case '\r':                            e('\\r');                            break;                        case '\t':                            e('\\t');                            break;                        default:                            c = c.charCodeAt();                            e('\\u00' + Math.floor(c / 16).toString(16) +                                (c % 16).toString(16));                        }                    }                }                e('"');                return;            case 'boolean':                e(String(x));                return;            default:                e('null');                return;            }        }        g(v);        return a.join('');    },/*    Parse a JSON text, producing a JavaScript value.*/    parse: function (text) {        return (/^(\s+|[,:{}\[\]]|"(\\["\\\/bfnrtu]|[^\x00-\x1f"\\]+)*"|-?\d+(\.\d*)?([eE][+-]?\d+)?|true|false|null)+$/.test(text)) &&            eval('(' + text + ')');    }};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,
                barLabelsCentered: false
            },
            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
                        }
                        var barinc = B / 2;
                        if(H.x.options.barLabelsCentered) barinc = 0;
                        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) - barinc) + "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"]
};/* $Id: flotr_extended.js,v 1.21 2010/09/02 20:15:00 jen Exp $ */

/** 
 * @author Bas Wenneker
 * @license MIT License <http://www.opensource.org/licenses/mit-license.php>
 * @version 0.2.0
 */
var FlotrExtended = {
	version: '%version%',
	author: 'Bas Wenneker',
	website: 'http://www.solutoire.com',
	isIphone: /iphone/i.test(navigator.userAgent),
	
	/**
	 * An object of the registered graph types. Use FlotrExtended.addType(type, object)
	 * to add your own type.
	 */
	graphTypes: {},
	
	/**
	 * The list of the registered plugins
	 */
	plugins: {},
	
	/**
	 * Can be used to add your own chart type. 
	 * @param {String} name - Type of chart, like 'pies', 'bars' etc.
	 * @param {String} graphType - The object containing the basic drawing functions (draw, etc)
	 */
	addType: function(name, graphType){
		FlotrExtended.graphTypes[name] = graphType;
		FlotrExtended.defaultOptions[name] = graphType.options || {};
		FlotrExtended.defaultOptions.defaultType = FlotrExtended.defaultOptions.defaultType || name;
	},
	
	/**
	 * Can be used to add a plugin
	 * @param {String} name - The name of the plugin
	 * @param {String} plugin - The object containing the plugin's data (callbacks, options, function1, function2, ...)
	 */
	addPlugin: function(name, plugin){
		FlotrExtended.plugins[name] = plugin;
		FlotrExtended.defaultOptions[name] = plugin.options || {};
	},
	
	/**
	 * Draws the graph. This function is here for backwards compatibility with Flotr version 0.1.0alpha.
	 * You could also draw graphs by directly calling FlotrExtended.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 FlotrExtended.Graph
	 * @return {Object} returns a new graph object and of course draws the graph.
	 */
	draw: function(el, data, options, GraphKlass){	
		GraphKlass = GraphKlass || FlotrExtended.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){
			serie = (serie.data) ? Object.clone(serie) : {data: serie};
			for (var 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 i, v, result = dest || {};
		for(i in src){
			v = src[i];
			result[i] = (v && typeof(v) === 'object' && !(v.constructor === Array || v.constructor === RegExp) && !Object.isElement(v)) ? FlotrExtended.merge(v, dest[i]) : result[i] = v;
		}
		return result;
	},
	
	/**
	 * Recursively clones an object.
	 * @param {Object} object - The object to clone
	 * @return {Object} the clone
	 */
	clone: function(object){
		var i, v, clone = {};
		for(i in object){
			v = object[i];
			clone[i] = (v && typeof(v) === 'object' && !(v.constructor === Array || v.constructor === RegExp) && !Object.isElement(v)) ? FlotrExtended.clone(v) : v;
		}
		return clone;
	},
	
	/**
	 * 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,
        magn = FlotrExtended.getMagnitude(delta),
        tickSize = 10,
		    norm = delta / magn; // Norm is between 1.0 and 10.0.
		    
		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+')';
	}, 
	
	/**
	 * Utility function to convert file size values in bytes to kB, MB, ...
	 * @param value {Number} - The value to convert
	 * @param precision {Number} - The number of digits after the comma (default: 2)
	 * @param base {Number} - The base (default: 1000)
	 */
	engineeringNotation: function(value, precision, base){
		var sizes =         ['Y','Z','E','P','T','G','M','k',''],
		    fractionSizes = ['y','z','a','f','p','n','u','m',''],
		    total = sizes.length;

		base = base || 1000;
		precision = Math.pow(10, precision || 2);

		if (value == 0) return 0;

		if (value > 1) {
			while (total-- && (value >= base)) value /= base;
		}
		else {
			sizes = fractionSizes;
			total = sizes.length;
			while (total-- && (value < 1)) value *= base;
		}

		return (Math.round(value * precision) / precision) + sizes[total];
	},
	
	/**
	 * 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);
	},
	floorInBase: function(n, base) {
		return base * Math.floor(n / base);
	},
	drawText: function(ctx, text, x, y, style) {
		if (!ctx.fillText || FlotrExtended.isIphone) {
			ctx.drawText(text, x, y, style);
			return;
		}
		
		style = Object.extend({
			size: '10px',
			color: '#000000',
			textAlign: 'left',
			textBaseline: 'bottom',
			weight: 1,
			angle: 0
		}, style);
		
		ctx.save();
		ctx.translate(x, y);
		ctx.rotate(style.angle);
		ctx.fillStyle = style.color;
		ctx.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif";
		ctx.textAlign = style.textAlign;
		ctx.textBaseline = style.textBaseline;
		ctx.fillText(text, 0, 0);
		ctx.restore();
	},
	measureText: function(ctx, text, style) {
		if (!ctx.fillText || FlotrExtended.isIphone) {
			return {width: ctx.measure(text, style)};
		}
		
		style = Object.extend({
			size: '10px',
			weight: 1,
			angle: 0
		}, style);
		
		ctx.save();
		ctx.rotate(style.angle);
		ctx.font = (style.weight > 1 ? "bold " : "") + (style.size*1.3) + "px sans-serif";
		var metrics = ctx.measureText(text);
		ctx.restore();
		return metrics;
	},
	getBestTextAlign: function(angle, style) {
    style = style || {textAlign: 'center', textBaseline: 'middle'};
    angle += FlotrExtended.getTextAngleFromAlign(style);
    
    if (Math.abs(Math.cos(angle)) > 10e-3) 
      style.textAlign    = (Math.cos(angle) > 0 ? 'right' : 'left');
    
    if (Math.abs(Math.sin(angle)) > 10e-3) 
      style.textBaseline = (Math.sin(angle) > 0 ? 'top' : 'bottom');
    
    return style;
  },
  alignTable: {
    'right middle' : 0,
    'right top'    : Math.PI/4,
    'center top'   : Math.PI/2,
    'left top'     : 3*(Math.PI/4),
    'left middle'  : Math.PI,
    'left bottom'  : -3*(Math.PI/4),
    'center bottom': -Math.PI/2,
    'right bottom' : -Math.PI/4,
    'center middle': 0
  },
  getTextAngleFromAlign: function(style) {
    return FlotrExtended.alignTable[style.textAlign+' '+style.textBaseline] || 0;
  }
};

FlotrExtended.defaultOptions = {
	colors: ['#00A8F0', '#C0D800', '#CB4B4B', '#4DA74D', '#9440ED'], //=> The default colorscheme. When there are > 5 series, additional colors are generated.
	title: null,             // => The graph's title
	subtitle: null,          // => The graph's subtitle
	shadowSize: 4,           // => size of the 'fake' shadow
	defaultType: null,       // => default series type
	HtmlText: true,          // => wether to draw the text using HTML or on the canvas
	fontSize: 7.5,           // => canvas' text font size
	resolution: 1,           // => resolution of the graph, to have printer-friendly graphs !
	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: function(v){return v}, // => 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: FlotrExtended.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,           // => color of the ticks
		mode: 'normal',        // => can be 'time' or 'normal'
		timeFormat: null,
		scaling: 'linear',     // => Scaling, can be 'linear' or 'logarithmic'
		base: Math.E,
		barLabelsCentered: false	// Are the labels for each bar in the middle of the bar?  If false, they are at the start of the bar
	},
	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: FlotrExtended.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,           // => The color of the ticks
		scaling: 'linear',     // => Scaling, can be 'linear' or 'logarithmic'
		base: Math.E,
        labelWidth: null,		// Do labels have a default width?
        labelWidthBothSides: 0   // Do we apply the label width to both sides?
	},
	y2axis: {
		titleAngle: 270
	},
	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
		circular: false,       // => if set to true, the grid will be circular, must be used when radars are drawn
        xaxisOutline: 0,       // How thick should the outline be on the xaxis?
        yaxisOutline: 0        // How thick should the outline be on the yaxis?
	},
	selection: {
		mode: null,            // => one of null, 'x', 'y' or 'xy'
		color: '#B6D9FF',      // => selection box color
		fps: 20                // => frames-per-second
	},
	crosshair: {
		mode: null,            // => one of null, 'x', 'y' or 'xy'
		color: '#FF0000',      // => crosshair color
		hideCursor: true       // => hide the cursor when the crosshair is shown
	},
	mouse: {
		track: false,          // => true to track the mouse, no tracking otherwise
		trackAll: false,
		position: 'se',        // => position of the value box (default south-east)
		relative: false,       // => next to the mouse cursor
		trackFormatter: FlotrExtended.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
		fillColor: null,       // => color to fill our select bar with only applies to bar and similar graphs (only bars for now)
		fillOpacity: 0.4,       // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill,
        showLine: false,		// Are we showing a line down the whole graph on hit?
        mouseBoxSize: null 		// How wide is the mousebox?
		
	}
};

/**
 * Flotr Graph class that plots a graph on creation.
 */
FlotrExtended.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){
    try {
		this.el = $(el);
    
		if (!this.el) throw 'The target container doesn\'t exist';
		if (!this.el.clientWidth) throw 'The target container must be visible';

		this.registerPlugins();
    
		this.el.fire('flotr:beforeinit', [this]);
    
		// Initialize some variables
		this.el.graph = this;
		this.data = data;
		this.lastMousePos = { pageX: null, pageY: null };
		this.selection = { first: { x: -1, y: -1}, second: { x: -1, y: -1} };
		this.plotOffset = {left: 0, right: 0, top: 0, bottom: 0};
		this.prevSelection = null;
		this.selectionInterval = null;
		this.ignoreClick = false;   
		this.prevHit = null;
		this.series = FlotrExtended.getSeries(data);
		this.setOptions(options);
    
		var type, p;
		for (type in FlotrExtended.graphTypes) {
			this[type] = Object.clone(FlotrExtended.graphTypes[type]);
			for (p in this[type]) {
				if (Object.isFunction(this[type][p]))
					this[type][p] = this[type][p].bind(this);
			}
		}
    
		// Create and prepare canvas.
		this.constructCanvas();
    
		this.el.fire('flotr:afterconstruct', [this]);
		
		// 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.setupAxes();
    
		this.draw();
		this.insertLegend();
    
		this.el.fire('flotr:afterinit', [this]);
    } catch (e) {
      try {
        console.error(e);
      } catch (e) {}
    }
	},
	/**
	 * Sets options and initializes some variables and color specific values, used by the constructor. 
	 * @param {Object} opts - options object
	 */
	setOptions: function(opts){
		var options = FlotrExtended.clone(FlotrExtended.defaultOptions);
		options.x2axis = Object.extend(Object.clone(options.xaxis), options.x2axis);
		options.y2axis = Object.extend(Object.clone(options.yaxis), options.y2axis);
		this.options = FlotrExtended.merge(opts || {}, options);
		
		// The 4 axes of the plot
		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;

		// Collect user-defined colors from series.
		for(i = neededColors - 1; i > -1; --i){
			c = this.series[i].color;
			if(c){
				--neededColors;
				if(Object.isNumber(c)) assignedColors.push(c);
				else usedColors.push(FlotrExtended.Color.parse(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 FlotrExtended.Color(100, 100, 100) : FlotrExtended.Color.parse(oc[i]);
			
			// Make sure each serie gets a different color.
			var sign = variation % 2 == 1 ? -1 : 1,
          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();
			}
			
			// Every series needs an axis
			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.
			for (var t in FlotrExtended.graphTypes){
				s[t] = Object.extend(Object.clone(this.options[t]), s[t]);
			}
			s.mouse = Object.extend(Object.clone(this.options.mouse), s.mouse);
			
			if(s.shadowSize == null) s.shadowSize = this.options.shadowSize;
		}
	},
	setupAxes: function(){
		/**
		 * Translates data number to pixel number
		 * @param {Number} v - data number
		 * @return {Number} translated pixel number
		 */
		function d2p(v, o){
			if (o.scaling === 'logarithmic') {
				v = Math.log(Math.max(v, Number.MIN_VALUE));
				if (o.base !== Math.E) 
					v /= Math.log(o.base);
			}
			return v;
		}

		/**
		 * Translates pixel number to data number
		 * @param {Number} v - pixel data
		 * @return {Number} translated data number
		 */
		function p2d(v, o){
			if (o.scaling === 'logarithmic')
				v = (o.base === Math.E) ? Math.exp(v) : Math.pow(o.base, v);
			return v;
		}

		this.axes.x.d2p = this.axes.x2.d2p = function(x){
			return (d2p(x, this.options) - this.min) * this.scale;
		};

		this.axes.x.p2d = this.axes.x2.p2d = function(x){
			return (p2d(x, this.options) / this.scale + this.min);
		};

		var ph = this.plotHeight;
		this.axes.y.d2p = this.axes.y2.d2p = function(y){
			return ph - (d2p(y, this.options) - this.min) * this.scale;
		};

		this.axes.y.p2d = this.axes.y2.p2d = function(y){
			return p2d(this.max -y / this.scale, this.options);
		};
	},
	/**
	 * 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;
		
		// The old canvases are retrieved to avoid memory leaks ...
		this.canvas = el.select('.flotr-canvas')[0];
		this.overlay = el.select('.flotr-overlay')[0];
		
		// ... and all the child elements are removed
		el.descendants().invoke('remove');

		// For positioning labels and overlay.
		el.style.position = 'relative';
		el.style.cursor = el.style.cursor || 'default';

		size = el.getDimensions();
		this.canvasWidth = size.width;
		this.canvasHeight = size.height;

		var style = {
			width: size.width+'px',
			height: size.height+'px'
		};

		var o = this.options;
		size.width *= o.resolution;
		size.height *= o.resolution;

		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 = $(document.createElement('canvas')); // Do NOT use new Element()
			c.className = 'flotr-canvas';
			c.style.cssText = 'position:absolute;left:0px;top:0px;';
		}
		c = this.canvas.writeAttribute(size).show().setStyle(style);
		c.context_ = null; // Reset the ExCanvas context
		el.insert(c);
    
		// Insert overlay canvas for interactive features.
		if (!this.overlay) {
			oc = this.overlay = $(document.createElement('canvas')); // Do NOT use new Element()
			oc.className = 'flotr-overlay';
			oc.style.cssText = 'position:absolute;left:0px;top:0px;';
		}
		oc = this.overlay.writeAttribute(size).show().setStyle(style);
		oc.context_ = null; // Reset the ExCanvas context
		el.insert(oc);
		
		if(Prototype.Browser.IE){
			window.G_vmlCanvasManager.initElement(c);
			window.G_vmlCanvasManager.initElement(oc);
		}
		this.ctx = c.getContext('2d');
		this.octx = oc.getContext('2d');
    
		if(!Prototype.Browser.IE){
			this.ctx.scale(o.resolution, o.resolution);
			this.octx.scale(o.resolution, o.resolution);
		}

		// Enable text functions
		this.textEnabled = !!this.ctx.drawText;
	},
  processColor: function(color, options){
    if (!color) return 'rgba(0, 0, 0, 0)';
    
    options = Object.extend({
      x1: 0, y1: 0, x2: this.plotWidth, y2: this.plotHeight, opacity: 1, ctx: this.ctx
    }, options);
    
    if (color instanceof FlotrExtended.Color) return color.adjust(null, null, null, options.opacity).toString();
    if (Object.isString(color)) return FlotrExtended.Color.parse(color).scale(null, null, null, options.opacity).toString();
		
    var grad = color.colors ? color : {colors: color};
    
    if (!options.ctx) {
      if (!Object.isArray(grad.colors)) return 'rgba(0, 0, 0, 0)';
      return FlotrExtended.Color.parse(Object.isArray(grad.colors[0]) ? grad.colors[0][1] : grad.colors[0]).scale(null, null, null, options.opacity).toString();
    }
    grad = Object.extend({start: 'top', end: 'bottom'}, grad); 
    
    if (/top/i.test(grad.start))  options.x1 = 0;
    if (/left/i.test(grad.start)) options.y1 = 0;
    if (/bottom/i.test(grad.end)) options.x2 = 0;
    if (/right/i.test(grad.end))  options.y2 = 0;

    var i, c, stop, gradient = options.ctx.createLinearGradient(options.x1, options.y1, options.x2, options.y2);
    for (i = 0; i < grad.colors.length; i++) {
      c = grad.colors[i];
      if (Object.isArray(c)) {
        stop = c[0];
        c = c[1];
      }
      else stop = i / (grad.colors.length-1);
      gradient.addColorStop(stop, FlotrExtended.Color.parse(c).scale(null, null, null, options.opacity));
    }
    return gradient;
  },
	registerPlugins: function(){
		var name, plugin, c;
		for (name in FlotrExtended.plugins) {
			plugin = FlotrExtended.plugins[name];
			for (c in plugin.callbacks) {
				this.el.observe(c, plugin.callbacks[c].bindAsEventListener(this));
			}
			this[name] = Object.clone(plugin);
			for (p in this[name]) {
				if (Object.isFunction(this[name][p]))
					this[name][p] = this[name][p].bind(this);
			}
		}
	},
	/**
	 * Calculates a text box dimensions, wether it is drawn on the canvas or inserted into the DOM
	 * @param {String} text - The text in the box
	 * @param {Object} canvasStyle - An object containing the style for the text if drawn on the canvas
	 * @param {String} HtmlStyle - A CSS style for the text if inserted into the DOM
	 * @param {Object} className - A CSS className for the text if inserted into the DOM
	 */
	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;
		}
	},
	/**
	 * Builds a matrix of the data to make the correspondance between the x values and the y values :
	 * X value => Y values from the axes
	 * @return {Array} The data grid
	 */
	loadDataGrid: function(){
		if (this.seriesData) return this.seriesData;

		var s = this.series,
		    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], 
					r = dg.find(function(row) {return row[0] == x});
				if (r) 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
		return this.seriesData = dg.sortBy(function(v){return v[0]});
	},
	/**
	 * Initializes event some handlers.
	 */
	initEvents: function () {
		//@todo: maybe stopObserving with only flotr functions
		this.overlay.stopObserving()
		    .observe('mousedown', this.mouseDownHandler.bindAsEventListener(this))
		    .observe('mousemove', this.mouseMoveHandler.bindAsEventListener(this))
		    .observe('mouseout', this.clearHit.bindAsEventListener(this))
		    .observe('click', this.clickHandler.bindAsEventListener(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 = a.x2.datamin = a.y.datamin = a.y2.datamin = Number.MAX_VALUE;
		a.x.datamax = a.x2.datamax = a.y.datamax = a.y2.datamax = -Number.MAX_VALUE;
		
		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;
						}
					}
				}
			}
		}
		
		this.findXAxesValues();
		
		this.calculateRange(a.x, 'x');
		
		if (a.x2.used) {
			this.calculateRange(a.x2, 'x');
		}
		
		this.calculateRange(a.y, 'y');
		
		if (a.y2.used) {
			this.calculateRange(a.y2, 'y');
		}
	},
	extendRange: function(axis, type) {
		var f = (type === 'y') ? 'extendYRange' : 'extendXRange'
		for (var t in FlotrExtended.graphTypes) {
			if (this[t][f]) this[t][f](axis);
		}
	},
	/**
	 * Calculates the range of an axis to apply autoscaling.
	 * @param {Object} axis - The axis for what the range will be calculated
	 */
	calculateRange: function(axis, type){
		var o = axis.options,
		    min = o.min != null ? o.min : axis.datamin,
		    max = o.max != null ? o.max : axis.datamax,
		    margin = o.autoscaleMargin;

		if(max - min == 0.0){
			var widen = (max == 0.0) ? 1.0 : 0.01;
			min -= widen;
			max += widen;
		}
		axis.tickSize = FlotrExtended.getTickSize(o.noTicks, min, max, o.tickDecimals);

		// Autoscaling.
		if(o.min == null && 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 != 0){
			max += axis.tickSize * margin;
			if(max > 0 && axis.datamax <= 0 && axis.datamax != axis.datamin) max = 0;				
			max = axis.tickSize * Math.ceil(max / axis.tickSize);
		}

		if (min == max) max = min + 1;

		axis.min = min;
		axis.max = max;
		
		this.extendRange(axis, type);
	},
	/** 
	 * Find every values of the x axes
	 */
	findXAxesValues: function(){
		var i, j, s;
		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 - The axis for what the ticks will be calculated
	 */
	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 {
			if (o.mode == 'time') {
				var tu = FlotrExtended.Date.timeUnits,
				    spec = FlotrExtended.Date.spec,
						delta = (axis.max - axis.min) / axis.options.noTicks,
						size, unit;

				for (i = 0; i < spec.length - 1; ++i) {
					var d = spec[i][0] * tu[spec[i][1]];
					if (delta < (d + spec[i+1][0] * tu[spec[i+1][1]]) / 2 && d >= axis.tickSize)
						break;
				}
				size = spec[i][0];
				unit = spec[i][1];
				
				// special-case the possibility of several years
				if (unit == "year") {
					size = FlotrExtended.getTickSize(axis.options.noTicks*tu.year, axis.min, axis.max, 0);
				}
				
				axis.tickSize = size;
				axis.tickUnit = unit;
				axis.ticks = FlotrExtended.Date.generator(axis);
			}
			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: FlotrExtended.toRad(axis.options.labelsAngle)}, 'font-size:smaller;', 'flotr-grid-label');
			
			// Check to see if we have a preset label width.
			if(axis == y && options.yaxis.labelWidth) axis.maxLabel.width = this.options.yaxis.labelWidth;
			
			axis.titleSize = this.getTextDimensions(axis.options.title, {size: options.fontSize*1.2, angle: FlotrExtended.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;
		p.bottom += (options.grid.circular ? 0 : (x.options.showLabels ?  (x.maxLabel.height + margin) : 0)) + 
		            (x.options.title ? (x.titleSize.height + margin) : 0) + maxOutset;
		
		p.top    += (options.grid.circular ? 0 : (x2.options.showLabels ? (x2.maxLabel.height + margin) : 0)) + 
		            (x2.options.title ? (x2.titleSize.height + margin) : 0) + this.subtitleHeight + this.titleHeight + maxOutset;
    
		p.left   += (options.grid.circular ? 0 : (y.options.showLabels ?  (y.maxLabel.width + margin) : 0)) + 
		            (y.options.title ? (y.titleSize.width + margin) : 0) + maxOutset;
		
		p.right  += (options.grid.circular ? 0 : (y2.options.showLabels ? (y2.maxLabel.width + margin) : 0)) + 
		            (y2.options.title ? (y2.titleSize.width + margin) : 0) + maxOutset;
    
		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, series and outline.
	 */
	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.drawOutline();
		this.el.fire('flotr:afterdraw', [this.series, this]);
	},
	/**
	 * Draws a grid for the graph.
	 */
	drawGrid: function(){
		var v, o = this.options,
		    ctx = this.ctx, a;
		    
		if(o.grid.verticalLines || o.grid.horizontalLines){
			this.el.fire('flotr:beforegrid', [this.axes.x, this.axes.y, o, this]);
		}
		ctx.save();
		ctx.lineWidth = 1;
		ctx.strokeStyle = o.grid.tickColor;
		
		if (o.grid.circular) {
			ctx.translate(this.plotOffset.left+this.plotWidth/2, this.plotOffset.top+this.plotHeight/2);
			var radius = Math.min(this.plotHeight, this.plotWidth)*o.radar.radiusRatio/2,
			    sides = this.axes.x.ticks.length,
					coeff = 2*(Math.PI/sides),
					angle = -Math.PI/2;
			
			// Draw grid lines in vertical direction.
			ctx.beginPath();
			
			if(o.grid.horizontalLines){
				a = this.axes.y;
				for(var i = 0; i < a.ticks.length; ++i){
					v = a.ticks[i].v;
					var ratio = v / a.max;
					
					for(var j = 0; j <= sides; ++j){
						ctx[j == 0 ? 'moveTo' : 'lineTo'](Math.cos(j*coeff+angle)*radius*ratio, Math.sin(j*coeff+angle)*radius*ratio);
					}
					//ctx.moveTo(radius*ratio, 0);
					//ctx.arc(0, 0, radius*ratio, 0, Math.PI*2, true);
				}
			}
			
			if(o.grid.verticalLines){
				for(var i = 0; i < sides; ++i){
					ctx.moveTo(0, 0);
					ctx.lineTo(Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius);
				}
			}
			ctx.stroke();
		}
		else {
			ctx.translate(this.plotOffset.left, this.plotOffset.top);
	
			// Draw grid background, if present in options.
			if(o.grid.backgroundColor != null){
				ctx.fillStyle = this.processColor(o.grid.backgroundColor, {x1: 0, y1: 0, x2: this.plotWidth, y2: this.plotHeight});
				ctx.fillRect(0, 0, this.plotWidth, this.plotHeight);
			}
			
			// Draw grid lines in vertical direction.
			ctx.beginPath();
			if(o.grid.verticalLines){
				a = this.axes.x;
				for(var i = 0; i < a.ticks.length; ++i){
					v = a.ticks[i].v;
					// Don't show lines on upper and lower bounds.
					if ((v <= a.min || v >= a.max) || 
					    (v == a.min || v == a.max) && o.grid.outlineWidth != 0)
						continue;
		
					ctx.moveTo(Math.floor(a.d2p(v)) + ctx.lineWidth/2, 0);
					ctx.lineTo(Math.floor(a.d2p(v)) + ctx.lineWidth/2, this.plotHeight);
				}
			}
			
			// Draw grid lines in horizontal direction.
			if(o.grid.horizontalLines){
				a = this.axes.y;
				for(var j = 0; j < a.ticks.length; ++j){
					v = a.ticks[j].v;
					// Don't show lines on upper and lower bounds.
					if ((v <= a.min || v >= a.max) || 
					    (v == a.min || v == a.max) && o.grid.outlineWidth != 0)
						continue;
		
					ctx.moveTo(0, Math.floor(a.d2p(v)) + ctx.lineWidth/2);
					ctx.lineTo(this.plotWidth, Math.floor(a.d2p(v)) + 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 a outline for the graph.
   */
	drawOutline: function(){
    var v, o = this.options,
        ctx = this.ctx;
		
    if (o.grid.outlineWidth == 0 && o.grid.xaxisOutline == 0 && o.grid.yaxisOutline == 0) return;
		
    ctx.save();
		
    if (o.grid.circular) {
      ctx.translate(this.plotOffset.left+this.plotWidth/2, this.plotOffset.top+this.plotHeight/2);
      var radius = Math.min(this.plotHeight, this.plotWidth)*o.radar.radiusRatio/2,
          sides = this.axes.x.ticks.length,
          coeff = 2*(Math.PI/sides),
          angle = -Math.PI/2;
      
      // Draw axis/grid border.
      ctx.beginPath();
      ctx.lineWidth = o.grid.outlineWidth;
      ctx.strokeStyle = o.grid.color;
      ctx.lineJoin = 'round';
      
      for(var i = 0; i <= sides; ++i){
        ctx[i == 0 ? 'moveTo' : 'lineTo'](Math.cos(i*coeff+angle)*radius, Math.sin(i*coeff+angle)*radius);
      }
      //ctx.arc(0, 0, radius, 0, Math.PI*2, true);

      ctx.stroke();
    }
    else {
      ctx.translate(this.plotOffset.left, this.plotOffset.top);
      
      // Draw axis/grid border.
      var lw = o.grid.outlineWidth,
          orig = 0.5-lw+((lw+1)%2/2);
      ctx.lineWidth = lw;
      ctx.strokeStyle = o.grid.color;
      ctx.lineJoin = 'miter';
      ctx.strokeRect(orig, orig, this.plotWidth, this.plotHeight);
      
      // Draw xaxis/yaxis outlines.
      ctx.translate(-this.plotOffset.left, -this.plotOffset.top);
      ctx.beginPath();
      if (o.grid.xaxisOutline != 0) {
    	  ctx.lineWidth = o.grid.xaxisOutline;
    	  ctx.strokeStyle = o.grid.color;
    	  ctx.moveTo(this.plotOffset.left, this.plotOffset.top);
    	  ctx.lineTo(this.plotOffset.left, this.plotHeight + this.plotOffset.top);
      }

      if (o.grid.yaxisOutline != 0) {
    	  ctx.lineWidth = o.grid.yaxisOutline;
    	  ctx.strokeStyle = o.grid.color;
    	  ctx.moveTo(this.plotOffset.left, this.plotHeight + this.plotOffset.top);
    	  ctx.lineTo(this.plotWidth + this.plotOffset.left, this.plotHeight + this.plotOffset.top);
      }

      ctx.stroke();      
      
    }
    
    ctx.restore();
	},
	/**
	 * 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, left, top,
		    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.grid.circular) {
			ctx.save();
			ctx.translate(this.plotOffset.left+this.plotWidth/2, this.plotOffset.top+this.plotHeight/2);
			var radius = this.plotHeight*options.radar.radiusRatio/2 + options.fontSize,
			    sides = this.axes.x.ticks.length,
					coeff = 2*(Math.PI/sides),
					angle = -Math.PI/2;
			
			var style = {
				size: options.fontSize
			};

			// Add x labels.
			axis = a.x;
			style.color = axis.options.color || options.grid.color;
			for(i = 0; i < axis.ticks.length && axis.options.showLabels; ++i){
				tick = axis.ticks[i];
				tick.label += '';
				if(!tick.label || tick.label.length == 0) continue;
				
				var x = Math.cos(i*coeff+angle) * radius, 
				    y = Math.sin(i*coeff+angle) * radius;
						
				style.angle = FlotrExtended.toRad(axis.options.labelsAngle);
				style.textBaseline = 'middle';
				style.textAlign = (Math.abs(x) < 0.1 ? 'center' : (x < 0 ? 'right' : 'left'));

				FlotrExtended.drawText(ctx, tick.label, x, y, 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; ++i){
				tick = axis.ticks[i];
				tick.label += '';
				if(!tick.label || tick.label.length == 0) continue;
				
				style.angle = FlotrExtended.toRad(axis.options.labelsAngle);
				style.textBaseline = 'middle';
				style.textAlign = 'left';
				
				FlotrExtended.drawText(ctx, tick.label, 3, -(axis.ticks[i].v / axis.max) * (radius - options.fontSize), style);
			}
			ctx.restore();
			return;
		}
    
		if (!options.HtmlText && this.textEnabled) {
			var style = {
				size: options.fontSize
			};
	
			// Add x labels.
			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;
				
				left = axis.d2p(tick.v);
				if (left < 0 || left > this.plotWidth) continue;
        
				style.angle = FlotrExtended.toRad(axis.options.labelsAngle);
				style.textAlign = 'center';
				style.textBaseline = 'top';
				style = FlotrExtended.getBestTextAlign(style.angle, style);
				
				FlotrExtended.drawText(
					ctx, tick.label,
					this.plotOffset.left + left, 
					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;
        
				left = axis.d2p(tick.v);
				if(left < 0 || left > this.plotWidth) continue;
        
				style.angle = FlotrExtended.toRad(axis.options.labelsAngle);
				style.textAlign = 'center';
				style.textBaseline = 'bottom';
				style = FlotrExtended.getBestTextAlign(style.angle, style);
				
				FlotrExtended.drawText(
					ctx, tick.label,
					this.plotOffset.left + left, 
					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) continue;
        
				top = axis.d2p(tick.v);
				if(top < 0 || top > this.plotHeight) continue;
				
				style.angle = FlotrExtended.toRad(axis.options.labelsAngle);
				style.textAlign = 'right';
				style.textBaseline = 'middle';
				style = FlotrExtended.getBestTextAlign(style.angle, style);
				
				FlotrExtended.drawText(
					ctx, tick.label,
					this.plotOffset.left - options.grid.labelMargin, 
					this.plotOffset.top + top,
					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;
        
				top = axis.d2p(tick.v);
				if(top < 0 || top > this.plotHeight) continue;
        
				style.angle = FlotrExtended.toRad(axis.options.labelsAngle);
				style.textAlign = 'left';
				style.textBaseline = 'middle';
				style = FlotrExtended.getBestTextAlign(style.angle, style);
				
				FlotrExtended.drawText(
					ctx, tick.label,
					this.plotOffset.left + this.plotWidth + options.grid.labelMargin, 
					this.plotOffset.top + top,
					style
				);
				
				ctx.save();
				ctx.strokeStyle = style.color;
				ctx.beginPath();
				ctx.moveTo(this.plotOffset.left + this.plotWidth - 8, this.plotOffset.top + axis.d2p(tick.v));
				ctx.lineTo(this.plotOffset.left + this.plotWidth,     this.plotOffset.top + axis.d2p(tick.v));
				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 || 
					    (this.plotOffset.left + axis.d2p(tick.v) < 0) || 
					    (this.plotOffset.left + axis.d2p(tick.v) > this.canvasWidth)) continue;
					
					var barinc = xBoxWidth/2;					
					if(axis.options.barLabelsCentered) barinc = 0;
						
					html.push(
					  '<div style="position:absolute;top:', 
					  (this.plotOffset.top + this.plotHeight + options.grid.labelMargin), 'px;left:', 
					  (this.plotOffset.left +axis.d2p(tick.v) - barinc), '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 || 
					    (this.plotOffset.left + axis.d2p(tick.v) < 0) || 
					    (this.plotOffset.left + axis.d2p(tick.v) > this.canvasWidth)) continue;
					
					html.push(
					  '<div style="position:absolute;top:', 
					  (this.plotOffset.top - options.grid.labelMargin - axis.maxLabel.height), 'px;left:', 
					  (this.plotOffset.left + axis.d2p(tick.v) - 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 ||
							 (this.plotOffset.top + axis.d2p(tick.v) < 0) || 
							 (this.plotOffset.top + axis.d2p(tick.v) > this.canvasHeight)) continue;
					
					html.push(
					  '<div style="position:absolute;top:', 
					  (this.plotOffset.top + axis.d2p(tick.v) - 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 flotr-grid-label-y">', 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 ||
							 (this.plotOffset.top + axis.d2p(tick.v) < 0) || 
							 (this.plotOffset.top + axis.d2p(tick.v) > this.canvasHeight)) continue;
					
					html.push(
					  '<div style="position:absolute;top:', 
					  (this.plotOffset.top + axis.d2p(tick.v) - 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 flotr-grid-label-y">', tick.label, '</div>'
					);

					ctx.moveTo(this.plotOffset.left + this.plotWidth - 8, this.plotOffset.top + axis.d2p(tick.v));
					ctx.lineTo(this.plotOffset.left + this.plotWidth,     this.plotOffset.top + axis.d2p(tick.v));
				}
				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,
				textAlign: 'center'
			};
			
			// Add subtitle
			if (options.subtitle){
				FlotrExtended.drawText(
					ctx, 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){
				FlotrExtended.drawText(
					ctx, options.title,
					this.plotOffset.left + this.plotWidth/2, 
					this.titleHeight - 2,
					style
				);
			}
			
			style.weight = 1.8;
			style.size *= 0.8;
			
			// Add x axis title
			if (a.x.options.title && a.x.used){
				style.textAlign = 'center';
				style.textBaseline = 'top';
				style.angle = FlotrExtended.toRad(a.x.options.titleAngle);
				style = FlotrExtended.getBestTextAlign(style.angle, style);
				FlotrExtended.drawText(
					ctx, 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.textAlign = 'center';
				style.textBaseline = 'bottom';
				style.angle = FlotrExtended.toRad(a.x2.options.titleAngle);
				style = FlotrExtended.getBestTextAlign(style.angle, style);
				FlotrExtended.drawText(
					ctx, 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.textAlign = 'right';
				style.textBaseline = 'middle';
				style.angle = FlotrExtended.toRad(a.y.options.titleAngle);
				style = FlotrExtended.getBestTextAlign(style.angle, style);
				FlotrExtended.drawText(
					ctx, 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.textAlign = 'left';
				style.textBaseline = 'middle';
				style.angle = FlotrExtended.toRad(a.y2.options.titleAngle);
				style = FlotrExtended.getBestTextAlign(style.angle, style);
				FlotrExtended.drawText(
					ctx, 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(type in FlotrExtended.graphTypes){
			if(series[type] && series[type].show){
				drawn = true;
				this[type].draw(series);
			}
		}
		
		if(!drawn){
			this[this.options.defaultType].draw(series);
		}
	},/**
	 * Adds a legend div to the canvas container or draws it on the canvas.
	 */
	insertLegend: function(){
		if(!this.options.legend.show)
			return;
			
		var series = this.series,
			plotOffset = this.plotOffset,
			options = this.options,
			legend = options.legend,
			fragments = [],
			rowStarted = false, 
			ctx = this.ctx,
			i;
			
		var noLegendItems = series.findAll(function(s) {return (s.label && !s.hide)}).length;

		if (noLegendItems) {
		    if (!options.HtmlText && this.textEnabled && !$(legend.container)) {
				var style = {
					size: options.fontSize*1.1,
					color: options.grid.color
				};

				var p = legend.position, 
				    m = legend.margin,
				    lbw = legend.labelBoxWidth,
				    lbh = legend.labelBoxHeight,
				    lbm = 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 = legend.labelFormatter(series[i].label);
					labelMaxWidth = Math.max(labelMaxWidth, FlotrExtended.measureText(ctx, label, style).width);
				}
				
				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 = this.processColor(options.legend.backgroundColor || 'rgb(240,240,240)', {opacity: options.legend.backgroundOpacity || 0.1});
				
				ctx.fillStyle = color;
				ctx.fillRect(offsetX, offsetY, legendWidth, legendHeight);
				ctx.strokeStyle = options.legend.labelBoxBorderColor;
				ctx.strokeRect(FlotrExtended.toPixel(offsetX), FlotrExtended.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 = legend.labelFormatter(series[i].label);
					
					ctx.fillStyle = series[i].color;
					ctx.fillRect(x, y, lbw-1, lbh-1);
					
					ctx.strokeStyle = legend.labelBoxBorderColor;
					ctx.lineWidth = 1;
					ctx.strokeRect(Math.ceil(x)-1.5, Math.ceil(y)-1.5, lbw+2, lbh+2);
					
					// Legend text
					FlotrExtended.drawText(ctx, 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 s = series[i],
		  			    label = legend.labelFormatter(s.label),
		  			    boxWidth = legend.labelBoxWidth,
		  			    boxHeight = legend.labelBoxHeight,
		  			    opacity = 'opacity:' + s.bars.fillOpacity + ';filter:alpha(opacity=' + s.bars.fillOpacity*100 + ');',
		  			    color = 'background-color:' + ((s.bars.show && s.bars.fillColor && s.bars.fill) ? s.bars.fillColor : s.color) + ';';
		  			
					fragments.push(
						'<td class="flotr-legend-color-box">',
							'<div style="border:1px solid ', legend.labelBoxBorderColor, ';padding:1px">',
								'<div style="width:', (boxWidth-1), 'px;height:', (boxHeight-1), 'px;border:1px solid ', series[i].color, '">', // Border
									'<div style="width:', boxWidth, 'px;height:', boxHeight, 'px;', opacity, color, '"></div>', // Background
								'</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).innerHTML = table;
		  			}
				    else {
		  				var pos = '', p = options.legend.position, m = options.legend.margin;
		  				
		  				     if(p.charAt(0) == 'n') pos += 'top:' + (m + plotOffset.top) + 'px;bottom:auto;';
		  				else if(p.charAt(0) == 's') pos += 'bottom:' + (m + plotOffset.bottom) + 'px;top:auto;';					
		  				     if(p.charAt(1) == 'e') pos += 'right:' + (m + plotOffset.right) + 'px;left:auto;';
		  				else if(p.charAt(1) == 'w') pos += 'left:' + (m + plotOffset.left) + 'px;right:auto;';
		  				     
		  				var div = this.el.insert('<div class="flotr-legend" style="position:absolute;z-index:2;' + pos +'">' + table + '</div>').select('div.flotr-legend')[0];
		  				
		  				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 : FlotrExtended.Color.extract(div);
		  						c = this.processColor(tmp, null, {opacity: 1});
		  					}
		  					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')[0].setOpacity(options.legend.backgroundOpacity);
		  				}
		  			}
		  		}
		    }
	    }
	},
	/**
	 * Calculates the coordinates from a mouse event object.
	 * @param {Event} event - Mouse Event object.
	 * @return {Object} Object with coordinates of the mouse.
	 */
	getEventPosition: function (event){
		var offset = this.overlay.cumulativeOffset(),
		    pointer = Event.pointer(event),
		    rx = (pointer.x - offset.left - this.plotOffset.left),
		    ry = (pointer.y - offset.top - this.plotOffset.top);
    
		return {
			x:  this.axes.x.p2d(rx),
			x2: this.axes.x2.p2d(rx),
			y:  this.axes.y.p2d(ry),
			y2: this.axes.y2.p2d(ry),
			relX: rx,
			relY: ry,
			absX: pointer.x,
			absY: pointer.y
		};
	},
	/**
	 * Observes the 'click' event and fires the 'flotr:click' event.
	 * @param {Event} event - 'click' Event object.
	 */
	clickHandler: function(event){
		if(this.ignoreClick){
			return this.ignoreClick = false;
		}
		this.el.fire('flotr:click', [this.getEventPosition(event), this]);
	},
	/**
	 * Observes mouse movement over the graph area. Fires the 'flotr:mousemove' event.
	 * @param {Event} event - 'mousemove' Event object.
	 */
	mouseMoveHandler: function(event){
 		var pos = this.getEventPosition(event);
    
		this.lastMousePos.pageX = pos.absX;
		this.lastMousePos.pageY = pos.absY;	
    	
    	//@todo Add another overlay for the crosshair
		if (this.options.crosshair.mode)
			this.clearCrosshair();
			
		if(this.selectionInterval == null && (this.options.mouse.track || this.series.any(function(s){return s.mouse && s.mouse.track;})))
			this.hit(pos);
			//this.newHit(pos);
		
		if (this.options.crosshair.mode)
			this.drawCrosshair(pos);
    
		this.el.fire('flotr:mousemove', [event, pos, this]);
	},
	/**
	 * Observes the 'mousedown' event.
	 * @param {Event} event - 'mousedown' Event object.
	 */
	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.bindAsEventListener(this), 1000/this.options.selection.fps);
		
		this.mouseUpHandler = this.mouseUpHandler.bindAsEventListener(this);
		document.observe('mouseup', this.mouseUpHandler);
	},
	/**
	 * Fires the 'flotr:select' event when the user made a selection.
	 */
	fireSelectEvent: function(){
		var a = this.axes, s = this.selection,
		    x1, x2, y1, y2;
		
		x1 = a.x.p2d(s.first.x);
		x2 = a.x.p2d(s.second.x);
		y1 = a.y.p2d(s.first.y);
		y2 = a.y.p2d(s.second.y);

		this.el.fire('flotr:select', [{
			x1:Math.min(x1, x2), 
			y1:Math.min(y1, y2), 
			x2:Math.max(x1, x2), 
			y2:Math.max(y1, y2),
			xfirst:x1, xsecond:x2, yfirst:y1, ysecond:y2
		}, this]);
	},
	/**
	 * Observes the mouseup event for the document. 
	 * @param {Event} event - 'mouseup' Event object.
	 */
	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;
		}
	},
	/**
	 * Calculates the position of the selection.
	 * @param {Object} pos - Position object.
	 * @param {Event} event - Event object.
	 */
	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);
		}
	},
	/**
	 * Updates (draws) the selection box.
	 */
	updateSelection: function(){
		if(this.lastMousePos.pageX == null) return;
		
		this.setSelectionPos(this.selection.second, this.lastMousePos);
		this.clearSelection();
		
		if(this.selectionIsSane()) this.drawSelection();
	},
	/**
	 * Removes the selection box from the overlay canvas.
	 */
	clearSelection: function() {
		if(this.prevSelection == null) return;
			
		var prevSelection = this.prevSelection,
			lw = this.octx.lineWidth,
			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);
		
		this.octx.clearRect(x + plotOffset.left - lw/2+0.5,
		                    y + plotOffset.top - lw/2+0.5,
		                    w + lw,
		                    h + lw);
		
		this.prevSelection = null;
	},
	/**
	 * Allows the user the manually select an area.
	 * @param {Object} area - Object with coordinates to select.
	 */
	setSelection: function(area, preventEvent){
		var options = this.options,
			xa = this.axes.x,
			ya = this.axes.y,
			vertScale = ya.scale,
			hozScale = xa.scale,
			selX = options.selection.mode.indexOf('x') != -1,
			selY = options.selection.mode.indexOf('y') != -1;
		
		this.clearSelection();

		this.selection.first.y  = (selX && !selY) ? 0 : (ya.max - area.y1) * vertScale;
		this.selection.second.y = (selX && !selY) ? this.plotHeight : (ya.max - area.y2) * vertScale;			
		this.selection.first.x  = (selY && !selX) ? 0 : (area.x1 - xa.min) * hozScale;
		this.selection.second.x = (selY && !selX) ? this.plotWidth : (area.x2 - xa.min) * hozScale;
		
		this.drawSelection();
		if (!preventEvent)
			this.fireSelectEvent();
	},
	/**
	 * Draws the selection box.
	 */
	drawSelection: function() {
		var prevSelection = this.prevSelection,
			s = this.selection,
			octx = this.octx,
			options = this.options,
			plotOffset = this.plotOffset;
		
		if(prevSelection != null &&
			s.first.x == prevSelection.first.x &&
			s.first.y == prevSelection.first.y && 
			s.second.x == prevSelection.second.x &&
			s.second.y == prevSelection.second.y)
			return;

		octx.save();
		octx.strokeStyle = this.processColor(options.selection.color, {opacity: 0.8});
		octx.lineWidth = 1;
		octx.lineJoin = 'miter';
		octx.fillStyle = this.processColor(options.selection.color, {opacity: 0.4});

		this.prevSelection = {
			first: { x: s.first.x, y: s.first.y },
			second: { x: s.second.x, y: s.second.y }
		};

		var x = Math.min(s.first.x, s.second.x),
		    y = Math.min(s.first.y, s.second.y),
		    w = Math.abs(s.second.x - s.first.x),
		    h = Math.abs(s.second.y - s.first.y);
		
		octx.fillRect(x + plotOffset.left+0.5, y + plotOffset.top+0.5, w, h);
		octx.strokeRect(x + plotOffset.left+0.5, y + plotOffset.top+0.5, w, h);
		octx.restore();
	},
	/**	 
	 * Draws the selection box.
	 */
	drawCrosshair: function(pos) {
		var octx = this.octx,
			options = this.options,
			plotOffset = this.plotOffset,
			x = plotOffset.left+pos.relX+0.5,
			y = plotOffset.top+pos.relY+0.5;
		
		if (pos.relX < 0 || pos.relY < 0 || pos.relX > this.plotWidth || pos.relY > this.plotHeight) {
			this.el.style.cursor = null;
			this.el.removeClassName('flotr-crosshair');
			return; 
		}
		
		this.lastMousePos.relX = null;
		this.lastMousePos.relY = null;
		
		if (options.crosshair.hideCursor) {
			this.el.style.cursor = Prototype.Browser.Gecko ? 'none' :'url(blank.cur),crosshair';
			this.el.addClassName('flotr-crosshair');
		}
		
		octx.save();
		octx.strokeStyle = options.crosshair.color;
		octx.lineWidth = 1;
		octx.beginPath();
		
		if (options.crosshair.mode.indexOf('x') != -1) {
			octx.moveTo(x, plotOffset.top);
			octx.lineTo(x, plotOffset.top + this.plotHeight);
			this.lastMousePos.relX = x;
		}
		
		if (options.crosshair.mode.indexOf('y') != -1) {
			octx.moveTo(plotOffset.left, y);
			octx.lineTo(plotOffset.left + this.plotWidth, y);
			this.lastMousePos.relY = y;
		}
		
		octx.stroke();
		octx.restore();
	},
	/**
	 * Removes the selection box from the overlay canvas.
	 */
	clearCrosshair: function() {
		if (this.lastMousePos.relX != null)
			this.octx.clearRect(this.lastMousePos.relX-0.5, this.plotOffset.top, 1,this.plotHeight+1);
		
		if (this.lastMousePos.relY != null)
			this.octx.clearRect(this.plotOffset.left, this.lastMousePos.relY-0.5, this.plotWidth+1, 1);		
	},
	/**
	 * Determines whether or not the selection is sane and should be drawn.
	 * @return {Boolean} - True when sane, false otherwise.
	 */
	selectionIsSane: function(){
		return Math.abs(this.selection.second.x - this.selection.first.x) >= 5 &&
		       Math.abs(this.selection.second.y - this.selection.first.y) >= 5;
	},
	/**
	 * Removes the mouse tracking point from the overlay.
	 */
	clearHit: function(){
		if(!this.prevHit) return;
    
		var prevHit = this.prevHit,
		    plotOffset = this.plotOffset,
		    s = prevHit.series,
		    lw = s.bars.lineWidth,
		    xa = prevHit.xaxis,
		    ya = prevHit.yaxis;
				
		//if(!s.bars.show){
			var r = s.points.radius;
			
			if(s.mouse.showLine) {
				this.octx.clearRect(
						xa.d2p(prevHit.x) + plotOffset.left - r*2,
						0,
						r*3 + s.points.lineWidth*3, 
						this.plotHeight + this.plotOffset.top
				);
			} else {
				this.octx.clearRect(
						xa.d2p(prevHit.x) + plotOffset.left - r*2,
						ya.d2p(prevHit.y) + plotOffset.top - r*2,
						r*3 + s.points.lineWidth*3, 
						r*3 + s.points.lineWidth*3
				);
			}
		/*}

		else {
			
			if(s.mouse.showLine) {
				this.octx.clearRect(
						xa.d2p(prevHit.x) + plotOffset.left - r*2,
						0,
						r*3 + s.points.lineWidth*3, 
						this.plotHeight + this.plotOffset.top
				);
			} else {
				var bw = s.bars.barWidth;		
				this.octx.clearRect(
					xa.d2p(prevHit.x - bw/2) + plotOffset.left - lw, 
					ya.d2p(prevHit.y >= 0 ? prevHit.y : 0) + plotOffset.top - lw, 
					xa.d2p(bw) + lw * 2, 
					ya.d2p(prevHit.y < 0 ? prevHit.y : 0) + lw * 2
				);
			}
		}*/
	},
	/**
	 * Updates the mouse tracking point on the overlay.
	 */
	drawHit: function(n){
		var octx = this.octx,
		    s = n.series,
		    xa = n.xaxis,
		    ya = n.yaxis;

		if(s.mouse.lineColor != null){
			octx.save();
			octx.lineWidth = s.points.lineWidth;
			octx.strokeStyle = s.mouse.lineColor;
			octx.fillStyle = this.processColor(s.mouse.fillColor || '#ffffff', {opacity: s.mouse.fillOpacity});
      
			//if(!s.bars.show){
				octx.translate(this.plotOffset.left, this.plotOffset.top);
				octx.beginPath();
				
  				if(s.mouse.showLine) {
  					// Draw a line down the entire graph.
	                octx.moveTo(xa.d2p(n.x), 0);
	                octx.lineTo(xa.d2p(n.x), this.plotHeight);
  				} else {
  	  				octx.arc(xa.d2p(n.x), ya.d2p(n.y), s.mouse.radius, 0, 2 * Math.PI, true);
  				}
  					
  				octx.fill();
  				octx.stroke();
				octx.closePath();
			/*}

			else {
  				octx.save();
  				octx.translate(this.plotOffset.left, this.plotOffset.top);
  				octx.beginPath();
  				
  				if(s.mouse.showLine) {
  					// Draw a line down the entire graph.
	                octx.moveTo(xa.d2p(n.x), 0);
	                octx.lineTo(xa.d2p(n.x), this.plotHeight);
  				} else {
        
	  				if (s.mouse.trackAll) {
	  					octx.moveTo(xa.d2p(n.x), ya.d2p(0));
	  					octx.lineTo(xa.d2p(n.x), ya.d2p(n.yaxis.max));
					}
					else {
	  					var bw = s.bars.barWidth;
	  					
	  					octx.moveTo(xa.d2p(n.x-(bw/2)), ya.d2p(0));
						octx.lineTo(xa.d2p(n.x-(bw/2)), ya.d2p(n.y));
	  					octx.lineTo(xa.d2p(n.x+(bw/2)), ya.d2p(n.y));
	  					octx.lineTo(xa.d2p(n.x+(bw/2)), ya.d2p(0));
	  
	  					if(s.mouse.fillColor) octx.fill();
					}
  				}

  				octx.stroke();
  				octx.closePath();
  				octx.restore();
			}*/
			octx.restore();
		}
		this.prevHit = n;
	},
	newHit: function(mouse){
		var series = this.series,
			options = this.options,
			decimals, label;

		for(var i = series.length-1; i > -1; --i){
			s = series[i];
			if(!s.mouse.track) continue;

			for(var type in FlotrExtended.graphTypes){
				if (!this[type].getHit) continue;
				
				var h = this[type].getHit(s, mouse);
				if (h.index !== undefined) {
					decimals = s.mouse.trackDecimals;
					if(decimals == null || decimals < 0) decimals = 0;
					
					label = s.mouse.trackFormatter(h);
					this.drawTooltip(label, h.x, h.y, s.mouse);
					this.mouseTrack.fire('flotr:hit', [h, this]);
				}
			}
		}
	},
	/**
	 * 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.
	 * @param {Object} mouse - Object that holds the relative x and y coordinates of the cursor.
	 */
	hit: function(mouse){
		var series = this.series,
			options = this.options,
			prevHit = this.prevHit,
			plotOffset = this.plotOffset,
			octx = this.octx, 
			data, sens, xsens, ysens, x, y, xa, ya, mx, my, i,
			mousepos = 'sw',
			/**
			 * Nearest data element.
			 */
			n = {
				dist:Number.MAX_VALUE,
				x:null,
				y:null,
				x_show:null,
				y_show:null,
				relX:mouse.relX,
				relY:mouse.relY,
				absX:mouse.absX,
				absY:mouse.absY,
				mouse:null,
				xaxis:null,
				yaxis:null,
				series:null,
				index:null,
				seriesIndex:null
			};

		if (options.mouse.trackAll) {
			for(i = 0; i < series.length; i++){
				s = series[0];
				data = s.data;
				xa = s.xaxis;
				ya = s.yaxis;
				xsens = (2*options.points.lineWidth)/xa.scale * s.mouse.sensibility;
				mx = xa.p2d(mouse.relX);
				my = ya.p2d(mouse.relY);
		
				for(var j = 0; j < data.length; j++){
					x = data[j][0];
					y = data[j][1];
					
					x_show = x;
					y_show = y;
					
					if(s.use_alternate_vals) x_show = data[j][2];
	                if(s.use_alternate_vals) y_show  = data[j][3];
		
					if (y === null ||
							xa.min > x || xa.max < x ||
							ya.min > y || ya.max < y ||
							mx < xa.min || mx > xa.max ||
							my < ya.min || my > ya.max) continue;
		
					var xdiff = Math.abs(x - mx);
		
					// Bars are not supported yet. Not sure how it should look with bars
					if((!s.bars.show && xdiff < xsens) 
							|| (s.bars.show && xdiff < s.bars.barWidth/2) 
							|| (y < 0 && my < 0 && my > y)) {
						
						var distance = xdiff;
						
						if (distance < n.dist) {
							n.dist = distance;
							n.x = x;
							n.y = y;
	  						n.x_show = x_show;
	  						n.y_show = y_show;
							n.xaxis = xa;
							n.yaxis = ya;
							n.mouse = s.mouse;
							n.series = s; 
							n.allSeries = series; // include all series
							n.index = j;
						}
					}
				}
			}
		} else {
		    for(i = 0; i < series.length; i++){
	  			s = series[i];
	  			if(!s.mouse.track) continue;
	  			
	  			data = s.data;
	  			xa = s.xaxis;
	  			ya = s.yaxis;
	  			sens = 2 * options.points.lineWidth * s.mouse.sensibility;
	  			xsens = sens/xa.scale;
	  			ysens = sens/ya.scale;
	  			
	  			if(s.mouse.showLine) ysens = 5000;
	  			
	  			mx = xa.p2d(mouse.relX);
	  			my = ya.p2d(mouse.relY);
	        
	  			//if (s.points) {
	  			//	var h = this.points.getHit(s, mouse);
	  			//	if (h.index !== undefined) console.log(h);
	  			//}
	  			
	  			for(var j = 0, xpow, ypow; j < data.length; j++){
	  				x = data[j][0];
	  				y = data[j][1];
	  				x_show = x;
	  				y_show = y;
	  				
	  				if(s.use_alternate_vals) x_show = data[j][2];
	                if(s.use_alternate_vals) y_show  = data[j][3];
	  				
	  				if (y === null || 
	  				    xa.min > x || xa.max < x || 
	  				    ya.min > y || ya.max < y) continue;
	  				
	  				var xdiff = Math.abs(x - mx),
	  				    ydiff = Math.abs(y - my);
	  				
	  				// we use a different set of criteria to determin if there has been a hit
	  				// depending on what type of graph we have
	  				if(
	  					(!s.bars.show && xdiff < xsens && ydiff < ysens) || 
	  					(!s.bars.show && s.mouse.showLine && xdiff < xsens) || 
	  				    (s.bars.show && xdiff < s.bars.barWidth/2 && ((y > 0 && my > 0 && my < y) || (y < 0 && my < 0 && my > y))) ||
	  				    (s.bars.show && s.mouse.showLine && xdiff < s.bars.barWidth/2)
	  				    ){
	  					var distance = Math.sqrt(xdiff*xdiff + ydiff*ydiff);
	  					if(distance < n.dist){
	  						n.dist = distance;
	  						n.x = x;
	  						n.y = y;
	  						n.x_show = x_show;
	  						n.y_show = y_show;
	  						
	  						n.xaxis = xa;
	  						n.yaxis = ya;
	  						n.mouse = s.mouse;
	  						n.series = s;
	  						n.allSeries = series;
	  						n.index = j;
	  						n.seriesIndex = i;
	  						
	  						// Are we on the right hand or left hand side of the graph?  We want to 
	  			            // flip to using the other side where needed.
	  			            if(j < parseInt(data.length/2)) {
	  			            	
	  			            	// Are we on the top or the bottom of the graph?
	  			            	if(ya.d2p(y) < this.plotHeight/2)
	  			            		mousepos = 'se';
	  			            	else
	  			            		mousepos = 'ne';
	  			            } else {
	  			            	// Are we on the top or the bottom of the graph?
	  			            	if(ya.d2p(y) < this.plotHeight/2)
	  			            		mousepos = 'sw';
	  			            	else
	  			            		mousepos = 'nw';
	  			            } 
	  						  						
	  					}
	  				}
	  			}
	  		}
		}
		
		
		if(n.series && (n.mouse && n.mouse.track && !prevHit || (prevHit /*&& (n.x != prevHit.x || n.y != prevHit.y)*/))){
			
			var mouseboxwidth = parseInt(this.plotWidth * 3/5);        	
        	if(n.mouse.mouseBoxSize) mouseboxwidth = n.mouse.mouseBoxSize;
			
			var mt = this.mouseTrack,
			    pos = '', 
			    s = n.series,
			    p = n.mouse.position, 
			    m = n.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;';
			
			// Get the mouse position - it is dependent on where we are in the graph, top/bottom, right/left.
			p = mousepos;			
			
			if (!n.mouse.relative) { // absolute to the canvas
				     if(p.charAt(0) == 'n') pos += 'top:' + (m + plotOffset.top) + 'px;bottom:auto;';
				else if(p.charAt(0) == 's') pos += 'bottom:' + (m + plotOffset.bottom) + 'px;top:auto;';
				     if(p.charAt(1) == 'e') pos += 'right:' + (m + plotOffset.right) + 'px;left:auto;';
				else if(p.charAt(1) == 'w') pos += 'left:' + (m + plotOffset.left) + 'px;right:auto;';
			}
			else { // relative to the mouse or in the case of bar like graphs to the bar
				//if(!s.bars.show){
					     if(p.charAt(0) == 'n') pos += 'bottom:' + (m - plotOffset.top - n.yaxis.d2p(n.y) + this.canvasHeight) + 'px;top:auto;';
					else if(p.charAt(0) == 's') pos += 'top:' + (m + plotOffset.top + n.yaxis.d2p(n.y)) + 'px;bottom:auto;';
					     if(p.charAt(1) == 'e') pos += 'left:' + (m + plotOffset.left + n.xaxis.d2p(n.x)) + 'px;right:auto;';
					else if(p.charAt(1) == 'w') pos += 'right:' + (m - plotOffset.left - n.xaxis.d2p(n.x) + this.canvasWidth) + 'px;left:auto;';
				/*}

				else {
					pos += 'bottom:' + (m - plotOffset.top - n.yaxis.d2p(n.y/2) + this.canvasHeight) + 'px;top:auto;';
					pos += 'left:' + (m + plotOffset.left + n.xaxis.d2p(n.x - options.bars.barWidth/2)) + 'px;right:auto;';
				}*/
			}
			elStyle += pos;
				     
			if(!mt){
				this.el.insert('<div class="flotr-mouse-value" style="'+elStyle+'"></div>');
				mt = this.mouseTrack = this.el.select('.flotr-mouse-value')[0];
			}
			else {
				mt.style.cssText = elStyle;
				this.mouseTrack = mt;
			}
			
			if(n.x !== null && n.y !== null){
				mt.show();
				
				this.clearHit();
				this.drawHit(n);
				
				var decimals = n.mouse.trackDecimals;
				if(decimals == null || decimals < 0) decimals = 0;
				
				if(s.use_alternate_vals) {
					
					mt.innerHTML = n.mouse.trackFormatter({
						x: n.x_show, 
						y: n.y_show, 
						series: n.series, 
						index: n.index,
						nearest: n
					});
					
				} else {
					
					mt.innerHTML = n.mouse.trackFormatter({
						x: n.x.toFixed(decimals), 
						y: n.y.toFixed(decimals), 
						series: n.series, 
						index: n.index,
						nearest: n
					});
					
				}
				
				mt.fire('flotr:hit', [n, this]);
			}
			else if(prevHit){
				mt.hide();
				this.clearHit();
			}
		}
		else if(this.prevHit) {
			this.mouseTrack.hide();
			this.clearHit();
		}
	},
	drawTooltip: function(content, x, y, options) {
		var mt = this.mouseTrack,
		    style = '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;', 
		    p = options.position, 
		    m = options.margin,
		    plotOffset = this.plotOffset;

		if (!mt) {
			this.el.insert('<div class="flotr-mouse-value"></div>');
			mt = this.mouseTrack = this.el.select('.flotr-mouse-value')[0];
		}

		if(x !== null && y !== null){
			if (!options.relative) { // absolute to the canvas
				     if(p.charAt(0) == 'n') style += 'top:' + (m + plotOffset.top) + 'px;bottom:auto;';
				else if(p.charAt(0) == 's') style += 'bottom:' + (m + plotOffset.bottom) + 'px;top:auto;';
				     if(p.charAt(1) == 'e') style += 'right:' + (m + plotOffset.right) + 'px;left:auto;';
				else if(p.charAt(1) == 'w') style += 'left:' + (m + plotOffset.left) + 'px;right:auto;';
			}
			else { // relative to the mouse
				     if(p.charAt(0) == 'n') style += 'bottom:' + (m - plotOffset.top - y + this.canvasHeight) + 'px;top:auto;';
				else if(p.charAt(0) == 's') style += 'top:' + (m + plotOffset.top + y) + 'px;bottom:auto;';
				     if(p.charAt(1) == 'e') style += 'left:' + (m + plotOffset.left + x) + 'px;right:auto;';
				else if(p.charAt(1) == 'w') style += 'right:' + (m - plotOffset.left - x + this.canvasWidth) + 'px;left:auto;';
			}
	
			mt.style.cssText = style;
			mt.update(content).show();
		}
		else {
			mt.hide();
		}
	},
	saveImage: function (type, width, height, replaceCanvas) {
		var image = null;
		if (Prototype.Browser.IE) {
			image = '<html><body>'+this.canvas.firstChild.innerHTML+'</body></html>';
			return window.open().document.write(image);
		}
			
		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');
	}
});

FlotrExtended.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();
	},
	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();
	},
	clone: function(){
		return new FlotrExtended.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;
	},
	distance: function(color){
		if (!color) return;
		color = new FlotrExtended.Color.parse(color);
		var dist = 0, 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(',')+')';
	}
});

Object.extend(FlotrExtended.Color, {
	/**
	 * Parses a color string and returns a corresponding Color.
	 * The different tests are in order of probability to improve speed.
	 * @param {String, Color} str - string thats representing a color
	 * @return {Color} returns a Color object or false
	 */
	parse: function(color){
		if (color instanceof FlotrExtended.Color) return color;

		var result, Color = FlotrExtended.Color;

		// #a0b1c2
		if((result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(color)))
			return new Color(parseInt(result[1],16), parseInt(result[2],16), parseInt(result[3],16));

		// 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(color)))
			return new Color(parseInt(result[1]), parseInt(result[2]), parseInt(result[3]));
	
		// #fff
		if((result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(color)))
			return new Color(parseInt(result[1]+result[1],16), parseInt(result[2]+result[2],16), parseInt(result[3]+result[3],16));
	
		// 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(color)))
			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(color)))
			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(color)))
			return new Color(parseFloat(result[1])*2.55, parseFloat(result[2])*2.55, parseFloat(result[3])*2.55, parseFloat(result[4]));

		// Otherwise, we're most likely dealing with a named color.
		var name = (color+'').strip().toLowerCase();
		if(name == 'transparent'){
			return new Color(255, 255, 255, 0);
		}
		return (result = Color.names[name]) ? new Color(result[0], result[1], result[2]) : new Color(0, 0, 0, 0);
	},
  
	/**
	 * Extracts the background-color of the passed element.
	 * @param {Element} element - The element from what the background color is extracted
	 * @return {String} color string
	 */
	extract: 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();
		} while(!element.nodeName.match(/^body$/i));

		// Catch Safari's way of signaling transparent.
		return new FlotrExtended.Color(color == 'rgba(0, 0, 0, 0)' ? 'transparent' : color);
	},
	
	names: {
		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]
	}
});

FlotrExtended.Date = {
	format: function(d, format) {
		if (!d) return;
		
		// We should maybe use an "official" date format spec, like PHP date() or ColdFusion 
		// http://fr.php.net/manual/en/function.date.php
		// http://livedocs.adobe.com/coldfusion/8/htmldocs/help.html?content=functions_c-d_29.html
		var tokens = {
			h: d.getUTCHours().toString(),
			H: leftPad(d.getUTCHours()),
			M: leftPad(d.getUTCMinutes()),
			S: leftPad(d.getUTCSeconds()),
			s: d.getUTCMilliseconds(),
			d: d.getUTCDate().toString(),
			m: (d.getUTCMonth() + 1).toString(),
			y: d.getUTCFullYear().toString(),
			b: FlotrExtended.Date.monthNames[d.getUTCMonth()]
		};

		function leftPad(n){
			n += '';
			return n.length == 1 ? "0" + n : n;
		}
		
		var r = [], c,
		    escape = false;
		
		for (var i = 0; i < format.length; ++i) {
			c = format.charAt(i);
			
			if (escape) {
				r.push(tokens[c] || c);
				escape = false;
			}
			else if (c == "%")
				escape = true;
			else
				r.push(c);
		}
		return r.join('');
	},
	getFormat: function(time, span) {
		var tu = FlotrExtended.Date.timeUnits;
		     if (time < tu.second) return "%h:%M:%S.%s";
		else if (time < tu.minute) return "%h:%M:%S";
		else if (time < tu.day)    return (span < 2 * tu.day) ? "%h:%M" : "%b %d %h:%M";
		else if (time < tu.month)  return "%b %d";
		else if (time < tu.year)   return (span < tu.year) ? "%b" : "%b %y";
		else                       return "%y";
	},
	formatter: function (v, axis) {
		var d = new Date(v);

		// first check global format
		if (axis.options.timeFormat != null)
			return FlotrExtended.Date.format(d, axis.options.timeFormat);
		
		var span = axis.max - axis.min,
		    t = axis.tickSize * FlotrExtended.Date.timeUnits[axis.tickUnit];
				
		return FlotrExtended.Date.format(d, FlotrExtended.Date.getFormat(t, span));
	},
	generator: function(axis) {
		var ticks = [],
			d = new Date(axis.min),
			tu = FlotrExtended.Date.timeUnits;
		
		var step = axis.tickSize * tu[axis.tickUnit];

		switch (axis.tickUnit) {
			case "millisecond": d.setUTCMilliseconds(FlotrExtended.floorInBase(d.getUTCMilliseconds(), axis.tickSize)); break;
			case "second": d.setUTCSeconds(FlotrExtended.floorInBase(d.getUTCSeconds(), axis.tickSize)); break;
			case "minute": d.setUTCMinutes(FlotrExtended.floorInBase(d.getUTCMinutes(), axis.tickSize)); break;
			case "hour":   d.setUTCHours(FlotrExtended.floorInBase(d.getUTCHours(), axis.tickSize)); break;
			case "month":  d.setUTCMonth(FlotrExtended.floorInBase(d.getUTCMonth(), axis.tickSize)); break;
			case "year":   d.setUTCFullYear(FlotrExtended.floorInBase(d.getUTCFullYear(), axis.tickSize));break;
		}
		
		// reset smaller components
		if (step >= tu.second)  d.setUTCMilliseconds(0);
		if (step >= tu.minute)  d.setUTCSeconds(0);
		if (step >= tu.hour)    d.setUTCMinutes(0);
		if (step >= tu.day)     d.setUTCHours(0);
		if (step >= tu.day * 4) d.setUTCDate(1);
		if (step >= tu.year)    d.setUTCMonth(0);

		var carry = 0, v = Number.NaN, prev;
		do {
			prev = v;
			v = d.getTime();
			ticks.push({ v:v, label:FlotrExtended.Date.formatter(v, axis) });
			if (axis.tickUnit == "month") {
				if (axis.tickSize < 1) {
					/* a bit complicated - we'll divide the month up but we need to take care of fractions
					 so we don't end up in the middle of a day */
					d.setUTCDate(1);
					var start = d.getTime();
					d.setUTCMonth(d.getUTCMonth() + 1);
					var end = d.getTime();
					d.setTime(v + carry * tu.hour + (end - start) * axis.tickSize);
					carry = d.getUTCHours();
					d.setUTCHours(0);
				}
				else
					d.setUTCMonth(d.getUTCMonth() + axis.tickSize);
			}
			else if (axis.tickUnit == "year") {
				d.setUTCFullYear(d.getUTCFullYear() + axis.tickSize);
			}
			else
				d.setTime(v + step);

		} while (v < axis.max && v != prev);
		
		return ticks;
	},
	timeUnits: {
		millisecond: 1,
		second: 1000,
		minute: 1000 * 60,
		hour:   1000 * 60 * 60,
		day:    1000 * 60 * 60 * 24,
		month:  1000 * 60 * 60 * 24 * 30,
		year:   1000 * 60 * 60 * 24 * 365.2425
	},
	// the allowed tick sizes, after 1 year we use an integer algorithm
	spec: [
		[1, "millisecond"], [20, "millisecond"], [50, "millisecond"], [100, "millisecond"], [200, "millisecond"], [500, "millisecond"], 
		[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"]
};

/** Lines **/
FlotrExtended.addType('lines', {
	options: {
		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
        paired: false,			// Is this line paired for a spread
        pair: 0,				// What's it paired with?
        lineOpacity: 1			// Lines can have opacity!
	},
	/**
	 * Draws lines series in the canvas element.
	 * @param {Object} series - Series with options.lines.show = true.
	 */
	draw: 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.lines.plot(series, offset + sw/2);

			ctx.strokeStyle = "rgba(0,0,0,0.2)";
			this.lines.plot(series, offset);

			if(series.lines.fill && !series.lines.paired) {
				ctx.fillStyle = "rgba(0,0,0,0.05)";
				this.lines.plotArea(series, offset + sw/2);
			}
		}

		ctx.lineWidth = lw;
		ctx.strokeStyle = series.color;
		
		if(series.lines.fill){
			ctx.fillStyle = this.processColor(series.lines.fillColor || series.color, {opacity: series.lines.fillOpacity});
			
			if(series.lines.paired) {
            	var pair = series.lines.pair;
            	this.lines.plotAreaBetween(this.series[pair],this.series[pair+1]);            	
            } else {
            	this.lines.plotArea(series, 0);
            }
			
		}

		this.lines.plot(series, 0);
		ctx.restore();
	},	
	plot: function(series, offset){
		var ctx = this.ctx,
		    xa = series.xaxis,
		    ya = series.yaxis,
  			data = series.data, 
  			length = data.length - 1, i;
			
		if(data.length < 2) return;

		var prevx = xa.d2p(data[0][0]),
		    prevy = ya.d2p(data[0][1]) + offset;

		ctx.beginPath();
		ctx.moveTo(prevx, prevy);

		for(i = 0; i < length; ++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 != xa.d2p(x1) || prevy != ya.d2p(y1) + offset)
				ctx.moveTo(xa.d2p(x1), ya.d2p(y1) + offset);
			
			prevx = xa.d2p(x2);
			prevy = ya.d2p(y2) + offset;
			ctx.lineTo(prevx, prevy);
		}
    
		ctx.stroke();
		ctx.closePath();
	},
	/**
	 * Function used to fill
	 * @param {Object} series - The series to draw
	 * @param {Object} offset
	 */
	plotArea: function(series, offset){
		var ctx = this.ctx,
		    xa = series.xaxis,
		    ya = series.yaxis,
		    data = series.data,
		    length = data.length - 1,
		    top, 
		    bottom = Math.min(Math.max(0, ya.min), ya.max),
		    lastX = 0,
		    first = true;
      
		if(data.length < 2) return;
    
		ctx.beginPath();
    
		for(var i = 0; i < length; ++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(xa.d2p(x1), ya.d2p(bottom) + offset);
				first = false;
			}
			
			/**
			 * Now check the case where both is outside.
			 */
			if(y1 >= ya.max && y2 >= ya.max){
				ctx.lineTo(xa.d2p(x1), ya.d2p(ya.max) + offset);
				ctx.lineTo(xa.d2p(x2), ya.d2p(ya.max) + offset);
				continue;
			}
			else if(y1 <= ya.min && y2 <= ya.min){
				ctx.lineTo(xa.d2p(x1), ya.d2p(ya.min) + offset);
				ctx.lineTo(xa.d2p(x2), ya.d2p(ya.min) + 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(xa.d2p(x1old), ya.d2p(top) + offset);
				ctx.lineTo(xa.d2p(x1), ya.d2p(top) + offset);
			}
		   	
			/**
			 * Fill the triangles.
			 */
			ctx.lineTo(xa.d2p(x1), ya.d2p(y1) + offset);
			ctx.lineTo(xa.d2p(x2), ya.d2p(y2) + offset);

			/**
			 * Fill the other rectangle if it's there.
			 */
			if(x2 != x2old){
				top = (y2 <= ya.min) ? ya.min : ya.max;
				ctx.lineTo(xa.d2p(x2old), ya.d2p(top) + offset);
				ctx.lineTo(xa.d2p(x2), ya.d2p(top) + offset);
			}

			lastX = Math.max(x2, x2old);
		}
		
		ctx.lineTo(xa.d2p(xa.max), ya.d2p(bottom) + offset);
		ctx.closePath();
		ctx.fill();
	},
		
	/**
	 * Function used to fill area between paired data for spreads
	 * @param {Object} series_top - The top series
	 * @param {Object} series_bot - The bottom series
	 * @param {Object} offset
	 */
	plotAreaBetween: function(series_top, series_bot){

		var ctx = this.ctx,
	    data_top = series_top.data,
	    data_bot = series_bot.data;	

	    xa = series_top.xaxis,
	    ya = series_top.yaxis;
		
		if (data_top.length < 2 || data_bot.length < 2) {
            return;
        } 

		// For each set of points, draw two filled in rectangles, all together this will
        // create the spread.
        for (topindex = 1; topindex < data_top.length; topindex++) {
        	
            // Assume positive values - this is DANGEROUS but we'll let it go, for now.        	
        	var x1 = xa.d2p(data_top[topindex-1][0]);
        	var y1 = ya.d2p(data_top[topindex-1][1]);
        	var x2 = xa.d2p(data_top[topindex][0]);
        	var y2 = ya.d2p(data_top[topindex][1]);
        	var x3 = xa.d2p(data_bot[topindex][0]);
        	var y3 = ya.d2p(data_bot[topindex][1]);
        	var x4 = xa.d2p(data_bot[topindex-1][0]);
        	var y4 = ya.d2p(data_bot[topindex-1][1]);
        	
        	ctx.beginPath();
        	ctx.moveTo(x1,y1);
        	ctx.lineTo(x2,y2); 
        	ctx.lineTo(x3,y3); 
        	ctx.lineTo(x4,y4); 
        	ctx.lineTo(x1,y1); 
        	ctx.closePath();
        	ctx.fill();            
            
        }
	
	}	
	
});

/** Bars **/
FlotrExtended.addType('bars', {
	options: {
		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,     // => horizontal bars (x and y inverted) @todo: needs fix
		stacked: false,        // => stacked bar charts
		centered: false         // => center the bars to their x axis value
	},
	/**
	 * Draws bar series in the canvas element.
	 * @param {Object} series - Series with options.bars.show = true.
	 */
	draw: 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;
    
		ctx.save();
		this.bars.plotShadows(series, bw, 0, series.bars.fill);
		ctx.restore();
    
		if(series.bars.fill){
			var color = series.bars.fillColor || series.color;
			ctx.fillStyle = this.processColor(color, {opacity: series.bars.fillOpacity});
		}
    
		this.bars.plot(series, bw, 0, series.bars.fill);
		ctx.restore();
	},
	plot: function(series, barWidth, offset, fill){
		var data = series.data;
		if(data.length < 1) return;
		
		var xa = series.xaxis,
		    ya = series.yaxis,
		    ctx = this.ctx, i;

		for(i = 0; i < data.length; i++){
			var x = data[i][0],
			    y = data[i][1],
				drawLeft = true, drawTop = true, drawRight = true;
			
			if (y === null) continue;
			
			// Stacked bars
			var stackOffset = 0;
			if(series.bars.stacked) {
				$H(xa.values).each(function(pair) {
					if (pair.key == x) {
						stackOffset = pair.value.stack || 0;
						pair.value.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 - (series.bars.centered ? barWidth/2 : 0), right = x + barWidth - (series.bars.centered ? barWidth/2 : 0), 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(xa.d2p(left), ya.d2p(bottom) + offset);
				ctx.lineTo(xa.d2p(left), ya.d2p(top) + offset);
				ctx.lineTo(xa.d2p(right), ya.d2p(top) + offset);
				ctx.lineTo(xa.d2p(right), ya.d2p(bottom) + offset);
				ctx.fill();
				ctx.closePath();
			}

			/**
			 * Draw bar outline/border.
			 */
			if(series.bars.lineWidth != 0 && (drawLeft || drawRight || drawTop)){
				ctx.beginPath();
				ctx.moveTo(xa.d2p(left), ya.d2p(bottom) + offset);
				
				ctx[drawLeft ?'lineTo':'moveTo'](xa.d2p(left), ya.d2p(top) + offset);
				ctx[drawTop  ?'lineTo':'moveTo'](xa.d2p(right), ya.d2p(top) + offset);
				ctx[drawRight?'lineTo':'moveTo'](xa.d2p(right), ya.d2p(bottom) + offset);
				         
				ctx.stroke();
				ctx.closePath();
			}
		}
	},
	plotShadows: function(series, barWidth, offset){
		var data = series.data;
		if(data.length < 1) return;
		
		var i, x, y, 
		    xa = series.xaxis,
		    ya = series.yaxis,
		    ctx = this.ctx,
		    sw = this.options.shadowSize;
		
		for(i = 0; i < data.length; i++){
			x = data[i][0];
		    y = data[i][1];
				
			if (y === null) continue;
			
			// Stacked bars
			var stackOffset = 0;
			if(series.bars.stacked) {
				$H(xa.values).each(function(pair) {
					if (pair.key == x) {
						stackOffset = pair.value.stackShadow || 0;
						pair.value.stackShadow = stackOffset + y;
					}
				});
			}
			
			// Horizontal bars
			if(series.bars.horizontal) 
				var left = stackOffset, right = x + stackOffset, bottom = y, top = y + barWidth;
			else 
				var left = x - (series.bars.centered ? barWidth/2 : 0), right = x + barWidth - (series.bars.centered ? barWidth/2 : 0), 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 =  xa.d2p(right)-xa.d2p(left)-((xa.d2p(right)+sw <= this.plotWidth) ? 0 : sw);
			var height = Math.max(0, ya.d2p(bottom)-ya.d2p(top)-((ya.d2p(bottom)+sw <= this.plotHeight) ? 0 : sw));
			
			ctx.fillStyle = 'rgba(0,0,0,0.05)';
			ctx.fillRect(Math.min(xa.d2p(left)+sw, this.plotWidth), Math.min(ya.d2p(top)+sw, this.plotWidth), width, height);
		}
	},
	extendXRange: function(axis) {
		if(axis.options.max == null){
			var newmin = axis.min,
			    newmax = axis.max,
			    i, j, x, s, b,
			    stackedSums = [], 
			    lastSerie = null;

			for(i = 0; i < this.series.length; ++i){
				s = this.series[i];
				b = s.bars;
				if(b.show && s.xaxis == axis) {
          if (b.centered) {
						newmax = Math.max(axis.datamax + 0.5, newmax);
						newmin = Math.min(axis.datamin - 0.5, newmin);
					}
          
					// For normal vertical bars
					if (!b.horizontal && (b.barWidth + axis.datamax > newmax))
						newmax = axis.max + (b.centered ? b.barWidth/2 : b.barWidth);

					// For horizontal stacked bars
					if(b.stacked && b.horizontal){
						for (j = 0; j < s.data.length; j++) {
							if (b.show && b.stacked) {
								x = s.data[j][0]+'';
								stackedSums[x] = (stackedSums[x] || 0) + s.data[j][1];
								lastSerie = s;
							}
						}
				    
						for (j in stackedSums) {
							newmax = Math.max(stackedSums[j], newmax);
						}
					}
				}
			}
			axis.lastSerie = lastSerie;
			axis.max = newmax;
			axis.min = newmin;
		}
	},
	extendYRange: function(axis){
		if(axis.options.max == null){
			var newmax = axis.max,
				  x, i, j, s, b,
				  stackedSums = {},
				  lastSerie = null;
									
			for(i = 0; i < this.series.length; ++i){
				s = this.series[i];
				b = s.bars;
				if (b.show && !s.hide && s.yaxis == axis) {
					// For normal horizontal bars
					if (b.horizontal && (b.barWidth + axis.datamax > newmax)){
						newmax = axis.max + b.barWidth;
					}
					
					// For vertical stacked bars
					if(b.stacked && !b.horizontal){
						for (j = 0; j < s.data.length; j++) {
							if (s.bars.show && s.bars.stacked) {
								x = s.data[j][0]+'';
								stackedSums[x] = (stackedSums[x] || 0) + s.data[j][1];
								lastSerie = s;
							}
						}
						
						for (j in stackedSums) {
							newmax = Math.max(stackedSums[j], newmax);
						}
					}
				}
			}
			axis.lastSerie = lastSerie;
			axis.max = newmax;
		}
	}
});

/** Points **/
FlotrExtended.addType('points', {
	options: {
		show: false,           // => setting to true will show points, false will hide
		showValues: false,  	// => setting to true will show values
		showValuesOnly: false,  // => setting to true will values instead of circles, if points is show=true
		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       // => opacity of color inside the points
	},
	/**
	 * Draws point series in the canvas element.
	 * @param {Object} series - Series with options.points.show = true.
	 */
	draw: function(series) {
		var ctx = this.ctx,
		    lw = series.lines.lineWidth,
		    sw = series.shadowSize;
		
		ctx.save();
		ctx.translate(this.plotOffset.left, this.plotOffset.top);
		
		if(sw > 0){
			ctx.lineWidth = sw / 2;
      
			ctx.strokeStyle = 'rgba(0,0,0,0.1)';
			this.points.plotShadows(series, sw/2 + ctx.lineWidth/2, series.points.radius);

			ctx.strokeStyle = 'rgba(0,0,0,0.2)';
			this.points.plotShadows(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.points.plot(series, series.points.radius, series.points.fill);
		ctx.restore();
	},
	plot: function (series, radius, fill) {
		var xa = series.xaxis,
		    ya = series.yaxis,
		    ctx = this.ctx, i, x,
		    data = series.data;
			
		for(i = data.length - 1; i > -1; --i){
			x = data[i][0], y = data[i][1];
			// To allow empty values
			if(y === null || x < xa.min || x > xa.max || y < ya.min || y > ya.max)
				continue;			
			
			if(series.points.showValues) {
				
				var style = {
					size: this.options.fontSize
				};
				style.color = this.options.grid.color;

				var textToShow = (Math.round(y*100)/100).toFixed(2);				
				
				var labelSize = this.getTextDimensions(textToShow, {size: this.options.fontSize, angle: 0}, '', '');
				FlotrExtended.drawText(ctx, textToShow, xa.d2p(x)-(labelSize.width/2), ya.d2p(y)-(labelSize.height/3), style);
				
			} 
			
			if(!series.points.showValuesOnly) {					
				
				ctx.beginPath();
				ctx.arc(xa.d2p(x), ya.d2p(y), radius, 0, 2 * Math.PI, true);
				if(fill) ctx.fill();
				ctx.stroke();
				ctx.closePath();
				
			}
		}
	},
	plotShadows: function(series, offset, radius){
		var xa = series.xaxis,
		    ya = series.yaxis,
		    ctx = this.ctx, i, x,
		    data = series.data;
		
		if(!series.points.showValuesOnly) {
			
			for(i = data.length - 1; i > -1; --i){
				x = data[i][0], y = data[i][1];
				if (y === null || x < xa.min || x > xa.max || y < ya.min || y > ya.max)
					continue;
				ctx.beginPath();
				ctx.arc(xa.d2p(x), ya.d2p(y) + offset, radius, 0, Math.PI, false);
				ctx.stroke();
				ctx.closePath();
			}
		}
		
	},
	getHit: function(series, pos) {
		var xdiff, ydiff, i, d, dist, x, y,
		    o = series.points,
		    data = series.data,
		    sens = series.mouse.sensibility * (o.lineWidth + o.radius),
		    hit = {
				index: null,
				series: series,
				distance: Number.MAX_VALUE,
				x: null,
				y: null,
				precision: 1
		    };
		
		for (i = data.length-1; i > -1; --i) {
			d = data[i];
			x = series.xaxis.d2p(d[0]);
			y = series.yaxis.d2p(d[1]);
			xdiff = x - pos.relX;
			ydiff = y - pos.relY;
			dist = Math.sqrt(xdiff*xdiff + ydiff*ydiff);
			
			if (dist < sens && dist < hit.distance) {
				hit = {
					index: i,
					series: series,
					distance: dist,
					data: d,
					x: x,
					y: y,
					precision: 1
				};
			}
		}
		
		return hit;
	},
	drawHit: function(series, index) {
		
	},
	clearHit: function(series, index) {
		
	}
});


/** Pie **/
/**
 * Formats the pies labels.
 * @param {Object} slice - Slice object
 * @return {String} Formatted pie label string
 */
FlotrExtended.defaultPieLabelFormatter = function(slice) {
	return (slice.fraction*100).toFixed(2)+'%';
};

FlotrExtended.addType('pie', {
	options: {
		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,            // => the number of pixels the splices will be far from the center
		sizeRatio: 0.6,        // => the size ratio of the pie relative to the plot 
		startAngle: Math.PI/4, // => the first slice start angle
		labelFormatter: FlotrExtended.defaultPieLabelFormatter,
		pie3D: false,          // => whether to draw the pie in 3 dimenstions or not (ineffective) 
		pie3DviewAngle: (Math.PI/2 * 0.8),
		pie3DspliceThickness: 20
	},
	/**
	 * Draws a pie in the canvas element.
	 * @param {Object} series - Series with options.pie.show = true.
	 */
	draw: function(series) {
		if (this.options.pie.drawn) return;
		var ctx = this.ctx,
		    options = this.options,
		    lw = series.pie.lineWidth,
		    sw = series.shadowSize,
		    data = series.data,
		    plotOffset = this.plotOffset,
		    radius = (Math.min(this.canvasWidth, this.canvasHeight) * series.pie.sizeRatio) / 2,
		    html = [],
			vScale = 1,//Math.cos(series.pie.viewAngle);
			plotTickness = Math.sin(series.pie.viewAngle)*series.pie.spliceThickness / vScale,
		
		style = {
			size: options.fontSize*1.2,
			color: options.grid.color,
			weight: 1.5
		},
		
		center = {
			x: plotOffset.left + (this.plotWidth)/2,
			y: plotOffset.top + (this.plotHeight)/2
		},
		
		// Pie portions
		portions = this.series.collect(function(hash, index){
			if (hash.pie.show && hash.data[0][1] !== null)
				return {
					name: (hash.label || hash.data[0][1]),
					value: [index, hash.data[0][1]],
					options: hash.pie,
					series: hash
				};
		}),
		
		// Sum of the portions' angles
		sum = portions.pluck('value').pluck(1).inject(0, function(acc, n) { return acc + n; }),
		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,
				value:    value,
				options:  slice.options,
				series:   slice.series,
				startAngle: 2 * angle * Math.PI,
				endAngle:   2 * (angle + fraction) * Math.PI
			};
		});
		
		ctx.save();
		
		if(sw > 0){
			slices.each(function (slice) {
				if (slice.startAngle == slice.endAngle) return;
				
				var bisection = (slice.startAngle + slice.endAngle) / 2,
				    xOffset = center.x + Math.cos(bisection) * slice.options.explode + sw,
				    yOffset = center.y + Math.sin(bisection) * slice.options.explode + sw;
				
				this.pie.plotSlice(xOffset, yOffset, radius, slice.startAngle, slice.endAngle, false, vScale);
				
				if (series.pie.fill) {
					ctx.fillStyle = 'rgba(0,0,0,0.1)';
					ctx.fill();
				}
			}, this);
		}
		
		if (options.HtmlText || !this.textEnabled)
			html = ['<div style="color:' + this.options.grid.color + '" class="flotr-labels">'];
		
		slices.each(function (slice, index) {
			if (slice.startAngle == slice.endAngle) return;
			
			var bisection = (slice.startAngle + slice.endAngle) / 2,
			    color = slice.series.color,
			    fillColor = slice.options.fillColor || color,
			    xOffset = center.x + Math.cos(bisection) * slice.options.explode,
			    yOffset = center.y + Math.sin(bisection) * slice.options.explode;
			
			this.pie.plotSlice(xOffset, yOffset, radius, slice.startAngle, slice.endAngle, false, vScale);
			
			if(series.pie.fill){
				ctx.fillStyle = this.processColor(fillColor, {opacity: series.pie.fillOpacity});
				ctx.fill();
			}
			ctx.lineWidth = lw;
			ctx.strokeStyle = color;
			ctx.stroke();
			
			var label = options.pie.labelFormatter(slice),
			    textAlignRight = (Math.cos(bisection) < 0),
			    textAlignTop = (Math.sin(bisection) > 0),
			    explodeCoeff = (slice.options.explode || series.pie.explode) + radius + 4,
			    distX = center.x + Math.cos(bisection) * explodeCoeff,
			    distY = center.y + Math.sin(bisection) * explodeCoeff;
			
			if (slice.fraction && label) {
				if (options.HtmlText || !this.textEnabled) {
					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.textAlign = textAlignRight ? 'right' : 'left';
					style.textBaseline = textAlignTop ? 'top' : 'bottom';
					FlotrExtended.drawText(ctx, label, distX, distY, style);
				}
			}
		}, this);
		
		if (options.HtmlText || !this.textEnabled) {
			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.scale(1, vScale);
		ctx.beginPath();
		ctx.moveTo(x, y);
		ctx.arc   (x, y, radius, startAngle, endAngle, fill);
		ctx.lineTo(x, y);
		ctx.closePath();
	}
});


/** Candles **/
FlotrExtended.addType('candles', {
	options: {
		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)
	},
	/**
	 * Draws candles series in the canvas element.
	 * @param {Object} series - Series with options.candles.show = true.
	 */
	draw: 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.candles.plotShadows(series, bw/2);
		this.candles.plot(series, bw/2);
		
		ctx.restore();
	},
	plot: function(series, offset){
		var data = series.data;
		if(data.length < 1) return;
		
		var xa = series.xaxis,
		    ya = series.yaxis,
		    ctx = this.ctx;

		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 - series.candles.candleWidth/2,
			    right   = x + series.candles.candleWidth/2,
			    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 = this.processColor(color, {opacity: series.candles.fillOpacity});
				ctx.fillRect(xa.d2p(left), ya.d2p(top2) + offset, xa.d2p(right) - xa.d2p(left), ya.d2p(bottom2) - ya.d2p(top2));
			}

			/**
			 * 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(xa.d2p((left + right) / 2)) + 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(ya.d2p(top) + offset));
					ctx.lineTo(x, Math.floor(ya.d2p(bottom) + offset));
					
					y = Math.floor(ya.d2p(open) + offset)+0.5;
					ctx.moveTo(Math.floor(xa.d2p(left))+pixelOffset, y);
					ctx.lineTo(x, y);
					
					y = Math.floor(ya.d2p(close) + offset)+0.5;
					ctx.moveTo(Math.floor(xa.d2p(right))+pixelOffset, y);
					ctx.lineTo(x, y);
				} 
				else {
					ctx.strokeRect(xa.d2p(left), ya.d2p(top2) + offset, xa.d2p(right) - xa.d2p(left), ya.d2p(bottom2) - ya.d2p(top2));
					
					ctx.beginPath();
					ctx.moveTo(x, Math.floor(ya.d2p(top2   ) + offset));
					ctx.lineTo(x, Math.floor(ya.d2p(top    ) + offset));
					ctx.moveTo(x, Math.floor(ya.d2p(bottom2) + offset));
					ctx.lineTo(x, Math.floor(ya.d2p(bottom ) + offset));
				}
				
				ctx.stroke();
				ctx.closePath();
				ctx.restore();
			}
		}
	},
	plotShadows: function(series, offset){
		var data = series.data;
		if(data.length < 1 || series.candles.barcharts) return;
		
		var xa = series.xaxis,
		    ya = series.yaxis,
		    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 - series.candles.candleWidth/2,
			    right  = x + series.candles.candleWidth/2,
			    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 =  xa.d2p(right)-xa.d2p(left)-((xa.d2p(right)+sw <= this.plotWidth) ? 0 : sw);
			var height = Math.max(0, ya.d2p(bottom)-ya.d2p(top)-((ya.d2p(bottom)+sw <= this.plotHeight) ? 0 : sw));
			
			this.ctx.fillStyle = 'rgba(0,0,0,0.05)';
			this.ctx.fillRect(Math.min(xa.d2p(left)+sw, this.plotWidth), Math.min(ya.d2p(top)+sw, this.plotWidth), width, height);
		}
	},
	extendXRange: function(axis){
		if(axis.options.max == null){
			var newmin = axis.min,
			    newmax = axis.max,
			    i, c;

			for(i = 0; i < this.series.length; ++i){
				c = this.series[i].candles;
				if(c.show && this.series[i].xaxis == axis) {
					// We don't use c.candleWidth in order not to stick the borders
					newmax = Math.max(axis.datamax + 0.5, newmax);
					newmin = Math.min(axis.datamin - 0.5, newmin);
				}
			}
			axis.max = newmax;
			axis.min = newmin;
		}
	}
});


/** Markers **/
/**
 * Formats the marker labels.
 * @param {Object} obj - Marker value Object {x:..,y:..}
 * @return {String} Formatted marker string
 */
FlotrExtended.defaultMarkerFormatter = function(obj){
	return (Math.round(obj.y*100)/100)+'';
};

FlotrExtended.addType('markers', {
	options: {
		show: false,           // => setting to true will show markers, false will hide
		lineWidth: 1,          // => line width of the rectangle around the marker
		fill: false,           // => fill or not the marekers' rectangles
		fillColor: "#FFFFFF",  // => fill color
		fillOpacity: 0.4,      // => fill opacity
		stroke: false,         // => draw the rectangle around the markers
		position: 'ct',        // => the markers position (vertical align: b, m, t, horizontal align: l, c, r)
		labelFormatter: FlotrExtended.defaultMarkerFormatter
	},
	/**
	 * Draws lines series in the canvas element.
	 * @param {Object} series - Series with options.lines.show = true.
	 */
	draw: function(series){
		series = series || this.series;
		var ctx = this.ctx,
		    xa = series.xaxis,
		    ya = series.yaxis,
		    options = series.markers,
		    data = series.data;
        
		ctx.save();
		ctx.translate(this.plotOffset.left, this.plotOffset.top);
		ctx.lineJoin = 'round';
		ctx.lineWidth = options.lineWidth;
		ctx.strokeStyle = 'rgba(0,0,0,0.5)';
		ctx.fillStyle = this.processColor(options.fillColor, {opacity: options.fillOpacity});

		for(var i = 0; i < data.length; ++i){
			var x = data[i][0], xPos = xa.d2p(x),
			    y = data[i][1], yPos = ya.d2p(y),
			    label = options.labelFormatter({x: x, y: y, index: i, data : data});
          
			this.markers.plot(xPos, yPos, label, options);
		}
    
		ctx.restore();
	},
	plot: function(x, y, label, options) {
		var ctx = this.ctx,
		    dim = this.getTextDimensions(label, null, null),
		    margin = 2,
		    left = x,
		    top = y;
        
		dim.width = Math.floor(dim.width+margin*2);
		dim.height = Math.floor(dim.height+margin*2);

		     if (options.position.indexOf('c') != -1) left -= dim.width/2 + margin;
		else if (options.position.indexOf('l') != -1) left -= dim.width;
    
		     if (options.position.indexOf('m') != -1) top -= dim.height/2 + margin;
		else if (options.position.indexOf('t') != -1) top -= dim.height;
    
		left = Math.floor(left)+0.5;
		top = Math.floor(top)+0.5;
    
		if(options.fill)
			ctx.fillRect(left, top, dim.width, dim.height);
      
		if(options.stroke)
			ctx.strokeRect(left, top, dim.width, dim.height);
    
		FlotrExtended.drawText(ctx, label, left+margin, top+margin, {textBaseline: 'top', textAlign: 'left'});
	}
});

FlotrExtended.addType('radar', {
	options: {
		show: false,           // => setting to true will show radar chart, false will hide
		lineWidth: 2,          // => line width in pixels
		fill: true,            // => true to fill the area from the line to the x axis, false for (transparent) no fill
		fillOpacity: 0.4,      // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
		radiusRatio: 0.90      // => ratio of the radar, against the plot size
	},
	draw: function(series){
		var ctx = this.ctx,
		    options = this.options;
		
		ctx.save();
		ctx.translate(this.plotOffset.left+this.plotWidth/2, this.plotOffset.top+this.plotHeight/2);
		ctx.lineWidth = series.radar.lineWidth;
		
		ctx.fillStyle = 'rgba(0,0,0,0.05)';
		ctx.strokeStyle = 'rgba(0,0,0,0.05)';
		this.radar.plot(series, series.shadowSize / 2);
		
		ctx.strokeStyle = 'rgba(0,0,0,0.1)';
		this.radar.plot(series, series.shadowSize / 4);
		
		ctx.strokeStyle = series.color;
		ctx.fillStyle = this.processColor(series.color, {opacity: series.radar.fillOpacity});
		this.radar.plot(series);
		
		ctx.restore();
	},
	plot: function(series, offset){
		var ctx = this.ctx,
		    options = this.options,
				data = series.data,
				radius = Math.min(this.plotHeight, this.plotWidth)*options.radar.radiusRatio/2,
			  coeff = 2*(Math.PI/data.length),
				angle = -Math.PI/2;
				
		offset = offset || 0;
		
		ctx.beginPath();
		for(var i = 0; i < data.length; ++i){
			var x = data[i][0],
			    y = data[i][1],
					ratio = y / this.axes.y.max;

			ctx[i == 0 ? 'moveTo' : 'lineTo'](Math.cos(i*coeff+angle)*radius*ratio + offset, Math.sin(i*coeff+angle)*radius*ratio + offset);
		}
		ctx.closePath();
		if (series.radar.fill) ctx.fill();
		ctx.stroke();
	}
});

FlotrExtended.addType('bubbles', {
	options: {
		show: false,      // => setting to true will show radar chart, false will hide
		lineWidth: 2,     // => line width in pixels
		fill: true,       // => true to fill the area from the line to the x axis, false for (transparent) no fill
		fillOpacity: 0.4, // => opacity of the fill color, set to 1 for a solid fill, 0 hides the fill
		baseRadius: 2     // => ratio of the radar, against the plot size
	},
	draw: function(series){
		var ctx = this.ctx,
		    options = this.options;
		
		ctx.save();
		ctx.translate(this.plotOffset.left, this.plotOffset.top);
		ctx.lineWidth = series.bubbles.lineWidth;
		
		ctx.fillStyle = 'rgba(0,0,0,0.05)';
		ctx.strokeStyle = 'rgba(0,0,0,0.05)';
		this.bubbles.plot(series, series.shadowSize / 2);
		
		ctx.strokeStyle = 'rgba(0,0,0,0.1)';
		this.bubbles.plot(series, series.shadowSize / 4);
		
		ctx.strokeStyle = series.color;
		ctx.fillStyle = this.processColor(series.color, {opacity: series.radar.fillOpacity});
		this.bubbles.plot(series);
		
		ctx.restore();
	},
	plot: function(series, offset){
		var ctx = this.ctx,
		    options = this.options,
		    data = series.data,
		    radius = options.bubbles.baseRadius;
				
		offset = offset || 0;
		
		for(var i = 0; i < data.length; ++i){
			var x = data[i][0],
			    y = data[i][1],
			    z = data[i][2];
          
			ctx.beginPath();
			ctx.arc(series.xaxis.d2p(x) + offset, series.yaxis.d2p(y) + offset, radius * z, 0, Math.PI*2, true);
			ctx.stroke();
			if (series.bubbles.fill) ctx.fill();
			ctx.closePath();
		}
	}/*,
	extendXRange: function(axis){
		if(axis.options.max == null){
			var newmin = axis.min,
			    newmax = axis.max,
			    i, j, c, r, data, d;
          
			for(i = 0; i < this.series.length; ++i){
				c = this.series[i].bubbles;
				if(c.show && this.series[i].xaxis == axis) {
					data = this.series[i].data;
					if (data)
					for(j = 0; j < data.length; j++) {
						d = data[j];
						r = d[2] * c.baseRadius * (this.plotWidth / (axis.datamax - axis.datamin));
  						newmax = Math.max(d[0] + r, newmax);
  						newmin = Math.min(d[0] - r, newmin);
					}
				}
			}
			axis.max = newmax;
			axis.min = newmin;
		}
	},
	extendYRange: function(axis){
		if(axis.options.max == null){
			var newmin = axis.min,
			    newmax = axis.max,
			    i, j, c, r, data, d;

			for(i = 0; i < this.series.length; ++i){
				c = this.series[i].bubbles;
				if(c.show && this.series[i].yaxis == axis) {
					data = this.series[i].data;
					if (data)
					for(j = 0; j < data.length; j++) {
						d = data[j];
						r = d[2] * c.baseRadius;
						newmax = Math.max(d[1] + r, newmax);
						newmin = Math.min(d[1] - r, newmin);
					}
				}
			}
			axis.max = newmax;
			axis.min = newmin;
		}
	}*/
});

FlotrExtended.addPlugin('spreadsheet', {
	options: {
		show: false,           // => show the data grid using two tabs
		tabGraphLabel: 'Graph',
		tabDataLabel: 'Data',
		toolbarDownload: 'Download CSV', // @todo: add better language support
		toolbarSelectAll: 'Select all',
		csvFileSeparator: ',',
		decimalSeparator: '.',
		tickFormatter: null
	},
	/**
	 * Builds the tabs in the DOM
	 */
	callbacks: {
    'flotr:afterconstruct': function(){
      this.el.select('.flotr-tabs-group,.flotr-datagrid-container').invoke('remove');
      
      if (!this.options.spreadsheet.show) return;
      
      var ss = this.spreadsheet;
      ss.tabsContainer = new Element('div', {style:'position:absolute;left:0px;width:'+this.canvasWidth+'px'}).addClassName('flotr-tabs-group');
  		ss.tabs = {
  			graph: new Element('div', {style:'float:left'}).addClassName('flotr-tab selected').update(this.options.spreadsheet.tabGraphLabel),
  			data: new Element('div', {style:'float:left'}).addClassName('flotr-tab').update(this.options.spreadsheet.tabDataLabel)
  		};
  		ss.tabsContainer.insert(ss.tabs.graph).insert(ss.tabs.data);
      
      this.el.insert({bottom: ss.tabsContainer});
      
      var offset = ss.tabs.data.getHeight() + 2;
      this.plotOffset.bottom += offset;
      ss.tabsContainer.setStyle({top: this.canvasHeight-offset+'px'});
      
  		ss.tabs.graph.observe('click', function(){ss.showTab('graph')});
  		ss.tabs.data.observe('click', function(){ss.showTab('data')});
  	}
  },
  /**
   * Constructs the data table for the spreadsheet
   * @todo make a spreadsheet manager (FlotrExtended.Spreadsheet)
   * @return {Element} The resulting table element
   */
	constructDataGrid: function(){
		// If the data grid has already been built, nothing to do here
		if (this.spreadsheet.datagrid) return this.spreadsheet.datagrid;
		
		var i, j, 
		    s = this.series,
		    datagrid = this.loadDataGrid(),
		    t = this.spreadsheet.datagrid = new Element('table').addClassName('flotr-datagrid'),
		    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',
				    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 if (this.options.spreadsheet.tickFormatter){
						label = this.options.spreadsheet.tickFormatter(content);
					}
					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');
				}).observe('mouseout', function(){
					t.select('colgroup col.hover, th.hover').invoke('removeClassName', 'hover');
				});
			});
		}
		
		var toolbar = new Element('div').addClassName('flotr-datagrid-toolbar').
			insert(
			  new Element('button', {type:'button'})
			      .addClassName('flotr-datagrid-toolbar-button')
			      .update(this.options.spreadsheet.toolbarDownload)
			      .observe('click', this.spreadsheet.downloadCSV.bindAsEventListener(this))
			).
			insert(
			  new Element('button', {type:'button'})
			      .addClassName('flotr-datagrid-toolbar-button')
			      .update(this.options.spreadsheet.toolbarSelectAll)
			      .observe('click', this.spreadsheet.selectAllData.bindAsEventListener(this))
			);
		
		var container = new Element('div', {
		  style: 'left:0px;top:0px;width:'+this.canvasWidth+'px;height:'+
		         (this.canvasHeight-this.spreadsheet.tabsContainer.getHeight()-2)+'px;overflow:auto;'
		}).addClassName('flotr-datagrid-container');
		
		container.insert(toolbar);
		t.wrap(container.hide());
		
		this.el.insert(container);
		return t;
	},	
	/**
	 * Shows the specified tab, by its name
	 * @todo make a tab manager (FlotrExtended.Tabs)
	 * @param {String} tabName - The tab name
	 */
	showTab: function(tabName){
		var selector = 'canvas, .flotr-labels, .flotr-legend, .flotr-legend-bg, .flotr-title, .flotr-subtitle';
		switch(tabName) {
			case 'graph':
				if (this.spreadsheet.datagrid)
					this.spreadsheet.datagrid.up().hide();
				this.el.select(selector).invoke('show');
				this.spreadsheet.tabs.data.removeClassName('selected');
				this.spreadsheet.tabs.graph.addClassName('selected');
			break;
			case 'data':
				this.spreadsheet.constructDataGrid();
				this.spreadsheet.datagrid.up().show();
				this.el.select(selector).invoke('hide');
				this.spreadsheet.tabs.data.addClassName('selected');
				this.spreadsheet.tabs.graph.removeClassName('selected');
			break;
		}
	},
	/**
	 * Selects the data table in the DOM for copy/paste
	 */
	selectAllData: function(){
		if (this.spreadsheet.tabs) {
			var selection, range, doc, win, node = this.spreadsheet.constructDataGrid();

			this.spreadsheet.showTab('data');
			
			// deferred to be able to select the table
			setTimeout(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();
				}
			}, 0);
			return true;
		}
		else return false;
	},
	/**
	 * Converts the data into CSV in order to download a file
	 */
	downloadCSV: function(){
		var i, csv = '',
		    series = this.series,
		    options = this.options,
		    dg = this.loadDataGrid(),
		    separator = encodeURIComponent(options.spreadsheet.csvFileSeparator);
		
		if (options.spreadsheet.decimalSeparator === options.spreadsheet.csvFileSeparator) {
			throw "The decimal separator is the same as the column separator ("+options.spreadsheet.decimalSeparator+")";
		}
		
		// The first row
		for (i = 0; i < series.length; ++i) {
			csv += separator+'"'+(series[i].label || String.fromCharCode(65+i)).replace(/\"/g, '\\"')+'"';
		}
		csv += "%0D%0A"; // \r\n
		
		// For each row
		for (i = 0; i < dg.length; ++i) {
			var rowLabel = '';
			// The first column
			if (options.xaxis.ticks) {
				var tick = options.xaxis.ticks.find(function(x){return x[0] == dg[i][0]});
				if (tick) rowLabel = tick[1];
			}
			else if (options.spreadsheet.tickFormatter){
				rowLabel = options.spreadsheet.tickFormatter(dg[i][0]);
			}
			else {
				rowLabel = options.xaxis.tickFormatter(dg[i][0]);
			}
			rowLabel = '"'+(rowLabel+'').replace(/\"/g, '\\"')+'"';
			var numbers = dg[i].slice(1).join(separator);
			if (options.spreadsheet.decimalSeparator !== '.') {
				numbers = numbers.replace(/\./g, options.spreadsheet.decimalSeparator);
			}
			csv += rowLabel+separator+numbers+"%0D%0A"; // \t and \r\n
		}
		if (Prototype.Browser.IE) {
			csv = csv.replace(new RegExp(separator, 'g'), decodeURIComponent(separator)).replace(/%0A/g, '\n').replace(/%0D/g, '\r');
			window.open().document.write(csv);
		}
		else window.open('data:text/csv,'+csv);
	}
});