Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active November 22, 2018 22:09
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nitaku/7483341 to your computer and use it in GitHub Desktop.
Save nitaku/7483341 to your computer and use it in GitHub Desktop.
ID-based force layout

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.

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;
}
<!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="http://d3js.org/d3.v3.min.js"></script>
<script src="index.js"></script>
</head>
<body onload="main()">
</body>
</html>
(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);
.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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment