block by nitaku 8746032

Basic force layout (ID-based, zoomable, fixed random seed)

Full Screen

A basic force layout that displays a node-link diagram of an ID-based graph. The random seed is set to a fixed value using the seedrandom library, in order to always produce the same configuration on page reload.

index.js


/* define a fixed random seed, to avoid to have a different layout on each page reload. change the string to randomize
*/

(function() {
  var force, graph, graph_layer, height, l, n, svg, update, width, zoom, _i, _j, _len, _len2, _ref, _ref2;

  Math.seedrandom('abcde');

  width = 960;

  height = 500;

  /* create the SVG
  */

  svg = d3.select('body').append('svg').attr('width', width).attr('height', height);

  /* create some fake data
  */

  graph = {
    nodes: [
      {
        id: 'A'
      }, {
        id: 'B'
      }, {
        id: 'C'
      }, {
        id: 'D'
      }, {
        id: 'E'
      }, {
        id: 'F'
      }, {
        id: 'G'
      }, {
        id: 'H'
      }, {
        id: 'I'
      }, {
        id: 'J'
      }, {
        id: 'K'
      }, {
        id: 'L'
      }, {
        id: 'M'
      }
    ],
    links: [
      {
        id: 1,
        source: 'A',
        target: 'B'
      }, {
        id: 2,
        source: 'B',
        target: 'C'
      }, {
        id: 3,
        source: 'C',
        target: 'A'
      }, {
        id: 4,
        source: 'B',
        target: 'D'
      }, {
        id: 5,
        source: 'D',
        target: 'C'
      }, {
        id: 6,
        source: 'D',
        target: 'E'
      }, {
        id: 7,
        source: 'E',
        target: 'F'
      }, {
        id: 8,
        source: 'F',
        target: 'G'
      }, {
        id: 9,
        source: 'F',
        target: 'H'
      }, {
        id: 10,
        source: 'G',
        target: 'H'
      }, {
        id: 11,
        source: 'G',
        target: 'I'
      }, {
        id: 12,
        source: 'H',
        target: 'I'
      }, {
        id: 13,
        source: 'J',
        target: 'E'
      }, {
        id: 14,
        source: 'J',
        target: 'L'
      }, {
        id: 15,
        source: 'J',
        target: 'K'
      }, {
        id: 16,
        source: 'K',
        target: 'L'
      }, {
        id: 17,
        source: 'L',
        target: 'M'
      }, {
        id: 18,
        source: 'M',
        target: 'K'
      }
    ]
  };

  /* objectify the graph
  */

  /* 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;
      if (l.target === n.id) l.target = n;
    }
  }

  /* store the graph in a zoomable layer
  */

  graph_layer = svg.append('g');

  /* define a zoom behavior
  */

  zoom = d3.behavior.zoom().scaleExtent([1, 10]).on('zoom', function() {
    /* whenever the user zooms,
    */
    /* modify translation and scale of the zoom group accordingly
    */    return graph_layer.attr('transform', "translate(" + (zoom.translate()) + ")scale(" + (zoom.scale()) + ")");
  });

  /* bind the zoom behavior to the main SVG
  */

  svg.call(zoom);

  /* initialize the force layout
  */

  force = d3.layout.force().size([width, height]).charge(-400).linkDistance(60).on('tick', (function() {
    /* update nodes and links
    */    graph_layer.selectAll('.node').attr('transform', function(d) {
      return "translate(" + d.x + "," + d.y + ")";
    });
    return graph_layer.selectAll('.link').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;
    });
  }));

  update = function() {
    /* update the layout
    */
    var links, new_nodes, nodes;
    force.nodes(graph.nodes).links(graph.links).start();
    /* create nodes and links
    */
    /* (links are drawn first to make them appear under the nodes)
    */
    /* also, overwrite the selections with their databound version
    */
    links = graph_layer.selectAll('.link').data(graph.links, function(d) {
      return d.id;
    });
    links.enter().append('line').attr('class', 'link');
    links.exit().remove();
    /* dragged nodes become fixed
    */
    nodes = graph_layer.selectAll('.node').data(graph.nodes, function(d) {
      return d.id;
    });
    new_nodes = nodes.enter().append('g').attr('class', 'node');
    new_nodes.append('circle').attr('r', 18);
    /* draw the label
    */
    new_nodes.append('text').text(function(d) {
      return d.id;
    }).attr('dy', '0.35em');
    return nodes.exit().remove();
  };

  update();

}).call(this);

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>ID-based, zoomable, fixed random seed</title>
        <link type="text/css" href="index.css" rel="stylesheet"/>
        <script src="//d3js.org/d3.v3.min.js"></script>
        <script src="//davidbau.com/encode/seedrandom-min.js"></script>
    </head>
    <body>
    </body>
    <script src="index.js"></script>
</html>

index.coffee

### define a fixed random seed, to avoid to have a different layout on each page reload. change the string to randomize ###
Math.seedrandom('abcde')

width = 960
height = 500

### create the SVG ###
svg = d3.select('body').append('svg')
    .attr('width', width)
    .attr('height', height)
    
### create some fake data ###
graph = {
    nodes: [
        {id: 'A'},
        {id: 'B'},
        {id: 'C'},
        {id: 'D'},
        {id: 'E'},
        {id: 'F'},
        {id: 'G'},
        {id: 'H'},
        {id: 'I'},
        {id: 'J'},
        {id: 'K'},
        {id: 'L'},
        {id: 'M'}
    ],
    links: [
        {id:  1, source: 'A', target: 'B'},
        {id:  2, source: 'B', target: 'C'},
        {id:  3, source: 'C', target: 'A'},
        {id:  4, source: 'B', target: 'D'},
        {id:  5, source: 'D', target: 'C'},
        {id:  6, source: 'D', target: 'E'},
        {id:  7, source: 'E', target: 'F'},
        {id:  8, source: 'F', target: 'G'},
        {id:  9, source: 'F', target: 'H'},
        {id: 10, source: 'G', target: 'H'},
        {id: 11, source: 'G', target: 'I'},
        {id: 12, source: 'H', target: 'I'},
        {id: 13, source: 'J', target: 'E'},
        {id: 14, source: 'J', target: 'L'},
        {id: 15, source: 'J', target: 'K'},
        {id: 16, source: 'K', target: 'L'},
        {id: 17, source: 'L', target: 'M'},
        {id: 18, source: 'M', target: 'K'}
    ]}

### objectify the graph ###
### 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
        
        if l.target is n.id
            l.target = n
            
### store the graph in a zoomable layer ###
graph_layer = svg.append('g')

### define a zoom behavior ###
zoom = d3.behavior.zoom()
    .scaleExtent([1,10]) # min-max zoom
    .on 'zoom', () ->
      ### whenever the user zooms, ###
      ### modify translation and scale of the zoom group accordingly ###
      graph_layer.attr('transform', "translate(#{zoom.translate()})scale(#{zoom.scale()})")
      
### bind the zoom behavior to the main SVG ###
svg.call(zoom)

### initialize the force layout ###
force = d3.layout.force()
    .size([width, height])
    .charge(-400)
    .linkDistance(60)
    .on('tick', (() ->
        ### update nodes and links  ###
        graph_layer.selectAll('.node')
            .attr('transform', (d) -> "translate(#{d.x},#{d.y})")
            
        graph_layer.selectAll('.link')
            .attr('x1', (d) -> d.source.x)
            .attr('y1', (d) -> d.source.y)
            .attr('x2', (d) -> d.target.x)
            .attr('y2', (d) -> d.target.y)
    ))
    
update = () ->
    ### update the layout ###
    force
        .nodes(graph.nodes)
        .links(graph.links)
        .start()
        
    ### create nodes and links ###
    ### (links are drawn first to make them appear under the nodes) ###
    ### also, overwrite the selections with their databound version ###
    links = graph_layer.selectAll('.link')
        .data(graph.links, (d) -> d.id)
        
    links
      .enter().append('line')
        .attr('class', 'link')
        
    links
      .exit().remove()
      
    ### dragged nodes become fixed ###
    nodes = graph_layer.selectAll('.node')
        .data(graph.nodes, (d) -> d.id)
        
    new_nodes = nodes
      .enter().append('g')
        .attr('class', 'node')
        
    new_nodes.append('circle')
        .attr('r', 18)
        
    ### draw the label ###
    new_nodes.append('text')
        .text((d) -> d.id)
        .attr('dy', '0.35em')
        
    nodes
      .exit().remove()
      
update()

index.css

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

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

.link {
  stroke: #dddddd;
  stroke-width: 4px;
}

index.sass

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