Compare commits

..

1 Commits

Author SHA1 Message Date
Dennis Hotson
7ee84bf6e0 Fixed a few issues reported by jshint 2013-03-12 10:43:45 +00:00
10 changed files with 690 additions and 978 deletions

View File

@ -21,9 +21,7 @@ Try to imagine it as a bunch of springs connected to each other.
Demo Demo
---- ----
[basic](http://dhotson.github.com/springy/demo.html) [demo](http://dhotson.github.com/springy/demo.html)
| [simplified API](http://dhotson.github.com/springy/demo-simple.html)
| [JSON API](http://dhotson.github.com/springy/demo-json.html)
Getting Started Getting Started
@ -45,9 +43,9 @@ See [demo.html](http://dhotson.github.com/springy/demo.html) for the way to
add nodes and edges to graph and springyui.js for the rendering example. 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): [demo2.html](http://techtonik.github.com/springy/demo2.html):
var graph = new Springy.Graph(); var graph = new Graph();
graph.addNodes('mark', 'higgs', 'other', 'etc'); graph.addNodes('mark', 'higgs', 'other', 'etc');
graph.addEdges( graph.addEdges(
['mark', 'higgs'], ['mark', 'higgs'],
@ -55,22 +53,6 @@ Springy 1.1+ supports simplified API for adding nodes and edges, see
['mark', 'other'] ['mark', 'other']
); );
Springy 1.2+ also accepts JSON, see
[demo-json.html](http://dhotson.github.com/springy/demo-json.html):
graphJSON = {
"nodes": ["mark", "higgs", "other", "etc"],
"edges": [
["mark", "higgs"],
["mark", "etc"],
["mark", "other"]
]
};
var graph = new Springy.Graph();
graph.loadJSON(graphJSON);
Advanced Drawing Advanced Drawing
---- ----
@ -80,7 +62,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 Springy.Graph(); var graph = new Graph();
// make some nodes // make some nodes
var node1 = graph.newNode({label: '1'}); var node1 = graph.newNode({label: '1'});
@ -91,12 +73,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 Springy.Layout.ForceDirected(graph, 400.0, 400.0, 0.5); var layout = new 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 Springy.Renderer(layout, var renderer = new Renderer(layout,
function clear() { function clear() {
// code to clear screen // code to clear screen
}, },

View File

@ -1,17 +0,0 @@
{
"name": "Springy",
"main": "springy.js",
"version": "2.7.1",
"homepage": "https://github.com/dhotson/springy",
"authors": [
"Dennis Hotson <dennis@99designs.com>"
],
"description": "A force directed graph layout algorithm",
"keywords": [
"graph",
"layout",
"visualization",
"physics"
],
"license": "MIT"
}

View File

@ -1,38 +0,0 @@
<html>
<body>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<script src="springy.js"></script>
<script src="springyui.js"></script>
<script>
var graphJSON = {
"nodes": [
"Amphitryon",
"Alcmene",
"Iphicles",
"Heracles"
],
"edges": [
["Amphitryon", "Alcmene"],
["Alcmene", "Amphitryon"],
["Amphitryon", "Iphicles"],
["Amphitryon", "Heracles"]
]
};
jQuery(function(){
var graph = new Springy.Graph();
graph.loadJSON(graphJSON);
var springy = jQuery('#springydemo').springy({
graph: graph
});
});
</script>
<canvas id="springydemo" width="640" height="480" />
</body>
</html>

View File

@ -112,7 +112,7 @@ Raphael.fn.connection = function (obj1, obj2, style) {
</script> </script>
<script> <script>
var graph = new Springy.Graph(); var graph = new 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 Springy.Layout.ForceDirected(graph, 640, 480.0, 0.5); var layout = new 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 Springy.Vector(-2, -2), topright: new Springy.Vector(2, 2)}; var targetBB = {bottomleft: new Vector(-2, -2), topright: new Vector(2, 2)};
// auto adjusting bounding box // auto adjusting bounding box
Springy.requestAnimationFrame(function adjust() { Layout.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))
}; };
Springy.requestAnimationFrame(adjust); Layout.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 Springy.Vector(sx, sy); return new Vector(sx, sy);
}; };
var renderer = new Springy.Renderer(layout, var renderer = new Renderer(layout,
function clear() { function clear() {
// code to clear screen // code to clear screen
}, },

View File

@ -1,33 +0,0 @@
<html>
<body>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<script src="springy.js"></script>
<script src="springyui.js"></script>
<script>
var graph = new Springy.Graph();
graph.addNodes('Dennis', 'Michael', 'Jessica', 'Timothy', 'Barbara')
graph.addNodes('Amphitryon', 'Alcmene', 'Iphicles', 'Heracles');
graph.addEdges(
['Dennis', 'Michael', {color: '#00A0B0', label: 'Foo bar'}],
['Michael', 'Dennis', {color: '#6A4A3C'}],
['Michael', 'Jessica', {color: '#CC333F'}],
['Jessica', 'Barbara', {color: '#EB6841'}],
['Michael', 'Timothy', {color: '#EDC951'}],
['Amphitryon', 'Alcmene', {color: '#7DBE3C'}],
['Alcmene', 'Amphitryon', {color: '#BE7D3C'}],
['Amphitryon', 'Iphicles'],
['Amphitryon', 'Heracles'],
['Barbara', 'Timothy', {color: '#6A4A3C'}]
);
jQuery(function(){
var springy = jQuery('#springydemo').springy({
graph: graph
});
});
</script>
<canvas id="springydemo" width="640" height="480" />
</body>
</html>

View File

@ -4,12 +4,9 @@
<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 Springy.Graph(); var graph = new Graph();
var dennis = graph.newNode({ var dennis = graph.newNode({label: 'Dennis'});
label: 'Dennis',
ondoubleclick: function() { console.log("Hello!"); }
});
var michael = graph.newNode({label: 'Michael'}); var michael = graph.newNode({label: 'Michael'});
var jessica = graph.newNode({label: 'Jessica'}); var jessica = graph.newNode({label: 'Jessica'});
var timothy = graph.newNode({label: 'Timothy'}); var timothy = graph.newNode({label: 'Timothy'});
@ -32,7 +29,7 @@ graph.newEdge(dennis, bianca, {color: '#CC333F'});
graph.newEdge(bianca, monty, {color: '#EB6841'}); graph.newEdge(bianca, monty, {color: '#EB6841'});
jQuery(function(){ jQuery(function(){
var springy = window.springy = jQuery('#springydemo').springy({ var springy = jQuery('#springydemo').springy({
graph: graph, graph: graph,
nodeSelected: function(node){ nodeSelected: function(node){
console.log('Node selected: ' + JSON.stringify(node.data)); console.log('Node selected: ' + JSON.stringify(node.data));

32
demo2.html Normal file
View File

@ -0,0 +1,32 @@
<html>
<body>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js"></script>
<script src="springy.js"></script>
<script src="springyui.js"></script>
<script>
var graph = new Graph();
graph.addNodes('Dennis', 'Michael', 'Jessica', 'Timothy', 'Barbara', 'Franklin')
graph.addNodes('Monty', 'James');
graph.addEdges(
['Dennis', 'Michael', {color: '#00A0B0', label: 'Foo bar'}],
['Michael', 'Dennis', {color: '#6A4A3C'}],
['Michael', 'Jessica', {color: '#CC333F'}],
['Jessica', 'Barbara', {color: '#EB6841'}],
['Michael', 'Timothy', {color: '#EDC951'}],
['Franklin', 'Monty', {color: '#7DBE3C'}],
['Dennis', 'Monty', {color: '#000000'}],
['Monty', 'James'],
['Barbara', 'Timothy', {color: '#6A4A3C'}]
);
jQuery(function(){
var springy = jQuery('#springydemo').springy({
graph: graph
});
});
</script>
<canvas id="springydemo" width="640" height="480" />
</body>
</html>

View File

@ -1,21 +0,0 @@
{
"name": "springy",
"version": "2.7.1",
"description": "A force directed graph layout algorithm in JavaScript.",
"main": "springy.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git://github.com/dhotson/springy.git"
},
"keywords": [
"graph",
"layout",
"visualization"
],
"author": "Dennis Hotson <dennis.hotson@gmail.com>",
"license": "MIT",
"readmeFilename": "README.mkdn"
}

View File

@ -1,7 +1,7 @@
/** /**
* Springy v2.7.1 * Springy v1.1.1
* *
* Copyright (c) 2010-2013 Dennis Hotson * Copyright (c) 2010 Dennis Hotson
* *
* Permission is hereby granted, free of charge, to any person * Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation * obtaining a copy of this software and associated documentation
@ -24,26 +24,13 @@
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE. * OTHER DEALINGS IN THE SOFTWARE.
*/ */
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD. Register as an anonymous module.
define(function () {
return (root.returnExportsGlobal = factory());
});
} else if (typeof exports === 'object') {
// Node. Does not work with strict CommonJS, but
// only CommonJS-like enviroments that support module.exports,
// like Node.
module.exports = factory();
} else {
// Browser globals
root.Springy = factory();
}
}(this, function() {
var Springy = {}; /*jshint globalstrict:true, browser:true */
var Graph = Springy.Graph = function() { // Enable strict mode for EC5 compatible browsers
'use strict';
var Graph = function() {
this.nodeSet = {}; this.nodeSet = {};
this.nodes = []; this.nodes = [];
this.edges = []; this.edges = [];
@ -52,30 +39,30 @@
this.nextNodeId = 0; this.nextNodeId = 0;
this.nextEdgeId = 0; this.nextEdgeId = 0;
this.eventListeners = []; this.eventListeners = [];
}; };
var Node = Springy.Node = function(id, data) { var 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 = Springy.Edge = function(id, source, target, data) { var Edge = function(id, source, target, data) {
this.id = id; this.id = id;
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);
} }
@ -84,19 +71,19 @@
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++) {
var name = arguments[i]; var name = arguments[i];
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; }
@ -124,88 +111,50 @@
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++) {
var e = arguments[i]; var e = arguments[i];
var node1 = this.nodeSet[e[0]]; var node1 = this.nodeSet[e[0]];
if (node1 == undefined) { if (node1 === undefined) {
throw new TypeError("invalid node name: " + e[0]); throw new TypeError('invalid node name: ' + e[0]);
} }
var node2 = this.nodeSet[e[1]]; var node2 = this.nodeSet[e[1]];
if (node2 == undefined) { if (node2 === undefined) {
throw new TypeError("invalid node name: " + e[1]); throw new TypeError('invalid node name: ' + e[1]);
} }
var attr = e[2]; var attr = e[2];
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;
}; };
// find the edges from node1 to node2
// add nodes and edges from JSON object Graph.prototype.getEdges = function(node1, node2) {
Graph.prototype.loadJSON = function(json) { if (node1.id in this.adjacency && node2.id in this.adjacency[node1.id]) {
/**
Springy's simple JSON format for graphs.
historically, Springy uses separate lists
of nodes and edges:
{
"nodes": [
"center",
"left",
"right",
"up",
"satellite"
],
"edges": [
["center", "left"],
["center", "right"],
["center", "up"]
]
}
**/
// parse if a string is passed (EC5+ browsers)
if (typeof json == 'string' || json instanceof String) {
json = JSON.parse( json );
}
if ('nodes' in json || 'edges' in json) {
this.addNodes.apply(this, json['nodes']);
this.addEdges.apply(this, json['edges']);
}
}
// find the edges from node1 to node2
Graph.prototype.getEdges = function(node1, node2) {
if (node1.id in this.adjacency
&& 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];
} }
@ -217,10 +166,11 @@
} }
this.detachNode(node); this.detachNode(node);
};
// removes edges associated with a given node };
Graph.prototype.detachNode = function(node) {
// removes edges associated with a given 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) {
@ -229,10 +179,10 @@
}, 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);
@ -243,29 +193,19 @@
for (var y in this.adjacency[x]) { for (var y in this.adjacency[x]) {
var edges = this.adjacency[x][y]; var edges = this.adjacency[x][y];
for (var j=edges.length - 1; j>=0; j--) { for (var j = edges.length - 1; j >= 0; j--) {
if (this.adjacency[x][y][j].id === edge.id) { if (this.adjacency[x][y][j].id === edge.id) {
this.adjacency[x][y].splice(j, 1); this.adjacency[x][y].splice(j, 1);
} }
} }
// Clean up empty edge arrays
if (this.adjacency[x][y].length == 0) {
delete this.adjacency[x][y];
}
}
// Clean up empty objects
if (isEmpty(this.adjacency[x])) {
delete this.adjacency[x];
} }
} }
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'}}
@ -273,9 +213,9 @@
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)));
@ -285,70 +225,69 @@
var from = nodes[e.from]; var from = nodes[e.from];
var to = nodes[e.to]; var to = nodes[e.to];
var id = (e.directed) var id = (e.directed) ?
? (id = e.type + "-" + from.id + "-" + to.id) (id = e.type + '-' + from.id + '-' + to.id) :
: (from.id < to.id) // normalise id for non-directed edges (from.id < to.id) ? // normalise id for non-directed edges
? e.type + "-" + from.id + "-" + to.id (e.type + '-' + from.id + '-' + to.id) :
: e.type + "-" + to.id + "-" + from.id; (e.type + '-' + to.id + '-' + from.id);
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 = Springy.Layout = {}; var Layout = {};
Layout.ForceDirected = function(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed) { 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
this.damping = damping; // velocity damping factor this.damping = damping; // velocity damping factor
this.minEnergyThreshold = minEnergyThreshold || 0.01; //threshold used to determine render stop
this.maxSpeed = maxSpeed || Infinity; // nodes aren't allowed to exceed this speed
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;
@ -362,18 +301,22 @@
}, this); }, this);
if (existingSpring !== false) { if (existingSpring !== false) {
return new Layout.ForceDirected.Spring(existingSpring.point1, existingSpring.point2, 0.0, 0.0); return new Layout.ForceDirected.Spring(
existingSpring.point1,
existingSpring.point2, 0.0, 0.0);
} }
var to = this.graph.getEdges(edge.target, edge.source); var to = this.graph.getEdges(edge.target, edge.source);
from.forEach(function(e){ from.forEach(function(e) {
if (existingSpring === false && e.id in this.edgeSprings) { if (existingSpring === false && e.id in this.edgeSprings) {
existingSpring = this.edgeSprings[e.id]; existingSpring = this.edgeSprings[e.id];
} }
}, this); }, this);
if (existingSpring !== false) { if (existingSpring !== false) {
return new Layout.ForceDirected.Spring(existingSpring.point2, existingSpring.point1, 0.0, 0.0); return new Layout.ForceDirected.Spring(
existingSpring.point2,
existingSpring.point1, 0.0, 0.0);
} }
this.edgeSprings[edge.id] = new Layout.ForceDirected.Spring( this.edgeSprings[edge.id] = new Layout.ForceDirected.Spring(
@ -382,93 +325,99 @@
} }
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)
{ {
var d = point1.p.subtract(point2.p); var d = point1.p.subtract(point2.p);
var distance = d.magnitude() + 0.1; // avoid massive forces at small distances (and divide by zero) // avoid massive forces at small distances (and divide by zero)
var distance = d.magnitude() + 0.1;
var direction = d.normalise(); var direction = d.normalise();
// apply force to each end point // apply force to each end point
point1.applyForce(direction.multiply(this.repulsion).divide(distance * distance * 0.5)); point1.applyForce(
point2.applyForce(direction.multiply(this.repulsion).divide(distance * distance * -0.5)); direction
.multiply(this.repulsion)
.divide(distance * distance * 0.5));
point2.applyForce(
direction
.multiply(this.repulsion)
.divide(distance * distance * -0.5));
} }
}); });
}); });
}; };
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 dir = spring.point2.p.subtract(spring.point1.p);
var displacement = spring.length - d.magnitude(); var displacement = spring.length - dir.magnitude();
var direction = d.normalise(); var direction = dir.normalise();
// apply force to each end point // apply force to each end point
spring.point1.applyForce(direction.multiply(spring.k * displacement * -0.5)); spring.point1.applyForce(
spring.point2.applyForce(direction.multiply(spring.k * displacement * 0.5)); direction
}); .multiply(spring.k * displacement * -0.5));
};
Layout.ForceDirected.prototype.attractToCentre = function() { spring.point2.applyForce(
direction
.multiply(spring.k * displacement * 0.5));
});
};
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
// 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);
if (point.v.magnitude() > this.maxSpeed) { point.a = new Vector(0, 0);
point.v = point.v.normalise().multiply(this.maxSpeed);
}
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
// 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();
@ -476,67 +425,56 @@
}); });
return energy; return energy;
}; };
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; // stolen from coffeescript, thanks jashkenas! ;-) // stolen from coffeescript, thanks jashkenas! ;-)
var __bind = function(fn, me) { return function() {
return fn.apply(me, arguments);
}; };
Springy.requestAnimationFrame = __bind(this.requestAnimationFrame || Layout.requestAnimationFrame = __bind(window.requestAnimationFrame ||
this.webkitRequestAnimationFrame || window.webkitRequestAnimationFrame ||
this.mozRequestAnimationFrame || window.mozRequestAnimationFrame ||
this.oRequestAnimationFrame || window.oRequestAnimationFrame ||
this.msRequestAnimationFrame || window.msRequestAnimationFrame ||
(function(callback, element) { (function(callback, element) {
this.setTimeout(callback, 10); window.setTimeout(callback, 10);
}), this); }), window);
/** // start simulation
* Start simulation if it's not running already. Layout.ForceDirected.prototype.start = function(render, done) {
* In case it's running then the call is ignored, and none of the callbacks passed is ever executed.
*/
Layout.ForceDirected.prototype.start = function(render, onRenderStop, onRenderStart) {
var t = this; var t = this;
if (this._started) return; if (this._started) return;
this._started = true; this._started = true;
this._stop = false;
if (onRenderStart !== undefined) { onRenderStart(); } Layout.requestAnimationFrame(function step() {
t.applyCoulombsLaw();
Springy.requestAnimationFrame(function step() { t.applyHookesLaw();
t.tick(0.03); t.attractToCentre();
t.updateVelocity(0.03);
t.updatePosition(0.03);
if (render !== undefined) { if (render !== undefined) {
render(); render();
} }
// stop simulation when energy of the system goes below a threshold // stop simulation when energy of the system goes below a threshold
if (t._stop || t.totalEnergy() < t.minEnergyThreshold) { if (t.totalEnergy() < 0.01) {
t._started = false; t._started = false;
if (onRenderStop !== undefined) { onRenderStop(); } if (done !== undefined) { done(); }
} else { } else {
Springy.requestAnimationFrame(step); Layout.requestAnimationFrame(step);
} }
}); });
}; };
Layout.ForceDirected.prototype.stop = function() { // Find the nearest point to a particular position
this._stop = true; Layout.ForceDirected.prototype.nearest = function(pos) {
}
Layout.ForceDirected.prototype.tick = function(timestep) {
this.applyCoulombsLaw();
this.applyHookesLaw();
this.attractToCentre();
this.updateVelocity(timestep);
this.updatePosition(timestep);
};
// Find the nearest point to a particular position
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) {
var point = t.point(n); var point = t.point(n);
var distance = point.p.subtract(pos).magnitude(); var distance = point.p.subtract(pos).magnitude();
@ -546,12 +484,12 @@
}); });
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);
this.eachNode(function(n, point) { this.eachNode(function(n, point) {
if (point.p.x < bottomleft.x) { if (point.p.x < bottomleft.x) {
@ -570,110 +508,88 @@
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 = Springy.Vector = function(x, y) { var 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) // Renderer handles the layout rendering loop
// { function Renderer(layout, clear, drawEdge, drawNode) {
// // hardcore vector arithmetic.. ohh yeah!
// // .. 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 ac = point.p.subtract(this.point1.p);
// return Math.abs(ac.x * n.x + ac.y * n.y);
// };
/**
* Renderer handles the layout rendering loop
* @param onRenderStop optional callback function that gets executed whenever rendering stops.
* @param onRenderStart optional callback function that gets executed whenever rendering starts.
* @param onRenderFrame optional callback function that gets executed after each frame is rendered.
*/
var Renderer = Springy.Renderer = function(layout, clear, drawEdge, drawNode, onRenderStop, onRenderStart, onRenderFrame) {
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.onRenderStop = onRenderStop;
this.onRenderStart = onRenderStart;
this.onRenderFrame = onRenderFrame;
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() {
* Starts the simulation of the layout in use.
*
* Note that in case the algorithm is still or already running then the layout that's in use
* might silently ignore the call, and your optional <code>done</code> callback is never executed.
* At least the built-in ForceDirected layout behaves in this way.
*
* @param done An optional callback function that gets executed when the springy algorithm stops,
* either because it ended or because stop() was called.
*/
Renderer.prototype.start = function(done) {
var t = this; var t = this;
this.layout.start(function render() { this.layout.start(function render() {
t.clear(); t.clear();
@ -685,51 +601,34 @@
t.layout.eachNode(function(node, point) { t.layout.eachNode(function(node, point) {
t.drawNode(node, point.p); t.drawNode(node, point.p);
}); });
});
};
if (t.onRenderFrame !== undefined) { t.onRenderFrame(); } // Array.forEach implementation for IE support..
}, this.onRenderStop, this.onRenderStart); //https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach
}; if (!Array.prototype.forEach) {
Array.prototype.forEach = function(callback, thisArg) {
Renderer.prototype.stop = function() {
this.layout.stop();
};
// Array.forEach implementation for IE support..
//https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Array/forEach
if ( !Array.prototype.forEach ) {
Array.prototype.forEach = function( callback, thisArg ) {
var T, k; var T, k;
if ( this == null ) { if (this === null) {
throw new TypeError( " this is null or not defined" ); throw new TypeError(' this is null or not defined');
} }
var O = Object(this); var O = Object(this);
var len = O.length >>> 0; // Hack to convert O.length to a UInt32 var len = O.length >>> 0; // Hack to convert O.length to a UInt32
if ( {}.toString.call(callback) != "[object Function]" ) { if ({}.toString.call(callback) != '[object Function]') {
throw new TypeError( callback + " is not a function" ); throw new TypeError(callback + ' is not a function');
} }
if ( thisArg ) { if (thisArg) {
T = thisArg; T = thisArg;
} }
k = 0; k = 0;
while( k < len ) { while (k < len) {
var kValue; var kValue;
if ( k in O ) { if (k in O) {
kValue = O[ k ]; kValue = O[k];
callback.call( T, kValue, k, O ); callback.call(T, kValue, k, O);
} }
k++; k++;
} }
}; };
} }
var isEmpty = function(obj) {
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
return false;
}
}
return true;
};
return Springy;
}));

View File

@ -26,53 +26,49 @@ Copyright (c) 2010 Dennis Hotson
(function() { (function() {
jQuery.fn.springy = function(params) { jQuery.fn.springy = function(params) {
var graph = this.graph = params.graph || new Springy.Graph(); var graph = this.graph = params.graph || new Graph();
var nodeFont = "16px Verdana, sans-serif";
var edgeFont = "8px Verdana, sans-serif";
var stiffness = params.stiffness || 400.0; var stiffness = params.stiffness || 400.0;
var repulsion = params.repulsion || 400.0; var repulsion = params.repulsion || 400.0;
var damping = params.damping || 0.5; var damping = params.damping || 0.5;
var minEnergyThreshold = params.minEnergyThreshold || 0.00001;
var nodeSelected = params.nodeSelected || null; var nodeSelected = params.nodeSelected || null;
var nodeImages = {};
var edgeLabelsUpright = true;
var canvas = this[0]; var canvas = this[0];
var ctx = canvas.getContext("2d"); var ctx = canvas.getContext('2d');
var layout = this.layout = new Springy.Layout.ForceDirected(graph, stiffness, repulsion, damping, minEnergyThreshold); var layout = this.layout = new 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 Springy.Vector(-2, -2), topright: new Springy.Vector(2, 2)}; var targetBB = {bottomleft: new Vector(-2, -2), topright: new Vector(2, 2)};
// auto adjusting bounding box // auto adjusting bounding box
Springy.requestAnimationFrame(function adjust() { Layout.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 = {
bottomleft: currentBB.bottomleft.add( targetBB.bottomleft.subtract(currentBB.bottomleft) bottomleft: currentBB.bottomleft.add(targetBB.bottomleft.subtract(currentBB.bottomleft)
.divide(10)), .divide(10)),
topright: currentBB.topright.add( targetBB.topright.subtract(currentBB.topright) topright: currentBB.topright.add(targetBB.topright.subtract(currentBB.topright)
.divide(10)) .divide(10))
}; };
Springy.requestAnimationFrame(adjust); Layout.requestAnimationFrame(adjust);
}); });
// convert to/from screen coordinates // convert to/from screen coordinates
var toScreen = function(p) { toScreen = function(p) {
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 Springy.Vector(sx, sy); return new Vector(sx, sy);
}; };
var 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 Springy.Vector(px, py); return new Vector(px, py);
}; };
// half-assed drag and drop // half-assed drag and drop
@ -81,6 +77,8 @@ jQuery.fn.springy = function(params) {
var dragged = null; var dragged = null;
jQuery(canvas).mousedown(function(e) { jQuery(canvas).mousedown(function(e) {
jQuery('.actions').hide();
var pos = jQuery(this).offset(); var pos = jQuery(this).offset();
var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top}); var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top});
selected = nearest = dragged = layout.nearest(p); selected = nearest = dragged = layout.nearest(p);
@ -96,17 +94,6 @@ jQuery.fn.springy = function(params) {
renderer.start(); renderer.start();
}); });
// Basic double click handler
jQuery(canvas).dblclick(function(e) {
var pos = jQuery(this).offset();
var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top});
selected = layout.nearest(p);
node = selected.node;
if (node && node.data && node.data.ondoubleclick) {
node.data.ondoubleclick();
}
});
jQuery(canvas).mousemove(function(e) { jQuery(canvas).mousemove(function(e) {
var pos = jQuery(this).offset(); var pos = jQuery(this).offset();
var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top}); var p = fromScreen({x: e.pageX - pos.left, y: e.pageY - pos.top});
@ -120,69 +107,33 @@ jQuery.fn.springy = function(params) {
renderer.start(); renderer.start();
}); });
jQuery(window).bind('mouseup',function(e) { jQuery(window).bind('mouseup', function(e) {
dragged = null; dragged = null;
}); });
var getTextWidth = function(node) { Node.prototype.getWidth = function() {
var text = (node.data.label !== undefined) ? node.data.label : node.id; var text = (this.data.label !== undefined) ? this.data.label : this.id;
if (node._width && node._width[text]) if (this._width && this._width[text])
return node._width[text]; return this._width[text];
ctx.save(); ctx.save();
ctx.font = (node.data.font !== undefined) ? node.data.font : nodeFont; ctx.font = '16px Verdana, sans-serif';
var width = ctx.measureText(text).width; var width = ctx.measureText(text).width + 10;
ctx.restore(); ctx.restore();
node._width || (node._width = {}); this._width || (this._width = {});
node._width[text] = width; this._width[text] = width;
return width; return width;
}; };
var getTextHeight = function(node) { Node.prototype.getHeight = function() {
return 16; return 20;
// In a more modular world, this would actually read the font size, but I think leaving it a constant is sufficient for now.
// If you change the font size, I'd adjust this too.
}; };
var getImageWidth = function(node) { var renderer = new Renderer(layout,
var width = (node.data.image.width !== undefined) ? node.data.image.width : nodeImages[node.data.image.src].object.width;
return width;
}
var getImageHeight = function(node) {
var height = (node.data.image.height !== undefined) ? node.data.image.height : nodeImages[node.data.image.src].object.height;
return height;
}
Springy.Node.prototype.getHeight = function() {
var height;
if (this.data.image == undefined) {
height = getTextHeight(this);
} else {
if (this.data.image.src in nodeImages && nodeImages[this.data.image.src].loaded) {
height = getImageHeight(this);
} else {height = 10;}
}
return height;
}
Springy.Node.prototype.getWidth = function() {
var width;
if (this.data.image == undefined) {
width = getTextWidth(this);
} else {
if (this.data.image.src in nodeImages && nodeImages[this.data.image.src].loaded) {
width = getImageWidth(this);
} else {width = 10;}
}
return width;
}
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);
}, },
function drawEdge(edge, p1, p2) { function drawEdge(edge, p1, p2) {
var x1 = toScreen(p1).x; var x1 = toScreen(p1).x;
@ -190,7 +141,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 Springy.Vector(x2-x1, y2-y1); var direction = new 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);
@ -200,28 +151,24 @@ jQuery.fn.springy = function(params) {
// Figure out edge's position in relation to other edges between the same nodes // Figure out edge's position in relation to other edges between the same nodes
var n = 0; var n = 0;
for (var i=0; i<from.length; i++) { for (var i = 0; i < from.length; i++) {
if (from[i].id === edge.id) { if (from[i].id === edge.id) {
n = i; n = i;
} }
} }
//change default to 10.0 to allow text fit between edges var spacing = 6.0;
var spacing = 12.0;
// Figure out how far off center the line should be drawn // Figure out how far off center the line should be drawn
var offset = normal.multiply(-((total - 1) * spacing)/2.0 + (n * spacing)); var offset = normal.multiply(-((total - 1) * spacing) / 2.0 + (n * spacing));
var paddingX = 6;
var paddingY = 6;
var s1 = toScreen(p1).add(offset); var s1 = toScreen(p1).add(offset);
var s2 = toScreen(p2).add(offset); var s2 = toScreen(p2).add(offset);
var boxWidth = edge.target.getWidth() + paddingX; var boxWidth = edge.target.getWidth();
var boxHeight = edge.target.getHeight() + paddingY; var boxHeight = edge.target.getHeight();
var intersection = intersect_line_box(s1, s2, {x: x2-boxWidth/2.0, y: y2-boxHeight/2.0}, boxWidth, boxHeight); var intersection = intersect_line_box(s1, s2, {x: x2 - boxWidth / 2.0, y: y2 - boxHeight / 2.0}, boxWidth, boxHeight);
if (!intersection) { if (!intersection) {
intersection = s2; intersection = s2;
@ -272,22 +219,13 @@ jQuery.fn.springy = function(params) {
// label // label
if (edge.data.label !== undefined) { if (edge.data.label !== undefined) {
text = edge.data.label text = edge.data.label;
ctx.save(); ctx.save();
ctx.textAlign = "center"; ctx.textAlign = 'center';
ctx.textBaseline = "top"; ctx.textBaseline = 'top';
ctx.font = (edge.data.font !== undefined) ? edge.data.font : edgeFont; ctx.font = '10px Helvetica, sans-serif';
ctx.fillStyle = stroke; ctx.fillStyle = '#5BA6EC';
var angle = Math.atan2(s2.y - s1.y, s2.x - s1.x); ctx.fillText(text, (x1 + x2) / 2, (y1 + y2) / 2);
var displacement = -8;
if (edgeLabelsUpright && (angle > Math.PI/2 || angle < -Math.PI/2)) {
displacement = 8;
angle += Math.PI;
}
var textPos = s1.add(s2).divide(2).add(normal.multiply(displacement));
ctx.translate(textPos.x, textPos.y);
ctx.rotate(angle);
ctx.fillText(text, 0,-2);
ctx.restore(); ctx.restore();
} }
@ -297,57 +235,30 @@ jQuery.fn.springy = function(params) {
ctx.save(); ctx.save();
// Pulled out the padding aspect sso that the size functions could be used in multiple places var boxWidth = node.getWidth();
// These should probably be settable by the user (and scoped higher) but this suffices for now var boxHeight = node.getHeight();
var paddingX = 6;
var paddingY = 6;
var contentWidth = node.getWidth();
var contentHeight = node.getHeight();
var boxWidth = contentWidth + paddingX;
var boxHeight = contentHeight + paddingY;
// clear background // clear background
ctx.clearRect(s.x - boxWidth/2, s.y - boxHeight/2, boxWidth, boxHeight); ctx.clearRect(s.x - boxWidth / 2, s.y - 10, boxWidth, 20);
// fill background // fill background
if (selected !== null && selected.node !== null && selected.node.id === node.id) { if (selected !== null && nearest.node !== null && selected.node.id === node.id) {
ctx.fillStyle = "#FFFFE0"; ctx.fillStyle = '#FFFFE0';
} else if (nearest !== null && nearest.node !== null && nearest.node.id === node.id) { } else if (nearest !== null && nearest.node !== null && nearest.node.id === node.id) {
ctx.fillStyle = "#EEEEEE"; ctx.fillStyle = '#EEEEEE';
} else { } else {
ctx.fillStyle = "#FFFFFF"; ctx.fillStyle = '#FFFFFF';
} }
ctx.fillRect(s.x - boxWidth/2, s.y - boxHeight/2, boxWidth, boxHeight); ctx.fillRect(s.x - boxWidth / 2, s.y - 10, boxWidth, 20);
if (node.data.image == undefined) { ctx.textAlign = 'left';
ctx.textAlign = "left"; ctx.textBaseline = 'top';
ctx.textBaseline = "top"; ctx.font = '16px Verdana, sans-serif';
ctx.font = (node.data.font !== undefined) ? node.data.font : nodeFont; ctx.fillStyle = '#000000';
ctx.fillStyle = (node.data.color !== undefined) ? node.data.color : "#000000"; ctx.font = '16px Verdana, sans-serif';
var text = (node.data.label !== undefined) ? node.data.label : node.id; var text = (node.data.label !== undefined) ? node.data.label : node.id;
ctx.fillText(text, s.x - contentWidth/2, s.y - contentHeight/2); ctx.fillText(text, s.x - boxWidth / 2 + 5, s.y - 8);
} else {
// Currently we just ignore any labels if the image object is set. One might want to extend this logic to allow for both, or other composite nodes.
var src = node.data.image.src; // There should probably be a sanity check here too, but un-src-ed images aren't exaclty a disaster.
if (src in nodeImages) {
if (nodeImages[src].loaded) {
// Our image is loaded, so it's safe to draw
ctx.drawImage(nodeImages[src].object, s.x - contentWidth/2, s.y - contentHeight/2, contentWidth, contentHeight);
}
}else{
// First time seeing an image with this src address, so add it to our set of image objects
// Note: we index images by their src to avoid making too many duplicates
nodeImages[src] = {};
var img = new Image();
nodeImages[src].object = img;
img.addEventListener("load", function () {
// HTMLImageElement objects are very finicky about being used before they are loaded, so we set a flag when it is done
nodeImages[src].loaded = true;
});
img.src = src;
}
}
ctx.restore(); ctx.restore();
} }
); );
@ -356,21 +267,21 @@ jQuery.fn.springy = function(params) {
// helpers for figuring out where to draw arrows // helpers for figuring out where to draw arrows
function intersect_line_line(p1, p2, p3, p4) { function intersect_line_line(p1, p2, p3, p4) {
var denom = ((p4.y - p3.y)*(p2.x - p1.x) - (p4.x - p3.x)*(p2.y - p1.y)); var denom = ((p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y));
// lines are parallel // lines are parallel
if (denom === 0) { if (denom === 0) {
return false; return false;
} }
var ua = ((p4.x - p3.x)*(p1.y - p3.y) - (p4.y - p3.y)*(p1.x - p3.x)) / denom; var ua = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / denom;
var ub = ((p2.x - p1.x)*(p1.y - p3.y) - (p2.y - p1.y)*(p1.x - p3.x)) / denom; var ub = ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / denom;
if (ua < 0 || ua > 1 || ub < 0 || ub > 1) { if (ua < 0 || ua > 1 || ub < 0 || ub > 1) {
return false; return false;
} }
return new Springy.Vector(p1.x + ua * (p2.x - p1.x), p1.y + ua * (p2.y - p1.y)); return new 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) {
@ -389,6 +300,6 @@ jQuery.fn.springy = function(params) {
} }
return this; return this;
} };
})(); })();