/**************************** * 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;