Skip to main content
Code Review

Return to Question

More tags
Link
bm1729
  • 141
  • 1
  • 5
edited tags
Link
bm1729
  • 141
  • 1
  • 5
Tweeted twitter.com/#!/StackCodeReview/status/183337390029541376
Changes made following comments
Source Link
bm1729
  • 141
  • 1
  • 5

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;
 });
 return total;
 },
 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;
 }
 });
 return toReturn;
 }
};
// 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
 fadeInNewData: function(widget, oldColour, callback) {
 var opacity = 0;
 var fadeIn = function() {
 opacity += 0.02;
 if (opacity <= 1) {
 renderer.clear(widget);
 if (oldColour) {
 renderer.drawSector(widget, oldColour, 0, utils.tau);
 }
 renderer.drawData(widget, opacity);
 setTimeout(fadeIn, 20);
 } else {
 callback();
 }
 };
 setTimeout(fadeIn, 20);
 },
 // Fade out the old data
 fadeOutOldData: function(widget, callback) {
 var opacity = 1;
 var fadeOut = function() {
 opacity -= 0.02;
 if (opacity >= 0) {
 renderer.clear(widget);
 renderer.drawData(widget, opacity);
 setTimeout(fadeOut, 20);
 } else {
 callback();
 }
 };
 setTimeout(fadeOut, 50);
 }
};
$.widget("ui.piChart", {
 canvas: null,
 $canvas: null,
 context: null,
 centreX: null,
 centreY: null,
 dataArray: null,
 isAnimating: false,
 radiusSqrd: null,
 hoverIndex: -1,
 // These options will be used as defaults
 options: {
 dataProvider: null,
 // Required
 width: 200,
 height: 200,
 radius: 80,
 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
 renderer.fadeInNewData(self, oldColour, function() {
 // 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>

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;
 });
 return total;
 },
 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;
 }
 });
 return toReturn;
 }
};
// 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
 fadeInNewData: function(widget, oldColour, callback) {
 var opacity = 0;
 var fadeIn = function() {
 opacity += 0.02;
 if (opacity <= 1) {
 renderer.clear(widget);
 if (oldColour) {
 renderer.drawSector(widget, oldColour, 0, utils.tau);
 }
 renderer.drawData(widget, opacity);
 setTimeout(fadeIn, 20);
 } else {
 callback();
 }
 };
 setTimeout(fadeIn, 20);
 },
 // Fade out the old data
 fadeOutOldData: function(widget, callback) {
 var opacity = 1;
 var fadeOut = function() {
 opacity -= 0.02;
 if (opacity >= 0) {
 renderer.clear(widget);
 renderer.drawData(widget, opacity);
 setTimeout(fadeOut, 20);
 } else {
 callback();
 }
 };
 setTimeout(fadeOut, 50);
 }
};
$.widget("ui.piChart", {
 canvas: null,
 $canvas: null,
 context: null,
 centreX: null,
 centreY: null,
 dataArray: null,
 isAnimating: false,
 radiusSqrd: null,
 hoverIndex: -1,
 // These options will be used as defaults
 options: {
 dataProvider: null,
 // Required
 width: 200,
 height: 200,
 radius: 80,
 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
 renderer.fadeInNewData(self, oldColour, function() {
 // 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>
Source Link
bm1729
  • 141
  • 1
  • 5
Loading
default

AltStyle によって変換されたページ (->オリジナル) /