block by nitaku 7483341

ID-based force layout

Full Screen

d3.js’s force layout works fine with index-based node references in links, since it substitutes them internally with the corresponding node from the nodes array. This simple approach is effective when we can refer to nodes by their index in the nodes array. But what if we have an ID for each node and we want links to refer to those IDs?

If we resolve the IDs into nodes before passing them to d3.js’s force layout, everything works as expected. This example illustrates the technique by repurposing another example by Mike Bostock.

Each node is given a string ID (a letter), and each link uses them to refer to the nodes it connects to. The code iterates through the links array to resolve the references.

Like in the original example, nodes are placed in precomputed positions, are made draggable, and are made fixed (i.e. not subject to the force) when dragged.

index.js

(function() {

  window.main = function() {
    var drag, force, graph, height, l, links, n, nodes, vis, width, _i, _j, _len, _len2, _ref, _ref2;
    width = 960;
    height = 500;
    /* create the SVG
    */
    vis = d3.select('body').append('svg').attr('width', width).attr('height', height);
    /* prepare nodes and links selections
    */
    nodes = vis.selectAll('.node');
    links = vis.selectAll('.link');
    /* initialize the force layout
    */
    force = d3.layout.force().size([width, height]).charge(-400).linkDistance(40).on('tick', (function() {
      /* update nodes and links
      */      nodes.attr('transform', function(d) {
        return "translate(" + d.x + "," + d.y + ")";
      });
      return links.attr('x1', function(d) {
        return d.source.x;
      }).attr('y1', function(d) {
        return d.source.y;
      }).attr('x2', function(d) {
        return d.target.x;
      }).attr('y2', function(d) {
        return d.target.y;
      });
    }));
    /* define a drag behavior to drag nodes
    */
    /* dragged nodes become fixed
    */
    drag = force.drag().on('dragstart', function(d) {
      return d.fixed = true;
    });
    /* create some fake data
    */
    graph = {
      'nodes': [
        {
          'id': 'A',
          'x': 469,
          'y': 410
        }, {
          'id': 'B',
          'x': 493,
          'y': 364
        }, {
          'id': 'C',
          'x': 442,
          'y': 365
        }, {
          'id': 'D',
          'x': 467,
          'y': 314
        }, {
          'id': 'E',
          'x': 477,
          'y': 248
        }, {
          'id': 'F',
          'x': 425,
          'y': 207
        }, {
          'id': 'G',
          'x': 402,
          'y': 155
        }, {
          'id': 'H',
          'x': 369,
          'y': 196
        }, {
          'id': 'I',
          'x': 350,
          'y': 148
        }, {
          'id': 'J',
          'x': 539,
          'y': 222
        }, {
          'id': 'K',
          'x': 594,
          'y': 235
        }, {
          'id': 'L',
          'x': 582,
          'y': 185
        }, {
          'id': 'M',
          'x': 633,
          'y': 200
        }
      ],
      'links': [
        {
          'source': 'A',
          'target': 'B'
        }, {
          'source': 'B',
          'target': 'C'
        }, {
          'source': 'C',
          'target': 'A'
        }, {
          'source': 'B',
          'target': 'D'
        }, {
          'source': 'D',
          'target': 'C'
        }, {
          'source': 'D',
          'target': 'E'
        }, {
          'source': 'E',
          'target': 'F'
        }, {
          'source': 'F',
          'target': 'G'
        }, {
          'source': 'F',
          'target': 'H'
        }, {
          'source': 'G',
          'target': 'H'
        }, {
          'source': 'G',
          'target': 'I'
        }, {
          'source': 'H',
          'target': 'I'
        }, {
          'source': 'J',
          'target': 'E'
        }, {
          'source': 'J',
          'target': 'L'
        }, {
          'source': 'J',
          'target': 'K'
        }, {
          'source': 'K',
          'target': 'L'
        }, {
          'source': 'L',
          'target': 'M'
        }, {
          'source': 'M',
          'target': 'K'
        }
      ]
    };
    /* resolve node IDs (not optimized at all!)
    */
    _ref = graph.links;
    for (_i = 0, _len = _ref.length; _i < _len; _i++) {
      l = _ref[_i];
      _ref2 = graph.nodes;
      for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
        n = _ref2[_j];
        if (l.source === n.id) {
          l.source = n;
          continue;
        }
        if (l.target === n.id) {
          l.target = n;
          continue;
        }
      }
    }
    /* create nodes and links
    */
    /* (links are drawn first to make them appear under the nodes)
    */
    /* also, overwrite the selections with their databound version
    */
    links = links.data(graph.links).enter().append('line').attr('class', 'link');
    nodes = nodes.data(graph.nodes).enter().append('g').attr('class', 'node').call(drag);
    nodes.append('circle').attr('r', 12);
    /* draw the label
    */
    nodes.append('text').text(function(d) {
      return d.id;
    }).attr('dy', '0.35em');
    /* run the force layout
    */
    return force.nodes(graph.nodes).links(graph.links).start();
  };

}).call(this);

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>ID-based force layout</title>
        <link type="text/css" href="index.css" rel="stylesheet"/>
        <script src="//d3js.org/d3.v3.min.js"></script>
        <script src="index.js"></script>
    </head>
    <body onload="main()">
    </body>
</html>

index.coffee

window.main = () ->
    width = 960
    height = 500
    
    ### create the SVG ###
    vis = d3.select('body').append('svg')
        .attr('width', width)
        .attr('height', height)
        
    ### prepare nodes and links selections ###
    nodes = vis.selectAll('.node')
    links = vis.selectAll('.link')
    
    ### initialize the force layout ###
    force = d3.layout.force()
        .size([width, height])
        .charge(-400)
        .linkDistance(40)
        .on('tick', (() ->
            ### update nodes and links  ###
            nodes
                .attr('transform', (d) -> "translate(#{d.x},#{d.y})")
                
            links
                .attr('x1', (d) -> d.source.x)
                .attr('y1', (d) -> d.source.y)
                .attr('x2', (d) -> d.target.x)
                .attr('y2', (d) -> d.target.y)
                
        ))
        
    ### define a drag behavior to drag nodes ###
    ### dragged nodes become fixed ###
    drag = force.drag()
        .on('dragstart', (d) -> d.fixed = true)
    
    ### create some fake data ###
    graph = {
        'nodes': [
            {'id': 'A', 'x': 469, 'y': 410},
            {'id': 'B', 'x': 493, 'y': 364},
            {'id': 'C', 'x': 442, 'y': 365},
            {'id': 'D', 'x': 467, 'y': 314},
            {'id': 'E', 'x': 477, 'y': 248},
            {'id': 'F', 'x': 425, 'y': 207},
            {'id': 'G', 'x': 402, 'y': 155},
            {'id': 'H', 'x': 369, 'y': 196},
            {'id': 'I', 'x': 350, 'y': 148},
            {'id': 'J', 'x': 539, 'y': 222},
            {'id': 'K', 'x': 594, 'y': 235},
            {'id': 'L', 'x': 582, 'y': 185},
            {'id': 'M', 'x': 633, 'y': 200}
        ],
        'links': [
            {'source': 'A', 'target': 'B'},
            {'source': 'B', 'target': 'C'},
            {'source': 'C', 'target': 'A'},
            {'source': 'B', 'target': 'D'},
            {'source': 'D', 'target': 'C'},
            {'source': 'D', 'target': 'E'},
            {'source': 'E', 'target': 'F'},
            {'source': 'F', 'target': 'G'},
            {'source': 'F', 'target': 'H'},
            {'source': 'G', 'target': 'H'},
            {'source': 'G', 'target': 'I'},
            {'source': 'H', 'target': 'I'},
            {'source': 'J', 'target': 'E'},
            {'source': 'J', 'target': 'L'},
            {'source': 'J', 'target': 'K'},
            {'source': 'K', 'target': 'L'},
            {'source': 'L', 'target': 'M'},
            {'source': 'M', 'target': 'K'}
        ]
    }
    
    ### resolve node IDs (not optimized at all!) ###
    for l in graph.links
        for n in graph.nodes
            if l.source is n.id
                l.source = n
                continue
                
            if l.target is n.id
                l.target = n
                continue
                
    ### create nodes and links ###
    ### (links are drawn first to make them appear under the nodes) ###
    ### also, overwrite the selections with their databound version ###
    links = links
        .data(graph.links)
      .enter().append('line')
        .attr('class', 'link')
        
    nodes = nodes
        .data(graph.nodes)
      .enter().append('g')
        .attr('class', 'node')
        .call(drag)
      
    nodes.append('circle')
        .attr('r', 12)
        
    ### draw the label ###
    nodes.append('text')
        .text((d) -> d.id)
        .attr('dy', '0.35em')
        
    ### run the force layout ###
    force
        .nodes(graph.nodes)
        .links(graph.links)
        .start()
        

index.css

.node > circle {
  stroke-width: 2px;
  stroke: gray;
  fill: #dddddd;
}

.node > text {
  pointer-events: none;
  font-family: sans-serif;
  text-anchor: middle;
}

.link {
  stroke-width: 2px;
  stroke: lightgrey;
}

index.sass

.node > circle
    stroke-width: 2px
    stroke: gray
    fill: #DDD
    
.node > text
    pointer-events: none
    font-family: sans-serif
    text-anchor: middle
    
.link
    stroke-width: 2px
    stroke: lightgray