Compare commits

...

29 Commits

Author SHA1 Message Date
Dennis Hotson
9654b64f85
Merge pull request #98 from iangilman/render
Changed onRender to onRenderFrame
2018-03-06 11:32:22 +11:00
Ian Gilman
8beaeff267 Changed onRender to onRenderFrame 2018-03-05 16:25:05 -08:00
Dennis Hotson
73ecbc773c
Merge pull request #97 from iangilman/render
Added onRender callback for after each frame
2018-03-01 23:54:53 +11:00
Ian Gilman
73fd49c58a Added onRender callback for after each frame 2018-02-28 16:33:23 -08:00
Dennis Hotson
6158298c9e Merge pull request #85 from mwcz/max-speed
add maxSpeed parameter to Layout.ForceDirected constructor
2016-12-29 10:22:56 +11:00
mwcz
e490969ea0 add maxSpeed parameter to Layout.ForceDirected constructor 2016-07-19 14:38:01 -04:00
Dennis Hotson
559a400331 Merge pull request #70 from shigeruNakajima/node_color
Add nodes a 'color' property.
2014-12-08 10:52:18 +11:00
Dennis Hotson
921b3e83e5 Fix whitespace 2014-12-08 10:39:11 +11:00
Dennis Hotson
6f99308b44 Small fix to UMD wrapper 2014-12-08 10:38:19 +11:00
Dennis Hotson
fd785d8b10 Version bump 2014-12-08 10:31:05 +11:00
Dennis Hotson
6716fab883 Universal module pattern (for browserify support) 2014-12-08 10:29:31 +11:00
shigeru.nakajima
322a7bae8b Add node color. 2014-10-10 18:48:27 +09:00
Dennis Hotson
4480a8e3ff Bump to 2.6.1 2014-07-30 21:37:13 +10:00
Dennis Hotson
c41ff98d3c Bump version to 2.6.0 2014-07-27 12:18:58 +10:00
Dennis Hotson
d3c3be9325 Merge pull request #67 from lgvalent/patch-1
Update springyui.js
2014-07-24 21:55:15 +10:00
Lucio Valentin
487afff1b2 Update springyui.js
A simple error at end of line 35.
2014-07-22 16:58:16 -03:00
Dennis Hotson
db74df106c Merge pull request #65 from Irrational86/master
Fixed order of 'onRenderStop' and 'onRenderStart' parameters when calling this.layout.start()
2014-07-18 11:33:30 +10:00
Dennis Hotson
c14da4feae Merge pull request #66 from Irrational86/springyui-minEnergyThreshold
Enabled the ability to specify the parameter 'minEnergyThreshold' in springyui.js
2014-07-18 11:32:31 +10:00
Jesse
f51be2fc48 Enabled the ability to specify the parameter 'minEnergyThreshold' in springyui.js.
Also made the default value in springyui be very small, in order to cause the animation to end/stop smoothly.
2014-07-12 17:32:02 -04:00
Jesse
b3145ce522 Fixed order of 'onRenderStop' and 'onRenderStop' parameters when calling this.layout.start(). 2014-07-12 14:18:05 -04:00
Dennis Hotson
0a588deed6 Bumped version to 2.5.0 2014-06-01 18:31:09 +10:00
Dennis Hotson
441ccfcc2b Added tick for manually stepping through simulation 2014-06-01 18:29:54 +10:00
Dennis Hotson
f64bda19bc Bumped verison to 2.4.0 2014-06-01 18:25:44 +10:00
Dennis Hotson
1d51239af1 Merge pull request #63 from WebOnWebOff/master
Allow configuration of minimum energy threshold
2014-06-01 18:17:33 +10:00
'WebOnWebOff'
50eed3e039 Allow configuration of minimum energy threshold 2014-05-20 17:18:00 +03:00
Dennis Hotson
28ca9cc5be Bumped version to 2.3.0 2014-02-26 22:42:58 +11:00
tdhsmith
6cb2dd813f Added auto text orientation for edge labels
Added new boolean option edgeLabelsUpright which makes edge labels automatically flip over the edge when they are upside-down.  Addresses readability aspect of #56.
2014-02-11 16:02:49 -06:00
tdhsmith
9eb89a03cf License mistake
Project standard doesn't seem to list all contributors in the license, so removed myself
2014-02-04 18:29:39 -06:00
tdhsmith
96f889ca41 Added basic functionality for image nodes
Extended the drawNode function to include basic image drawing. Adjusted the height and width functions to accommodate natural image dimensions as well as those set with node.data.image.width etc.
2014-02-04 18:19:33 -06:00
4 changed files with 154 additions and 61 deletions

View File

@ -1,7 +1,7 @@
{
"name": "Springy",
"main": "springy.js",
"version": "2.2.2",
"version": "2.7.1",
"homepage": "https://github.com/dhotson/springy",
"authors": [
"Dennis Hotson <dennis@99designs.com>"

View File

@ -1,6 +1,6 @@
{
"name": "springy",
"version": "2.2.2",
"version": "2.7.1",
"description": "A force directed graph layout algorithm in JavaScript.",
"main": "springy.js",
"scripts": {

View File

@ -1,5 +1,5 @@
/**
* Springy v2.2.2
* Springy v2.7.1
*
* Copyright (c) 2010-2013 Dennis Hotson
*
@ -24,22 +24,24 @@
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* 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() {
(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 Springy = {};
var Graph = Springy.Graph = function() {
this.nodeSet = {};
@ -325,11 +327,13 @@
// -----------
var Layout = Springy.Layout = {};
Layout.ForceDirected = function(graph, stiffness, repulsion, damping) {
Layout.ForceDirected = function(graph, stiffness, repulsion, damping, minEnergyThreshold, maxSpeed) {
this.graph = graph;
this.stiffness = stiffness; // spring stiffness constant
this.repulsion = repulsion; // repulsion constant
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.edgeSprings = {}; // keep track of springs associated with edges
@ -448,6 +452,9 @@
// 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);
if (point.v.magnitude() > this.maxSpeed) {
point.v = point.v.normalise().multiply(this.maxSpeed);
}
point.a = new Vector(0,0);
});
};
@ -473,14 +480,14 @@
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; // stolen from coffeescript, thanks jashkenas! ;-)
Springy.requestAnimationFrame = __bind(root.requestAnimationFrame ||
root.webkitRequestAnimationFrame ||
root.mozRequestAnimationFrame ||
root.oRequestAnimationFrame ||
root.msRequestAnimationFrame ||
Springy.requestAnimationFrame = __bind(this.requestAnimationFrame ||
this.webkitRequestAnimationFrame ||
this.mozRequestAnimationFrame ||
this.oRequestAnimationFrame ||
this.msRequestAnimationFrame ||
(function(callback, element) {
root.setTimeout(callback, 10);
}), root);
this.setTimeout(callback, 10);
}), this);
/**
@ -497,18 +504,14 @@
if (onRenderStart !== undefined) { onRenderStart(); }
Springy.requestAnimationFrame(function step() {
t.applyCoulombsLaw();
t.applyHookesLaw();
t.attractToCentre();
t.updateVelocity(0.03);
t.updatePosition(0.03);
t.tick(0.03);
if (render !== undefined) {
render();
}
// stop simulation when energy of the system goes below a threshold
if (t._stop || t.totalEnergy() < 0.01) {
if (t._stop || t.totalEnergy() < t.minEnergyThreshold) {
t._started = false;
if (onRenderStop !== undefined) { onRenderStop(); }
} else {
@ -521,6 +524,14 @@
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
Layout.ForceDirected.prototype.nearest = function(pos) {
var min = {node: null, point: null, distance: null};
@ -634,14 +645,16 @@
* 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) {
var Renderer = Springy.Renderer = function(layout, clear, drawEdge, drawNode, onRenderStop, onRenderStart, onRenderFrame) {
this.layout = layout;
this.clear = clear;
this.drawEdge = drawEdge;
this.drawNode = drawNode;
this.onRenderStop = onRenderStop;
this.onRenderStart = onRenderStart;
this.onRenderFrame = onRenderFrame;
this.layout.graph.addGraphListener(this);
}
@ -672,7 +685,9 @@
t.layout.eachNode(function(node, point) {
t.drawNode(node, point.p);
});
}, this.onRenderStart, this.onRenderStop);
if (t.onRenderFrame !== undefined) { t.onRenderFrame(); }
}, this.onRenderStop, this.onRenderStart);
};
Renderer.prototype.stop = function() {
@ -715,4 +730,6 @@
}
return true;
};
}).call(this);
return Springy;
}));

View File

@ -32,12 +32,15 @@ jQuery.fn.springy = function(params) {
var stiffness = params.stiffness || 400.0;
var repulsion = params.repulsion || 400.0;
var damping = params.damping || 0.5;
var minEnergyThreshold = params.minEnergyThreshold || 0.00001;
var nodeSelected = params.nodeSelected || null;
var nodeImages = {};
var edgeLabelsUpright = true;
var canvas = this[0];
var ctx = canvas.getContext("2d");
var layout = this.layout = new Springy.Layout.ForceDirected(graph, stiffness, repulsion, damping);
var layout = this.layout = new Springy.Layout.ForceDirected(graph, stiffness, repulsion, damping, minEnergyThreshold);
// calculate bounding box of graph layout.. with ease-in
var currentBB = layout.getBoundingBox();
@ -121,26 +124,62 @@ jQuery.fn.springy = function(params) {
dragged = null;
});
Springy.Node.prototype.getWidth = function() {
var text = (this.data.label !== undefined) ? this.data.label : this.id;
if (this._width && this._width[text])
return this._width[text];
var getTextWidth = function(node) {
var text = (node.data.label !== undefined) ? node.data.label : node.id;
if (node._width && node._width[text])
return node._width[text];
ctx.save();
ctx.font = (this.data.font !== undefined) ? this.data.font : nodeFont;
var width = ctx.measureText(text).width + 10;
ctx.font = (node.data.font !== undefined) ? node.data.font : nodeFont;
var width = ctx.measureText(text).width;
ctx.restore();
this._width || (this._width = {});
this._width[text] = width;
node._width || (node._width = {});
node._width[text] = width;
return width;
};
Springy.Node.prototype.getHeight = function() {
return 20;
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() {
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() {
ctx.clearRect(0,0,canvas.width,canvas.height);
@ -173,11 +212,14 @@ jQuery.fn.springy = function(params) {
// Figure out how far off center the line should be drawn
var offset = normal.multiply(-((total - 1) * spacing)/2.0 + (n * spacing));
var paddingX = 6;
var paddingY = 6;
var s1 = toScreen(p1).add(offset);
var s2 = toScreen(p2).add(offset);
var boxWidth = edge.target.getWidth();
var boxHeight = edge.target.getHeight();
var boxWidth = edge.target.getWidth() + paddingX;
var boxHeight = edge.target.getHeight() + paddingY;
var intersection = intersect_line_box(s1, s2, {x: x2-boxWidth/2.0, y: y2-boxHeight/2.0}, boxWidth, boxHeight);
@ -236,9 +278,15 @@ jQuery.fn.springy = function(params) {
ctx.textBaseline = "top";
ctx.font = (edge.data.font !== undefined) ? edge.data.font : edgeFont;
ctx.fillStyle = stroke;
var textPos = s1.add(s2).divide(2).add(normal.multiply(-8));
var angle = Math.atan2(s2.y - s1.y, s2.x - s1.x);
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(Math.atan2(s2.y - s1.y, s2.x - s1.x));
ctx.rotate(angle);
ctx.fillText(text, 0,-2);
ctx.restore();
}
@ -249,11 +297,18 @@ jQuery.fn.springy = function(params) {
ctx.save();
var boxWidth = node.getWidth();
var boxHeight = node.getHeight();
// Pulled out the padding aspect sso that the size functions could be used in multiple places
// These should probably be settable by the user (and scoped higher) but this suffices for now
var paddingX = 6;
var paddingY = 6;
var contentWidth = node.getWidth();
var contentHeight = node.getHeight();
var boxWidth = contentWidth + paddingX;
var boxHeight = contentHeight + paddingY;
// clear background
ctx.clearRect(s.x - boxWidth/2, s.y - 10, boxWidth, 20);
ctx.clearRect(s.x - boxWidth/2, s.y - boxHeight/2, boxWidth, boxHeight);
// fill background
if (selected !== null && selected.node !== null && selected.node.id === node.id) {
@ -263,15 +318,36 @@ jQuery.fn.springy = function(params) {
} else {
ctx.fillStyle = "#FFFFFF";
}
ctx.fillRect(s.x - boxWidth/2, s.y - 10, boxWidth, 20);
ctx.textAlign = "left";
ctx.textBaseline = "top";
ctx.font = (node.data.font !== undefined) ? node.data.font : nodeFont;
ctx.fillStyle = "#000000";
var text = (node.data.label !== undefined) ? node.data.label : node.id;
ctx.fillText(text, s.x - boxWidth/2 + 5, s.y - 8);
ctx.fillRect(s.x - boxWidth/2, s.y - boxHeight/2, boxWidth, boxHeight);
if (node.data.image == undefined) {
ctx.textAlign = "left";
ctx.textBaseline = "top";
ctx.font = (node.data.font !== undefined) ? node.data.font : nodeFont;
ctx.fillStyle = (node.data.color !== undefined) ? node.data.color : "#000000";
var text = (node.data.label !== undefined) ? node.data.label : node.id;
ctx.fillText(text, s.x - contentWidth/2, s.y - contentHeight/2);
} 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();
}
);