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.
415 lines
14 KiB
415 lines
14 KiB
/**
|
|
* Edge bundling laytout
|
|
*
|
|
* Use MINGLE algorithm
|
|
* Multilevel agglomerative edge bundling for visualizing large graphs
|
|
*
|
|
* @module echarts/layout/EdgeBundling
|
|
*/
|
|
define(function (require) {
|
|
|
|
var KDTree = require('../data/KDTree');
|
|
var vec2 = require('zrender/tool/vector');
|
|
var v2Create = vec2.create;
|
|
var v2DistSquare = vec2.distSquare;
|
|
var v2Dist = vec2.dist;
|
|
var v2Copy = vec2.copy;
|
|
var v2Clone = vec2.clone;
|
|
|
|
function squaredDistance(a, b) {
|
|
a = a.array;
|
|
b = b.array;
|
|
|
|
var x = b[0] - a[0];
|
|
var y = b[1] - a[1];
|
|
var z = b[2] - a[2];
|
|
var w = b[3] - a[3];
|
|
|
|
return x * x + y * y + z * z + w * w;
|
|
}
|
|
|
|
function CoarsenedEdge(group) {
|
|
this.points = [
|
|
group.mp0, group.mp1
|
|
];
|
|
|
|
this.group = group;
|
|
}
|
|
|
|
function Edge(edge) {
|
|
var points = edge.points;
|
|
// Sort on y
|
|
if (
|
|
points[0][1] < points[1][1]
|
|
// If coarsened edge is flipped, the final composition of meet point
|
|
// will be unordered
|
|
|| edge instanceof CoarsenedEdge
|
|
) {
|
|
this.array = [points[0][0], points[0][1], points[1][0], points[1][1]];
|
|
this._startPoint = points[0];
|
|
this._endPoint = points[1];
|
|
}
|
|
else {
|
|
this.array = [points[1][0], points[1][1], points[0][0], points[0][1]];
|
|
this._startPoint = points[1];
|
|
this._endPoint = points[0];
|
|
}
|
|
|
|
this.ink = v2Dist(points[0], points[1]);
|
|
|
|
this.edge = edge;
|
|
|
|
this.group = null;
|
|
}
|
|
|
|
Edge.prototype.getStartPoint = function () {
|
|
return this._startPoint;
|
|
};
|
|
|
|
Edge.prototype.getEndPoint = function () {
|
|
return this._endPoint;
|
|
};
|
|
|
|
function BundledEdgeGroup() {
|
|
|
|
this.edgeList = [];
|
|
|
|
this.mp0 = v2Create();
|
|
this.mp1 = v2Create();
|
|
|
|
this.ink = 0;
|
|
}
|
|
|
|
BundledEdgeGroup.prototype.addEdge = function (edge) {
|
|
edge.group = this;
|
|
this.edgeList.push(edge);
|
|
};
|
|
|
|
BundledEdgeGroup.prototype.removeEdge = function (edge) {
|
|
edge.group = null;
|
|
this.edgeList.splice(this.edgeList.indexOf(edge), 1);
|
|
};
|
|
|
|
/**
|
|
* @constructor
|
|
* @alias module:echarts/layout/EdgeBundling
|
|
*/
|
|
function EdgeBundling() {
|
|
this.maxNearestEdge = 6;
|
|
this.maxTurningAngle = Math.PI / 4;
|
|
this.maxIteration = 20;
|
|
}
|
|
|
|
EdgeBundling.prototype = {
|
|
|
|
constructor: EdgeBundling,
|
|
|
|
run: function (rawEdges) {
|
|
var res = this._iterate(rawEdges);
|
|
var nIterate = 0;
|
|
while (nIterate++ < this.maxIteration) {
|
|
var coarsenedEdges = [];
|
|
for (var i = 0; i < res.groups.length; i++) {
|
|
coarsenedEdges.push(new CoarsenedEdge(res.groups[i]));
|
|
}
|
|
var newRes = this._iterate(coarsenedEdges);
|
|
if (newRes.savedInk <= 0) {
|
|
break;
|
|
} else {
|
|
res = newRes;
|
|
}
|
|
}
|
|
|
|
// Get new edges
|
|
var newEdges = [];
|
|
|
|
function pointApproxEqual(p0, p1) {
|
|
// Use Float32Array may affect the precision
|
|
return v2DistSquare(p0, p1) < 1e-10;
|
|
}
|
|
// Clone all points to make sure all points in edge will not reference to the same array
|
|
// And clean the duplicate points
|
|
function cleanEdgePoints(edgePoints, rawEdgePoints) {
|
|
var res = [];
|
|
var off = 0;
|
|
for (var i = 0; i < edgePoints.length; i++) {
|
|
if (! (off > 0 && pointApproxEqual(edgePoints[i], res[off - 1]))) {
|
|
res[off++] = v2Clone(edgePoints[i]);
|
|
}
|
|
}
|
|
// Edge has been reversed
|
|
if (rawEdgePoints[0] && !pointApproxEqual(res[0], rawEdgePoints[0])) {
|
|
res = res.reverse();
|
|
}
|
|
return res;
|
|
}
|
|
|
|
var buildNewEdges = function (groups, fromEdgePoints) {
|
|
var newEdgePoints;
|
|
for (var i = 0; i < groups.length; i++) {
|
|
var group = groups[i];
|
|
if (
|
|
group.edgeList[0]
|
|
&& (group.edgeList[0].edge instanceof CoarsenedEdge)
|
|
) {
|
|
var newGroups = [];
|
|
for (var j = 0; j < group.edgeList.length; j++) {
|
|
newGroups.push(group.edgeList[j].edge.group);
|
|
}
|
|
if (! fromEdgePoints) {
|
|
newEdgePoints = [];
|
|
} else {
|
|
newEdgePoints = fromEdgePoints.slice();
|
|
}
|
|
newEdgePoints.unshift(group.mp0);
|
|
newEdgePoints.push(group.mp1);
|
|
buildNewEdges(newGroups, newEdgePoints);
|
|
} else {
|
|
// console.log(group.edgeList.length);
|
|
for (var j = 0; j < group.edgeList.length; j++) {
|
|
var edge = group.edgeList[j];
|
|
if (! fromEdgePoints) {
|
|
newEdgePoints = [];
|
|
} else {
|
|
newEdgePoints = fromEdgePoints.slice();
|
|
}
|
|
newEdgePoints.unshift(group.mp0);
|
|
newEdgePoints.push(group.mp1);
|
|
newEdgePoints.unshift(edge.getStartPoint());
|
|
newEdgePoints.push(edge.getEndPoint());
|
|
newEdges.push({
|
|
points: cleanEdgePoints(newEdgePoints, edge.edge.points),
|
|
rawEdge: edge.edge
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
buildNewEdges(res.groups);
|
|
|
|
return newEdges;
|
|
},
|
|
|
|
_iterate: function (rawEdges) {
|
|
var edges = [];
|
|
var groups = [];
|
|
var totalSavedInk = 0;
|
|
for (var i = 0; i < rawEdges.length; i++) {
|
|
var edge = new Edge(rawEdges[i]);
|
|
edges.push(edge);
|
|
}
|
|
|
|
var tree = new KDTree(edges, 4);
|
|
|
|
var nearests = [];
|
|
|
|
var _mp0 = v2Create();
|
|
var _mp1 = v2Create();
|
|
var _newGroupInk = 0;
|
|
var mp0 = v2Create();
|
|
var mp1 = v2Create();
|
|
var newGroupInk = 0;
|
|
for (var i = 0; i < edges.length; i++) {
|
|
var edge = edges[i];
|
|
if (edge.group) {
|
|
// Edge have been groupped
|
|
continue;
|
|
}
|
|
tree.nearestN(
|
|
edge, this.maxNearestEdge,
|
|
squaredDistance, nearests
|
|
);
|
|
var maxSavedInk = 0;
|
|
var mostSavingInkEdge = null;
|
|
var lastCheckedGroup = null;
|
|
for (var j = 0; j < nearests.length; j++) {
|
|
var nearest = nearests[j];
|
|
var savedInk = 0;
|
|
if (nearest.group) {
|
|
if (nearest.group !== lastCheckedGroup) {
|
|
lastCheckedGroup = nearest.group;
|
|
_newGroupInk = this._calculateGroupEdgeInk(
|
|
nearest.group, edge, _mp0, _mp1
|
|
);
|
|
savedInk = nearest.group.ink + edge.ink - _newGroupInk;
|
|
}
|
|
}
|
|
else {
|
|
_newGroupInk = this._calculateEdgeEdgeInk(
|
|
edge, nearest, _mp0, _mp1
|
|
);
|
|
savedInk = nearest.ink + edge.ink - _newGroupInk;
|
|
}
|
|
if (savedInk > maxSavedInk) {
|
|
maxSavedInk = savedInk;
|
|
mostSavingInkEdge = nearest;
|
|
v2Copy(mp1, _mp1);
|
|
v2Copy(mp0, _mp0);
|
|
newGroupInk = _newGroupInk;
|
|
}
|
|
}
|
|
if (mostSavingInkEdge) {
|
|
totalSavedInk += maxSavedInk;
|
|
var group;
|
|
if (! mostSavingInkEdge.group) {
|
|
group = new BundledEdgeGroup();
|
|
groups.push(group);
|
|
group.addEdge(mostSavingInkEdge);
|
|
}
|
|
group = mostSavingInkEdge.group;
|
|
// Use the meet point and group ink calculated before
|
|
v2Copy(group.mp0, mp0);
|
|
v2Copy(group.mp1, mp1);
|
|
group.ink = newGroupInk;
|
|
mostSavingInkEdge.group.addEdge(edge);
|
|
}
|
|
else {
|
|
var group = new BundledEdgeGroup();
|
|
groups.push(group);
|
|
v2Copy(group.mp0, edge.getStartPoint());
|
|
v2Copy(group.mp1, edge.getEndPoint());
|
|
group.ink = edge.ink;
|
|
group.addEdge(edge);
|
|
}
|
|
}
|
|
|
|
return {
|
|
groups: groups,
|
|
edges: edges,
|
|
savedInk: totalSavedInk
|
|
};
|
|
},
|
|
|
|
_calculateEdgeEdgeInk: (function () {
|
|
var startPointSet = [];
|
|
var endPointSet = [];
|
|
return function (e0, e1, mp0, mp1) {
|
|
startPointSet[0] = e0.getStartPoint();
|
|
startPointSet[1] = e1.getStartPoint();
|
|
endPointSet[0] = e0.getEndPoint();
|
|
endPointSet[1] = e1.getEndPoint();
|
|
|
|
this._calculateMeetPoints(
|
|
startPointSet, endPointSet, mp0, mp1
|
|
);
|
|
var ink = v2Dist(startPointSet[0], mp0)
|
|
+ v2Dist(mp0, mp1)
|
|
+ v2Dist(mp1, endPointSet[0])
|
|
+ v2Dist(startPointSet[1], mp0)
|
|
+ v2Dist(mp1, endPointSet[1]);
|
|
|
|
return ink;
|
|
};
|
|
})(),
|
|
|
|
_calculateGroupEdgeInk: function (group, edgeTryAdd, mp0, mp1) {
|
|
var startPointSet = [];
|
|
var endPointSet = [];
|
|
for (var i = 0; i < group.edgeList.length; i++) {
|
|
var edge = group.edgeList[i];
|
|
startPointSet.push(edge.getStartPoint());
|
|
endPointSet.push(edge.getEndPoint());
|
|
}
|
|
startPointSet.push(edgeTryAdd.getStartPoint());
|
|
endPointSet.push(edgeTryAdd.getEndPoint());
|
|
|
|
this._calculateMeetPoints(
|
|
startPointSet, endPointSet, mp0, mp1
|
|
);
|
|
|
|
var ink = v2Dist(mp0, mp1);
|
|
for (var i = 0; i < startPointSet.length; i++) {
|
|
ink += v2Dist(startPointSet[i], mp0)
|
|
+ v2Dist(endPointSet[i], mp1);
|
|
}
|
|
|
|
return ink;
|
|
},
|
|
|
|
/**
|
|
* Calculating the meet points
|
|
* @method
|
|
* @param {Array} startPointSet Start points set of bundled edges
|
|
* @param {Array} endPointSet End points set of bundled edges
|
|
* @param {Array.<number>} mp0 Output meet point 0
|
|
* @param {Array.<number>} mp1 Output meet point 1
|
|
*/
|
|
_calculateMeetPoints: (function () {
|
|
var cp0 = v2Create();
|
|
var cp1 = v2Create();
|
|
return function (startPointSet, endPointSet, mp0, mp1) {
|
|
vec2.set(cp0, 0, 0);
|
|
vec2.set(cp1, 0, 0);
|
|
var len = startPointSet.length;
|
|
// Calculate the centroid of start points set
|
|
for (var i = 0; i < len; i++) {
|
|
vec2.add(cp0, cp0, startPointSet[i]);
|
|
}
|
|
vec2.scale(cp0, cp0, 1 / len);
|
|
|
|
// Calculate the centroid of end points set
|
|
len = endPointSet.length;
|
|
for (var i = 0; i < len; i++) {
|
|
vec2.add(cp1, cp1, endPointSet[i]);
|
|
}
|
|
vec2.scale(cp1, cp1, 1 / len);
|
|
|
|
this._limitTurningAngle(
|
|
startPointSet, cp0, cp1, mp0
|
|
);
|
|
this._limitTurningAngle(
|
|
endPointSet, cp1, cp0, mp1
|
|
);
|
|
};
|
|
})(),
|
|
|
|
_limitTurningAngle: (function () {
|
|
var v10 = v2Create();
|
|
var vTmp = v2Create();
|
|
var project = v2Create();
|
|
var tmpOut = v2Create();
|
|
return function (pointSet, p0, p1, out) {
|
|
// Limit the max turning angle
|
|
var maxTurningAngleCos = Math.cos(this.maxTurningAngle);
|
|
var maxTurningAngleTan = Math.tan(this.maxTurningAngle);
|
|
|
|
vec2.sub(v10, p0, p1);
|
|
vec2.normalize(v10, v10);
|
|
|
|
// Simply copy the centroid point if no need to turn the angle
|
|
vec2.copy(out, p0);
|
|
|
|
var maxMovement = 0;
|
|
for (var i = 0; i < pointSet.length; i++) {
|
|
var p = pointSet[i];
|
|
vec2.sub(vTmp, p, p0);
|
|
var len = vec2.len(vTmp);
|
|
vec2.scale(vTmp, vTmp, 1 / len);
|
|
var turningAngleCos = vec2.dot(vTmp, v10);
|
|
// Turning angle is to large
|
|
if (turningAngleCos < maxTurningAngleCos) {
|
|
// Calculat p's project point on vector p1-p0
|
|
// and distance to the vector
|
|
vec2.scaleAndAdd(
|
|
project, p0, v10, len * turningAngleCos
|
|
);
|
|
var distance = v2Dist(project, p);
|
|
|
|
// Use the max turning angle to calculate the new meet point
|
|
var d = distance / maxTurningAngleTan;
|
|
vec2.scaleAndAdd(tmpOut, project, v10, -d);
|
|
|
|
var movement = v2DistSquare(tmpOut, p0);
|
|
if (movement > maxMovement) {
|
|
maxMovement = movement;
|
|
vec2.copy(out, tmpOut);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
})()
|
|
};
|
|
|
|
return EdgeBundling;
|
|
});
|