Search code examples
javascriptd3.jsforce-layoutd3-force-directed

Apply d3force to a network with multiple groups


I am a beginner at d3 hence this question... I have a network with a bunch of nodes, links and groups which looks something like below:

I want to implement a d3-force on it so that each group has its own force and when one group is acted on, the other group doesnt get affected.

Notice how node 5, has group 0 since its a singleton. So all singletons will have group 0.

network = {
  "nodes" : [
    {
      "id":0,
      "x":0,
      "y":0,
      "group":1
    },
    {
      "id":1,
      "x":11,
      "y":-11,
      "group":1
    },
    {
      "id":2,
      "x":22,
      "y":-22,
      "group":1
    },
    {
      "id":3,
      "x":33,
      "y":-33,
      "group":2
    },
    {
      "id":4,
      "x":44,
      "y":-44,
      "group":2
    },
    {
      "id":5,
      "x":55,
      "y":-55,
      "group":0
    }],
  "links" : [
    {
      "from": 0,
      "to"  : 1
    },
    {
      "from": 1,
      "to"  : 2
    },
    {
      "from": 3,
      "to"  : 4
    }
  ]
}

Solution

  • Here is an example how to go about it, note I had to re-rewrite the grouping function as your's has a bug in it.

    var networkData = {
      "nodes": [{
        "id": "0",
        "x": 1509.9862,
        "y": -609.1013,
        "row_count": 2
      }, {
        "id": "1",
        "x": 1645.9578,
        "y": -85.06705,
        "row_count": 3
      }, {
        "id": "2",
        "x": 1948.1533,
        "y": -469.3646,
        "row_count": 2
      }, {
        "id": "3",
        "x": 1490.8839,
        "y": -705.9929,
        "row_count": 2
      }, {
        "id": "4",
        "x": 2370.9739,
        "y": -114.61766,
        "row_count": 2
      }, {
        "id": "5",
        "x": 1788.3419,
        "y": -460.89978,
        "row_count": 2
      }, {
        "id": "6",
        "x": 1601.6083,
        "y": -459.14755,
        "row_count": 2
      }, {
        "id": "7",
        "x": 1967.1221,
        "y": -179.95412,
        "row_count": 2
      }, {
        "id": "8",
        "x": 2305.796,
        "y": -398.09714,
        "row_count": 2
      }, {
        "id": "9",
        "x": 2075.197,
        "y": -119.19522,
        "row_count": 2
      }, {
        "id": "10",
        "x": 144.70703,
        "y": -492.124,
        "row_count": 2
      }, {
        "id": "11",
        "x": 1782.7756,
        "y": -95.29288,
        "row_count": 2
      }, {
        "id": "12",
        "x": 2490.2249,
        "y": -395.42737,
        "row_count": 4
      }, {
        "id": "13",
        "x": 1434.9111,
        "y": -507.22018,
        "row_count": 2
      }, {
        "id": "14",
        "x": 1918.9606,
        "y": -132.97313,
        "row_count": 2
      }, {
        "id": "15",
        "x": 150.78381,
        "y": -375.75558,
        "row_count": 4
      }, {
        "id": "16",
        "x": 117.40755,
        "y": -313.40042,
        "row_count": 2
      }, {
        "id": "17",
        "x": 128.44798,
        "y": -569.65533,
        "row_count": 2
      }, {
        "id": "18",
        "x": 2241.112,
        "y": -397.3375,
        "row_count": 5
      }, {
        "id": "19",
        "x": 93.45022,
        "y": -510.2351,
        "row_count": 2
      }, {
        "id": "20",
        "x": 2554.5647,
        "y": -395.42737,
        "row_count": 2
      }, {
        "id": "21",
        "x": 1552.0585,
        "y": -175.00697,
        "row_count": 2
      }, {
        "id": "22",
        "x": 1839.4581,
        "y": -137.50499,
        "row_count": 2
      }, {
        "id": "23",
        "x": 88.88344,
        "y": -374.78418,
        "row_count": 2
      }, {
        "id": "24",
        "x": 1588.2749,
        "y": -583.3556,
        "row_count": 2
      }, {
        "id": "25",
        "x": 115.34364,
        "y": -439.38092,
        "row_count": 5
      }, {
        "id": "26",
        "x": 1527.4338,
        "y": -425.7689,
        "row_count": 2
      }, {
        "id": "27",
        "x": 1466.7098,
        "y": -580.9009,
        "row_count": 1
      }, {
        "id": "28",
        "x": 1614.1482,
        "y": -508.55847,
        "row_count": 1
      }, {
        "id": "29",
        "x": 1653.5695,
        "y": -705.9929,
        "row_count": 2
      }, {
        "id": "30",
        "x": 1555.2239,
        "y": -705.9929,
        "row_count": 2
      }, {
        "id": "31",
        "x": 2226.4775,
        "y": -111.886284,
        "row_count": 2
      }, {
        "id": "32",
        "x": 1458.8619,
        "y": -462.99728,
        "row_count": 2
      }, {
        "id": "33",
        "x": 1541.3425,
        "y": -599.3965,
        "row_count": 1
      }, {
        "id": "34",
        "x": 1572.1593,
        "y": -445.52554,
        "row_count": 1
      }, {
        "id": "35",
        "x": 1804.5553,
        "y": -705.9929,
        "row_count": 2
      }, {
        "id": "36",
        "x": 2299.7493,
        "y": -113.641975,
        "row_count": 2
      }, {
        "id": "37",
        "x": 1447.5342,
        "y": -536.6993,
        "row_count": 1
      }, {
        "id": "38",
        "x": 1633.1509,
        "y": -212.4338,
        "row_count": 2
      }, {
        "id": "39",
        "x": 1782.9479,
        "y": -406.7332,
        "row_count": 2
      }, {
        "id": "40",
        "x": 1955.5413,
        "y": -705.9929,
        "row_count": 2
      }, {
        "id": "41",
        "x": 2151.4285,
        "y": -111.490974,
        "row_count": 2
      }, {
        "id": "42",
        "x": 1598.4803,
        "y": -553.35645,
        "row_count": 1
      }, {
        "id": "43",
        "x": 1500.1262,
        "y": -441.0764,
        "row_count": 2
      }, {
        "id": "44",
        "x": 1496.883,
        "y": -587.66406,
        "row_count": 1
      }, {
        "id": "45",
        "x": 1596.4176,
        "y": -483.6955,
        "row_count": 1
      }, {
        "id": "46",
        "x": 1455.5319,
        "y": -492.96527,
        "row_count": 2
      }, {
        "id": "47",
        "x": 1563.5104,
        "y": -579.53015,
        "row_count": 1
      }, {
        "id": "48",
        "x": 2037.2152,
        "y": -292.1214,
        "row_count": 2
      }, {
        "id": "49",
        "x": 282.7343,
        "y": -516.7465,
        "row_count": 2
      }, {
        "id": "50",
        "x": 167.14136,
        "y": -428.0224,
        "row_count": 2
      }, {
        "id": "51",
        "x": 2003.0924,
        "y": -437.64548,
        "row_count": 2
      }, {
        "id": "52",
        "x": 99.81963,
        "y": -175.3665,
        "row_count": 2
      }, {
        "id": "53",
        "x": 179.92572,
        "y": -468.74872,
        "row_count": 3
      }, {
        "id": "54",
        "x": 1717.0072,
        "y": -705.9929,
        "row_count": 2
      }, {
        "id": "55",
        "x": 2007.9512,
        "y": -233.56764,
        "row_count": 2
      }, {
        "id": "56",
        "x": 1542.7771,
        "y": -445.58582,
        "row_count": 1
      }, {
        "id": "57",
        "x": 1617.18,
        "y": -149.27104,
        "row_count": 5
      }, {
        "id": "58",
        "x": 1469.2253,
        "y": -555.9452,
        "row_count": 1
      }, {
        "id": "59",
        "x": 1595.1055,
        "y": -524.9397,
        "row_count": 1
      }, {
        "id": "60",
        "x": 1484.0056,
        "y": -464.2597,
        "row_count": 1
      }, {
        "id": "61",
        "x": 1523.9562,
        "y": -581.0812,
        "row_count": 1
      }, {
        "id": "62",
        "x": 2425.8848,
        "y": -395.42737,
        "row_count": 4
      }, {
        "id": "63",
        "x": 108.56919,
        "y": -243.38808,
        "row_count": 4
      }, {
        "id": "64",
        "x": 1867.9934,
        "y": -705.9929,
        "row_count": 2
      }, {
        "id": "65",
        "x": 1996.9839,
        "y": -123.64043,
        "row_count": 3
      }, {
        "id": "66",
        "x": 1948.1533,
        "y": -405.92636,
        "row_count": 2
      }, {
        "id": "67",
        "x": 1572.0945,
        "y": -470.88098,
        "row_count": 1
      }, {
        "id": "68",
        "x": 1465.1875,
        "y": -518.3188,
        "row_count": 1
      }, {
        "id": "69",
        "x": 1572.8579,
        "y": -554.43756,
        "row_count": 1
      }, {
        "id": "70",
        "x": 2106.5273,
        "y": -705.9929,
        "row_count": 2
      }, {
        "id": "71",
        "x": 1519.5193,
        "y": -458.15424,
        "row_count": 1
      }, {
        "id": "72",
        "x": 1494.6997,
        "y": -561.67957,
        "row_count": 2
      }, {
        "id": "73",
        "x": 1579.804,
        "y": -504.15784,
        "row_count": 1
      }, {
        "id": "74",
        "x": 1482.025,
        "y": -489.7116,
        "row_count": 1
      }, {
        "id": "75",
        "x": 2175.6614,
        "y": -396.56552,
        "row_count": 2
      }, {
        "id": "76",
        "x": 1541.8685,
        "y": -563.2375,
        "row_count": 1
      }, {
        "id": "77",
        "x": 1547.088,
        "y": -472.80585,
        "row_count": 1
      }, {
        "id": "78",
        "x": 1485.3854,
        "y": -532.9362,
        "row_count": 1
      }, {
        "id": "79",
        "x": 89.103745,
        "y": -110.00311,
        "row_count": 2
      }, {
        "id": "80",
        "x": 1555.3121,
        "y": -116.60735,
        "row_count": 2
      }, {
        "id": "81",
        "x": 1426.544,
        "y": -705.9929,
        "row_count": 2
      }, {
        "id": "82",
        "x": 1566.833,
        "y": -530.323,
        "row_count": 2
      }, {
        "id": "83",
        "x": 1688.4487,
        "y": -172.91998,
        "row_count": 2
      }, {
        "id": "84",
        "x": 68.698006,
        "y": -573.6171,
        "row_count": 2
      }, {
        "id": "85",
        "x": 1764.1693,
        "y": -158.18617,
        "row_count": 2
      }, {
        "id": "86",
        "x": 219.68867,
        "y": -397.56903,
        "row_count": 2
      }, {
        "id": "87",
        "x": 1730.5239,
        "y": -424.70016,
        "row_count": 2
      }, {
        "id": "88",
        "x": 1837.7125,
        "y": -415.37985,
        "row_count": 2
      }, {
        "id": "89",
        "x": 2439.1343,
        "y": -114.35799,
        "row_count": 2
      }, {
        "id": "90",
        "x": 1852.3489,
        "y": -475.04614,
        "row_count": 3
      }, {
        "id": "91",
        "x": 1715.4718,
        "y": -70.03096,
        "row_count": 2
      }, {
        "id": "92",
        "x": 2018.9792,
        "y": -705.9929,
        "row_count": 2
      }, {
        "id": "93",
        "x": 2169.9653,
        "y": -705.9929,
        "row_count": 2
      }, {
        "id": "94",
        "x": 1508.6908,
        "y": -480.54773,
        "row_count": 4
      }, {
        "id": "95",
        "x": 1699.9895,
        "y": -126.77199,
        "row_count": 2
      }, {
        "id": "96",
        "x": 2110.977,
        "y": -395.80646,
        "row_count": 3
      }, {
        "id": "97",
        "x": 2123.8252,
        "y": -164.62895,
        "row_count": 2
      }, {
        "id": "98",
        "x": 1517.4421,
        "y": -551.2502,
        "row_count": 1
      }, {
        "id": "99",
        "x": 266.0747,
        "y": -450.88815,
        "row_count": 2
      }, {
        "id": "100",
        "x": 1555.2823,
        "y": -497.76593,
        "row_count": 1
      }, {
        "id": "101",
        "x": 1497.4402,
        "y": -510.5418,
        "row_count": 1
      }, {
        "id": "102",
        "x": 1542.789,
        "y": -536.3863,
        "row_count": 1
      }, {
        "id": "103",
        "x": 1528.8363,
        "y": -494.63577,
        "row_count": 1
      }, {
        "id": "104",
        "x": 1515.9209,
        "y": -525.9001,
        "row_count": 1
      }, {
        "id": "105",
        "x": 1526.9814,
        "y": -515.76776,
        "row_count": 1
      }, {
        "id": "106",
        "x": 1737.522,
        "y": -129.01044,
        "row_count": 2
      }],
      "links": [{
        "from": 1,
        "to": 57
      }, {
        "from": 1,
        "to": 91
      }, {
        "from": 2,
        "to": 51
      }, {
        "from": 2,
        "to": 66
      }, {
        "from": 3,
        "to": 30
      }, {
        "from": 3,
        "to": 81
      }, {
        "from": 4,
        "to": 36
      }, {
        "from": 4,
        "to": 89
      }, {
        "from": 5,
        "to": 87
      }, {
        "from": 5,
        "to": 88
      }, {
        "from": 5,
        "to": 90
      }, {
        "from": 7,
        "to": 14
      }, {
        "from": 7,
        "to": 55
      }, {
        "from": 7,
        "to": 65
      }, {
        "from": 8,
        "to": 18
      }, {
        "from": 9,
        "to": 41
      }, {
        "from": 9,
        "to": 65
      }, {
        "from": 9,
        "to": 97
      }, {
        "from": 10,
        "to": 25
      }, {
        "from": 10,
        "to": 50
      }, {
        "from": 10,
        "to": 53
      }, {
        "from": 11,
        "to": 22
      }, {
        "from": 11,
        "to": 85
      }, {
        "from": 11,
        "to": 91
      }, {
        "from": 11,
        "to": 106
      }, {
        "from": 12,
        "to": 20
      }, {
        "from": 12,
        "to": 62
      }, {
        "from": 14,
        "to": 22
      }, {
        "from": 14,
        "to": 65
      }, {
        "from": 15,
        "to": 16
      }, {
        "from": 15,
        "to": 23
      }, {
        "from": 15,
        "to": 25
      }, {
        "from": 15,
        "to": 50
      }, {
        "from": 15,
        "to": 86
      }, {
        "from": 16,
        "to": 23
      }, {
        "from": 16,
        "to": 63
      }, {
        "from": 17,
        "to": 19
      }, {
        "from": 17,
        "to": 84
      }, {
        "from": 18,
        "to": 75
      }, {
        "from": 19,
        "to": 25
      }, {
        "from": 19,
        "to": 84
      }, {
        "from": 21,
        "to": 57
      }, {
        "from": 21,
        "to": 80
      }, {
        "from": 22,
        "to": 85
      }, {
        "from": 23,
        "to": 25
      }, {
        "from": 25,
        "to": 50
      }, {
        "from": 25,
        "to": 53
      }, {
        "from": 29,
        "to": 54
      }, {
        "from": 31,
        "to": 36
      }, {
        "from": 31,
        "to": 41
      }, {
        "from": 35,
        "to": 64
      }, {
        "from": 38,
        "to": 57
      }, {
        "from": 38,
        "to": 83
      }, {
        "from": 39,
        "to": 87
      }, {
        "from": 39,
        "to": 88
      }, {
        "from": 40,
        "to": 92
      }, {
        "from": 41,
        "to": 97
      }, {
        "from": 48,
        "to": 55
      }, {
        "from": 49,
        "to": 99
      }, {
        "from": 50,
        "to": 53
      }, {
        "from": 50,
        "to": 86
      }, {
        "from": 51,
        "to": 66
      }, {
        "from": 52,
        "to": 63
      }, {
        "from": 52,
        "to": 79
      }, {
        "from": 57,
        "to": 80
      }, {
        "from": 57,
        "to": 83
      }, {
        "from": 70,
        "to": 93
      }, {
        "from": 75,
        "to": 96
      }, {
        "from": 83,
        "to": 85
      }, {
        "from": 83,
        "to": 95
      }, {
        "from": 83,
        "to": 106
      }, {
        "from": 85,
        "to": 95
      }, {
        "from": 85,
        "to": 106
      }, {
        "from": 86,
        "to": 99
      }, {
        "from": 88,
        "to": 90
      }, {
        "from": 91,
        "to": 106
      }, {
        "from": 95,
        "to": 106
      }],
      "node_count": 107,
      "link_count": 77
    }
    
    
    /* creating groups */
    // union-find is a data structure that can union two sets and check
    // whether two element in the same set.
    
    var rootNode = {};
    
    function group2(nodes, links) {
      nodes.forEach((n, i) => n._gid = i + 1);
    
      const nodeMap = nodes.reduce((result, node) => {
        result[node.id] = node;
        return result
      }, {});
    
      const linkedNodes = (node, group) => {
        return links.filter(link => link.source == node.id || link.target == node.id)
          .map(link => nodeMap[link.source] === node ? nodeMap[link.target] : nodeMap[link.source])
          .filter(n => n.group != group)
      }
    
      const infect = (node, group) => {
        node.group = group;
        linkedNodes(node, group).map(n => infect(n, group))
      }
    
      let n = nodes.find(node => !node.group);
      while (n) {
        infect(n, n._gid);
        n = nodes.find(node => !node.group)
      }
    }
    /*
    function group(nodes, links) {
      // create n set with each set has the node as its only element
      links.forEach(function(link, i) {
        link.source = link.from;
        link.target = link.to;
      });
    
      nodes.forEach(function(node, i) {
        node.weight = 1;
        rootNode[node.id] = node.id;
      });
    
      // union each set that has a link between them
      links.forEach(function(link, i) {
        union(link.from, link.to);
      });
    
      // for each unioned set, group nodes together
      var id = 1;
      var groupIdCnt = {};
      var groupIds = {};
      var groups = {};
    
      nodes.forEach(function(node, i) {
        var f = find(node.id);
        if (typeof groupIds[f] === 'undefined') {
          groupIds[f] = id;
          groupIdCnt[id] = 1;
          id++;
        } else {
          groupIdCnt[groupIds[f]]++;
        }
    
        if (groupIdCnt[groupIds[f]] === 1) {
          node['group'] = 0;
          //console.log(node)
        } else {
          node['group'] = groupIds[f];
          //console.log(node)
        }
    
        if (typeof groups[node['group']] === 'undefined') {
          groups[node['group']] = [];
        }
        groups[node['group']].push(node);
      });
    
      return Object.values(groups);
    
    }
    
    // find rootNode of each set
    function find(node) {
      // if it is the root, return
      if (rootNode[node] === node) {
        return node;
      }
      // if not, find the rootNode and point to it
      rootNode[node] = find(rootNode[node]);
      return rootNode[node];
    }
    
    // update the rootNode of set which includes node1 to the rootNode of set which includes node 2
    function union(node1, node2) {
      rootNode[find(node1)] = find(node2);
    } */
    
    var links = networkData.links;
    var nodes = networkData.nodes;
    
    for (var i = 0; i < nodes.length; i++) {
      nodes[i].level = 1;
    }
    
    for (var i = 0; i < links.length; i++) {
      links[i].source = links[i].from;
      links[i].target = links[i].to;
    }
    
    var groups = group2(nodes, links)
    
    
    
    
    
    /** ====== D3 FORCE CODE =======*/
    function getNodeColor(node, neighbors) {
      return node.level === 1 ? 'red' : 'gray'
    }
    
    
    function getLinkColor(node, link) {
      return isNeighborLink(node, link) ? 'green' : '#000'
    }
    
    var width = 600
    var height = 600
    var color = d3.scaleOrdinal(d3.schemeCategory10);
    var svg = d3.select('svg')
    svg.attr('width', width).attr('height', height)
    
    // simulation setup with all forces
    var linkForce = d3
      .forceLink()
      .id(function(link) {
        return link.id
      })
      .strength(function(link) {
        return 1
      })
    
    var chargeForce = d3.forceManyBody()
      .strength(function(node) {
        if (links.find(link => link.source == node.id || link.target == node.id)) {
          return -100
        } else {
          return 0
        }
      }).distanceMax([100])
    
    var simulation = d3
      .forceSimulation()
      .force('link', linkForce)
      .force('charge', chargeForce)
      .force('center', d3.forceCenter(width / 2, height / 2))
    
    
    var dragDrop = d3.drag().on('start', function(node) {
      simulation.nodes().forEach(n => {
        if (n.group != node.group) {
          n.fx = n.x
          n.fy = n.y
        }
      })
    
    }).on('drag', function(node) {
      simulation.alphaTarget(0.7).restart()
      node.fx = d3.event.x
      node.fy = d3.event.y
    }).on('end', function(node) {
      simulation.nodes().forEach(node => {
        node.fx = null
        node.fy = null
      })
      simulation.alphaTarget(0).restart()
    })
    
    var linkElements = svg.append("g")
      .attr("class", "links")
      .selectAll("line")
      .data(links)
      .enter().append("line")
      .attr("stroke-width", 1)
      .attr("stroke", "rgba(50, 50, 50, 0.2)")
    
    var nodeElements = svg.append("g")
      .attr("class", "nodes")
      .selectAll("circle")
      .data(nodes)
      .enter().append("circle")
      .attr("r", 5)
      .attr("fill", d => color(d.group))
      .call(dragDrop)
    
    
    
    simulation.nodes(nodes).on('tick', () => {
      nodeElements
        .attr('cx', function(node) {
          return node.x
        })
        .attr('cy', function(node) {
          return node.y
        })
    
      linkElements
        .attr('x1', function(link) {
          return link.source.x
        })
        .attr('y1', function(link) {
          return link.source.y
        })
        .attr('x2', function(link) {
          return link.target.x
        })
        .attr('y2', function(link) {
          return link.target.y
        })
    })
    
    simulation.force("link").links(links)
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <svg width="1000" height="600"></svg>