ngraph.forcelayout
Version:
Force directed graph drawing layout
1,640 lines (1,381 loc) • 54.9 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ngraphCreateLayout = f()}})(function(){var define,module,exports;return (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){
module.exports = createLayout;
module.exports.simulator = require('./lib/createPhysicsSimulator');
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 = (physicsSettings && physicsSettings.createSimulator) || require('./lib/createPhysicsSimulator');
var physicsSimulator = createSimulator(physicsSettings);
if (Array.isArray(physicsSettings)) throw new Error('Physics settings is expected to be an object');
// Starting from v20 of ngraph we use `Set` instead of `Array` for `node.links`
var nodeMass = if (graph.version !== undefined && graph.version >= 20) ? defaultNodeMassWithSet : defaultNodeMass;
if (physicsSettings && typeof physicsSettings.nodeMass === 'function') {
nodeMass = physicsSettings.nodeMass;
}
var nodeBodies = new Map();
var springs = {};
var bodiesCount = 0;
var springTransform = physicsSimulator.settings.springTransform || noop;
// Initialize physics with what we have in the graph:
initPhysics();
listenToEvents();
var wasStable = false;
var api = {
/**
* Performs one step of iterative layout algorithm
*
* @returns {boolean} true if the system should be considered stable; False otherwise.
* The system is stable if no further call to `step()` can improve the layout.
*/
step: function() {
if (bodiesCount === 0) {
updateStableStatus(true);
return true;
}
var lastMove = physicsSimulator.step();
// Save the movement in case if someone wants to query it in the step
// callback.
api.lastMove = lastMove;
// Allow listeners to perform low-level actions after nodes are updated.
api.fire('step');
var ratio = lastMove/bodiesCount;
var isStableNow = ratio <= 0.01; // TODO: The number is somewhat arbitrary...
updateStableStatus(isStableNow);
return isStableNow;
},
/**
* 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();
},
/**
* Iterates over each body in the layout simulator and performs a callback(body, nodeId)
*/
forEachBody: forEachBody,
/*
* 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);
api.fire('disposed');
},
/**
* 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 treated as tail of the link (fromId)
*/
getSpring: getSpring,
/**
* Returns length of cumulative force vector. The closer this to zero - the more stable the system is
*/
getForceVectorLength: getForceVectorLength,
/**
* [Read only] Gets current physics simulator
*/
simulator: physicsSimulator,
/**
* Gets the graph that was used for layout
*/
graph: graph,
/**
* Gets amount of movement performed during last step operation
*/
lastMove: 0
};
eventify(api);
return api;
function updateStableStatus(isStableNow) {
if (wasStable !== isStableNow) {
wasStable = isStableNow;
onStableChanged(isStableNow);
}
}
function forEachBody(cb) {
nodeBodies.forEach(cb);
}
function getForceVectorLength() {
var fx = 0, fy = 0;
forEachBody(function(body) {
fx += Math.abs(body.force.x);
fy += Math.abs(body.force.y);
});
return Math.sqrt(fx * fx + fy * fy);
}
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.get(nodeId);
}
function listenToEvents() {
graph.on('changed', onGraphChanged);
}
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);
}
}
}
bodiesCount = graph.getNodesCount();
}
function initPhysics() {
bodiesCount = 0;
graph.forEachNode(function (node) {
initBody(node.id);
bodiesCount += 1;
});
graph.forEachLink(initLink);
}
function initBody(nodeId) {
var body = nodeBodies.get(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);
body.id = nodeId;
nodeBodies.set(nodeId, body);
updateBodyMass(nodeId);
if (isNodeOriginallyPinned(node)) {
body.isPinned = true;
}
}
}
function releaseNode(node) {
var nodeId = node.id;
var body = nodeBodies.get(nodeId);
if (body) {
nodeBodies.delete(nodeId);
physicsSimulator.removeBody(body);
}
}
function initLink(link) {
updateBodyMass(link.fromId);
updateBodyMass(link.toId);
var fromBody = nodeBodies.get(link.fromId),
toBody = nodeBodies.get(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.get(link.fromId) : nodeBodies.get(link.toId);
if (otherBody && otherBody.pos) {
neighbors.push(otherBody);
}
}
return neighbors;
}
function updateBodyMass(nodeId) {
var body = nodeBodies.get(nodeId);
body.mass = nodeMass(nodeId);
if (Number.isNaN(body.mass)) {
throw new Error('Node mass should be a number');
}
}
/**
* Checks whether graph node has in its settings pinned attribute,
* which means layout algorithm cannot move it. Node can be marked
* 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.get(nodeId);
if (!body) {
initBody(nodeId);
body = nodeBodies.get(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 defaultNodeMass(nodeId) {
var links = graph.getLinks(nodeId);
if (!links) return 1;
return 1 + links.length / 3.0;
}
function defaultNodeMassWithSet(nodeId) {
var links = graph.getLinks(nodeId);
if (!links) return 1;
return 1 + links.size / 3.0;
}
}
function noop() { }
},{"./lib/createPhysicsSimulator":10,"ngraph.events":12}],2:[function(require,module,exports){
const getVariableName = require('./getVariableName');
module.exports = function createPatternBuilder(dimension) {
return pattern;
function pattern(template, config) {
let indent = (config && config.indent) || 0;
let join = (config && config.join !== undefined) ? config.join : '\n';
let indentString = Array(indent + 1).join(' ');
let buffer = [];
for (let i = 0; i < dimension; ++i) {
let variableName = getVariableName(i);
let prefix = (i === 0) ? '' : indentString;
buffer.push(prefix + template.replace(/{var}/g, variableName));
}
return buffer.join(join);
}
};
},{"./getVariableName":9}],3:[function(require,module,exports){
module.exports = generateBoundsFunction;
module.exports.generateFunctionBody = generateBoundsFunctionBody;
const createPatternBuilder = require('./createPatternBuilder');
function generateBoundsFunction(dimension) {
let code = generateBoundsFunctionBody(dimension);
return new Function('bodies', 'settings', 'random', code);
}
function generateBoundsFunctionBody(dimension) {
let pattern = createPatternBuilder(dimension);
let code = `
var boundingBox = {
${pattern('min_{var}: 0, max_{var}: 0,', {indent: 4})}
};
return {
box: boundingBox,
update: updateBoundingBox,
reset: resetBoundingBox,
getBestNewPosition: function (neighbors) {
var ${pattern('base_{var} = 0', {join: ', '})};
if (neighbors.length) {
for (var i = 0; i < neighbors.length; ++i) {
let neighborPos = neighbors[i].pos;
${pattern('base_{var} += neighborPos.{var};', {indent: 10})}
}
${pattern('base_{var} /= neighbors.length;', {indent: 8})}
} else {
${pattern('base_{var} = (boundingBox.min_{var} + boundingBox.max_{var}) / 2;', {indent: 8})}
}
var springLength = settings.springLength;
return {
${pattern('{var}: base_{var} + (random.nextDouble() - 0.5) * springLength,', {indent: 8})}
};
}
};
function updateBoundingBox() {
var i = bodies.length;
if (i === 0) return; // No bodies - no borders.
${pattern('var max_{var} = -Infinity;', {indent: 4})}
${pattern('var min_{var} = Infinity;', {indent: 4})}
while(i--) {
// this is O(n), it could be done faster with quadtree, if we check the root node bounds
var bodyPos = bodies[i].pos;
${pattern('if (bodyPos.{var} < min_{var}) min_{var} = bodyPos.{var};', {indent: 6})}
${pattern('if (bodyPos.{var} > max_{var}) max_{var} = bodyPos.{var};', {indent: 6})}
}
${pattern('boundingBox.min_{var} = min_{var};', {indent: 4})}
${pattern('boundingBox.max_{var} = max_{var};', {indent: 4})}
}
function resetBoundingBox() {
${pattern('boundingBox.min_{var} = boundingBox.max_{var} = 0;', {indent: 4})}
}
`;
return code;
}
},{"./createPatternBuilder":2}],4:[function(require,module,exports){
const createPatternBuilder = require('./createPatternBuilder');
module.exports = generateCreateBodyFunction;
module.exports.generateCreateBodyFunctionBody = generateCreateBodyFunctionBody;
// InlineTransform: getVectorCode
module.exports.getVectorCode = getVectorCode;
// InlineTransform: getBodyCode
module.exports.getBodyCode = getBodyCode;
// InlineTransformExport: module.exports = function() { return Body; }
function generateCreateBodyFunction(dimension, debugSetters) {
let code = generateCreateBodyFunctionBody(dimension, debugSetters);
let {Body} = (new Function(code))();
return Body;
}
function generateCreateBodyFunctionBody(dimension, debugSetters) {
let code = `
${getVectorCode(dimension, debugSetters)}
${getBodyCode(dimension, debugSetters)}
return {Body: Body, Vector: Vector};
`;
return code;
}
function getBodyCode(dimension) {
let pattern = createPatternBuilder(dimension);
let variableList = pattern('{var}', {join: ', '});
return `
function Body(${variableList}) {
this.isPinned = false;
this.pos = new Vector(${variableList});
this.force = new Vector();
this.velocity = new Vector();
this.mass = 1;
this.springCount = 0;
this.springLength = 0;
}
Body.prototype.reset = function() {
this.force.reset();
this.springCount = 0;
this.springLength = 0;
}
Body.prototype.setPosition = function (${variableList}) {
${pattern('this.pos.{var} = {var} || 0;', {indent: 2})}
};`;
}
function getVectorCode(dimension, debugSetters) {
let pattern = createPatternBuilder(dimension);
let setters = '';
if (debugSetters) {
setters = `${pattern("\n\
var v{var};\n\
Object.defineProperty(this, '{var}', {\n\
set: function(v) { \n\
if (!Number.isFinite(v)) throw new Error('Cannot set non-numbers to {var}');\n\
v{var} = v; \n\
},\n\
get: function() { return v{var}; }\n\
});")}`;
}
let variableList = pattern('{var}', {join: ', '});
return `function Vector(${variableList}) {
${setters}
if (typeof arguments[0] === 'object') {
// could be another vector
let v = arguments[0];
${pattern('if (!Number.isFinite(v.{var})) throw new Error("Expected value is not a finite number at Vector constructor ({var})");', {indent: 4})}
${pattern('this.{var} = v.{var};', {indent: 4})}
} else {
${pattern('this.{var} = typeof {var} === "number" ? {var} : 0;', {indent: 4})}
}
}
Vector.prototype.reset = function () {
${pattern('this.{var} = ', {join: ''})}0;
};`;
}
},{"./createPatternBuilder":2}],5:[function(require,module,exports){
const createPatternBuilder = require('./createPatternBuilder');
module.exports = generateCreateDragForceFunction;
module.exports.generateCreateDragForceFunctionBody = generateCreateDragForceFunctionBody;
function generateCreateDragForceFunction(dimension) {
let code = generateCreateDragForceFunctionBody(dimension);
return new Function('options', code);
}
function generateCreateDragForceFunctionBody(dimension) {
let pattern = createPatternBuilder(dimension);
let code = `
if (!Number.isFinite(options.dragCoefficient)) throw new Error('dragCoefficient is not a finite number');
return {
update: function(body) {
${pattern('body.force.{var} -= options.dragCoefficient * body.velocity.{var};', {indent: 6})}
}
};
`;
return code;
}
},{"./createPatternBuilder":2}],6:[function(require,module,exports){
const createPatternBuilder = require('./createPatternBuilder');
module.exports = generateCreateSpringForceFunction;
module.exports.generateCreateSpringForceFunctionBody = generateCreateSpringForceFunctionBody;
function generateCreateSpringForceFunction(dimension) {
let code = generateCreateSpringForceFunctionBody(dimension);
return new Function('options', 'random', code);
}
function generateCreateSpringForceFunctionBody(dimension) {
let pattern = createPatternBuilder(dimension);
let code = `
if (!Number.isFinite(options.springCoefficient)) throw new Error('Spring coefficient is not a number');
if (!Number.isFinite(options.springLength)) throw new Error('Spring length is not a number');
return {
/**
* Updates forces acting on a spring
*/
update: function (spring) {
var body1 = spring.from;
var body2 = spring.to;
var length = spring.length < 0 ? options.springLength : spring.length;
${pattern('var d{var} = body2.pos.{var} - body1.pos.{var};', {indent: 6})}
var r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})});
if (r === 0) {
${pattern('d{var} = (random.nextDouble() - 0.5) / 50;', {indent: 8})}
r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})});
}
var d = r - length;
var coefficient = ((spring.coefficient > 0) ? spring.coefficient : options.springCoefficient) * d / r;
${pattern('body1.force.{var} += coefficient * d{var}', {indent: 6})};
body1.springCount += 1;
body1.springLength += r;
${pattern('body2.force.{var} -= coefficient * d{var}', {indent: 6})};
body2.springCount += 1;
body2.springLength += r;
}
};
`;
return code;
}
},{"./createPatternBuilder":2}],7:[function(require,module,exports){
const createPatternBuilder = require('./createPatternBuilder');
module.exports = generateIntegratorFunction;
module.exports.generateIntegratorFunctionBody = generateIntegratorFunctionBody;
function generateIntegratorFunction(dimension) {
let code = generateIntegratorFunctionBody(dimension);
return new Function('bodies', 'timeStep', 'adaptiveTimeStepWeight', code);
}
function generateIntegratorFunctionBody(dimension) {
let pattern = createPatternBuilder(dimension);
let code = `
var length = bodies.length;
if (length === 0) return 0;
${pattern('var d{var} = 0, t{var} = 0;', {indent: 2})}
for (var i = 0; i < length; ++i) {
var body = bodies[i];
if (body.isPinned) continue;
if (adaptiveTimeStepWeight && body.springCount) {
timeStep = (adaptiveTimeStepWeight * body.springLength/body.springCount);
}
var coeff = timeStep / body.mass;
${pattern('body.velocity.{var} += coeff * body.force.{var};', {indent: 4})}
${pattern('var v{var} = body.velocity.{var};', {indent: 4})}
var v = Math.sqrt(${pattern('v{var} * v{var}', {join: ' + '})});
if (v > 1) {
// We normalize it so that we move within timeStep range.
// for the case when v <= 1 - we let velocity to fade out.
${pattern('body.velocity.{var} = v{var} / v;', {indent: 6})}
}
${pattern('d{var} = timeStep * body.velocity.{var};', {indent: 4})}
${pattern('body.pos.{var} += d{var};', {indent: 4})}
${pattern('t{var} += Math.abs(d{var});', {indent: 4})}
}
return (${pattern('t{var} * t{var}', {join: ' + '})})/length;
`;
return code;
}
},{"./createPatternBuilder":2}],8:[function(require,module,exports){
const createPatternBuilder = require('./createPatternBuilder');
const getVariableName = require('./getVariableName');
module.exports = generateQuadTreeFunction;
module.exports.generateQuadTreeFunctionBody = generateQuadTreeFunctionBody;
// These exports are for InlineTransform tool.
// InlineTransform: getInsertStackCode
module.exports.getInsertStackCode = getInsertStackCode;
// InlineTransform: getQuadNodeCode
module.exports.getQuadNodeCode = getQuadNodeCode;
// InlineTransform: isSamePosition
module.exports.isSamePosition = isSamePosition;
// InlineTransform: getChildBodyCode
module.exports.getChildBodyCode = getChildBodyCode;
// InlineTransform: setChildBodyCode
module.exports.setChildBodyCode = setChildBodyCode;
function generateQuadTreeFunction(dimension) {
let code = generateQuadTreeFunctionBody(dimension);
return (new Function(code))();
}
function generateQuadTreeFunctionBody(dimension) {
let pattern = createPatternBuilder(dimension);
let quadCount = Math.pow(2, dimension);
let code = `
${getInsertStackCode()}
${getQuadNodeCode(dimension)}
${isSamePosition(dimension)}
${getChildBodyCode(dimension)}
${setChildBodyCode(dimension)}
function createQuadTree(options, random) {
options = options || {};
options.gravity = typeof options.gravity === 'number' ? options.gravity : -1;
options.theta = typeof options.theta === 'number' ? options.theta : 0.8;
var gravity = options.gravity;
var updateQueue = [];
var insertStack = new InsertStack();
var theta = options.theta;
var nodesCache = [];
var currentInCache = 0;
var root = newNode();
return {
insertBodies: insertBodies,
/**
* Gets root node if it is present
*/
getRoot: function() {
return root;
},
updateBodyForce: update,
options: function(newOptions) {
if (newOptions) {
if (typeof newOptions.gravity === 'number') {
gravity = newOptions.gravity;
}
if (typeof newOptions.theta === 'number') {
theta = newOptions.theta;
}
return this;
}
return {
gravity: gravity,
theta: theta
};
}
};
function newNode() {
// To avoid pressure on GC we reuse nodes.
var node = nodesCache[currentInCache];
if (node) {
${assignQuads(' node.')}
node.body = null;
node.mass = ${pattern('node.mass_{var} = ', {join: ''})}0;
${pattern('node.min_{var} = node.max_{var} = ', {join: ''})}0;
} else {
node = new QuadNode();
nodesCache[currentInCache] = node;
}
++currentInCache;
return node;
}
function update(sourceBody) {
var queue = updateQueue;
var v;
${pattern('var d{var};', {indent: 4})}
var r;
${pattern('var f{var} = 0;', {indent: 4})}
var queueLength = 1;
var shiftIdx = 0;
var pushIdx = 1;
queue[0] = root;
while (queueLength) {
var node = queue[shiftIdx];
var body = node.body;
queueLength -= 1;
shiftIdx += 1;
var differentBody = (body !== sourceBody);
if (body && differentBody) {
// If the current node is a leaf node (and it is not source body),
// calculate the force exerted by the current node on body, and add this
// amount to body's net force.
${pattern('d{var} = body.pos.{var} - sourceBody.pos.{var};', {indent: 8})}
r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})});
if (r === 0) {
// Poor man's protection against zero distance.
${pattern('d{var} = (random.nextDouble() - 0.5) / 50;', {indent: 10})}
r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})});
}
// This is standard gravitation force calculation but we divide
// by r^3 to save two operations when normalizing force vector.
v = gravity * body.mass * sourceBody.mass / (r * r * r);
${pattern('f{var} += v * d{var};', {indent: 8})}
} else if (differentBody) {
// Otherwise, calculate the ratio s / r, where s is the width of the region
// represented by the internal node, and r is the distance between the body
// and the node's center-of-mass
${pattern('d{var} = node.mass_{var} / node.mass - sourceBody.pos.{var};', {indent: 8})}
r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})});
if (r === 0) {
// Sorry about code duplication. I don't want to create many functions
// right away. Just want to see performance first.
${pattern('d{var} = (random.nextDouble() - 0.5) / 50;', {indent: 10})}
r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})});
}
// If s / r < θ, treat this internal node as a single body, and calculate the
// force it exerts on sourceBody, and add this amount to sourceBody's net force.
if ((node.max_${getVariableName(0)} - node.min_${getVariableName(0)}) / r < theta) {
// in the if statement above we consider node's width only
// because the region was made into square during tree creation.
// Thus there is no difference between using width or height.
v = gravity * node.mass * sourceBody.mass / (r * r * r);
${pattern('f{var} += v * d{var};', {indent: 10})}
} else {
// Otherwise, run the procedure recursively on each of the current node's children.
// I intentionally unfolded this loop, to save several CPU cycles.
${runRecursiveOnChildren()}
}
}
}
${pattern('sourceBody.force.{var} += f{var};', {indent: 4})}
}
function insertBodies(bodies) {
${pattern('var {var}min = Number.MAX_VALUE;', {indent: 4})}
${pattern('var {var}max = Number.MIN_VALUE;', {indent: 4})}
var i = bodies.length;
// To reduce quad tree depth we are looking for exact bounding box of all particles.
while (i--) {
var pos = bodies[i].pos;
${pattern('if (pos.{var} < {var}min) {var}min = pos.{var};', {indent: 6})}
${pattern('if (pos.{var} > {var}max) {var}max = pos.{var};', {indent: 6})}
}
// Makes the bounds square.
var maxSideLength = -Infinity;
${pattern('if ({var}max - {var}min > maxSideLength) maxSideLength = {var}max - {var}min ;', {indent: 4})}
currentInCache = 0;
root = newNode();
${pattern('root.min_{var} = {var}min;', {indent: 4})}
${pattern('root.max_{var} = {var}min + maxSideLength;', {indent: 4})}
i = bodies.length - 1;
if (i >= 0) {
root.body = bodies[i];
}
while (i--) {
insert(bodies[i], root);
}
}
function insert(newBody) {
insertStack.reset();
insertStack.push(root, newBody);
while (!insertStack.isEmpty()) {
var stackItem = insertStack.pop();
var node = stackItem.node;
var body = stackItem.body;
if (!node.body) {
// This is internal node. Update the total mass of the node and center-of-mass.
${pattern('var {var} = body.pos.{var};', {indent: 8})}
node.mass += body.mass;
${pattern('node.mass_{var} += body.mass * {var};', {indent: 8})}
// Recursively insert the body in the appropriate quadrant.
// But first find the appropriate quadrant.
var quadIdx = 0; // Assume we are in the 0's quad.
${pattern('var min_{var} = node.min_{var};', {indent: 8})}
${pattern('var max_{var} = (min_{var} + node.max_{var}) / 2;', {indent: 8})}
${assignInsertionQuadIndex(8)}
var child = getChild(node, quadIdx);
if (!child) {
// The node is internal but this quadrant is not taken. Add
// subnode to it.
child = newNode();
${pattern('child.min_{var} = min_{var};', {indent: 10})}
${pattern('child.max_{var} = max_{var};', {indent: 10})}
child.body = body;
setChild(node, quadIdx, child);
} else {
// continue searching in this quadrant.
insertStack.push(child, body);
}
} else {
// We are trying to add to the leaf node.
// We have to convert current leaf into internal node
// and continue adding two nodes.
var oldBody = node.body;
node.body = null; // internal nodes do not cary bodies
if (isSamePosition(oldBody.pos, body.pos)) {
// Prevent infinite subdivision by bumping one node
// anywhere in this quadrant
var retriesCount = 3;
do {
var offset = random.nextDouble();
${pattern('var d{var} = (node.max_{var} - node.min_{var}) * offset;', {indent: 12})}
${pattern('oldBody.pos.{var} = node.min_{var} + d{var};', {indent: 12})}
retriesCount -= 1;
// Make sure we don't bump it out of the box. If we do, next iteration should fix it
} while (retriesCount > 0 && isSamePosition(oldBody.pos, body.pos));
if (retriesCount === 0 && isSamePosition(oldBody.pos, body.pos)) {
// This is very bad, we ran out of precision.
// if we do not return from the method we'll get into
// infinite loop here. So we sacrifice correctness of layout, and keep the app running
// Next layout iteration should get larger bounding box in the first step and fix this
return;
}
}
// Next iteration should subdivide node further.
insertStack.push(node, oldBody);
insertStack.push(node, body);
}
}
}
}
return createQuadTree;
`;
return code;
function assignInsertionQuadIndex(indentCount) {
let insertionCode = [];
let indent = Array(indentCount + 1).join(' ');
for (let i = 0; i < dimension; ++i) {
insertionCode.push(indent + `if (${getVariableName(i)} > max_${getVariableName(i)}) {`);
insertionCode.push(indent + ` quadIdx = quadIdx + ${Math.pow(2, i)};`);
insertionCode.push(indent + ` min_${getVariableName(i)} = max_${getVariableName(i)};`);
insertionCode.push(indent + ` max_${getVariableName(i)} = node.max_${getVariableName(i)};`);
insertionCode.push(indent + `}`);
}
return insertionCode.join('\n');
// if (x > max_x) { // somewhere in the eastern part.
// quadIdx = quadIdx + 1;
// left = right;
// right = node.right;
// }
}
function runRecursiveOnChildren() {
let indent = Array(11).join(' ');
let recursiveCode = [];
for (let i = 0; i < quadCount; ++i) {
recursiveCode.push(indent + `if (node.quad${i}) {`);
recursiveCode.push(indent + ` queue[pushIdx] = node.quad${i};`);
recursiveCode.push(indent + ` queueLength += 1;`);
recursiveCode.push(indent + ` pushIdx += 1;`);
recursiveCode.push(indent + `}`);
}
return recursiveCode.join('\n');
// if (node.quad0) {
// queue[pushIdx] = node.quad0;
// queueLength += 1;
// pushIdx += 1;
// }
}
function assignQuads(indent) {
// this.quad0 = null;
// this.quad1 = null;
// this.quad2 = null;
// this.quad3 = null;
let quads = [];
for (let i = 0; i < quadCount; ++i) {
quads.push(`${indent}quad${i} = null;`);
}
return quads.join('\n');
}
}
function isSamePosition(dimension) {
let pattern = createPatternBuilder(dimension);
return `
function isSamePosition(point1, point2) {
${pattern('var d{var} = Math.abs(point1.{var} - point2.{var});', {indent: 2})}
return ${pattern('d{var} < 1e-8', {join: ' && '})};
}
`;
}
function setChildBodyCode(dimension) {
var quadCount = Math.pow(2, dimension);
return `
function setChild(node, idx, child) {
${setChildBody()}
}`;
function setChildBody() {
let childBody = [];
for (let i = 0; i < quadCount; ++i) {
let prefix = (i === 0) ? ' ' : ' else ';
childBody.push(`${prefix}if (idx === ${i}) node.quad${i} = child;`);
}
return childBody.join('\n');
// if (idx === 0) node.quad0 = child;
// else if (idx === 1) node.quad1 = child;
// else if (idx === 2) node.quad2 = child;
// else if (idx === 3) node.quad3 = child;
}
}
function getChildBodyCode(dimension) {
return `function getChild(node, idx) {
${getChildBody()}
return null;
}`;
function getChildBody() {
let childBody = [];
let quadCount = Math.pow(2, dimension);
for (let i = 0; i < quadCount; ++i) {
childBody.push(` if (idx === ${i}) return node.quad${i};`);
}
return childBody.join('\n');
// if (idx === 0) return node.quad0;
// if (idx === 1) return node.quad1;
// if (idx === 2) return node.quad2;
// if (idx === 3) return node.quad3;
}
}
function getQuadNodeCode(dimension) {
let pattern = createPatternBuilder(dimension);
let quadCount = Math.pow(2, dimension);
var quadNodeCode = `
function QuadNode() {
// body stored inside this node. In quad tree only leaf nodes (by construction)
// contain bodies:
this.body = null;
// Child nodes are stored in quads. Each quad is presented by number:
// 0 | 1
// -----
// 2 | 3
${assignQuads(' this.')}
// Total mass of current node
this.mass = 0;
// Center of mass coordinates
${pattern('this.mass_{var} = 0;', {indent: 2})}
// bounding box coordinates
${pattern('this.min_{var} = 0;', {indent: 2})}
${pattern('this.max_{var} = 0;', {indent: 2})}
}
`;
return quadNodeCode;
function assignQuads(indent) {
// this.quad0 = null;
// this.quad1 = null;
// this.quad2 = null;
// this.quad3 = null;
let quads = [];
for (let i = 0; i < quadCount; ++i) {
quads.push(`${indent}quad${i} = null;`);
}
return quads.join('\n');
}
}
function getInsertStackCode() {
return `
/**
* Our implementation of QuadTree is non-recursive to avoid GC hit
* This data structure represent stack of elements
* which we are trying to insert into quad tree.
*/
function InsertStack () {
this.stack = [];
this.popIdx = 0;
}
InsertStack.prototype = {
isEmpty: function() {
return this.popIdx === 0;
},
push: function (node, body) {
var item = this.stack[this.popIdx];
if (!item) {
// we are trying to avoid memory pressure: create new element
// only when absolutely necessary
this.stack[this.popIdx] = new InsertStackElement(node, body);
} else {
item.node = node;
item.body = body;
}
++this.popIdx;
},
pop: function () {
if (this.popIdx > 0) {
return this.stack[--this.popIdx];
}
},
reset: function () {
this.popIdx = 0;
}
};
function InsertStackElement(node, body) {
this.node = node; // QuadTree node
this.body = body; // physical body which needs to be inserted to node
}
`;
}
},{"./createPatternBuilder":2,"./getVariableName":9}],9:[function(require,module,exports){
module.exports = function getVariableName(index) {
if (index === 0) return 'x';
if (index === 1) return 'y';
if (index === 2) return 'z';
return 'c' + (index + 1);
};
},{}],10:[function(require,module,exports){
/**
* Manages a simulation of physical forces acting on bodies and springs.
*/
module.exports = createPhysicsSimulator;
var generateCreateBodyFunction = require('./codeGenerators/generateCreateBody');
var generateQuadTreeFunction = require('./codeGenerators/generateQuadTree');
var generateBoundsFunction = require('./codeGenerators/generateBounds');
var generateCreateDragForceFunction = require('./codeGenerators/generateCreateDragForce');
var generateCreateSpringForceFunction = require('./codeGenerators/generateCreateSpringForce');
var generateIntegratorFunction = require('./codeGenerators/generateIntegrator');
var dimensionalCache = {};
function createPhysicsSimulator(settings) {
var Spring = require('./spring');
var merge = require('ngraph.merge');
var eventify = require('ngraph.events');
if (settings) {
// Check for names from older versions of the layout
if (settings.springCoeff !== undefined) throw new Error('springCoeff was renamed to springCoefficient');
if (settings.dragCoeff !== undefined) throw new Error('dragCoeff was renamed to dragCoefficient');
}
settings = merge(settings, {
/**
* Ideal length for links (springs in physical model).
*/
springLength: 10,
/**
* Hook's law coefficient. 1 - solid spring.
*/
springCoefficient: 0.8,
/**
* 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: -12,
/**
* 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.
*/
dragCoefficient: 0.9, // TODO: Need to rename this to something better. E.g. `dragCoefficient`
/**
* Default time step (dt) for forces integration
*/
timeStep : 0.5,
/**
* Adaptive time step uses average spring length to compute actual time step:
* See: https://twitter.com/anvaka/status/1293067160755957760
*/
adaptiveTimeStepWeight: 0,
/**
* This parameter defines number of dimensions of the space where simulation
* is performed.
*/
dimensions: 2,
/**
* In debug mode more checks are performed, this will help you catch errors
* quickly, however for production build it is recommended to turn off this flag
* to speed up computation.
*/
debug: false
});
var factory = dimensionalCache[settings.dimensions];
if (!factory) {
var dimensions = settings.dimensions;
factory = {
Body: generateCreateBodyFunction(dimensions, settings.debug),
createQuadTree: generateQuadTreeFunction(dimensions),
createBounds: generateBoundsFunction(dimensions),
createDragForce: generateCreateDragForceFunction(dimensions),
createSpringForce: generateCreateSpringForceFunction(dimensions),
integrate: generateIntegratorFunction(dimensions),
};
dimensionalCache[dimensions] = factory;
}
var Body = factory.Body;
var createQuadTree = factory.createQuadTree;
var createBounds = factory.createBounds;
var createDragForce = factory.createDragForce;
var createSpringForce = factory.createSpringForce;
var integrate = factory.integrate;
var createBody = pos => new Body(pos);
var random = require('ngraph.random').random(42);
var bodies = []; // Bodies in this simulation.
var springs = []; // Springs in this simulation.
var quadTree = createQuadTree(settings, random);
var bounds = createBounds(bodies, settings, random);
var springForce = createSpringForce(settings, random);
var dragForce = createDragForce(settings);
var totalMovement = 0; // how much movement we made on last step
var forces = [];
var forceMap = new Map();
var iterationNumber = 0;
addForce('nbody', nbodyForce);
addForce('spring', updateSpringForce);
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,
quadTree: quadTree,
/**
* Array of springs, registered with current simulator
*
* Note: To add new spring, use addSpring() method. This property is only
* exposed for testing/performance purposes.
*/
springs: springs,
/**
* Returns settings with which current simulator was initialized
*/
settings: settings,
/**
* Adds a new force to simulation
*/
addForce: addForce,
/**
* Removes a force from the simulation.
*/
removeForce: removeForce,
/**
* Returns a map of all registered forces.
*/
getForces: getForces,
/**
* Performs one step of force simulation.
*
* @returns {boolean} true if system is considered stable; False otherwise.
*/
step: function () {
for (var i = 0; i < forces.length; ++i) {
forces[i](iterationNumber);
}
var movement = integrate(bodies, settings.timeStep, settings.adaptiveTimeStepWeight);
iterationNumber += 1;
return movement;
},
/**
* Adds body to the system
*
* @param {ngraph.physics.primitives.Body} body physical body
*
* @returns {ngraph.physics.primitives.Body} added body
*/
addBody: function (body) {
if (!body) {
throw new Error('Body is required');
}
bodies.push(body);
return body;
},
/**
* Adds body to the system at given position
*
* @param {Object} pos position of a body
*
* @returns {ngraph.physics.primitives.Body} added body
*/
addBodyAt: function (pos) {
if (!pos) {
throw new Error('Body position is required');
}
var body = createBody(pos);
bodies.push(body);
return body;
},
/**
* Removes body from the system
*
* @param {ngraph.physics.primitives.Body} body to remove
*
* @returns {Boolean} true if body found and removed. falsy otherwise;
*/
removeBody: function (body) {
if (!body) { return; }
var idx = bodies.indexOf(body);
if (idx < 0) { return; }
bodies.splice(idx, 1);
if (bodies.length === 0) {
bounds.reset();
}
return true;
},
/**
* Adds a spring to this simulation.
*
* @returns {Object} - a handle for a spring. If you want to later remove
* spring pass it to removeSpring() method.
*/
addSpring: function (body1, body2, springLength, springCoefficient) {
if (!body1 || !body2) {
throw new Error('Cannot add null spring to force simulator');
}
if (typeof springLength !== 'number') {
springLength = -1; // assume global configuration
}
var spring = new Spring(body1, body2, springLength, springCoefficient >= 0 ? springCoefficient : -1);
springs.push(spring);
// TODO: could mark simulator as dirty.
return spring;
},
/**
* Returns amount of movement performed on last step() call
*/
getTotalMovement: function () {
return totalMovement;
},
/**
* Removes spring from the system
*
* @param {Object} spring to remove. Spring is an object returned by addSpring
*
* @returns {Boolean} true if spring found and removed. falsy otherwise;
*/
removeSpring: function (spring) {
if (!spring) { return; }
var idx = springs.indexOf(spring);
if (idx > -1) {
springs.splice(idx, 1);
return true;
}
},
getBestNewBodyPosition: function (neighbors) {
return bounds.getBestNewPosition(neighbors);
},
/**
* Returns bounding box which covers all bodies
*/
getBBox: getBoundingBox,
getBoundingBox: getBoundingBox,
invalidateBBox: function () {
console.warn('invalidateBBox() is deprecated, bounds always recomputed on `getBBox()` call');
},
// TODO: Move the force specific stuff to force
gravity: function (value) {
if (value !== undefined) {
settings.gravity = value;
quadTree.options({gravity: value});
return this;
} else {
return settings.gravity;
}
},
theta: function (value) {
if (value !== undefined) {
settings.theta = value;
quadTree.options({theta: value});
return this;
} else {
return settings.theta;
}
},
/**
* Returns pseudo-random number generator instance.
*/
random: random
};
// allow settings modification via public API:
expose(settings, publicApi);
eventify(publicApi);
return publicApi;
function getBoundingBox() {
bounds.update();
return bounds.box;
}
function addForce(forceName, forceFunction) {
if (forceMap.has(forceName)) throw new Error('Force ' + forceName + ' is already added');
forceMap.set(forceName, forceFunction);
forces.push(forceFunction);
}
function removeForce(forceName) {
var forceIndex = forces.indexOf(forceMap.get(forceName));
if (forceIndex < 0) return;
forces.splice(forceIndex, 1);
forceMap.delete(forceName);
}
function getForces() {
// TODO: Should I trust them or clone the forces?
return forceMap;
}
function nbodyForce(/* iterationUmber */) {
if (bodies.length === 0) return;
quadTree.insertBodies(bodies);
var i = bodies.length;
while (i--) {
var body = bodies[i];
if (!body.isPinned) {
body.reset();
quadTree.updateBodyForce(body);
dragForce.update(body);
}
}
}
function updateSpringForce() {
var i = springs.length;
while (i--) {
springForce.update(springs[i]);
}
}
}
function expose(settings, target) {
for (var key in settings) {
augment(settings, target, key);
}
}
function augment(source, target, key) {
if (!source.hasOwnProperty(key)) return;
if (typeof target[key] === 'function') {
// this accessor is already defined. Ignore it
return;
}
var sourceIsNumber = Number.isFinite(source[key]);
if (sourceIsNumber) {
target[key] = function (value) {
if (value !== undefined) {
if (!Number.isFinite(value)) throw new Error('Value of ' + key + ' should be a valid number.');
source[key] = value;
return target;
}
return source[key];
};
} else {
target[key] = function (value) {
if (value !== undefined) {
source[key] = value;
return target;
}
return source[key];
};
}
}
},{"./codeGenerators/generateBounds":3,"./codeGenerators/generateCreateBody":4,"./codeGenerators/generateCreateDragForce":5,"./codeGenerators/generateCreateSpringForce":6,"./codeGenerators/generateIntegrator":7,"./codeGenerators/generateQuadTree":8,"./spring":11,"ngraph.events":12,"ngraph.merge":13,"ngraph.random":14}],11:[function(require,module,exports){
module.exports = Spring;
/**
* Represents a physical spring. Spring connects two bodies, has rest length
* stiffness coefficient and optional weight
*/
function Spring(fromBody, toBody, length, springCoefficient) {
this.from = fromBody;
this.to = toBody;
this.length = length;
this.coefficient = springCoefficient;
}
},{}],12:[function(require,module,exports){
module.exports = function eventify(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,