Wednesday, August 17, 2011

jQuery Animated Graph using jCarousel - Part 3

Let's get down to business, creating the plugin. I'm going to assume that you have at least looked over jQuery's Plugins/Authoring documentation at this point. If you haven't, you should probably stop here and take the time to read it over.

Here's the plugin overview without any of the logic that we will be using:


(function($) {	
	var defaults = {
		columns: 6,                                 // Number of viewable thumbnails
		imgHeight: 50,                              // jCarousel slider image height
		imgWidth: 75,                               // jCarousel slider image width
		jsonScript: '',	                            // JSON return file './scripts/json/benchmark.js', 'class.somethinge.php', or anything that will return a JSON object can be used
		jsonObject: null,							// JSON Object. Overrides jsonScript
		ajaxData: {},                               // Data array to pass to the AJAX call
		cssClass: 'jcarousel-skin-benchmark',       // jCarousel css skin to use
		imgEvent: 'click',                          // 'click' or 'mouseenter'
		afterEvent: $.noop,                       	// Additional call added to the image event
		animate: true,                              // Turns off animation
		jCarouselEnabled: true,						// Use jCarousel
		onComplete: $.noop
	},
	initLoad = true, // set true for initial setup
	methods = {
		_init: function (options) {
			// Checks to see if the object already exists, much like a Singleton pattern would.
			return this.each(function () {
				// Even though we're using this.each, I'm only writing this plugin to work with an ID not a class.
				// However, you can have multiple instances of the plugin on the same page if you want.
			});
		},
		_getIntervalMax: function (max) {
			// Finds the max value that we will use on our x-axis
		},
		_drawGraph: function (data, jsonData) {
			// Draws the bar graphs when we click on a jCarousel image
		},
		_processAjax: function (data) {
			// Used to make AJAX calls to a DB or other data source.
			// The results will be returned in JSON format.
		},
		_process: function (data, jsonData) {
			// Method used to generate the HTML needed for the plugin
		},
		next: function () {
			// Exposed public method so that we can make the graph go to the next item auto-magically
		}
	}
	
	$.fn.graph = function (method) {
		if ( methods[method] ) {
			return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));
		} else if ( typeof method === 'object' || ! method ) {
			return methods._init.apply( this, arguments );
		} else {
			$.error( 'Method ' +  method + ' does not exist on jQuery.graph' );
		} 
	};
})(jQuery)


Now I'll give each method individually with comments to help understand what's going on.

_init: function (options) {
	return this.each(function () {
		// initial setup
		var $this = $(this),
			data = $this.data('graph');
		
		// if data is set, then the plugin already exists and we don't need to do this step
		if (!data) {
			// Check to see if any options were passed in and combine them with our default options
			if (options) {
				options = $.extend({}, defaults, options)
			}
			
			// setup the data object that we'll be passing around
			var data = {
					element: $this,					// The element as a jQuery object
					elementID: $this.attr('id'),	// The element ID
					options: options,				// The combined options
					total: 0						// Set to 0 because we don't know how many total items we have yet
				}
			
			// Check to see if we're using AJAX or just JSON object defined on the page
			if (options.jsonObject === null) {
				methods._processAjax(data);
			} else {
				methods._process(data, options.jsonObject);
			}
		}
	});
},


_getIntervalMax: function (max) {
	var multiplier = 1;
	
	// Hopefull we don't ever go above 999999 on our interval scale
	if (max > 100 && max < 999) {
		multiplier = 10;
	} else if (max > 1000 && max < 9999) {
		multiplier = 100;
	} else if (max > 10000 && max < 99999) {
		multiplier = 1000;
	} else if (max > 100000 && max < 999999) {
		multiplier = 10000;
	}
	
	// Switches MAX into a number < 100 and adds 10 so we don't
	// end up with over flow on the bar graphs numbers
	max = parseInt(max / multiplier, 0) + 10;
	
	// Divide by 6 until we find a number with no remainder
	// I'm using 6 because I want 6 intervals beyond 0
	while (max % 6 != 0) {
		max += 1;
	}
	
	// Multiply out our number to make it fit the scale we want and send it back.
	return max * multiplier;
},


_drawGraph: function (data, jsonData) {
	// Define everything that we're going to need to work with here
	var graph = $("#" + data.elementID + " .graphs"),
		caption = $("#" + data.elementID + " .graph_caption"),
		scaleMsg = $("#" + data.elementID + " .scaleMsg"),
		width = graph.width(),
		values = jsonData.results,
		ct = values.length,
		i,
		drawWidth = 0,
		max = Math.max.apply(null, values),
		scale = methods._getIntervalMax(max),
		temp = 0,
		scaleMax = (7 * scale) / 6,	// Fixes the scale so that calculate distances will be correct
		options = data.options;
	
	caption.find("h2").html(jsonData.title);	// Graph Title
	caption.find("h3").html(jsonData.def);		// Graph definition
	scaleMsg.html(jsonData.scaleMsg);			// Graph message
	
	// Interate through all the results
	for (i = 0; i < ct; i += 1) {
		drawWidth = Math.round( (values[i] / scaleMax) * width );	// Calculates the bar width in relation to the scale
		var bar = $("#" + data.elementID + "_graph_" + i.toString()); // Find the bar that we are going to modify

		// Are we going to animate this graph?
		if (options.animate === true) {
			// Perform animations. See jQuery API for help on .animate()
			bar.stop(true, false).animate({ width: drawWidth }, {
				duration: 500, 
				complete: function () {
					temp += 1;

					// Is this the last bar that needs to be drawn?
					if (temp === (ct)) {
						// We don't call the afterEvent on initial loads
						if (initLoad === false) {
							options.afterEvent.call();                                      
						} else {
							initLoad = false;
						}
					} 
				}
			});
		} else {
			bar.width(drawWidth); // non-animated width adjustment
		}
		
		// Display the value to the right of the bar
		$("#" + data.elementID + "_graph_" + i.toString() + "_value").text(values[i] || "[Not Tested]");
	}

	// afterEvent call when animation is off
	if (options.animate === false) {
		options.afterEvent.call();
	}
	
	// Set the scale values along the x-axis
	for (i = 6; i >= 0; i -= 1) {
		$("#" + data.elementID + "_grid_base_" + i.toString()).html(" " + (scale / 6)  * i);
	}
},


_processAjax: function (data) {
	// See the jQuery API for $.ajax() on help with this section
	var options = data.options;
	
	$.ajax({
		url: options.jsonScript,
		type: 'GET',
		data: options.ajaxData,
		dataType: 'json',
		success: function (jsonData) {
			methods._process(data, jsonData);
		},
		complete: function (jqXHR, textStatus) {
			if (options.onComplete !== $.noop) {
				options.onComplete.call();
			}
		}
	});
},


_process: function (data, jsonData) {
	var ct = jsonData.axis.length,
		total = ct,	// count of the metrics we are going to compare
		options = data.options,
		i = 0,
		html = "<div class='graph_body'>\n" + 
			"<div class='header' >\n" + 
				"<div class='graph_caption'><h2></h2><h3></h3></div>\n" +
				"<div class='scaleMsg'></div>\n" +								
			"</div>\n" + 
				"<div class='graph_content clearfix'>\n" + 
					"<div class='titles'>\n",
		html2 = "<div class='graphs'>\n",
		html3 = "";
	
	// Generate the HTML needed for each bar graph
	for (i = 0; i < ct; i += 1) {
		if (i === 0) {
			extraClass = " first_bar";
		} else if (i === (ct -1)) {
			extraClass = " last_bar";
		} else {
			extraClass = "";
		}
				
		html += "<div class='title' >" + jsonData.axis[i] + "</div>\n"
		html2 += "<div class='row'><div id='" + data.elementID + "_graph_" + i.toString() + "' class='bar" + 
			extraClass + "'></div><div id='" + data.elementID + "_graph_" + i.toString() + "_value' class='value'></div></div>\n";
	}
	
	// Generate the HTML needed for each interval on the x-axis
	for (i = 0; i < 7; i += 1) {
		html3 += "<div id='" + data.elementID + "_grid_base_" + i.toString() + "' class='axis'></div>";
	}
	
	// Close everything up
	html2 += "</div>\n";
	html += "</div>\n" + 
				html2 + 
			"<div class='spacer'></div>" + 
				html3 + 
			"</div>\n" + 
		"</div>\n";
			
	// Check to see if we're using jCarousel
	if (options.jCarouselEnabled === true) {
		ct = jsonData.metrics.length;
		html += "<ul class='metrics " + options.cssClass + "' >\n";
				
		for (i = 0; i < ct; i += 1) {
			html += "<li><img src='" + jsonData.path + jsonData.metrics[i].img + "' alt='" + jsonData.metrics[i].title + "' height='" + 
				options.imgHeight + "' width='" + options.imgWidth + "' /><p>" + jsonData.metrics[i].name + "</li>\n";
		}
				
		html += "</ul>\n";
			
		// attach the HTML to the element
		data.element.html(html);
				
		ct = 0;
		$("#" + data.elementID + " .metrics li").each( function () {
			$(this).data("test", jsonData.metrics[ct]).data("num", ct + 1);;
			
			// Setup the initial graph
			if (ct === 0) {
				methods._drawGraph(data, jsonData.metrics[ct]);
				$('#' + data.elementID + " .graph_body").data("num", ct + 1);
			}
			
			// Bind the click/mouseover event to the jCarousel item
			$(this).bind(options.imgEvent, function () {
				var me = $(this);
				$('#' + data.elementID + " .graph_body").data("num", me.data("num"));
				methods._drawGraph(data, me.data("test"));
			});
					
			ct += 1;
		});
		
		// jCarousel setup call
		$("#" + data.elementID + " .metrics").jcarousel({
			scroll: options.columns,
			vertical: options.vertical
		});
	} else {
		data.element.html(html);
		methods._drawGraph(data, jsonData.metrics[0]);
	}
	
	// set the total
	data.total = total;
	
	// Attach the data object to the element
	data.element.data('graph', data);
},


next: function () {
	// Get the data object from the element
	var data = $(this).data('graph'),
		graph = $('#' + data.elementID + " .graph_body"),
		num = graph.data("num") + 1;
	
	// Perform next iteration
	num = num <= data.total ? num : 1;
	graph.data("num", num);
	
	// Scroll the jCarousel
	$('#' + data.elementID + " .metrics").data('jcarousel').scroll($.jcarousel.intval(num));
	// Redraw the graphs
	methods._drawGraph(data, $("#" + data.elementID + " .metrics li:nth-child(" + num + ")").data("test"));
}


That should be it, you now have the fully functional plugin. The only problem is that even with the plugin setup complete, it's not going to look very good at this point. Next time we'll go over setting up the jCarousel and Graph CSS that will complete the plugin. I'll also have the completed plugin up ready for download

No comments:

Post a Comment