ngraph.forcelayout3d
Version:
Force directed graph layout in 3d
1,857 lines (1,565 loc) • 91.1 kB
JavaScript
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 3000);
var isWebGlSupported = ( function () { try { var canvas = document.createElement( 'canvas' ); return !! window.WebGLRenderingContext && ( canvas.getContext( 'webgl' ) || canvas.getContext( 'experimental-webgl' ) ); } catch( e ) { return false; } } )();
var renderer = isWebGlSupported ? new THREE.WebGLRenderer() : new THREE.CanvasRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
var graph = require('ngraph.generators').grid3(10, 10, 10);
var layout = require('../')(graph, { integrator: 'verlet'});
var geometry = new THREE.CubeGeometry(5,5,5);
var nodeMaterial = new THREE.MeshBasicMaterial({color: 0x009e8f});
var ui = {};
graph.forEachNode(function (node) {
var pos = layout.getNodePosition(node.id);
var cube = new THREE.Mesh(geometry, nodeMaterial);
ui[node.id] = cube;
scene.add(cube);
});
var render = function () {
requestAnimationFrame(render);
layout.step();
for (var key in ui) {
var cube = ui[key];
var pos = layout.getNodePosition(key);
cube.position.x = pos.x;
cube.position.y = pos.y;
cube.position.z = pos.z;
}
var timer = Date.now() * 0.0002;
camera.position.x = Math.cos( timer ) * 1500;
camera.position.z = Math.sin( timer ) * 1500;
camera.lookAt( scene.position );
renderer.render(scene, camera);
};
render();
},{"../":2,"ngraph.generators":12}],2:[function(require,module,exports){
/**
* This module provides all required forces to regular ngraph.physics.simulator
* to make it 3D simulator. Ideally ngraph.physics.simulator should operate
* with vectors, but on practices that showed performance decrease... Maybe
* I was doing it wrong, will see if I can refactor/throw away this module.
*/
module.exports = createLayout;
createLayout.get2dLayout = require('ngraph.forcelayout');
function createLayout(graph, physicsSettings) {
var merge = require('ngraph.merge');
physicsSettings = merge(physicsSettings, {
createQuadTree: require('ngraph.quadtreebh3d'),
createBounds: require('./lib/bounds'),
createDragForce: require('./lib/dragForce'),
createSpringForce: require('./lib/springForce'),
integrator: getIntegrator(physicsSettings),
createBody: require('./lib/createBody')
});
return createLayout.get2dLayout(graph, physicsSettings);
}
function getIntegrator(physicsSettings) {
if (physicsSettings && physicsSettings.integrator === 'verlet') {
return require('./lib/verletIntegrator.js');
}
return require('./lib/eulerIntegrator')
}
},{"./lib/bounds":3,"./lib/createBody":4,"./lib/dragForce":5,"./lib/eulerIntegrator":6,"./lib/springForce":7,"./lib/verletIntegrator.js":8,"ngraph.forcelayout":11,"ngraph.merge":14,"ngraph.quadtreebh3d":27}],3:[function(require,module,exports){
module.exports = function (bodies, settings) {
var random = require('ngraph.random').random(42);
var boundingBox = { x1: 0, y1: 0, z1: 0, x2: 0, y2: 0, z2: 0 };
return {
box: boundingBox,
update: updateBoundingBox,
reset : function () {
boundingBox.x1 = boundingBox.y1 = 0;
boundingBox.x2 = boundingBox.y2 = 0;
boundingBox.z1 = boundingBox.z2 = 0;
},
getBestNewPosition: function (neighbors) {
var graphRect = boundingBox;
var baseX = 0, baseY = 0, baseZ = 0;
if (neighbors.length) {
for (var i = 0; i < neighbors.length; ++i) {
baseX += neighbors[i].pos.x;
baseY += neighbors[i].pos.y;
baseZ += neighbors[i].pos.z;
}
baseX /= neighbors.length;
baseY /= neighbors.length;
baseZ /= neighbors.length;
} else {
baseX = (graphRect.x1 + graphRect.x2) / 2;
baseY = (graphRect.y1 + graphRect.y2) / 2;
baseZ = (graphRect.z1 + graphRect.z2) / 2;
}
var springLength = settings.springLength;
return {
x: baseX + random.next(springLength) - springLength / 2,
y: baseY + random.next(springLength) - springLength / 2,
z: baseZ + random.next(springLength) - springLength / 2
};
}
};
function updateBoundingBox() {
var i = bodies.length;
if (i === 0) { return; } // don't have to wory here.
var x1 = Number.MAX_VALUE,
y1 = Number.MAX_VALUE,
z1 = Number.MAX_VALUE,
x2 = Number.MIN_VALUE,
y2 = Number.MIN_VALUE,
z2 = Number.MIN_VALUE;
while(i--) {
// this is O(n), could it be done faster with quadtree?
// how about pinned nodes?
var body = bodies[i];
if (body.isPinned) {
body.pos.x = body.prevPos.x;
body.pos.y = body.prevPos.y;
body.pos.z = body.prevPos.z;
} else {
body.prevPos.x = body.pos.x;
body.prevPos.y = body.pos.y;
body.prevPos.z = body.pos.z;
}
if (body.pos.x < x1) {
x1 = body.pos.x;
}
if (body.pos.x > x2) {
x2 = body.pos.x;
}
if (body.pos.y < y1) {
y1 = body.pos.y;
}
if (body.pos.y > y2) {
y2 = body.pos.y;
}
if (body.pos.z < z1) {
z1 = body.pos.z;
}
if (body.pos.z > z2) {
z2 = body.pos.z;
}
}
boundingBox.x1 = x1;
boundingBox.x2 = x2;
boundingBox.y1 = y1;
boundingBox.y2 = y2;
boundingBox.z1 = z1;
boundingBox.z2 = z2;
}
};
},{"ngraph.random":31}],4:[function(require,module,exports){
var physics = require('ngraph.physics.primitives');
module.exports = function(pos) {
return new physics.Body3d(pos);
}
},{"ngraph.physics.primitives":15}],5:[function(require,module,exports){
/**
* Represents 3d drag force, which reduces force value on each step by given
* coefficient.
*
* @param {Object} options for the drag force
* @param {Number=} options.dragCoeff drag force coefficient. 0.1 by default
*/
module.exports = function (options) {
var merge = require('ngraph.merge'),
expose = require('ngraph.expose');
options = merge(options, {
dragCoeff: 0.02
});
var api = {
update : function (body) {
body.force.x -= options.dragCoeff * body.velocity.x;
body.force.y -= options.dragCoeff * body.velocity.y;
body.force.z -= options.dragCoeff * body.velocity.z;
}
};
// let easy access to dragCoeff:
expose(options, api, ['dragCoeff']);
return api;
};
},{"ngraph.expose":10,"ngraph.merge":14}],6:[function(require,module,exports){
/**
* Performs 3d forces integration, using given timestep. Uses Euler method to solve
* differential equation (http://en.wikipedia.org/wiki/Euler_method ).
*
* @returns {Number} squared distance of total position updates.
*/
module.exports = integrate;
function integrate(bodies, timeStep) {
var dx = 0, tx = 0,
dy = 0, ty = 0,
dz = 0, tz = 0,
i,
max = bodies.length;
for (i = 0; i < max; ++i) {
var body = bodies[i],
coeff = timeStep / body.mass;
body.velocity.x += coeff * body.force.x;
body.velocity.y += coeff * body.force.y;
body.velocity.z += coeff * body.force.z;
var vx = body.velocity.x,
vy = body.velocity.y,
vz = body.velocity.z,
v = Math.sqrt(vx * vx + vy * vy + vz * vz);
if (v > 1) {
body.velocity.x = vx / v;
body.velocity.y = vy / v;
body.velocity.z = vz / v;
}
dx = timeStep * body.velocity.x;
dy = timeStep * body.velocity.y;
dz = timeStep * body.velocity.z;
body.pos.x += dx;
body.pos.y += dy;
body.pos.z += dz;
tx += Math.abs(dx); ty += Math.abs(dy); tz += Math.abs(dz);
}
return (tx * tx + ty * ty + tz * tz)/bodies.length;
}
},{}],7:[function(require,module,exports){
/**
* Represents 3d spring force, which updates forces acting on two bodies, conntected
* by a spring.
*
* @param {Object} options for the spring force
* @param {Number=} options.springCoeff spring force coefficient.
* @param {Number=} options.springLength desired length of a spring at rest.
*/
module.exports = function (options) {
var merge = require('ngraph.merge');
var random = require('ngraph.random').random(42);
var expose = require('ngraph.expose');
options = merge(options, {
springCoeff: 0.0002,
springLength: 80
});
var api = {
/**
* Upsates forces acting on a spring
*/
update : function (spring) {
var body1 = spring.from,
body2 = spring.to,
length = spring.length < 0 ? options.springLength : spring.length,
dx = body2.pos.x - body1.pos.x,
dy = body2.pos.y - body1.pos.y,
dz = body2.pos.z - body1.pos.z,
r = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (r === 0) {
dx = (random.nextDouble() - 0.5) / 50;
dy = (random.nextDouble() - 0.5) / 50;
dz = (random.nextDouble() - 0.5) / 50;
r = Math.sqrt(dx * dx + dy * dy + dz * dz);
}
var d = r - length;
var coeff = ((!spring.coeff || spring.coeff < 0) ? options.springCoeff : spring.coeff) * d / r * spring.weight;
body1.force.x += coeff * dx;
body1.force.y += coeff * dy;
body1.force.z += coeff * dz;
body2.force.x -= coeff * dx;
body2.force.y -= coeff * dy;
body2.force.z -= coeff * dz;
}
};
expose(options, api, ['springCoeff', 'springLength']);
return api;
}
},{"ngraph.expose":10,"ngraph.merge":14,"ngraph.random":31}],8:[function(require,module,exports){
module.exports = integrate;
function integrate(bodies, timeStep) {
var tx = 0, ty = 0, tz = 0,
i, max = bodies.length;
for (i = 0; i < max; ++i) {
var body = bodies[i],
coeff = timeStep * timeStep / body.mass;
body.pos.x = 2 * body.pos.x - body.prevPos.x + body.force.x * coeff;
body.pos.y = 2 * body.pos.y - body.prevPos.y + body.force.y * coeff;
body.pos.z = 2 * body.pos.z - body.prevPos.z + body.force.z * coeff;
tx += Math.abs(body.pos.x - body.prevPos.x)
ty += Math.abs(body.pos.y - body.prevPos.y)
tz += Math.abs(body.pos.z - body.prevPos.z)
}
return (tx * tx + ty * ty + tz * tz)/bodies.length;
}
},{}],9:[function(require,module,exports){
module.exports = function(subject) {
validateSubject(subject);
var eventsStorage = createEventsStorage(subject);
subject.on = eventsStorage.on;
subject.off = eventsStorage.off;
subject.fire = eventsStorage.fire;
return subject;
};
function createEventsStorage(subject) {
// Store all event listeners to this hash. Key is event name, value is array
// of callback records.
//
// A callback record consists of callback function and its optional context:
// { 'eventName' => [{callback: function, ctx: object}] }
var registeredEvents = Object.create(null);
return {
on: function (eventName, callback, ctx) {
if (typeof callback !== 'function') {
throw new Error('callback is expected to be a function');
}
var handlers = registeredEvents[eventName];
if (!handlers) {
handlers = registeredEvents[eventName] = [];
}
handlers.push({callback: callback, ctx: ctx});
return subject;
},
off: function (eventName, callback) {
var wantToRemoveAll = (typeof eventName === 'undefined');
if (wantToRemoveAll) {
// Killing old events storage should be enough in this case:
registeredEvents = Object.create(null);
return subject;
}
if (registeredEvents[eventName]) {
var deleteAllCallbacksForEvent = (typeof callback !== 'function');
if (deleteAllCallbacksForEvent) {
delete registeredEvents[eventName];
} else {
var callbacks = registeredEvents[eventName];
for (var i = 0; i < callbacks.length; ++i) {
if (callbacks[i].callback === callback) {
callbacks.splice(i, 1);
}
}
}
}
return subject;
},
fire: function (eventName) {
var callbacks = registeredEvents[eventName];
if (!callbacks) {
return subject;
}
var fireArguments;
if (arguments.length > 1) {
fireArguments = Array.prototype.splice.call(arguments, 1);
}
for(var i = 0; i < callbacks.length; ++i) {
var callbackInfo = callbacks[i];
callbackInfo.callback.apply(callbackInfo.ctx, fireArguments);
}
return subject;
}
};
}
function validateSubject(subject) {
if (!subject) {
throw new Error('Eventify cannot use falsy object as events subject');
}
var reservedWords = ['on', 'fire', 'off'];
for (var i = 0; i < reservedWords.length; ++i) {
if (subject.hasOwnProperty(reservedWords[i])) {
throw new Error("Subject cannot be eventified, since it already has property '" + reservedWords[i] + "'");
}
}
}
},{}],10:[function(require,module,exports){
module.exports = exposeProperties;
/**
* Augments `target` object with getter/setter functions, which modify settings
*
* @example
* var target = {};
* exposeProperties({ age: 42}, target);
* target.age(); // returns 42
* target.age(24); // make age 24;
*
* var filteredTarget = {};
* exposeProperties({ age: 42, name: 'John'}, filteredTarget, ['name']);
* filteredTarget.name(); // returns 'John'
* filteredTarget.age === undefined; // true
*/
function exposeProperties(settings, target, filter) {
var needsFilter = Object.prototype.toString.call(filter) === '[object Array]';
if (needsFilter) {
for (var i = 0; i < filter.length; ++i) {
augment(settings, target, filter[i]);
}
} else {
for (var key in settings) {
augment(settings, target, key);
}
}
}
function augment(source, target, key) {
if (source.hasOwnProperty(key)) {
if (typeof target[key] === 'function') {
// this accessor is already defined. Ignore it
return;
}
target[key] = function (value) {
if (value !== undefined) {
source[key] = value;
return target;
}
return source[key];
}
}
}
},{}],11:[function(require,module,exports){
module.exports = createLayout;
module.exports.simulator = require('ngraph.physics.simulator');
var eventify = require('ngraph.events');
/**
* Creates force based layout for a given graph.
* @param {ngraph.graph} graph which needs to be laid out
* @param {object} physicsSettings if you need custom settings
* for physics simulator you can pass your own settings here. If it's not passed
* a default one will be created.
*/
function createLayout(graph, physicsSettings) {
if (!graph) {
throw new Error('Graph structure cannot be undefined');
}
var createSimulator = require('ngraph.physics.simulator');
var physicsSimulator = createSimulator(physicsSettings);
var nodeBodies = typeof Object.create === 'function' ? Object.create(null) : {};
var springs = {};
var springTransform = physicsSimulator.settings.springTransform || noop;
// Initialize physical objects according to what we have in the graph:
initPhysics();
listenToEvents();
var api = {
/**
* Performs one step of iterative layout algorithm
*/
step: function() {
return physicsSimulator.step();
},
/**
* For a given `nodeId` returns position
*/
getNodePosition: function (nodeId) {
return getInitializedBody(nodeId).pos;
},
/**
* Sets position of a node to a given coordinates
* @param {string} nodeId node identifier
* @param {number} x position of a node
* @param {number} y position of a node
* @param {number=} z position of node (only if applicable to body)
*/
setNodePosition: function (nodeId) {
var body = getInitializedBody(nodeId);
body.setPosition.apply(body, Array.prototype.slice.call(arguments, 1));
},
/**
* @returns {Object} Link position by link id
* @returns {Object.from} {x, y} coordinates of link start
* @returns {Object.to} {x, y} coordinates of link end
*/
getLinkPosition: function (linkId) {
var spring = springs[linkId];
if (spring) {
return {
from: spring.from.pos,
to: spring.to.pos
};
}
},
/**
* @returns {Object} area required to fit in the graph. Object contains
* `x1`, `y1` - top left coordinates
* `x2`, `y2` - bottom right coordinates
*/
getGraphRect: function () {
return physicsSimulator.getBBox();
},
/*
* Requests layout algorithm to pin/unpin node to its current position
* Pinned nodes should not be affected by layout algorithm and always
* remain at their position
*/
pinNode: function (node, isPinned) {
var body = getInitializedBody(node.id);
body.isPinned = !!isPinned;
},
/**
* Checks whether given graph's node is currently pinned
*/
isNodePinned: function (node) {
return getInitializedBody(node.id).isPinned;
},
/**
* Request to release all resources
*/
dispose: function() {
graph.off('changed', onGraphChanged);
physicsSimulator.off('stable', onStableChanged);
},
/**
* Gets physical body for a given node id. If node is not found undefined
* value is returned.
*/
getBody: getBody,
/**
* Gets spring for a given edge.
*
* @param {string} linkId link identifer. If two arguments are passed then
* this argument is treated as formNodeId
* @param {string=} toId when defined this parameter denotes head of the link
* and first argument is trated as tail of the link (fromId)
*/
getSpring: getSpring,
/**
* [Read only] Gets current physics simulator
*/
simulator: physicsSimulator
};
eventify(api);
return api;
function getSpring(fromId, toId) {
var linkId;
if (toId === undefined) {
if (typeof fromId !== 'object') {
// assume fromId as a linkId:
linkId = fromId;
} else {
// assume fromId to be a link object:
linkId = fromId.id;
}
} else {
// toId is defined, should grab link:
var link = graph.hasLink(fromId, toId);
if (!link) return;
linkId = link.id;
}
return springs[linkId];
}
function getBody(nodeId) {
return nodeBodies[nodeId];
}
function listenToEvents() {
graph.on('changed', onGraphChanged);
physicsSimulator.on('stable', onStableChanged);
}
function onStableChanged(isStable) {
api.fire('stable', isStable);
}
function onGraphChanged(changes) {
for (var i = 0; i < changes.length; ++i) {
var change = changes[i];
if (change.changeType === 'add') {
if (change.node) {
initBody(change.node.id);
}
if (change.link) {
initLink(change.link);
}
} else if (change.changeType === 'remove') {
if (change.node) {
releaseNode(change.node);
}
if (change.link) {
releaseLink(change.link);
}
}
}
}
function initPhysics() {
graph.forEachNode(function (node) {
initBody(node.id);
});
graph.forEachLink(initLink);
}
function initBody(nodeId) {
var body = nodeBodies[nodeId];
if (!body) {
var node = graph.getNode(nodeId);
if (!node) {
throw new Error('initBody() was called with unknown node id');
}
var pos = node.position;
if (!pos) {
var neighbors = getNeighborBodies(node);
pos = physicsSimulator.getBestNewBodyPosition(neighbors);
}
body = physicsSimulator.addBodyAt(pos);
nodeBodies[nodeId] = body;
updateBodyMass(nodeId);
if (isNodeOriginallyPinned(node)) {
body.isPinned = true;
}
}
}
function releaseNode(node) {
var nodeId = node.id;
var body = nodeBodies[nodeId];
if (body) {
nodeBodies[nodeId] = null;
delete nodeBodies[nodeId];
physicsSimulator.removeBody(body);
}
}
function initLink(link) {
updateBodyMass(link.fromId);
updateBodyMass(link.toId);
var fromBody = nodeBodies[link.fromId],
toBody = nodeBodies[link.toId],
spring = physicsSimulator.addSpring(fromBody, toBody, link.length);
springTransform(link, spring);
springs[link.id] = spring;
}
function releaseLink(link) {
var spring = springs[link.id];
if (spring) {
var from = graph.getNode(link.fromId),
to = graph.getNode(link.toId);
if (from) updateBodyMass(from.id);
if (to) updateBodyMass(to.id);
delete springs[link.id];
physicsSimulator.removeSpring(spring);
}
}
function getNeighborBodies(node) {
// TODO: Could probably be done better on memory
var neighbors = [];
if (!node.links) {
return neighbors;
}
var maxNeighbors = Math.min(node.links.length, 2);
for (var i = 0; i < maxNeighbors; ++i) {
var link = node.links[i];
var otherBody = link.fromId !== node.id ? nodeBodies[link.fromId] : nodeBodies[link.toId];
if (otherBody && otherBody.pos) {
neighbors.push(otherBody);
}
}
return neighbors;
}
function updateBodyMass(nodeId) {
var body = nodeBodies[nodeId];
body.mass = nodeMass(nodeId);
}
/**
* Checks whether graph node has in its settings pinned attribute,
* which means layout algorithm cannot move it. Node can be preconfigured
* as pinned, if it has "isPinned" attribute, or when node.data has it.
*
* @param {Object} node a graph node to check
* @return {Boolean} true if node should be treated as pinned; false otherwise.
*/
function isNodeOriginallyPinned(node) {
return (node && (node.isPinned || (node.data && node.data.isPinned)));
}
function getInitializedBody(nodeId) {
var body = nodeBodies[nodeId];
if (!body) {
initBody(nodeId);
body = nodeBodies[nodeId];
}
return body;
}
/**
* Calculates mass of a body, which corresponds to node with given id.
*
* @param {String|Number} nodeId identifier of a node, for which body mass needs to be calculated
* @returns {Number} recommended mass of the body;
*/
function nodeMass(nodeId) {
var links = graph.getLinks(nodeId);
if (!links) return 1;
return 1 + links.length / 3.0;
}
}
function noop() { }
},{"ngraph.events":9,"ngraph.physics.simulator":16}],12:[function(require,module,exports){
module.exports = {
ladder: ladder,
complete: complete,
completeBipartite: completeBipartite,
balancedBinTree: balancedBinTree,
path: path,
circularLadder: circularLadder,
grid: grid,
grid3: grid3,
noLinks: noLinks,
wattsStrogatz: wattsStrogatz
};
var createGraph = require('ngraph.graph');
function ladder(n) {
/**
* Ladder graph is a graph in form of ladder
* @param {Number} n Represents number of steps in the ladder
*/
if (!n || n < 0) {
throw new Error("Invalid number of nodes");
}
var g = createGraph(),
i;
for (i = 0; i < n - 1; ++i) {
g.addLink(i, i + 1);
// first row
g.addLink(n + i, n + i + 1);
// second row
g.addLink(i, n + i);
// ladder's step
}
g.addLink(n - 1, 2 * n - 1);
// last step in the ladder;
return g;
}
function circularLadder(n) {
/**
* Circular ladder with n steps.
*
* @param {Number} n of steps in the ladder.
*/
if (!n || n < 0) {
throw new Error("Invalid number of nodes");
}
var g = ladder(n);
g.addLink(0, n - 1);
g.addLink(n, 2 * n - 1);
return g;
}
function complete(n) {
/**
* Complete graph Kn.
*
* @param {Number} n represents number of nodes in the complete graph.
*/
if (!n || n < 1) {
throw new Error("At least two nodes are expected for complete graph");
}
var g = createGraph(),
i,
j;
for (i = 0; i < n; ++i) {
for (j = i + 1; j < n; ++j) {
if (i !== j) {
g.addLink(i, j);
}
}
}
return g;
}
function completeBipartite (n, m) {
/**
* Complete bipartite graph K n,m. Each node in the
* first partition is connected to all nodes in the second partition.
*
* @param {Number} n represents number of nodes in the first graph partition
* @param {Number} m represents number of nodes in the second graph partition
*/
if (!n || !m || n < 0 || m < 0) {
throw new Error("Graph dimensions are invalid. Number of nodes in each partition should be greater than 0");
}
var g = createGraph(),
i, j;
for (i = 0; i < n; ++i) {
for (j = n; j < n + m; ++j) {
g.addLink(i, j);
}
}
return g;
}
function path(n) {
/**
* Path graph with n steps.
*
* @param {Number} n number of nodes in the path
*/
if (!n || n < 0) {
throw new Error("Invalid number of nodes");
}
var g = createGraph(),
i;
g.addNode(0);
for (i = 1; i < n; ++i) {
g.addLink(i - 1, i);
}
return g;
}
function grid(n, m) {
/**
* Grid graph with n rows and m columns.
*
* @param {Number} n of rows in the graph.
* @param {Number} m of columns in the graph.
*/
if (n < 1 || m < 1) {
throw new Error("Invalid number of nodes in grid graph");
}
var g = createGraph(),
i,
j;
if (n === 1 && m === 1) {
g.addNode(0);
return g;
}
for (i = 0; i < n; ++i) {
for (j = 0; j < m; ++j) {
var node = i + j * n;
if (i > 0) { g.addLink(node, i - 1 + j * n); }
if (j > 0) { g.addLink(node, i + (j - 1) * n); }
}
}
return g;
}
function grid3(n, m, z) {
/**
* 3D grid with n rows and m columns and z levels.
*
* @param {Number} n of rows in the graph.
* @param {Number} m of columns in the graph.
* @param {Number} z of levels in the graph.
*/
if (n < 1 || m < 1 || z < 1) {
throw new Error("Invalid number of nodes in grid3 graph");
}
var g = createGraph(),
i, j, k;
if (n === 1 && m === 1 && z === 1) {
g.addNode(0);
return g;
}
for (k = 0; k < z; ++k) {
for (i = 0; i < n; ++i) {
for (j = 0; j < m; ++j) {
var level = k * n * m;
var node = i + j * n + level;
if (i > 0) { g.addLink(node, i - 1 + j * n + level); }
if (j > 0) { g.addLink(node, i + (j - 1) * n + level); }
if (k > 0) { g.addLink(node, i + j * n + (k - 1) * n * m ); }
}
}
}
return g;
}
function balancedBinTree(n) {
/**
* Balanced binary tree with n levels.
*
* @param {Number} n of levels in the binary tree
*/
if (n < 0) {
throw new Error("Invalid number of nodes in balanced tree");
}
var g = createGraph(),
count = Math.pow(2, n),
level;
if (n === 0) {
g.addNode(1);
}
for (level = 1; level < count; ++level) {
var root = level,
left = root * 2,
right = root * 2 + 1;
g.addLink(root, left);
g.addLink(root, right);
}
return g;
}
function noLinks(n) {
/**
* Graph with no links
*
* @param {Number} n of nodes in the graph
*/
if (n < 0) {
throw new Error("Number of nodes shoul be >= 0");
}
var g = createGraph(), i;
for (i = 0; i < n; ++i) {
g.addNode(i);
}
return g;
}
function wattsStrogatz(n, k, p, seed) {
/**
* Watts-Strogatz small-world graph.
*
* @param {Number} n The number of nodes
* @param {Number} k Each node is connected to k nearest neighbors in ring topology
* @param {Number} p The probability of rewiring each edge
* @see https://github.com/networkx/networkx/blob/master/networkx/generators/random_graphs.py
*/
if (k >= n) throw new Error('Choose smaller `k`. It cannot be larger than number of nodes `n`');
var random = require('ngraph.random').random(seed || 42);
var g = createGraph(), i, to;
for (i = 0; i < n; ++i) {
g.addNode(i);
}
// connect each node to k/2 neighbors
var neighborsSize = Math.floor(k/2 + 1);
for (var j = 1; j < neighborsSize; ++j) {
for (i = 0; i < n; ++i) {
to = (j + i) % n;
g.addLink(i, to);
}
}
// rewire edges from each node
// loop over all nodes in order (label) and neighbors in order (distance)
// no self loops or multiple edges allowed
for (j = 1; j < neighborsSize; ++j) {
for (i = 0; i < n; ++i) {
if (random.nextDouble() < p) {
var from = i;
to = (j + i) % n;
var newTo = random.next(n);
var needsRewire = (newTo === from || g.hasLink(from, newTo));
if (needsRewire && g.getLinks(from).length === n - 1) {
// we cannot rewire this node, it has too many links.
continue;
}
// Enforce no self-loops or multiple edges
while (needsRewire) {
newTo = random.next(n);
needsRewire = (newTo === from || g.hasLink(from, newTo));
}
var link = g.hasLink(from, to);
g.removeLink(link);
g.addLink(from, newTo);
}
}
}
return g;
}
},{"ngraph.graph":13,"ngraph.random":31}],13:[function(require,module,exports){
/**
* @fileOverview Contains definition of the core graph object.
*/
/**
* @example
* var graph = require('ngraph.graph')();
* graph.addNode(1); // graph has one node.
* graph.addLink(2, 3); // now graph contains three nodes and one link.
*
*/
module.exports = createGraph;
var eventify = require('ngraph.events');
/**
* Creates a new graph
*/
function createGraph(options) {
// Graph structure is maintained as dictionary of nodes
// and array of links. Each node has 'links' property which
// hold all links related to that node. And general links
// array is used to speed up all links enumeration. This is inefficient
// in terms of memory, but simplifies coding.
options = options || {};
if (options.uniqueLinkId === undefined) {
// Request each link id to be unique between same nodes. This negatively
// impacts `addLink()` performance (O(n), where n - number of edges of each
// vertex), but makes operations with multigraphs more accessible.
options.uniqueLinkId = true;
}
var nodes = typeof Object.create === 'function' ? Object.create(null) : {},
links = [],
// Hash of multi-edges. Used to track ids of edges between same nodes
multiEdges = {},
nodesCount = 0,
suspendEvents = 0,
forEachNode = createNodeIterator(),
createLink = options.uniqueLinkId ? createUniqueLink : createSingleLink,
// Our graph API provides means to listen to graph changes. Users can subscribe
// to be notified about changes in the graph by using `on` method. However
// in some cases they don't use it. To avoid unnecessary memory consumption
// we will not record graph changes until we have at least one subscriber.
// Code below supports this optimization.
//
// Accumulates all changes made during graph updates.
// Each change element contains:
// changeType - one of the strings: 'add', 'remove' or 'update';
// node - if change is related to node this property is set to changed graph's node;
// link - if change is related to link this property is set to changed graph's link;
changes = [],
recordLinkChange = noop,
recordNodeChange = noop,
enterModification = noop,
exitModification = noop;
// this is our public API:
var graphPart = {
/**
* Adds node to the graph. If node with given id already exists in the graph
* its data is extended with whatever comes in 'data' argument.
*
* @param nodeId the node's identifier. A string or number is preferred.
* @param [data] additional data for the node being added. If node already
* exists its data object is augmented with the new one.
*
* @return {node} The newly added node or node with given id if it already exists.
*/
addNode: addNode,
/**
* Adds a link to the graph. The function always create a new
* link between two nodes. If one of the nodes does not exists
* a new node is created.
*
* @param fromId link start node id;
* @param toId link end node id;
* @param [data] additional data to be set on the new link;
*
* @return {link} The newly created link
*/
addLink: addLink,
/**
* Removes link from the graph. If link does not exist does nothing.
*
* @param link - object returned by addLink() or getLinks() methods.
*
* @returns true if link was removed; false otherwise.
*/
removeLink: removeLink,
/**
* Removes node with given id from the graph. If node does not exist in the graph
* does nothing.
*
* @param nodeId node's identifier passed to addNode() function.
*
* @returns true if node was removed; false otherwise.
*/
removeNode: removeNode,
/**
* Gets node with given identifier. If node does not exist undefined value is returned.
*
* @param nodeId requested node identifier;
*
* @return {node} in with requested identifier or undefined if no such node exists.
*/
getNode: getNode,
/**
* Gets number of nodes in this graph.
*
* @return number of nodes in the graph.
*/
getNodesCount: function() {
return nodesCount;
},
/**
* Gets total number of links in the graph.
*/
getLinksCount: function() {
return links.length;
},
/**
* Gets all links (inbound and outbound) from the node with given id.
* If node with given id is not found null is returned.
*
* @param nodeId requested node identifier.
*
* @return Array of links from and to requested node if such node exists;
* otherwise null is returned.
*/
getLinks: getLinks,
/**
* Invokes callback on each node of the graph.
*
* @param {Function(node)} callback Function to be invoked. The function
* is passed one argument: visited node.
*/
forEachNode: forEachNode,
/**
* Invokes callback on every linked (adjacent) node to the given one.
*
* @param nodeId Identifier of the requested node.
* @param {Function(node, link)} callback Function to be called on all linked nodes.
* The function is passed two parameters: adjacent node and link object itself.
* @param oriented if true graph treated as oriented.
*/
forEachLinkedNode: forEachLinkedNode,
/**
* Enumerates all links in the graph
*
* @param {Function(link)} callback Function to be called on all links in the graph.
* The function is passed one parameter: graph's link object.
*
* Link object contains at least the following fields:
* fromId - node id where link starts;
* toId - node id where link ends,
* data - additional data passed to graph.addLink() method.
*/
forEachLink: forEachLink,
/**
* Suspend all notifications about graph changes until
* endUpdate is called.
*/
beginUpdate: enterModification,
/**
* Resumes all notifications about graph changes and fires
* graph 'changed' event in case there are any pending changes.
*/
endUpdate: exitModification,
/**
* Removes all nodes and links from the graph.
*/
clear: clear,
/**
* Detects whether there is a link between two nodes.
* Operation complexity is O(n) where n - number of links of a node.
* NOTE: this function is synonim for getLink()
*
* @returns link if there is one. null otherwise.
*/
hasLink: getLink,
/**
* Gets an edge between two nodes.
* Operation complexity is O(n) where n - number of links of a node.
*
* @param {string} fromId link start identifier
* @param {string} toId link end identifier
*
* @returns link if there is one. null otherwise.
*/
getLink: getLink
};
// this will add `on()` and `fire()` methods.
eventify(graphPart);
monitorSubscribers();
return graphPart;
function monitorSubscribers() {
var realOn = graphPart.on;
// replace real `on` with our temporary on, which will trigger change
// modification monitoring:
graphPart.on = on;
function on() {
// now it's time to start tracking stuff:
graphPart.beginUpdate = enterModification = enterModificationReal;
graphPart.endUpdate = exitModification = exitModificationReal;
recordLinkChange = recordLinkChangeReal;
recordNodeChange = recordNodeChangeReal;
// this will replace current `on` method with real pub/sub from `eventify`.
graphPart.on = realOn;
// delegate to real `on` handler:
return realOn.apply(graphPart, arguments);
}
}
function recordLinkChangeReal(link, changeType) {
changes.push({
link: link,
changeType: changeType
});
}
function recordNodeChangeReal(node, changeType) {
changes.push({
node: node,
changeType: changeType
});
}
function addNode(nodeId, data) {
if (nodeId === undefined) {
throw new Error('Invalid node identifier');
}
enterModification();
var node = getNode(nodeId);
if (!node) {
node = new Node(nodeId);
nodesCount++;
recordNodeChange(node, 'add');
} else {
recordNodeChange(node, 'update');
}
node.data = data;
nodes[nodeId] = node;
exitModification();
return node;
}
function getNode(nodeId) {
return nodes[nodeId];
}
function removeNode(nodeId) {
var node = getNode(nodeId);
if (!node) {
return false;
}
enterModification();
if (node.links) {
while (node.links.length) {
var link = node.links[0];
removeLink(link);
}
}
delete nodes[nodeId];
nodesCount--;
recordNodeChange(node, 'remove');
exitModification();
return true;
}
function addLink(fromId, toId, data) {
enterModification();
var fromNode = getNode(fromId) || addNode(fromId);
var toNode = getNode(toId) || addNode(toId);
var link = createLink(fromId, toId, data);
links.push(link);
// TODO: this is not cool. On large graphs potentially would consume more memory.
addLinkToNode(fromNode, link);
if (fromId !== toId) {
// make sure we are not duplicating links for self-loops
addLinkToNode(toNode, link);
}
recordLinkChange(link, 'add');
exitModification();
return link;
}
function createSingleLink(fromId, toId, data) {
var linkId = makeLinkId(fromId, toId);
return new Link(fromId, toId, data, linkId);
}
function createUniqueLink(fromId, toId, data) {
// TODO: Get rid of this method.
var linkId = makeLinkId(fromId, toId);
var isMultiEdge = multiEdges.hasOwnProperty(linkId);
if (isMultiEdge || getLink(fromId, toId)) {
if (!isMultiEdge) {
multiEdges[linkId] = 0;
}
var suffix = '@' + (++multiEdges[linkId]);
linkId = makeLinkId(fromId + suffix, toId + suffix);
}
return new Link(fromId, toId, data, linkId);
}
function getLinks(nodeId) {
var node = getNode(nodeId);
return node ? node.links : null;
}
function removeLink(link) {
if (!link) {
return false;
}
var idx = indexOfElementInArray(link, links);
if (idx < 0) {
return false;
}
enterModification();
links.splice(idx, 1);
var fromNode = getNode(link.fromId);
var toNode = getNode(link.toId);
if (fromNode) {
idx = indexOfElementInArray(link, fromNode.links);
if (idx >= 0) {
fromNode.links.splice(idx, 1);
}
}
if (toNode) {
idx = indexOfElementInArray(link, toNode.links);
if (idx >= 0) {
toNode.links.splice(idx, 1);
}
}
recordLinkChange(link, 'remove');
exitModification();
return true;
}
function getLink(fromNodeId, toNodeId) {
// TODO: Use sorted links to speed this up
var node = getNode(fromNodeId),
i;
if (!node || !node.links) {
return null;
}
for (i = 0; i < node.links.length; ++i) {
var link = node.links[i];
if (link.fromId === fromNodeId && link.toId === toNodeId) {
return link;
}
}
return null; // no link.
}
function clear() {
enterModification();
forEachNode(function(node) {
removeNode(node.id);
});
exitModification();
}
function forEachLink(callback) {
var i, length;
if (typeof callback === 'function') {
for (i = 0, length = links.length; i < length; ++i) {
callback(links[i]);
}
}
}
function forEachLinkedNode(nodeId, callback, oriented) {
var node = getNode(nodeId);
if (node && node.links && typeof callback === 'function') {
if (oriented) {
return forEachOrientedLink(node.links, nodeId, callback);
} else {
return forEachNonOrientedLink(node.links, nodeId, callback);
}
}
}
function forEachNonOrientedLink(links, nodeId, callback) {
var quitFast;
for (var i = 0; i < links.length; ++i) {
var link = links[i];
var linkedNodeId = link.fromId === nodeId ? link.toId : link.fromId;
quitFast = callback(nodes[linkedNodeId], link);
if (quitFast) {
return true; // Client does not need more iterations. Break now.
}
}
}
function forEachOrientedLink(links, nodeId, callback) {
var quitFast;
for (var i = 0; i < links.length; ++i) {
var link = links[i];
if (link.fromId === nodeId) {
quitFast = callback(nodes[link.toId], link);
if (quitFast) {
return true; // Client does not need more iterations. Break now.
}
}
}
}
// we will not fire anything until users of this library explicitly call `on()`
// method.
function noop() {}
// Enter, Exit modification allows bulk graph updates without firing events.
function enterModificationReal() {
suspendEvents += 1;
}
function exitModificationReal() {
suspendEvents -= 1;
if (suspendEvents === 0 && changes.length > 0) {
graphPart.fire('changed', changes);
changes.length = 0;
}
}
function createNodeIterator() {
// Object.keys iterator is 1.3x faster than `for in` loop.
// See `https://github.com/anvaka/ngraph.graph/tree/bench-for-in-vs-obj-keys`
// branch for perf test
return Object.keys ? objectKeysIterator : forInIterator;
}
function objectKeysIterator(callback) {
if (typeof callback !== 'function') {
return;
}
var keys = Object.keys(nodes);
for (var i = 0; i < keys.length; ++i) {
if (callback(nodes[keys[i]])) {
return true; // client doesn't want to proceed. Return.
}
}
}
function forInIterator(callback) {
if (typeof callback !== 'function') {
return;
}
var node;
for (node in nodes) {
if (callback(nodes[node])) {
return true; // client doesn't want to proceed. Return.
}
}
}
}
// need this for old browsers. Should this be a separate module?
function indexOfElementInArray(element, array) {
if (!array) return -1;
if (array.indexOf) {
return array.indexOf(element);
}
var len = array.length,
i;
for (i = 0; i < len; i += 1) {
if (array[i] === element) {
return i;
}
}
return -1;
}
/**
* Internal structure to represent node;
*/
function Node(id) {
this.id = id;
this.links = null;
this.data = null;
}
function addLinkToNode(node, link) {
if (node.links) {
node.links.push(link);
} else {
node.links = [link];
}
}
/**
* Internal structure to represent links;
*/
function Link(fromId, toId, data, id) {
this.fromId = fromId;
this.toId = toId;
this.data = data;
this.id = id;
}
function hashCode(str) {
var hash = 0, i, chr, len;
if (str.length == 0) return hash;
for (i = 0, len = str.length; i < len; i++) {
chr = str.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
}
function makeLinkId(fromId, toId) {
return hashCode(fromId.toString() + '👉 ' + toId.toString());
}
},{"ngraph.events":9}],14:[function(require,module,exports){
module.exports = merge;
/**
* Augments `target` with properties in `options`. Does not override
* target's properties if they are defined and matches expected type in
* options
*
* @returns {Object} merged object
*/
function merge(target, options) {
var key;
if (!target) { target = {}; }
if (options) {
for (key in options) {
if (options.hasOwnProperty(key)) {
var targetHasIt = target.hasOwnProperty(key),
optionsValueType = typeof options[key],
shouldReplace = !targetHasIt || (typeof target[key] !== optionsValueType);
if (shouldReplace) {
target[key] = options[key];
} else if (optionsValueType === 'object') {
// go deep, don't care about loops here, we are simple API!:
target[key] = merge(target[key], options[key]);
}
}
}
}
return target;
}
},{}],15:[function(require,module,exports){
module.exports = {
Body: Body,
Vector2d: Vector2d,
Body3d: Body3d,
Vector3d: Vector3d
};
function Body(x, y) {
this.pos = new Vector2d(x, y);
this.prevPos = new Vector2d(x, y);
this.force = new Vector2d();
this.velocity = new Vector2d();
this.mass = 1;
}
Body.prototype.setPosition = function (x, y) {
this.prevPos.x = this.pos.x = x;
this.prevPos.y = this.pos.y = y;
};
function Vector2d(x, y) {
if (x && typeof x !== 'number') {
// could be another vector
this.x = typeof x.x === 'number' ? x.x : 0;
this.y = typeof x.y === 'number' ? x.y : 0;
} else {
this.x = typeof x === 'number' ? x : 0;
this.y = typeof y === 'number' ? y : 0;
}
}
Vector2d.prototype.reset = function () {
this.x = this.y = 0;
};
function Body3d(x, y, z) {
this.pos = new Vector3d(x, y, z);
this.prevPos = new Vector3d(x, y, z);
this.force = new Vector3d();
this.velocity = new Vector3d();
this.mass = 1;
}
Body3d.prototype.setPosition = function (x, y, z) {
this.prevPos.x = this.pos.x = x;
this.prevPos.y = this.pos.y = y;
this.prevPos.z = this.pos.z = z;
};
function Vector3d(x, y, z) {
if (x && typeof x !== 'number') {
// could be another vector
this.x = typeof x.x === 'number' ? x.x : 0;
this.y = typeof x.y === 'number' ? x.y : 0;
this.z = typeof x.z === 'number' ? x.z : 0;
} else {
this.x = typeof x === 'number' ? x : 0;
this.y = typeof y === 'number' ? y : 0;
this.z = typeof z === 'number' ? z : 0;
}
};
Vector3d.prototype.reset = function () {
this.x = this.y = this.z = 0;
};
},{}],16:[function(require,module,exports){
/**
* Manages a simulation of physical forces acting on bodies and springs.
*/
module.exports = physicsSimulator;
function physicsSimulator(settings) {
var Spring = require('./lib/spring');
var expose = require('ngraph.expose');
var merge = require('ngraph.merge');
var eventify = require('ngraph.events');
settings = merge(settings, {
/**
* Ideal length for links (springs in physical model).
*/
springLength: 30,
/**
* Hook's law coefficient. 1 - solid spring.
*/
springCoeff: 0.0008,
/**
* Coulomb's law coefficient. It's used to repel nodes thus should be negative
* if you make it positive nodes start attract each other :).
*/
gravity: -1.2,
/**
* Theta coefficient from Barnes Hut simulation. Ranged between (0, 1).
* The closer it's to 1 the more nodes algorithm will have to go through.
* Setting it to one makes Barnes Hut simulation no different from
* brute-force forces calculation (each node is considered).
*/
theta: 0.8,
/**
* Drag force coefficient. Used to slow down system, thus should be less than 1.
* The closer it is to 0 the less tight system will be.
*/
dragCoeff: 0.02,
/**
* Default time step (dt) for forces integration
*/
timeStep : 20,
/**
* Maximum movement of the system which can be considered as stabilized
*/
stableThreshold: 0.009
});
// We allow clients to override basic factory methods:
var createQuadTree = settings.createQuadTree || require('ngraph.quadtreebh');
var createBounds = settings.createBounds || require('./lib/bounds');
var createDragForce = settings.createDragForce || require('./lib/dragForce');
var createSpringForce = settings.createSpringForce || require('./lib/springForce');
var integrate = settings.integrator || require('./lib/eulerIntegrator');
var createBody = settings.createBody || require('./lib/createBody');
var bodies = [], // Bodies in this simulation.
springs = [], // Springs in this simulation.
quadTree = createQuadTree(settings),
bounds = createBounds(bodies, settings),
springForce = createSpringForce(settings),
dragForce = createDragForce(settings);
var totalMovement = 0; // how much movement we made on last step
var lastStable = false; // indicates whether system was stable on last step() call
var publicApi = {
/**
* Array of bodies, registered with current simulator
*
* Note: To add new body, use addBody() method. This property is only
* exposed for testing/performance purposes.
*/
bodies: bodies,
/**
* Array of springs, registered with current simulator
*
* Note: To add new spri