( function( $ ) {
	
	$.feeder = {
		'cfg': {
			'postTemplate': {
				'source': '', // Facebook, Twitter, etc.
				'type': '', // text, image, link, video -> mostly for facebook
				'text': '', // what it sounds like
				'image': '', // url of this posts image, if it has one
				'link': '', // link this post references, if it references one
				'author': '', // who authored it 
				'source_url': '', // link to the original post
				'pretty_date': '', // a nicer way to say when this post was published
				'published_at': '' // what it sounds like
			},
			'parserConfigs': {
				'facebook': {
					'propertyMap': {
						'type': 'type',
						'text': function( data ) {
							if ( data.message ) {
								return data.message;
							} else if ( data.story ) {
								return data.story;
							} else {
								return ""
							}
						}, 
						'image': 'picture', 
						'author': 'from.name',
						'source_url': function( data ) {
							return 'http://facebook.com/' + data.from.id + '/posts/' + data.id.replace( data.from.id + '_', '' );
						},
						'pretty_date': function( data ) {
							// ugh, there has got to be a better way to do this
							var dateData = data.created_time.match(/([0-9]{4})\-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})/),
								date = new Date( Date.UTC( dateData[1], dateData[2] - 1, dateData[3], dateData[4], dateData[5], dateData[6] ) );
							return humaneDate( date );
						},
						'published_at': function( data ) {
							// seriously rediculous
							var dateData = data.created_time.match(/([0-9]{4})\-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})/),
								date = new Date( Date.UTC( dateData[1], dateData[2] - 1, dateData[3], dateData[4], dateData[5], dateData[6] ) );
							return date;
						}
					},
					'filter': function( post ) {
						// filter out wall posts that aren't from the wall owner
						if ( post.to ) {
							return post.from.id == post.to.data[0].id;
						} else {
							// if the to attribtue is not present, we assume ownership
							return true;
						}
						
					}
				},
				'twitter': {
					'propertyMap': {
						'text': 'text', 
						'author': function( post ) {
							return '@' + post.from_user;
						},
						'source_url': function( data ) {
							return 'http://twitter.com/' + data.from_user + '/status/' + data.id_str;
						},
						'pretty_date': function( data ) {
							return humaneDate( new Date( data.created_at ) );
						},
						'published_at': function( data ) {
							return new Date( data.created_at );
						}
					},
					'filter': function( post ) {
						// filter out @replies
						return !post.hasOwnProperty( 'to_user' ); 
					}
				}
			},
			'template':
				'<div class="feeder-post feeder-post-{{source}}">' + 
					'<p class="feeder-post-text">{{{text}}}</p>' + 
					'<a href="{{source_url}}" class="feeder-post-stamp" target="_blank">From {{author}} {{pretty_date}}</a>' +
				'</div>'
		},
		'fn': {
			'loadTwitterData': function( username ) {
				return $.getJSON( 'http://search.twitter.com/search.json?q=from:' + username + '&count=5&callback=?' );
			},
			'loadFacebookData': function( access_token, page_id ) {
				return $.getJSON( 'https://graph.facebook.com/' + page_id + '/feed?access_token=' + access_token + '&callback=?' );
			},
			// parses the data object with the given propertyMap and returns the results
			'parse': function( context, data, source ) {
				var post = {},
					posts = [],
					config = context.parserConfigs[source],
					propertyMap = config.propertyMap,
					filter = config.filter,
					propertyName,
					property;
				for ( var i = 0, l = data.length; i < l; i++ ) {
					if ( filter( data[i] ) ) {
						post = $.extend( {}, $.feeder.cfg.postTemplate );
						post.source = source;
						for ( propertyName in propertyMap ) {
							if( propertyMap.hasOwnProperty( propertyName ) ) {
								if ( typeof( propertyMap[propertyName] ) === "function" ) {
									// if it's a function, run it
									post[propertyName] = propertyMap[propertyName]( data[i] );
								} else if ( typeof( propertyMap[propertyName] ) === "string" ) {
									// if it's a string, split it on periods and evaluate it
									property = propertyMap[propertyName].split( '.' );
									// start with the top most object
									post[propertyName] = data[i][property.shift()];
									while( property.length > 0 ) {
										// if we have lower levels to tred, keep going
										post[propertyName] = post[propertyName][property.shift()];
									}
								}
							}
						}
						posts.push( post );
					}
				}
				return posts;
			},
			'orderPosts': function( posts, orderAttribute ) {
				return posts.sort( function( postA, postB ) { return postB[orderAttribute] - postA[orderAttribute]; } );
			},
			'renderPosts': function( $container, posts, template ) {
				var post, i, l,
					urlexp = /(\b(https?):\/\/[A-Z0-9+&@#\/%?=~_|!:,.;]*[A-Z0-9+&@#\/%=~_|])/ig;
				for( i = 0, l = posts.length; i < l; i++ ) {
					if( posts[i].text ) {
						posts[i].text = posts[i].text.replace( urlexp,'<a href="$1" target="_blank">$1</a>' );
					}
					post = Mustache.to_html( template, posts[i] );
					$container.append( post );
				}
			}
		}
	};
	
	/** 
		* feeder -- grabs updates from a public facebook wall and/or twitter and displays them on your site. 
		* 
		* @param object options An assortment of options for configuring the behavior of the plugin
		* - fb_access_token string A valid facebook access_token - more info here -> http://developers.facebook.com/docs/authentication/
		* - fb_page_id string The facebook page you want to cull data from  
		* - twitter_name string The twitter username
		* @return object A jquery object 
		* 
		*/
		
	$.fn.feeder = function( options ) {
		return $( this ).each( function() {
			var $container = $( this ),
				context = $.extend({}, $.feeder.cfg, options );
			context.$container = $container;
			context.posts = [];
			$.when( 
				$.feeder.fn.loadFacebookData( context.fb_access_token, context.fb_page_id ),
				$.feeder.fn.loadTwitterData( context.twitter_username ) )
				.then( function( facebookData, twitterData ) {
					try {
						context.posts = $.feeder.fn.parse( context, facebookData[0].data, 'facebook' );
					} catch( e1 ) { }
					try {
						context.posts = context.posts.concat( $.feeder.fn.parse( context, twitterData[0].results, 'twitter' ) );
					} catch( e2 ) { }
					context.posts = $.feeder.fn.orderPosts( context.posts, 'published_at' );
					$container.empty();
					$.feeder.fn.renderPosts( $container, context.posts.slice(0,5), context.template );
				} );
		} );
	};

	/*
	* Javascript Humane Dates
	* Copyright (c) 2008 Dean Landolt (deanlandolt.com)
	* Re-write by Zach Leatherman (zachleat.com)
	*
	* Adopted from the John Resig's pretty.js
	* at http://ejohn.org/blog/javascript-pretty-date
	* and henrah's proposed modification
	* at http://ejohn.org/blog/javascript-pretty-date/#commenhttp://ejohn.org/files/pretty.jst-297458
	*
	* Licensed under the MIT license.
	*/

	function humaneDate(date, compareTo){
			var lang = {
							ago: 'Ago',
							from: 'From Now',
							now: 'Just Now',
							minute: 'Minute',
							minutes: 'Minutes',
							hour: 'Hour',
							hours: 'Hours',
							day: 'Day',
							days: 'Days',
							week: 'Week',
							weeks: 'Weeks',
							month: 'Month',
							months: 'Months',
							year: 'Year',
							years: 'Years'
					},
					formats = [
							[60, lang.now],
							[3600, lang.minute, lang.minutes, 60], // 60 minutes, 1 minute
							[86400, lang.hour, lang.hours, 3600], // 24 hours, 1 hour
							[604800, lang.day, lang.days, 86400], // 7 days, 1 day
							[2628000, lang.week, lang.weeks, 604800], // ~1 month, 1 week
							[31536000, lang.month, lang.months, 2628000], // 1 year, ~1 month
							[Infinity, lang.year, lang.years, 31536000] // Infinity, 1 year
					],
					isString = typeof date == 'string',
					date = isString ?
											new Date(('' + date).replace(/-/g,"/").replace(/[TZ]/g," ")) :
											date,
					compareTo = compareTo || new Date(),
					seconds = (compareTo - date +
													(compareTo.getTimezoneOffset() -
															// if we received a GMT time from a string, doesn't include time zone bias
															// if we got a date object, the time zone is built in, we need to remove it.
															(isString ? 0 : date.getTimezoneOffset())
													) * 60000
											) / 1000,
					token;

			if(seconds < 0) {
					seconds = Math.abs(seconds);
					token = ' ' + lang.from;
			} else {
					token = ' ' + lang.ago;
			}

			/*
	* 0 seconds && < 60 seconds Now
	* 60 seconds 1 Minute
	* > 60 seconds && < 60 minutes X Minutes
	* 60 minutes 1 Hour
	* > 60 minutes && < 24 hours X Hours
	* 24 hours 1 Day
	* > 24 hours && < 7 days X Days
	* 7 days 1 Week
	* > 7 days && < ~ 1 Month X Weeks
	* ~ 1 Month 1 Month
	* > ~ 1 Month && < 1 Year X Months
	* 1 Year 1 Year
	* > 1 Year X Years
	*
	* Single units are +10%. 1 Year shows first at 1 Year + 10%
	*/

			function normalize(val, single)
			{
					var margin = 0.1;
					if(val >= single && val <= single * (1+margin)) {
							return single;
					}
					return val;
			}

			for(var i = 0, format = formats[0]; formats[i]; format = formats[++i]) {
					if(seconds < format[0]) {
							if(i === 0) {
									// Now
									return format[1];
							}

							var val = Math.ceil(normalize(seconds, format[3]) / (format[3]));
							return val +
											' ' +
											(val != 1 ? format[2] : format[1]) +
											(i > 0 ? token : '');
					}
			}
	};
	
}( jQuery ) );
/*! Javascript plotting library for jQuery, v. 0.7.
 *
 * Released under the MIT license by IOLA, December 2007.
 *
 */

// first an inline dependency, jquery.colorhelpers.js, we inline it here
// for convenience

/* Plugin for jQuery for working with colors.
 * 
 * Version 1.1.
 * 
 * Inspiration from jQuery color animation plugin by John Resig.
 *
 * Released under the MIT license by Ole Laursen, October 2009.
 *
 * Examples:
 *
 *   $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
 *   var c = $.color.extract($("#mydiv"), 'background-color');
 *   console.log(c.r, c.g, c.b, c.a);
 *   $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
 *
 * Note that .scale() and .add() return the same modified object
 * instead of making a new one.
 *
 * V. 1.1: Fix error handling so e.g. parsing an empty string does
 * produce a color rather than just crashing.
 */ 
(function(B){B.color={};B.color.make=function(F,E,C,D){var G={};G.r=F||0;G.g=E||0;G.b=C||0;G.a=D!=null?D:1;G.add=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]+=I}return G.normalize()};G.scale=function(J,I){for(var H=0;H<J.length;++H){G[J.charAt(H)]*=I}return G.normalize()};G.toString=function(){if(G.a>=1){return"rgb("+[G.r,G.g,G.b].join(",")+")"}else{return"rgba("+[G.r,G.g,G.b,G.a].join(",")+")"}};G.normalize=function(){function H(J,K,I){return K<J?J:(K>I?I:K)}G.r=H(0,parseInt(G.r),255);G.g=H(0,parseInt(G.g),255);G.b=H(0,parseInt(G.b),255);G.a=H(0,G.a,1);return G};G.clone=function(){return B.color.make(G.r,G.b,G.g,G.a)};return G.normalize()};B.color.extract=function(D,C){var E;do{E=D.css(C).toLowerCase();if(E!=""&&E!="transparent"){break}D=D.parent()}while(!B.nodeName(D.get(0),"body"));if(E=="rgba(0, 0, 0, 0)"){E="transparent"}return B.color.parse(E)};B.color.parse=function(F){var E,C=B.color.make;if(E=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10))}if(E=/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(F)){return C(parseInt(E[1],10),parseInt(E[2],10),parseInt(E[3],10),parseFloat(E[4]))}if(E=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55)}if(E=/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(F)){return C(parseFloat(E[1])*2.55,parseFloat(E[2])*2.55,parseFloat(E[3])*2.55,parseFloat(E[4]))}if(E=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(F)){return C(parseInt(E[1],16),parseInt(E[2],16),parseInt(E[3],16))}if(E=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(F)){return C(parseInt(E[1]+E[1],16),parseInt(E[2]+E[2],16),parseInt(E[3]+E[3],16))}var D=B.trim(F).toLowerCase();if(D=="transparent"){return C(255,255,255,0)}else{E=A[D]||[0,0,0];return C(E[0],E[1],E[2])}};var A={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]}})(jQuery);

// the actual Flot code
(function($) {
    function Plot(placeholder, data_, options_, plugins) {
        // data is on the form:
        //   [ series1, series2 ... ]
        // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
        // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
        
        var series = [],
            options = {
                // the color theme used for graphs
                colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
                legend: {
                    show: true,
                    noColumns: 1, // number of colums in legend table
                    labelFormatter: null, // fn: string -> string
                    labelBoxBorderColor: "#ccc", // border color for the little label boxes
                    container: null, // container (as jQuery object) to put legend in, null means default on top of graph
                    position: "ne", // 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
                },
                xaxis: {
                    show: null, // null = auto-detect, true = always, false = never
                    position: "bottom", // or "top"
                    mode: null, // null or "time"
                    font: null, // null (derived from CSS in placeholder) or object like { size: 11, style: "italic", weight: "bold", family: "sans-serif", variant: "small-caps" }
                    color: null, // base color, labels, ticks
                    tickColor: null, // possibly different color of ticks, e.g. "rgba(0,0,0,0.15)"
                    transform: null, // null or f: number -> number to transform axis
                    inverseTransform: null, // if transform is set, this should be the inverse function
                    min: null, // min. value to show, null means set automatically
                    max: null, // max. value to show, null means set automatically
                    autoscaleMargin: null, // margin in % to add if auto-setting min/max
                    ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
                    tickFormatter: null, // fn: number -> string
                    labelWidth: null, // size of tick labels in pixels
                    labelHeight: null,
                    reserveSpace: null, // whether to reserve space even if axis isn't shown
                    tickLength: null, // size in pixels of ticks, or "full" for whole line
                    alignTicksWithAxis: null, // axis number or null for no sync
                    
                    // mode specific options
                    tickDecimals: null, // no. of decimals, null means auto
                    tickSize: null, // number or [number, "unit"]
                    minTickSize: null, // number or [number, "unit"]
                    monthNames: null, // list of names of months
                    timeformat: null, // format string to use
                    twelveHourClock: false // 12 or 24 time in time mode
                },
                yaxis: {
                    autoscaleMargin: 0.02,
                    position: "left" // or "right"
                },
                xaxes: [],
                yaxes: [],
                series: {
                    points: {
                        show: false,
                        radius: 3,
                        lineWidth: 2, // in pixels
                        fill: true,
                        fillColor: "#ffffff",
                        symbol: "circle" // or callback
                    },
                    lines: {
                        // we don't put in show: false so we can see
                        // whether lines were actively disabled 
                        lineWidth: 2, // in pixels
                        fill: false,
                        fillColor: null,
                        steps: false
                    },
                    bars: {
                        show: false,
                        lineWidth: 2, // in pixels
                        barWidth: 1, // in units of the x axis
                        fill: true,
                        fillColor: null,
                        align: "left", // or "center" 
                        horizontal: false
                    },
                    shadowSize: 3
                },
                grid: {
                    show: true,
                    aboveData: false,
                    color: "#545454", // primary color used for outline and labels
                    backgroundColor: null, // null for transparent, else color
                    borderColor: null, // set if different from the grid color
                    tickColor: null, // color for the ticks, e.g. "rgba(0,0,0,0.15)"
                    labelMargin: 5, // in pixels
                    axisMargin: 8, // in pixels
                    borderWidth: 2, // in pixels
                    minBorderMargin: null, // in pixels, null means taken from points radius
                    markings: null, // array of ranges or fn: axes -> array of ranges
                    markingsColor: "#f4f4f4",
                    markingsLineWidth: 2,
                    // interactive stuff
                    clickable: false,
                    hoverable: false,
                    autoHighlight: true, // highlight in case mouse is near
                    mouseActiveRadius: 10 // how far the mouse can be away to activate an item
                },
                interaction: {
                    redrawOverlayInterval: 1000/60 // time between updates, -1 means in same flow
                },
                hooks: {}
            },
        canvas = null,      // the canvas for the plot itself
        overlay = null,     // canvas for interactive stuff on top of plot
        eventHolder = null, // jQuery object that events should be bound to
        ctx = null, octx = null,
        xaxes = [], yaxes = [],
        plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
        canvasWidth = 0, canvasHeight = 0,
        plotWidth = 0, plotHeight = 0,
        hooks = {
            processOptions: [],
            processRawData: [],
            processDatapoints: [],
            drawSeries: [],
            draw: [],
            bindEvents: [],
            drawOverlay: [],
            shutdown: []
        },
        plot = this;

        // public functions
        plot.setData = setData;
        plot.setupGrid = setupGrid;
        plot.draw = draw;
        plot.getPlaceholder = function() { return placeholder; };
        plot.getCanvas = function() { return canvas; };
        plot.getPlotOffset = function() { return plotOffset; };
        plot.width = function () { return plotWidth; };
        plot.height = function () { return plotHeight; };
        plot.offset = function () {
            var o = eventHolder.offset();
            o.left += plotOffset.left;
            o.top += plotOffset.top;
            return o;
        };
        plot.getData = function () { return series; };
        plot.getAxes = function () {
            var res = {}, i;
            $.each(xaxes.concat(yaxes), function (_, axis) {
                if (axis)
                    res[axis.direction + (axis.n != 1 ? axis.n : "") + "axis"] = axis;
            });
            return res;
        };
        plot.getXAxes = function () { return xaxes; };
        plot.getYAxes = function () { return yaxes; };
        plot.c2p = canvasToAxisCoords;
        plot.p2c = axisToCanvasCoords;
        plot.getOptions = function () { return options; };
        plot.highlight = highlight;
        plot.unhighlight = unhighlight;
        plot.triggerRedrawOverlay = triggerRedrawOverlay;
        plot.pointOffset = function(point) {
            return {
                left: parseInt(xaxes[axisNumber(point, "x") - 1].p2c(+point.x) + plotOffset.left),
                top: parseInt(yaxes[axisNumber(point, "y") - 1].p2c(+point.y) + plotOffset.top)
            };
        };
        plot.shutdown = shutdown;
        plot.resize = function () {
            getCanvasDimensions();
            resizeCanvas(canvas);
            resizeCanvas(overlay);
        };

        // public attributes
        plot.hooks = hooks;
        
        // initialize
        initPlugins(plot);
        parseOptions(options_);
        setupCanvases();
        setData(data_);
        setupGrid();
        draw();
        bindEvents();


        function executeHooks(hook, args) {
            args = [plot].concat(args);
            for (var i = 0; i < hook.length; ++i)
                hook[i].apply(this, args);
        }

        function initPlugins() {
            for (var i = 0; i < plugins.length; ++i) {
                var p = plugins[i];
                p.init(plot);
                if (p.options)
                    $.extend(true, options, p.options);
            }
        }
        
        function parseOptions(opts) {
            var i;
            
            $.extend(true, options, opts);
            
            if (options.xaxis.color == null)
                options.xaxis.color = options.grid.color;
            if (options.yaxis.color == null)
                options.yaxis.color = options.grid.color;
            
            if (options.xaxis.tickColor == null) // backwards-compatibility
                options.xaxis.tickColor = options.grid.tickColor;
            if (options.yaxis.tickColor == null) // backwards-compatibility
                options.yaxis.tickColor = options.grid.tickColor;

            if (options.grid.borderColor == null)
                options.grid.borderColor = options.grid.color;
            if (options.grid.tickColor == null)
                options.grid.tickColor = $.color.parse(options.grid.color).scale('a', 0.22).toString();
            
            // fill in defaults in axes, copy at least always the
            // first as the rest of the code assumes it'll be there
            for (i = 0; i < Math.max(1, options.xaxes.length); ++i)
                options.xaxes[i] = $.extend(true, {}, options.xaxis, options.xaxes[i]);
            for (i = 0; i < Math.max(1, options.yaxes.length); ++i)
                options.yaxes[i] = $.extend(true, {}, options.yaxis, options.yaxes[i]);

            // backwards compatibility, to be removed in future
            if (options.xaxis.noTicks && options.xaxis.ticks == null)
                options.xaxis.ticks = options.xaxis.noTicks;
            if (options.yaxis.noTicks && options.yaxis.ticks == null)
                options.yaxis.ticks = options.yaxis.noTicks;
            if (options.x2axis) {
                options.xaxes[1] = $.extend(true, {}, options.xaxis, options.x2axis);
                options.xaxes[1].position = "top";
            }
            if (options.y2axis) {
                options.yaxes[1] = $.extend(true, {}, options.yaxis, options.y2axis);
                options.yaxes[1].position = "right";
            }
            if (options.grid.coloredAreas)
                options.grid.markings = options.grid.coloredAreas;
            if (options.grid.coloredAreasColor)
                options.grid.markingsColor = options.grid.coloredAreasColor;
            if (options.lines)
                $.extend(true, options.series.lines, options.lines);
            if (options.points)
                $.extend(true, options.series.points, options.points);
            if (options.bars)
                $.extend(true, options.series.bars, options.bars);
            if (options.shadowSize != null)
                options.series.shadowSize = options.shadowSize;

            // save options on axes for future reference
            for (i = 0; i < options.xaxes.length; ++i)
                getOrCreateAxis(xaxes, i + 1).options = options.xaxes[i];
            for (i = 0; i < options.yaxes.length; ++i)
                getOrCreateAxis(yaxes, i + 1).options = options.yaxes[i];

            // add hooks from options
            for (var n in hooks)
                if (options.hooks[n] && options.hooks[n].length)
                    hooks[n] = hooks[n].concat(options.hooks[n]);

            executeHooks(hooks.processOptions, [options]);
        }

        function setData(d) {
            series = parseData(d);
            fillInSeriesOptions();
            processData();
        }
        
        function parseData(d) {
            var res = [];
            for (var i = 0; i < d.length; ++i) {
                var s = $.extend(true, {}, options.series);

                if (d[i].data != null) {
                    s.data = d[i].data; // move the data instead of deep-copy
                    delete d[i].data;

                    $.extend(true, s, d[i]);

                    d[i].data = s.data;
                }
                else
                    s.data = d[i];
                res.push(s);
            }

            return res;
        }
        
        function axisNumber(obj, coord) {
            var a = obj[coord + "axis"];
            if (typeof a == "object") // if we got a real axis, extract number
                a = a.n;
            if (typeof a != "number")
                a = 1; // default to first axis
            return a;
        }

        function allAxes() {
            // return flat array without annoying null entries
            return $.grep(xaxes.concat(yaxes), function (a) { return a; });
        }
        
        function canvasToAxisCoords(pos) {
            // return an object with x/y corresponding to all used axes 
            var res = {}, i, axis;
            for (i = 0; i < xaxes.length; ++i) {
                axis = xaxes[i];
                if (axis && axis.used)
                    res["x" + axis.n] = axis.c2p(pos.left);
            }

            for (i = 0; i < yaxes.length; ++i) {
                axis = yaxes[i];
                if (axis && axis.used)
                    res["y" + axis.n] = axis.c2p(pos.top);
            }
            
            if (res.x1 !== undefined)
                res.x = res.x1;
            if (res.y1 !== undefined)
                res.y = res.y1;

            return res;
        }
        
        function axisToCanvasCoords(pos) {
            // get canvas coords from the first pair of x/y found in pos
            var res = {}, i, axis, key;

            for (i = 0; i < xaxes.length; ++i) {
                axis = xaxes[i];
                if (axis && axis.used) {
                    key = "x" + axis.n;
                    if (pos[key] == null && axis.n == 1)
                        key = "x";

                    if (pos[key] != null) {
                        res.left = axis.p2c(pos[key]);
                        break;
                    }
                }
            }
            
            for (i = 0; i < yaxes.length; ++i) {
                axis = yaxes[i];
                if (axis && axis.used) {
                    key = "y" + axis.n;
                    if (pos[key] == null && axis.n == 1)
                        key = "y";

                    if (pos[key] != null) {
                        res.top = axis.p2c(pos[key]);
                        break;
                    }
                }
            }
            
            return res;
        }
        
        function getOrCreateAxis(axes, number) {
            if (!axes[number - 1])
                axes[number - 1] = {
                    n: number, // save the number for future reference
                    direction: axes == xaxes ? "x" : "y",
                    options: $.extend(true, {}, axes == xaxes ? options.xaxis : options.yaxis)
                };
                
            return axes[number - 1];
        }

        function fillInSeriesOptions() {
            var i;
            
            // collect what we already got of colors
            var neededColors = series.length,
                usedColors = [],
                assignedColors = [];
            for (i = 0; i < series.length; ++i) {
                var sc = series[i].color;
                if (sc != null) {
                    --neededColors;
                    if (typeof sc == "number")
                        assignedColors.push(sc);
                    else
                        usedColors.push($.color.parse(series[i].color));
                }
            }
            
            // we might need to generate more colors if higher indices
            // are assigned
            for (i = 0; i < assignedColors.length; ++i) {
                neededColors = Math.max(neededColors, assignedColors[i] + 1);
            }

            // produce colors as needed
            var colors = [], variation = 0;
            i = 0;
            while (colors.length < neededColors) {
                var c;
                if (options.colors.length == i) // check degenerate case
                    c = $.color.make(100, 100, 100);
                else
                    c = $.color.parse(options.colors[i]);

                // vary color if needed
                var sign = variation % 2 == 1 ? -1 : 1;
                c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2);

                // FIXME: if we're getting to close to something else,
                // we should probably skip this one
                colors.push(c);
                
                ++i;
                if (i >= options.colors.length) {
                    i = 0;
                    ++variation;
                }
            }

            // fill in the options
            var colori = 0, s;
            for (i = 0; i < series.length; ++i) {
                s = series[i];
                
                // assign colors
                if (s.color == null) {
                    s.color = colors[colori].toString();
                    ++colori;
                }
                else if (typeof s.color == "number")
                    s.color = colors[s.color].toString();

                // turn on lines automatically in case nothing is set
                if (s.lines.show == null) {
                    var v, show = true;
                    for (v in s)
                        if (s[v] && s[v].show) {
                            show = false;
                            break;
                        }
                    if (show)
                        s.lines.show = true;
                }

                // setup axes
                s.xaxis = getOrCreateAxis(xaxes, axisNumber(s, "x"));
                s.yaxis = getOrCreateAxis(yaxes, axisNumber(s, "y"));
            }
        }
        
        function processData() {
            var topSentry = Number.POSITIVE_INFINITY,
                bottomSentry = Number.NEGATIVE_INFINITY,
                fakeInfinity = Number.MAX_VALUE,
                i, j, k, m, length,
                s, points, ps, x, y, axis, val, f, p;

            function updateAxis(axis, min, max) {
                if (min < axis.datamin && min != -fakeInfinity)
                    axis.datamin = min;
                if (max > axis.datamax && max != fakeInfinity)
                    axis.datamax = max;
            }

            $.each(allAxes(), function (_, axis) {
                // init axis
                axis.datamin = topSentry;
                axis.datamax = bottomSentry;
                axis.used = false;
            });
            
            for (i = 0; i < series.length; ++i) {
                s = series[i];
                s.datapoints = { points: [] };
                
                executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
            }
            
            // first pass: clean and copy data
            for (i = 0; i < series.length; ++i) {
                s = series[i];

                var data = s.data, format = s.datapoints.format;

                if (!format) {
                    format = [];
                    // find out how to copy
                    format.push({ x: true, number: true, required: true });
                    format.push({ y: true, number: true, required: true });

                    if (s.bars.show || (s.lines.show && s.lines.fill)) {
                        format.push({ y: true, number: true, required: false, defaultValue: 0 });
                        if (s.bars.horizontal) {
                            delete format[format.length - 1].y;
                            format[format.length - 1].x = true;
                        }
                    }
                    
                    s.datapoints.format = format;
                }

                if (s.datapoints.pointsize != null)
                    continue; // already filled in

                s.datapoints.pointsize = format.length;
                
                ps = s.datapoints.pointsize;
                points = s.datapoints.points;

                insertSteps = s.lines.show && s.lines.steps;
                s.xaxis.used = s.yaxis.used = true;
                
                for (j = k = 0; j < data.length; ++j, k += ps) {
                    p = data[j];

                    var nullify = p == null;
                    if (!nullify) {
                        for (m = 0; m < ps; ++m) {
                            val = p[m];
                            f = format[m];

                            if (f) {
                                if (f.number && val != null) {
                                    val = +val; // convert to number
                                    if (isNaN(val))
                                        val = null;
                                    else if (val == Infinity)
                                        val = fakeInfinity;
                                    else if (val == -Infinity)
                                        val = -fakeInfinity;
                                }

                                if (val == null) {
                                    if (f.required)
                                        nullify = true;
                                    
                                    if (f.defaultValue != null)
                                        val = f.defaultValue;
                                }
                            }
                            
                            points[k + m] = val;
                        }
                    }
                    
                    if (nullify) {
                        for (m = 0; m < ps; ++m) {
                            val = points[k + m];
                            if (val != null) {
                                f = format[m];
                                // extract min/max info
                                if (f.x)
                                    updateAxis(s.xaxis, val, val);
                                if (f.y)
                                    updateAxis(s.yaxis, val, val);
                            }
                            points[k + m] = null;
                        }
                    }
                    else {
                        // a little bit of line specific stuff that
                        // perhaps shouldn't be here, but lacking
                        // better means...
                        if (insertSteps && k > 0
                            && points[k - ps] != null
                            && points[k - ps] != points[k]
                            && points[k - ps + 1] != points[k + 1]) {
                            // copy the point to make room for a middle point
                            for (m = 0; m < ps; ++m)
                                points[k + ps + m] = points[k + m];

                            // middle point has same y
                            points[k + 1] = points[k - ps + 1];

                            // we've added a point, better reflect that
                            k += ps;
                        }
                    }
                }
            }

            // give the hooks a chance to run
            for (i = 0; i < series.length; ++i) {
                s = series[i];
                
                executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
            }

            // second pass: find datamax/datamin for auto-scaling
            for (i = 0; i < series.length; ++i) {
                s = series[i];
                points = s.datapoints.points,
                ps = s.datapoints.pointsize;
                format = s.datapoints.format;

                var xmin = topSentry, ymin = topSentry,
                    xmax = bottomSentry, ymax = bottomSentry;
                
                for (j = 0; j < points.length; j += ps) {
                    if (points[j] == null)
                        continue;

                    for (m = 0; m < ps; ++m) {
                        val = points[j + m];
                        f = format[m];
                        if (!f || val == fakeInfinity || val == -fakeInfinity)
                            continue;
                        
                        if (f.x) {
                            if (val < xmin)
                                xmin = val;
                            if (val > xmax)
                                xmax = val;
                        }
                        if (f.y) {
                            if (val < ymin)
                                ymin = val;
                            if (val > ymax)
                                ymax = val;
                        }
                    }
                }
                
                if (s.bars.show) {
                    // make sure we got room for the bar on the dancing floor
                    var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2;
                    if (s.bars.horizontal) {
                        ymin += delta;
                        ymax += delta + s.bars.barWidth;
                    }
                    else {
                        xmin += delta;
                        xmax += delta + s.bars.barWidth;
                    }
                }
                
                updateAxis(s.xaxis, xmin, xmax);
                updateAxis(s.yaxis, ymin, ymax);
            }

            $.each(allAxes(), function (_, axis) {
                if (axis.datamin == topSentry)
                    axis.datamin = null;
                if (axis.datamax == bottomSentry)
                    axis.datamax = null;
            });
        }

        function makeCanvas(skipPositioning, cls) {
            var c = document.createElement('canvas');
            c.className = cls;
            c.width = canvasWidth;
            c.height = canvasHeight;
                    
            if (!skipPositioning)
                $(c).css({ position: 'absolute', left: 0, top: 0 });
                
            $(c).appendTo(placeholder);
                
            if (!c.getContext) // excanvas hack
                c = window.G_vmlCanvasManager.initElement(c);

            // used for resetting in case we get replotted
            c.getContext("2d").save();
            
            return c;
        }

        function getCanvasDimensions() {
            canvasWidth = placeholder.width();
            canvasHeight = placeholder.height();
            
            if (canvasWidth <= 0 || canvasHeight <= 0)
                throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
        }

        function resizeCanvas(c) {
            // resizing should reset the state (excanvas seems to be
            // buggy though)
            if (c.width != canvasWidth)
                c.width = canvasWidth;

            if (c.height != canvasHeight)
                c.height = canvasHeight;

            // so try to get back to the initial state (even if it's
            // gone now, this should be safe according to the spec)
            var cctx = c.getContext("2d");
            cctx.restore();

            // and save again
            cctx.save();
        }
        
        function setupCanvases() {
            var reused,
                existingCanvas = placeholder.children("canvas.flot-base"),
                existingOverlay = placeholder.children("canvas.flot-overlay");

            if (existingCanvas.length == 0 || existingOverlay == 0) {
                // init everything
                
                placeholder.html(""); // make sure placeholder is clear
            
                placeholder.css({ padding: 0 }); // padding messes up the positioning
                
                if (placeholder.css("position") == 'static')
                    placeholder.css("position", "relative"); // for positioning labels and overlay

                getCanvasDimensions();
                
                canvas = makeCanvas(true, "flot-base");
                overlay = makeCanvas(false, "flot-overlay"); // overlay canvas for interactive features

                reused = false;
            }
            else {
                // reuse existing elements

                canvas = existingCanvas.get(0);
                overlay = existingOverlay.get(0);

                reused = true;
            }

            ctx = canvas.getContext("2d");
            octx = overlay.getContext("2d");

            // define which element we're listening for events on
            eventHolder = $(overlay);

            if (reused) {
                // run shutdown in the old plot object
                placeholder.data("plot").shutdown();

                // reset reused canvases
                plot.resize();
                
                // make sure overlay pixels are cleared (canvas is cleared when we redraw)
                octx.clearRect(0, 0, canvasWidth, canvasHeight);
                
                // then whack any remaining obvious garbage left
                eventHolder.unbind();
                placeholder.children().not([canvas, overlay]).remove();
            }

            // save in case we get replotted
            placeholder.data("plot", plot);
        }

        function bindEvents() {
            // bind events
            if (options.grid.hoverable) {
                eventHolder.mousemove(onMouseMove);
                eventHolder.mouseleave(onMouseLeave);
            }

            if (options.grid.clickable)
                eventHolder.click(onClick);

            executeHooks(hooks.bindEvents, [eventHolder]);
        }

        function shutdown() {
            if (redrawTimeout)
                clearTimeout(redrawTimeout);
            
            eventHolder.unbind("mousemove", onMouseMove);
            eventHolder.unbind("mouseleave", onMouseLeave);
            eventHolder.unbind("click", onClick);
            
            executeHooks(hooks.shutdown, [eventHolder]);
        }

        function setTransformationHelpers(axis) {
            // set helper functions on the axis, assumes plot area
            // has been computed already
            
            function identity(x) { return x; }
            
            var s, m, t = axis.options.transform || identity,
                it = axis.options.inverseTransform;
            
            // precompute how much the axis is scaling a point
            // in canvas space
            if (axis.direction == "x") {
                s = axis.scale = plotWidth / Math.abs(t(axis.max) - t(axis.min));
                m = Math.min(t(axis.max), t(axis.min));
            }
            else {
                s = axis.scale = plotHeight / Math.abs(t(axis.max) - t(axis.min));
                s = -s;
                m = Math.max(t(axis.max), t(axis.min));
            }

            // data point to canvas coordinate
            if (t == identity) // slight optimization
                axis.p2c = function (p) { return (p - m) * s; };
            else
                axis.p2c = function (p) { return (t(p) - m) * s; };
            // canvas coordinate to data point
            if (!it)
                axis.c2p = function (c) { return m + c / s; };
            else
                axis.c2p = function (c) { return it(m + c / s); };
        }

        function measureTickLabels(axis) {
            var opts = axis.options, ticks = axis.ticks || [],
                axisw = opts.labelWidth || 0, axish = opts.labelHeight || 0,
                f = axis.font;

            ctx.save();
            ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px '" + f.family + "'";

            for (var i = 0; i < ticks.length; ++i) {
                var t = ticks[i];
                
                t.lines = [];
                t.width = t.height = 0;

                if (!t.label)
                    continue;

                // accept various kinds of newlines, including HTML ones
                // (you can actually split directly on regexps in Javascript,
                // but IE is unfortunately broken)
                var lines = t.label.replace(/<br ?\/?>|\r\n|\r/g, "\n").split("\n");
                // for (var j = 0; j < lines.length; ++j) {
                //     var line = { text: lines[j] },
                //         m = ctx.measureText(line.text);
                //     
                //     line.width = m.width;
                //     // m.height might not be defined, not in the
                //     // standard yet
                //     line.height = m.height != null ? m.height : f.size;
                // 
                //     // add a bit of margin since font rendering is
                //     // not pixel perfect and cut off letters look
                //     // bad, this also doubles as spacing between
                //     // lines
                //     line.height += Math.round(f.size * 0.15);
                // 
                //     t.width = Math.max(line.width, t.width);
                //     t.height += line.height;
                // 
                //     t.lines.push(line);
                // }

                if (opts.labelWidth == null)
                    axisw = Math.max(axisw, t.width);
                if (opts.labelHeight == null)
                    axish = Math.max(axish, t.height);
            }
            ctx.restore();

            axis.labelWidth = Math.ceil(axisw);
            axis.labelHeight = Math.ceil(axish);
        }

        function allocateAxisBoxFirstPhase(axis) {
            // find the bounding box of the axis by looking at label
            // widths/heights and ticks, make room by diminishing the
            // plotOffset; this first phase only looks at one
            // dimension per axis, the other dimension depends on the
            // other axes so will have to wait

            var lw = axis.labelWidth,
                lh = axis.labelHeight,
                pos = axis.options.position,
                tickLength = axis.options.tickLength,
                axisMargin = options.grid.axisMargin,
                padding = options.grid.labelMargin,
                all = axis.direction == "x" ? xaxes : yaxes,
                index;

            // determine axis margin
            var samePosition = $.grep(all, function (a) {
                return a && a.options.position == pos && a.reserveSpace;
            });
            if ($.inArray(axis, samePosition) == samePosition.length - 1)
                axisMargin = 0; // outermost

            // determine tick length - if we're innermost, we can use "full"
            if (tickLength == null) {
                var sameDirection = $.grep(all, function (a) {
                    return a && a.reserveSpace;
                });
                
                var innermost = $.inArray(axis, sameDirection) == 0;
                if (innermost)
                    tickLength = "full";
                else
                    tickLength = 5;
            }
            
            if (!isNaN(+tickLength))
                padding += +tickLength;

            // compute box
            if (axis.direction == "x") {
                lh += padding;
                
                if (pos == "bottom") {
                    //plotOffset.bottom += lh + axisMargin;
                    axis.box = { top: canvasHeight - plotOffset.bottom, height: lh };
                }
                else {
                    axis.box = { top: plotOffset.top + axisMargin, height: lh };
                    //plotOffset.top += lh + axisMargin;
                }
            }
            else {
                lw += padding;
                
                if (pos == "left") {
                    axis.box = { left: plotOffset.left + axisMargin, width: lw };
                    //plotOffset.left += lw + axisMargin;
                }
                else {
                    //plotOffset.right += lw + axisMargin;
                    axis.box = { left: canvasWidth - plotOffset.right, width: lw };
                }
            }

             // save for future reference
            axis.position = pos;
            axis.tickLength = tickLength;
            axis.box.padding = padding;
            axis.innermost = innermost;
        }

        function allocateAxisBoxSecondPhase(axis) {
            // now that all axis boxes have been placed in one
            // dimension, we can set the remaining dimension coordinates
            if (axis.direction == "x") {
                axis.box.left = plotOffset.left - axis.labelWidth / 2;
                axis.box.width = canvasWidth - plotOffset.left - plotOffset.right + axis.labelWidth;
            }
            else {
                axis.box.top = plotOffset.top - axis.labelHeight / 2;
                axis.box.height = canvasHeight - plotOffset.bottom - plotOffset.top + axis.labelHeight;
            }
        }

        function adjustLayoutForThingsStickingOut() {
            // possibly adjust plot offset to ensure everything stays
            // inside the canvas and isn't clipped off
            
            var minMargin = options.grid.minBorderMargin,
                margins = { x: 0, y: 0 }, i, axis;

            // check stuff from the plot (FIXME: this should just read
            // a value from the series, otherwise it's impossible to
            // customize)
            if (minMargin == null) {
                minMargin = 0;
                for (i = 0; i < series.length; ++i)
                    minMargin = Math.max(minMargin, 2 * (series[i].points.radius + series[i].points.lineWidth/2));
            }

            margins.x = margins.y = Math.ceil(minMargin);
            
            // check axis labels, note we don't check the actual
            // labels but instead use the overall width/height to not
            // jump as much around with replots
            $.each(allAxes(), function (_, axis) {
                var dir = axis.direction;
                if (axis.reserveSpace)
                    margins[dir] = Math.ceil(Math.max(margins[dir], (dir == "x" ? axis.labelWidth : axis.labelHeight) / 2));
            });
            // plotOffset.left = Math.max(margins.x, plotOffset.left);
            //             plotOffset.right = Math.max(margins.x, plotOffset.right);
            //             plotOffset.top = Math.max(margins.y, plotOffset.top);
            //             plotOffset.bottom = Math.max(margins.y, plotOffset.bottom);
        }
        
        function setupGrid() {
            var i, axes = allAxes(), showGrid = options.grid.show;

            // init plot offset
            for (var a in plotOffset)
                plotOffset[a] = showGrid ? options.grid.borderWidth : 0;
            
            // init axes
            $.each(axes, function (_, axis) {
                axis.show = axis.options.show;
                if (axis.show == null)
                    axis.show = axis.used; // by default an axis is visible if it's got data
                
                axis.reserveSpace = axis.show || axis.options.reserveSpace;

                setRange(axis);
            });
            
            if (showGrid) {
                // determine from the placeholder the font size ~ height of font ~ 1 em
                var fontDefaults = {
                    style: placeholder.css("font-style"),
                    size: Math.round(0.8 * (+placeholder.css("font-size").replace("px", "") || 13)),
                    variant: placeholder.css("font-variant"),
                    weight: placeholder.css("font-weight"),
                    family: placeholder.css("font-family")
                };

                var allocatedAxes = $.grep(axes, function (axis) { return axis.reserveSpace; });

                $.each(allocatedAxes, function (_, axis) {
                    // make the ticks
                    setupTickGeneration(axis);
                    setTicks(axis);
                    snapRangeToTicks(axis, axis.ticks);

                    // find labelWidth/Height for axis
                    axis.font = $.extend({}, fontDefaults, axis.options.font);
                    measureTickLabels(axis);
                });

                // with all dimensions calculated, we can compute the
                // axis bounding boxes, start from the outside
                // (reverse order)
                for (i = allocatedAxes.length - 1; i >= 0; --i)
                    allocateAxisBoxFirstPhase(allocatedAxes[i]);

                // make sure we've got enough space for things that
                // might stick out
                adjustLayoutForThingsStickingOut();

                $.each(allocatedAxes, function (_, axis) {
                    allocateAxisBoxSecondPhase(axis);
                });
            }
            
            plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
            plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;

            // now we got the proper plot dimensions, we can compute the scaling
            $.each(axes, function (_, axis) {
                setTransformationHelpers(axis);
            });
            
            insertLegend();
        }
        
        function setRange(axis) {
            var opts = axis.options,
                min = +(opts.min != null ? opts.min : axis.datamin),
                max = +(opts.max != null ? opts.max : axis.datamax),
                delta = max - min;

            if (delta == 0.0) {
                // degenerate case
                var widen = max == 0 ? 1 : 0.01;

                if (opts.min == null)
                    min -= widen;
                // always widen max if we couldn't widen min to ensure we
                // don't fall into min == max which doesn't work
                if (opts.max == null || opts.min != null)
                    max += widen;
            }
            else {
                // consider autoscaling
                var margin = opts.autoscaleMargin;
                if (margin != null) {
                    if (opts.min == null) {
                        min -= delta * margin;
                        // make sure we don't go below zero if all values
                        // are positive
                        if (min < 0 && axis.datamin != null && axis.datamin >= 0)
                            min = 0;
                    }
                    if (opts.max == null) {
                        max += delta * margin;
                        if (max > 0 && axis.datamax != null && axis.datamax <= 0)
                            max = 0;
                    }
                }
            }
            axis.min = min;
            axis.max = max;
        }

        function setupTickGeneration(axis) {
            var opts = axis.options;
                
            // estimate number of ticks
            var noTicks;
            if (typeof opts.ticks == "number" && opts.ticks > 0)
                noTicks = opts.ticks;
            else
                // heuristic based on the model a*sqrt(x) fitted to
                // some data points that seemed reasonable
                noTicks = 0.3 * Math.sqrt(axis.direction == "x" ? canvasWidth : canvasHeight);

            var delta = (axis.max - axis.min) / noTicks,
                size, generator, unit, formatter, i, magn, norm;

            if (opts.mode == "time") {
                // pretty handling of time
                
                // map of app. size of time units in milliseconds
                var timeUnitSize = {
                    "second": 1000,
                    "minute": 60 * 1000,
                    "hour": 60 * 60 * 1000,
                    "day": 24 * 60 * 60 * 1000,
                    "month": 30 * 24 * 60 * 60 * 1000,
                    "year": 365.2425 * 24 * 60 * 60 * 1000
                };


                // the allowed tick sizes, after 1 year we use
                // an integer algorithm
                var 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"]
                ];

                var minSize = 0;
                if (opts.minTickSize != null) {
                    if (typeof opts.tickSize == "number")
                        minSize = opts.tickSize;
                    else
                        minSize = opts.minTickSize[0] * timeUnitSize[opts.minTickSize[1]];
                }

                for (var i = 0; i < spec.length - 1; ++i)
                    if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]
                                 + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
                       && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
                        break;
                size = spec[i][0];
                unit = spec[i][1];
                
                // special-case the possibility of several years
                if (unit == "year") {
                    magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
                    norm = (delta / timeUnitSize.year) / magn;
                    if (norm < 1.5)
                        size = 1;
                    else if (norm < 3)
                        size = 2;
                    else if (norm < 7.5)
                        size = 5;
                    else
                        size = 10;

                    size *= magn;
                }

                axis.tickSize = opts.tickSize || [size, unit];
                
                generator = function(axis) {
                    var ticks = [],
                        tickSize = axis.tickSize[0], unit = axis.tickSize[1],
                        d = new Date(axis.min);
                    
                    var step = tickSize * timeUnitSize[unit];

                    if (unit == "second")
                        d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize));
                    if (unit == "minute")
                        d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize));
                    if (unit == "hour")
                        d.setUTCHours(floorInBase(d.getUTCHours(), tickSize));
                    if (unit == "month")
                        d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize));
                    if (unit == "year")
                        d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize));
                    
                    // reset smaller components
                    d.setUTCMilliseconds(0);
                    if (step >= timeUnitSize.minute)
                        d.setUTCSeconds(0);
                    if (step >= timeUnitSize.hour)
                        d.setUTCMinutes(0);
                    if (step >= timeUnitSize.day)
                        d.setUTCHours(0);
                    if (step >= timeUnitSize.day * 4)
                        d.setUTCDate(1);
                    if (step >= timeUnitSize.year)
                        d.setUTCMonth(0);


                    var carry = 0, v = Number.NaN, prev;
                    do {
                        prev = v;
                        v = d.getTime();
                        ticks.push(v);
                        if (unit == "month") {
                            if (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 * timeUnitSize.hour + (end - start) * tickSize);
                                carry = d.getUTCHours();
                                d.setUTCHours(0);
                            }
                            else
                                d.setUTCMonth(d.getUTCMonth() + tickSize);
                        }
                        else if (unit == "year") {
                            d.setUTCFullYear(d.getUTCFullYear() + tickSize);
                        }
                        else
                            d.setTime(v + step);
                    } while (v < axis.max && v != prev);

                    return ticks;
                };

                formatter = function (v, axis) {
                    var d = new Date(v);

                    // first check global format
                    if (opts.timeformat != null)
                        return $.plot.formatDate(d, opts.timeformat, opts.monthNames);
                    
                    var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
                    var span = axis.max - axis.min;
                    var suffix = (opts.twelveHourClock) ? " %p" : "";
                    
                    if (t < timeUnitSize.minute)
                        fmt = "%h:%M:%S" + suffix;
                    else if (t < timeUnitSize.day) {
                        if (span < 2 * timeUnitSize.day)
                            fmt = "%h:%M" + suffix;
                        else
                            fmt = "%b %d %h:%M" + suffix;
                    }
                    else if (t < timeUnitSize.month)
                        fmt = "%b %d";
                    else if (t < timeUnitSize.year) {
                        if (span < timeUnitSize.year)
                            fmt = "%b";
                        else
                            fmt = "%b %y";
                    }
                    else
                        fmt = "%y";
                    
                    return $.plot.formatDate(d, fmt, opts.monthNames);
                };
            }
            else {
                // pretty rounding of base-10 numbers
                var maxDec = opts.tickDecimals;
                var dec = -Math.floor(Math.log(delta) / Math.LN10);
                if (maxDec != null && dec > maxDec)
                    dec = maxDec;

                magn = Math.pow(10, -dec);
                norm = delta / magn; // norm is between 1.0 and 10.0
                
                if (norm < 1.5)
                    size = 1;
                else if (norm < 3) {
                    size = 2;
                    // special case for 2.5, requires an extra decimal
                    if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
                        size = 2.5;
                        ++dec;
                    }
                }
                else if (norm < 7.5)
                    size = 5;
                else
                    size = 10;

                size *= magn;
                
                if (opts.minTickSize != null && size < opts.minTickSize)
                    size = opts.minTickSize;

                axis.tickDecimals = Math.max(0, maxDec != null ? maxDec : dec);
                axis.tickSize = opts.tickSize || size;

                generator = function (axis) {
                    var ticks = [];

                    // spew out all possible ticks
                    var start = floorInBase(axis.min, axis.tickSize),
                        i = 0, v = Number.NaN, prev;
                    do {
                        prev = v;
                        v = start + i * axis.tickSize;
                        ticks.push(v);
                        ++i;
                    } while (v < axis.max && v != prev);
                    return ticks;
                };

                formatter = function (v, axis) {
                    return v.toFixed(axis.tickDecimals);
                };
            }

            if (opts.alignTicksWithAxis != null) {
                var otherAxis = (axis.direction == "x" ? xaxes : yaxes)[opts.alignTicksWithAxis - 1];
                if (otherAxis && otherAxis.used && otherAxis != axis) {
                    // consider snapping min/max to outermost nice ticks
                    var niceTicks = generator(axis);
                    if (niceTicks.length > 0) {
                        if (opts.min == null)
                            axis.min = Math.min(axis.min, niceTicks[0]);
                        if (opts.max == null && niceTicks.length > 1)
                            axis.max = Math.max(axis.max, niceTicks[niceTicks.length - 1]);
                    }
                    
                    generator = function (axis) {
                        // copy ticks, scaled to this axis
                        var ticks = [], v, i;
                        for (i = 0; i < otherAxis.ticks.length; ++i) {
                            v = (otherAxis.ticks[i].v - otherAxis.min) / (otherAxis.max - otherAxis.min);
                            v = axis.min + v * (axis.max - axis.min);
                            ticks.push(v);
                        }
                        return ticks;
                    };
                    
                    // we might need an extra decimal since forced
                    // ticks don't necessarily fit naturally
                    if (!axis.mode && opts.tickDecimals == null) {
                        var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1),
                            ts = generator(axis);

                        // only proceed if the tick interval rounded
                        // with an extra decimal doesn't give us a
                        // zero at end
                        if (!(ts.length > 1 && /\..*0$/.test((ts[1] - ts[0]).toFixed(extraDec))))
                            axis.tickDecimals = extraDec;
                    }
                }
            }

            axis.tickGenerator = generator;
            if ($.isFunction(opts.tickFormatter))
                axis.tickFormatter = function (v, axis) { return "" + opts.tickFormatter(v, axis); };
            else
                axis.tickFormatter = formatter;
        }
        
        function setTicks(axis) {
            var oticks = axis.options.ticks, ticks = [];
            if (oticks == null || (typeof oticks == "number" && oticks > 0))
                ticks = axis.tickGenerator(axis);
            else if (oticks) {
                if ($.isFunction(oticks))
                    // generate the ticks
                    ticks = oticks(axis);
                else
                    ticks = oticks;
            }

            // clean up/labelify the supplied ticks, copy them over
            var i, v;
            axis.ticks = [];
            for (i = 0; i < ticks.length; ++i) {
                var label = null;
                var t = ticks[i];
                if (typeof t == "object") {
                    v = +t[0];
                    if (t.length > 1)
                        label = t[1];
                }
                else
                    v = +t;
                if (label == null)
                    label = axis.tickFormatter(v, axis);
                if (!isNaN(v))
                    axis.ticks.push({ v: v, label: label });
            }
        }

        function snapRangeToTicks(axis, ticks) {
            if (axis.options.autoscaleMargin && ticks.length > 0) {
                // snap to ticks
                if (axis.options.min == null)
                    axis.min = Math.min(axis.min, ticks[0].v);
                if (axis.options.max == null && ticks.length > 1)
                    axis.max = Math.max(axis.max, ticks[ticks.length - 1].v);
            }
        }
      
        function draw() {
            ctx.clearRect(0, 0, canvasWidth, canvasHeight);

            var grid = options.grid;

            // draw background, if any
            if (grid.show && grid.backgroundColor)
                drawBackground();
            
            if (grid.show && !grid.aboveData) {
                drawGrid();
                drawAxisLabels();
            }

            for (var i = 0; i < series.length; ++i) {
                executeHooks(hooks.drawSeries, [ctx, series[i]]);
                drawSeries(series[i]);
            }

            executeHooks(hooks.draw, [ctx]);
            
            if (grid.show && grid.aboveData) {
                drawGrid();
                drawAxisLabels();
            }
        }

        function extractRange(ranges, coord) {
            var axis, from, to, key, axes = allAxes();

            for (i = 0; i < axes.length; ++i) {
                axis = axes[i];
                if (axis.direction == coord) {
                    key = coord + axis.n + "axis";
                    if (!ranges[key] && axis.n == 1)
                        key = coord + "axis"; // support x1axis as xaxis
                    if (ranges[key]) {
                        from = ranges[key].from;
                        to = ranges[key].to;
                        break;
                    }
                }
            }

            // backwards-compat stuff - to be removed in future
            if (!ranges[key]) {
                axis = coord == "x" ? xaxes[0] : yaxes[0];
                from = ranges[coord + "1"];
                to = ranges[coord + "2"];
            }

            // auto-reverse as an added bonus
            if (from != null && to != null && from > to) {
                var tmp = from;
                from = to;
                to = tmp;
            }
            
            return { from: from, to: to, axis: axis };
        }
        
        function drawBackground() {
            ctx.save();
            ctx.translate(plotOffset.left, plotOffset.top);

            ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
            ctx.fillRect(0, 0, plotWidth, plotHeight);
            ctx.restore();
        }

        function drawGrid() {
            var i;
            
            ctx.save();
            ctx.translate(plotOffset.left, plotOffset.top);
            // draw markings
            var markings = options.grid.markings;
            if (markings) {
                if ($.isFunction(markings)) {
                    var axes = plot.getAxes();
                    // xmin etc. is backwards compatibility, to be
                    // removed in the future
                    axes.xmin = axes.xaxis.min;
                    axes.xmax = axes.xaxis.max;
                    axes.ymin = axes.yaxis.min;
                    axes.ymax = axes.yaxis.max;
                    
                    markings = markings(axes);
                }

                for (i = 0; i < markings.length; ++i) {
                    var m = markings[i],
                        xrange = extractRange(m, "x"),
                        yrange = extractRange(m, "y");

                    // fill in missing
                    if (xrange.from == null)
                        xrange.from = xrange.axis.min;
                    if (xrange.to == null)
                        xrange.to = xrange.axis.max;
                    if (yrange.from == null)
                        yrange.from = yrange.axis.min;
                    if (yrange.to == null)
                        yrange.to = yrange.axis.max;

                    // clip
                    if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
                        yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
                        continue;

                    xrange.from = Math.max(xrange.from, xrange.axis.min);
                    xrange.to = Math.min(xrange.to, xrange.axis.max);
                    yrange.from = Math.max(yrange.from, yrange.axis.min);
                    yrange.to = Math.min(yrange.to, yrange.axis.max);

                    if (xrange.from == xrange.to && yrange.from == yrange.to)
                        continue;

                    // then draw
                    xrange.from = xrange.axis.p2c(xrange.from);
                    xrange.to = xrange.axis.p2c(xrange.to);
                    yrange.from = yrange.axis.p2c(yrange.from);
                    yrange.to = yrange.axis.p2c(yrange.to);
                    
                    if (xrange.from == xrange.to || yrange.from == yrange.to) {
                        // draw line
                        ctx.beginPath();
                        ctx.strokeStyle = m.color || options.grid.markingsColor;
                        ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
                        ctx.moveTo(xrange.from, yrange.from);
                        ctx.lineTo(xrange.to, yrange.to);
                        ctx.stroke();
                    }
                    else {
                        // fill area
                        ctx.fillStyle = m.color || options.grid.markingsColor;
                        ctx.fillRect(xrange.from, yrange.to,
                                     xrange.to - xrange.from,
                                     yrange.from - yrange.to);
                    }
                }
            }
            
            // draw the ticks
            var axes = allAxes(), bw = options.grid.borderWidth;

            for (var j = 0; j < axes.length; ++j) {
                var axis = axes[j], box = axis.box,
                    t = axis.tickLength, x, y, xoff, yoff;
                if (!axis.show || axis.ticks.length == 0)
                    continue;
                
                ctx.strokeStyle = axis.options.tickColor || $.color.parse(axis.options.color).scale('a', 0.22).toString();
                ctx.lineWidth = 1;

                // find the edges
                if (axis.direction == "x") {
                    x = 0;
                    if (t == "full")
                        y = (axis.position == "top" ? 0 : plotHeight);
                    else
                        y = box.top - plotOffset.top + (axis.position == "top" ? box.height : 0);
                }
                else {
                    y = 0;
                    if (t == "full")
                        x = (axis.position == "left" ? 0 : plotWidth);
                    else
                        x = box.left - plotOffset.left + (axis.position == "left" ? box.width : 0);
                }
                
                // draw tick bar
                if (!axis.innermost) {
                    ctx.beginPath();
                    xoff = yoff = 0;
                    if (axis.direction == "x")
                        xoff = plotWidth;
                    else
                        yoff = plotHeight;
                    
                    if (ctx.lineWidth == 1) {
                        x = Math.floor(x) + 0.5;
                        y = Math.floor(y) + 0.5;
                    }

                    ctx.moveTo(x, y);
                    ctx.lineTo(x + xoff, y + yoff);
                    ctx.stroke();
                }

                // draw ticks
                ctx.beginPath();
                for (i = 0; i < axis.ticks.length; ++i) {
                    var v = axis.ticks[i].v;
                    
                    xoff = yoff = 0;

                    if (v < axis.min || v > axis.max
                        // skip those lying on the axes if we got a border
                        || (t == "full" && bw > 0
                            && (v == axis.min || v == axis.max)))
                        continue;

                    if (axis.direction == "x") {
                        x = axis.p2c(v);
                        yoff = t == "full" ? -plotHeight : t;
                        
                        if (axis.position == "top")
                            yoff = -yoff;
                    }
                    else {
                        y = axis.p2c(v);
                        xoff = t == "full" ? -plotWidth : t;
                        
                        if (axis.position == "left")
                            xoff = -xoff;
                    }

                    if (ctx.lineWidth == 1) {
                        if (axis.direction == "x")
                            x = Math.floor(x) + 0.5;
                        else
                            y = Math.floor(y) + 0.5;
                    }

                    ctx.moveTo(x, y);
                    ctx.lineTo(x + xoff, y + yoff);
                }
                
                ctx.stroke();
            }
            
            
            // draw border
            if (bw) {
                ctx.lineWidth = bw;
                ctx.strokeStyle = options.grid.borderColor;
                ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
            }

            ctx.restore();
        }

        function drawAxisLabels() {
            ctx.save();

            $.each(allAxes(), function (_, axis) {
                if (!axis.show || axis.ticks.length == 0)
                    return;
                    
                var box = axis.box, f = axis.font;
                // placeholder.append('<div style="position:absolute;opacity:0.10;background-color:red;left:' + box.left + 'px;top:' + box.top + 'px;width:' + box.width +  'px;height:' + box.height + 'px"></div>') // debug

                ctx.fillStyle = axis.options.color;
                // Important: Don't use quotes around axis.font.family! Just around single 
                // font names like 'Times New Roman' that have a space or special character in it.
                ctx.font = f.style + " " + f.variant + " " + f.weight + " " + f.size + "px " + f.family;
                ctx.textAlign = "start";
                // middle align the labels - top would be more
                // natural, but browsers can differ a pixel or two in
                // where they consider the top to be, so instead we
                // middle align to minimize variation between browsers
                // and compensate when calculating the coordinates
                ctx.textBaseline = "middle";
                
                for (var i = 0; i < axis.ticks.length; ++i) {
                    var tick = axis.ticks[i];
                    if (!tick.label || tick.v < axis.min || tick.v > axis.max)
                        continue;

                    var x, y, offset = 0, line;
                    for (var k = 0; k < tick.lines.length; ++k) {
                        line = tick.lines[k];
                        
                        if (axis.direction == "x") {
                            x = plotOffset.left + axis.p2c(tick.v) - line.width/2;
                            if (axis.position == "bottom")
                                y = box.top + box.padding;
                            else
                                y = box.top + box.height - box.padding - tick.height;
                        }
                        else {
                            y = plotOffset.top + axis.p2c(tick.v) - tick.height/2;
                            if (axis.position == "left")
                                x = box.left + box.width - box.padding - line.width;
                            else
                                x = box.left + box.padding;
                        }

                        // account for middle aligning and line number
                        y += line.height/2 + offset;
                        offset += line.height;

                        if ($.browser.opera) {
                            // FIXME: UGLY BROWSER DETECTION
                            // round the coordinates since Opera
                            // otherwise switches to more ugly
                            // rendering (probably non-hinted) and
                            // offset the y coordinates since it seems
                            // to be off pretty consistently compared
                            // to the other browsers
                            x = Math.floor(x);
                            y = Math.ceil(y - 2);
                        }
												if ( 'fillText' in ctx ) {
													ctx.fillText(line.text, x, y);
												}
                    }
                }
            });

            ctx.restore();
        }

        function drawSeries(series) {
            if (series.lines.show)
                drawSeriesLines(series);
            if (series.bars.show)
                drawSeriesBars(series);
            if (series.points.show)
                drawSeriesPoints(series);
        }
        
        function drawSeriesLines(series) {
            function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
                var points = datapoints.points,
                    ps = datapoints.pointsize,
                    prevx = null, prevy = null;
                
                ctx.beginPath();
                for (var i = ps; i < points.length; i += ps) {
                    var x1 = points[i - ps], y1 = points[i - ps + 1],
                        x2 = points[i], y2 = points[i + 1];
                    
                    if (x1 == null || x2 == null)
                        continue;

                    // clip with ymin
                    if (y1 <= y2 && y1 < axisy.min) {
                        if (y2 < axisy.min)
                            continue;   // line segment is outside
                        // compute new intersection point
                        x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
                        y1 = axisy.min;
                    }
                    else if (y2 <= y1 && y2 < axisy.min) {
                        if (y1 < axisy.min)
                            continue;
                        x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
                        y2 = axisy.min;
                    }

                    // clip with ymax
                    if (y1 >= y2 && y1 > axisy.max) {
                        if (y2 > axisy.max)
                            continue;
                        x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
                        y1 = axisy.max;
                    }
                    else if (y2 >= y1 && y2 > axisy.max) {
                        if (y1 > axisy.max)
                            continue;
                        x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
                        y2 = axisy.max;
                    }

                    // clip with xmin
                    if (x1 <= x2 && x1 < axisx.min) {
                        if (x2 < axisx.min)
                            continue;
                        y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
                        x1 = axisx.min;
                    }
                    else if (x2 <= x1 && x2 < axisx.min) {
                        if (x1 < axisx.min)
                            continue;
                        y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
                        x2 = axisx.min;
                    }

                    // clip with xmax
                    if (x1 >= x2 && x1 > axisx.max) {
                        if (x2 > axisx.max)
                            continue;
                        y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
                        x1 = axisx.max;
                    }
                    else if (x2 >= x1 && x2 > axisx.max) {
                        if (x1 > axisx.max)
                            continue;
                        y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
                        x2 = axisx.max;
                    }

                    if (x1 != prevx || y1 != prevy)
                        ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
                    
                    prevx = x2;
                    prevy = y2;
                    ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
                }
                ctx.stroke();
            }

            function plotLineArea(datapoints, axisx, axisy) {
                var points = datapoints.points,
                    ps = datapoints.pointsize,
                    bottom = Math.min(Math.max(0, axisy.min), axisy.max),
                    i = 0, top, areaOpen = false,
                    ypos = 1, segmentStart = 0, segmentEnd = 0;

                // we process each segment in two turns, first forward
                // direction to sketch out top, then once we hit the
                // end we go backwards to sketch the bottom
                while (true) {
                    if (ps > 0 && i > points.length + ps)
                        break;

                    i += ps; // ps is negative if going backwards

                    var x1 = points[i - ps],
                        y1 = points[i - ps + ypos],
                        x2 = points[i], y2 = points[i + ypos];

                    if (areaOpen) {
                        if (ps > 0 && x1 != null && x2 == null) {
                            // at turning point
                            segmentEnd = i;
                            ps = -ps;
                            ypos = 2;
                            continue;
                        }

                        if (ps < 0 && i == segmentStart + ps) {
                            // done with the reverse sweep
                            ctx.fill();
                            areaOpen = false;
                            ps = -ps;
                            ypos = 1;
                            i = segmentStart = segmentEnd + ps;
                            continue;
                        }
                    }

                    if (x1 == null || x2 == null)
                        continue;

                    // clip x values
                    
                    // clip with xmin
                    if (x1 <= x2 && x1 < axisx.min) {
                        if (x2 < axisx.min)
                            continue;
                        y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
                        x1 = axisx.min;
                    }
                    else if (x2 <= x1 && x2 < axisx.min) {
                        if (x1 < axisx.min)
                            continue;
                        y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
                        x2 = axisx.min;
                    }

                    // clip with xmax
                    if (x1 >= x2 && x1 > axisx.max) {
                        if (x2 > axisx.max)
                            continue;
                        y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
                        x1 = axisx.max;
                    }
                    else if (x2 >= x1 && x2 > axisx.max) {
                        if (x1 > axisx.max)
                            continue;
                        y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
                        x2 = axisx.max;
                    }

                    if (!areaOpen) {
                        // open area
                        ctx.beginPath();
                        ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
                        areaOpen = true;
                    }
                    
                    // now first check the case where both is outside
                    if (y1 >= axisy.max && y2 >= axisy.max) {
                        ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
                        continue;
                    }
                    else if (y1 <= axisy.min && y2 <= axisy.min) {
                        ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
                        continue;
                    }
                    
                    // else it's a bit more complicated, there might
                    // be a flat maxed out rectangle first, then a
                    // triangular cutout or reverse; to find these
                    // keep track of the current x values
                    var x1old = x1, x2old = x2;

                    // clip the y values, without shortcutting, we
                    // go through all cases in turn
                    
                    // clip with ymin
                    if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
                        x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
                        y1 = axisy.min;
                    }
                    else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
                        x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
                        y2 = axisy.min;
                    }

                    // clip with ymax
                    if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
                        x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
                        y1 = axisy.max;
                    }
                    else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
                        x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
                        y2 = axisy.max;
                    }

                    // if the x value was changed we got a rectangle
                    // to fill
                    if (x1 != x1old) {
                        ctx.lineTo(axisx.p2c(x1old), axisy.p2c(y1));
                        // it goes to (x1, y1), but we fill that below
                    }
                    
                    // fill triangular section, this sometimes result
                    // in redundant points if (x1, y1) hasn't changed
                    // from previous line to, but we just ignore that
                    ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
                    ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));

                    // fill the other rectangle if it's there
                    if (x2 != x2old) {
                        ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
                        ctx.lineTo(axisx.p2c(x2old), axisy.p2c(y2));
                    }
                }
            }

            ctx.save();
            ctx.translate(plotOffset.left, plotOffset.top);
            ctx.lineJoin = "round";

            var lw = series.lines.lineWidth,
                sw = series.shadowSize;
            // FIXME: consider another form of shadow when filling is turned on
            if (lw > 0 && sw > 0) {
                // draw shadow as a thick and thin line with transparency
                ctx.lineWidth = sw;
                ctx.strokeStyle = "rgba(0,0,0,0.1)";
                // position shadow at angle from the mid of line
                var angle = Math.PI/18;
                plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
                ctx.lineWidth = sw/2;
                plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
            }

            ctx.lineWidth = lw;
            ctx.strokeStyle = series.color;
            var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
            if (fillStyle) {
                ctx.fillStyle = fillStyle;
                plotLineArea(series.datapoints, series.xaxis, series.yaxis);
            }

            if (lw > 0)
                plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
            ctx.restore();
        }

        function drawSeriesPoints(series) {
            function plotPoints(datapoints, radius, fillStyle, offset, shadow, axisx, axisy, symbol) {
                var points = datapoints.points, ps = datapoints.pointsize;

                for (var i = 0; i < points.length; i += ps) {
                    var x = points[i], y = points[i + 1];
                    if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
                        continue;
                    
                    ctx.beginPath();
                    x = axisx.p2c(x);
                    y = axisy.p2c(y) + offset;
                    if (symbol == "circle")
                        ctx.arc(x, y, radius, 0, shadow ? Math.PI : Math.PI * 2, false);
                    else
                        symbol(ctx, x, y, radius, shadow);
                    ctx.closePath();
                    
                    if (fillStyle) {
                        ctx.fillStyle = fillStyle;
                        ctx.fill();
                    }
                    ctx.stroke();
                }
            }
            
            ctx.save();
            ctx.translate(plotOffset.left, plotOffset.top);

            var lw = series.points.lineWidth,
                sw = series.shadowSize,
                radius = series.points.radius,
                symbol = series.points.symbol;
            if (lw > 0 && sw > 0) {
                // draw shadow in two steps
                var w = sw / 2;
                ctx.lineWidth = w;
                ctx.strokeStyle = "rgba(0,0,0,0.1)";
                plotPoints(series.datapoints, radius, null, w + w/2, true,
                           series.xaxis, series.yaxis, symbol);

                ctx.strokeStyle = "rgba(0,0,0,0.2)";
                plotPoints(series.datapoints, radius, null, w/2, true,
                           series.xaxis, series.yaxis, symbol);
            }

            ctx.lineWidth = lw;
            ctx.strokeStyle = series.color;
            plotPoints(series.datapoints, radius,
                       getFillStyle(series.points, series.color), 0, false,
                       series.xaxis, series.yaxis, symbol);
            ctx.restore();
        }

        function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal, lineWidth) {
            var left, right, bottom, top,
                drawLeft, drawRight, drawTop, drawBottom,
                tmp;

            // in horizontal mode, we start the bar from the left
            // instead of from the bottom so it appears to be
            // horizontal rather than vertical
            if (horizontal) {
                drawBottom = drawRight = drawTop = true;
                drawLeft = false;
                left = b;
                right = x;
                top = y + barLeft;
                bottom = y + barRight;

                // account for negative bars
                if (right < left) {
                    tmp = right;
                    right = left;
                    left = tmp;
                    drawLeft = true;
                    drawRight = false;
                }
            }
            else {
                drawLeft = drawRight = drawTop = true;
                drawBottom = false;
                left = x + barLeft;
                right = x + barRight;
                bottom = b;
                top = y;

                // account for negative bars
                if (top < bottom) {
                    tmp = top;
                    top = bottom;
                    bottom = tmp;
                    drawBottom = true;
                    drawTop = false;
                }
            }
           
            // clip
            if (right < axisx.min || left > axisx.max ||
                top < axisy.min || bottom > axisy.max)
                return;
            
            if (left < axisx.min) {
                left = axisx.min;
                drawLeft = false;
            }

            if (right > axisx.max) {
                right = axisx.max;
                drawRight = false;
            }

            if (bottom < axisy.min) {
                bottom = axisy.min;
                drawBottom = false;
            }
            
            if (top > axisy.max) {
                top = axisy.max;
                drawTop = false;
            }

            left = axisx.p2c(left);
            bottom = axisy.p2c(bottom);
            right = axisx.p2c(right);
            top = axisy.p2c(top);
            
            // fill the bar
            if (fillStyleCallback) {
                c.beginPath();
                c.moveTo(left, bottom);
                c.lineTo(left, top);
                c.lineTo(right, top);
                c.lineTo(right, bottom);
                c.fillStyle = fillStyleCallback(bottom, top);
                c.fill();
            }

            // draw outline
            if (lineWidth > 0 && (drawLeft || drawRight || drawTop || drawBottom)) {
                c.beginPath();

                // FIXME: inline moveTo is buggy with excanvas
                c.moveTo(left, bottom + offset);
                if (drawLeft)
                    c.lineTo(left, top + offset);
                else
                    c.moveTo(left, top + offset);
                if (drawTop)
                    c.lineTo(right, top + offset);
                else
                    c.moveTo(right, top + offset);
                if (drawRight)
                    c.lineTo(right, bottom + offset);
                else
                    c.moveTo(right, bottom + offset);
                if (drawBottom)
                    c.lineTo(left, bottom + offset);
                else
                    c.moveTo(left, bottom + offset);
                c.stroke();
            }
        }
        
        function drawSeriesBars(series) {
            function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
                var points = datapoints.points, ps = datapoints.pointsize;
                
                for (var i = 0; i < points.length; i += ps) {
                    if (points[i] == null)
                        continue;
                    drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal, series.bars.lineWidth);
                }
            }

            ctx.save();
            ctx.translate(plotOffset.left, plotOffset.top);

            // FIXME: figure out a way to add shadows (for instance along the right edge)
            ctx.lineWidth = series.bars.lineWidth;
            ctx.strokeStyle = series.color;
            var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
            var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
            plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
            ctx.restore();
        }

        function getFillStyle(filloptions, seriesColor, bottom, top) {
            var fill = filloptions.fill;
            if (!fill)
                return null;

            if (filloptions.fillColor)
                return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
            
            var c = $.color.parse(seriesColor);
            c.a = typeof fill == "number" ? fill : 0.4;
            c.normalize();
            return c.toString();
        }
        
        function insertLegend() {
            placeholder.find(".legend").remove();

            if (!options.legend.show)
                return;
            
            var fragments = [], rowStarted = false,
                lf = options.legend.labelFormatter, s, label;
            for (var i = 0; i < series.length; ++i) {
                s = series[i];
                label = s.label;
                if (!label)
                    continue;
                
                if (i % options.legend.noColumns == 0) {
                    if (rowStarted)
                        fragments.push('</tr>');
                    fragments.push('<tr>');
                    rowStarted = true;
                }

                if (lf)
                    label = lf(label, s);
                
                fragments.push(
                    '<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + s.color + ';overflow:hidden"></div></div></td>' +
                    '<td class="legendLabel">' + label + '</td>');
            }
            if (rowStarted)
                fragments.push('</tr>');
            
            if (fragments.length == 0)
                return;

            var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
            if (options.legend.container != null)
                $(options.legend.container).html(table);
            else {
                var pos = "",
                    p = options.legend.position,
                    m = options.legend.margin;
                if (m[0] == null)
                    m = [m, m];
                if (p.charAt(0) == "n")
                    pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
                else if (p.charAt(0) == "s")
                    pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
                if (p.charAt(1) == "e")
                    pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
                else if (p.charAt(1) == "w")
                    pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
                var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
                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) {
                        c = options.grid.backgroundColor;
                        if (c && typeof c == "string")
                            c = $.color.parse(c);
                        else
                            c = $.color.extract(legend, 'background-color');
                        c.a = 1;
                        c = c.toString();
                    }
                    var div = legend.children();
                    $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
                }
            }
        }


        // interactive features
        
        var highlights = [],
            redrawTimeout = null;
        
        // returns the data item the mouse is over, or null if none is found
        function findNearbyItem(mouseX, mouseY, seriesFilter) {
            var maxDistance = options.grid.mouseActiveRadius,
                smallestDistance = maxDistance * maxDistance + 1,
                item = null, foundPoint = false, i, j;

            for (i = series.length - 1; i >= 0; --i) {
                if (!seriesFilter(series[i]))
                    continue;
                
                var s = series[i],
                    axisx = s.xaxis,
                    axisy = s.yaxis,
                    points = s.datapoints.points,
                    ps = s.datapoints.pointsize,
                    mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
                    my = axisy.c2p(mouseY),
                    maxx = maxDistance / axisx.scale,
                    maxy = maxDistance / axisy.scale;

                // with inverse transforms, we can't use the maxx/maxy
                // optimization, sadly
                if (axisx.options.inverseTransform)
                    maxx = Number.MAX_VALUE;
                if (axisy.options.inverseTransform)
                    maxy = Number.MAX_VALUE;
                
                if (s.lines.show || s.points.show) {
                    for (j = 0; j < points.length; j += ps) {
                        var x = points[j], y = points[j + 1];
                        if (x == null)
                            continue;
                        
                        // For points and lines, the cursor must be within a
                        // certain distance to the data point
                        if (x - mx > maxx || x - mx < -maxx ||
                            y - my > maxy || y - my < -maxy)
                            continue;

                        // We have to calculate distances in pixels, not in
                        // data units, because the scales of the axes may be different
                        var dx = Math.abs(axisx.p2c(x) - mouseX),
                            dy = Math.abs(axisy.p2c(y) - mouseY),
                            dist = dx * dx + dy * dy; // we save the sqrt

                        // use <= to ensure last point takes precedence
                        // (last generally means on top of)
                        if (dist < smallestDistance) {
                            smallestDistance = dist;
                            item = [i, j / ps];
                        }
                    }
                }
                    
                if (s.bars.show && !item) { // no other point can be nearby
                    var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
                        barRight = barLeft + s.bars.barWidth;
                    
                    for (j = 0; j < points.length; j += ps) {
                        var x = points[j], y = points[j + 1], b = points[j + 2];
                        if (x == null)
                            continue;
  
                        // for a bar graph, the cursor must be inside the bar
                        if (series[i].bars.horizontal ? 
                            (mx <= Math.max(b, x) && mx >= Math.min(b, x) && 
                             my >= y + barLeft && my <= y + barRight) :
                            (mx >= x + barLeft && mx <= x + barRight &&
                             my >= Math.min(b, y) && my <= Math.max(b, y)))
                                item = [i, j / ps];
                    }
                }
            }

            if (item) {
                i = item[0];
                j = item[1];
                ps = series[i].datapoints.pointsize;
                
                return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
                         dataIndex: j,
                         series: series[i],
                         seriesIndex: i };
            }
            
            return null;
        }

        function onMouseMove(e) {
            if (options.grid.hoverable)
                triggerClickHoverEvent("plothover", e,
                                       function (s) { return s["hoverable"] != false; });
        }

        function onMouseLeave(e) {
            if (options.grid.hoverable)
                triggerClickHoverEvent("plothover", e,
                                       function (s) { return false; });
        }

        function onClick(e) {
            triggerClickHoverEvent("plotclick", e,
                                   function (s) { return s["clickable"] != false; });
        }

        // trigger click or hover event (they send the same parameters
        // so we share their code)
        function triggerClickHoverEvent(eventname, event, seriesFilter) {
            var offset = eventHolder.offset(),
                canvasX = event.pageX - offset.left - plotOffset.left,
                canvasY = event.pageY - offset.top - plotOffset.top,
            pos = canvasToAxisCoords({ left: canvasX, top: canvasY });

            pos.pageX = event.pageX;
            pos.pageY = event.pageY;

            var item = findNearbyItem(canvasX, canvasY, seriesFilter);

            if (item) {
                // fill in mouse pos for any listeners out there
                item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left);
                item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top);
            }

            if (options.grid.autoHighlight) {
                // clear auto-highlights
                for (var i = 0; i < highlights.length; ++i) {
                    var h = highlights[i];
                    if (h.auto == eventname &&
                        !(item && h.series == item.series &&
                          h.point[0] == item.datapoint[0] &&
                          h.point[1] == item.datapoint[1]))
                        unhighlight(h.series, h.point);
                }
                
                if (item)
                    highlight(item.series, item.datapoint, eventname);
            }
            
            placeholder.trigger(eventname, [ pos, item ]);
        }

        function triggerRedrawOverlay() {
            var t = options.interaction.redrawOverlayInterval;
            if (t == -1) {      // skip event queue
                drawOverlay();
                return;
            }
            
            if (!redrawTimeout)
                redrawTimeout = setTimeout(drawOverlay, t);
        }

        function drawOverlay() {
            redrawTimeout = null;

            // draw highlights
            octx.save();
            octx.clearRect(0, 0, canvasWidth, canvasHeight);
            octx.translate(plotOffset.left, plotOffset.top);
            
            var i, hi;
            for (i = 0; i < highlights.length; ++i) {
                hi = highlights[i];

                if (hi.series.bars.show)
                    drawBarHighlight(hi.series, hi.point);
                else
                    drawPointHighlight(hi.series, hi.point);
            }
            octx.restore();
            
            executeHooks(hooks.drawOverlay, [octx]);
        }
        
        function highlight(s, point, auto) {
            if (typeof s == "number")
                s = series[s];

            if (typeof point == "number") {
                var ps = s.datapoints.pointsize;
                point = s.datapoints.points.slice(ps * point, ps * (point + 1));
            }

            var i = indexOfHighlight(s, point);
            if (i == -1) {
                highlights.push({ series: s, point: point, auto: auto });

                triggerRedrawOverlay();
            }
            else if (!auto)
                highlights[i].auto = false;
        }
            
        function unhighlight(s, point) {
            if (s == null && point == null) {
                highlights = [];
                triggerRedrawOverlay();
            }
            
            if (typeof s == "number")
                s = series[s];

            if (typeof point == "number")
                point = s.data[point];

            var i = indexOfHighlight(s, point);
            if (i != -1) {
                highlights.splice(i, 1);

                triggerRedrawOverlay();
            }
        }
        
        function indexOfHighlight(s, p) {
            for (var i = 0; i < highlights.length; ++i) {
                var h = highlights[i];
                if (h.series == s && h.point[0] == p[0]
                    && h.point[1] == p[1])
                    return i;
            }
            return -1;
        }
        
        function drawPointHighlight(series, point) {
            var x = point[0], y = point[1],
                axisx = series.xaxis, axisy = series.yaxis;
            
            if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
                return;
            
            var pointRadius = series.points.radius + series.points.lineWidth / 2;
            octx.lineWidth = pointRadius;
            octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
            var radius = 1.5 * pointRadius,
                x = axisx.p2c(x),
                y = axisy.p2c(y);
            
            octx.beginPath();
            if (series.points.symbol == "circle")
                octx.arc(x, y, radius, 0, 2 * Math.PI, false);
            else
                series.points.symbol(octx, x, y, radius, false);
            octx.closePath();
            octx.stroke();
        }

        function drawBarHighlight(series, point) {
            octx.lineWidth = series.bars.lineWidth;
            octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
            var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString();
            var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
            drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
                    0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal, series.bars.lineWidth);
        }

        function getColorOrGradient(spec, bottom, top, defaultColor) {
            if (typeof spec == "string")
                return spec;
            else {
                // assume this is a gradient spec; IE currently only
                // supports a simple vertical gradient properly, so that's
                // what we support too
                var gradient = ctx.createLinearGradient(0, top, 0, bottom);
                
                for (var i = 0, l = spec.colors.length; i < l; ++i) {
                    var c = spec.colors[i];
                    if (typeof c != "string") {
                        var co = $.color.parse(defaultColor);
                        if (c.brightness != null)
                            co = co.scale('rgb', c.brightness);
                        if (c.opacity != null)
                            co.a *= c.opacity;
                        c = co.toString();
                    }
                    gradient.addColorStop(i / (l - 1), c);
                }
                
                return gradient;
            }
        }
    }

    $.plot = function(placeholder, data, options) {
        //var t0 = new Date();
        var plot = new Plot($(placeholder), data, options, $.plot.plugins);
        //(window.console ? console.log : alert)("time used (msecs): " + ((new Date()).getTime() - t0.getTime()));
        return plot;
    };

    $.plot.version = "0.7";
    
    $.plot.plugins = [];

    // returns a string with the date d formatted according to fmt
    $.plot.formatDate = function(d, fmt, monthNames) {
        var leftPad = function(n) {
            n = "" + n;
            return n.length == 1 ? "0" + n : n;
        };
        
        var r = [];
        var escape = false, padNext = false;
        var hours = d.getUTCHours();
        var isAM = hours < 12;
        if (monthNames == null)
            monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

        if (fmt.search(/%p|%P/) != -1) {
            if (hours > 12) {
                hours = hours - 12;
            } else if (hours == 0) {
                hours = 12;
            }
        }
        for (var i = 0; i < fmt.length; ++i) {
            var c = fmt.charAt(i);
            
            if (escape) {
                switch (c) {
                case 'h': c = "" + hours; break;
                case 'H': c = leftPad(hours); break;
                case 'M': c = leftPad(d.getUTCMinutes()); break;
                case 'S': c = leftPad(d.getUTCSeconds()); break;
                case 'd': c = "" + d.getUTCDate(); break;
                case 'm': c = "" + (d.getUTCMonth() + 1); break;
                case 'y': c = "" + d.getUTCFullYear(); break;
                case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
                case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
                case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
                case '0': c = ""; padNext = true; break;
                }
                if (c && padNext) {
                    c = leftPad(c);
                    padNext = false;
                }
                r.push(c);
                if (!padNext)
                    escape = false;
            }
            else {
                if (c == "%")
                    escape = true;
                else
                    r.push(c);
            }
        }
        return r.join("");
    };
    
    // round to nearby lower multiple of base
    function floorInBase(n, base) {
        return base * Math.floor(n / base);
    }
    
})(jQuery);
( function( $ ) {
	
	$.fn.geLargeline = function( data, color ) {
		return $( this ).each( function() {
			var $graph = $( this ),
				paths = [
					{ 'data': data,
						'color': color,
						'shadowSize': 0,
						'lines': {
							'lineWidth': 3
						}
					},
					{ 'data': [data[data.length - 1]],
						'lines': {
							'show': false
						},
						'points': {
							'show': true,
							'fill': true,
							'fillColor': color,
							'radius': 4
						},
						'color': color
					}],
				options = {
					'xaxis': { 
						'show': false,
						'mode': 'time',
						'min': data[0][0],
						'max': data[data.length-1][0] + ( ( data[data.length-1][0] - data[0][0] ) / data.length ),
						'reserveSpace': false,
						'labelWidth': 0,
						'labelHeight': 0
					},
					'yaxis': {
						'show': true,
						'color': '#ffffff',
						'tickColor': '#cccccc',
						'ticks': 6,
						'reserveSpace': false,
						'labelWidth': 0,
						'labelHeight': 0
					},
					'grid': {
						'borderWidth': 0,
						'labelMargin': 0
					},
					series: {
						lines: { show: true },
						points: { show: false }
					}
				};
			// Just graph it
			$.plot( '#' + $graph.attr( 'id' ), paths, options );
		} );
	};
	
	/* 
	 * geSparkline -- wrapper for rendering a simple flot sparkline
	 * 
	 * @param array data An array of data used to plot the line graph. 
	 * @param string color A css compatible color (rgb, rgba, or hex) to use for coloring the line
	 * @return object A jquery object 
	 * 
	 */
	$.fn.geSparkline = function( data, color ) {
		// make sure we only take the last seven points of the data
		data = data.slice( Math.max( 0, data.length - 7 ) );
		return $( this ).each( function() {
			var $graph = $( this ),
				paths = [
					{ 'data': data,
						'color': color,
						'shadowSize': 3,
						'lines': {
							'show': true
						}
					},
					{ 'data': [data[0]],
						'lines': {
							'show': false
						},
						'points': {
							'show': true,
							'fill': true,
							'fillColor': color,
							'radius': 2
						},
						'color': color
				}],
				options = {
					'xaxis': { 
						'show': false,
						'autoscaleMargin': 0.1,
						'reserveSpace': false,
						'labelWidth': 0,
						'labelHeight': 0
					},
					'yaxis': {
						'show': true,
						'color': '#ffffff',
						'tickColor': '#cccccc',
						'ticks': 6,
						'reserveSpace': false,
						'labelWidth': 0,
						'labelHeight': 0
					},
					'grid': {
						'borderColor': '#cccccc',
						'borderWidth': 1
					},
					series: {
						lines: { show: true },
						points: { show: false }
					}
				};
				
			// Just graph it
			$.plot( '#' + $graph.attr( 'id' ), paths, options );
		} );
	};
	
	/* 
	 * geImageGraph -- wrapper for rendering a flot graph with images as the dots. 
	 * Delays graph rendering until the image is loaded, to ensure it can be drawn to the canvas
	 * without errors. 
	 * 
	 * @param array data An array of data used to plot the line graph. 
	 * @param string color A css compatible color (rgb, rgba, or hex) to use for coloring the line
	 * @param string image_path A path to an image file which should be used for the points
	 * @param string arrow_path A path to an arrow image file to render the dY
	 * @param string arrow_text A string to render below or above the arrow 
	 * @param object userOptions Optional parameters. imageLabel, and arrowAngle.
	 * @return object A jquery object 
	 * 
	 */
	$.fn.geImageGraph = function( data, color, image_path, callback ) {
		// make sure we only take the last three points of the data
		data = data.slice( data.length - 3 );
		// make sure we handle groups, and always return a jquery object to support chaining
		return $( this ).each( function() {
			var $graph = $( this ),
				points = [],
				img = new Image(),
				loadImage,
				$imageLabel = $( '<div class="image-graph-label"></div>' ),
				$arrow = $( '<div class="image-graph-arrow"></div>' ),
				lastPoint = data[ data.length - 1 ],
				flot,
				ctx,
				paths = [{
					'data': data,
					'color': 'rgba(193,193,193,0.33)',
					'shadowSize': 0,
					'lines': {
						'lineWidth': 4
					},
					'points': {
						'show': true,
						'symbol': function( ctx, x, y, radius, shadow ) {
							var height = img.height,
								width = img.width,
								onLastPoint = ( ctx.canvas.width - x < width + 40 ),
								arrowTrend;
								
							// add the point to our graph's data
							points.push( [x, y] );
							// if this isn't the last point in our data set, scale the image down, and fade it
							if ( !onLastPoint ) {
								width = width * 0.4;
								height = height * 0.4;
								ctx.globalAlpha = 0.5;
							}
							x = Math.min( x - ( width / 2 ), ctx.canvas.width - width );
							x = Math.max( x, 0 );
							y = Math.min( ( 195 - ( height ) ),   y - ( height / 2 ) );
							y = Math.max( y, 0 );
							ctx.drawImage( img, Math.floor( x ), Math.floor( y ), Math.floor( width ), Math.floor( height ) );
							if ( onLastPoint ) {
								arrowTrend = points[1][1] > points[2][1] ? 1 : -1;
								// add our arrow div
								$graph.prepend( $arrow
									.css( {
										'position': 'absolute',
										'top': points[1][1] > ( ctx.canvas.height / 2 ) ? 10 : points[1][1] + ( 20 * arrowTrend ),
										'left': points[1][0] + 15
									} ) );
								// add the image label div over our last image
								$graph.prepend( $imageLabel
									.css( {
										'position': 'absolute',
										'top': y,
										'left': x,
										'width': width,
										'height': height,
										'line-height': ( height - 7 ) + 'px'
									} ) );
							}
						ctx.globalAlpha = 1;
						}
					}
					}],
				options = {
					'xaxis': {
						'show': false,
						'autoscaleMargin': 0.15
					},
					'yaxis': {
						'autoscaleMargin': 0.3,
						'show': true,
						'color': '#ffffff',
						'tickColor': '#cccccc',
						'ticks': 6,
						'reserveSpace': false,
						'labelWidth': 0,
						'labelHeight': 0
					},
					'grid': {
						'borderWidth': 0,
						'labelMargin': 0
					},
					'series': {
						'lines': { 'show': true },
						'points': { 'show': false }
					}
				};
			loadImage = function() {
				var dfd = $.Deferred();
				img.onload = function() {
					dfd.resolve();
				};
				// start loading the image
				img.src = image_path;
				// return the deferred promise
				return dfd.promise();
			};
			$.when( loadImage() )
				.then( function() {
					// Once the image is loaded, render the graph
					flot = $.plot( '#' + $graph.attr( 'id' ), paths, options );
					// run our callback if we've got it
					if( typeof( callback ) === 'function' ) callback();
				} );
		} );
	};
	
}( jQuery ) );
( function( $ ) {
	
	/**
	* fitText is a jquery plugin that will scale the font size of an element until the text
	* fits within the specified width
	* @method fitText
	* @param integer width The target width
	*/
	$.fn.fitText = function( width ) {
		return $( this ).each( function() {
			var $oText = $( this ),
				oFontSize = $oText.css( 'fontSize' ),
				$text = $( this ).wrapInner( '<span></span>' ).find( 'span' ),
				textWidth;
			$text.css( {
				'fontSize': 'inherit',
				'whiteSpace': 'nowrap'
			} );
			textWidth = parseInt( $text.width(), 10 );
			while ( textWidth > width ) {
				$text.css( 'fontSize', parseInt( $text.css( 'fontSize' ), 10 ) - 1 );
				textWidth = parseInt( $text.width(), 10 );
			}
			$oText
				.css( {
					'fontSize': parseInt( $text.css( 'fontSize' ), 10 ),
					'lineHeight': parseInt( $text.css( 'fontSize' ), 10 ) + 3 + 'px'
				} )
				.text( $text.text() );
		} );
	};
	
}( jQuery ) );
( function( $ ) {
	
	/**
	* evenOutWith is a jquery plug that will attempt to align the bottom of two elements with each other.
	* It was built with a specific case in mind and has not be thoroughly tested. 
	* @method evenOutWith 
	* @param string or object target The target element we're hoping to match. Can be a selector or a jquery object.
	* 
	*/
	$.fn.evenOutWith = function( target ) {
		var $target, 
			$this,
			targetBase,
			thisBase,
			paddignDelta;
		return $( this ).each( function() {
			$this = $( this );
			if ( typeof( target ) === "string" ) {
				$target = $( target );
			} else {
				$target = target;
			}
			targetBase = $target.height() + $target.offset().top;
			thisBase = $this.height() + $this.offset().top;
			paddingDelta = ( parseInt( $this.css( 'paddingTop' ), 10 ) + parseInt( $this.css( 'paddingBottom' ), 10 ) ) -
				( parseInt( $target.css( 'paddingTop' ), 10 ) + parseInt( $target.css( 'paddingBottom' ), 10 ) );
			if ( targetBase > thisBase ) {
				$this.height( targetBase - $this.offset().top - paddingDelta );
			} else {
				$target.height( thisBase - $target.offset().top + paddingDelta );
			}
		} );
	};
	
}( jQuery ) );
/*
  mustache.js — Logic-less templates in JavaScript

  See http://mustache.github.com/ for more info.
*/

var Mustache = function() {
  var Renderer = function() {};

  Renderer.prototype = {
    otag: "{{",
    ctag: "}}",
    pragmas: {},
    buffer: [],
    pragmas_implemented: {
      "IMPLICIT-ITERATOR": true
    },
    context: {},

    render: function(template, context, partials, in_recursion) {
      // reset buffer & set context
      if(!in_recursion) {
        this.context = context;
        this.buffer = []; // TODO: make this non-lazy
      }

      // fail fast
      if(!this.includes("", template)) {
        if(in_recursion) {
          return template;
        } else {
          this.send(template);
          return;
        }
      }

      template = this.render_pragmas(template);
      var html = this.render_section(template, context, partials);
      if(in_recursion) {
        return this.render_tags(html, context, partials, in_recursion);
      }

      this.render_tags(html, context, partials, in_recursion);
    },

    /*
      Sends parsed lines
    */
    send: function(line) {
      if(line !== "") {
        this.buffer.push(line);
      }
    },

    /*
      Looks for %PRAGMAS
    */
    render_pragmas: function(template) {
      // no pragmas
      if(!this.includes("%", template)) {
        return template;
      }

      var that = this;
      var regex = new RegExp(this.otag + "%([\\w-]+) ?([\\w]+=[\\w]+)?" +
            this.ctag, "g");
      return template.replace(regex, function(match, pragma, options) {
        if(!that.pragmas_implemented[pragma]) {
          throw({message: 
            "This implementation of mustache doesn't understand the '" +
            pragma + "' pragma"});
        }
        that.pragmas[pragma] = {};
        if(options) {
          var opts = options.split("=");
          that.pragmas[pragma][opts[0]] = opts[1];
        }
        return "";
        // ignore unknown pragmas silently
      });
    },

    /*
      Tries to find a partial in the curent scope and render it
    */
    render_partial: function(name, context, partials) {
      name = this.trim(name);
      if(!partials || partials[name] === undefined) {
        throw({message: "unknown_partial '" + name + "'"});
      }
      if(typeof(context[name]) != "object") {
        return this.render(partials[name], context, partials, true);
      }
      return this.render(partials[name], context[name], partials, true);
    },

    /*
      Renders inverted (^) and normal (#) sections
    */
    render_section: function(template, context, partials) {
      if(!this.includes("#", template) && !this.includes("^", template)) {
        return template;
      }

      var that = this;
      // CSW - Added "+?" so it finds the tighest bound, not the widest
      var regex = new RegExp(this.otag + "(\\^|\\#)\\s*(.+)\\s*" + this.ctag +
              "\n*([\\s\\S]+?)" + this.otag + "\\/\\s*\\2\\s*" + this.ctag +
              "\\s*", "mg");

      // for each {{#foo}}{{/foo}} section do...
      return template.replace(regex, function(match, type, name, content) {
        var value = that.find(name, context);
        if(type == "^") { // inverted section
          if(!value || that.is_array(value) && value.length === 0) {
            // false or empty list, render it
            return that.render(content, context, partials, true);
          } else {
            return "";
          }
        } else if(type == "#") { // normal section
          if(that.is_array(value)) { // Enumerable, Let's loop!
            return that.map(value, function(row) {
              return that.render(content, that.create_context(row),
                partials, true);
            }).join("");
          } else if(that.is_object(value)) { // Object, Use it as subcontext!
            return that.render(content, that.create_context(value),
              partials, true);
          } else if(typeof value === "function") {
            // higher order section
            return value.call(context, content, function(text) {
              return that.render(text, context, partials, true);
            });
          } else if(value) { // boolean section
            return that.render(content, context, partials, true);
          } else {
            return "";
          }
        }
      });
    },

    /*
      Replace {{foo}} and friends with values from our view
    */
    render_tags: function(template, context, partials, in_recursion) {
      // tit for tat
      var that = this;

      var new_regex = function() {
        return new RegExp(that.otag + "(=|!|>|\\{|%)?([^\\/#\\^]+?)\\1?" +
          that.ctag + "+", "g");
      };

      var regex = new_regex();
      var tag_replace_callback = function(match, operator, name) {
        switch(operator) {
        case "!": // ignore comments
          return "";
        case "=": // set new delimiters, rebuild the replace regexp
          that.set_delimiters(name);
          regex = new_regex();
          return "";
        case ">": // render partial
          return that.render_partial(name, context, partials);
        case "{": // the triple mustache is unescaped
          return that.find(name, context);
        default: // escape the value
          return that.escape(that.find(name, context));
        }
      };
      var lines = template.split("\n");
      for(var i = 0; i < lines.length; i++) {
        lines[i] = lines[i].replace(regex, tag_replace_callback, this);
        if(!in_recursion) {
          this.send(lines[i]);
        }
      }

      if(in_recursion) {
        return lines.join("\n");
      }
    },

    set_delimiters: function(delimiters) {
      var dels = delimiters.split(" ");
      this.otag = this.escape_regex(dels[0]);
      this.ctag = this.escape_regex(dels[1]);
    },

    escape_regex: function(text) {
      // thank you Simon Willison
      if(!arguments.callee.sRE) {
        var specials = [
          '/', '.', '*', '+', '?', '|',
          '(', ')', '[', ']', '{', '}', '\\'
        ];
        arguments.callee.sRE = new RegExp(
          '(\\' + specials.join('|\\') + ')', 'g'
        );
      }
      return text.replace(arguments.callee.sRE, '\\$1');
    },

    /*
      find `name` in current `context`. That is find me a value
      from the view object
    */
    find: function(name, context) {
      name = this.trim(name);

      // Checks whether a value is thruthy or false or 0
      function is_kinda_truthy(bool) {
        return bool === false || bool === 0 || bool;
      }

      var value;
      if(is_kinda_truthy(context[name])) {
        value = context[name];
      } else if(is_kinda_truthy(this.context[name])) {
        value = this.context[name];
      }

      if(typeof value === "function") {
        return value.apply(context);
      }
      if(value !== undefined) {
        return value;
      }
      // silently ignore unkown variables
      return "";
    },

    // Utility methods

    /* includes tag */
    includes: function(needle, haystack) {
      return haystack.indexOf(this.otag + needle) != -1;
    },

    /*
      Does away with nasty characters
    */
    escape: function(s) {
      s = String(s === null ? "" : s);
      return s.replace(/&(?!\w+;)|["'<>\\]/g, function(s) {
        switch(s) {
        case "&": return "&amp;";
        case "\\": return "\\\\";
        case '"': return '&quot;';
        case "'": return '&#39;';
        case "<": return "&lt;";
        case ">": return "&gt;";
        default: return s;
        }
      });
    },

    // by @langalex, support for arrays of strings
    create_context: function(_context) {
      if(this.is_object(_context)) {
        return _context;
      } else {
        var iterator = ".";
        if(this.pragmas["IMPLICIT-ITERATOR"]) {
          iterator = this.pragmas["IMPLICIT-ITERATOR"].iterator;
        }
        var ctx = {};
        ctx[iterator] = _context;
        return ctx;
      }
    },

    is_object: function(a) {
      return a && typeof a == "object";
    },

    is_array: function(a) {
      return Object.prototype.toString.call(a) === '[object Array]';
    },

    /*
      Gets rid of leading and trailing whitespace
    */
    trim: function(s) {
      return s.replace(/^\s*|\s*$/g, "");
    },

    /*
      Why, why, why? Because IE. Cry, cry cry.
    */
    map: function(array, fn) {
      if (typeof array.map == "function") {
        return array.map(fn);
      } else {
        var r = [];
        var l = array.length;
        for(var i = 0; i < l; i++) {
          r.push(fn(array[i]));
        }
        return r;
      }
    }
  };

  return({
    name: "mustache.js",
    version: "0.3.1-dev",

    /*
      Turns a template and view into HTML
    */
    to_html: function(template, view, partials, send_fun) {
      var renderer = new Renderer();
      if(send_fun) {
        renderer.send = send_fun;
      }
      renderer.render(template, view, partials);
      if(!send_fun) {
        return renderer.buffer.join("\n");
      }
    }
  });
}();

/** 
	* GE - Healthymagination.com Homepage Module
	* Handles the low level, homepage-specific javascript. 
	* 
	* Renders three graphs on the homepage, using medhelp data
	* Renders a fancy morsel counter, and allows users to say they completed today's morsel
	* Loads facebook and twitter feeds
	*
	*/ 
( function( ge, $ ) {
	
	// if modules is already setup, use it. Otherwise we start with an empty object
	ge.modules = ge.modules || {};
	
	// add our code to the ge.modules namespace
	// hp is declared as a local variable so we can reference it inside of the module code
	var hp = ge.modules.homepage = {
		'cfg': {
			'fontsReady': false,
			'domReady': false,
			'graphDataURL': '',
			'assetBase': ''
		},
		'evt': {
			'fontsReady': function() {
				hp.cfg.fontsReady = true;
				$( '.image-graph-label' ).fitText( 52 );
			},
			'ready': function() {
				hp.cfg.domReady = true;
				hp.fn.styleNumbers();
				hp.fn.initMorsel();
				hp.fn.setupSocial();
				hp.cfg.graphDataURL = $( '#app-graphs' ).attr( 'rel' );
				hp.cfg.assetBase = $( '#app-graphs' ).attr( 'data-asset-base' );
				hp.fn.initGraphs();
				hp.fn.wyomingTicker();
				hp.fn.jawboneWidgets();
				$( '#hero .challenge-content-inner' ).evenOutWith( '#wyoming' );
			}
		},
		'fn': {
			'initGraphs': function() {
				// load the data
				if ( $( '#app-graphs' ).size() > 0 ) {
					$.when( hp.fn.loadGraphData() )
						.then( hp.fn.renderGraphs );
				}
			},
			'loadGraphData': function() {
				// add activity indicators to each of the graphs
				$( 'div.graph-block' ).each( function() {
					$( this )
						.append( '<span class="ge-loader"></span>' );
				} );
				return $.ajax( {
					'url': hp.cfg.graphDataURL,
					'dataType': 'JSON'
					} );
			},
			'renderGraphs': function( data ) {
				var calorieData = [],
					sleepData = [],
					moodData = [],
					calorieLabel = '',
					calorieTrend,
					sleepTrend,
					moodTrend,
					sleepLabel = '',
					moodLabel = '',
					dataConvert = function dataConvert( data ) {
						var returnData = [],
							date,
							time;
						for( date in data ) {
							if( data.hasOwnProperty( date ) ) {
								// special case for sleep data 
								time = (new Date( date.substr( 0, 4 ), Number( date.substr( 5, 2 ) ) - 1, date.substr( 8, 2 ) ) ).getTime();
								if ( typeof( data[date] ) === 'object' ) {
									returnData.push( [time, data[date].average_sleep_length] );
								} else {
									returnData.push( [time, data[date]] );
								}
								
							}
						}
						returnData.sort( function( a, b ) { return a[0] - b[0]; } );
						return returnData;
					};
				
				calorieData = dataConvert( data.calories_burned );
				sleepData = dataConvert( data.sleep );
				moodData = dataConvert( data.mood );
				
				calorieLabel = (calorieData[calorieData.length - 1][1]).toFixed(0) + ' calories'; 
				moodLabel = ["Depressed", "Horrible", "Bad", "Okay", "Good", "Excellent", "Manic"][Math.max( Math.min( Math.round( moodData[moodData.length - 1][1] ), 6 ), 0 )];
				calorieTrend = calorieData[calorieData.length - 1][1] < calorieData[calorieData.length - 2][1];
				sleepTrend = sleepData[sleepData.length - 1][1] < sleepData[sleepData.length - 2][1];
				moodTrend = moodData[moodData.length - 1][1] < moodData[moodData.length - 2][1];
				
				// update the h4's
				$( '#calories-graph-container h4' )
					.text( $( '#calories-graph-container h4' ).text() + ' ' + 
						( calorieData[calorieData.length - 1][1] ).toFixed(0) + ' calories.' );
				$( '#sleep-graph-container h4' )
					.text( $( '#sleep-graph-container h4' ).text() + ' ' + 
						( sleepData[sleepData.length - 1][1] ).toFixed(1) + ' hours.');
				$( '#mood-graph-container h4' ).text( $( '#mood-graph-container h4' ).text() + ' ' + moodLabel.toLowerCase() + '.' );
				
				// render sparklines
				$( '#calories-sparkline' )
					.empty()
					.geSparkline( calorieData, '#f96042' );
				$( '#sleep-sparkline' )
					.empty()
					.geSparkline( sleepData, '#00c2f8' );
				$( '#mood-sparkline' )
					.empty()
					.geSparkline( moodData, '#89c557' );
				// render big graphics
				$( '#calories-graph' )
					.empty()
					.geImageGraph( 
						calorieData, '#f96042', 
						hp.cfg.assetBase + 'images/homepage_v3/runner.png', function() {
							$( '#calories-graph' )
								.find( 'div.image-graph-arrow' )
									.addClass( 'ge_font' )
									.text( calorieLabel );
							if ( calorieTrend ) {
								$( '#calories-graph' )
									.find( 'div.image-graph-arrow' )
										.addClass( 'image-graph-arrow-down' );
							}
						} );
				$( '#sleep-graph' )
					.empty()
					.geImageGraph( 
						sleepData, '#00c2f8', 
						hp.cfg.assetBase + 'images/homepage_v3/clock.png', function() {
							$( '#sleep-graph' )
								.find( 'div.image-graph-arrow' )
									.addClass( 'ge_font' )
									.text( (sleepData[sleepData.length - 1][1]).toFixed(1) + ' hours' );
							if ( sleepTrend ) {
								$( '#sleep-graph' )
									.find( 'div.image-graph-arrow' )
										.addClass( 'image-graph-arrow-down' );
							}
						} );
				$( '#mood-graph' )
					.empty()
					.geImageGraph( 
						moodData, '#89c557', 
						hp.cfg.assetBase + 'images/homepage_v3/moody.png', function() {
							$( '#mood-graph' )
								.find( 'div.image-graph-label' )
									.addClass( 'ge_font' )
									.text( moodLabel.toUpperCase() );
							$( '#mood-graph' )
								.find( 'div.image-graph-arrow' )
									.addClass( 'ge_font' )
									.text( ( moodTrend ? 'DOWN ' : 'UP ' ) + 
										( ( Math.abs( moodData[moodData.length - 1][1] - moodData[moodData.length - 2][1] ) /  moodData[moodData.length - 2][1] ) * 100 ).toFixed(1) + '%' );
									if ( moodTrend ) {
										$( '#mood-graph' )
											.find( 'div.image-graph-arrow' )
												.addClass( 'image-graph-arrow-down' );
									}
						} );
				// bind click handlers
				$( '#calories-sparkline, #expand-calories-graph' )
					.unbind( 'click.ge' )
					.bind( 'click.ge', function( e ) {
						e.preventDefault();
						var h3Text = $( '#calories-graph-container h3' ).text(),
							h4Text = $( '#calories-graph-container h4' ).text();
						hp.fn.openLargeGraph( calorieData, '#f96042', h3Text, h4Text );
					} );
				$( '#sleep-sparkline, #expand-sleep-graph' )
					.unbind( 'click.ge' )
					.bind( 'click.ge', function( e ) {
						e.preventDefault();
						var h3Text = $( '#sleep-graph-container h3' ).text(),
							h4Text = $( '#sleep-graph-container h4' ).text();
						hp.fn.openLargeGraph( sleepData, '#00c2f8', h3Text, h4Text );
					} );
				$( '#mood-sparkline, #expand-mood-graph' )
					.unbind( 'click.ge' )
					.bind( 'click.ge', function( e ) {
						e.preventDefault();
						var h3Text = $( '#mood-graph-container h3' ).text(),
							h4Text = $( '#mood-graph-container h4' ).text();
						hp.fn.openLargeGraph( moodData, '#89c557', h3Text, h4Text );
					} );
				// remove loaders
				$( '#app-graphs span.ge-loader' )
					.fadeOut( 'fast', function() { $( this ).remove(); } );
			},
			openLargeGraph: function( data, color, h3Text, h4Text ) {
				var template = '<div class="large-graph" id="large-graph">' + 
						'<h3>My Diet Diary</h3>'+
						'<h4>Today, the average app user has burned</h4>' +
						'<div class="largeline" id="largeline"></div>' +
						'<span class="left-graph-label"></span>' + 
						'<span class="right-graph-label"></span>' +
						'<a href="#" class="ge_font close-btn">CLOSE</a>' + 
					'</div>',
					$container = $( template ),
					dateMin = new Date( data[0][0] ),
					dateMax = new Date( data[data.length -1][0] ),
					monthAbrevs = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
				
				// style the h3 -- look at all this chaining! gross!
				$container
					.find( 'h3' )
						.css( 'color', color )
						.text( h3Text )
					.end()
					.find( 'h4' )
						.text( h4Text )
					.end()
					// handle closing
					.find( 'a.close-btn' )
						.bind( 'click.ge', function( e ) {
							e.preventDefault();
							$( '#app-graphs div.large-graph' )
								.fadeOut( 'fast', function() {
									// for some reason, IE throws an error with .remove()
									$( '#app-graphs div.large-graph' ).detach();
									$( '#graph-container' ).fadeIn( 'fast' );
								} );
						} )
						.end()
						.find( 'span.left-graph-label' )
							.text( monthAbrevs[dateMin.getMonth()] + " " + dateMin.getDate() )
						.end()
						.find( 'span.right-graph-label' )
							.text( monthAbrevs[dateMax.getMonth()] + " " + dateMax.getDate() )
						.end()
					.hide();
				// add the element
				$( '#graph-container' )
					.fadeOut( 'fast', function() {
						$( '#graph-container' )
							.before( $container );
						$container.fadeIn( 'fast', function() {
							$( this )
								.find( 'div.largeline' )
								.geLargeline( data, color );
						} );
					} );
			},
			'styleNumbers': function() {
				$( '.morsel_digit' ).each( function() {
					var $string = $( this ), 
						text = $string.text();
					$string.empty();
					for( var i = 0, l = text.length; i < l; i++ ) {
						$string.append( '<span class="morsel-digit ge_font">' + text.substr( i, 1 ) + '<span class="morsel-digit-glass-light"></span></span>' );
					}
				} );
			},
			'initMorsel': function() {
				// show loader
				if( $( '#morsel-container' ).size() > 0 ) {
						$( '#morsel-container' )
							.append( '<span class="ge-loader"></span>' );
						$.getJSON( 
							ge.fn.staticURL( '/lib/proxy.php?file=http://healthymagination.com/morsel/api/action/daily' ), 
							hp.fn.renderMorsel );
				}
			},
			'renderMorsel': function( data ) {
				var action = data.result.action,
					morselsCompleted = ge.fn.getData( 'morselsCompleted' ) || {},
					$completedButton = $( '#morsel-completed-btn' );
				// hide and remove the loader
				$( '#morsel-container' )
					.find( '.ge-loader' )
					.fadeOut( 'fast', function() { $( this ).remove(); } );
				// update the morsel text
				$( '#morsel-title' ).text( action.title );
				$( '#morsel-description' ).text( action.description );
				$( '#morsel-completed-count' )
					.text( action.totalCompleted )
					.each( function() {
						// stylize the count
						var $string = $( this ), 
							text = $string.text();
						$string.empty();
						for( var i = 0, l = text.length; i < l; i++ ) {
							$string.append( '<span class="morsel-digit ge_font">' + text.substr( i, 1 ) + '<span class="morsel-digit-glass-light"></span></span>' );
						}
					} );
				// bind the click handler to the compelted morsel button, unless they've already compelted it
				if ( action.id in morselsCompleted ) {
					$completedButton.addClass( 'disabled' );
				} else {
					$completedButton.bind( 'click.ge.homepage', function( e ) {
						e.preventDefault();
						hp.fn.completeMorsel( action.action_id );
					} );
				}
			},
			'completeMorsel': function( action_id ) {
				var morselsCompleted = ge.fn.getData( 'morselsCompleted' ) || {},
					$completedButton = $( '#morsel-completed-btn' );
				// if the button is disabled, GTFO
				if( $completedButton.is( '.disabled' ) ) return false;
				// show the loader
				$completedButton
					.css( 'position', 'relative' )
					.append( '<span class="ge-loader"></span>' );
				$.ajax( {
					'url': ge.fn.staticURL( '/lib/proxy.php' ), 
					'data': {
						'file': 'http://healthymagination.com/morsel/api/action/complete?action_id=' + action_id
					},
					'dataType': 'JSON',
					'success': function( data, status, xhr ) {
						var action = data.result.detail;
						// hide the loader
						$completedButton
							.find( '.ge-loader' )
							.fadeOut( 'fast', function() { $( this ).remove(); } );
						// disable the button
						$completedButton
							.addClass( 'disabled' );
						// update the morselsCopleted
						$( '#morsel-completed-count' )
							.text( action.totalCompleted )
							.each( function() {
								// stylize the count
								var $string = $( this ), 
									text = $string.text();
								$string.empty();
								for( var i = 0, l = text.length; i < l; i++ ) {
									$string.append( '<span class="morsel-digit ge_font">' + text.substr( i, 1 ) + '<span class="morsel-digit-glass-light"></span></span>' );
								}
							} );
						morselsCompleted[String( action.action_id )] = ( new Date() ).getTime();
						ge.fn.storeData( 'morselsCompleted', morselsCompleted );
					}, 
					'error': function( a, b, c ) {
						// RECOVER!
					}
				} );
			},
			'setupSocial': function() {
				$( '.social-feed' ).feeder( {
					'fb_access_token': '114129828692597|9WleoTB2WBtWU9NdxoPlP7X5_xo',
					'fb_page_id': 'healthymagination',
					'twitter_username': 'gehealthy'
				} );
			},
			'wyomingTicker': function() {
				// Script for retrieving current facebook counter information from WY Women First App
				var counterUrl = "http://www.gedigitalmedia.com/facebook/wywomenfirst/js/ajax.count.php";

				$.ajax({
					url: counterUrl,
					cache: false,
					dataType: "jsonp",
					success: function(data) {
						var count = data[0].total.toString();
						var maxDigits = 5;
						while(count.length < maxDigits) {
							count = "0" + count;
						}
						for(i = 0; i < count.length; i++) {
							$( '.mammogram-counter ul' ).append( '<li>' + count.charAt(i) + '</li>');
						}
					}
				});
			},
			'jawboneWidgets': function() {
				var $widgets = $( '.widget' ),
					$sleep = $widgets.eq( 0 ),
					$walk = $widgets.eq( 1 ),
					$run = $widgets.eq( 2 ),
					populateWidgets;
				
				populateWidgets = function ( data ) {
					
					var sleepPer,
						walkPer,
						walkPos;
					
					// update sleep text w/ our # of years
					$sleep.find( 'p span').text( data.sleep );
					// get the percentage of our total
					sleepPer = data.sleep / 20;
					if ( sleepPer > 1 ){
						sleepPer = 1;
					}
					// set the width of the sheep jumping image to be that percentage of its parent element.
					$sleep.find( '.sheep' ).width( $sleep.find( '.visualization' ).width() * sleepPer );
					
					// update walk text w/ our # of miles
					$walk.find( 'p span').text( data.walk );
					// get the percentage of our total and divide by -230 to get position to set bg imge
					walkPer =  data.walk / 24900;
					if ( walkPer > 1 ){
						walkPer = 1;
					}
					walkPos = ( 1 - walkPer ) * -230;
					// set the bg position of feet image to be that percentage of its parent element.
					$walk.find( '.miles' ).css('backgroundPosition', walkPos + 'px 0');
					
					// update walk text w/ our name, # of miles and img src
					$run.find( '.steps span').text( data['run-steps'].toFixed(1) );
				};
				
				// this is stubbed data, to be replaced w/ something doug sets up
				$.getJSON('/jawboneData.json', function(json, textStatus) {
					populateWidgets( json );
				});

			}
		}
	};
	
	ge.fn.loadModule( 'homepage' );
	
}( GE || {}, jQuery ) );

