Compare commits

..

No commits in common. "master" and "namespace" have entirely different histories.

5 changed files with 79 additions and 253 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 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'],
@ -67,7 +67,7 @@ Springy 1.2+ also accepts JSON, see
] ]
}; };
var graph = new Springy.Graph(); var graph = new 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 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 +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 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,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 v2.0.0
* *
* 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,24 +24,22 @@
* 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 = {}; (function() {
// Enable strict mode for EC5 compatible browsers
"use strict";
// 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() { var Graph = Springy.Graph = function() {
this.nodeSet = {}; this.nodeSet = {};
@ -217,6 +215,7 @@
} }
this.detachNode(node); this.detachNode(node);
}; };
// removes edges associated with a given node // removes edges associated with a given node
@ -248,16 +247,6 @@
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];
} }
} }
@ -327,13 +316,11 @@
// ----------- // -----------
var Layout = Springy.Layout = {}; var Layout = Springy.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
@ -452,9 +439,6 @@
// 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);
if (point.v.magnitude() > this.maxSpeed) {
point.v = point.v.normalise().multiply(this.maxSpeed);
}
point.a = new Vector(0,0); point.a = new Vector(0,0);
}); });
}; };
@ -480,40 +464,39 @@
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! ;-)
Springy.requestAnimationFrame = __bind(this.requestAnimationFrame || Springy.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; this._stop = false;
if (onRenderStart !== undefined) { onRenderStart(); }
Springy.requestAnimationFrame(function step() { Springy.requestAnimationFrame(function step() {
t.tick(0.03); t.applyCoulombsLaw();
t.applyHookesLaw();
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._stop || t.totalEnergy() < 0.01) {
t._started = false; t._started = false;
if (onRenderStop !== undefined) { onRenderStop(); } if (done !== undefined) { done(); }
} else { } else {
Springy.requestAnimationFrame(step); Springy.requestAnimationFrame(step);
} }
@ -524,14 +507,6 @@
this._stop = true; this._stop = true;
} }
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 // 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};
@ -641,20 +616,12 @@
// 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 var Renderer = Springy.Renderer = function(layout, clear, drawEdge, drawNode) {
* @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);
} }
@ -663,17 +630,7 @@
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,9 +642,7 @@
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(); }
}, this.onRenderStop, this.onRenderStart);
}; };
Renderer.prototype.stop = function() { Renderer.prototype.stop = function() {
@ -721,15 +676,4 @@
} }
}; };
} }
}).call(this);
var isEmpty = function(obj) {
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
return false;
}
}
return true;
};
return Springy;
}));

View File

@ -27,20 +27,16 @@ Copyright (c) 2010 Dennis Hotson
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 Springy.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 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();
@ -61,14 +57,14 @@ jQuery.fn.springy = function(params) {
}); });
// 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 Springy.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;
@ -124,61 +120,25 @@ jQuery.fn.springy = function(params) {
dragged = null; dragged = null;
}); });
var getTextWidth = function(node) { Springy.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) {
return 16;
// 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 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() { Springy.Node.prototype.getHeight = function() {
var height; return 20;
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, var renderer = this.renderer = new Springy.Renderer(layout,
function clear() { function clear() {
@ -206,20 +166,16 @@ jQuery.fn.springy = function(params) {
} }
} }
//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);
@ -276,18 +232,9 @@ jQuery.fn.springy = function(params) {
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 +244,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 = (node.data.font !== undefined) ? node.data.font : nodeFont; ctx.font = "16px Verdana, sans-serif";
ctx.fillStyle = (node.data.color !== undefined) ? node.data.color : "#000000"; ctx.fillStyle = "#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();
} }
); );