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:
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:
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).