Click here to Skip to main content
Click here to Skip to main content

HTML5 Bar Chart

, 25 Mar 2013
Rate this:
Please Sign up or sign in to vote.
A jQuery plugin for a 3D Bar Chart.
Prize winner in Competition "Best Web Dev article of March 2013"

Table of Contents

Introduction

When programmers face a problem they have already solved before, they usually rely on code reuse, and obviously, on their own previously acquired software knowledge, to build new software. As software development becomes mature, the code reuse routine often becomes a more standardized process which includes techniques, collections of implementations and abstractions such as software libraries, design patterns and frameworks, so that other people in the developer's team can take advantage of the common knowledge/code implementation reuse. When it comes to JavaScript development, the presence of the ubiquitous jQuery library can sometimes lead to the development of plugins, which are a broader kind of code reuse, because they can be made public and help a much wider developer community.

This article approaches two separate and very distinct programming capabilities: the first one is the ability to design your own jQuery plugin (that is, extending the usefulness of the jQuery's dollar sign) and the other is the art behind drawing your own 3D bar charts, using already existing free libraries, without relying on third party tools.

The goal of the article is not to provide a 100% professional, flawless 3D charting tool, nor a ultimate jQuery plugin, but instead it tries to give you a direction on how simple things can be made using simple tools.

System Requirements

This article contains the code needed to run a website relying no programming languages other than JavaScript, and consequently without references to assemblies or C# code, so it doesn't need to be compiled. All you need is a development environment to run websites, such as Visual Studio or Visual Studio Express 2012 for Web.

jQuery Plugin: Some Good Practices

When developers reach the point where they decide to write a jQuery plugin, probably they are wanting not only to abstract functionalities and reuse the code, but they also want to share some useful code with the jQuery developer community. But there is also the case when you decide to create your own jQuery plugin as a programming exercise. Whatever the case may be, keep in mind that there are some guidelines which are worth following, in order to be successful, save time and avoid bad surprises. Begin small, keep your code in line with the best practices and improve it as you code. You can learn more about jQuery plugin best practices by reading it in jQuery's Plugins/Authoring documentation page.

We must start by creating a immediately-invoked function expression (IIFE) in JavaScript code. The IIFE is a design pattern that provides the self-containment of private functions variables and functions within the plugin scope, thus avoiding the pollution of JavaScript's Global Environment. JavaScript developers will easily recognize the IIFE pattern by the following code:

(function(){
      /* code */ 
    }());

In the above code, the outermost pair of parentheses wrap the function in an expression and immediately forces its evaluation. The pair of parentheses in the last line of code invokes the function immediately.

When it comes to jQuery plugin development, it's important to pass the jQuery reference as a parameter in our IIFE expression, so that the dollar sign ($) can be used safely within the scope of the plugin, without the risk of external libraries overriding the dollar sign:

(function($){
      /* jQuery Plugin code goes here. Notice the $ sign will never have a meaning other than the jQuery object. */ 
    }(jQuery));

Next, we create the function that will hold and execute the whole of our bar chart plugin functionality. Notice the options parameter, which will contain all the initialization settings needed to configure the bar chart according to the bar chart requirements:

(function($){
  $.fn.barChart = function (options) {
        //as expected, our plugin code falls here.
    }
}(jQuery));

Inside the plugin function, the context is given by the this JavaScript keyword. Most often than not, developers will be tempted to reference the context by enclosing it using the dollar sign (i.e. jQuery) function: "$(this)", instead of just this. This is a common mistake, since the this keyword already refers to the jQuery object and not the DOM element inside which the bar chart is being created:

(function($){
  $.fn.barChart = function (options) {
        var self = this;
    }
}(jQuery));

In the above JavaScript code, we are storing the value of the this object in the self reference. This is needed specifically inside functions, where the this keyword behaves as the context for the function itself, instead of the context for the outermost plugin function. Thus, the self will be used as the context for the bar chart plugin instead.

The plugin code starts by defining a series of settings that will become the default values for the most common configurations. This will provide our plugin users with convenient standard values that can be either configured (allowing a flexible charting component) or ignored (so that the plugin user can provide the smallest set of startup configuration).

As the plugin component gets more sophisticated, it's generally a good idea to provide a more complete and comprehensive set of default settings, in order to give users a powerful, flexible and unobtrusive plugin.

$.fn.barChart = function (options) {

var self = this;
this.empty();

// Create some defaults, extending them with any options that were provided
var settings = $.extend({
    'id': generateGuid(),
    'discreteValueField': 'value',
    'categoryField': 'name',
    'colorField': 'color',
    'scaleText': 'values',
    'font': 'helvetica, calibri',
    'border': '1px solid #c0c0c0',
    'backgroundColor': '#FBEDBB',
    'title': '',
    'width': null,
    'height': null,
    'marginTop': 60,
    'marginLeft': 40,
    'marginRight': 15,
    'marginBottom': 15,
    'axisWidth': 50,
    'xLabelsHeight': 80,
    'barColor': '#ff0000',
    'depth3D': 0,
    'angle': 0,
    'onDataItemClick': null
}, options);

The Paper JS Plugin

The Html5 Bar Chart plugin relies heavily on the awesome Paper JS library. PaperJS was developed by Jürg Lehni and Jonathan Puckey and is an open source HTML5 library for vector graphics scripting, which runs on top of the HTML5 canvas element, exposing a powerful programming and well designed interface. PaperJs is compatible with Scriptographer, a scripting environment for Adobe Illustrator with more than 10 years of development.

Since HTML5 Bar Chart needs the PaperJS, which in turn need the Canvas element, it would be reasonable that we required the user to provide a HTML5 Canvas element in order to build the bar chart. But instead, the bar chart plugin is called on an ordinary div element, and the very bar chart plugin creates the canvas element. Of course, here we are using the standard jQuery DOM element creation syntax. Notice the last line in the following code snippet, where we append the newly created canvas element to the target DOM div element (referenced by the this keyword):

var newCanvas =
$('<canvas>').attr({
    id: settings.id,
    width: settings.width,
    height: settings.height
}).css({
    border: settings.border,
    backgroundColor: settings.backgroundColor
});
this.append(newCanvas);

By default, when you start using Paper JS, the library automatically creates a new so-called "Paper Scope", that is, a scope object that is bound to the canvas on which Paper JS will render the graphics. This default scope is provided by the paper instance. But since we may need to create multiple charts (bound to multiple canvasses) in the same page, we should create one PaperScope per chart. The following line generates a brand new instance of PaperScope:

paper = new paper.PaperScope();

And finally we make the Paper Scope bound to our canvas:

paper.setup(newCanvas.attr('id'));

The Chart Title

We render the bar chart title by defining a new PointText object containing the title as configured in the initial bar chart settings. Some adjustments are needed for the chart title, such as defining the layout as center-justified and positioning it at the horizontal middle point of the canvas view.

//Rendering the bar chart title
var text = new PointText(paper.view.viewSize.width / 2, 15);
text.paragraphStyle.justification = 'center';
text.characterStyle.fontSize = 10;
text.characterStyle.font = settings.font;
text.content = settings.title;

Most of the elements of the bar chart will gravitate around the point where both the horizontal and vertical axes cross, therefore it is convenient to define a variable with an object that holds the zero point coordinates. Notice that the margins are discounted in the calculation.

//The Zero Point defines where the two axes cross each other
var zeroPoint = {
    x: settings.marginLeft + settings.axisWidth,
    y: paper.view.viewSize.height - settings.marginBottom - settings.xLabelsHeight
}

The Scale (Ruler) Line

Next we define the Path object representing the vertical line at the left of the chart. This line will be used as the margin for the scale itself.

//Rendering the left (scale) line
var leftPath = new Path();
leftPath.strokeColor = 'black';
leftPath.add(zeroPoint.x, settings.marginTop);
leftPath.add(zeroPoint.x, zeroPoint.y);

Since bar charts are about comparison of numbers, it's clear that rendering the value bars will only be possible if all values are taken into consideration. That is, we should first discover the maximum and minimum discrete values from our data array, and only then will we be able to render the bars properly:

//Discovering the maximum and minimum discrete values
var xOffset = 0;
var dataItemBarAreaWidth = (paper.view.viewSize.width - settings.marginLeft - settings.marginRight - 
    settings.depth3D - settings.axisWidth) / settings.data.length;
$(settings.data).each(function (index, item) {
    var value = item[settings.discreteValueField];
    maxDiscreteValue = Math.max(maxDiscreteValue, value);
    minDiscreteValue = Math.min(minDiscreteValue, value);
    item.value = value;
    item.originalValue = value;
});

The Magnitude Caption

Depending on how big the numbers are, we may end up with a cluttered mess of numbers in our bar chart. Fortunately, the magnitude variable is up to the task of simplifying the data visualization by cutting the numbers by thousands, millions, and so on, making the bar chart more pleasant to read and clear to understand.

//Discovering the magnitude value based on the maximum discrete value
var magnitude = 1;
var magnitudeLabel = '';
if (maxDiscreteValue > 1000000000) {
    magnitude = 1000000000;
    magnitudeLabel = '(in billions)'
}
else if (maxDiscreteValue > 1000000) {
    magnitude = 1000000;
    magnitudeLabel = '(in millions)'
}
else if (maxDiscreteValue > 1000) {
    magnitude = 1000;
    magnitudeLabel = '(in thousands)'
}

Once the magnitude value is found, each and every value in the data set must be re-scaled:

//Each value must be re-scaled based on the magnitude
$(settings.data).each(function (index, item) {
    item.value = item.value / magnitude;
});

maxDiscreteValue = maxDiscreteValue / magnitude;
minDiscreteValue = minDiscreteValue / magnitude;

In the cases when the resulting re-scaled maximum value ends up with too many digits, it would be a problem to show it correctly. That's why a rounding method is used, so that it doesn't end up with an excessive number of digits.

//Rounding the numbers to the same number of digits
var maxDiscreteValueLength = (parseInt(maxDiscreteValue + '').toString()).length - 2;
var roundLimit = Math.pow(10, maxDiscreteValueLength);
var maxScaleValue = Math.ceil(maxDiscreteValue / roundLimit) * roundLimit;

The Scale

With all the numbers correctly re-scaled, we now must write all the scale values (arbitrarily defined as 5 distinct values) alongside the scale line, beginning with zero and ending with the max scale value. Again, a PointText object is instantiated, and then positioned at the left side of the scale line. Each scale value must be rounded so that it doesn't have excessive number of digits.

//Rendering the scale values
var scaleCount = 5;
var lastScaleValue = 0;
for (var scale = 0; scale <= scaleCount; scale++) {
    var y = zeroPoint.y - scale * (zeroPoint.y - settings.marginTop) / scaleCount;

    var scaleText = new PointText(zeroPoint.x - 10, y + 5);
    scaleText.paragraphStyle.justification = 'right';
    scaleText.characterStyle.fontSize = 8;
    scaleText.characterStyle.font = settings.font;
    var value = ((maxScaleValue - 0) / scaleCount) * scale;

    if (value.toString().length - lastScaleValue.toString().length > 2) {
        var lastDigitsCount = (lastScaleValue.toString().length - parseInt(lastScaleValue).toString().length) - 1;
        var pow = Math.pow(10, lastDigitsCount);
        value = parseInt(pow * value) / pow;
    }
    scaleText.content = addCommas(value);

    lastScaleValue = value;
    var scalePath = new Path();
    scalePath.strokeColor = 'black';
    scalePath.add(zeroPoint.x - 5, y);
    scalePath.add(zeroPoint.x, y);
}

Category Names

Finally we draw the horizontal bottom line which will separate the bars from the category names in our bar chart:

//Rendering the horizontal (bottom) line
var bottomPath = new Path();
bottomPath.strokeColor = 'black';
bottomPath.add(zeroPoint.x, zeroPoint.y + 1);
bottomPath.add(paper.view.viewSize.width - settings.marginRight, zeroPoint.y + 1);

At the left margin, we place the caption that will explain what the discrete values are about. Notice that the instance of PointText is rotated by 270 degrees, which means the text is flowing from the bottom to the top of the chart.

//The rotated caption for the discrete values
var discreteValuesCaption = new PointText(settings.marginLeft * .5, paper.view.viewSize.height / 2);
discreteValuesCaption.paragraphStyle.justification = 'center';
discreteValuesCaption.characterStyle.fontSize = 11;
discreteValuesCaption.characterStyle.font = settings.font;
discreteValuesCaption.content = settings.discreteValuesCaption;
discreteValuesCaption.rotate(270);

maxDiscreteValueLength = (parseInt(maxDiscreteValue + '').toString()).length - 2;
roundLimit = Math.pow(10, maxDiscreteValueLength);
maxScaleValue = Math.ceil(maxDiscreteValue / roundLimit) * roundLimit;

Alongside the caption with the discrete values caption, there is the caption for the magnitude of the values. This is needed so that the user doesn't mistake the scale numbers by the captions only, but also take the magnitude in consideration.

//The rotated caption for the magnitude
var discreteValuesCaption2 = new PointText(settings.marginLeft, paper.view.viewSize.height / 2);
discreteValuesCaption2.paragraphStyle.justification = 'center';
discreteValuesCaption2.characterStyle.fontSize = 12;
discreteValuesCaption2.characterStyle.font = settings.font;
discreteValuesCaption2.content = magnitudeLabel;
discreteValuesCaption2.rotate(270);

Since the bar chart has a 3D effect, we must calculate the position for the top right corner of that 3D figure, which will in turn be used to draw each chart bar.

//The {x,y} offset point, used to define the deep corner of the bar
depth3DPoint = {
    x: -settings.depth3D * Math.cos(settings.angle * (Math.PI / 180)),
    y: settings.depth3D * Math.sin(settings.angle * (Math.PI / 180)),
};

Now we use once again a new instance of the PointText to render the category names below the horizontal bottom line. Notice that, for the sake of saving space in our chart, the category names are rotated by 270 degrees, which means that the text is now flowing from the bottom to the top of the chart.

//Creates one bar for each category
$(settings.data).each(function (index, item) {
    var value = item.value;
    var originalValue = item.originalValue;
    var categoryName = item[settings.categoryNameField];
    var color = item[settings.colorField];

    var middleX = zeroPoint.x + dataItemBarAreaWidth * index + dataItemBarAreaWidth / 2;

    //Generates and renders the category bar
    var g = new html5Chart.Bar(
        {
            categoryName: categoryName,
            value: value,
            originalValue: originalValue,

            middleX: middleX,
            dataItemBarAreaWidth: dataItemBarAreaWidth,
            barHeightRatio: barHeightRatio,

            depth3DPoint: depth3DPoint,
            zeroPoint: zeroPoint,
            color: color
        });
    bars.push(g);

    //We set the hidden span's html value so that jQuery width() and height() functions can calculate the
    //dimensions of the rendered, then we used them to position the text.
    $(newHiddenSpan).html(categoryName);

    var barLabelLineX = zeroPoint.x + dataItemBarAreaWidth * index + dataItemBarAreaWidth / 2;

    var barLabelLine = new Path();
    barLabelLine.strokeColor = 'black';
    barLabelLine.add(barLabelLineX, zeroPoint.y);
    barLabelLine.add(barLabelLineX, zeroPoint.y + 5);

    //Renders the category names below the bars
    var barLabel = new PointText(barLabelLineX + 5, zeroPoint.y + 
                        $(newHiddenSpan).width() / 2 + categoryNameMargin);
    barLabel.paragraphStyle.justification = 'center';
    barLabel.characterStyle.fontSize = 10;
    barLabel.characterStyle.font = settings.font;
    barLabel.content = categoryName;
    barLabel.rotate(270);
});

As expected, the method draw is responsible to render the view:

paper.view.draw();

The Startup Animation

When the bar chart is first shown on the canvas element, instead of just pushing a static image of the chart, we initiate a nice animation, where each bar starts at the height zero and grows until it reaches its defined height. This functionality would most likely have a low priority in any project, but it adds an interesting visual effect that captures the user attention to the data being displayed, in a professional way.

//Generates the initial animation
var animationPercPerFrame = 5;
var ellapsedTime = 0;
var accumulatedBarHeight = 0;
paper.view.onFrame = function (event) {
    ellapsedTime += event.time;

    var animationCount = 0;
    animationPercPerFrame = easingOut(ellapsedTime, 0, 100, 40);
    $(bars).each(function (index, bar) {
        var animationResult = bar.animate(animationPercPerFrame);

        if (animationResult.animated > 0) {
            animationCount++;
        }
        accumulatedBarHeight += animationResult.step;
    });
    if (animationCount == 0) {
        paper.view.onFrame = null;
    }
}

The 3D Bars

It would be a reasonable decision if we treated the category bar as an object. That is, each bar would have to be initialized, and would have predefined properties and methods. But instead of creating a brand new object from scratch, we extend the Paper JS Group object. The Group object is a collection of items, and this kind of Paper JS object is particularly useful for our category bar objects, because the underlying Group object can hold the 3 visual components of the bar: the front face, the top face and the left face.

//Renders the bar chart bar as a group of Paper JS items
html5Chart.Bar = Group.extend({
    initialize: function (config, items) {
        ...
    }, 
    createBarSidePath: function (color, p1, p2, p3, p4) {
        ...
    },
    getFrontPolygonPath: function () {
        ...
    },
    getTopPolygonPath: function () {
        ...
    },
    getSidePolygonPath: function () {
        ...
    },
    setBarTopY: function (y) {
        ...
    },
    animate: function (animationPercPerFrame) {
        ...
    },
    colourNameToHex: function (colour) {
        ...
    }
});

As expected, the initialization of the html5Chart.Bar will take in the basic settings needed to render the bar. As we will see later on, the categoryName, and originalValue properties are used to render the caption balloon with category data that is shown as the user moves the mouse over the bar. The zeroPoint provides the y coordinate that represents the bottom line for the bars. The middleX is the horizontal offset representing the middle position of the bar region. The dataItemBarAreaWidth is the amount of space allocated for each bar. The parameter barHeightRatio is the pixel/value ratio that was previously calculated based on the maximum discrete value. Finally, the depth3DPoint is the pair of coordinates measuring the offset distance regarding to the top right position of the front facing side of the bar. Next, The cardinal points in the point codes indicate the position of the points regarding to the bar.

Another interesting fact about the initialization function is the color scheme. Since we are rendering 3D bars, they would appear unrealistic if we painted every side of the bar with the same color. So it would be nice if we adjusted the RGB (red/green/blue) components of the color in order to create light/darker effect in the 3D bar. Parsing the RGB components from a hexadecimal color value is relatively simple, as we see by the code snippet below. But the problem gets harder when the user provides named colors for the bars. In this case, the colourNameToHex (obviously) converts hard-coded color names into their hexadecimal counterparts, using a simple dictionary.

Finally, the children collection property of the underlying Group object is initialized, and then the 3 sides of the bar (created as Path objects) are added to the children of this group.

initialize: function (config, items) {
    this.categoryName = config.categoryName;
    this.value = config.value;
    this.originalValue = config.originalValue;
    this.zeroPoint = config.zeroPoint;
    this.middleX = config.middleX;
    this.dataItemBarAreaWidth = config.dataItemBarAreaWidth;
    this.barHeightRatio = config.barHeightRatio;
    this.depth3DPoint = config.depth3DPoint;

    //The cardinal points in the point codes indicate the position of the points regarding to the bar.
    var pNW = { x: this.middleX - (this.dataItemBarAreaWidth * .75) / 2, 
      y: this.zeroPoint.y - this.barHeightRatio * this.value };
    var pNE = { x: this.middleX + (this.dataItemBarAreaWidth * .75) / 2, 
      y: this.zeroPoint.y - this.barHeightRatio * this.value };
    var pSW = { x: this.middleX - (this.dataItemBarAreaWidth * .75) / 2, y: this.zeroPoint.y };
    var pSE = { x: this.middleX + (this.dataItemBarAreaWidth * .75) / 2, y: this.zeroPoint.y };
    var pNW2 = { x: pNW.x - this.depth3DPoint.x, y: pNW.y - this.depth3DPoint.y };
    var pNE2 = { x: pNE.x - this.depth3DPoint.x, y: pNW.y - this.depth3DPoint.y };
    var pSW2 = { x: pSW.x - this.depth3DPoint.x, y: pSW.y - this.depth3DPoint.y };
    var pSE2 = { x: pSE.x - this.depth3DPoint.x, y: pSW.y - this.depth3DPoint.y };

    this.bottomValue = pSE.y;
    this.topValue = pNE.y;
    this.currentValue = pSE.y;

    var color = config.color;
    var color2 = config.color2;
    var color3 = config.color3;

    var hexColor = this.colourNameToHex(color);
    if (!hexColor)
        hexColor = color;

    if (hexColor) {
        var r = hexColor.substring(1, 3);
        var g = hexColor.substring(3, 5);
        var b = hexColor.substring(5, 7);
        var decR = parseInt(r, 16);
        var decG = parseInt(g, 16);
        var decB = parseInt(b, 16);
        var darkFactor1 = .9;
        var darkFactor2 = .8;
        color2 = 'rgb(' + Math.round(decR * darkFactor1) + ',' + 
          Math.round(decG * darkFactor1) + ',' + Math.round(decB * darkFactor1) + ')';
        color3 = 'rgb(' + Math.round(decR * darkFactor2) + ',' + 
          Math.round(decG * darkFactor2) + ',' + Math.round(decB * darkFactor2) + ')';
    }

    var dataItem3DPath = this.createBarSidePath(color2, pSW, pSE, pSE, pSW);
    var dataItem3DTopPath = this.createBarSidePath(color, pSW, pSE, pSE2, pSW2);
    var dataItem3DSidePath = this.createBarSidePath(color3, pSE, pSE2, pSE2, pSE);

    items = [];
    items.push(dataItem3DPath);
    items.push(dataItem3DTopPath);
    items.push(dataItem3DSidePath);

    this.base();
    this._children = [];
    this._namedChildren = {};
    this.addChildren(!items || !Array.isArray(items)
            || typeof items[0] !== 'object' ? arguments : items);
    this.value = this.children[0].segments[2].point.y - this.children[0].segments[1].point.y;
},

Creating each side of the bar is relatively simple: the color and points are passed as arguments, and a new Path representing a closed polygon is returned from the function.

createBarSidePath: function (color, p1, p2, p3, p4) {
    var path = new Path();
    path.fillColor = color;
    path.strokeWidth = 0;
    path.add(p1.x, p1.y);
    path.add(p2.x, p2.y);
    path.add(p3.x, p3.y);
    path.add(p4.x, p4.y);
    path.closed = true;

    return path;
},

The setBarTopY function redefine the position for the 3 top points of our bar. This is particularly useful when rendering the start up animation of the chart.

setBarTopY: function (y) {
    this.currentValue = y;
    var frontPolygonPath = this.getFrontPolygonPath();
    frontPolygonPath.segments[0].point.y = y;
    frontPolygonPath.segments[1].point.y = y;

    var topPolygonPath = this.getTopPolygonPath();
    topPolygonPath.segments[0].point.y = y;
    topPolygonPath.segments[1].point.y = y;
    topPolygonPath.segments[2].point.y = y - this.depth3DPoint.y;
    topPolygonPath.segments[3].point.y = y - this.depth3DPoint.y;

    var sidePolygonPath = this.getSidePolygonPath();
    sidePolygonPath.segments[0].point.y = y;
    sidePolygonPath.segments[1].point.y = y - this.depth3DPoint.y;
},

As said before, the animation adds a pleasant look and feel to our bar chart. The animate function is the invoked at the beginning of the rendering process, roughly at a rate of 60 frames per second.

animate: function (animationPercPerFrame) {
    var step = 0;
    var animated = false;
    if (this.currentValue < this.topValue) {
        this.currentValue == this.topValue;
    }
    else {
        step = (this.bottomValue - this.topValue) * (animationPercPerFrame / 100);

        var y = this.zeroPoint.y - (animationPercPerFrame / 100) * (this.bottomValue - this.topValue);

        this.setBarTopY(y);
        animated = true;
    }
    return {
        step: step,
        animated: animated
    };
},

Converting Color Names to Hex Codes

As we've seen before, the colourNameToHex is a function that uses a dictionary to convert the hard coded color names to their equivalent hexadecimal values.

colourNameToHex: function (colour) {
    var colours = {
        "aliceblue": "#f0f8ff", "antiquewhite": "#faebd7", "aqua": "#00ffff", "aquamarine": "#7fffd4", "azure": "#f0ffff",
        "beige": "#f5f5dc", "bisque": "#ffe4c4", "black": "#000000", "blanchedalmond": "#ffebcd", "blue": "#0000ff", 
        "blueviolet": "#8a2be2", "brown": "#a52a2a", "burlywood": "#deb887",
        ...
        ...many colors later...
        ...
        "yellow": "#ffff00", "yellowgreen": "#9acd32"
    };

    if (typeof colours[colour.toLowerCase()] != 'undefined')
        return colours[colour.toLowerCase()];

    return false;
}

The Floating Pop Up

Another nice feature of the HTML5 bar chart is the floating caption pop up that appears whenever the user moves the mouse over one of the bars. The caption displays the category name and the discrete value. In our code, the pop up functionalities have been encapsulated as an extension of the Group object.

//Creates a group of Paper JS items that represent the pop up caption
//that displays the current category name and corresponding discrete value
//when the user moves the mouse over it
html5Chart.Popup = Group.extend({
    initialize: function (options) {
        var settings = this.settings = $.extend({
            'fontSize': '10',
            'font': 'helvetica, calibri',
            'color': 'color',
            'fillColor': 'orange',
            'strokeColor': 'black',
            'strokeWidth': '1'
        }, options);

        this.popupCenter = {
            x: paper.view.viewSize.width / 2,
            y: paper.view.viewSize.height / 2,
        };
        var text = '';

        $(newHiddenSpan).css('font-family', settings.font);
        $(newHiddenSpan).css('font-size', settings.fontSize * 1.6);
        $(newHiddenSpan).html(text);
        self.append(newHiddenSpan);
        var textSize = { width: 200, height: 20 };

        var popupText = new paper.PointText(textSize.width / 2, textSize.height * .75);
        popupText.paragraphStyle.justification = 'center';
        popupText.characterStyle.fontSize = settings.fontSize;
        popupText.characterStyle.font = settings.font;
        popupText.content = text;

        var rectangle = new Rectangle(new Point(0, 0), textSize);
        var cornerSize = new Size(5, 5);
        var popupBorder = new Path.RoundRectangle(rectangle, cornerSize);
        popupBorder.strokeColor = settings.strokeColor;
        popupBorder.strokeWidth = settings.strokeWidth;
        popupBorder.fillColor = settings.fillColor;
        this.base();
        this._children = [];
        this._namedChildren = {};
        this.addChildren([popupBorder, popupText]);
        this.visible = false;
        return this;
    },
    resetPopup: function (text) {
        if (this.text != text) {
            this.text = text;
            var settings = this.settings;
            $(newHiddenSpan).css('font-family', settings.font);
            $(newHiddenSpan).css('font-size', settings.fontSize * 1.6);
            $(newHiddenSpan).html(text);
            var textSize = { width: $(newHiddenSpan).width(), height: $(newHiddenSpan).height() };
            var rectangle = new Rectangle(new Point(this.position.x - textSize.width / 2, 
                                          this.position.y - textSize.height / 2), textSize);
            var cornerSize = new Size(5, 5);
            var popupBorder = new Path.RoundRectangle(rectangle, cornerSize);
            popupBorder.strokeColor = settings.strokeColor;
            popupBorder.strokeWidth = settings.strokeWidth;
            popupBorder.fillColor = settings.fillColor;
            var border = this.getBorder();
            var popupText = this.getLabel();
            popupText.paragraphStyle.justification = 'center';
            popupText.characterStyle.fontSize = settings.fontSize;
            popupText.characterStyle.font = settings.font;
            popupText.content = text;
            this.removeChildren();
            this.addChildren([popupBorder, popupText]);
        }
    },
    getBorder: function () {
        return this.children[0];
    },
    getLabel: function () {
        return this.children[1];
    }
});

When the user moves the mouse over the bars, the pop up is displayed. When the mouse leaves the bar area, the pop up is hidden. This functionality is possible thanks to the tool.onMouseMove function of Paper JS, along with another Paper JS function (paper.project.hitTest) that tests the mouse position against the bar objects already present in the chart.

tool.onMouseMove = function (event) {
    var hitResult = paper.project.hitTest(event.point, hitOptions);
    self.selectedItemPopup.visible = false;
    self.css('cursor', '');
    if (hitResult && hitResult.item) {
        if (hitResult.item.parent) {
            self.selectedItemPopup.position = new Point(event.point.x, event.point.y - 40);
            if (hitResult.item.parent.categoryName) {
                if (selectedBar) {
                    if (selectedBar != hitResult.item.parent) {
                        selectedBar.opacity = 1;
                        selectedBar.strokeWidth = 0;
                        selectedBar.strokeColor = undefined;
                        self.selectedItemPopup.visible = false;
                        self.css('cursor', '');
                    }
                }
                selectedBar = hitResult.item.parent;
                selectedBar.opacity = .5;
                selectedBar.strokeWidth = 1;
                selectedBar.strokeColor = 'black';
                self.selectedItemPopup.visible = true;
                self.css('cursor', 'pointer');
                if (self.selectedItemPopup.resetPopup) {
                    var value = selectedBar.originalValue;
                    value = parseInt(value * 100) / 100;
                    self.selectedItemPopup.resetPopup(selectedBar.categoryName + ': ' + addCommas(value));
                }

                if (settings.onDataItemMouseMove) {
                    settings.onDataItemMouseMove({
                        categoryName: selectedBar.categoryName,
                        value: selectedBar.originalValue
                    });
                }
            }
        }
    }
    else {
        if (selectedBar) {
            selectedBar.opacity = 1;
            selectedBar.strokeWidth = 0;
            selectedBar.strokeColor = undefined;
            selectedBar = null;
            self.css('cursor', '');
        }
    }
}

Whenever the user clicks over a category bar area, the bar chart plugin responds by returning both the category name and the value of that category. Of course, the onDataItemClick callback is only invoked if the user have previously subscribed to that event callback.

tool.onMouseUp = function () {
    if (selectedBar) {
        if (settings.onDataItemClick) {
            settings.onDataItemClick({
                categoryName: selectedBar.categoryName,
                value: selectedBar.originalValue
            });
        }
    }
}

Final Considerations

This article summed up the basic techniques needed for developing bar charts (as well as other types of graphic tools) using jQuery plugin and the Paper JS library. I hope the article's explanation is reasonably clear, and in any case it remains open for future improvements.

Thanks for the reading, and please feel free to express your opinions regarding the article and/or the accompanying code.

History

  • 2013-03-18: Initial version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Marcelo Ricardo de Oliveira
Software Developer
Brazil Brazil
Marcelo Ricardo de Oliveira is a senior software developer who lives with his lovely wife Luciana and his little buddy and stepson Kauê in Guarulhos, Brazil, is co-founder of the Brazilian TV Guide TV Map and currently works for ILang Educação.
 
He is often working with serious, enterprise projects, although in spare time he's trying to write fun Code Project articles involving WPF, Silverlight, XNA, HTML5 canvas, Windows Phone app development, game development and music.
 
Published Windows Phone apps:
 
 
Awards:
 
CodeProject MVP 2012
CodeProject MVP 2011
 
Best Web Dev article of March 2013
Best Web Dev article of August 2012
Best Web Dev article of May 2012
Best Mobile article of January 2012
Best Mobile article of December 2011
Best Mobile article of October 2011
Best Web Dev article of September 2011
Best Web Dev article of August 2011
HTML5 / CSS3 Competition - Second Prize
Best ASP.NET article of June 2011
Best ASP.NET article of May 2011
Best ASP.NET article of April 2011
Best C# article of November 2010
Best overall article of November 2010
Best C# article of October 2010
Best C# article of September 2010
Best overall article of September 2010
Best overall article of February 2010
Best C# article of November 2009

Comments and Discussions

 
QuestionYou're giving, so I'm giving you a 5!!! PinmemberDewey25-Mar-13 12:29 
AnswerRe: You're giving, so I'm giving you a 5!!! PinmvpMarcelo Ricardo de Oliveira26-Mar-13 16:36 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web01 | 2.8.140721.1 | Last Updated 25 Mar 2013
Article Copyright 2013 by Marcelo Ricardo de Oliveira
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid