Table of Contents
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.
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.
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(){
}());
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));
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) {
}
}(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();
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 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'));
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.
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.
var zeroPoint = {
x: settings.marginLeft + settings.axisWidth,
y: paper.view.viewSize.height - settings.marginBottom - settings.xLabelsHeight
}
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.
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:
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;
});
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.
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:
$(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.
var maxDiscreteValueLength = (parseInt(maxDiscreteValue + '').toString()).length - 2;
var roundLimit = Math.pow(10, maxDiscreteValueLength);
var maxScaleValue = Math.ceil(maxDiscreteValue / roundLimit) * roundLimit;
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.
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);
}
Finally we draw the horizontal bottom line which will separate the bars from the category names in our bar chart:
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.
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.
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.
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.
$(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;
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);
$(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);
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();
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.
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;
}
}
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.
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;
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
};
},
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;
}
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.
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
});
}
}
}
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.
- 2013-03-18: Initial version.