Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 85cbad0
Showing
5 changed files
with
248 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} |