# Pie Chart jQuery-UI plugin

I would greatly appreciate the input of any gurus out there. I have recently begun learning JavaScript and then jQuery and jQuery-UI and have thought I would take a stab at writing my own jQuery-UI plugin, the result of which you can see here:

It's basically a pie chart into which you can drill down, rendered using HTML5 canvas. The data provided is dummy and so just oscillates between two data sets. I envisaged it displaying population of continents then by country upon drilldown etc.

What I'm after is constructive criticism. If there are any best practices I have violated or any obvious functionality I have missed then please let me know.

jQuery UI plugin:

(function($) { // Utility object with helper functions var utils = { tau: 2 * Math.PI, angleOrigin: -Math.PI / 2, sum: function(toSum) { var total = 0;$.each(toSum, function(n, value) {
total += value;
});
},
normalise: function(toNormalise) {
var total = utils.sum(toNormalise);
var toReturn = [];
$.each(toNormalise, function(n, value) { toReturn.push(utils.tau * value / total); }); return toReturn; }, distanceSqrd: function(x1, y1, x2, y2) { return Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2); }, bearing: function(x, y, originX, originY) { var toReturn = Math.atan2(x - originX, originY - y); if (toReturn < 0) { toReturn += utils.tau; } return toReturn; }, getIndex: function(bearing, dataArray) { var cumulativeAngle = 0; var toReturn = 0;$.each(dataArray, function(n, value) {
cumulativeAngle += value;
if (bearing < cumulativeAngle) {
toReturn = n;
return false;
}
});
}
};

// Object for storing drawing functionality
var renderer = {
clear: function(widget) {
widget.context.clearRect(0, 0, widget.options.width, widget.options.height);
},
drawData: function(widget, opacity) {
var startAngle = utils.angleOrigin;
$.each(widget.dataArray, function(n, value) { var colour = widget.options.colourFn(n); renderer.drawSector(widget, colour, startAngle, startAngle + value, opacity); startAngle += value; }); }, drawSector: function(widget, colour, startAngle, endAngle, opacity) { var context = widget.context; context.globalAlpha = opacity || 1; context.fillStyle = colour; context.beginPath(); context.moveTo(widget.centreX, widget.centreY); context.arc(widget.centreX, widget.centreY, widget.options.radius, startAngle, endAngle); context.lineTo(widget.centreX, widget.centreY); context.closePath(); context.fill(); context.globalAlpha = 1; }, // Based on current data, the selected segment will swallow the others then call the callback swallowOthers: function(widget, selectedIndex, callback) { var startAngle = utils.angleOrigin;$.each(widget.dataArray, function(n, value) {
if (n === selectedIndex) {
return false;
} else {
startAngle += value;
}
});
var endAngle = startAngle + widget.dataArray[selectedIndex];
var stepSize = (utils.tau - widget.dataArray[selectedIndex]) / 50;
var colour = widget.options.colourFn(selectedIndex);
var swallow = function() {
if (endAngle - startAngle < utils.tau) {
endAngle += stepSize;
renderer.drawSector(widget, colour, startAngle, endAngle);
setTimeout(swallow, 20);
} else {
callback();
}
};
setTimeout(swallow, 20);
},
// The current data will fade in from the old colour then call the callback
var opacity = 0;
opacity += 0.02;
if (opacity <= 1) {
renderer.clear(widget);
if (oldColour) {
renderer.drawSector(widget, oldColour, 0, utils.tau);
}
renderer.drawData(widget, opacity);
} else {
callback();
}
};
},
// Fade out the old data
var opacity = 1;
opacity -= 0.02;
if (opacity >= 0) {
renderer.clear(widget);
renderer.drawData(widget, opacity);
} else {
callback();
}
};
}
};

$.widget("ui.piChart", { canvas: null,$canvas: null,
context: null,
centreX: null,
centreY: null,
dataArray: null,
isAnimating: false,
hoverIndex: -1,

// These options will be used as defaults
options: {
dataProvider: null,
// Required
width: 200,
height: 200,
colourFn: function(selectedIndex) {
var defaultRainbow = ['red', 'orange', 'yellow', 'green', 'blue'];
return defaultRainbow[selectedIndex % defaultRainbow.length];
},
animationComplete: function(selectedIndex) {},
mouseMove: function(hoverIndex) {}
},

// Set up the widget
_create: function() {

// Store reference to self
var self = this;

// Create HTML5 canvas
this.canvas = $('<canvas>', { width: this.options.width, height: this.options.height }).attr('id', 'ui-piChart-canvas').appendTo(this.element[0])[0]; this.canvas.width = this.options.width; this.canvas.height = this.options.height; this.$canvas = $(this.canvas); // Other useful variables to store this.context = this.canvas.getContext('2d'); this.centreX = this.options.width / 2; this.centreY = this.options.height / 2; this.radiusSqrd = this.options.radius * this.options.radius; // Get current data this.dataArray = utils.normalise(this.options.dataProvider.getRoot()); // Initial draw of the data renderer.clear(this); renderer.drawData(this); // Click event handler this.$canvas.click(function(event) {
if (!self.isAnimating && utils.distanceSqrd(event.offsetX, event.offsetY, self.centreX, self.centreY) < self.radiusSqrd) {

// Get the selected index based on the click location
var bearing = utils.bearing(event.offsetX, event.offsetY, self.centreX, self.centreY);
var selectedIndex = utils.getIndex(bearing, self.dataArray);

// Check whether there is child data for the selected index
if (self.options.dataProvider.hasChildren(selectedIndex)) {

// Start the animation
self.isAnimating = true;

// Store the previous colour for the purposes of fade in
var oldColour = self.options.colourFn(selectedIndex);

// First swallow the other segments
renderer.swallowOthers(self, selectedIndex, function() {

// Reset the data
self.dataArray = utils.normalise(self.options.dataProvider.getChildren(selectedIndex));

// Fade in the new data

// Paint the pie chart for a final time
renderer.clear(self);
renderer.drawData(self);
self.options.animationComplete(selectedIndex);
self.isAnimating = false;
});
});
}
}
});

// Mousemove event
this.$canvas.mousemove(function(event) { if (!self.isAnimating && utils.distanceSqrd(event.offsetX, event.offsetY, self.centreX, self.centreY) < self.radiusSqrd) { // Get the selected index based on the click location var bearing = utils.bearing(event.offsetX, event.offsetY, self.centreX, self.centreY); var selectedIndex = utils.getIndex(bearing, self.dataArray); if (selectedIndex !== self.hoverIndex) { self.hoverIndex = selectedIndex; self.options.mouseMove(self.hoverIndex); } } }); }, // Use the _setOption method to respond to changes to options _setOption: function(key, value) { // Store reference to self var self = this; switch (key) { case "data": if (!self.isAnimating) { // Start the animation self.isAnimating = true; // Redraw with new data renderer.fadeOutOldData(self, function() { self.dataArray = utils.normalise(value); renderer.clear(self); renderer.fadeInNewData(self, null, function() { // Paint the pie chart for a final time renderer.clear(self); renderer.drawData(self); self.options.animationComplete(-1); self.isAnimating = false; }); }); } break; // TODO - Other options to set go here } // In jQuery UI 1.8, you have to manually invoke the _setOption method from the base widget$.Widget.prototype._setOption.apply(this, arguments);
},

// Use the destroy method to clean up any modifications your widget has made to the DOM
destroy: function() {
$(this.canvas).remove(); // In jQuery UI 1.8, you must invoke the destroy method from the base widget$.Widget.prototype.destroy.call(this);
}
});

})(jQuery);


The code being used by the client:

<script type="text/javascript">
$(function() { // Dummy data provider which switches between two datasets var dataProvider = (function() { var data1 = [1, 2, 3, 4, 25]; var data2 = [3, 3, 4, 5]; var oddCalls = false; return { getRoot: function() { oddCalls = false; return data2; }, getChildren: function(selectedIndex) {$('p#lastSelectedIndex').text('Selected index: ' + selectedIndex);
oddCalls = (oddCalls === false);
if (oddCalls) {
return data1;
} else {
return data2;
}
},
hasChildren: function(selectedIndex) {
return true;
}
};
})();

// Create the pi chart
$('#create').click(function() {$('#canvasDiv').piChart({
dataProvider: dataProvider,
mouseMove: function(hoverIndex) {
$('p#hoverIndex').text('Hover index: ' + hoverIndex); } });$('p#lastSelectedIndex').text('Root');
});

// Reset the dataset usin
$('#reset').click(function() {$('#canvasDiv').piChart('option', 'data', dataProvider.getRoot());
$('p#lastSelectedIndex').text('Root');$('p#hoverIndex').text('');
});

// Destroy the pi chart
$('#destroy').click(function() {$('#canvasDiv').piChart('destroy');
$('p#lastSelectedIndex').text('');$('p#hoverIndex').text('');
});

});​
</script>

<div id="canvasDiv"></div>

<button id="create">Create</button>

<button id="reset">Reset</button>

<button id="destroy">Destroy</button>

<p id="lastSelectedIndex"></p>

<p id="hoverIndex"></p>


Happy nit-picking!

• Come on chaps. Anyone? Even if that you would not change anything (which I doubt - it cannot be perfect!) then I would like to know. May 1, 2012 at 13:27

There is a javascript concept about one var per scope. Personally I'm not a big fan of it but you might want to read this: http://wonko.com/post/try-to-use-one-var-statement-per-scope-in-javascript