@ -42,8 +42,9 @@ class ClusterEngine {
}
for ( let i = 0 ; i < nodesToCluster . length ; i ++ ) {
this . clusterByConnection ( nodesToCluster [ i ] , options , fals e) ;
this . clusterByConnection ( nodesToCluster [ i ] , options , tru e) ;
}
this . body . emitter . emit ( '_dataChanged' ) ;
}
@ -73,7 +74,9 @@ class ClusterEngine {
// collect the nodes that will be in the cluster
for ( let i = 0 ; i < node . edges . length ; i ++ ) {
let edge = node . edges [ i ] ;
childEdgesObj [ edge . id ] = edge ;
if ( edge . hiddenByCluster !== true ) {
childEdgesObj [ edge . id ] = edge ;
}
}
}
}
@ -83,53 +86,72 @@ 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 = [ ] ;
let usedNodes = { } ;
let edge , edges , node , nodeId , visibleEdges ;
// collect the nodes that will be in the cluster
for ( let i = 0 ; i < this . body . nodeIndices . length ; i ++ ) {
let childNodesObj = { } ;
let childEdgesObj = { } ;
let nodeId = this . body . nodeIndices [ i ] ;
let visibleEdges = 0 ;
let edge ;
for ( let j = 0 ; j < this . body . nodes [ nodeId ] . edges . length ; j ++ ) {
if ( this . body . nodes [ nodeId ] . edges [ j ] . options . hidden === false ) {
visibleEdges ++ ;
edge = this . body . nodes [ nodeId ] . edges [ j ] ;
nodeId = this . body . nodeIndices [ i ] ;
// if this node is already used in another cluster this session, we do not have to re-evaluate it.
if ( usedNodes [ nodeId ] === undefined ) {
visibleEdges = 0 ;
node = this . body . nodes [ nodeId ] ;
edges = [ ] ;
for ( let j = 0 ; j < node . edges . length ; j ++ ) {
edge = node . edges [ j ] ;
if ( edge . hiddenByCluster !== true ) {
edges . push ( edge ) ;
}
}
}
if ( visibleEdges === 1 ) {
// this is an outlier
let childNodeId = this . _getConnectedId ( edge , nodeId ) ;
if ( childNodeId !== nodeId ) {
if ( options . joinCondition === undefined ) {
if ( this . _checkIfUsed ( clusters , nodeId , edge . id ) === false && this . _checkIfUsed ( clusters , childNodeId , edge . id ) === false ) {
childEdgesObj [ edge . id ] = edge ;
childNodesObj [ nodeId ] = this . body . nodes [ nodeId ] ;
childNodesObj [ childNodeId ] = this . body . nodes [ childNodeId ] ;
}
}
else {
let clonedOptions = this . _cloneOptions ( this . body . nodes [ nodeId ] ) ;
if ( options . joinCondition ( clonedOptions ) === true && this . _checkIfUsed ( clusters , nodeId , edge . id ) === false ) {
childEdgesObj [ edge . id ] = edge ;
childNodesObj [ nodeId ] = this . body . nodes [ nodeId ] ;
// this node qualifies, we collect its neighbours to start the clustering process.
if ( edges . length === edgeCount ) {
let gatheringSuccessful = true ;
for ( let j = 0 ; j < edges . length ; j ++ ) {
edge = edges [ j ] ;
let childNodeId = this . _getConnectedId ( edge , nodeId ) ;
// if unused and if not referencing itself
if ( childNodeId !== nodeId && usedNodes [ nodeId ] === undefined ) {
// add the nodes to the list by the join condition.
if ( options . joinCondition === undefined ) {
childEdgesObj [ edge . id ] = edge ;
childNodesObj [ nodeId ] = this . body . nodes [ nodeId ] ;
childNodesObj [ childNodeId ] = this . body . nodes [ childNodeId ] ;
usedNodes [ nodeId ] = true ;
}
else {
let clonedOptions = this . _cloneOptions ( this . body . nodes [ nodeId ] ) ;
if ( options . joinCondition ( clonedOptions ) === true ) {
childEdgesObj [ edge . id ] = edge ;
childNodesObj [ nodeId ] = this . body . nodes [ nodeId ] ;
usedNodes [ nodeId ] = true ;
}
else {
// this node does not qualify after all.
gatheringSuccessful = false ;
break ;
}
}
}
clonedOptions = this . _cloneOptions ( this . body . nodes [ childNodeId ] ) ;
if ( options . joinCondition ( clonedOptions ) === true && this . _checkIfUsed ( clusters , nodeId , edge . id ) === false ) {
childEdgesObj [ edge . id ] = edge ;
childNodesObj [ childNodeId ] = this . body . nodes [ childNodeId ] ;
else {
// this node does not qualify after all.
gatheringSuccessful = fals e;
break ;
}
}
if ( Object . keys ( childNodesObj ) . length > 0 && Object . keys ( childEdgesObj ) . length > 0 ) {
// add to the cluster queue
if ( Object . keys ( childNodesObj ) . length > 0 && Object . keys ( childEdgesObj ) . length > 0 && gatheringSuccessful === true ) {
clusters . push ( { nodes : childNodesObj , edges : childEdgesObj } )
}
}
@ -145,17 +167,26 @@ 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 ) ;
}
_checkIfUsed ( clusters , nodeId , edgeId ) {
for ( let i = 0 ; i < clusters . length ; i ++ ) {
let cluster = clusters [ i ] ;
if ( cluster . nodes [ nodeId ] !== undefined || cluster . edges [ edgeId ] !== undefined ) {
return true ;
}
}
return false ;
/ * *
* Cluster all nodes in the network that have only 2 edge
* @ param options
* @ param refreshData
* /
clusterBridges ( options , refreshData = true ) {
this . clusterByEdgeCount ( 2 , options , refreshData ) ;
}
/ * *
* suck all connected nodes of a node into the node .
* @ param nodeId
@ -187,25 +218,31 @@ class ClusterEngine {
// collect the nodes that will be in the cluster
for ( let i = 0 ; i < node . edges . length ; i ++ ) {
let edge = node . edges [ i ] ;
let childNodeId = this . _getConnectedId ( edge , parentNodeId ) ;
if ( edge . hiddenByCluster !== true ) {
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 the child node is not in a cluster (may not be needed now with the edge.hiddenByCluster check)
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 {
// swallow the edge if it is self-referencing.
childEdgesObj [ edge . id ] = edge ;
childNodesObj [ childNodeId ] = this . body . nodes [ childNodeId ] ;
}
}
}
else {
childEdgesObj [ edge . id ] = edge ;
}
}
this . _cluster ( childNodesObj , childEdgesObj , options , refreshData ) ;
@ -235,18 +272,21 @@ class ClusterEngine {
/ * *
* This function creates the edges that will be attached to the cluster .
* This function creates the edges that will be attached to the cluster
* It looks for edges that are connected to the nodes from the " outside ' of the cluster .
*
* @ param childNodesObj
* @ param childEdgesObj
* @ param newEdges
* @ param options
* @ private
* /
_createClusterEdges ( childNodesObj , childEdgesObj , newEdges , c lusterNodeProperties , clusterEdgeProperties ) {
_createClusterEdges ( childNodesObj , clusterNodeProperties , clusterEdgeProperties ) {
let edge , childNodeId , childNode , toId , fromId , otherNodeId ;
// loop over all child nodes and their edges to find edges going out of the cluster
// these edges will be replaced by clusterEdges.
let childKeys = Object . keys ( childNodesObj ) ;
let createEdges = [ ] ;
for ( let i = 0 ; i < childKeys . length ; i ++ ) {
childNodeId = childKeys [ i ] ;
childNode = childNodesObj [ childNodeId ] ;
@ -254,31 +294,56 @@ class ClusterEngine {
// construct new edges from the cluster to others
for ( let j = 0 ; j < childNode . edges . length ; j ++ ) {
edge = childNode . edges [ j ] ;
childEdgesObj [ edge . id ] = edge ;
// childNodeId position will be replaced by the cluster .
if ( edge . toId == childNodeId ) { // this is a double equals because ints and strings can be interchanged here.
toId = clusterNodeProperties . id ;
fromId = edge . fromId ;
otherNodeId = fromId ;
}
else {
toId = edge . toId ;
fromId = clusterNodeProperties . id ;
otherNodeId = toId ;
}
// we only handle edges that are visible to the system, not the disabled ones from the clustering process.
if ( edge . hiddenByCluster !== true ) {
// set up the from and to .
if ( edge . toId == childNodeId ) { // this is a double equals because ints and strings can be interchanged here.
toId = clusterNodeProperties . id ;
fromId = edge . fromId ;
otherNodeId = fromId ;
}
else {
toId = edge . toId ;
fromId = clusterNodeProperties . id ;
otherNodeId = toId ;
}
// if the node connected to the cluster is also in the cluster we do not need a new edge.
if ( childNodesObj [ otherNodeId ] === undefined ) {
let clonedOptions = this . _cloneOptions ( edge , 'edge' ) ;
util . deepExtend ( clonedOptions , clusterEdgeProperties ) ;
clonedOptions . from = fromId ;
clonedOptions . to = toId ;
clonedOptions . id = 'clusterEdge:' + util . randomUUID ( ) ;
newEdges . push ( this . body . functions . createEdge ( clonedOptions ) ) ;
// Only edges from the cluster outwards are being replaced.
if ( childNodesObj [ otherNodeId ] === undefined ) {
createEdges . push ( { edge : edge , fromId : fromId , toId : toId } ) ;
}
}
}
}
// here we actually create the replacement edges. We could not do this in the loop above as the creation process
// would add an edge to the edges array we are iterating over.
for ( let j = 0 ; j < createEdges . length ; j ++ ) {
let edge = createEdges [ j ] . edge ;
// copy the options of the edge we will replace
let clonedOptions = this . _cloneOptions ( edge , 'edge' ) ;
// make sure the properties of clusterEdges are superimposed on it
util . deepExtend ( clonedOptions , clusterEdgeProperties ) ;
// set up the edge
clonedOptions . from = createEdges [ j ] . fromId ;
clonedOptions . to = createEdges [ j ] . toId ;
clonedOptions . id = 'clusterEdge:' + util . randomUUID ( ) ;
//clonedOptions.id = '(cf: ' + createEdges[j].fromId + " to: " + createEdges[j].toId + ")" + Math.random();
// create the edge and give a reference to the one it replaced.
let newEdge = this . body . functions . createEdge ( clonedOptions ) ;
newEdge . clusteringEdgeReplacingId = edge . id ;
// connect the edge.
this . body . edges [ newEdge . id ] = newEdge ;
newEdge . connect ( ) ;
// hide the replaced edge
edge . setOptions ( { physics : false , hidden : true } ) ;
edge . hiddenByCluster = true ;
}
}
/ * *
@ -304,8 +369,17 @@ class ClusterEngine {
* @ private
* /
_cluster ( childNodesObj , childEdgesObj , options , refreshData = true ) {
// kill condition: no children so cant cluster
if ( Object . keys ( childNodesObj ) . length === 0 ) { return ; }
// kill condition: no children so can't cluster or only one node in the cluster, dont bother
if ( Object . keys ( childNodesObj ) . length < 2 ) { 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 ) ;
@ -314,17 +388,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 ) ;
}
}
}
@ -350,9 +428,7 @@ class ClusterEngine {
clusterNodeProperties . x = pos . x ;
}
if ( clusterNodeProperties . y === undefined ) {
if ( pos === undefined ) {
pos = this . _getClusterPosition ( childNodesObj ) ;
}
if ( pos === undefined ) { pos = this . _getClusterPosition ( childNodesObj ) ; }
clusterNodeProperties . y = pos . y ;
}
@ -371,28 +447,15 @@ class ClusterEngine {
this . body . nodes [ clusterNodeProperties . id ] = clusterNode ;
// create the new edges that will connect to the cluster
let newEdges = [ ] ;
this . _createClusterEdges ( childNodesObj , childEdgesObj , newEdges , clusterNodeProperties , options . clusterEdgeProperties ) ;
this . _createClusterEdges ( childNodesObj , clusterNodeProperties , options . clusterEdgeProperties ) ;
// disable the childEdges
for ( let edgeId in childEdgesObj ) {
if ( childEdgesObj . hasOwnProperty ( edgeId ) ) {
if ( this . body . edges [ edgeId ] !== undefined ) {
let edge = this . body . edges [ edgeId ] ;
// if this is a cluster edge that is fully encompassed in the cluster, we want to delete it
// this check verifies that both of the connected nodes are in this cluster
if ( edgeId . substr ( 0 , 12 ) === "clusterEdge:" && childNodesObj [ edge . fromId ] !== undefined && childNodesObj [ edge . toId ] !== undefined ) {
edge . cleanup ( ) ;
// this removes the edge from node.edges, which is why edgeIds is formed
edge . disconnect ( ) ;
delete childEdgesObj [ edgeId ] ;
delete this . body . edges [ edgeId ] ;
}
else {
edge . setOptions ( { physics : false , hidden : true } ) ;
//edge.options.hidden = true;
}
edge . setOptions ( { physics : false , hidden : true } ) ;
edge . hiddenByCluster = true ;
}
}
}
@ -405,12 +468,6 @@ class ClusterEngine {
}
}
// push new edges to global
for ( let i = 0 ; i < newEdges . length ; i ++ ) {
this . body . edges [ newEdges [ i ] . id ] = newEdges [ i ] ;
this . body . edges [ newEdges [ i ] . id ] . connect ( ) ;
}
// set ID to undefined so no duplicates arise
clusterNodeProperties . id = undefined ;
@ -496,8 +553,8 @@ class ClusterEngine {
if ( containedNodes . hasOwnProperty ( nodeId ) ) {
let containedNode = this . body . nodes [ nodeId ] ;
if ( newPositions [ nodeId ] !== undefined ) {
containedNode . x = newPositions [ nodeId ] . x || clusterNode . x ;
containedNode . y = newPositions [ nodeId ] . y || clusterNode . y ;
containedNode . x = ( newPositions [ nodeId ] . x === undefined ? clusterNode . x : newPositions [ nodeId ] . x ) ;
containedNode . y = ( newPositions [ nodeId ] . y === undefined ? clusterNode . y : newPositions [ nodeId ] . y ) ;
}
}
}
@ -525,78 +582,79 @@ class ClusterEngine {
containedNode . vy = clusterNode . vy ;
// we use these methods to avoid reinstantiating the shape, which happens with setOptions.
//containedNode.toggleHidden(false);
//containedNode.togglePhysics(true);
containedNode . setOptions ( { hidden : false , physics : true } ) ;
delete this . clusteredNodes [ nodeId ] ;
}
}
// release edges
for ( let edgeId in containedEdges ) {
if ( containedEdges . hasOwnProperty ( edgeId ) ) {
let edge = containedEdges [ edgeId ] ;
// if this edge was a temporary edge and it's connected nodes do not exist anymore, we remove it from the data
if ( this . body . nodes [ edge . fromId ] === undefined || this . body . nodes [ edge . toId ] === undefined || edge . toId == clusterNodeId || edge . fromId == clusterNodeId ) {
edge . cleanup ( ) ;
// this removes the edge from node.edges, which is why edgeIds is formed
edge . disconnect ( ) ;
delete this . body . edges [ edgeId ] ;
}
else {
// one of the nodes connected to this edge is in a cluster. We give the edge to that cluster so it will be released when that cluster is opened.
if ( this . clusteredNodes [ edge . fromId ] !== undefined || this . clusteredNodes [ edge . toId ] !== undefined ) {
let fromId , toId ;
let clusteredNode = this . clusteredNodes [ edge . fromId ] || this . clusteredNodes [ edge . toId ] ;
let clusterId = clusteredNode . clusterId ;
let clusterNode = this . body . nodes [ clusterId ] ;
clusterNode . containedEdges [ edgeId ] = edge ;
if ( this . clusteredNodes [ edge . fromId ] !== undefined ) {
fromId = clusterId ;
toId = edge . toId ;
}
else {
fromId = edge . fromId ;
toId = clusterId ;
}
// if both from and to nodes are visible, we create a new temporary edge
if ( this . body . nodes [ fromId ] . options . hidden !== true && this . body . nodes [ toId ] . options . hidden !== true ) {
let clonedOptions = this . _cloneOptions ( edge , 'edge' ) ;
let id = 'clusterEdge:' + util . randomUUID ( ) ;
util . deepExtend ( clonedOptions , clusterNode . clusterEdgeProperties ) ;
util . deepExtend ( clonedOptions , { from : fromId , to : toId , hidden : false , physics : true , id : id } ) ;
let newEdge = this . body . functions . createEdge ( clonedOptions ) ;
this . body . edges [ id ] = newEdge ;
this . body . edges [ id ] . connect ( ) ;
}
// copy the clusterNode edges because we cannot iterate over an object that we add or remove from.
let edgesToBeDeleted = [ ] ;
for ( let i = 0 ; i < clusterNode . edges . length ; i ++ ) {
edgesToBeDeleted . push ( clusterNode . edges [ i ] ) ;
}
// actually handling the deleting.
for ( let i = 0 ; i < edgesToBeDeleted . length ; i ++ ) {
let edge = edgesToBeDeleted [ i ] ;
let otherNodeId = this . _getConnectedId ( edge , clusterNodeId ) ;
// if the other node is in another cluster, we transfer ownership of this edge to the other cluster
if ( this . clusteredNodes [ otherNodeId ] !== undefined ) {
// transfer ownership:
let otherCluster = this . body . nodes [ this . clusteredNodes [ otherNodeId ] . clusterId ] ;
let transferEdge = this . body . edges [ edge . clusteringEdgeReplacingId ] ;
if ( transferEdge !== undefined ) {
otherCluster . containedEdges [ transferEdge . id ] = transferEdge ;
// delete local reference
delete containedEdges [ transferEdge . id ] ;
// create new cluster edge from the otherCluster:
// get to and from
let fromId = transferEdge . fromId ;
let toId = transferEdge . toId ;
if ( transferEdge . toId == otherNodeId ) {
toId = this . clusteredNodes [ otherNodeId ] . clusterId ;
}
else {
edge . setOptions ( { physics : true , hidden : false } ) ;
//edge.options.hidden = false;
//edge.togglePhysics(true);
fromId = this . clusteredNodes [ otherNodeId ] . clusterId ;
}
// clone the options and apply the cluster options to them
let clonedOptions = this . _cloneOptions ( transferEdge , 'edge' ) ;
util . deepExtend ( clonedOptions , otherCluster . clusterEdgeProperties ) ;
// apply the edge specific options to it.
let id = 'clusterEdge:' + util . randomUUID ( ) ;
util . deepExtend ( clonedOptions , { from : fromId , to : toId , hidden : false , physics : true , id : id } ) ;
// create it
let newEdge = this . body . functions . createEdge ( clonedOptions ) ;
newEdge . clusteringEdgeReplacingId = transferEdge . id ;
this . body . edges [ id ] = newEdge ;
this . body . edges [ id ] . connect ( ) ;
}
}
else {
let replacedEdge = this . body . edges [ edge . clusteringEdgeReplacingId ] ;
if ( replacedEdge !== undefined ) {
replacedEdge . setOptions ( { physics : true , hidden : false } ) ;
replacedEdge . hiddenByCluster = false ;
}
}
edge . cleanup ( ) ;
// this removes the edge from node.edges, which is why edgeIds is formed
edge . disconnect ( ) ;
delete this . body . edges [ edge . id ] ;
}
// remove all temporary edges, make an array of ids so we don't remove from the list we're iterating over.
let removeIds = [ ] ;
for ( let i = 0 ; i < clusterNode . edges . length ; i ++ ) {
let edgeId = clusterNode . edges [ i ] . id ;
removeIds . push ( edgeId ) ;
}
// actually removing the edges
for ( let i = 0 ; i < removeIds . length ; i ++ ) {
let edgeId = removeIds [ i ] ;
this . body . edges [ edgeId ] . cleanup ( ) ;
// this removes the edge from node.edges, which is why edgeIds is formed
this . body . edges [ edgeId ] . disconnect ( ) ;
delete this . body . edges [ edgeId ] ;
// handle the releasing of the edges
for ( let edgeId in containedEdges ) {
if ( containedEdges . hasOwnProperty ( edgeId ) ) {
let edge = containedEdges [ edgeId ] ;
edge . setOptions ( { physics : true , hidden : false } ) ;
}
}
// remove clusterNode