Javascript Tutorial

Create a Bar Chart Plug-in for jQuery

In this tutorial, you’ll learn how to create a bar chart plug-in for jQuery. You’ll start by creating a simple plug-in and then move on to the more complex logic. This tutorial is suitable for people with no experience with jQuery as well as those with minimal Javascript experience. More advanced readers will probably want to skim over the minutiae or skip this tutorial entirely and head right to the repository.

First Steps: a Simple jQuery plug-in

Before we dive into the complexities of bar charts, let’s create an extremely simple plug-in as gentle introduction to jQuery plug-in development. If your jQuery plug-in fu is already strong, you should probably skip right to the second half of this tutorial.

Let’s create a Black Box plug-in that can redact elements on the page. This won’t be CIA-quality security however, it will just hide the text and turn the elements into black boxes.

First, we’ll create an HTML page that loads jQuery and contains some text to be redacted. Google hosts the latest versions of some of the more popular Javascript libraries on their network, so we’ll take advantage of that. If you’re working offline, you’ll want to download jQuery and link to your local copy. We’ll also add a script tag for our code into the header. Enter this code and save it to a file called blackbox.html (or a name of your choosing):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
    <title>My Black Box Plug-In</title>
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
    <script type="text/javascript">
        
        // Our code will go here
        
    </script>
</head>
<body>
    <h1>Top Secret!</h1>
    <p>
        Lorem ipsum dolor sit amet, <span class="redacted">consectetur adipisicing</span> elit,
        sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
        <span class="redacted">Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
        nisi ut aliquip ex</span> ea commodo consequat.
    </p>
    <div class="redacted">Secret Section</div>
    <p>
        Duis aute irure dolor in reprehenderit in voluptate velit esse
        cillum dolore eu <em class="redacted">fugiat nulla pariatur</em>.
        Excepteur sint occaecat cupidatat non proident, sunt in culpa qui
        officia <cite class="redacted">deserunt mollit</cite> anim id est laborum.
    </p>
</body>
</html>

Open it up in your browser and you should see something like this:

Hello, world screenshot

That’s good and all but this isn’t an HTML tutorial so let’s roll up our sleeves and get into some Javascript.

There are two ways to extend jQuery with a plug-in. The plug-in can add functionality to the global jQuery object, like $.ajax() or $.json(), or the plug-in can extend jQuery elements, like $('#mydiv').doStuff().

For this tutorial we will be using the latter technique, extending jQuery elements with simple, black-box redaction functionality.

Plug-ins that operate on jQuery objects must register themselves in the global jQuery.fn object. Add the following code into the script tag in your HTML file, replacing the // Our code will go here comment:

jQuery.fn.blackbox = function() {
    return this.each(function() {
        jQuery(this).wrapInner(jQuery('<span />').css("opacity", "0")).
                     css("backgroundColor", "#000000");
    });
};

And we’re done! Well, I did say it was simple, didn’t I?

Actually, there are a few different things going on in the code so let’s look at them individually.

First, we define our plug-in by adding a ‘blackbox’ method to the global jQuery fn object. This function will be called in the context of a jQuery object. In other words, if our plug-in is called like so $('p').blackbox(), in our function this would be $('p').

Next, we iterate over the elements in the current jQuery object using the each() function. As the parameter to each(), we provide an iterator function that will be called in the context of each element in the object. So this in our iterator function will be the actual DOM element.

Let’s look at that iterator function. First, since this is the DOM element, we start by getting a jQuery object by calling jQuery(this). Once we have a jQuery object, we use jQuery’s wrapInner() function to wrap the HTML inside the element in a SPAN tag. We generate this SPAN tag on-the-fly by passing the HTML to the global jQuery function to create the DOM element and then chaining a call to jQuery’s css() function, setting its opacity set to 0. This results in a crude “hiding” of the text. We then chain a call to jQuery’s css() function and set the backgroundColor of the original element to black.

Finally, it’s important that our plug-in function return the jQuery object when it’s done. This is what enables jQuery’s signature method chaining style and it’s considered bad form to break that paradigm.

Now that you know how the plug-in is built, let’s see it in use. Add the following code to the same script tag on your page, immediately after the code above:

$(document).ready(function() {
    $('.redacted').blackbox();
    $('cite,em').blackbox();
});

Reload the page in your browser and you’ll notice that the sensitive information is now safe from spies (well, at least the extremely lazy and/or incompetent ones):

Screenshot: Hello, ?

That’s about as simple as a jQuery plug-in can get. Let’s expand it a little bit by allowing users to customize its behavior:

jQuery.fn.blackbox = function(bgColor) {
    return this.each(function() {
        $(this).wrapInner($('<span />').css("opacity", "0")).
                css("backgroundColor", bgColor || "#000000");
    });
};

You’ll notice that we’ve added a bgColor parameter to the plug-in function. This will allow users to customize the color of the redaction boxes. This also means we need a better name for our plug-in!

If you’re new to Javascript, there’s a technique at work here that might seem strange. Javascript doesn’t enforce the presence of function parameters. It’s perfectly valid for our plugin function to be called without the bgColor parameter. To handle this case we use the logical OR operator to default the color to the string "#000000" in the event bgColor isn’t defined.

Now we can turn those cite and em boxes blue, while leaving the .redacted text with the default color:

$(document).ready(function() {
    $('.redacted').blackbox();
    $('cite,em').blackbox('#003366');
});

Which results in this impressive display:

Screenshot: Hello, blue?

That concludes our gentle introduction to jQuery plug-in creation. Now let’s dive into some bar charting.

Next Step: the Bar Fly plug-in code

Let’s start by looking at the entire Bar Fly plug-in and then we’ll walk through the code in detail. More experienced readers will probably want to skim over the more basic details.

(function ($) {
    
    // Create a new chart in +domNode+ with the given
    // +options+.  domNode can be a unique selector or
    // a jQuery element.
    //
    function BarFly(domNode, options) {
        this._el = $(domNode);
        this._init(options || {});
        this.draw();
    }
    
    BarFly.prototype = {
        
        // Activates (displays) the specified data set
        //
        activate: function(dataId) {
            if (this._isMultiset && this._setActiveData(dataId)) this.draw();
        },
        
        // Add a data set to the chart.  This is a no-op if
        // the barchart was created in "single set" mode (the
        // "data" parameter was an Array of values rather than
        // an Object).
        // data can be an Array of values (if none was given
        // when the chart was created)
        // 
        //   addData([1, 2, 3, 4])
        // 
        // or data can be an Object containing one or more
        // data sets:
        // 
        //   addData({setA: [1, 2, 3], setB: [4, 5, 6]})
        // 
        addData: function(data) {
            if ($.isArray(data)) {
                if (this._isMultiset == null) {
                    this._isMultiset = false;
                    this._addDataSet("1", data);
                }
            } else if (this._isMultiset != false) {
                this._isMultiset = true;
                var self = this;
                $.each(data, function(i, ds) { self._addDataSet(i, ds); });
            }
        },
        
        // Redraws the chart.
        //
        draw: function() {
            if (!this._activeSet) return false;
            if (!this._barEls) this._setupDom();
            var self = this,
                data = this._dataSets[this._activeSet],
                dataStyle = this._getDataStyle(),
                barStyle = {},
                barScale = this._el.innerHeight() / (this._dataRangeMax - this._dataRangeMin);
            
            if (this._animation) this._barEls.stop();
            
            this._barEls.each(function(i) {
                barStyle.height = Math.max(0,Math.floor((data[i] - self._dataRangeMin) * barScale - self._barPadH))+'px';
                if (self._animation) {
                    $(this).css(dataStyle).animate(barStyle, self._animation.duration, self._animation.easing);
                } else {
                    $(this).css(dataStyle).css(barStyle);
                }
            });
            this._el.trigger('barfly.drawn', [ this._activeSet, !!this._animation ]);
        },
        
        // Returns the list of data set names.
        //
        listData: function() {
            var dataIds = [];
            for (var id in this._dataSets) dataIds.push(id);
            return dataIds;
        },
        
        // Add the given data set to the chart.  If no
        // range was provided when the chart was created,
        // the calculated range will be adjusted to fit
        // this new data, if necessary.  All data sets
        // must have the same number of elements.
        //
        _addDataSet: function(dataId, data) {
            var id = dataId.toString();
            this._dataSets[id] = data;
            if (!this._activeSet) this._setActiveData(id);
            if (!this._isBounded) {
                for (var i=0,n=data.length; i<n; ++i) {
                    if ((this._dataRangeMax == null) || (this._dataRangeMax < data[i]))
                        this._dataRangeMax = data[i];
                    if ((this._dataRangeMin == null) || (this._dataRangeMin > data[i]))
                        this._dataRangeMin = data[i];
                }
            }
            this._el.trigger('barfly.added', [ dataId ]);
        },
        
        // Retrieves the custom style for the currently
        // active data set, if available.
        //
        _getDataStyle: function() {
            var id = this._isMultiset ? this._activeSet : "1";
            return $.extend({}, this._barStyle, this._dataStyles[id] || {});
        },
        
        // Initialize a new BarFly instance with the given
        // options.  Called from the constructor when a new
        // chart is created
        //
        _init: function(options) {
            this._dataSets = {};
            this._dataStyles = {};
            this._initAnimation(options);
            if (options.range) this._setRange(options.range);
            if (options.data) this.addData(options.data);
            if (options.active) this._setActiveData(options.active);
            this._barSpacing = (options.barSpacing) ? parseInt(options.barSpacing) : 0;
            this._setChartStyle(options.chartStyle);
            this._setBarStyle(options.barStyle);
            if (options.dataStyle) this._setDataStyles(options.dataStyle);
        },
        
        // Initializes the animation parameters based on the
        // given options and the global defaults.
        //
        _initAnimation: function(options) {
            if (typeof options.animation == "object") {
                this._animation = $.extend({}, options.animation);
            } else if ($.barflyDefaults.animation && options.animation != false) {
                this._animation = $.extend({}, $.barflyDefaults.animation);
            } else {
                this._animation = false;
            }
        },
        
        // Finds the data set identified by dataId
        // and makes it active.  If there is no such
        // data set or if it's already active, this
        // will return false
        //
        _setActiveData: function(dataId) {
            var id = dataId.toString();
            if (!this._dataSets[id] || this._activeSet == id) return false;
            this._activeSet = id;
            this._el.trigger('barfly.activate', [ id ]);
            return id;
        },
        
        // Sets the base CSS style for the bars in the
        // chart based on the defaults and the given style.
        //
        _setBarStyle: function(style) {
            this._barStyle = $.extend({}, this._baseBarStyle, $.barflyDefaults.barStyle, (style || {}));
        },
        
        // Sets the CSS style for the chart based on the
        // defaults and the given style object.
        //
        _setChartStyle: function(style) {
            this._chartStyle = $.extend({}, this._baseChartStyle, $.barflyDefaults.chartStyle, (style || {}));
        },
        
        // Sets up the CSS styles for the individual data sets
        // in the chart based on the provided style(s).
        //
        _setDataStyles: function(style) {
            this._dataStyles = {};
            if (this._isMultiset) {
                for (var id in style)
                    this._dataStyles[id] = $.extend({}, style[id] || {});
            } else {
                this._dataStyles["1"] = $.extend({}, style);
            }
        },
        
        // Sets the bounds for the chart.
        // The range can be an Array of min and max values:
        // 
        //   _setRange([0, 100])
        // 
        // Or the range can be an Object containing the min
        // and max values:
        // 
        //   _setRange({min: 12, max: 38})
        // 
        // If min is omitted, it will default to 0:
        // 
        //   _setRange({max: 300})
        // 
        _setRange: function(range) {
            if ($.isArray(range)) {             // range: [0, 73]
                this._dataRangeMin = range[0];
                this._dataRangeMax = range[1];
            } else {                            // range: {min: 0, max: 73}
                this._dataRangeMin = range.min || 0;
                this._dataRangeMax = range.max;
            }
            this._isBounded = true;
        },
        
        // Initialize the DOM elements for the chart
        //
        _setupDom: function() {
            var self = this,
                _el = this._el,
                data = this._dataSets[this._activeSet],
                dataSize = data.length,
                dataStyle = this._getDataStyle(),
                barStyle = $.extend({}, this._barStyle);
            
            // setup the chart container element
            _el.addClass('barflyChart').css(this._chartStyle);
            if (_el.width() == 0) _el.width($.barflyDefaults.chartWidth);
            if (_el.height() == 0) _el.height($.barflyDefaults.chartHeight);
            
            // create the bar elements and insert into container
            var barCode = [];
            for (var i=0; i<dataSize; ++i) barCode.push('<div class="barflyBar"></div>');
            _el.append(barCode.join(""));
            this._barEls = $('div.barflyBar', _el);
            
            // use first bar to figure out the box "padding"
            barStyle.left = '0';
            var x = this._barEls.eq(0).css(dataStyle).css(barStyle).width(0).height(0);
            this._barPadW = x.outerWidth();
            this._barPadH = x.outerHeight();
            
            // resize and position the bar elements inside the chart
            var barScale = _el.innerWidth() / dataSize,
                barSpacing = this._barSpacing;
            barStyle.bottom = (this._barPadH > 0) ? (0 - Math.floor(this._barPadH / 2))+'px' : 0;
            this._barEls.css(dataStyle).each(function(i) {
                var thisLeft = Math.floor(i * barScale + barSpacing),
                    nextLeft = Math.floor((i + 1) * barScale + barSpacing);
                barStyle.left = thisLeft+'px';
                barStyle.width = (nextLeft - thisLeft - self._barPadW - 2 * barSpacing)+'px';
                $(this).addClass('barflyBar'+i).css(barStyle);
            });
        },
        
        // The minimum CSS styles required for proper bar display
        //
        _baseBarStyle: { "position": "absolute", "display": "block" },
        
        // The minimum CSS styles required for proper chart display
        //
        _baseChartStyle: { "position": "relative", "display": "block", "overflow": "hidden" },
        
        _activeSet: null,
        _animation: null,
        _barEls: null,
        _el: null,
        _dataRangeMin: null,
        _dataRangeMax: null,
        _dataSets: null,
        _isBounded: null,
        _isMultiset: null,
        _barStyle: null,
        _chartStyle: null,
        _dataStyles: null
    };
    
    // Register the `barfly` method in the jQuery element
    // object.  Only creates a chart in an element once.
    // Subsequent calls on the same element will be ignored.
    //
    $.fn.barfly = function(options) {
        return this.each(function() {
            var el = $(this);
            if (!el.data('barfly'))
                el.data("barfly", new BarFly(this, options));
        });
    };
    
    // Default configuration values for all Bar Fly charts.
    //
    $.barflyDefaults = {
        chartStyle: {
            "backgroundColor": "#eeeeee",
            "borderWidth": "1px",
            "borderColor": "#666666",
            "borderStyle": "solid"
        },
        chartWidth: 400,
        chartHeight: 100,
        barStyle: {
            "backgroundColor": "#999999",
            "borderWidth": "1px",
            "borderColor": "#666666",
            "borderStyle": "solid"
        },
        animation: { duration: 500, easing: "linear" }
    };
    
    // Global helper.  Slightly easier than having
    // to access the "barfly" data value direclty.
    // 
    //   // Create a barfly chart:
    //   
    //   $('#my_chart').barfly({data: {one: [1,2,3,4,5], two: [4,5,3,1,2]}});
    //   
    //   // Now the helper can be used to access
    //   // and interact with the chart instance:
    //   
    //   $.barfly('#my_chart').activate('two');
    // 
    $.barfly = function(el) {
        return $(el).eq(0).data('barfly');
    };
    
})(jQuery);

Note that the entire plug-in is wrapped in a function call. This creates a lexical scope around the code and ensures that $ will reference our jQuery library. This pattern - (fn ($) { code(); })(jQuery); - is very common in jQuery development and Javascript in general.

We’re going to encapsulate the charting functionality in the BarFly class. Every time a chart is requested, an instance of the BarFly class will be created to generate and manage it.

// Create a new chart in +domNode+ with the given
// +options+.  domNode can be a unique selector or
// a jQuery element.
//
function BarFly(domNode, options) {
    this._el = $(domNode);
    this._init(options || {});
    this.draw();
}

We start with a minimal constructor that takes two parameters. The first is the DOM node into which the chart will be injected. The second is a Object containing the configuration options for this chart.

The constructor stores the chart’s DOM element in the instance variable _el, delegates initialization to the _init function and then draws the chart.

Immediately after the constructor, we define the prototype for the BarFly class. Essentially, the prototype defines the class’ instance functions and variables. This is where we’ll concentrate our work.

Note: Unlike a lot of languages, Javascript doesn’t have the concept of privacy. All instance functions and variables are publicly accessible. As a convention, I prepend an underscore to the name of any function or variable that I’m using privately.


Let’s take a look at the initialization function that’s called from the constructor:

_init: function(options) {
    this._dataSets = {};
    this._dataStyles = {};
    this._initAnimation(options);
    if (options.range) this._setRange(options.range);
    if (options.data) this.addData(options.data);
    if (options.active) this._setActiveData(options.active);
    this._barSpacing = (options.barSpacing) ? parseInt(options.barSpacing) : 0;
    this._setChartStyle(options.chartStyle);
    this._setBarStyle(options.barStyle);
    if (options.dataStyle) this._setDataStyles(options.dataStyle);
},

We start off by initializing a couple of data structures that will be used internally to store the data sets and their associated styles.

Note: Javascript Objects should be thought of as being passed by reference. Assigning an Object to a variable directly in the prototype of a class will create a class variable rather than an instance variable. Setting instance variables should be done by the instance code itself.


Next, the _initAnimation function is called. It is passed the instance configuration options. This function is responsible for setting up the configuration objects that will be passed to any animations.


_initAnimation: function(options) {
    if (typeof options.animation == "object") {
        this._animation = $.extend({}, options.animation);
    } else if ($.barflyDefaults.animation && options.animation != false) {
        this._animation = $.extend({}, $.barflyDefaults.animation);
    } else {
        this._animation = false;
    }
},

There are three possible scenarios we deal with here. First, if the instance configuration defines the animation parameters, we will use them. If not, we’ll use the default parameters if they have been defined and if animation hasn’t been disabled. Failing that, all animation will be disabled.


Our initialization function will now call the _setRange() function, if a range for the chart has been defined.


_setRange: function(range) {
    if ($.isArray(range)) {             // range: [0, 73]
        this._dataRangeMin = range[0];
        this._dataRangeMax = range[1];
    } else {                            // range: {min: 0, max: 73}
        this._dataRangeMin = range.min || 0;
        this._dataRangeMax = range.max;
    }
    this._isBounded = true;
},

This will set the bounds for the chart. The range can be an Array or an Object so we use the jQuery utility function $.isArray() to differentiate. If range is an Array, it will contain 2 values: the minimum and the maximum. If range is an Object, it will contain a max value and, optionally, a min value. We store these values in the _dataRangeMin and _dataRangeMax instance variables for use later when we’re drawing the chart. We also set a the _isBounded flag to true, indicating that the user has specified a range and we won’t have to calculate it ourselves.


The next step in the initialization process is to add any data sets that were provided. This is handled by the addData function:


addData: function(data) {
    if ($.isArray(data)) {
        if (this._isMultiset == null) {
            this._isMultiset = false;
            this._addDataSet("1", data);
        }
    } else if (this._isMultiset != false) {
        this._isMultiset = true;
        var self = this;
        $.each(data, function(i, ds) { self._addDataSet(i, ds); });
    }
},

As with the _setRange function we just looked at, the parameter to this function can be an Array or an Object so we’ll differentiate in the same way.

If data is an Array, it will contain the actual values for the data set. Setting the data set in this manner will put the chart into “single set mode”. This is because the data set is anonymous and therefore can’t be referenced later. So we have to check to see if this instance already has a data set defined (since addData()) is a public function it can be called outside of the initialization process. If we haven’t set the _isMultiset flag yet, we’ll set the flag to false and call _addDataSet() to handle the actual storage of the data. Since the data set has no name, we assign it the name "1".

If data is an Object, it will contain one or more data sets identified by name. First we need to make sure this instance isn’t operating in “single set mode” (_isMultiset is either true or null). If not, we’ll set the _isMultiset flag and iterate over all the key-value pairs in data, calling _addDataSet() for each pair.

NOTE: The var self = this pattern is used frequently in Javascript code. A function retains access to the lexical scope in which it was created. The context (eg, this) in which it is eventually executed can (and usually will) be different. By storing a reference to the current context in a local variable, the function can retain access to the proper context no matter how or where it is invoked. Naming this variable self is merely a convention, it can called be anything you like but really should be consistent throughout your code (for example, some people cleverly call name the variable that).


Let’s look at the _addDataSet() function that’s being called from addData():


_addDataSet: function(dataId, data) {
    var id = dataId.toString();
    this._dataSets[id] = data;
    if (!this._activeSet) this._setActiveData(id);
    if (!this._isBounded) {
        for (var i=0,n=data.length; i<n; ++i) {
            if ((this._dataRangeMax == null) || (this._dataRangeMax < data[i]))
                this._dataRangeMax = data[i];
            if ((this._dataRangeMin == null) || (this._dataRangeMin > data[i]))
                this._dataRangeMin = data[i];
        }
    }
    this._el.trigger('barfly.added', [ dataId ]);
},

This function is called with an identifier for a data set and an Array of values. It starts by ensuring the data set identifier is a String and then stores the values in the _dataSets instance variable that we initialized earlier in the _init() function.

Next, if we don’t currently have an active data set we’ll make this data set active by calling the _setActiveData() function with the identifier.

After that we’ll check the _isBounded instance variable that we saw earlier in the _setRange() function. If it’s not set to true, it means the user didn’t specify a range for the chart and we’ll need to calculate a range based on the data values. We do this by simply iterating over the values and setting _dataRangeMin and _dataRangeMax as appropriate.

Finally, we trigger an event named barfly.added on our containing DOM element and pass the identifier of the newly added data set. The details of the jQuery event system are outside the scope of this tutorial so please refer to the jQuery Event documentation for a thorough explanation.


Now let’s take a look at the _setActiveData() function we just saw:


_setActiveData: function(dataId) {
    var id = dataId.toString();
    if (!this._dataSets[id] || this._activeSet == id) return false;
    this._activeSet = id;
    this._el.trigger('barfly.activate', [ id ]);
    return id;
},

Again here we start my ensuring the data set identifier is a String. Then, we make sure that the identifier actually exists in our _dataSets Object and that it’s not already the active data set. If either of those things is true, we exit the function by returning false;

Otherwise, we activate the data set by setting the _activeSet instance variable and triggering the barfly.activate event on our containing DOM element, passing the identifier of the newly activated data set.


Next in the initialization process, the functions _setChartStyle and _setBarStyle are called. They are very similar so we’ll look at the together:


_setBarStyle: function(style) {
    this._barStyle = $.extend({}, this._baseBarStyle, $.barflyDefaults.barStyle, (style || {}));
},

_setChartStyle: function(style) {
    this._chartStyle = $.extend({}, this._baseChartStyle, $.barflyDefaults.chartStyle, (style || {}));
},

In both functions, an instance variable is being set to an Object containing CSS styles. We use the jQuery utility function $.extend() to create a new Object containing a combination of the values in all the other parameters.


Next, our initializer calls the _setDataStyles() function if the configuration options Object contains data styles.


_setDataStyles: function(style) {
    this._dataStyles = {};
    if (this._isMultiset) {
        for (var id in style)
            this._dataStyles[id] = $.extend({}, style[id] || {});
    } else {
        this._dataStyles["1"] = $.extend({}, style);
    }
},

This is essentially doing the same thing as the _setBarStyle and _setChartStyle functions. The difference here is that there will be multiple style Objects when there are multiple data sets, so the data set styles are stored in an Object indexed by the data set identifier.


The _draw() function is responsible for drawing (and redrawing) the chart:


draw: function() {
    if (!this._activeSet) return false;
    if (!this._barEls) this._setupDom();
    var self = this,
        data = this._dataSets[this._activeSet],
        dataStyle = this._getDataStyle(),
        barStyle = {},
        barScale = this._el.innerHeight() / (this._dataRangeMax - this._dataRangeMin);
    
    if (this._animation) this._barEls.stop();
    
    this._barEls.each(function(i) {
        barStyle.height = Math.max(0,Math.floor((data[i] - self._dataRangeMin) * barScale - self._barPadH))+'px';
        if (self._animation) {
            $(this).css(dataStyle).animate(barStyle, self._animation.duration, self._animation.easing);
        } else {
            $(this).css(dataStyle).css(barStyle);
        }
    });
    this._el.trigger('barfly.drawn', [ this._activeSet, !!this._animation ]);
},

First, it ensures that there’s an active data set and exits if there isn’t. It then checks to see if the _barEls instance variable has been set. This variable holds the bar elements for the chart and if it hasn’t been set the _setupDom() function is called.

Now that we have a data set and bar elements to work with, we can draw the chart. We call _getDataStyle() to retrieve any custom CSS styles that may be defined for this data set.

The barScale variable will containing the scaling factor for converting the data values to pixels when determining the height of the bars. We get this value by taking the total number of vertical pixels available (this._el.innerHeight() returns the height inside our containing DOM element) and dividing it by the maximum data value.

If there’s a animation currently running, we stop it.

Now we iterate over each of the bar elements and adjust them based on the new data set.

We start by calculating the height of the bar element based on the value in the data set and the scaling factor we calculated earlier. We use Math.max() to ensure won’t be any negative height values. The instance variable _barPadH adjusts the height to compensate for any border or padding values. It will be explained next when we look at the _setupDom() function.

Finally, the element has the style of the current data set applied to it. This is applied immediately, regardless of whether or not animation is enabled. Then the height of the bar element is adjusted by applying barStyle to the element. This is done immediately if animation is disabled. Otherwise, an animation is initiated with the appropriate duration and easing values.

After all the elements have been updated, the barfly.drawn event is triggered on our containing DOM element with two parameters: the identifier of the active data set and a Boolean value indicating whether or not the transition was animated (true means it was animated).


Most of the heavy lifting happens in the _setupDom function:


_setupDom: function() {
    var self = this,
        _el = this._el,
        data = this._dataSets[this._activeSet],
        dataSize = data.length,
        dataStyle = this._getDataStyle(),
        barStyle = $.extend({}, this._barStyle);
    
    // setup the chart container element
    _el.addClass('barflyChart').css(this._chartStyle);
    if (_el.width() == 0) _el.width($.barflyDefaults.chartWidth);
    if (_el.height() == 0) _el.height($.barflyDefaults.chartHeight);
    
    // create the bar elements and insert into container
    var barCode = [];
    for (var i=0; i<dataSize; ++i) barCode.push('<div class="barflyBar"></div>');
    _el.append(barCode.join(""));
    this._barEls = $('div.barflyBar', _el);
    
    // use first bar to figure out the box "padding"
    barStyle.left = '0';
    var x = this._barEls.eq(0).css(dataStyle).css(barStyle).width(0).height(0);
    this._barPadW = x.outerWidth();
    this._barPadH = x.outerHeight();
    
    // resize and position the bar elements inside the chart
    var barScale = _el.innerWidth() / dataSize,
        barSpacing = this._barSpacing;
    barStyle.bottom = (this._barPadH > 0) ? (0 - Math.floor(this._barPadH / 2))+'px' : 0;
    this._barEls.css(dataStyle).each(function(i) {
        var thisLeft = Math.floor(i * barScale + barSpacing),
            nextLeft = Math.floor((i + 1) * barScale + barSpacing);
        barStyle.left = thisLeft+'px';
        barStyle.width = (nextLeft - thisLeft - self._barPadW - 2 * barSpacing)+'px';
        $(this).addClass('barflyBar'+i).css(barStyle);
    });
},

We start by getting the active data set, the custom CSS styles for the active data set and the default CSS styles for all bars.

We prepare our containing DOM element by adding the barflyChart class to it and applying our CSS styles (in _chartStyle). If our containing DOM element doesn’t have a width and/or height we set them based on the values in $.barflyDefaults. This will handle elements that aren’t styled as display: block.

Next we create one bar element for each value in the data set. Each of these elements is a DIV with the barflyBar class applied to it. Each element is appended inside our containing DOM element. After all the bar elements have been created, we select them as a jQuery object and store them in the instance variable _barEls.

Since borders and padding style values can increase the dimensions of a DOM element, if we want to accurately position our bars we need to first figure out this padding. We accomplish this by applying the data set and bar styles to the first bar element, setting its width and height to 0 and then check its actual width and height. This will tell us how much the dimensions have been increased by the CSS styling.

We then calculate the scaling factor for the bar width by simply dividing the number of values in the data set by the inner width of our containing DOM element. This will yield the width of each bar element in pixels.

We also calculate the bottom position of each bar element. If the height of the bar element wasn’t increased by the CSS styles, this value will be zero. Otherwise it will be adjusted downward to compensate for the increased bar height.

Finally we apply the data set style and iterate over the bar elements. We position them in the chart by setting their left position based on the scaling value we calculated earlier and the _barSpacing value set in the _init() function. We then calculate the actual width of the bar by taking the left position of the next bar and subtracting the padding and spacing values. We then add a CSS class to each bar element based on its position (eg. barfly7) and finally, we apply the CSS styles for the bar.


There are two more “public” functions left to examine. The activate() function takes a data set identifier as its parameter and attempts to activate it. It first ensures that the chart has multiple data sets and then calls _setActiveData which, as we saw earlier, will try to set the active data set and will return false if it cannot.

If the data set was successfully activated, the draw() function is called to update the chart.


activate: function(dataId) {
    if (this._isMultiset && this._setActiveData(dataId)) this.draw();
},


The listData() function simply returns an Array containing the identifiers of the registered data sets:


listData: function() {
    var dataIds = [];
    for (var id in this._dataSets) dataIds.push(id);
    return dataIds;
},


Here we register our plug-in with jQuery under the name barfly in the same way we set up our blackbox function in the first part of this tutorial.


$.fn.barfly = function(options) {
    return this.each(function() {
        var el = $(this);
        if (!el.data('barfly'))
            el.data("barfly", new BarFly(this, options));
    });
};

The plug-in function takes an options object as its only parameter. It first checks to see if a BarFly instance already exists for an element before creating a new one.


The default configuration values for all Bar Fly charts are stored in the global $.barflyDefaults Object. This provides an avenue for mass customization if multiple charts are in use:


$.barflyDefaults = {
    chartStyle: {
        "backgroundColor": "#eeeeee",
        "borderWidth": "1px",
        "borderColor": "#666666",
        "borderStyle": "solid"
    },
    chartWidth: 400,
    chartHeight: 100,
    barStyle: {
        "backgroundColor": "#999999",
        "borderWidth": "1px",
        "borderColor": "#666666",
        "borderStyle": "solid"
    },
    animation: { duration: 500, easing: "linear" }
};


The final bit of code is the global $.barfly() helper function. This isn’t strictly necessary but it does provide a slightly more elegant way of accessing the BarFly chart instance for a given element.


$.barfly = function(el) {
    return $(el).eq(0).data('barfly');
};

So rather than accessing the chart instance by pulling it out of the element’s data store, $('#my_chart').data('barfly'), it can now be accessed like so: $.barfly('#my_chart').

Conclusion

Hopefully you have found this tutorial helpful. There are links to the source code and other resources in the sidebar of this page.

Please use the comment form below for questions and feedback.

Let's Work Together…

We're available for Javascript, Ruby on Rails and iPhone development projects.

Tutorial Resources

Bar Fly Documentation

Bar Fly source code

Bar Fly issue tracker

jQuery Documentation

jQuery Plug-in Authoring Guide

blog comments powered by Disqus