ngraph.forcelayout
Version:
Force directed graph drawing layout
461 lines (399 loc) • 14.8 kB
JavaScript
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
}
`;
}