635 lines
16 KiB
JavaScript
635 lines
16 KiB
JavaScript
|
|
/****************************
|
|||
|
|
* 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.
|
|||
|
|
// That’s why we limited the increase of global speed s(t)(G) to 50% of the previous step s(t−1)(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;
|