From f75a89366c3a4eef30a8568986217484fdbbe884 Mon Sep 17 00:00:00 2001 From: wimrijnders Date: Sat, 2 Sep 2017 12:32:31 +0200 Subject: [PATCH] Network: Cluster node handling due to dynamic data change. (#3399) * First fix for opening clusters, added unit tests * Added opening of child cluster to parent cluster * Added more tests for multi-level clusters * Commenting fixes * Added unit tests for option allowSingleNodeCluster, fixes due to that * Added test for allowSingleNodeCluster with nested clusters * Fixes due to linting * Removed TODO from code --- lib/network/modules/Clustering.js | 122 ++++-- .../modules/components/nodes/Cluster.js | 61 +++ test/Network.test.js | 413 +++++++++++++++++- 3 files changed, 556 insertions(+), 40 deletions(-) diff --git a/lib/network/modules/Clustering.js b/lib/network/modules/Clustering.js index 3168cc67..cce5ed1e 100644 --- a/lib/network/modules/Clustering.js +++ b/lib/network/modules/Clustering.js @@ -3,10 +3,8 @@ # TODO - `edgeReplacedById` not cleaned up yet on cluster edge removal -- check correct working for everything for clustered clusters (could use a unit test) -- Handle recursive unclustering on node removal -- `updateState()` not complete; scan TODO's there - +- allowSingleNodeCluster could be a global option as well; currently needs to always + be passed to clustering methods ---------------------------------------------- @@ -63,7 +61,7 @@ A cluster node has the following extra fields: - `isCluster : true` - indication that this is a cluster node - `containedNodes` - hash of nodes contained in this cluster - `containedEdges` - same for edges -- `edges` - hash of cluster edges for this node +- `edges` - array of cluster edges for this node **NOTE:** @@ -598,9 +596,9 @@ class ClusterEngine { // force the ID to remain the same clusterNodeProperties.id = clusterId; - // create the clusterNode + // create the cluster Node + // Note that allowSingleNodeCluster, if present, is stored in the options as well let clusterNode = this.body.functions.createNode(clusterNodeProperties, Cluster); - clusterNode.isCluster = true; clusterNode.containedNodes = childNodesObj; clusterNode.containedEdges = childEdgesObj; // cache a copy from the cluster edge properties if we have to reconnect others later on @@ -695,13 +693,42 @@ class ClusterEngine { */ openCluster(clusterNodeId, options, 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 + if (clusterNodeId === undefined) { + throw new Error("No clusterNodeId supplied to openCluster."); } + let clusterNode = this.body.nodes[clusterNodeId]; + + if (clusterNode === undefined) { + throw new Error("The clusterNodeId supplied to openCluster does not exist."); + } + if (clusterNode.isCluster !== true + || clusterNode.containedNodes === undefined + || clusterNode.containedEdges === undefined) { + throw new Error("The node:" + clusterNodeId + " is not a valid cluster."); + } + + // Check if current cluster is clustered itself + let stack = this.findNode(clusterNodeId); + let parentIndex = stack.indexOf(clusterNodeId) - 1; + if (parentIndex >= 0) { + // Current cluster is clustered; transfer contained nodes and edges to parent + let parentClusterNodeId = stack[parentIndex]; + let parentClusterNode = this.body.nodes[parentClusterNodeId]; + + // clustering.clusteredNodes and clustering.clusteredEdges remain unchanged + parentClusterNode._openChildCluster(clusterNodeId); + + // All components of child cluster node have been transferred. It can die now. + delete this.body.nodes[clusterNodeId]; + if (refreshData === true) { + this.body.emitter.emit('_dataChanged'); + } + + return; + } + + // main body let containedNodes = clusterNode.containedNodes; let containedEdges = clusterNode.containedEdges; @@ -729,15 +756,11 @@ class ClusterEngine { } else { // copy the position from the cluster - for (let nodeId in containedNodes) { - if (containedNodes.hasOwnProperty(nodeId)) { - let containedNode = this.body.nodes[nodeId]; - containedNode = containedNodes[nodeId]; - // inherit position - if (containedNode.options.fixed.x === false) {containedNode.x = clusterNode.x;} - if (containedNode.options.fixed.y === false) {containedNode.y = clusterNode.y;} - } - } + util.forEach(containedNodes, function(containedNode) { + // inherit position + if (containedNode.options.fixed.x === false) {containedNode.x = clusterNode.x;} + if (containedNode.options.fixed.y === false) {containedNode.y = clusterNode.y;} + }); } // release nodes @@ -1201,6 +1224,12 @@ class ClusterEngine { let deletedEdgeIds = []; let self = this; + + /** + * Utility function to iterate over clustering nodes only + * + * @param {Function} callback function to call for each cluster node + */ let eachClusterNode = (callback) => { for (nodeId in this.body.nodes) { let node = this.body.nodes[nodeId]; @@ -1266,7 +1295,25 @@ class ClusterEngine { for (edgeId in this.body.edges) { let edge = this.body.edges[edgeId]; - if (!edge.endPointsValid()) { + // Explicitly scan the contained edges for validity + let isValid = true; + let replacedIds = edge.clusteringEdgeReplacingIds; + if (replacedIds !== undefined) { + let numValid = 0; + + for (let n in replacedIds) { + let containedEdgeId = replacedIds[n]; + let containedEdge = this.body.edges[containedEdgeId]; + + if (containedEdge !== undefined && containedEdge.endPointsValid()) { + numValid += 1; + } + } + + isValid = (numValid > 0); + } + + if (!edge.endPointsValid() || !isValid) { deletedEdgeIds.push(edgeId); } } @@ -1302,7 +1349,7 @@ class ClusterEngine { // Remove cluster edges from active list (this.body.edges). // deletedEdgeIds still contains id of regular edges, but these should all - // be gone upon entering this method + // be gone when you reach here. for (n in deletedEdgeIds) { delete this.body.edges[deletedEdgeIds[n]]; } @@ -1343,8 +1390,33 @@ class ClusterEngine { } - // TODO: Cluster nodes may now be empty or because of selected options may not be allowed to contain 1 node - // Remove these cluster nodes if necessary. + // Clusters may be nested to any level. Keep on opening until nothing to open + var changed = false; + var continueLoop = true; + while (continueLoop) { + let clustersToOpen = []; + + // Determine the id's of clusters that need opening + eachClusterNode(function(clusterNode) { + let numNodes = Object.keys(clusterNode.containedNodes).length; + let allowSingle = (clusterNode.options.allowSingleNodeCluster === true); + if ((allowSingle && numNodes < 1) || (!allowSingle && numNodes < 2)) { + clustersToOpen.push(clusterNode.id); + } + }); + + // Open them + for (let n in clustersToOpen) { + this.openCluster(clustersToOpen[n], {}, false /* Don't refresh, we're in an refresh/update already */); + } + + continueLoop = (clustersToOpen.length > 0); + changed = changed || continueLoop; + } + + if (changed) { + this._updateState() // Redo this method (recursion possible! should be safe) + } } diff --git a/lib/network/modules/components/nodes/Cluster.js b/lib/network/modules/components/nodes/Cluster.js index 7d138107..7ea3a206 100644 --- a/lib/network/modules/components/nodes/Cluster.js +++ b/lib/network/modules/components/nodes/Cluster.js @@ -23,6 +23,67 @@ class Cluster extends Node { this.containedNodes = {}; this.containedEdges = {}; } + + + /** + * Transfer child cluster data to current and disconnect the child cluster. + * + * Please consult the header comment in 'Clustering.js' for the fields set here. + * + * @param {string|number} childClusterId id of child cluster to open + */ + _openChildCluster(childClusterId) { + let childCluster = this.body.nodes[childClusterId]; + if (this.containedNodes[childClusterId] === undefined) { + throw new Error('node with id: ' + childClusterId + ' not in current cluster'); + } + if (!childCluster.isCluster) { + throw new Error('node with id: ' + childClusterId + ' is not a cluster'); + } + + // Disconnect child cluster from current cluster + delete this.containedNodes[childClusterId]; + for(let n in childCluster.edges) { + let edgeId = childCluster.edges[n].id; + delete this.containedEdges[edgeId]; + } + + // Transfer nodes and edges + for (let nodeId in childCluster.containedNodes) { + this.containedNodes[nodeId] = childCluster.containedNodes[nodeId]; + } + childCluster.containedNodes = {}; + + for (let edgeId in childCluster.containedEdges) { + this.containedEdges[edgeId] = childCluster.containedEdges[edgeId]; + } + childCluster.containedEdges = {}; + + // Transfer edges within cluster edges which are clustered + for (let n in childCluster.edges) { + let clusterEdge = childCluster.edges[n]; + + for (let m in this.edges) { + let parentClusterEdge = this.edges[m]; + let index = parentClusterEdge.clusteringEdgeReplacingIds.indexOf(clusterEdge.id); + if (index === -1) continue; + + for (let n in clusterEdge.clusteringEdgeReplacingIds) { + let srcId = clusterEdge.clusteringEdgeReplacingIds[n]; + parentClusterEdge.clusteringEdgeReplacingIds.push(srcId); + + // Maintain correct bookkeeping for transferred edge + this.body.edges[srcId].edgeReplacedById = parentClusterEdge.id; + } + + // Remove cluster edge from parent cluster edge + parentClusterEdge.clusteringEdgeReplacingIds.splice(index, 1); + break; // Assumption: a clustered edge can only be present in a single clustering edge + } + } + childCluster.edges = []; + } } + export default Cluster; diff --git a/test/Network.test.js b/test/Network.test.js index d7ddb977..8a5359b0 100644 --- a/test/Network.test.js +++ b/test/Network.test.js @@ -1,3 +1,15 @@ +/** + * + * Useful during debugging + * ======================= + * + * console.log(JSON.stringify(output, null, 2)); + * + * for (let i in network.body.edges) { + * let edge = network.body.edges[i]; + * console.log("" + i + ": from: " + edge.fromId + ", to: " + edge.toId); + * } + */ var fs = require('fs'); var assert = require('assert'); var vis = require('../dist/vis'); @@ -7,9 +19,6 @@ var stdout = require('test-console').stdout; var Validator = require("./../lib/shared/Validator").default; //var {printStyle} = require('./../lib/shared/Validator'); -// Useful during debugging: -// console.log(JSON.stringify(output, null, 2)); - /** * Merge all options of object b into object b @@ -153,8 +162,8 @@ function log(network) { function assertNumNodes(network, expectedPresent, expectedVisible) { if (expectedVisible === undefined) expectedVisible = expectedPresent; - assert.equal(Object.keys(network.body.nodes).length, expectedPresent); - assert.equal(network.body.nodeIndices.length, expectedVisible); + assert.equal(Object.keys(network.body.nodes).length, expectedPresent, "Total number of nodes does not match"); + assert.equal(network.body.nodeIndices.length, expectedVisible, "Number of visible nodes does not match"); }; @@ -164,8 +173,8 @@ function assertNumNodes(network, expectedPresent, expectedVisible) { function assertNumEdges(network, expectedPresent, expectedVisible) { if (expectedVisible === undefined) expectedVisible = expectedPresent; - assert.equal(Object.keys(network.body.edges).length, expectedPresent); - assert.equal(network.body.edgeIndices.length, expectedVisible); + assert.equal(Object.keys(network.body.edges).length, expectedPresent, "Total number of edges does not match"); + assert.equal(network.body.edgeIndices.length, expectedVisible, "Number of visible edges does not match"); }; @@ -478,6 +487,99 @@ describe('Edge', function () { describe('Clustering', function () { + /** + * Helper function for clustering + */ + function clusterTo(network, clusterId, nodeList, allowSingle) { + var options = { + joinCondition: function(node) { + return nodeList.indexOf(node.id) !== -1; + }, + clusterNodeProperties: { + id: clusterId, + label: clusterId + } + } + + if (allowSingle === true) { + options.clusterNodeProperties.allowSingleNodeCluster = true + } + + network.cluster(options); + } + + + it('properly handles options allowSingleNodeCluster', function() { + var [network, data, numNodes, numEdges] = createSampleNetwork(); + data.edges.update({from: 1, to: 11,}); + numEdges += 1; + assertNumNodes(network, numNodes); + assertNumEdges(network, numEdges); + + clusterTo(network, 'c1', [3,4]); + numNodes += 1; // A clustering node is now hiding two nodes + numEdges += 1; // One clustering edges now hiding two edges + assertNumNodes(network, numNodes, numNodes - 2); + assertNumEdges(network, numEdges, numEdges - 2); + + // Cluster of single node should fail, because by default allowSingleNodeCluster == false + clusterTo(network, 'c2', [14]); + assertNumNodes(network, numNodes, numNodes - 2); // Nothing changed + assertNumEdges(network, numEdges, numEdges - 2); + assert(network.body.nodes['c2'] === undefined); // Cluster not created + + // Redo with allowSingleNodeCluster == true + clusterTo(network, 'c2', [14], true); + numNodes += 1; + numEdges += 1; + assertNumNodes(network, numNodes, numNodes - 3); + assertNumEdges(network, numEdges, numEdges - 3); + assert(network.body.nodes['c2'] !== undefined); // Cluster created + + + // allowSingleNodeCluster: true with two nodes + // removing one clustered node should retain cluster + clusterTo(network, 'c3', [11, 12], true); + numNodes += 1; // Added cluster + numEdges += 2; + assertNumNodes(network, numNodes, 6); + assertNumEdges(network, numEdges, 5); + + data.nodes.remove(12); + assert(network.body.nodes['c3'] !== undefined); // Cluster should still be present + numNodes -= 1; // removed node + numEdges -= 3; // cluster edge C3-13 should be removed + assertNumNodes(network, numNodes, 6); + assertNumEdges(network, numEdges, 4); + }); + + + it('removes nested clusters with allowSingleNodeCluster === true', function() { + var [network, data, numNodes, numEdges] = createSampleNetwork(); + // Create a chain of nested clusters, three deep + clusterTo(network, 'c1', [4], true); + clusterTo(network, 'c2', ['c1'], true); + clusterTo(network, 'c3', ['c2'], true); + numNodes += 3; + numEdges += 3; + assertNumNodes(network, numNodes, numNodes - 3); + assertNumEdges(network, numEdges, numEdges - 3); + assert(network.body.nodes['c1'] !== undefined); + assert(network.body.nodes['c2'] !== undefined); + assert(network.body.nodes['c3'] !== undefined); + + // The whole chain should be removed when the bottom-most node is deleted + data.nodes.remove(4); + numNodes -= 4; + numEdges -= 4; + assertNumNodes(network, numNodes); + assertNumEdges(network, numEdges); + assert(network.body.nodes['c1'] === undefined); + assert(network.body.nodes['c2'] === undefined); + assert(network.body.nodes['c3'] === undefined); + }); + + /** * Check on fix for #1218 */ @@ -486,8 +588,8 @@ describe('Clustering', function () { createCluster(network); numNodes += 1; // A clustering node is now hiding two nodes - assertNumNodes(network, numNodes, numNodes - 2); numEdges += 2; // Two clustering edges now hide two edges + assertNumNodes(network, numNodes, numNodes - 2); assertNumEdges(network, numEdges, numEdges - 2); //console.log("Creating node 21") @@ -500,8 +602,8 @@ describe('Clustering', function () { // '1' is part of the cluster so should // connect to cluster instead data.edges.update([{from: 21, to: 1}]); - assertNumNodes(network, numNodes, numNodes - 2); // nodes unchanged numEdges += 2; // A new clustering edge is hiding a new edge + assertNumNodes(network, numNodes, numNodes - 2); // nodes unchanged assertNumEdges(network, numEdges, numEdges - 3); }); @@ -515,8 +617,8 @@ describe('Clustering', function () { createCluster(network); numNodes += 1; // A clustering node is now hiding two nodes - assertNumNodes(network, numNodes, numNodes - 2); numEdges += 2; // Two clustering edges now hide two edges + assertNumNodes(network, numNodes, numNodes - 2); assertNumEdges(network, numEdges, numEdges - 2); // End block same as previous test @@ -531,15 +633,15 @@ describe('Clustering', function () { // 12 was connected to 11, which is clustered numNodes -= 1; // 12 removed, one less node - assertNumNodes(network, numNodes, numNodes - 2); numEdges -= 3; // clustering edge c1-12 and 2 edges of 12 gone + assertNumNodes(network, numNodes, numNodes - 2); assertNumEdges(network, numEdges, numEdges - 1); //console.log("Unclustering c1"); network.openCluster("c1"); numNodes -= 1; // cluster node removed, one less node - assertNumNodes(network, numNodes, numNodes); // all are visible again numEdges -= 1; // clustering edge gone, regular edge visible + assertNumNodes(network, numNodes, numNodes); // all are visible again assertNumEdges(network, numEdges, numEdges); // all are visible again }); @@ -561,26 +663,28 @@ describe('Clustering', function () { network.cluster(clusterOptionsByData); numNodes += 1; // new cluster node - assertNumNodes(network, numNodes, numNodes - 3); // 3 clustered nodes numEdges += 1; // 1 cluster edge expected + assertNumNodes(network, numNodes, numNodes - 3); // 3 clustered nodes assertNumEdges(network, numEdges, numEdges - 3); // 3 edges hidden //console.log("removing node 2, which is inside the cluster"); data.nodes.remove(2); numNodes -= 1; // clustered node removed - assertNumNodes(network, numNodes, numNodes - 2); // view doesn't change numEdges -= 2; // edges removed hidden in cluster + assertNumNodes(network, numNodes, numNodes - 2); // view doesn't change assertNumEdges(network, numEdges, numEdges - 1); // view doesn't change //console.log("Unclustering c1"); network.openCluster("c1") numNodes -= 1; // cluster node gone - assertNumNodes(network, numNodes, numNodes); // all visible numEdges -= 1; // cluster edge gone + assertNumNodes(network, numNodes, numNodes); // all visible assertNumEdges(network, numEdges, numEdges); // all visible //log(network); }); + + /** * Helper function for setting up a graph for testing clusterByEdgeCount() */ @@ -711,10 +815,289 @@ describe('Clustering', function () { assertJoinCondition(joinLevel_ , [[2,4,5]]); }); + + /////////////////////////////////////////////////////////////// + // Automatic opening of clusters due to dynamic data change + /////////////////////////////////////////////////////////////// + + /** + * Helper function, created nested clusters, three deep + */ + function createNetwork1() { + var [network, data, numNodes, numEdges] = createSampleNetwork(); + + clusterTo(network, 'c1', [3,4]); + numNodes += 1; // new cluster node + numEdges += 1; // 1 cluster edge expected + assertNumNodes(network, numNodes, numNodes - 2); // 2 clustered nodes + assertNumEdges(network, numEdges, numEdges - 2); // 2 edges hidden + + clusterTo(network, 'c2', [2,'c1']); + numNodes += 1; // new cluster node + numEdges += 1; // 2 cluster edges expected + assertNumNodes(network, numNodes, numNodes - 4); // 4 clustered nodes, including c1 + assertNumEdges(network, numEdges, numEdges - 4); // 4 edges hidden, including edge for c1 + + clusterTo(network, 'c3', [1,'c2']); + // Attempt at visualization: parentheses belong to the cluster one level above + // c3 + // ( -c2 ) + // ( -c1 ) + // 14-13-12-11 1 -2 (-3-4) + numNodes += 1; // new cluster node + numEdges += 0; // No new cluster edge expected + assertNumNodes(network, numNodes, numNodes - 6); // 6 clustered nodes, including c1 and c2 + assertNumEdges(network, numEdges, numEdges - 5); // 5 edges hidden, including edges for c1 and c2 + + return [network, data, numNodes, numEdges]; + } + + + it('opens clusters automatically when nodes deleted', function () { + var [network, data, numNodes, numEdges] = createSampleNetwork(); + + // Simple case: cluster of two nodes, delete one node + clusterTo(network, 'c1', [3,4]); + numNodes += 1; // new cluster node + numEdges += 1; // 1 cluster edge expected + assertNumNodes(network, numNodes, numNodes - 2); // 2 clustered nodes + assertNumEdges(network, numEdges, numEdges - 2); // 2 edges hidden + + data.nodes.remove(4); + numNodes -= 2; // deleting clustered node also removes cluster node + numEdges -= 2; // cluster edge should also be removed + assertNumNodes(network, numNodes, numNodes); + assertNumEdges(network, numEdges, numEdges); + + + // Extended case: nested nodes, three deep + [network, data, numNodes, numEdges] = createNetwork1(); + + data.nodes.remove(4); + // c3 + // ( -c2 ) + // 14-13-12-11 1 (-2 -3) + numNodes -= 2; // node removed, c1 also gone + numEdges -= 2; + assertNumNodes(network, numNodes, numNodes - 4); + assertNumEdges(network, numEdges, numEdges - 3); + + data.nodes.remove(1); + // c2 + // 14-13-12-11 (2 -3) + numNodes -= 2; // node removed, c3 also gone + numEdges -= 2; + assertNumNodes(network, numNodes, numNodes - 2); + assertNumEdges(network, numEdges, numEdges - 1); + + data.nodes.remove(2); + // 14-13-12-11 3 + numNodes -= 2; // node removed, c2 also gone + numEdges -= 1; + assertNumNodes(network, numNodes); // All visible again + assertNumEdges(network, numEdges); + + // Same as previous step, but remove all the given nodes in one go + // The result should be the same. + [network, data, numNodes, numEdges] = createNetwork1(); // nested nodes, three deep + data.nodes.remove([1,2,4]); + // 14-13-12-11 3 + assertNumNodes(network, 5); + assertNumEdges(network, 3); + }); + + + /////////////////////////////////////////////////////////////// + // Opening of clusters at various clustering depths + /////////////////////////////////////////////////////////////// + + /** + * Check correct opening of a single cluster. + * This is the 'simple' case. + */ + it('properly opens 1-level clusters', function () { + var [network, data, numNodes, numEdges] = createSampleNetwork(); + + // Pedantic: make a cluster of everything + clusterTo(network, 'c1', [1,2,3,4,11, 12, 13, 14]); + // c1(14-13-12-11 1-2-3-4) + numNodes += 1; + assertNumNodes(network, numNodes, 1); // Just the clustering node visible + assertNumEdges(network, numEdges, 0); // No extra edges! + + network.clustering.openCluster('c1', {}); + numNodes -= 1; + assertNumNodes(network, numNodes, numNodes); // Expecting same as original + assertNumEdges(network, numEdges, numEdges); + + // One external connection + [network, data, numNodes, numEdges] = createSampleNetwork(); + // 14-13-12-11 1-2-3-4 + clusterTo(network, 'c1', [3,4]); + network.clustering.openCluster('c1', {}); + assertNumNodes(network, numNodes, numNodes); // Expecting same as original + assertNumEdges(network, numEdges, numEdges); + + // Two external connections + clusterTo(network, 'c1', [2,3]); + network.clustering.openCluster('c1', {}); + assertNumNodes(network, numNodes, numNodes); // Expecting same as original + assertNumEdges(network, numEdges, numEdges); + + // One external connection to cluster + clusterTo(network, 'c1', [1,2]); + clusterTo(network, 'c2', [3,4]); + // 14-13-12-11 c1(1-2-)-c2(-3-4) + network.clustering.openCluster('c1', {}); + // 14-13-12-11 1-2-c2(-3-4) + numNodes += 1; + numEdges += 1; + assertNumNodes(network, numNodes, numNodes - 2); + assertNumEdges(network, numEdges, numEdges - 2); + + // two external connections to clusters + [network, data, numNodes, numEdges] = createSampleNetwork(); + data.edges.update({ + from: 1, + to: 11, + }); + numEdges += 1; + assertNumNodes(network, numNodes, numNodes); + assertNumEdges(network, numEdges, numEdges); + + clusterTo(network, 'c1', [1,2]); + // 14-13-12-11-c1(-1-2-)-3-4 + numNodes += 1; + numEdges += 2; + clusterTo(network, 'c2', [3,4]); + // 14-13-12-11-c1(-1-2-)-c2(-3-4) + // NOTE: clustering edges are hidden by clustering here! + numNodes += 1; + numEdges += 1; + clusterTo(network, 'c3', [11,12]); + // 14-13-c3(-12-11-)-c1(-1-2-)-c2(-3-4) + numNodes += 1; + numEdges += 2; + assertNumNodes(network, numNodes, numNodes - 6); + assertNumEdges(network, numEdges, numEdges - 8); // 6 regular edges hidden; also 2 clustering!!!!! + + network.clustering.openCluster('c1', {}); + numNodes -= 1; + numEdges -= 2; + // 14-13-c3(-12-11-)-1-2-c2(-3-4) + assertNumNodes(network, numNodes, numNodes - 4); + assertNumEdges(network, numEdges, numEdges - 5); + }); + + + /** + * Check correct opening of nested clusters. + * The test uses clustering three levels deep and opens the middle one. + */ + it('properly opens clustered clusters', function () { + var [network, data, numNodes, numEdges] = createSampleNetwork(); + data.edges.update({from: 1, to: 11,}); + numEdges += 1; + clusterTo(network, 'c1', [3,4]); + clusterTo(network, 'c2', [2,'c1']); + clusterTo(network, 'c3', [1,'c2']); + // Attempt at visualization: parentheses belong to the cluster one level above + // -c3 + // ( -c2 ) + // ( -c1 ) + // 14-13-12-11 -1 -2 (-3-4) + numNodes += 3; + numEdges += 3; + //console.log("numNodes: " + numNodes + "; numEdges: " + numEdges); + assertNumNodes(network, numNodes, numNodes - 6); + assertNumEdges(network, numEdges, numEdges - 6); + + // Open the middle cluster + network.clustering.openCluster('c2', {}); + // -c3 + // ( -c1 ) + // 14-13-12-11 -1 -2 (-3-4) + numNodes -= 1; + numEdges -= 1; + assertNumNodes(network, numNodes, numNodes - 5); + assertNumEdges(network, numEdges, numEdges - 5); + + // + // Same, with one external connection to cluster + // + var [network, data, numNodes, numEdges] = createSampleNetwork(); + data.edges.update({from: 1, to: 11,}); + data.edges.update({from: 2, to: 12,}); + numEdges += 2; + // 14-13-12-11-1-2-3-4 + // |------| + assertNumNodes(network, numNodes); + assertNumEdges(network, numEdges); + + + clusterTo(network, 'c0', [11,12]); + clusterTo(network, 'c1', [3,4]); + clusterTo(network, 'c2', [2,'c1']); + clusterTo(network, 'c3', [1,'c2']); + // +----------------+ + // | c3 | + // | +----------+ | + // | | c2 | | + // +-------+ | | +----+ | | + // | c0 | | | | c1 | | | + // 14-13-|-12-11-|-|-1-|-2-|-3-4| | | + // | | | | | | +----+ | | + // +-------+ | | | | | + // | | +----------+ | + // | | | | + // | +----------------+ + // |------------| + // (I) + numNodes += 4; + numEdges = 15; + assertNumNodes(network, numNodes, 4); + assertNumEdges(network, numEdges, 3); // (I) link 2-12 is combined into cluster edge for 11-1 + + // Open the middle cluster + network.clustering.openCluster('c2', {}); + // +--------------+ + // | c3 | + // | | + // +-------+ | +----+ | + // | c0 | | | c1 | | + // 14-13-|-12-11-|-|-1--2-|-3-4| | + // | | | | | +----+ | + // +-------+ | | | + // | | | | + // | +--------------+ + // |-----------| + // (I) + numNodes -= 1; + numEdges -= 2; + assertNumNodes(network, numNodes, 4); // visibility doesn't change, cluster opened within cluster + assertNumEdges(network, numEdges, 3); // (I) + + // Open the top cluster + network.clustering.openCluster('c3', {}); + // + // +-------+ +----+ + // | c0 | | c1 | + // 14-13-|-12-11-|-1-2-|-3-4| + // | | | | +----+ + // +-------+ | + // | | + // |--------| + // (II) + numNodes -= 1; + numEdges = 12; + assertNumNodes(network, numNodes, 6); // visibility doesn't change, cluster opened within cluster + assertNumEdges(network, numEdges, 6); // (II) link 2-12 visible again + }); }); // Clustering describe('on node.js', function () { + it('should be running', function () { assert(this.container !== null, 'Container div not found');