Take the 2-minute tour ×
Code Review Stack Exchange is a question and answer site for peer programmer code reviews. It's 100% free, no registration required.

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:

http://jsfiddle.net/ben1729/djA6G/ .

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;
        });
        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>

Happy nit-picking!

share|improve this question
    
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. –  ben1729 May 1 '12 at 13:27

1 Answer 1

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

share|improve this answer

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.