Projects

WebGL Demos
PHP Data Pivot
PHP Data Subtotals
HTML5 Graph
Java NW3D2
JS Code Formatter
HTML5 Clock
Silverlight Gauge
Java NW3D
Java Fireworks
Java Early 3D
Java Snow
Java Dogfight
Java Water Simulation
Java Bump Mapping
Java Elite Ships

Bar Graph using HTML5 canvas

Description

This is a canvas-based bar graph with the following features:-


Demonstration

Let's look at a demo before discussing the code. Click here to re-draw the graph with random data.


The <canvas> element is not supported by your browser!

Randomise


Using the graph

The graph uses a canvas object, which must be present in the web page and have the correct ID. The text within the element is not displayed unless HTML5 canvas isn't supported.

	The canvas element is not supported by your browser!

In the demonstration above the graph is defined as follows.

	graph1 = new BarGraph(c);
	graph1.vals = [6300,200,5520,3760,9,320,819,1308,405,-2101,640,1999];
	graph1.cats = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
	graph1.title = "£ Sales by Month";
	graph1.bgcol1 = "#eef";
	graph1.bgcol2 = "#77f";
	graph1.effect = "twang";
	graph1.drawGraph();

You can see the values and labels being passed as arrays, the title being set and the background colour being overridden. Properties 'bgcol1' and 'bgcol2' are either end of the background colour gradient. We'll discuss the 'effect' property later. The final statement calls the 'drawGraph' method, which sets-up the global graph properties, calculates scaling and then decides how to render the graph.


drawGraph()

 this.drawGraph = function() {
	var tw;
	var maxval = 0;
	var minval = 0;
	//Setup
	elems = this.cats.length;
	pitch = (cw-(2*this.xmargin)) / elems;	//horizontal interval
	//Track min/max values
	for (i = 0; i < elems; i++) {
		if (this.vals[i] > maxval) maxval = Math.round(this.vals[i]);
		if (this.vals[i] < minval) minval = Math.round(this.vals[i]);
	}

The above determines the spacing/pitch between bars based on the number of elements, the size of the canvas and the margins. It also determines what the minimum and maximum data values are.

	//Decide on Y-axis scale
	var range = maxval - minval;
	var absmax = maxval;
	if (Math.abs(minval) > maxval) absmax = Math.abs(minval);
	var a = Math.ceil(absmax/3);
	var b = a.toString().length;   //Length of interval value if split into 3
	//If estimated interval has a string length of more than 1 (i.e. decimal 10 or greater) then apply rounding to next lower power of 10
	if (b>1) a = parseInt(a / Math.pow(10,b-1))*Math.pow(10,b-1);
	var posticks = Math.ceil(maxval / a);
	var negticks = Math.ceil(-minval / a);
	this.yaxisint = a;
	this.yaxisposticks = posticks;
	this.yaxisnegticks = negticks;
	this.yaxisticks = posticks + negticks;

Firstly, determine the absolute maximum y-axis value irrespective of whether it is positive or negative. Next, try dividing this value by 3 to give a rough indication of what increment each y-axis label might represent. Apply an appropriate power of 10 rounding based on the magnitude of this number in order to make the y-axis labels more human-readable. Finally, store the number of labels required, for later use.

	//Should we abbreviate using thousands or millions?
	if (absmax > 10000000) {
		this.scalefactor = 1000000;
		this.scalesuffix = "M";
	} else if (absmax > 10000) {
		this.scalefactor = 1000;
		this.scalesuffix = "K";
	} else {
		this.scalefactor = 1;
		this.scalesuffix = "";
	}

Once we know the magnitude of the values we are dealing with it may be beneficial to apply scaling and units so that the y-axis labels are narrower and more readable. The above code tests for conditions where the y-axis (positive and/or negative) is sized beyond either ten thousand or a million, and applies a scale factor and unit suffix.

	switch(this.effect)  {
		//No special effect, just draw complete graph
		case "none":
			this.drawFrame();
			this.drawBars();
			this.drawXAxis();
			break;
		//Grow bars
		case "grow":
			barper = 0;
			this.animBars();
			break;
		//Twang bars
		case "twang":
			barper = 0;
			this.animphase = 1;
			this.animBars();
			break;
	}
 }

The final section decides how to proceed with drawing the graph based on the type of animation, if any, that has been chosen. If no animation has been selected then the main components of the graph are drawn once and processing stops. N.B. Draw order is important as the bars need to be in front of the background and horizontal guide lines but behind the x-axis labels (otherwise a negative value bar would cover the labels). The two animation types use 'animBars()', which is described below.

animBars()

Two animation types: 'grow' and 'twang' are currently supported.

this.animBars = function() {
  switch(this.effect)  {
    //Grow the bars in a linear fashion	
    case "grow":
      barper += 10;
      this.drawFrame();
      this.drawBars();
      this.drawXAxis();
       if (barper < 100) setTimeout(function() { that.animBars() }, 50);
       break;

As the name sugests, the 'grow' option starts at zero and increases the length of the bar by 10% during each iteration. If the length of the bar is less than 100% then the function is scheduled to be re-executed using the setTimeout command.

    //Grow the bars past their full value then shrink back below the
    //full value, before finally growing back to the final value.
    case "twang":
      this.drawFrame();
      switch(this.animphase) {
        //Initial growth
        case 1:
          barper += 15;
          this.drawBars();
          if (barper > 110)	this.animphase = 2;
            setTimeout(function(){ that.animBars()}, 50);
            break;
          //Shrink back
          case 2:
            barper -= 10;
            this.drawBars();
            if (barper < 100) this.animphase = 3;
		  setTimeout(function(){ that.animBars()}, 50);
          break;
        //Stretch to final
        case 3:
          barper = 100;
          this.drawBars();
          break;
	  }
      this.drawXAxis();
      break;
    }
}

The 'twang' code is a bit more complex in that it breaks the animation down into phases. During the first phase the bar grows in length as with the previous method. However, it is allowed to grow past 100% before triggering phase two, which then reduces the bar size. When the bar has shrunk to below 100% the final phase, three, increases it back to its full and final height. The idea being that there is a pleasant, elastic effect.

drawFrame()

This routine colours the background and draws the y-axis, incuding the tick marks and labels.

 this.drawFrame = function() {
	//Background
	var gradfill = c2d.createLinearGradient(0, 0, 0, ch);
	gradfill.addColorStop(0, this.bgcol1);
	gradfill.addColorStop(1, this.bgcol2);
	c2d.font="Bold 20px Calibri";
	c2d.textBaseline="middle";
	c2d.fillStyle = gradfill;
	c2d.fillRect(0, 0, cw, ch);
	//Graph title
	c2d.strokeStyle = this.fgcol;
	c2d.fillStyle = this.fgcol;
	var tw = c2d.measureText(this.title).width;
	c2d.fillText(this.title, (cw/2)-(tw/2), 15);

Create a two-colour gradient for the background and fill vertically. Measure the width of the graph title in the chosen font and draw, horizontally centred.

	//How far down the y-axis should the x-axis reside?
	this.xaxisypos = this.ymargin + (ch - 2 * this.ymargin) * (this.yaxisposticks / this.yaxisticks);
	//Vertical axis
	c2d.lineWidth = 2;
	c2d.beginPath();
	c2d.moveTo(this.xmargin, this.ymargin);
	c2d.lineTo(this.xmargin, ch - this.ymargin);
	c2d.stroke();

The vertical position of the x-axis is calculated based on whether there are any negative values or not. The y-axis is simply a vertical line.

	//Draw y-axis labels
	c2d.font="Bold 11px Calibri";
	var vint = (ch - (2 * this.ymargin)) / this.yaxisticks;
	var vindex = -this.yaxisnegticks;
	for (i = 0; i <= this.yaxisticks; i++) {
		var y = ch - this.ymargin - (i * vint);
		var ylabel = vindex * this.yaxisint / this.scalefactor;
		ylabel = ylabel.toString()+this.scalesuffix;
		tw = c2d.measureText(ylabel).width;
		//tick marks
		c2d.lineWidth = 2;
		c2d.strokeStyle = this.fgcol;
		c2d.beginPath();
		c2d.moveTo(this.xmargin+1, y);
		c2d.lineTo(this.xmargin-3, y);
		c2d.stroke();
		//horizontal guide lines
		c2d.lineWidth = 0.5;
		c2d.strokeStyle = "#888";
		c2d.beginPath();
		c2d.moveTo(this.xmargin+1, y);
		c2d.lineTo(cw - this.xmargin, y);
		c2d.stroke();
		c2d.fillText(ylabel, this.xmargin - tw - 5, y);
		vindex++;
	}
 }

For each label position draw a tick mark against the y-axis and a label with appropriate scaling and suffix as required. The width of each text label is measured so that an offset can be calculated, allowing each label to be right-aligned.

drawBars()

 this.drawBars = function() {
	var barw = pitch * this.bw;
	var graphrange = this.yaxisticks * this.yaxisint;
	//Draw value bars
	c2d.lineWidth = 1;
	c2d.strokeStyle = this.barcol1;
	//Left edge starting position of first bar.
	//equals half an interval plus half of the bar width percentage.
	var sp = this.xmargin - (pitch*(0.5+(0.5*this.bw)));	
	for (i = 0; i < elems; i++) {
		var barx = sp + ((i+1) * pitch);
		//Bar ratio = this bar versus total graph value range
		var br = this.vals[i] / graphrange;
		//Scale up to usable graph area
		var barh = br * (ch - 2 * this.ymargin);						
		//Scale by percentage (supports animation)
		barh = barh * (barper/100);
		//Position of top of bar to be drawn
		var bary = this.xaxisypos - barh;
		//Create a gradient fill appropriate to
		//the location and dimensions of the bar
		var gradfill = c2d.createLinearGradient(0, bary, 0, bary+barh);
		gradfill.addColorStop(0, this.barcol1);
		gradfill.addColorStop(1, this.barcol2);
		c2d.fillStyle = gradfill;
		//Draw this bar
		c2d.fillRect(barx, bary, barw, barh);
		c2d.strokeRect(barx, bary, barw, barh);
	}
 }

Bars are drawn equispaced according to the width of the canvas and number of values. A two-colour gradient is defined, local to the position of each bar.

drawXAxis()

 this.drawXAxis = function() {
	c2d.strokeStyle = this.fgcol;
	c2d.fillStyle = this.fgcol;
	c2d.lineWidth = 2;
	c2d.beginPath();
	c2d.moveTo(this.xmargin, this.xaxisypos);
	c2d.lineTo(cw-this.xmargin, this.xaxisypos);
	c2d.stroke();
	//X-axis labels
	c2d.font="Bold 11px Calibri";
	for (i = 0; i < elems; i++) {
		var x = this.xmargin- (pitch / 2) + ((i+1) * pitch);
		c2d.beginPath();
		c2d.moveTo(x, this.xaxisypos);
		c2d.lineTo(x, this.xaxisypos +3);
		c2d.stroke();
		tw = c2d.measureText(this.cats[i]).width;
		c2d.fillText(this.cats[i], x-(tw/2), this.xaxisypos +12);
	}
 }

For each item within the data series, draw a tick mark and centred label on the x-axis.

Source code

Graph.js