Compare commits

..

55 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
Dennis Hotson
98369ebcb9 Consistent version number bump 2014-01-11 06:12:42 +11:00
Dennis Hotson
fae78f1055 Bump version to 2.2.1 2014-01-11 05:27:19 +11:00
Dennis Hotson
36a98b03cd Font size bug fix 2014-01-11 05:26:23 +11:00
Dennis Hotson
e2e24d0601 Bump version to 2.2.0 2014-01-11 05:18:43 +11:00
Dennis Hotson
00cbc7f64e Improved calculation of text angle and spacing 2014-01-11 05:17:41 +11:00
Dennis Hotson
36c885871e Merge remote-tracking branch 'oak-tree/master'
* oak-tree/master:
  set label of edge at the angle of this edge.
2014-01-11 04:40:40 +11:00
Dennis Hotson
4ed5f13302 Whitespace fixes 2014-01-11 04:12:30 +11:00
Dennis Hotson
c7f7d040ec Bumped version 2014-01-11 04:10:39 +11:00
Dennis Hotson
cec178680f Switch order of onRenderStop and onRenderStart to preserve backwards compat 2014-01-11 04:10:34 +11:00
Dennis Hotson
ac17d28337 Merge remote-tracking branch 'fabiankessler/master'
* fabiankessler/master:
  Render start and stop callbacks for all cases.
  Documented that start() is silently ignored if running.
  added done callback to renderer start method.
  updated copyright year
2014-01-11 03:58:56 +11:00
cp123127
1b85a2e9b8 Fixed bug that causes inconsistant behavior for highlighting selected nodes. 2014-01-11 03:48:17 +11:00
Dennis Hotson
bcd9efd220 Whitespace fix 2014-01-11 03:45:40 +11:00
Dennis Hotson
a1f848d8d3 Merge pull request #44 from mikeleber/patch-1
Update springyui.js
2014-01-10 08:42:59 -08:00
oak-tree
4acb7e9fb7 set label of edge at the angle of this edge.
moreover, if two vertices have more than 1 edge allow each edge to have its own label. make sure each labels does not interfere each other.
2014-01-08 21:04:52 +02:00
Dennis Hotson
8be075d656 Add springy to bower 2013-08-23 19:39:38 +10:00
mikeleber
4adb4308dd Update springyui.js
font property for nodes and edges
graph.newEdge(AHM_10, AHM_50 ,{font:'10px Verdana, sans-serif', label: 'ablehnen', color: '#97f23d'});
2013-05-30 11:43:54 +02:00
Fabian Kessler
81213595ed Render start and stop callbacks for all cases.
Rendering can happen on demand (by calling start()) but also when adding/removing nodes and relations. I also need to be informed when rendering starts in my use case. I have a re-arrange button that needs to be toggled whenever rendering is in progress, and with these events it's nice and clean.
When the event callbacks are passed in to the Springy.Renderer start() method only (as in my previous commit) then all the other cases are left out (adding/removing nodes/relations).
2013-04-21 02:57:18 +03:00
Fabian Kessler
ffca60dcab Documented that start() is silently ignored if running. 2013-04-20 23:48:58 +03:00
Fabian Kessler
1b20da23e2 added done callback to renderer start method.
The done callback existed in the ForceDirected layout start method, but no one passed that in. Maybe I don't understand how it's meant to be used? Or something was lost on the way with refactorings in springy? Anyway, with this change it's possible to call the Springy.Renderer start() method with a done callback. Please accept it, or document how it's meant to be used. Thanks ;-)
2013-04-20 23:43:28 +03:00
Fabian Kessler
143d2d4c41 updated copyright year
The 2010 made me think that the project was abandoned. Also, arbor.js writes that it's based on springy. Only now that I take a closer look I see that there are recent commits.
2013-04-20 23:38:12 +03:00
Dennis Hotson
5439db1423 Added npm package.json 2013-03-27 01:19:47 +00:00
Dennis Hotson
43f725e61a Accidental global functions 2013-03-19 21:41:01 +11:00
Dennis Hotson
5ed407c69d Clean up adjacency table when edges are deleted 2013-03-15 11:19:47 +00:00
Dennis Hotson
80108b0f31 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
2013-03-15 11:11:11 +00:00
Dennis Hotson
43cae6cf02 Replace references to window with root 2013-03-15 10:56:22 +00:00
Dennis Hotson
a1d7eb6f18 Updated README with namespace 2013-03-15 10:54:36 +00:00
5 changed files with 253 additions and 79 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
[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.addEdges(
['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);
@ -80,7 +80,7 @@ things before you get started.
This is the basic graph API, you can create nodes and edges etc.
// make a new graph
var graph = new Graph();
var graph = new Springy.Graph();
// make some nodes
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:
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.
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() {
// code to clear screen
},

17
bower.json Normal file
View File

@ -0,0 +1,17 @@
{
"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"
}

21
package.json Normal file
View File

@ -0,0 +1,21 @@
{
"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.0.0
* Springy v2.7.1
*
* Copyright (c) 2010 Dennis Hotson
* Copyright (c) 2010-2013 Dennis Hotson
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
@ -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 = {};
@ -215,7 +217,6 @@
}
this.detachNode(node);
};
// removes edges associated with a given node
@ -247,6 +248,16 @@
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];
}
}
@ -316,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
@ -439,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);
});
};
@ -464,39 +480,40 @@
var __bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; // stolen from coffeescript, thanks jashkenas! ;-)
Springy.requestAnimationFrame = __bind(window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback, element) {
window.setTimeout(callback, 10);
}, window);
Springy.requestAnimationFrame = __bind(this.requestAnimationFrame ||
this.webkitRequestAnimationFrame ||
this.mozRequestAnimationFrame ||
this.oRequestAnimationFrame ||
this.msRequestAnimationFrame ||
(function(callback, element) {
this.setTimeout(callback, 10);
}), this);
// start simulation
Layout.ForceDirected.prototype.start = function(render, done) {
/**
* Start simulation if it's not running already.
* 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;
if (this._started) return;
this._started = true;
this._stop = false;
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 (done !== undefined) { done(); }
if (onRenderStop !== undefined) { onRenderStop(); }
} else {
Springy.requestAnimationFrame(step);
}
@ -507,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};
@ -616,12 +641,20 @@
// return Math.abs(ac.x * n.x + ac.y * n.y);
// };
// Renderer handles the layout rendering loop
var Renderer = Springy.Renderer = function(layout, clear, drawEdge, drawNode) {
/**
* 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.clear = clear;
this.drawEdge = drawEdge;
this.drawNode = drawNode;
this.onRenderStop = onRenderStop;
this.onRenderStart = onRenderStart;
this.onRenderFrame = onRenderFrame;
this.layout.graph.addGraphListener(this);
}
@ -630,7 +663,17 @@
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;
this.layout.start(function render() {
t.clear();
@ -642,7 +685,9 @@
t.layout.eachNode(function(node, point) {
t.drawNode(node, point.p);
});
});
if (t.onRenderFrame !== undefined) { t.onRenderFrame(); }
}, this.onRenderStop, this.onRenderStart);
};
Renderer.prototype.stop = function() {
@ -676,4 +721,15 @@
}
};
}
}).call(this);
var isEmpty = function(obj) {
for (var k in obj) {
if (obj.hasOwnProperty(k)) {
return false;
}
}
return true;
};
return Springy;
}));

View File

@ -27,16 +27,20 @@ Copyright (c) 2010 Dennis Hotson
jQuery.fn.springy = function(params) {
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 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();
@ -57,14 +61,14 @@ jQuery.fn.springy = function(params) {
});
// convert to/from screen coordinates
toScreen = function(p) {
var toScreen = function(p) {
var size = currentBB.topright.subtract(currentBB.bottomleft);
var sx = p.subtract(currentBB.bottomleft).divide(size.x).x * canvas.width;
var sy = p.subtract(currentBB.bottomleft).divide(size.y).y * canvas.height;
return new Springy.Vector(sx, sy);
};
fromScreen = function(s) {
var fromScreen = function(s) {
var size = currentBB.topright.subtract(currentBB.bottomleft);
var px = (s.x / canvas.width) * size.x + currentBB.bottomleft.x;
var py = (s.y / canvas.height) * size.y + currentBB.bottomleft.y;
@ -120,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 = "16px Verdana, sans-serif";
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);
@ -166,16 +206,20 @@ jQuery.fn.springy = function(params) {
}
}
var spacing = 6.0;
//change default to 10.0 to allow text fit between edges
var spacing = 12.0;
// 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);
@ -232,9 +276,18 @@ jQuery.fn.springy = function(params) {
ctx.save();
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.font = "10px Helvetica, sans-serif";
ctx.fillStyle = "#5BA6EC";
ctx.fillText(text, (x1+x2)/2, (y1+y2)/2);
ctx.font = (edge.data.font !== undefined) ? edge.data.font : edgeFont;
ctx.fillStyle = stroke;
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(angle);
ctx.fillText(text, 0,-2);
ctx.restore();
}
@ -244,30 +297,57 @@ 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 && nearest.node !== null && selected.node.id === node.id) {
if (selected !== null && selected.node !== null && selected.node.id === node.id) {
ctx.fillStyle = "#FFFFE0";
} else if (nearest !== null && nearest.node !== null && nearest.node.id === node.id) {
ctx.fillStyle = "#EEEEEE";
} else {
ctx.fillStyle = "#FFFFFF";
}
ctx.fillRect(s.x - boxWidth/2, s.y - 10, boxWidth, 20);
ctx.textAlign = "left";
ctx.textBaseline = "top";
ctx.font = "16px Verdana, sans-serif";
ctx.fillStyle = "#000000";
ctx.font = "16px Verdana, sans-serif";
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();
}
);