software

HTML5 Canvas Demo -- Graph Viewer/Editor

I do a lot of work with graphs on a day to day basis, and with my interest in HTML5 and the platform independence around it, I decided to take a bit of my free time to try to design a very simple graph viewer/editor that would work cross platform in HTML5. The code below was tested in Chrome 16, IE 9, and Firefox 10.
Goals:

  • Be able to display 100 nodes connected with edges using the HTML5 Canvas element
  • Be able to drag and move nodes
  • Be able to add nodes
  • Be able to add edges
  • Concentrate on key concepts and not worry so much about tertiary functionality or looks (mostly z-indexing and where the edges connect to the node)

The results of this experiment can be viewed on my code page at http://code.brianmokeefe.com/Graph.html. Most of the functionality is encapsulated in the Javascript file at http://code.brianmokeefe.com/Graph.js. Note: for the latter file, you probably want to right-click and save target, as it is a scripting file. The JS file is licensed under the Apache License, Version 2.0.

Some key components to note:

  1. There are two main objects used here: (a) the StateMachine to manage the states of the page (Select, Add Node, Add Edge) as controlled by the toggle buttons at the top, and (b) the Graph.Manager object that controls the graph functionality, including events received from the DOM.
  2. The HTML5 canvas object is created dynamically in the Graph.Manager constructor.
  3. The Graph.Manager.paint() call manages drawing the node and edges (via the draw() function in the Graph.Node and Graph.Edge objects)
  4. It doesn't work on my Android phone yet, except for adding a node (probably because I didn't do anything with touch).

NOTE: In the code snippets below, anything denoted as "this" is scoped by the object that contains it. It is not intended as cut-and-paste code without understanding the entire class from the Javascript file above.

In the Graph.Manager constructor, this section creates the HTML5 canvas object, and its resulting 2D context.

	// Set the HTML5 Canvas object
	this.canvas = document.createElement('canvas');
	this.canvas.height = height;
	this.canvas.width = width;
	this.canvas.style.position = 'relative';
	this.context = this.canvas.getContext('2d');

	// Add the canvas to the container element
	this.container.appendChild(this.canvas);

The Context object is the important piece to drawing in the Canvas object. It is used here to draw a node (in this case a circle centered at x,y with a given radius. 0, 2*Math.PI signifies drawing a full circle -- an arc from 0 to 360 degrees).

	context.save();
	
	// Draw node
	context.strokeStyle = 'rgb(0,0,0)';
	context.fillStyle = this.fillColor;
	context.beginPath();
	context.arc(this.position.x, this.position.y, this.radius, 0, 2 * Math.PI, false);
	context.closePath();
	context.stroke();
	context.fill();
		
	context.restore();

This code draws an edge between the two nodes. It has very simple logic to connect the edge to the node at 0, 90, 180, or 270 degrees depending upon the position of the nodes in relation to each other (e.g., basically, if the two nodes are further apart horizontally, connect them on the left and right sides; if they are further apart vertically, connect them on the top or bottom, as appropriate).

Graph.Edge.prototype.draw = function(context) {
	context.save();

	var deltaX = this.parentNode.getPosition().x - this.childNode.getPosition().x;
	var deltaY = this.parentNode.getPosition().y - this.childNode.getPosition().y;
	var coordStart, coordStop;
	
	if (Math.abs(deltaX) >= Math.abs(deltaY)) {
		coordStart = (deltaX < 0) ? this.parentNode.getRight() : this.parentNode.getLeft();
		coordStop = (deltaX < 0) ? this.childNode.getLeft() : this.childNode.getRight();
	} else {
		coordStart = (deltaY < 0) ? this.parentNode.getTop() : this.parentNode.getBottom();
		coordStop = (deltaY < 0) ? this.childNode.getBottom() : this.childNode.getTop();		
	}
	
	// Draw line and arrow from parent to child
	context.strokeStyle = 'rgb(0,0,0)';
	context.fillStyle = 'rgb(0,0,0)';
	
	// Draw the line 
	context.beginPath();
	context.moveTo(coordStart.x,coordStart.y);
	context.lineTo(coordStop.x,coordStop.y);
	context.closePath();
	context.stroke();
	context.fill();
	
	context.restore();
};

There is also code in the HTML page to visualize the edge as two nodes are being connected. The paintWithCallback function does the same as the paint() function above, except it calls the function defined as the parameter immediately after the paint. In this case, it allows us to draw the edge as the mouse drag from the start to the end occurs. The code is very similar to the Edge code above, except it is drawing from the node to the mouse pointer, instead of from one node to the next.

manager.paintWithCallback(function(context) {
	var deltaX = _stateMachine.data.getNode().getPosition().x - evt.offsetX;
	var deltaY = _stateMachine.data.getNode().getPosition().y - evt.offsetY;
	var coordStart;
	var coordStop = new Graph.Coordinates(evt.offsetX, evt.offsetY);
			
	if (Math.abs(deltaX) >= Math.abs(deltaY)) {
		coordStart = (deltaX < 0) ? _stateMachine.data.getNode().getRight() : _stateMachine.data.getNode().getLeft();
	} else {
		coordStart = (deltaY < 0) ? _stateMachine.data.getNode().getTop() : _stateMachine.data.getNode().getBottom();
	}

	context.save();

	// Draw line and arrow from parent to child
	context.strokeStyle = 'rgb(0,0,0)';
	context.fillStyle = 'rgb(0,0,0)';
	context.beginPath();
	context.moveTo(coordStart.x,coordStart.y);
	context.lineTo(coordStop.x,coordStop.y);
	context.closePath();
	context.stroke();
	context.fill();
			
	context.restore();		
});

Finally, because the varying Javascript implementations differ in the way that they create the event DOM object, this class normalizes the implementations for simplification. Basically, if the event object doesn't have offsetX, then the same value is created by subtracting clientX from the event target's left and top offset.

Graph.BrowserEvent = function(evt) {
	this.offsetX = evt.offsetX ? evt.offsetX : (evt.clientX - evt.currentTarget.offsetLeft);
	this.offsetY = evt.offsetY ? evt.offsetY : (evt.clientY - evt.currentTarget.offsetTop);
	this.type = evt.type;
	this.whoAmI = "Graph.BrowserEvent";
};

I hope this is useful. If you have any questions or comments, please drop me an email at the address below, or via Twitter (@brianmokeefe).

Syndicate content