Search code examples
javascriptvis.jsvis.js-network

vis.js - fit a set of nodes on screen


I have a network graph in vis.js with many nodes. When selecting a certain group, I would like to pan and zoom the graph so that all nodes of that group fit on screen.

I am traversing each node in the graph and calculating a bounding box for all the nodes I am interested in, then I use the moveTo method to move and scale the graph to the center of that bounding box. Pseudo-code:

var allNodes = data.nodes.get({
    returnType: "Object"
});
var bounds;
for (n in allNodes) {
    if (matchesCondition(allNodes[n])) {
        bounds = extendBounds(bounds, graph.getBoundingBox(allNodes[n]));                   
    }
}
var newViewport = {
    position: {
        x: (bounds.x1+bounds.x2)/2;
        y: (bounds.y1+bounds.y2)/2;
    },
    // What is the visible width, where do I get it from?
    scale: Math.min(??? / (bounds.x2-bounds.x1), ??? / (bounds.y2-bounds.y1))
}
graph.moveTo(newViewport);

The question: how do I calculate the scale, i.e. what do I replace the ??? with in the pseudo-code above?


Solution

  • Sample data from Vis.js groups example.

    For fitting the viewport, you can simply use the native .fit() method. Since the documentation doesn't provide hashtaggable links, here's the API description:

    Zooms out so all nodes fit on the canvas. You can supply options to customize this:

    {
      nodes:[Array of nodeIds],
      animation: { //can be a boolean too
        duration: Number
        easingFunction: String
      }
    }
    

    The nodes can be used to zoom to fit only specific nodes in the view.

    With this in mind, all we need to do is get all nodes in a given group. Surprisingly, the user-land API doesn't seem to offer a method for this (?), so a small filtering method is necessary.

    //TODO: Is there no user-land API for this?
    var getGroup = function getGroup(nodeId) {
      var nodesHandler = network.nodesHandler;
      var innerNodes = nodesHandler.body.nodes;
      //Lazily assume ids match indices
      var node = innerNodes[nodeId];
      return node.options.group;
    };
    
    var getGroupNodes = function getGroupNodes(group) {
      // http://elijahmanor.com/reducing-filter-and-map-down-to-reduce/
      var filtered = nodes.reduce(function(output, node) {
        if (node.group === group) {
          output.push(node.id);
        }
        return output;
      }, []);
      return filtered;
    };
    
    //START Vis.js group example
    
    var color = 'gray';
    var len = undefined;
    
    var nodes = [{
      id: 0,
      label: "0",
      group: 0
    }, {
      id: 1,
      label: "1",
      group: 0
    }, {
      id: 2,
      label: "2",
      group: 0
    }, {
      id: 3,
      label: "3",
      group: 1
    }, {
      id: 4,
      label: "4",
      group: 1
    }, {
      id: 5,
      label: "5",
      group: 1
    }, {
      id: 6,
      label: "6",
      group: 2
    }, {
      id: 7,
      label: "7",
      group: 2
    }, {
      id: 8,
      label: "8",
      group: 2
    }, {
      id: 9,
      label: "9",
      group: 3
    }, {
      id: 10,
      label: "10",
      group: 3
    }, {
      id: 11,
      label: "11",
      group: 3
    }, {
      id: 12,
      label: "12",
      group: 4
    }, {
      id: 13,
      label: "13",
      group: 4
    }, {
      id: 14,
      label: "14",
      group: 4
    }, {
      id: 15,
      label: "15",
      group: 5
    }, {
      id: 16,
      label: "16",
      group: 5
    }, {
      id: 17,
      label: "17",
      group: 5
    }, {
      id: 18,
      label: "18",
      group: 6
    }, {
      id: 19,
      label: "19",
      group: 6
    }, {
      id: 20,
      label: "20",
      group: 6
    }, {
      id: 21,
      label: "21",
      group: 7
    }, {
      id: 22,
      label: "22",
      group: 7
    }, {
      id: 23,
      label: "23",
      group: 7
    }, {
      id: 24,
      label: "24",
      group: 8
    }, {
      id: 25,
      label: "25",
      group: 8
    }, {
      id: 26,
      label: "26",
      group: 8
    }, {
      id: 27,
      label: "27",
      group: 9
    }, {
      id: 28,
      label: "28",
      group: 9
    }, {
      id: 29,
      label: "29",
      group: 9
    }];
    var edges = [{
      from: 1,
      to: 0
    }, {
      from: 2,
      to: 0
    }, {
      from: 4,
      to: 3
    }, {
      from: 5,
      to: 4
    }, {
      from: 4,
      to: 0
    }, {
      from: 7,
      to: 6
    }, {
      from: 8,
      to: 7
    }, {
      from: 7,
      to: 0
    }, {
      from: 10,
      to: 9
    }, {
      from: 11,
      to: 10
    }, {
      from: 10,
      to: 4
    }, {
      from: 13,
      to: 12
    }, {
      from: 14,
      to: 13
    }, {
      from: 13,
      to: 0
    }, {
      from: 16,
      to: 15
    }, {
      from: 17,
      to: 15
    }, {
      from: 15,
      to: 10
    }, {
      from: 19,
      to: 18
    }, {
      from: 20,
      to: 19
    }, {
      from: 19,
      to: 4
    }, {
      from: 22,
      to: 21
    }, {
      from: 23,
      to: 22
    }, {
      from: 22,
      to: 13
    }, {
      from: 25,
      to: 24
    }, {
      from: 26,
      to: 25
    }, {
      from: 25,
      to: 7
    }, {
      from: 28,
      to: 27
    }, {
      from: 29,
      to: 28
    }, {
      from: 28,
      to: 0
    }];
    
    // create a network
    var container = document.getElementById('mynetwork');
    var data = {
      nodes: nodes,
      edges: edges
    };
    var options = {
      nodes: {
        shape: 'dot',
        size: 30,
        font: {
          size: 32,
          color: '#ffffff'
        },
        borderWidth: 2
      },
      edges: {
        width: 2
      }
    };
    network = new vis.Network(container, data, options);
    
    //END Vis.js group example
    
    network.on("click", function(e) {
      //Zoom only on single node clicks, zoom out otherwise
      if (e.nodes.length !== 1) {
        network.fit();
        return;
      }
      var nodeId = e.nodes[0];
      //Find out what group the node belongs to
      var group = getGroup(nodeId);
      //TODO: How do you want to handle ungrouped nodes?
      if (group === undefined) return;
      var groupNodes = getGroupNodes(group);
      network.fit({
        nodes: groupNodes
      });
    });
    html,
    body,
    #mynetwork {
      width: 100%;
      height: 100%;
      margin: 0;
    }
    <script src="http://cdnjs.cloudflare.com/ajax/libs/vis/4.3.0/vis.min.js"></script>
    <div id="mynetwork"></div>