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.
781 lines
24 KiB
781 lines
24 KiB
4 years ago
|
// 1. Graph Drawing by Force-directed Placement
|
||
|
// 2. http://webatlas.fr/tempshare/ForceAtlas2_Paper.pdf
|
||
|
define(function __echartsForceLayoutWorker(require) {
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
var vec2;
|
||
|
// In web worker
|
||
|
var inWorker = typeof(window) === 'undefined' && typeof(require) === 'undefined';
|
||
|
if (inWorker) {
|
||
|
vec2 = {
|
||
|
create: function(x, y) {
|
||
|
var out = new Float32Array(2);
|
||
|
out[0] = x || 0;
|
||
|
out[1] = y || 0;
|
||
|
return out;
|
||
|
},
|
||
|
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;
|
||
|
},
|
||
|
dot: function (v1, v2) {
|
||
|
return v1[0] * v2[0] + v1[1] * v2[1];
|
||
|
},
|
||
|
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;
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
else {
|
||
|
vec2 = require('zrender/tool/vector');
|
||
|
}
|
||
|
var ArrayCtor = typeof(Float32Array) == 'undefined' ? Array : Float32Array;
|
||
|
|
||
|
/****************************
|
||
|
* Class: Region
|
||
|
***************************/
|
||
|
|
||
|
function Region() {
|
||
|
|
||
|
this.subRegions = [];
|
||
|
|
||
|
this.nSubRegions = 0;
|
||
|
|
||
|
this.node = null;
|
||
|
|
||
|
this.mass = 0;
|
||
|
|
||
|
this.centerOfMass = null;
|
||
|
|
||
|
this.bbox = new ArrayCtor(4);
|
||
|
|
||
|
this.size = 0;
|
||
|
}
|
||
|
|
||
|
// Reset before update
|
||
|
Region.prototype.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
|
||
|
Region.prototype.afterUpdate = function() {
|
||
|
this.subRegions.length = this.nSubRegions;
|
||
|
for (var i = 0; i < this.nSubRegions; i++) {
|
||
|
this.subRegions[i].afterUpdate();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Region.prototype.addNode = function(node) {
|
||
|
if (this.nSubRegions === 0) {
|
||
|
if (this.node == null) {
|
||
|
this.node = node;
|
||
|
return;
|
||
|
}
|
||
|
else {
|
||
|
this._addNodeToSubRegion(this.node);
|
||
|
this.node = null;
|
||
|
}
|
||
|
}
|
||
|
this._addNodeToSubRegion(node);
|
||
|
|
||
|
this._updateCenterOfMass(node);
|
||
|
};
|
||
|
|
||
|
Region.prototype.findSubRegion = function(x, y) {
|
||
|
for (var i = 0; i < this.nSubRegions; i++) {
|
||
|
var region = this.subRegions[i];
|
||
|
if (region.contain(x, y)) {
|
||
|
return region;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
Region.prototype.contain = function(x, y) {
|
||
|
return this.bbox[0] <= x
|
||
|
&& this.bbox[2] >= x
|
||
|
&& this.bbox[1] <= y
|
||
|
&& this.bbox[3] >= y;
|
||
|
};
|
||
|
|
||
|
Region.prototype.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;
|
||
|
};
|
||
|
|
||
|
Region.prototype._newSubRegion = function() {
|
||
|
var subRegion = this.subRegions[this.nSubRegions];
|
||
|
if (!subRegion) {
|
||
|
subRegion = new Region();
|
||
|
this.subRegions[this.nSubRegions] = subRegion;
|
||
|
}
|
||
|
this.nSubRegions++;
|
||
|
return subRegion;
|
||
|
};
|
||
|
|
||
|
Region.prototype._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);
|
||
|
};
|
||
|
|
||
|
Region.prototype._updateCenterOfMass = function(node) {
|
||
|
// Incrementally update
|
||
|
if (this.centerOfMass == null) {
|
||
|
this.centerOfMass = vec2.create();
|
||
|
}
|
||
|
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 = vec2.create();
|
||
|
|
||
|
this.force = vec2.create();
|
||
|
this.forcePrev = vec2.create();
|
||
|
|
||
|
this.speed = vec2.create();
|
||
|
this.speedPrev = vec2.create();
|
||
|
|
||
|
// If repulsionByDegree is true
|
||
|
// mass = inDegree + outDegree + 1
|
||
|
// Else
|
||
|
// mass is manually set
|
||
|
this.mass = 1;
|
||
|
|
||
|
this.inDegree = 0;
|
||
|
this.outDegree = 0;
|
||
|
}
|
||
|
|
||
|
/****************************
|
||
|
* Class: Graph Edge
|
||
|
***************************/
|
||
|
function GraphEdge(node1, node2) {
|
||
|
this.node1 = node1;
|
||
|
this.node2 = node2;
|
||
|
|
||
|
this.weight = 1;
|
||
|
}
|
||
|
|
||
|
/****************************
|
||
|
* Class: ForceLayout
|
||
|
***************************/
|
||
|
function ForceLayout() {
|
||
|
|
||
|
this.barnesHutOptimize = false;
|
||
|
this.barnesHutTheta = 1.5;
|
||
|
|
||
|
this.repulsionByDegree = false;
|
||
|
|
||
|
this.preventNodeOverlap = false;
|
||
|
this.preventNodeEdgeOverlap = false;
|
||
|
|
||
|
this.strongGravity = true;
|
||
|
|
||
|
this.gravity = 1.0;
|
||
|
this.scaling = 1.0;
|
||
|
|
||
|
this.edgeWeightInfluence = 1.0;
|
||
|
|
||
|
this.center = [0, 0];
|
||
|
this.width = 500;
|
||
|
this.height = 500;
|
||
|
|
||
|
this.maxSpeedIncrease = 1;
|
||
|
|
||
|
this.nodes = [];
|
||
|
this.edges = [];
|
||
|
|
||
|
this.bbox = new ArrayCtor(4);
|
||
|
|
||
|
this._rootRegion = new Region();
|
||
|
this._rootRegion.centerOfMass = vec2.create();
|
||
|
|
||
|
this._massArr = null;
|
||
|
|
||
|
this._k = 0;
|
||
|
}
|
||
|
|
||
|
ForceLayout.prototype.nodeToNodeRepulsionFactor = function (mass, d, k) {
|
||
|
return k * k * mass / d;
|
||
|
};
|
||
|
|
||
|
ForceLayout.prototype.edgeToNodeRepulsionFactor = function (mass, d, k) {
|
||
|
return k * mass / d;
|
||
|
};
|
||
|
|
||
|
ForceLayout.prototype.attractionFactor = function (w, d, k) {
|
||
|
return w * d / k;
|
||
|
};
|
||
|
|
||
|
ForceLayout.prototype.initNodes = function(positionArr, massArr, sizeArr) {
|
||
|
|
||
|
this.temperature = 1.0;
|
||
|
|
||
|
var nNodes = positionArr.length / 2;
|
||
|
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;
|
||
|
if (haveSize) {
|
||
|
this._sizeArr = sizeArr;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ForceLayout.prototype.initEdges = function(edgeArr, edgeWeightArr) {
|
||
|
var nEdges = edgeArr.length / 2;
|
||
|
this.edges.length = 0;
|
||
|
var edgeHaveWeight = typeof(edgeWeightArr) !== 'undefined';
|
||
|
|
||
|
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) {
|
||
|
continue;
|
||
|
}
|
||
|
sNode.outDegree++;
|
||
|
tNode.inDegree++;
|
||
|
var edge = new GraphEdge(sNode, tNode);
|
||
|
|
||
|
if (edgeHaveWeight) {
|
||
|
edge.weight = edgeWeightArr[i];
|
||
|
}
|
||
|
|
||
|
this.edges.push(edge);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ForceLayout.prototype.update = function() {
|
||
|
|
||
|
var nNodes = this.nodes.length;
|
||
|
|
||
|
this.updateBBox();
|
||
|
|
||
|
this._k = 0.4 * this.scaling * Math.sqrt(this.width * this.height / nNodes);
|
||
|
|
||
|
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();
|
||
|
}
|
||
|
else {
|
||
|
// Update center of mass of whole graph
|
||
|
var mass = 0;
|
||
|
var centerOfMass = this._rootRegion.centerOfMass;
|
||
|
vec2.set(centerOfMass, 0, 0);
|
||
|
for (var i = 0; i < nNodes; i++) {
|
||
|
var node = this.nodes[i];
|
||
|
mass += node.mass;
|
||
|
vec2.scaleAndAdd(centerOfMass, centerOfMass, node.position, node.mass);
|
||
|
}
|
||
|
if (mass > 0) {
|
||
|
vec2.scale(centerOfMass, centerOfMass, 1 / mass);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
this.updateForce();
|
||
|
|
||
|
this.updatePosition();
|
||
|
};
|
||
|
|
||
|
ForceLayout.prototype.updateForce = function () {
|
||
|
var nNodes = this.nodes.length;
|
||
|
// Reset forces
|
||
|
for (var i = 0; i < nNodes; i++) {
|
||
|
var node = this.nodes[i];
|
||
|
vec2.copy(node.forcePrev, node.force);
|
||
|
vec2.copy(node.speedPrev, node.speed);
|
||
|
vec2.set(node.force, 0, 0);
|
||
|
}
|
||
|
|
||
|
this.updateNodeNodeForce();
|
||
|
|
||
|
if (this.gravity > 0) {
|
||
|
this.updateGravityForce();
|
||
|
}
|
||
|
|
||
|
this.updateEdgeForce();
|
||
|
|
||
|
if (this.preventNodeEdgeOverlap) {
|
||
|
this.updateNodeEdgeForce();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ForceLayout.prototype.updatePosition = function () {
|
||
|
var nNodes = this.nodes.length;
|
||
|
// Apply forces
|
||
|
// var speed = vec2.create();
|
||
|
var v = vec2.create();
|
||
|
for (var i = 0; i < nNodes; i++) {
|
||
|
var node = this.nodes[i];
|
||
|
var speed = node.speed;
|
||
|
|
||
|
// var swing = vec2.dist(node.force, node.forcePrev);
|
||
|
// // var swing = 30;
|
||
|
// vec2.scale(node.force, node.force, 1 / (1 + Math.sqrt(swing)));
|
||
|
vec2.scale(node.force, node.force, 1 / 30);
|
||
|
|
||
|
// contraint force
|
||
|
var df = vec2.len(node.force) + 0.1;
|
||
|
var scale = Math.min(df, 500.0) / df;
|
||
|
vec2.scale(node.force, node.force, scale);
|
||
|
|
||
|
vec2.add(speed, speed, node.force);
|
||
|
vec2.scale(speed, speed, this.temperature);
|
||
|
|
||
|
// Prevent swinging
|
||
|
// Limited the increase of speed up to 100% each step
|
||
|
// TODO adjust by nodes number
|
||
|
// TODO First iterate speed control
|
||
|
vec2.sub(v, speed, node.speedPrev);
|
||
|
var swing = vec2.len(v);
|
||
|
if (swing > 0) {
|
||
|
vec2.scale(v, v, 1 / swing);
|
||
|
var base = vec2.len(node.speedPrev);
|
||
|
if (base > 0) {
|
||
|
swing = Math.min(swing / base, this.maxSpeedIncrease) * base;
|
||
|
vec2.scaleAndAdd(speed, node.speedPrev, v, swing);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// constraint speed
|
||
|
var ds = vec2.len(speed);
|
||
|
var scale = Math.min(ds, 100.0) / (ds + 0.1);
|
||
|
vec2.scale(speed, speed, scale);
|
||
|
|
||
|
vec2.add(node.position, node.position, speed);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ForceLayout.prototype.updateNodeNodeForce = function () {
|
||
|
var nNodes = this.nodes.length;
|
||
|
// 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);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ForceLayout.prototype.updateGravityForce = function () {
|
||
|
for (var i = 0; i < this.nodes.length; i++) {
|
||
|
this.applyNodeGravity(this.nodes[i]);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ForceLayout.prototype.updateEdgeForce = function () {
|
||
|
// Attraction
|
||
|
for (var i = 0; i < this.edges.length; i++) {
|
||
|
this.applyEdgeAttraction(this.edges[i]);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ForceLayout.prototype.updateNodeEdgeForce = function () {
|
||
|
for (var i = 0; i < this.nodes.length; i++) {
|
||
|
for (var j = 0; j < this.edges.length; j++) {
|
||
|
this.applyEdgeToNodeRepulsion(this.edges[j], this.nodes[i]);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
ForceLayout.prototype.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 {
|
||
|
// Static region and node
|
||
|
if (region.mass === 0 && node.mass === 0) {
|
||
|
return;
|
||
|
}
|
||
|
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._k * this._k * (node.mass + region.mass) / (d2 + 1);
|
||
|
vec2.scaleAndAdd(node.force, node.force, v, factor * 2);
|
||
|
}
|
||
|
else {
|
||
|
for (var i = 0; i < region.nSubRegions; i++) {
|
||
|
this.applyRegionToNodeRepulsion(region.subRegions[i], node);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
})();
|
||
|
|
||
|
ForceLayout.prototype.applyNodeToNodeRepulsion = (function() {
|
||
|
var v = vec2.create();
|
||
|
return function applyNodeToNodeRepulsion(na, nb, oneWay) {
|
||
|
if (na === nb) {
|
||
|
return;
|
||
|
}
|
||
|
// Two static node
|
||
|
if (na.mass === 0 && nb.mass === 0) {
|
||
|
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;
|
||
|
var mass = na.mass + nb.mass;
|
||
|
var d = Math.sqrt(d2);
|
||
|
|
||
|
// Normalize v
|
||
|
vec2.scale(v, v, 1 / d);
|
||
|
|
||
|
if (this.preventNodeOverlap) {
|
||
|
d = d - na.size - nb.size;
|
||
|
if (d > 0) {
|
||
|
factor = this.nodeToNodeRepulsionFactor(
|
||
|
mass, d, this._k
|
||
|
);
|
||
|
}
|
||
|
else if (d <= 0) {
|
||
|
// A stronger repulsion if overlap
|
||
|
factor = this._k * this._k * 10 * mass;
|
||
|
}
|
||
|
}
|
||
|
else {
|
||
|
factor = this.nodeToNodeRepulsionFactor(
|
||
|
mass, d, this._k
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (!oneWay) {
|
||
|
vec2.scaleAndAdd(na.force, na.force, v, factor * 2);
|
||
|
}
|
||
|
vec2.scaleAndAdd(nb.force, nb.force, v, -factor * 2);
|
||
|
};
|
||
|
})();
|
||
|
|
||
|
ForceLayout.prototype.applyEdgeAttraction = (function() {
|
||
|
var v = vec2.create();
|
||
|
return function applyEdgeAttraction(edge) {
|
||
|
var na = edge.node1;
|
||
|
var nb = edge.node2;
|
||
|
|
||
|
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;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var factor = this.attractionFactor(w, d, this._k);
|
||
|
|
||
|
vec2.scaleAndAdd(na.force, na.force, v, -factor);
|
||
|
vec2.scaleAndAdd(nb.force, nb.force, v, factor);
|
||
|
};
|
||
|
})();
|
||
|
|
||
|
ForceLayout.prototype.applyNodeGravity = (function() {
|
||
|
var v = vec2.create();
|
||
|
return function(node) {
|
||
|
// PENDING Move to centerOfMass or [0, 0] ?
|
||
|
// vec2.sub(v, this._rootRegion.centerOfMass, node.position);
|
||
|
// vec2.negate(v, node.position);
|
||
|
vec2.sub(v, this.center, node.position);
|
||
|
if (this.width > this.height) {
|
||
|
// Stronger gravity on y axis
|
||
|
v[1] *= this.width / this.height;
|
||
|
}
|
||
|
else {
|
||
|
// Stronger gravity on x axis
|
||
|
v[0] *= this.height / this.width;
|
||
|
}
|
||
|
var d = vec2.len(v) / 100;
|
||
|
|
||
|
if (this.strongGravity) {
|
||
|
vec2.scaleAndAdd(node.force, node.force, v, d * this.gravity * node.mass);
|
||
|
}
|
||
|
else {
|
||
|
vec2.scaleAndAdd(node.force, node.force, v, this.gravity * node.mass / (d + 1));
|
||
|
}
|
||
|
};
|
||
|
})();
|
||
|
|
||
|
ForceLayout.prototype.applyEdgeToNodeRepulsion = (function () {
|
||
|
var v12 = vec2.create();
|
||
|
var v13 = vec2.create();
|
||
|
var p = vec2.create();
|
||
|
return function (e, n3) {
|
||
|
var n1 = e.node1;
|
||
|
var n2 = e.node2;
|
||
|
|
||
|
if (n1 === n3 || n2 === n3) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
vec2.sub(v12, n2.position, n1.position);
|
||
|
vec2.sub(v13, n3.position, n1.position);
|
||
|
|
||
|
var len12 = vec2.len(v12);
|
||
|
vec2.scale(v12, v12, 1 / len12);
|
||
|
var len = vec2.dot(v12, v13);
|
||
|
|
||
|
// n3 can't project on line n1-n2
|
||
|
if (len < 0 || len > len12) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Project point
|
||
|
vec2.scaleAndAdd(p, n1.position, v12, len);
|
||
|
|
||
|
// n3 distance to line n1-n2
|
||
|
var dist = vec2.dist(p, n3.position) - n3.size;
|
||
|
var factor = this.edgeToNodeRepulsionFactor(
|
||
|
n3.mass, Math.max(dist, 0.1), 100
|
||
|
);
|
||
|
// Use v12 as normal vector
|
||
|
vec2.sub(v12, n3.position, p);
|
||
|
vec2.normalize(v12, v12);
|
||
|
vec2.scaleAndAdd(n3.force, n3.force, v12, factor);
|
||
|
|
||
|
// PENDING
|
||
|
vec2.scaleAndAdd(n1.force, n1.force, v12, -factor);
|
||
|
vec2.scaleAndAdd(n2.force, n2.force, v12, -factor);
|
||
|
};
|
||
|
})();
|
||
|
|
||
|
ForceLayout.prototype.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;
|
||
|
};
|
||
|
|
||
|
ForceLayout.getWorkerCode = function() {
|
||
|
var str = __echartsForceLayoutWorker.toString();
|
||
|
return str.slice(str.indexOf('{') + 1, str.lastIndexOf('return'));
|
||
|
};
|
||
|
|
||
|
/****************************
|
||
|
* Main process
|
||
|
***************************/
|
||
|
|
||
|
/* jshint ignore:start */
|
||
|
if (inWorker) {
|
||
|
var forceLayout = null;
|
||
|
|
||
|
self.onmessage = function(e) {
|
||
|
// Position read back
|
||
|
if (e.data instanceof ArrayBuffer) {
|
||
|
if (!forceLayout) return;
|
||
|
|
||
|
var positionArr = new Float32Array(e.data);
|
||
|
var nNodes = positionArr.length / 2;
|
||
|
for (var i = 0; i < nNodes; i++) {
|
||
|
var node = forceLayout.nodes[i];
|
||
|
node.position[0] = positionArr[i * 2];
|
||
|
node.position[1] = positionArr[i * 2 + 1];
|
||
|
}
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
switch(e.data.cmd) {
|
||
|
case 'init':
|
||
|
if (!forceLayout) {
|
||
|
forceLayout = new ForceLayout();
|
||
|
}
|
||
|
forceLayout.initNodes(e.data.nodesPosition, e.data.nodesMass, e.data.nodesSize);
|
||
|
forceLayout.initEdges(e.data.edges, e.data.edgesWeight);
|
||
|
break;
|
||
|
case 'updateConfig':
|
||
|
if (forceLayout) {
|
||
|
for (var name in e.data.config) {
|
||
|
forceLayout[name] = e.data.config[name];
|
||
|
}
|
||
|
}
|
||
|
break;
|
||
|
case 'update':
|
||
|
var steps = e.data.steps;
|
||
|
|
||
|
if (forceLayout) {
|
||
|
var nNodes = forceLayout.nodes.length;
|
||
|
var positionArr = new Float32Array(nNodes * 2);
|
||
|
|
||
|
forceLayout.temperature = e.data.temperature;
|
||
|
|
||
|
for (var i = 0; i < steps; i++) {
|
||
|
forceLayout.update();
|
||
|
forceLayout.temperature *= e.data.coolDown;
|
||
|
}
|
||
|
// Callback
|
||
|
for (var i = 0; i < nNodes; i++) {
|
||
|
var node = forceLayout.nodes[i];
|
||
|
positionArr[i * 2] = node.position[0];
|
||
|
positionArr[i * 2 + 1] = node.position[1];
|
||
|
}
|
||
|
|
||
|
self.postMessage(positionArr.buffer, [positionArr.buffer]);
|
||
|
}
|
||
|
else {
|
||
|
// Not initialzied yet
|
||
|
var emptyArr = new Float32Array();
|
||
|
// Post transfer object
|
||
|
self.postMessage(emptyArr.buffer, [emptyArr.buffer]);
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
/* jshint ignore:end */
|
||
|
|
||
|
return ForceLayout;
|
||
|
});
|