var util = require("../../util"); import Cluster from './components/nodes/Cluster' class ClusterEngine { constructor(body) { this.body = body; this.clusteredNodes = {}; this.options = {}; this.defaultOptions = {}; util.extend(this.options, this.defaultOptions); } setOptions(options) { if (options !== undefined) { } } /** * * @param hubsize * @param options */ clusterByConnectionCount(hubsize, options) { if (hubsize === undefined) { hubsize = this._getHubSize(); } else if (tyepof(hubsize) === "object") { options = this._checkOptions(hubsize); hubsize = this._getHubSize(); } var nodesToCluster = []; for (var i = 0; i < this.body.nodeIndices.length; i++) { var node = this.body.nodes[this.body.nodeIndices[i]]; if (node.edges.length >= hubsize) { nodesToCluster.push(node.id); } } for (var i = 0; i < nodesToCluster.length; i++) { var node = this.body.nodes[nodesToCluster[i]]; this.clusterByConnection(node,options,{},{},false); } this.body.emitter.emit('_dataChanged'); } /** * loop over all nodes, check if they adhere to the condition and cluster if needed. * @param options * @param refreshData */ clusterByNodeData(options = {}, refreshData = true) { if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");} // check if the options object is fine, append if needed options = this._checkOptions(options); var childNodesObj = {}; var childEdgesObj = {} // collect the nodes that will be in the cluster for (var i = 0; i < this.body.nodeIndices.length; i++) { var nodeId = this.body.nodeIndices[i]; var clonedOptions = this._cloneOptions(nodeId); if (options.joinCondition(clonedOptions) === true) { childNodesObj[nodeId] = this.body.nodes[nodeId]; } } this._cluster(childNodesObj, childEdgesObj, options, refreshData); } /** * Cluster all nodes in the network that have only 1 edge * @param options * @param refreshData */ clusterOutliers(options, refreshData = true) { options = this._checkOptions(options); var clusters = []; // collect the nodes that will be in the cluster for (var i = 0; i < this.body.nodeIndices.length; i++) { var childNodesObj = {}; var childEdgesObj = {}; var nodeId = this.body.nodeIndices[i]; if (this.body.nodes[nodeId].edges.length === 1) { var edge = this.body.nodes[nodeId].edges[0]; var childNodeId = this._getConnectedId(edge, nodeId); if (childNodeId != nodeId) { if (options.joinCondition === undefined) { childNodesObj[nodeId] = this.body.nodes[nodeId]; childNodesObj[childNodeId] = this.body.nodes[childNodeId]; } else { var clonedOptions = this._cloneOptions(nodeId); if (options.joinCondition(clonedOptions) === true) { childNodesObj[nodeId] = this.body.nodes[nodeId]; } clonedOptions = this._cloneOptions(childNodeId); if (options.joinCondition(clonedOptions) === true) { childNodesObj[childNodeId] = this.body.nodes[childNodeId]; } } clusters.push({nodes:childNodesObj, edges:childEdgesObj}) } } } for (var i = 0; i < clusters.length; i++) { this._cluster(clusters[i].nodes, clusters[i].edges, options, false) } if (refreshData === true) { this.body.emitter.emit('_dataChanged'); } } /** * * @param nodeId * @param options * @param refreshData */ clusterByConnection(nodeId, options, refreshData = true) { // kill conditions if (nodeId === undefined) {throw new Error("No nodeId supplied to clusterByConnection!");} if (this.body.nodes[nodeId] === undefined) {throw new Error("The nodeId given to clusterByConnection does not exist!");} var node = this.body.nodes[nodeId]; options = this._checkOptions(options, node); if (options.clusterNodeProperties.x === undefined) {options.clusterNodeProperties.x = node.x;} if (options.clusterNodeProperties.y === undefined) {options.clusterNodeProperties.y = node.y;} if (options.clusterNodeProperties.fixed === undefined) { options.clusterNodeProperties.fixed = {}; options.clusterNodeProperties.fixed.x = node.options.fixed.x; options.clusterNodeProperties.fixed.y = node.options.fixed.y; } var childNodesObj = {}; var childEdgesObj = {} var parentNodeId = node.id; var parentClonedOptions = this._cloneOptions(parentNodeId); childNodesObj[parentNodeId] = node; // collect the nodes that will be in the cluster for (var i = 0; i < node.edges.length; i++) { var edge = node.edges[i]; var 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. var childClonedOptions = this._cloneOptions(childNodeId); if (options.joinCondition(parentClonedOptions, childClonedOptions) === true) { childEdgesObj[edge.id] = edge; childNodesObj[childNodeId] = this.body.nodes[childNodeId]; } } } else { childEdgesObj[edge.id] = edge; } } this._cluster(childNodesObj, childEdgesObj, options, refreshData); } /** * This returns a clone of the options or options of the edge or node to be used for construction of new edges or check functions for new nodes. * @param objId * @param type * @returns {{}} * @private */ _cloneOptions(objId, type) { var clonedOptions = {}; if (type === undefined || type === 'node') { util.deepExtend(clonedOptions, this.body.nodes[objId].options, true); util.deepExtend(clonedOptions, this.body.nodes[objId].properties, true); clonedOptions.amountOfConnections = this.body.nodes[objId].edges.length; } else { util.deepExtend(clonedOptions, this.body.edges[objId].properties, true); } return clonedOptions; } /** * This function creates the edges that will be attached to the cluster. * * @param childNodesObj * @param childEdgesObj * @param newEdges * @param options * @private */ _createClusterEdges (childNodesObj, childEdgesObj, newEdges, options) { var edge, childNodeId, childNode; var childKeys = Object.keys(childNodesObj); for (var i = 0; i < childKeys.length; i++) { childNodeId = childKeys[i]; childNode = childNodesObj[childNodeId]; // mark all edges for removal from global and construct new edges from the cluster to others for (var j = 0; j < childNode.edges.length; j++) { edge = childNode.edges[j]; childEdgesObj[edge.id] = edge; var otherNodeId = edge.toId; var otherOnTo = true; if (edge.toId != childNodeId) { otherNodeId = edge.toId; otherOnTo = true; } else if (edge.fromId != childNodeId) { otherNodeId = edge.fromId; otherOnTo = false; } if (childNodesObj[otherNodeId] === undefined) { var clonedOptions = this._cloneOptions(edge.id, 'edge'); util.deepExtend(clonedOptions, options.clusterEdgeProperties); if (otherOnTo === true) { clonedOptions.from = options.clusterNodeProperties.id; clonedOptions.to = otherNodeId; } else { clonedOptions.from = otherNodeId; clonedOptions.to = options.clusterNodeProperties.id; } clonedOptions.id = 'clusterEdge:' + util.randomUUID(); newEdges.push(this.body.functions.createEdge(clonedOptions)) } } } } /** * This function checks the options that can be supplied to the different cluster functions * for certain fields and inserts defaults if needed * @param options * @returns {*} * @private */ _checkOptions(options = {}) { if (options.clusterEdgeProperties === undefined) {options.clusterEdgeProperties = {};} if (options.clusterNodeProperties === undefined) {options.clusterNodeProperties = {};} return options; } /** * * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node * @param {Object} childEdgesObj | object with edge objects, id as keys * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties} * @param {Boolean} refreshData | when true, do not wrap up * @private */ _cluster(childNodesObj, childEdgesObj, options, refreshData = true) { // kill condition: no children so cant cluster if (Object.keys(childNodesObj).length === 0) {return;} // check if we have an unique id; if (options.clusterNodeProperties.id === undefined) {options.clusterNodeProperties.id = 'cluster:' + util.randomUUID();} var clusterId = options.clusterNodeProperties.id; // create the new edges that will connect to the cluster var newEdges = []; this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options); // construct the clusterNodeProperties var clusterNodeProperties = options.clusterNodeProperties; if (options.processProperties !== undefined) { // get the childNode options var childNodesOptions = []; for (var nodeId in childNodesObj) { var clonedOptions = this._cloneOptions(nodeId); childNodesOptions.push(clonedOptions); } // get clusterproperties based on childNodes var childEdgesOptions = []; for (var edgeId in childEdgesObj) { var clonedOptions = this._cloneOptions(edgeId, 'edge'); childEdgesOptions.push(clonedOptions); } clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions); if (!clusterNodeProperties) { throw new Error("The processClusterProperties function does not return properties!"); } } if (clusterNodeProperties.label === undefined) { clusterNodeProperties.label = 'cluster'; } // give the clusterNode a postion if it does not have one. var pos = undefined; if (clusterNodeProperties.x === undefined) { pos = this._getClusterPosition(childNodesObj); clusterNodeProperties.x = pos.x; clusterNodeProperties.allowedToMoveX = true; } if (clusterNodeProperties.x === undefined) { if (pos === undefined) { pos = this._getClusterPosition(childNodesObj); } clusterNodeProperties.y = pos.y; clusterNodeProperties.allowedToMoveY = true; } // force the ID to remain the same clusterNodeProperties.id = clusterId; // create the clusterNode var clusterNode = this.body.functions.createNode(clusterNodeProperties, Cluster); clusterNode.isCluster = true; clusterNode.containedNodes = childNodesObj; clusterNode.containedEdges = childEdgesObj; // disable the childEdges for (var edgeId in childEdgesObj) { if (childEdgesObj.hasOwnProperty(edgeId)) { if (this.body.edges[edgeId] !== undefined) { let edge = this.body.edges[edgeId]; edge.togglePhysics(false); edge.options.hidden = true; } } } // disable the childNodes for (var nodeId in childNodesObj) { if (childNodesObj.hasOwnProperty(nodeId)) { this.clusteredNodes[nodeId] = {clusterId:clusterNodeProperties.id, node: this.body.nodes[nodeId]}; this.body.nodes[nodeId].togglePhysics(false); this.body.nodes[nodeId].options.hidden = true; } } // finally put the cluster node into global this.body.nodes[clusterNodeProperties.id] = clusterNode; // push new edges to global for (var 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; // wrap up if (refreshData === true) { this.body.emitter.emit('_dataChanged'); } } /** * Check if a node is a cluster. * @param nodeId * @returns {*} */ isCluster(nodeId) { if (this.body.nodes[nodeId] !== undefined) { return this.body.nodes[nodeId].isCluster === true; } else { console.log("Node does not exist.") return false; } } /** * get the position of the cluster node based on what's inside * @param {object} childNodesObj | object with node objects, id as keys * @returns {{x: number, y: number}} * @private */ _getClusterPosition(childNodesObj) { var childKeys = Object.keys(childNodesObj); var minX = childNodesObj[childKeys[0]].x; var maxX = childNodesObj[childKeys[0]].x; var minY = childNodesObj[childKeys[0]].y; var maxY = childNodesObj[childKeys[0]].y; var node; for (var i = 0; i < childKeys.lenght; i++) { node = childNodesObj[childKeys[0]]; minX = node.x < minX ? node.x : minX; maxX = node.x > maxX ? node.x : maxX; minY = node.y < minY ? node.y : minY; maxY = node.y > maxY ? node.y : maxY; } return {x: 0.5*(minX + maxX), y: 0.5*(minY + maxY)}; } /** * Open a cluster by calling this function. * @param {String} clusterNodeId | the ID of the cluster node * @param {Boolean} refreshData | wrap up afterwards if not true */ openCluster(clusterNodeId, refreshData = true) { // kill conditions if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");} if (this.body.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");} if (this.body.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return}; var clusterNode = this.body.nodes[clusterNodeId]; var containedNodes = clusterNode.containedNodes; var containedEdges = clusterNode.containedEdges; // release nodes for (var nodeId in containedNodes) { if (containedNodes.hasOwnProperty(nodeId)) { let containedNode = this.body.nodes[nodeId]; containedNode = containedNodes[nodeId]; // inherit position containedNode.x = clusterNode.x; containedNode.y = clusterNode.y; // inherit speed containedNode.vx = clusterNode.vx; containedNode.vy = clusterNode.vy; containedNode.options.hidden = false; containedNode.togglePhysics(true); delete this.clusteredNodes[nodeId]; } } // release edges for (var edgeId in containedEdges) { if (containedEdges.hasOwnProperty(edgeId)) { var edge = this.body.edges[edgeId]; edge.options.hidden = false; edge.togglePhysics(true); } } // remove all temporary edges for (var i = 0; i < clusterNode.edges.length; i++) { var edgeId = clusterNode.edges[i].id; var viaId = this.body.edges[edgeId].via.id; if (viaId) { this.body.edges[edgeId].via = undefined; delete this.body.nodes[viaId]; } // this removes the edge from node.edges, which is why edgeIds is formed this.body.edges[edgeId].disconnect(); delete this.body.edges[edgeId]; } // remove clusterNode delete this.body.nodes[clusterNodeId]; if (refreshData === true) { this.body.emitter.emit('_dataChanged'); } } /** * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to * is currently residing in cluster B * @param edge * @param nodeId * @param from * @private */ _connectEdge(edge, nodeId, from) { var clusterStack = this._getClusterStack(nodeId); if (from === true) { edge.from = clusterStack[clusterStack.length - 1]; edge.fromId = clusterStack[clusterStack.length - 1].id; clusterStack.pop() edge.fromArray = clusterStack; } else { edge.to = clusterStack[clusterStack.length - 1]; edge.toId = clusterStack[clusterStack.length - 1].id; clusterStack.pop(); edge.toArray = clusterStack; } edge.connect(); } /** * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node * @param nodeId * @returns {Array} * @private */ _getClusterStack(nodeId) { var stack = []; var max = 100; var counter = 0; while (this.clusteredNodes[nodeId] !== undefined && counter < max) { stack.push(this.clusteredNodes[nodeId].node); nodeId = this.clusteredNodes[nodeId].clusterId; counter++; } stack.push(this.body.nodes[nodeId]); return stack; } /** * Get the Id the node is connected to * @param edge * @param nodeId * @returns {*} * @private */ _getConnectedId(edge, nodeId) { if (edge.toId != nodeId) { return edge.toId; } else if (edge.fromId != nodeId) { return edge.fromId; } else { return edge.fromId; } } /** * We determine how many connections denote an important hub. * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%) * * @private */ _getHubSize() { var average = 0; var averageSquared = 0; var hubCounter = 0; var largestHub = 0; for (var i = 0; i < this.body.nodeIndices.length; i++) { var node = this.body.nodes[this.body.nodeIndices[i]]; if (node.edges.length > largestHub) { largestHub = node.edges.length; } average += node.edges.length; averageSquared += Math.pow(node.edges.length,2); hubCounter += 1; } average = average / hubCounter; averageSquared = averageSquared / hubCounter; var variance = averageSquared - Math.pow(average,2); var standardDeviation = Math.sqrt(variance); var hubThreshold = Math.floor(average + 2*standardDeviation); // always have at least one to cluster if (hubThreshold > largestHub) { hubThreshold = largestHub; } return hubThreshold; }; } export default ClusterEngine;