Merge branch 'namespace'

* namespace:
  Replace references to `window` with `root`
  Updated README with namespace
  Bump the major version number
  Added a top level Springy namespace
This commit is contained in:
Dennis Hotson 2013-03-15 11:11:11 +00:00
commit 80108b0f31
7 changed files with 641 additions and 629 deletions

View File

@ -47,7 +47,7 @@ add nodes and edges to graph and springyui.js for the rendering example.
Springy 1.1+ supports simplified API for adding nodes and edges, see Springy 1.1+ supports simplified API for adding nodes and edges, see
[demo-simple.html](http://dhotson.github.com/springy/demo-simple.html): [demo-simple.html](http://dhotson.github.com/springy/demo-simple.html):
var graph = new Graph(); var graph = new Springy.Graph();
graph.addNodes('mark', 'higgs', 'other', 'etc'); graph.addNodes('mark', 'higgs', 'other', 'etc');
graph.addEdges( graph.addEdges(
['mark', 'higgs'], ['mark', 'higgs'],
@ -67,7 +67,7 @@ Springy 1.2+ also accepts JSON, see
] ]
}; };
var graph = new Graph(); var graph = new Springy.Graph();
graph.loadJSON(graphJSON); graph.loadJSON(graphJSON);
@ -80,7 +80,7 @@ things before you get started.
This is the basic graph API, you can create nodes and edges etc. This is the basic graph API, you can create nodes and edges etc.
// make a new graph // make a new graph
var graph = new Graph(); var graph = new Springy.Graph();
// make some nodes // make some nodes
var node1 = graph.newNode({label: '1'}); var node1 = graph.newNode({label: '1'});
@ -91,12 +91,12 @@ This is the basic graph API, you can create nodes and edges etc.
So now to draw this graph, lets make a layout object: So now to draw this graph, lets make a layout object:
var layout = new Layout.ForceDirected(graph, 400.0, 400.0, 0.5); var layout = new Springy.Layout.ForceDirected(graph, 400.0, 400.0, 0.5);
I've written a Renderer class, which will handle the rendering loop. I've written a Renderer class, which will handle the rendering loop.
You just need to provide some callbacks to do the actual drawing. You just need to provide some callbacks to do the actual drawing.
var renderer = new Renderer(layout, var renderer = new Springy.Renderer(layout,
function clear() { function clear() {
// code to clear screen // code to clear screen
}, },

View File

@ -22,7 +22,7 @@ var graphJSON = {
}; };
jQuery(function(){ jQuery(function(){
var graph = new Graph(); var graph = new Springy.Graph();
graph.loadJSON(graphJSON); graph.loadJSON(graphJSON);
var springy = jQuery('#springydemo').springy({ var springy = jQuery('#springydemo').springy({

View File

@ -112,7 +112,7 @@ Raphael.fn.connection = function (obj1, obj2, style) {
</script> </script>
<script> <script>
var graph = new Graph(); var graph = new Springy.Graph();
var dennis = graph.newNode({label: 'Dennis'}); var dennis = graph.newNode({label: 'Dennis'});
var michael = graph.newNode({label: 'Michael'}); var michael = graph.newNode({label: 'Michael'});
@ -166,16 +166,16 @@ function moveSet(set, x, y) {
} }
function doit() { function doit() {
var layout = new Layout.ForceDirected(graph, 640, 480.0, 0.5); var layout = new Springy.Layout.ForceDirected(graph, 640, 480.0, 0.5);
var r = Raphael("holder", 640, 480); var r = Raphael("holder", 640, 480);
// calculate bounding box of graph layout.. with ease-in // calculate bounding box of graph layout.. with ease-in
var currentBB = layout.getBoundingBox(); var currentBB = layout.getBoundingBox();
var targetBB = {bottomleft: new Vector(-2, -2), topright: new Vector(2, 2)}; var targetBB = {bottomleft: new Springy.Vector(-2, -2), topright: new Springy.Vector(2, 2)};
// auto adjusting bounding box // auto adjusting bounding box
Layout.requestAnimationFrame(function adjust() { Springy.requestAnimationFrame(function adjust() {
targetBB = layout.getBoundingBox(); targetBB = layout.getBoundingBox();
// current gets 20% closer to target every iteration // current gets 20% closer to target every iteration
currentBB = { currentBB = {
@ -185,7 +185,7 @@ function doit() {
.divide(10)) .divide(10))
}; };
Layout.requestAnimationFrame(adjust); Springy.requestAnimationFrame(adjust);
}); });
// convert to/from screen coordinates // convert to/from screen coordinates
@ -193,11 +193,11 @@ function doit() {
var size = currentBB.topright.subtract(currentBB.bottomleft); var size = currentBB.topright.subtract(currentBB.bottomleft);
var sx = p.subtract(currentBB.bottomleft).divide(size.x).x * r.width; var sx = p.subtract(currentBB.bottomleft).divide(size.x).x * r.width;
var sy = p.subtract(currentBB.bottomleft).divide(size.y).y * r.height; var sy = p.subtract(currentBB.bottomleft).divide(size.y).y * r.height;
return new Vector(sx, sy); return new Springy.Vector(sx, sy);
}; };
var renderer = new Renderer(layout, var renderer = new Springy.Renderer(layout,
function clear() { function clear() {
// code to clear screen // code to clear screen
}, },

View File

@ -4,7 +4,7 @@
<script src="springy.js"></script> <script src="springy.js"></script>
<script src="springyui.js"></script> <script src="springyui.js"></script>
<script> <script>
var graph = new Graph(); var graph = new Springy.Graph();
graph.addNodes('Dennis', 'Michael', 'Jessica', 'Timothy', 'Barbara') graph.addNodes('Dennis', 'Michael', 'Jessica', 'Timothy', 'Barbara')
graph.addNodes('Amphitryon', 'Alcmene', 'Iphicles', 'Heracles'); graph.addNodes('Amphitryon', 'Alcmene', 'Iphicles', 'Heracles');

View File

@ -4,7 +4,7 @@
<script src="springy.js"></script> <script src="springy.js"></script>
<script src="springyui.js"></script> <script src="springyui.js"></script>
<script> <script>
var graph = new Graph(); var graph = new Springy.Graph();
var dennis = graph.newNode({ var dennis = graph.newNode({
label: 'Dennis', label: 'Dennis',

View File

@ -1,5 +1,5 @@
/** /**
* Springy v1.2.0 * Springy v2.0.0
* *
* Copyright (c) 2010 Dennis Hotson * Copyright (c) 2010 Dennis Hotson
* *
@ -25,10 +25,23 @@
* OTHER DEALINGS IN THE SOFTWARE. * OTHER DEALINGS IN THE SOFTWARE.
*/ */
// Enable strict mode for EC5 compatible browsers (function() {
"use strict"; // Enable strict mode for EC5 compatible browsers
"use strict";
var Graph = function() { // Establish the root object, `window` in the browser, or `global` on the server.
var root = this;
// The top-level namespace. All public Springy classes and modules will
// be attached to this. Exported for both CommonJS and the browser.
var Springy;
if (typeof exports !== 'undefined') {
Springy = exports;
} else {
Springy = root.Springy = {};
}
var Graph = Springy.Graph = function() {
this.nodeSet = {}; this.nodeSet = {};
this.nodes = []; this.nodes = [];
this.edges = []; this.edges = [];
@ -37,31 +50,30 @@ var Graph = function() {
this.nextNodeId = 0; this.nextNodeId = 0;
this.nextEdgeId = 0; this.nextEdgeId = 0;
this.eventListeners = []; this.eventListeners = [];
}; };
var Node = function(id, data) { var Node = Springy.Node = function(id, data) {
this.id = id; this.id = id;
this.data = (data !== undefined) ? data : {}; this.data = (data !== undefined) ? data : {};
// Data fields used by layout algorithm in this file: // Data fields used by layout algorithm in this file:
// this.data.mass // this.data.mass
// Data used by default renderer in springyui.js // Data used by default renderer in springyui.js
// this.data.label // this.data.label
}; };
var Edge = function(id, source, target, data) { var Edge = Springy.Edge = function(id, source, target, data) {
this.id = id; this.id = id;
/** @type {Node} */
this.source = source; this.source = source;
this.target = target; this.target = target;
this.data = (data !== undefined) ? data : {}; this.data = (data !== undefined) ? data : {};
// Edge data field used by layout alorithm // Edge data field used by layout alorithm
// this.data.length // this.data.length
// this.data.type // this.data.type
}; };
Graph.prototype.addNode = function(node) { Graph.prototype.addNode = function(node) {
if (!(node.id in this.nodeSet)) { if (!(node.id in this.nodeSet)) {
this.nodes.push(node); this.nodes.push(node);
} }
@ -70,9 +82,9 @@ Graph.prototype.addNode = function(node) {
this.notify(); this.notify();
return node; return node;
}; };
Graph.prototype.addNodes = function() { Graph.prototype.addNodes = function() {
// accepts variable number of arguments, where each argument // accepts variable number of arguments, where each argument
// is a string that becomes both node identifier and label // is a string that becomes both node identifier and label
for (var i = 0; i < arguments.length; i++) { for (var i = 0; i < arguments.length; i++) {
@ -80,9 +92,9 @@ Graph.prototype.addNodes = function() {
var node = new Node(name, {label:name}); var node = new Node(name, {label:name});
this.addNode(node); this.addNode(node);
} }
}; };
Graph.prototype.addEdge = function(edge) { Graph.prototype.addEdge = function(edge) {
var exists = false; var exists = false;
this.edges.forEach(function(e) { this.edges.forEach(function(e) {
if (edge.id === e.id) { exists = true; } if (edge.id === e.id) { exists = true; }
@ -110,9 +122,9 @@ Graph.prototype.addEdge = function(edge) {
this.notify(); this.notify();
return edge; return edge;
}; };
Graph.prototype.addEdges = function() { Graph.prototype.addEdges = function() {
// accepts variable number of arguments, where each argument // accepts variable number of arguments, where each argument
// is a triple [nodeid1, nodeid2, attributes] // is a triple [nodeid1, nodeid2, attributes]
for (var i = 0; i < arguments.length; i++) { for (var i = 0; i < arguments.length; i++) {
@ -129,28 +141,28 @@ Graph.prototype.addEdges = function() {
this.newEdge(node1, node2, attr); this.newEdge(node1, node2, attr);
} }
}; };
Graph.prototype.newNode = function(data) { Graph.prototype.newNode = function(data) {
var node = new Node(this.nextNodeId++, data); var node = new Node(this.nextNodeId++, data);
this.addNode(node); this.addNode(node);
return node; return node;
}; };
Graph.prototype.newEdge = function(source, target, data) { Graph.prototype.newEdge = function(source, target, data) {
var edge = new Edge(this.nextEdgeId++, source, target, data); var edge = new Edge(this.nextEdgeId++, source, target, data);
this.addEdge(edge); this.addEdge(edge);
return edge; return edge;
}; };
// add nodes and edges from JSON object // add nodes and edges from JSON object
Graph.prototype.loadJSON = function(json) { Graph.prototype.loadJSON = function(json) {
/** /**
Springy's simple JSON format for graphs. Springy's simple JSON format for graphs.
historically, Springy uses separate lists historically, Springy uses separate lists
of nodes and edges: of nodes and edges:
{ {
"nodes": [ "nodes": [
@ -167,7 +179,7 @@ of nodes and edges:
] ]
} }
**/ **/
// parse if a string is passed (EC5+ browsers) // parse if a string is passed (EC5+ browsers)
if (typeof json == 'string' || json instanceof String) { if (typeof json == 'string' || json instanceof String) {
json = JSON.parse( json ); json = JSON.parse( json );
@ -177,21 +189,21 @@ of nodes and edges:
this.addNodes.apply(this, json['nodes']); this.addNodes.apply(this, json['nodes']);
this.addEdges.apply(this, json['edges']); this.addEdges.apply(this, json['edges']);
} }
} }
// find the edges from node1 to node2 // find the edges from node1 to node2
Graph.prototype.getEdges = function(node1, node2) { Graph.prototype.getEdges = function(node1, node2) {
if (node1.id in this.adjacency if (node1.id in this.adjacency
&& node2.id in this.adjacency[node1.id]) { && node2.id in this.adjacency[node1.id]) {
return this.adjacency[node1.id][node2.id]; return this.adjacency[node1.id][node2.id];
} }
return []; return [];
}; };
// remove a node and it's associated edges from the graph // remove a node and it's associated edges from the graph
Graph.prototype.removeNode = function(node) { Graph.prototype.removeNode = function(node) {
if (node.id in this.nodeSet) { if (node.id in this.nodeSet) {
delete this.nodeSet[node.id]; delete this.nodeSet[node.id];
} }
@ -204,10 +216,10 @@ Graph.prototype.removeNode = function(node) {
this.detachNode(node); this.detachNode(node);
}; };
// removes edges associated with a given node // removes edges associated with a given node
Graph.prototype.detachNode = function(node) { Graph.prototype.detachNode = function(node) {
var tmpEdges = this.edges.slice(); var tmpEdges = this.edges.slice();
tmpEdges.forEach(function(e) { tmpEdges.forEach(function(e) {
if (e.source.id === node.id || e.target.id === node.id) { if (e.source.id === node.id || e.target.id === node.id) {
@ -216,10 +228,10 @@ Graph.prototype.detachNode = function(node) {
}, this); }, this);
this.notify(); this.notify();
}; };
// remove a node and it's associated edges from the graph // remove a node and it's associated edges from the graph
Graph.prototype.removeEdge = function(edge) { Graph.prototype.removeEdge = function(edge) {
for (var i = this.edges.length - 1; i >= 0; i--) { for (var i = this.edges.length - 1; i >= 0; i--) {
if (this.edges[i].id === edge.id) { if (this.edges[i].id === edge.id) {
this.edges.splice(i, 1); this.edges.splice(i, 1);
@ -239,10 +251,10 @@ Graph.prototype.removeEdge = function(edge) {
} }
this.notify(); this.notify();
}; };
/* Merge a list of nodes and edges into the current graph. eg. /* Merge a list of nodes and edges into the current graph. eg.
var o = { var o = {
nodes: [ nodes: [
{id: 123, data: {type: 'user', userid: 123, displayname: 'aaa'}}, {id: 123, data: {type: 'user', userid: 123, displayname: 'aaa'}},
{id: 234, data: {type: 'user', userid: 234, displayname: 'bbb'}} {id: 234, data: {type: 'user', userid: 234, displayname: 'bbb'}}
@ -250,9 +262,9 @@ var o = {
edges: [ edges: [
{from: 0, to: 1, type: 'submitted_design', directed: true, data: {weight: }} {from: 0, to: 1, type: 'submitted_design', directed: true, data: {weight: }}
] ]
} }
*/ */
Graph.prototype.merge = function(data) { Graph.prototype.merge = function(data) {
var nodes = []; var nodes = [];
data.nodes.forEach(function(n) { data.nodes.forEach(function(n) {
nodes.push(this.addNode(new Node(n.id, n.data))); nodes.push(this.addNode(new Node(n.id, n.data)));
@ -271,40 +283,40 @@ Graph.prototype.merge = function(data) {
var edge = this.addEdge(new Edge(id, from, to, e.data)); var edge = this.addEdge(new Edge(id, from, to, e.data));
edge.data.type = e.type; edge.data.type = e.type;
}, this); }, this);
}; };
Graph.prototype.filterNodes = function(fn) { Graph.prototype.filterNodes = function(fn) {
var tmpNodes = this.nodes.slice(); var tmpNodes = this.nodes.slice();
tmpNodes.forEach(function(n) { tmpNodes.forEach(function(n) {
if (!fn(n)) { if (!fn(n)) {
this.removeNode(n); this.removeNode(n);
} }
}, this); }, this);
}; };
Graph.prototype.filterEdges = function(fn) { Graph.prototype.filterEdges = function(fn) {
var tmpEdges = this.edges.slice(); var tmpEdges = this.edges.slice();
tmpEdges.forEach(function(e) { tmpEdges.forEach(function(e) {
if (!fn(e)) { if (!fn(e)) {
this.removeEdge(e); this.removeEdge(e);
} }
}, this); }, this);
}; };
Graph.prototype.addGraphListener = function(obj) { Graph.prototype.addGraphListener = function(obj) {
this.eventListeners.push(obj); this.eventListeners.push(obj);
}; };
Graph.prototype.notify = function() { Graph.prototype.notify = function() {
this.eventListeners.forEach(function(obj){ this.eventListeners.forEach(function(obj){
obj.graphChanged(); obj.graphChanged();
}); });
}; };
// ----------- // -----------
var Layout = {}; var Layout = Springy.Layout = {};
Layout.ForceDirected = function(graph, stiffness, repulsion, damping) { Layout.ForceDirected = function(graph, stiffness, repulsion, damping) {
this.graph = graph; this.graph = graph;
this.stiffness = stiffness; // spring stiffness constant this.stiffness = stiffness; // spring stiffness constant
this.repulsion = repulsion; // repulsion constant this.repulsion = repulsion; // repulsion constant
@ -312,18 +324,18 @@ Layout.ForceDirected = function(graph, stiffness, repulsion, damping) {
this.nodePoints = {}; // keep track of points associated with nodes this.nodePoints = {}; // keep track of points associated with nodes
this.edgeSprings = {}; // keep track of springs associated with edges this.edgeSprings = {}; // keep track of springs associated with edges
}; };
Layout.ForceDirected.prototype.point = function(node) { Layout.ForceDirected.prototype.point = function(node) {
if (!(node.id in this.nodePoints)) { if (!(node.id in this.nodePoints)) {
var mass = (node.data.mass !== undefined) ? node.data.mass : 1.0; var mass = (node.data.mass !== undefined) ? node.data.mass : 1.0;
this.nodePoints[node.id] = new Layout.ForceDirected.Point(Vector.random(), mass); this.nodePoints[node.id] = new Layout.ForceDirected.Point(Vector.random(), mass);
} }
return this.nodePoints[node.id]; return this.nodePoints[node.id];
}; };
Layout.ForceDirected.prototype.spring = function(edge) { Layout.ForceDirected.prototype.spring = function(edge) {
if (!(edge.id in this.edgeSprings)) { if (!(edge.id in this.edgeSprings)) {
var length = (edge.data.length !== undefined) ? edge.data.length : 1.0; var length = (edge.data.length !== undefined) ? edge.data.length : 1.0;
@ -357,35 +369,35 @@ Layout.ForceDirected.prototype.spring = function(edge) {
} }
return this.edgeSprings[edge.id]; return this.edgeSprings[edge.id];
}; };
// callback should accept two arguments: Node, Point // callback should accept two arguments: Node, Point
Layout.ForceDirected.prototype.eachNode = function(callback) { Layout.ForceDirected.prototype.eachNode = function(callback) {
var t = this; var t = this;
this.graph.nodes.forEach(function(n){ this.graph.nodes.forEach(function(n){
callback.call(t, n, t.point(n)); callback.call(t, n, t.point(n));
}); });
}; };
// callback should accept two arguments: Edge, Spring // callback should accept two arguments: Edge, Spring
Layout.ForceDirected.prototype.eachEdge = function(callback) { Layout.ForceDirected.prototype.eachEdge = function(callback) {
var t = this; var t = this;
this.graph.edges.forEach(function(e){ this.graph.edges.forEach(function(e){
callback.call(t, e, t.spring(e)); callback.call(t, e, t.spring(e));
}); });
}; };
// callback should accept one argument: Spring // callback should accept one argument: Spring
Layout.ForceDirected.prototype.eachSpring = function(callback) { Layout.ForceDirected.prototype.eachSpring = function(callback) {
var t = this; var t = this;
this.graph.edges.forEach(function(e){ this.graph.edges.forEach(function(e){
callback.call(t, t.spring(e)); callback.call(t, t.spring(e));
}); });
}; };
// Physics stuff // Physics stuff
Layout.ForceDirected.prototype.applyCoulombsLaw = function() { Layout.ForceDirected.prototype.applyCoulombsLaw = function() {
this.eachNode(function(n1, point1) { this.eachNode(function(n1, point1) {
this.eachNode(function(n2, point2) { this.eachNode(function(n2, point2) {
if (point1 !== point2) if (point1 !== point2)
@ -400,9 +412,9 @@ Layout.ForceDirected.prototype.applyCoulombsLaw = function() {
} }
}); });
}); });
}; };
Layout.ForceDirected.prototype.applyHookesLaw = function() { Layout.ForceDirected.prototype.applyHookesLaw = function() {
this.eachSpring(function(spring){ this.eachSpring(function(spring){
var d = spring.point2.p.subtract(spring.point1.p); // the direction of the spring var d = spring.point2.p.subtract(spring.point1.p); // the direction of the spring
var displacement = spring.length - d.magnitude(); var displacement = spring.length - d.magnitude();
@ -412,35 +424,35 @@ Layout.ForceDirected.prototype.applyHookesLaw = function() {
spring.point1.applyForce(direction.multiply(spring.k * displacement * -0.5)); spring.point1.applyForce(direction.multiply(spring.k * displacement * -0.5));
spring.point2.applyForce(direction.multiply(spring.k * displacement * 0.5)); spring.point2.applyForce(direction.multiply(spring.k * displacement * 0.5));
}); });
}; };
Layout.ForceDirected.prototype.attractToCentre = function() { Layout.ForceDirected.prototype.attractToCentre = function() {
this.eachNode(function(node, point) { this.eachNode(function(node, point) {
var direction = point.p.multiply(-1.0); var direction = point.p.multiply(-1.0);
point.applyForce(direction.multiply(this.repulsion / 50.0)); point.applyForce(direction.multiply(this.repulsion / 50.0));
}); });
}; };
Layout.ForceDirected.prototype.updateVelocity = function(timestep) { Layout.ForceDirected.prototype.updateVelocity = function(timestep) {
this.eachNode(function(node, point) { this.eachNode(function(node, point) {
// Is this, along with updatePosition below, the only places that your // Is this, along with updatePosition below, the only places that your
// integration code exist? // integration code exist?
point.v = point.v.add(point.a.multiply(timestep)).multiply(this.damping); point.v = point.v.add(point.a.multiply(timestep)).multiply(this.damping);
point.a = new Vector(0,0); point.a = new Vector(0,0);
}); });
}; };
Layout.ForceDirected.prototype.updatePosition = function(timestep) { Layout.ForceDirected.prototype.updatePosition = function(timestep) {
this.eachNode(function(node, point) { this.eachNode(function(node, point) {
// Same question as above; along with updateVelocity, is this all of // Same question as above; along with updateVelocity, is this all of
// your integration code? // your integration code?
point.p = point.p.add(point.v.multiply(timestep)); point.p = point.p.add(point.v.multiply(timestep));
}); });
}; };
// Calculate the total kinetic energy of the system // Calculate the total kinetic energy of the system
Layout.ForceDirected.prototype.totalEnergy = function(timestep) { Layout.ForceDirected.prototype.totalEnergy = function(timestep) {
var energy = 0.0; var energy = 0.0;
this.eachNode(function(node, point) { this.eachNode(function(node, point) {
var speed = point.v.magnitude(); var speed = point.v.magnitude();
@ -448,29 +460,29 @@ Layout.ForceDirected.prototype.totalEnergy = function(timestep) {
}); });
return energy; return energy;
}; };
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; // stolen from coffeescript, thanks jashkenas! ;-) var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; // stolen from coffeescript, thanks jashkenas! ;-)
Layout.requestAnimationFrame = __bind(window.requestAnimationFrame || Springy.requestAnimationFrame = __bind(root.requestAnimationFrame ||
window.webkitRequestAnimationFrame || root.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame || root.mozRequestAnimationFrame ||
window.oRequestAnimationFrame || root.oRequestAnimationFrame ||
window.msRequestAnimationFrame || root.msRequestAnimationFrame ||
function(callback, element) { (function(callback, element) {
window.setTimeout(callback, 10); root.setTimeout(callback, 10);
}, window); }), root);
// start simulation // start simulation
Layout.ForceDirected.prototype.start = function(render, done) { Layout.ForceDirected.prototype.start = function(render, done) {
var t = this; var t = this;
if (this._started) return; if (this._started) return;
this._started = true; this._started = true;
this._stop = false; this._stop = false;
Layout.requestAnimationFrame(function step() { Springy.requestAnimationFrame(function step() {
t.applyCoulombsLaw(); t.applyCoulombsLaw();
t.applyHookesLaw(); t.applyHookesLaw();
t.attractToCentre(); t.attractToCentre();
@ -486,17 +498,17 @@ Layout.ForceDirected.prototype.start = function(render, done) {
t._started = false; t._started = false;
if (done !== undefined) { done(); } if (done !== undefined) { done(); }
} else { } else {
Layout.requestAnimationFrame(step); Springy.requestAnimationFrame(step);
} }
}); });
}; };
Layout.ForceDirected.prototype.stop = function() { Layout.ForceDirected.prototype.stop = function() {
this._stop = true; this._stop = true;
} }
// Find the nearest point to a particular position // Find the nearest point to a particular position
Layout.ForceDirected.prototype.nearest = function(pos) { Layout.ForceDirected.prototype.nearest = function(pos) {
var min = {node: null, point: null, distance: null}; var min = {node: null, point: null, distance: null};
var t = this; var t = this;
this.graph.nodes.forEach(function(n){ this.graph.nodes.forEach(function(n){
@ -509,10 +521,10 @@ Layout.ForceDirected.prototype.nearest = function(pos) {
}); });
return min; return min;
}; };
// returns [bottomleft, topright] // returns [bottomleft, topright]
Layout.ForceDirected.prototype.getBoundingBox = function() { Layout.ForceDirected.prototype.getBoundingBox = function() {
var bottomleft = new Vector(-2,-2); var bottomleft = new Vector(-2,-2);
var topright = new Vector(2,2); var topright = new Vector(2,2);
@ -534,91 +546,91 @@ Layout.ForceDirected.prototype.getBoundingBox = function() {
var padding = topright.subtract(bottomleft).multiply(0.07); // ~5% padding var padding = topright.subtract(bottomleft).multiply(0.07); // ~5% padding
return {bottomleft: bottomleft.subtract(padding), topright: topright.add(padding)}; return {bottomleft: bottomleft.subtract(padding), topright: topright.add(padding)};
}; };
// Vector // Vector
var Vector = function(x, y) { var Vector = Springy.Vector = function(x, y) {
this.x = x; this.x = x;
this.y = y; this.y = y;
}; };
Vector.random = function() { Vector.random = function() {
return new Vector(10.0 * (Math.random() - 0.5), 10.0 * (Math.random() - 0.5)); return new Vector(10.0 * (Math.random() - 0.5), 10.0 * (Math.random() - 0.5));
}; };
Vector.prototype.add = function(v2) { Vector.prototype.add = function(v2) {
return new Vector(this.x + v2.x, this.y + v2.y); return new Vector(this.x + v2.x, this.y + v2.y);
}; };
Vector.prototype.subtract = function(v2) { Vector.prototype.subtract = function(v2) {
return new Vector(this.x - v2.x, this.y - v2.y); return new Vector(this.x - v2.x, this.y - v2.y);
}; };
Vector.prototype.multiply = function(n) { Vector.prototype.multiply = function(n) {
return new Vector(this.x * n, this.y * n); return new Vector(this.x * n, this.y * n);
}; };
Vector.prototype.divide = function(n) { Vector.prototype.divide = function(n) {
return new Vector((this.x / n) || 0, (this.y / n) || 0); // Avoid divide by zero errors.. return new Vector((this.x / n) || 0, (this.y / n) || 0); // Avoid divide by zero errors..
}; };
Vector.prototype.magnitude = function() { Vector.prototype.magnitude = function() {
return Math.sqrt(this.x*this.x + this.y*this.y); return Math.sqrt(this.x*this.x + this.y*this.y);
}; };
Vector.prototype.normal = function() { Vector.prototype.normal = function() {
return new Vector(-this.y, this.x); return new Vector(-this.y, this.x);
}; };
Vector.prototype.normalise = function() { Vector.prototype.normalise = function() {
return this.divide(this.magnitude()); return this.divide(this.magnitude());
}; };
// Point // Point
Layout.ForceDirected.Point = function(position, mass) { Layout.ForceDirected.Point = function(position, mass) {
this.p = position; // position this.p = position; // position
this.m = mass; // mass this.m = mass; // mass
this.v = new Vector(0, 0); // velocity this.v = new Vector(0, 0); // velocity
this.a = new Vector(0, 0); // acceleration this.a = new Vector(0, 0); // acceleration
}; };
Layout.ForceDirected.Point.prototype.applyForce = function(force) { Layout.ForceDirected.Point.prototype.applyForce = function(force) {
this.a = this.a.add(force.divide(this.m)); this.a = this.a.add(force.divide(this.m));
}; };
// Spring // Spring
Layout.ForceDirected.Spring = function(point1, point2, length, k) { Layout.ForceDirected.Spring = function(point1, point2, length, k) {
this.point1 = point1; this.point1 = point1;
this.point2 = point2; this.point2 = point2;
this.length = length; // spring length at rest this.length = length; // spring length at rest
this.k = k; // spring constant (See Hooke's law) .. how stiff the spring is this.k = k; // spring constant (See Hooke's law) .. how stiff the spring is
}; };
// Layout.ForceDirected.Spring.prototype.distanceToPoint = function(point) // Layout.ForceDirected.Spring.prototype.distanceToPoint = function(point)
// { // {
// // hardcore vector arithmetic.. ohh yeah! // // hardcore vector arithmetic.. ohh yeah!
// // .. see http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment/865080#865080 // // .. see http://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment/865080#865080
// var n = this.point2.p.subtract(this.point1.p).normalise().normal(); // var n = this.point2.p.subtract(this.point1.p).normalise().normal();
// var ac = point.p.subtract(this.point1.p); // var ac = point.p.subtract(this.point1.p);
// return Math.abs(ac.x * n.x + ac.y * n.y); // return Math.abs(ac.x * n.x + ac.y * n.y);
// }; // };
// Renderer handles the layout rendering loop // Renderer handles the layout rendering loop
function Renderer(layout, clear, drawEdge, drawNode) { var Renderer = Springy.Renderer = function(layout, clear, drawEdge, drawNode) {
this.layout = layout; this.layout = layout;
this.clear = clear; this.clear = clear;
this.drawEdge = drawEdge; this.drawEdge = drawEdge;
this.drawNode = drawNode; this.drawNode = drawNode;
this.layout.graph.addGraphListener(this); this.layout.graph.addGraphListener(this);
} }
Renderer.prototype.graphChanged = function(e) { Renderer.prototype.graphChanged = function(e) {
this.start(); this.start();
}; };
Renderer.prototype.start = function() { Renderer.prototype.start = function() {
var t = this; var t = this;
this.layout.start(function render() { this.layout.start(function render() {
t.clear(); t.clear();
@ -631,15 +643,15 @@ Renderer.prototype.start = function() {
t.drawNode(node, point.p); t.drawNode(node, point.p);
}); });
}); });
}; };
Renderer.prototype.stop = function() { Renderer.prototype.stop = function() {
this.layout.stop(); this.layout.stop();
}; };
// Array.forEach implementation for IE support.. // Array.forEach implementation for IE support..
//https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach //https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach
if ( !Array.prototype.forEach ) { if ( !Array.prototype.forEach ) {
Array.prototype.forEach = function( callback, thisArg ) { Array.prototype.forEach = function( callback, thisArg ) {
var T, k; var T, k;
if ( this == null ) { if ( this == null ) {
@ -663,5 +675,5 @@ if ( !Array.prototype.forEach ) {
k++; k++;
} }
}; };
} }
}).call(this);

View File

@ -26,7 +26,7 @@ Copyright (c) 2010 Dennis Hotson
(function() { (function() {
jQuery.fn.springy = function(params) { jQuery.fn.springy = function(params) {
var graph = this.graph = params.graph || new Graph(); var graph = this.graph = params.graph || new Springy.Graph();
var stiffness = params.stiffness || 400.0; var stiffness = params.stiffness || 400.0;
var repulsion = params.repulsion || 400.0; var repulsion = params.repulsion || 400.0;
@ -36,14 +36,14 @@ jQuery.fn.springy = function(params) {
var canvas = this[0]; var canvas = this[0];
var ctx = canvas.getContext("2d"); var ctx = canvas.getContext("2d");
var layout = this.layout = new Layout.ForceDirected(graph, stiffness, repulsion, damping); var layout = this.layout = new Springy.Layout.ForceDirected(graph, stiffness, repulsion, damping);
// calculate bounding box of graph layout.. with ease-in // calculate bounding box of graph layout.. with ease-in
var currentBB = layout.getBoundingBox(); var currentBB = layout.getBoundingBox();
var targetBB = {bottomleft: new Vector(-2, -2), topright: new Vector(2, 2)}; var targetBB = {bottomleft: new Springy.Vector(-2, -2), topright: new Springy.Vector(2, 2)};
// auto adjusting bounding box // auto adjusting bounding box
Layout.requestAnimationFrame(function adjust() { Springy.requestAnimationFrame(function adjust() {
targetBB = layout.getBoundingBox(); targetBB = layout.getBoundingBox();
// current gets 20% closer to target every iteration // current gets 20% closer to target every iteration
currentBB = { currentBB = {
@ -53,7 +53,7 @@ jQuery.fn.springy = function(params) {
.divide(10)) .divide(10))
}; };
Layout.requestAnimationFrame(adjust); Springy.requestAnimationFrame(adjust);
}); });
// convert to/from screen coordinates // convert to/from screen coordinates
@ -61,14 +61,14 @@ jQuery.fn.springy = function(params) {
var size = currentBB.topright.subtract(currentBB.bottomleft); var size = currentBB.topright.subtract(currentBB.bottomleft);
var sx = p.subtract(currentBB.bottomleft).divide(size.x).x * canvas.width; var sx = p.subtract(currentBB.bottomleft).divide(size.x).x * canvas.width;
var sy = p.subtract(currentBB.bottomleft).divide(size.y).y * canvas.height; var sy = p.subtract(currentBB.bottomleft).divide(size.y).y * canvas.height;
return new Vector(sx, sy); return new Springy.Vector(sx, sy);
}; };
fromScreen = function(s) { fromScreen = function(s) {
var size = currentBB.topright.subtract(currentBB.bottomleft); var size = currentBB.topright.subtract(currentBB.bottomleft);
var px = (s.x / canvas.width) * size.x + currentBB.bottomleft.x; var px = (s.x / canvas.width) * size.x + currentBB.bottomleft.x;
var py = (s.y / canvas.height) * size.y + currentBB.bottomleft.y; var py = (s.y / canvas.height) * size.y + currentBB.bottomleft.y;
return new Vector(px, py); return new Springy.Vector(px, py);
}; };
// half-assed drag and drop // half-assed drag and drop
@ -120,7 +120,7 @@ jQuery.fn.springy = function(params) {
dragged = null; dragged = null;
}); });
Node.prototype.getWidth = function() { Springy.Node.prototype.getWidth = function() {
var text = (this.data.label !== undefined) ? this.data.label : this.id; var text = (this.data.label !== undefined) ? this.data.label : this.id;
if (this._width && this._width[text]) if (this._width && this._width[text])
return this._width[text]; return this._width[text];
@ -136,11 +136,11 @@ jQuery.fn.springy = function(params) {
return width; return width;
}; };
Node.prototype.getHeight = function() { Springy.Node.prototype.getHeight = function() {
return 20; return 20;
}; };
var renderer = this.renderer = new Renderer(layout, var renderer = this.renderer = new Springy.Renderer(layout,
function clear() { function clear() {
ctx.clearRect(0,0,canvas.width,canvas.height); ctx.clearRect(0,0,canvas.width,canvas.height);
}, },
@ -150,7 +150,7 @@ jQuery.fn.springy = function(params) {
var x2 = toScreen(p2).x; var x2 = toScreen(p2).x;
var y2 = toScreen(p2).y; var y2 = toScreen(p2).y;
var direction = new Vector(x2-x1, y2-y1); var direction = new Springy.Vector(x2-x1, y2-y1);
var normal = direction.normal().normalise(); var normal = direction.normal().normalise();
var from = graph.getEdges(edge.source, edge.target); var from = graph.getEdges(edge.source, edge.target);
@ -290,7 +290,7 @@ jQuery.fn.springy = function(params) {
return false; return false;
} }
return new Vector(p1.x + ua * (p2.x - p1.x), p1.y + ua * (p2.y - p1.y)); return new Springy.Vector(p1.x + ua * (p2.x - p1.x), p1.y + ua * (p2.y - p1.y));
} }
function intersect_line_box(p1, p2, p3, w, h) { function intersect_line_box(p1, p2, p3, w, h) {