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.
(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);
<!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>
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()
.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;
}
.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