Files
T-DAS/Epost.TestToolsWeb/Content/code/es-modules/modules/networkgraph/layouts.js

500 lines
15 KiB
JavaScript
Raw Normal View History

2023-01-13 15:30:20 +08:00
/**
* Networkgraph series
*
* (c) 2010-2018 Paweł Fus
*
* License: www.highcharts.com/license
*/
'use strict';
import H from '../../parts/Globals.js';
var pick = H.pick;
H.layouts = {
'reingold-fruchterman': function (options) {
this.options = options;
this.nodes = [];
this.links = [];
this.series = [];
this.box = {
x: 0,
y: 0,
width: 0,
height: 0
};
this.setInitialRendering(true);
}
};
H.extend(
/**
* Reingold-Fruchterman algorithm from
* "Graph Drawing by Force-directed Placement" paper.
*/
H.layouts['reingold-fruchterman'].prototype,
{
run: function () {
var layout = this,
series = this.series,
options = this.options;
if (layout.initialRendering) {
layout.initPositions();
// Render elements in initial positions:
series.forEach(function (s) {
s.render();
});
}
// Algorithm:
function localLayout() {
// Barycenter forces:
layout.applyBarycenterForces();
// Repulsive forces:
layout.applyRepulsiveForces();
// Attractive forces:
layout.applyAttractiveForces();
// Limit to the plotting area and cool down:
layout.applyLimits(layout.temperature);
// Cool down:
layout.temperature -= layout.diffTemperature;
layout.prevSystemTemperature = layout.systemTemperature;
layout.systemTemperature = layout.getSystemTemperature();
if (options.enableSimulation) {
series.forEach(function (s) {
s.render();
});
if (
layout.maxIterations-- &&
!layout.isStable()
) {
layout.simulation = H.win.requestAnimationFrame(
localLayout
);
} else {
layout.simulation = false;
}
}
}
layout.setK();
layout.resetSimulation(options);
if (options.enableSimulation) {
// Animate it:
layout.simulation = H.win.requestAnimationFrame(localLayout);
} else {
// Synchronous rendering:
while (
layout.maxIterations-- &&
!layout.isStable()
) {
localLayout();
}
series.forEach(function (s) {
s.render();
});
}
},
stop: function () {
if (this.simulation) {
H.win.cancelAnimationFrame(this.simulation);
}
},
setArea: function (x, y, w, h) {
this.box = {
left: x,
top: y,
width: w,
height: h
};
},
setK: function () {
// Optimal distance between nodes,
// available space around the node:
this.k = this.options.linkLength ||
Math.pow(
this.box.width * this.box.height / this.nodes.length,
0.4
);
},
addNodes: function (nodes) {
nodes.forEach(function (node) {
if (this.nodes.indexOf(node) === -1) {
this.nodes.push(node);
}
}, this);
},
removeNode: function (node) {
var index = this.nodes.indexOf(node);
if (index !== -1) {
this.nodes.splice(index, 1);
}
},
removeLink: function (link) {
var index = this.links.indexOf(link);
if (index !== -1) {
this.links.splice(index, 1);
}
},
addLinks: function (links) {
links.forEach(function (link) {
if (this.links.indexOf(link) === -1) {
this.links.push(link);
}
}, this);
},
addSeries: function (series) {
if (this.series.indexOf(series) === -1) {
this.series.push(series);
}
},
clear: function () {
this.nodes.length = 0;
this.links.length = 0;
this.series.length = 0;
this.resetSimulation();
},
resetSimulation: function () {
this.forcedStop = false;
this.systemTemperature = 0;
this.setMaxIterations();
this.setTemperature();
this.setDiffTemperature();
},
setMaxIterations: function (maxIterations) {
this.maxIterations = pick(
maxIterations,
this.options.maxIterations
);
},
setTemperature: function () {
this.temperature = Math.sqrt(this.nodes.length);
},
setDiffTemperature: function () {
this.diffTemperature = this.temperature /
(this.options.maxIterations + 1);
},
setInitialRendering: function (enable) {
this.initialRendering = enable;
},
initPositions: function () {
var initialPositions = this.options.initialPositions;
if (H.isFunction(initialPositions)) {
initialPositions.call(this);
} else if (initialPositions === 'circle') {
this.setCircularPositions();
} else {
this.setRandomPositions();
}
},
setCircularPositions: function () {
var box = this.box,
nodes = this.nodes,
nodesLength = nodes.length + 1,
angle = 2 * Math.PI / nodesLength,
rootNodes = nodes.filter(function (node) {
return node.linksTo.length === 0;
}),
sortedNodes = [];
function addToNodes(node) {
node.linksFrom.forEach(function (link) {
sortedNodes.push(link.toNode);
addToNodes(link.toNode);
});
}
// Start with identified root nodes an sort the nodes by their
// hierarchy. In trees, this ensures that branches don't cross
// eachother.
rootNodes.forEach(function (rootNode) {
sortedNodes.push(rootNode);
addToNodes(rootNode);
});
// Cyclic tree, no root node found
if (!sortedNodes.length) {
sortedNodes = nodes;
// Dangling, cyclic trees
} else {
nodes.forEach(function (node) {
if (sortedNodes.indexOf(node) === -1) {
sortedNodes.push(node);
}
});
}
// Initial positions are laid out along a small circle, appearing
// as a cluster in the middle
sortedNodes.forEach(function (node, index) {
node.plotX = pick(
node.plotX,
box.width / 2 + Math.cos(index * angle)
);
node.plotY = pick(
node.plotY,
box.height / 2 + Math.sin(index * angle)
);
node.dispX = 0;
node.dispY = 0;
});
},
setRandomPositions: function () {
var box = this.box,
nodes = this.nodes,
nodesLength = nodes.length + 1;
// Return a repeatable, quasi-random number based on an integer
// input. For the initial positions
function unrandom(n) {
var rand = n * n / Math.PI;
rand = rand - Math.floor(rand);
return rand;
}
// Initial positions:
nodes.forEach(
function (node, index) {
node.plotX = pick(
node.plotX,
box.width * unrandom(index)
);
node.plotY = pick(
node.plotY,
box.height * unrandom(nodesLength + index)
);
node.dispX = 0;
node.dispY = 0;
}
);
},
applyBarycenterForces: function () {
var nodesLength = this.nodes.length,
gravitationalConstant = this.options.gravitationalConstant,
cx = 0,
cy = 0;
// Calculate center:
this.nodes.forEach(function (node) {
cx += node.plotX;
cy += node.plotY;
});
this.barycenter = {
x: cx,
y: cy
};
// Apply forces:
this.nodes.forEach(function (node) {
var degree = node.getDegree(),
phi = degree * (1 + degree / 2);
node.dispX = (cx / nodesLength - node.plotX) *
gravitationalConstant * phi;
node.dispY = (cy / nodesLength - node.plotY) *
gravitationalConstant * phi;
});
},
applyRepulsiveForces: function () {
var layout = this,
nodes = layout.nodes,
options = layout.options,
k = this.k;
nodes.forEach(function (node) {
nodes.forEach(function (repNode) {
var force,
distanceR,
distanceXY;
if (
// Node can not repulse itself:
node !== repNode &&
// Only close nodes affect each other:
/* layout.getDistR(node, repNode) < 2 * k && */
// Not dragged:
!node.fixedPosition
) {
distanceXY = layout.getDistXY(node, repNode);
distanceR = layout.vectorLength(distanceXY);
if (distanceR !== 0) {
force = options.repulsiveForce.call(
layout, distanceR, k
);
node.dispX += (distanceXY.x / distanceR) * force;
node.dispY += (distanceXY.y / distanceR) * force;
}
}
});
});
},
applyAttractiveForces: function () {
var layout = this,
links = layout.links,
options = this.options,
k = this.k;
links.forEach(function (link) {
var distanceXY = layout.getDistXY(
link.fromNode,
link.toNode
),
distanceR = layout.vectorLength(distanceXY),
force = options.attractiveForce.call(
layout, distanceR, k
);
if (distanceR !== 0) {
if (!link.fromNode.fixedPosition) {
link.fromNode.dispX -= (distanceXY.x / distanceR) *
force;
link.fromNode.dispY -= (distanceXY.y / distanceR) *
force;
}
if (!link.toNode.fixedPosition) {
link.toNode.dispX += (distanceXY.x / distanceR) *
force;
link.toNode.dispY += (distanceXY.y / distanceR) *
force;
}
}
});
},
applyLimits: function (temperature) {
var layout = this,
options = layout.options,
nodes = layout.nodes,
box = layout.box,
distanceR;
nodes.forEach(function (node) {
if (node.fixedPosition) {
return;
}
// Friction:
node.dispX += options.friction * node.dispX;
node.dispY += options.friction * node.dispY;
distanceR = node.temperature = layout.vectorLength({
x: node.dispX,
y: node.dispY
});
// Place nodes:
if (distanceR !== 0) {
node.plotX += node.dispX / distanceR *
Math.min(Math.abs(node.dispX), temperature);
node.plotY += node.dispY / distanceR *
Math.min(Math.abs(node.dispY), temperature);
}
/*
TO DO: Consider elastic collision instead of stopping.
o' means end position when hitting plotting area edge:
- "inealstic":
o
\
______
| o'
| \
| \
- "elastic"/"bounced":
o
\
______
| ^
| / \
|o' \
*/
// Limit X-coordinates:
node.plotX = Math.round(
Math.max(
Math.min(
node.plotX,
box.width
),
box.left
)
);
// Limit Y-coordinates:
node.plotY = Math.round(
Math.max(
Math.min(
node.plotY,
box.height
),
box.top
)
);
// Reset displacement:
node.dispX = 0;
node.dispY = 0;
});
},
isStable: function () {
return Math.abs(
this.systemTemperature -
this.prevSystemTemperature
) === 0;
},
getSystemTemperature: function () {
return this.nodes.reduce(function (value, node) {
return value + node.temperature;
}, 0);
},
vectorLength: function (vector) {
return Math.sqrt(vector.x * vector.x + vector.y * vector.y);
},
getDistR: function (nodeA, nodeB) {
var distance = this.getDistXY(nodeA, nodeB);
return Math.sqrt(
distance.x * distance.x +
distance.y * distance.y
);
},
getDistXY: function (nodeA, nodeB) {
var xDist = nodeA.plotX - nodeB.plotX,
yDist = nodeA.plotY - nodeB.plotY;
return {
x: xDist,
y: yDist,
absX: Math.abs(xDist),
absY: Math.abs(yDist)
};
}
}
);