Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
jackrusher committed Apr 3, 2010
0 parents commit 85cbad0
Show file tree
Hide file tree
Showing 5 changed files with 248 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README
@@ -0,0 +1,5 @@
The simplest possible force directed graph layout algorithm implemented as a Javascript library that uses SVG objects. It is in no way meant to be production library, just a demonstration of force-directed layout as an adjunct to a conversation on that topic.

demo1 -- super simple drawing of the taxonomic position of hedgehogs
demo2 -- a sentence diagram, shows custom colors and edge styles
demo3 -- populating a graph with data acquired via jQuery
44 changes: 44 additions & 0 deletions demo1.xhtml
@@ -0,0 +1,44 @@
<?xml version="1.0"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Simple Graph Layout: Hedgepigs</title>
<style>
body { background: black; font: 13px/13px helvetica, sans-serif; }
#canvas { display: block; margin: auto; width: 960px; height: 720px; border: #111 1px solid; }
</style>
<script type="text/javascript" src="graph.js"></script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" id="canvas"></svg>
<script type="text/javascript">
<![CDATA[
var g = new Graph("canvas", 960, 700 );

g.createVertex("Animalia");
g.createVertex("Chordata");
g.createEdge("Chordata","Animalia");
g.createVertex("Mammalia");
g.createEdge("Mammalia","Chordata");
g.createVertex("Erinaceomorpha");
g.createEdge("Erinaceomorpha","Mammalia");
g.createVertex("Erinaceidae");
g.createEdge("Erinaceidae","Erinaceomorpha");
g.createVertex("Erinaceinae");
g.createEdge("Erinaceinae","Erinaceidae");

g.createVertex("Atelerix");
g.createEdge("Atelerix","Erinaceinae");
g.createVertex("Erinaceus");
g.createEdge("Erinaceus","Erinaceinae");
g.createVertex("Hemiechinus");
g.createEdge("Hemiechinus","Erinaceinae");
g.createVertex("Mesechinus");
g.createEdge("Mesechinus","Erinaceinae");
g.createVertex("Paraechinus");
g.createEdge("Paraechinus","Erinaceinae");

g.go();
]]>
</script>
</body>
</html>
47 changes: 47 additions & 0 deletions demo2.xhtml
@@ -0,0 +1,47 @@
<?xml version="1.0"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Simple Graph Layout: Sentence Diagram</title>
<style>
body { background: black; font: 13px/13px helvetica, sans-serif; }
#canvas { display: block; margin: auto; width: 960px; height: 720px; border: #111 1px solid; }
</style>
<script type="text/javascript" src="graph.js"></script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" id="canvas"></svg>
<script type="text/javascript">
<![CDATA[
var g = new Graph("canvas", 960, 700 );

// reduce repulsion and spring length for more compact layout
g.repulsion = g.repulsion / 8;
g.spring_length = 1;

g.createVertex("time","#000066"); // set node color
g.createVertex("flies");
g.createVertex("like");
g.createVertex("an");
g.createVertex("arrow");
g.createVertex("but");
g.createVertex("fruit");
g.createVertex("flies (2)");
g.createVertex("like (2)");
g.createVertex("a");
g.createVertex("banana");

g.createEdge("time","flies");
g.createEdge("flies","like");
g.createEdge("like","arrow");
g.createEdge("arrow","an");
g.createEdge("flies","but","stroke-dasharray: 2, 2; stroke: #666; stroke-width: 3;"); // custom edge style
g.createEdge("but","flies (2)");
g.createEdge("flies (2)","fruit");
g.createEdge("flies (2)","like (2)");
g.createEdge("like (2)","banana");
g.createEdge("banana","a");
g.go();
]]>
</script>
</body>
</html>
36 changes: 36 additions & 0 deletions demo3.xhtml
@@ -0,0 +1,36 @@
<?xml version="1.0"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Simple Graph Layout: Last.fm Similar Bands</title>
<style>
body { background: black; font: 13px/13px helvetica, sans-serif; }
#canvas { display: block; margin: auto; width: 960px; height: 720px; border: #111 1px solid; }
</style>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script type="text/javascript" src="graph.js"></script>
</head>
<body>
<svg xmlns="http://www.w3.org/2000/svg" id="canvas"></svg>
<script type="text/javascript">
<![CDATA[
var g = new Graph("canvas", 960, 720 );
g.repulsion = g.repulsion * 2; // stronger repulsion
g.spring_length = 50; // longer springs

// fetch the ten most similar artists to this one
var bandname = "Sparklehorse";
$.getJSON("http://ws.audioscrobbler.com/2.0/?method=artist.getSimilar&artist=" + bandname + "&api_key=6b33ba424a7817ac02bdac996895eba4&limit=8&format=json&callback=?", function(data) {
rootname = data.similarartists['@attr']['artist'];
g.createVertex(rootname, "#006600");
$.each(data.similarartists.artist, function(i,item){
g.createVertex(item.name);
// custom edge style, width based on similarity
style = "stroke: #666; stroke-width: " + item.match * 5 +";";
g.createEdge(item.name,rootname,style);
});
});
g.go();
]]>
</script>
</body>
</html>
116 changes: 116 additions & 0 deletions graph.js
@@ -0,0 +1,116 @@
function Graph( canvas_name, width, height ) {
this.svg = "http://www.w3.org/2000/svg";
this.canvas = document.getElementById(canvas_name);
this.width = width;
this.height = height;
this.canvas.style.width = width + "px";
this.canvas.style.height = height + "px";
this.vertices = {};
this.forcex = {};
this.forcey = {};
this.stepsize = 0.0005;
this.iteration = 0;
this.task = null;

// tunables to adjust the layout
this.repulsion = 200000; // repulsion constant, adjust for wider/narrower spacing
this.spring_length = 20; // base resting length of springs
}

Graph.prototype.createVertex = function( name, color ) { // XXX -- should support separate id and name
// create an SVG rectangle, attach additional attributed to it
var vertex = document.createElementNS(this.svg, "rect");
if( color === undefined ) {
color = "#222";
}
vertex.setAttribute("style", "fill: "+color+"; stroke-width: 1px;");
vertex.setAttribute("rx", "10px"); // round the edges
// random placement with a 10% margin at the edges
vertex.posx = Math.random() * (this.width * 0.8) + (this.width * 0.1);
vertex.posy = (Math.random() * (this.height * 0.8)) + (this.height * 0.1);
vertex.setAttribute("x", vertex.posx );
vertex.setAttribute("y", vertex.posy );
vertex.edges = new Array();
this.canvas.appendChild(vertex);

// text label
vertex.name = name;
vertex.textLabel = document.createElementNS(this.svg, "text");
vertex.textLabel.setAttribute("style", "fill: #fff; stroke-width: 1px;");
vertex.textLabel.appendChild( document.createTextNode( name ) );
this.canvas.appendChild( vertex.textLabel );

// get the size of the rectangle from the text label's bounding box
vertex.h = vertex.textLabel.getBBox().height + 10;
vertex.w = vertex.textLabel.getBBox().width + 10;
vertex.setAttribute("height", vertex.h + "px");
vertex.setAttribute("width", vertex.w + "px");

this.vertices[name] = vertex;
}

Graph.prototype.createEdge = function( a, b, style ) {
var line = document.createElementNS(this.svg, "path");
if( style === undefined ) {
style = "stroke: #444; stroke-width: 3px;";
}
line.setAttribute("style", style);
this.canvas.insertBefore(line, this.canvas.firstChild);
this.vertices[a].edges[b] = { "dest" : b, "line": line };
this.vertices[b].edges[a] = { "dest" : a, "line": line };
}

Graph.prototype.updateLayout = function() {
for (i in this.vertices) {
this.forcex[i] = 0;
this.forcey[i] = 0;
for (j in this.vertices) {
if( i !== j ) {
// using rectangle's center, bounding box would be better
var deltax = this.vertices[j].posx - this.vertices[i].posx;
var deltay = this.vertices[j].posy - this.vertices[i].posy;
var distance = Math.sqrt(deltax * deltax + deltay * deltay);

// Coulomb's law -- repulsion varies inversely with distance
this.forcex[i] -= (this.repulsion / Math.pow( distance, 2 )) * deltax;
this.forcey[i] -= (this.repulsion / Math.pow( distance, 2 )) * deltay;

// spring force along edges, follows Hooke's law
if( this.vertices[i].edges[j] ) {
this.forcex[i] += (distance - this.spring_length) * deltax;
this.forcey[i] += (distance - this.spring_length) * deltay;
}
}
}
}
for (i in this.vertices) {
// update rectangles
this.vertices[i].posx += this.forcex[i] * this.stepsize;
this.vertices[i].posy += this.forcey[i] * this.stepsize;
this.vertices[i].setAttribute("x", this.vertices[i].posx );
this.vertices[i].setAttribute("y", this.vertices[i].posy );
// update labels
this.vertices[i].textLabel.setAttribute("x", this.vertices[i].posx + 5 + "px");
this.vertices[i].textLabel.setAttribute("y", this.vertices[i].posy + (2*this.vertices[i].h/3 )+ "px");
// update edges
for (j in this.vertices[i].edges) {
this.vertices[i].edges[j].line.setAttribute("d", "M"+(this.vertices[i].posx+(this.vertices[i].w/2))+","+(this.vertices[i].posy+(this.vertices[i].h/2))+" L"+(this.vertices[this.vertices[i].edges[j].dest].posx+(this.vertices[this.vertices[i].edges[j].dest].w/2))+" "+(this.vertices[this.vertices[i].edges[j].dest].posy+(this.vertices[this.vertices[i].edges[j].dest].h/2)));
}
}
this.iteration++;
if( this.iteration > 300 ) // XXX -- should watch for rest state, not just quit after N iterations
this.quit();
}
Graph.prototype.go = function() {
// already running
if (this.task) {
return;
}
obj = this;
this.iteration = 0;
this.task = window.setInterval(function(){ obj.updateLayout(); }, 1);
}
Graph.prototype.quit = function() {
window.clearInterval(this.task);
this.task = null;
}

0 comments on commit 85cbad0

Please sign in to comment.