Browse Source

- made it pretty, fixed bugs in clustering

kamadaKawai
Alex de Mulder 9 years ago
parent
commit
09d3dfbb09
8 changed files with 793 additions and 439 deletions
  1. +543
    -353
      dist/vis.js
  2. +9
    -2
      lib/network/Network.js
  3. +62
    -27
      lib/network/modules/Clustering.js
  4. +87
    -26
      lib/network/modules/KamadaKawai.js
  5. +75
    -7
      lib/network/modules/LayoutEngine.js
  6. +3
    -13
      lib/network/modules/PhysicsEngine.js
  7. +11
    -11
      lib/network/modules/components/algorithms/FloydWarshall.js
  8. +3
    -0
      lib/network/modules/components/edges/BezierEdgeDynamic.js

+ 543
- 353
dist/vis.js
File diff suppressed because it is too large
View File


+ 9
- 2
lib/network/Network.js View File

@ -24,11 +24,11 @@ import InteractionHandler from './modules/InteractionHandler';
import SelectionHandler from "./modules/SelectionHandler";
import LayoutEngine from "./modules/LayoutEngine";
import ManipulationSystem from "./modules/ManipulationSystem";
import Configurator from "./../shared/Configurator";
import Configurator from "./../shared/Configurator";
import Validator from "./../shared/Validator";
import {printStyle} from "./../shared/Validator";
import {allOptions, configureOptions} from './options.js';
import KamadaKawai from "./modules/KamadaKawai.js"
/**
@ -92,6 +92,7 @@ function Network(container, data, options) {
createEdge: function() {},
getPointer: function() {}
},
modules: {},
view: {
scale: 1,
translation: {x: 0, y: 0}
@ -119,6 +120,9 @@ function Network(container, data, options) {
this.nodesHandler = new NodesHandler(this.body, this.images, this.groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options
this.edgesHandler = new EdgesHandler(this.body, this.images, this.groups); // Handle adding, deleting and updating of edges as well as global options
this.body.modules["kamadaKawai"] = new KamadaKawai(this.body,150,0.05); // Layouting algorithm.
this.body.modules["clustering"] = this.clustering;
// create the DOM elements
this.canvas._create();
@ -332,6 +336,9 @@ Network.prototype.setData = function (data) {
// emit change in data
this.body.emitter.emit("_dataChanged");
// emit data loaded
this.body.emitter.emit("_dataLoaded");
// find a stable position or start animating to a stable position
this.body.emitter.emit("initPhysics");
};

+ 62
- 27
lib/network/modules/Clustering.js View File

@ -44,6 +44,7 @@ class ClusterEngine {
for (let i = 0; i < nodesToCluster.length; i++) {
this.clusterByConnection(nodesToCluster[i],options,false);
}
this.body.emitter.emit('_dataChanged');
}
@ -83,11 +84,12 @@ class ClusterEngine {
/**
* Cluster all nodes in the network that have only 1 edge
* @param options
* @param refreshData
*/
clusterOutliers(options, refreshData = true) {
* Cluster all nodes in the network that have only X edges
* @param edgeCount
* @param options
* @param refreshData
*/
clusterByEdgeCount(edgeCount, options, refreshData = true) {
options = this._checkOptions(options);
let clusters = [];
@ -105,8 +107,8 @@ class ClusterEngine {
}
}
if (visibleEdges === 1) {
// this is an outlier
if (visibleEdges === edgeCount) {
// this is a qualifying node
let childNodeId = this._getConnectedId(edge, nodeId);
if (childNodeId !== nodeId) {
if (options.joinCondition === undefined) {
@ -145,6 +147,24 @@ class ClusterEngine {
}
}
/**
* Cluster all nodes in the network that have only 1 edge
* @param options
* @param refreshData
*/
clusterOutliers(options, refreshData = true) {
this.clusterByEdgeCount(1,options,refreshData);
}
/**
* Cluster all nodes in the network that have only 2 edge
* @param options
* @param refreshData
*/
clusterBridges(options, refreshData = true) {
this.clusterByEdgeCount(2,options,refreshData);
}
_checkIfUsed(clusters, nodeId, edgeId) {
for (let i = 0; i < clusters.length; i++) {
@ -189,25 +209,26 @@ class ClusterEngine {
let edge = node.edges[i];
let childNodeId = this._getConnectedId(edge, parentNodeId);
if (childNodeId !== parentNodeId) {
if (options.joinCondition === undefined) {
childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
}
else {
// clone the options and insert some additional parameters that could be interesting.
let childClonedOptions = this._cloneOptions(this.body.nodes[childNodeId]);
if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) {
if (this.clusteredNodes[childNodeId] === undefined) {
if (childNodeId !== parentNodeId) {
if (options.joinCondition === undefined) {
childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
}
else {
// clone the options and insert some additional parameters that could be interesting.
let childClonedOptions = this._cloneOptions(this.body.nodes[childNodeId]);
if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) {
childEdgesObj[edge.id] = edge;
childNodesObj[childNodeId] = this.body.nodes[childNodeId];
}
}
}
else {
childEdgesObj[edge.id] = edge;
}
}
else {
childEdgesObj[edge.id] = edge;
}
}
this._cluster(childNodesObj, childEdgesObj, options, refreshData);
}
@ -304,9 +325,19 @@ class ClusterEngine {
* @private
*/
_cluster(childNodesObj, childEdgesObj, options, refreshData = true) {
// kill condition: no children so cant cluster
// kill condition: no children so can't cluster
if (Object.keys(childNodesObj).length === 0) {return;}
// check if this cluster call is not trying to cluster anything that is in another cluster.
for (let nodeId in childNodesObj) {
if (childNodesObj.hasOwnProperty(nodeId)) {
if (this.clusteredNodes[nodeId] !== undefined) {
return;
}
}
}
let clusterNodeProperties = util.deepExtend({},options.clusterNodeProperties);
// construct the clusterNodeProperties
@ -314,17 +345,21 @@ class ClusterEngine {
// get the childNode options
let childNodesOptions = [];
for (let nodeId in childNodesObj) {
let clonedOptions = this._cloneOptions(childNodesObj[nodeId]);
childNodesOptions.push(clonedOptions);
if (childNodesObj.hasOwnProperty(nodeId)) {
let clonedOptions = this._cloneOptions(childNodesObj[nodeId]);
childNodesOptions.push(clonedOptions);
}
}
// get clusterproperties based on childNodes
let childEdgesOptions = [];
for (let edgeId in childEdgesObj) {
// these cluster edges will be removed on creation of the cluster.
if (edgeId.substr(0,12) !== "clusterEdge:") {
let clonedOptions = this._cloneOptions(childEdgesObj[edgeId], 'edge');
childEdgesOptions.push(clonedOptions);
if (childEdgesObj.hasOwnProperty(edgeId)) {
// these cluster edges will be removed on creation of the cluster.
if (edgeId.substr(0, 12) !== "clusterEdge:") {
let clonedOptions = this._cloneOptions(childEdgesObj[edgeId], 'edge');
childEdgesOptions.push(clonedOptions);
}
}
}

lib/network/modules/components/KamadaKawai.js → lib/network/modules/KamadaKawai.js View File

@ -2,8 +2,18 @@
* Created by Alex on 8/7/2015.
*/
import FloydWarshall from "./FloydWarshall.js"
// distance finding algorithm
import FloydWarshall from "./components/algorithms/FloydWarshall.js"
/**
* KamadaKawai positions the nodes initially based on
*
* "AN ALGORITHM FOR DRAWING GENERAL UNDIRECTED GRAPHS"
* -- Tomihisa KAMADA and Satoru KAWAI in 1989
*
* Possible optimizations in the distance calculation can be implemented.
*/
class KamadaKawai {
constructor(body, edgeLength, edgeStrength) {
this.body = body;
@ -12,6 +22,10 @@ class KamadaKawai {
this.distanceSolver = new FloydWarshall();
}
/**
* Not sure if needed but can be used to update the spring length and spring constant
* @param options
*/
setOptions(options) {
if (options) {
if (options.springLength) {
@ -23,10 +37,15 @@ class KamadaKawai {
}
}
solve(nodesArray, edgesArray) {
console.time("FLOYD - getDistances");
/**
* Position the system
* @param nodesArray
* @param edgesArray
*/
solve(nodesArray, edgesArray, ignoreClusters = false) {
// get distance matrix
let D_matrix = this.distanceSolver.getDistances(this.body, nodesArray, edgesArray); // distance matrix
console.timeEnd("FLOYD - getDistances");
// get the L Matrix
this._createL_matrix(D_matrix);
@ -34,42 +53,64 @@ class KamadaKawai {
// get the K Matrix
this._createK_matrix(D_matrix);
console.time("positioning")
// calculate positions
let threshold = 0.01;
let counter = 0;
let maxIterations = Math.min(10*this.body.nodeIndices.length);;
let maxEnergy = 1e9; // just to pass the first check.
let highE_nodeId = 0, dE_dx = 0, dE_dy = 0;
while (maxEnergy > threshold && counter < maxIterations) {
counter += 1;
[highE_nodeId, maxEnergy, dE_dx, dE_dy] = this._getHighestEnergyNode();
this._moveNode(highE_nodeId, dE_dx, dE_dy);
let innerThreshold = 1;
let iterations = 0;
let maxIterations = Math.max(1000,Math.min(10*this.body.nodeIndices.length,6000));
let maxInnerIterations = 5;
let maxEnergy = 1e9;
let highE_nodeId = 0, dE_dx = 0, dE_dy = 0, delta_m = 0, subIterations = 0;
while (maxEnergy > threshold && iterations < maxIterations) {
iterations += 1;
[highE_nodeId, maxEnergy, dE_dx, dE_dy] = this._getHighestEnergyNode(ignoreClusters);
delta_m = maxEnergy;
subIterations = 0;
while(delta_m > innerThreshold && subIterations < maxInnerIterations) {
subIterations += 1;
this._moveNode(highE_nodeId, dE_dx, dE_dy);
[delta_m,dE_dx,dE_dy] = this._getEnergy(highE_nodeId);
}
}
console.timeEnd("positioning")
}
_getHighestEnergyNode() {
/**
* get the node with the highest energy
* @returns {*[]}
* @private
*/
_getHighestEnergyNode(ignoreClusters) {
let nodesArray = this.body.nodeIndices;
let nodes = this.body.nodes;
let maxEnergy = 0;
let maxEnergyNode = nodesArray[0];
let energies = {dE_dx: 0, dE_dy: 0};
let maxEnergyNodeId = nodesArray[0];
let dE_dx_max = 0, dE_dy_max = 0;
for (let nodeIdx = 0; nodeIdx < nodesArray.length; nodeIdx++) {
let m = nodesArray[nodeIdx];
let [delta_m,dE_dx,dE_dy] = this._getEnergy(m);
if (maxEnergy < delta_m) {
maxEnergy = delta_m;
maxEnergyNode = m;
energies.dE_dx = dE_dx;
energies.dE_dy = dE_dy;
// by not evaluating nodes with predefined positions we should only move nodes that have no positions.
if ((nodes[m].predefinedPosition === false || nodes[m].isCluster === true && ignoreClusters === true) || nodes[m].options.fixed.x === true || nodes[m].options.fixed.y === true) {
let [delta_m,dE_dx,dE_dy] = this._getEnergy(m);
if (maxEnergy < delta_m) {
maxEnergy = delta_m;
maxEnergyNodeId = m;
dE_dx_max = dE_dx;
dE_dy_max = dE_dy;
}
}
}
return [maxEnergyNode, maxEnergy, energies.dE_dx, energies.dE_dy];
return [maxEnergyNodeId, maxEnergy, dE_dx_max, dE_dy_max];
}
/**
* calculate the energy of a single node
* @param m
* @returns {*[]}
* @private
*/
_getEnergy(m) {
let nodesArray = this.body.nodeIndices;
let nodes = this.body.nodes;
@ -93,6 +134,14 @@ class KamadaKawai {
return [delta_m, dE_dx, dE_dy];
}
/**
* move the node based on it's energy
* the dx and dy are calculated from the linear system proposed by Kamada and Kawai
* @param m
* @param dE_dx
* @param dE_dy
* @private
*/
_moveNode(m, dE_dx, dE_dy) {
let nodesArray = this.body.nodeIndices;
let nodes = this.body.nodes;
@ -125,6 +174,12 @@ class KamadaKawai {
nodes[m].y += dy;
}
/**
* Create the L matrix: edge length times shortest path
* @param D_matrix
* @private
*/
_createL_matrix(D_matrix) {
let nodesArray = this.body.nodeIndices;
let edgeLength = this.springLength;
@ -138,6 +193,12 @@ class KamadaKawai {
}
}
/**
* Create the K matrix: spring constants times shortest path
* @param D_matrix
* @private
*/
_createK_matrix(D_matrix) {
let nodesArray = this.body.nodeIndices;
let edgeStrength = this.springConstant;

+ 75
- 7
lib/network/modules/LayoutEngine.js View File

@ -1,7 +1,6 @@
'use strict'
import KamadaKawai from "./components/KamadaKawai.js"
var util = require('../../util');
let util = require('../../util');
class LayoutEngine {
constructor(body) {
@ -12,6 +11,7 @@ class LayoutEngine {
this.options = {};
this.optionsBackup = {};
this.defaultOptions = {
randomSeed: undefined,
hierarchical: {
@ -32,6 +32,9 @@ class LayoutEngine {
this.body.emitter.on('_dataChanged', () => {
this.setupHierarchicalLayout();
});
this.body.emitter.on('_dataLoaded', () => {
this.layoutNetwork();
});
this.body.emitter.on('_resetHierarchicalLayout', () => {
this.setupHierarchicalLayout();
});
@ -146,7 +149,7 @@ class LayoutEngine {
}
seededRandom() {
var x = Math.sin(this.randomSeed++) * 10000;
let x = Math.sin(this.randomSeed++) * 10000;
return x - Math.floor(x);
}
@ -167,6 +170,75 @@ class LayoutEngine {
}
}
layoutNetwork() {
// first check if we should KamadaKawai to layout. The threshold is if less than half of the visible
// nodes have predefined positions we use this.
let positionDefined = 0;
for (let i = 0; i < this.body.nodeIndices.length; i++) {
let node = this.body.nodes[this.body.nodeIndices[i]];
if (node.predefinedPosition === true) {
positionDefined += 1;
}
}
// if less than half of the nodes have a predefined position we continue
if (positionDefined < 0.5 * this.body.nodeIndices.length) {
let levels = 0;
// if there are a lot of nodes, we cluster before we run the algorithm.
if (this.body.nodeIndices.length > 100) {
let startLength = this.body.nodeIndices.length;
while(this.body.nodeIndices.length > 150) {
levels += 1;
if (levels % 5 === 0) {
this.body.modules.clustering.clusterByHubsize();
}
else if (levels % 3 === 0) {
this.body.modules.clustering.clusterBridges();
}
else {
this.body.modules.clustering.clusterOutliers();
}
console.log('levels', levels)
}
this.body.modules.kamadaKawai.setOptions({springLength: Math.max(150,2*startLength)})
}
// position the system for these nodes and edges
this.body.modules.kamadaKawai.solve(this.body.nodeIndices, this.body.edgeIndices, true);
// uncluster all clusters
if (levels > 0) {
let clustersPresent = true;
while (clustersPresent === true) {
clustersPresent = false;
for (let i = 0; i < this.body.nodeIndices.length; i++) {
if (this.body.nodes[this.body.nodeIndices[i]].isCluster === true) {
clustersPresent = true;
this.body.modules.clustering.openCluster(this.body.nodeIndices[i], {
releaseFunction: function (clusterPosition, containedNodesPositions) {
var newPositions = {};
for (let nodeId in containedNodesPositions) {
if (containedNodesPositions.hasOwnProperty(nodeId)) {
newPositions[nodeId] = {x:clusterPosition.x, y:clusterPosition.y};
}
}
return newPositions;
}
}, false);
}
}
if (clustersPresent === true) {
this.body.emitter.emit('_dataChanged');
}
}
}
// reposition all bezier nodes.
this.body.emitter.emit("_repositionBezierNodes");
}
}
getSeed() {
return this.initialRandomSeed;
}
@ -178,10 +250,6 @@ class LayoutEngine {
* @private
*/
setupHierarchicalLayout() {
let kk = new KamadaKawai(this.body,100,0.05);
kk.solve(this.body.nodeIndices, this.body.edgeIndices);
return
if (this.options.hierarchical.enabled === true && this.body.nodeIndices.length > 0) {
// get the size of the largest hubs and check if the user has defined a level for a node.
let node, nodeId;

+ 3
- 13
lib/network/modules/PhysicsEngine.js View File

@ -21,7 +21,6 @@ class PhysicsEngine {
this.previousStates = {};
this.freezeCache = {};
this.renderTimer = undefined;
this.initialStabilizationEmitted = false;
this.stabilized = false;
this.startedStabilization = false;
@ -236,21 +235,12 @@ class PhysicsEngine {
}
if (this.stabilized === true) {
if (this.stabilizationIterations > 1) {
// trigger the 'stabilized' event.
// The event is triggered on the next tick, to prevent the case that
// it is fired while initializing the Network, in which case you would not
// be able to catch it
this.startedStabilization = false;
//this._emitStabilized();
}
this.stopSimulation();
}
}
_emitStabilized() {
if (this.stabilizationIterations > 1 || this.initialStabilizationEmitted === false) {
this.initialStabilizationEmitted = true;
if (this.stabilizationIterations > 1) {
setTimeout(() => {
this.body.emitter.emit('stabilized', {iterations: this.stabilizationIterations});
this.stabilizationIterations = 0;
@ -377,7 +367,6 @@ class PhysicsEngine {
nodesPresent = true;
}
if (nodesPresent === true) {
if (vminCorrected > 0.5*this.options.maxVelocity) {
return false;
@ -548,11 +537,12 @@ class PhysicsEngine {
if (this.options.stabilization.onlyDynamicEdges === true) {
this._restoreFrozenNodes();
}
this.body.emitter.emit('stabilizationIterationsDone');
this.body.emitter.emit('_requestRedraw');
if (this.stabilized === true) {
console.log("emitted")
this._emitStabilized();
}
else {

lib/network/modules/components/FloydWarshall.js → lib/network/modules/components/algorithms/FloydWarshall.js View File

@ -7,14 +7,16 @@ class FloydWarshall {
constructor(){}
getDistances(body, nodesArray, edgesArray) {
let D_matrix = {}
let D_matrix = {};
let edges = body.edges;
// prepare matrix with large numbers
for (let i = 0; i < nodesArray.length; i++) {
D_matrix[nodesArray[i]] = {};
D_matrix[nodesArray[i]] = {};
for (let j = 0; j < nodesArray.length; j++) {
D_matrix[nodesArray[i]][nodesArray[j]] = 1e9;
D_matrix[nodesArray[i]][nodesArray[j]] = (i == j ? 0 : 1e9);
D_matrix[nodesArray[i]][nodesArray[j]] = (i == j ? 0 : 1e9);
}
}
@ -25,20 +27,18 @@ class FloydWarshall {
D_matrix[edge.toId][edge.fromId] = 1;
}
// calculate all pair distances
for (let k = 0; k < nodesArray.length; k++) {
for (let i = 0; i < nodesArray.length; i++) {
for (let j = 0; j < nodesArray.length; j++) {
let nodeCount = nodesArray.length;
// Adapted FloydWarshall based on unidirectionality to greatly reduce complexity.
for (let k = 0; k < nodeCount; k++) {
for (let i = 0; i < nodeCount-1; i++) {
for (let j = i+1; j < nodeCount; j++) {
D_matrix[nodesArray[i]][nodesArray[j]] = Math.min(D_matrix[nodesArray[i]][nodesArray[j]],D_matrix[nodesArray[i]][nodesArray[k]] + D_matrix[nodesArray[k]][nodesArray[j]])
D_matrix[nodesArray[j]][nodesArray[i]] = D_matrix[nodesArray[i]][nodesArray[j]];
}
}
}
// remove the self references from the matrix
for (let i = 0; i < nodesArray.length; i++) {
delete D_matrix[nodesArray[i]][nodesArray[i]];
}
return D_matrix;
}
}

+ 3
- 0
lib/network/modules/components/edges/BezierEdgeDynamic.js View File

@ -4,6 +4,8 @@ class BezierEdgeDynamic extends BezierEdgeBase {
constructor(options, body, labelModule) {
//this.via = undefined; // Here for completeness but not allowed to defined before super() is invoked.
super(options, body, labelModule); // --> this calls the setOptions below
this._boundFunction = () => {this.positionBezierNode();};
this.body.emitter.on("_repositionBezierNodes", this._boundFunction);
}
setOptions(options) {
@ -41,6 +43,7 @@ class BezierEdgeDynamic extends BezierEdgeBase {
* @returns {boolean}
*/
cleanup() {
this.body.emitter.off("_repositionBezierNodes", this._boundFunction);
if (this.via !== undefined) {
delete this.body.nodes[this.via.id];
this.via = undefined;

Loading…
Cancel
Save