You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

635 lines
16 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/****************************
* Vector2 math functions
***************************/
function forceAtlas2Worker() {
var vec2 = {
create: function () {
return new Float32Array(2);
},
dist: function (a, b) {
var x = b[0] - a[0];
var y = b[1] - a[1];
return Math.sqrt(x * x + y * y);
},
len: function (a) {
var x = a[0];
var y = a[1];
return Math.sqrt(x * x + y * y);
},
scaleAndAdd: function (out, a, b, scale) {
out[0] = a[0] + b[0] * scale;
out[1] = a[1] + b[1] * scale;
return out;
},
scale: function (out, a, b) {
out[0] = a[0] * b;
out[1] = a[1] * b;
return out;
},
add: function (out, a, b) {
out[0] = a[0] + b[0];
out[1] = a[1] + b[1];
return out;
},
sub: function (out, a, b) {
out[0] = a[0] - b[0];
out[1] = a[1] - b[1];
return out;
},
normalize: function (out, a) {
var x = a[0];
var y = a[1];
var len = x * x + y * y;
if (len > 0) {
//TODO: evaluate use of glm_invsqrt here?
len = 1 / Math.sqrt(len);
out[0] = a[0] * len;
out[1] = a[1] * len;
}
return out;
},
negate: function (out, a) {
out[0] = -a[0];
out[1] = -a[1];
return out;
},
copy: function (out, a) {
out[0] = a[0];
out[1] = a[1];
return out;
},
set: function (out, x, y) {
out[0] = x;
out[1] = y;
return out;
}
};
/****************************
* Class: Region
***************************/
function Region() {
this.subRegions = [];
this.nSubRegions = 0;
this.node = null;
this.mass = 0;
this.centerOfMass = null;
this.bbox = new Float32Array(4);
this.size = 0;
}
var regionProto = Region.prototype; // Reset before update
regionProto.beforeUpdate = function () {
for (var i = 0; i < this.nSubRegions; i++) {
this.subRegions[i].beforeUpdate();
}
this.mass = 0;
if (this.centerOfMass) {
this.centerOfMass[0] = 0;
this.centerOfMass[1] = 0;
}
this.nSubRegions = 0;
this.node = null;
}; // Clear after update
regionProto.afterUpdate = function () {
this.subRegions.length = this.nSubRegions;
for (var i = 0; i < this.nSubRegions; i++) {
this.subRegions[i].afterUpdate();
}
};
regionProto.addNode = function (node) {
if (this.nSubRegions === 0) {
if (this.node == null) {
this.node = node;
return;
} // Already have node, subdivide self.
else {
this._addNodeToSubRegion(this.node);
this.node = null;
}
}
this._addNodeToSubRegion(node);
this._updateCenterOfMass(node);
};
regionProto.findSubRegion = function (x, y) {
for (var i = 0; i < this.nSubRegions; i++) {
var region = this.subRegions[i];
if (region.contain(x, y)) {
return region;
}
}
};
regionProto.contain = function (x, y) {
return this.bbox[0] <= x && this.bbox[2] >= x && this.bbox[1] <= y && this.bbox[3] >= y;
};
regionProto.setBBox = function (minX, minY, maxX, maxY) {
// Min
this.bbox[0] = minX;
this.bbox[1] = minY; // Max
this.bbox[2] = maxX;
this.bbox[3] = maxY;
this.size = (maxX - minX + maxY - minY) / 2;
};
regionProto._newSubRegion = function () {
var subRegion = this.subRegions[this.nSubRegions];
if (!subRegion) {
subRegion = new Region();
this.subRegions[this.nSubRegions] = subRegion;
}
this.nSubRegions++;
return subRegion;
};
regionProto._addNodeToSubRegion = function (node) {
var subRegion = this.findSubRegion(node.position[0], node.position[1]);
var bbox = this.bbox;
if (!subRegion) {
var cx = (bbox[0] + bbox[2]) / 2;
var cy = (bbox[1] + bbox[3]) / 2;
var w = (bbox[2] - bbox[0]) / 2;
var h = (bbox[3] - bbox[1]) / 2;
var xi = node.position[0] >= cx ? 1 : 0;
var yi = node.position[1] >= cy ? 1 : 0;
var subRegion = this._newSubRegion(); // Min
subRegion.setBBox( // Min
xi * w + bbox[0], yi * h + bbox[1], // Max
(xi + 1) * w + bbox[0], (yi + 1) * h + bbox[1]);
}
subRegion.addNode(node);
};
regionProto._updateCenterOfMass = function (node) {
// Incrementally update
if (this.centerOfMass == null) {
this.centerOfMass = new Float32Array(2);
}
var x = this.centerOfMass[0] * this.mass;
var y = this.centerOfMass[1] * this.mass;
x += node.position[0] * node.mass;
y += node.position[1] * node.mass;
this.mass += node.mass;
this.centerOfMass[0] = x / this.mass;
this.centerOfMass[1] = y / this.mass;
};
/****************************
* Class: Graph Node
***************************/
function GraphNode() {
this.position = new Float32Array(2);
this.force = vec2.create();
this.forcePrev = vec2.create(); // If repulsionByDegree is true
// mass = inDegree + outDegree + 1
// Else
// mass is manually set
this.mass = 1;
this.inDegree = 0;
this.outDegree = 0; // Optional
// this.size = 1;
}
/****************************
* Class: Graph Edge
***************************/
function GraphEdge(source, target) {
this.source = source;
this.target = target;
this.weight = 1;
}
/****************************
* Class: ForceStlas2
***************************/
function ForceAtlas2() {
//-------------
// Configs
// If auto settings is true
// barnesHutOptimize,
// barnesHutTheta,
// scaling,
// jitterTolerence
// Will be set by the system automatically
// preventOverlap will be set false
// if node size is not given
this.autoSettings = true; // Barnes Hut
// http://arborjs.org/docs/barnes-hut
this.barnesHutOptimize = true;
this.barnesHutTheta = 1.5; // Force Atlas2 Configs
this.repulsionByDegree = true;
this.linLogMode = false;
this.strongGravityMode = false;
this.gravity = 1.0;
this.scaling = 1.0;
this.edgeWeightInfluence = 1.0;
this.jitterTolerence = 0.1; // TODO
this.preventOverlap = false;
this.dissuadeHubs = false; //
this.rootRegion = new Region();
this.rootRegion.centerOfMass = vec2.create();
this.nodes = [];
this.edges = [];
this.bbox = new Float32Array(4);
this.gravityCenter = null;
this._massArr = null;
this._swingingArr = null;
this._sizeArr = null;
this._globalSpeed = 0;
}
var forceAtlas2Proto = ForceAtlas2.prototype;
forceAtlas2Proto.initNodes = function (positionArr, massArr, sizeArr) {
var nNodes = massArr.length;
this.nodes.length = 0;
var haveSize = typeof sizeArr != 'undefined';
for (var i = 0; i < nNodes; i++) {
var node = new GraphNode();
node.position[0] = positionArr[i * 2];
node.position[1] = positionArr[i * 2 + 1];
node.mass = massArr[i];
if (haveSize) {
node.size = sizeArr[i];
}
this.nodes.push(node);
}
this._massArr = massArr;
this._swingingArr = new Float32Array(nNodes);
if (haveSize) {
this._sizeArr = sizeArr;
}
};
forceAtlas2Proto.initEdges = function (edgeArr, edgeWeightArr) {
var nEdges = edgeArr.length / 2;
this.edges.length = 0;
for (var i = 0; i < nEdges; i++) {
var sIdx = edgeArr[i * 2];
var tIdx = edgeArr[i * 2 + 1];
var sNode = this.nodes[sIdx];
var tNode = this.nodes[tIdx];
if (!sNode || !tNode) {
console.error('Node not exists, try initNodes before initEdges');
return;
}
sNode.outDegree++;
tNode.inDegree++;
var edge = new GraphEdge(sNode, tNode);
if (edgeWeightArr) {
edge.weight = edgeWeightArr[i];
}
this.edges.push(edge);
}
};
forceAtlas2Proto.updateSettings = function () {
if (this.repulsionByDegree) {
for (var i = 0; i < this.nodes.length; i++) {
var node = this.nodes[i];
node.mass = node.inDegree + node.outDegree + 1;
}
} else {
for (var i = 0; i < this.nodes.length; i++) {
var node = this.nodes[i];
node.mass = this._massArr[i];
}
}
};
forceAtlas2Proto.update = function () {
var nNodes = this.nodes.length;
this.updateSettings();
this.updateBBox(); // Update region
if (this.barnesHutOptimize) {
this.rootRegion.setBBox(this.bbox[0], this.bbox[1], this.bbox[2], this.bbox[3]);
this.rootRegion.beforeUpdate();
for (var i = 0; i < nNodes; i++) {
this.rootRegion.addNode(this.nodes[i]);
}
this.rootRegion.afterUpdate();
} // Reset forces
for (var i = 0; i < nNodes; i++) {
var node = this.nodes[i];
vec2.copy(node.forcePrev, node.force);
vec2.set(node.force, 0, 0);
} // Compute forces
// Repulsion
for (var i = 0; i < nNodes; i++) {
var na = this.nodes[i];
if (this.barnesHutOptimize) {
this.applyRegionToNodeRepulsion(this.rootRegion, na);
} else {
for (var j = i + 1; j < nNodes; j++) {
var nb = this.nodes[j];
this.applyNodeToNodeRepulsion(na, nb, false);
}
} // Gravity
if (this.gravity > 0) {
if (this.strongGravityMode) {
this.applyNodeStrongGravity(na);
} else {
this.applyNodeGravity(na);
}
}
} // Attraction
for (var i = 0; i < this.edges.length; i++) {
this.applyEdgeAttraction(this.edges[i]);
} // Handle swinging
var swingWeightedSum = 0;
var tractionWeightedSum = 0;
var tmp = vec2.create();
for (var i = 0; i < nNodes; i++) {
var node = this.nodes[i];
var swing = vec2.dist(node.force, node.forcePrev);
swingWeightedSum += swing * node.mass;
vec2.add(tmp, node.force, node.forcePrev);
var traction = vec2.len(tmp) * 0.5;
tractionWeightedSum += traction * node.mass; // Save the value for using later
this._swingingArr[i] = swing;
}
var globalSpeed = this.jitterTolerence * this.jitterTolerence * tractionWeightedSum / swingWeightedSum; // NB: During our tests we observed that an excessive rise of the global speed could have a negative impact.
// Thats why we limited the increase of global speed s(t)(G) to 50% of the previous step s(t1)(G).
if (this._globalSpeed > 0) {
globalSpeed = Math.min(globalSpeed / this._globalSpeed, 1.5) * this._globalSpeed;
}
this._globalSpeed = globalSpeed; // Apply forces
for (var i = 0; i < nNodes; i++) {
var node = this.nodes[i];
var swing = this._swingingArr[i];
var speed = 0.1 * globalSpeed / (1 + globalSpeed * Math.sqrt(swing)); // Additional constraint to prevent local speed gets too high
var df = vec2.len(node.force);
if (df > 0) {
speed = Math.min(df * speed, 10) / df;
vec2.scaleAndAdd(node.position, node.position, node.force, speed);
}
}
};
forceAtlas2Proto.applyRegionToNodeRepulsion = function () {
var v = vec2.create();
return function applyRegionToNodeRepulsion(region, node) {
if (region.node) {
// Region is a leaf
this.applyNodeToNodeRepulsion(region.node, node, true);
} else {
vec2.sub(v, node.position, region.centerOfMass);
var d2 = v[0] * v[0] + v[1] * v[1];
if (d2 > this.barnesHutTheta * region.size * region.size) {
var factor = this.scaling * node.mass * region.mass / d2;
vec2.scaleAndAdd(node.force, node.force, v, factor);
} else {
for (var i = 0; i < region.nSubRegions; i++) {
this.applyRegionToNodeRepulsion(region.subRegions[i], node);
}
}
}
};
}();
forceAtlas2Proto.applyNodeToNodeRepulsion = function () {
var v = vec2.create();
return function applyNodeToNodeRepulsion(na, nb, oneWay) {
if (na == nb) {
return;
}
vec2.sub(v, na.position, nb.position);
var d2 = v[0] * v[0] + v[1] * v[1]; // PENDING
if (d2 === 0) {
return;
}
var factor;
if (this.preventOverlap) {
var d = Math.sqrt(d2);
d = d - na.size - nb.size;
if (d > 0) {
factor = this.scaling * na.mass * nb.mass / (d * d);
} else if (d < 0) {
// A stronger repulsion if overlap
factor = this.scaling * 100 * na.mass * nb.mass;
} else {
// No repulsion
return;
}
} else {
// Divide factor by an extra `d` to normalize the `v`
factor = this.scaling * na.mass * nb.mass / d2;
}
vec2.scaleAndAdd(na.force, na.force, v, factor);
vec2.scaleAndAdd(nb.force, nb.force, v, -factor);
};
}();
forceAtlas2Proto.applyEdgeAttraction = function () {
var v = vec2.create();
return function applyEdgeAttraction(edge) {
var na = edge.source;
var nb = edge.target;
vec2.sub(v, na.position, nb.position);
var d = vec2.len(v);
var w;
if (this.edgeWeightInfluence === 0) {
w = 1;
} else if (this.edgeWeightInfluence === 1) {
w = edge.weight;
} else {
w = Math.pow(edge.weight, this.edgeWeightInfluence);
}
var factor;
if (this.preventOverlap) {
d = d - na.size - nb.size;
if (d <= 0) {
// No attraction
return;
}
}
if (this.linLogMode) {
// Divide factor by an extra `d` to normalize the `v`
factor = -w * Math.log(d + 1) / (d + 1);
} else {
factor = -w;
}
vec2.scaleAndAdd(na.force, na.force, v, factor);
vec2.scaleAndAdd(nb.force, nb.force, v, -factor);
};
}();
forceAtlas2Proto.applyNodeGravity = function () {
var v = vec2.create();
return function (node) {
vec2.sub(v, this.gravityCenter, node.position);
var d = vec2.len(v);
vec2.scaleAndAdd(node.force, node.force, v, this.gravity * node.mass / (d + 1));
};
}();
forceAtlas2Proto.applyNodeStrongGravity = function () {
var v = vec2.create();
return function (node) {
vec2.sub(v, this.gravityCenter, node.position);
vec2.scaleAndAdd(node.force, node.force, v, this.gravity * node.mass);
};
}();
forceAtlas2Proto.updateBBox = function () {
var minX = Infinity;
var minY = Infinity;
var maxX = -Infinity;
var maxY = -Infinity;
for (var i = 0; i < this.nodes.length; i++) {
var pos = this.nodes[i].position;
minX = Math.min(minX, pos[0]);
minY = Math.min(minY, pos[1]);
maxX = Math.max(maxX, pos[0]);
maxY = Math.max(maxY, pos[1]);
}
this.bbox[0] = minX;
this.bbox[1] = minY;
this.bbox[2] = maxX;
this.bbox[3] = maxY;
};
forceAtlas2Proto.getGlobalSpeed = function () {
return this._globalSpeed;
};
/****************************
* Main process
***************************/
var forceAtlas2 = null;
self.onmessage = function (e) {
switch (e.data.cmd) {
case 'init':
forceAtlas2 = new ForceAtlas2();
forceAtlas2.initNodes(e.data.nodesPosition, e.data.nodesMass, e.data.nodesSize);
forceAtlas2.initEdges(e.data.edges, e.data.edgesWeight);
break;
case 'updateConfig':
if (forceAtlas2) {
for (var name in e.data.config) {
forceAtlas2[name] = e.data.config[name];
}
}
break;
case 'update':
var steps = e.data.steps;
if (forceAtlas2) {
for (var i = 0; i < steps; i++) {
forceAtlas2.update();
}
var nNodes = forceAtlas2.nodes.length;
var positionArr = new Float32Array(nNodes * 2); // Callback
for (var i = 0; i < nNodes; i++) {
var node = forceAtlas2.nodes[i];
positionArr[i * 2] = node.position[0];
positionArr[i * 2 + 1] = node.position[1];
}
self.postMessage({
buffer: positionArr.buffer,
globalSpeed: forceAtlas2.getGlobalSpeed()
}, [positionArr.buffer]);
} else {
// Not initialzied yet
var emptyArr = new Float32Array(); // Post transfer object
self.postMessage({
buffer: emptyArr.buffer,
globalSpeed: forceAtlas2.getGlobalSpeed()
}, [emptyArr.buffer]);
}
break;
}
};
}
export default forceAtlas2Worker;