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.
-
-
Save mayblue9/cc9ccc03dead9989fe5aa6a5dc78d38a to your computer and use it in GitHub Desktop.
Basic force layout (ID-based, zoomable, fixed random seed)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
### 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() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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="http://d3js.org/d3.v3.min.js"></script> | |
<script src="http://davidbau.com/encode/seedrandom-min.js"></script> | |
</head> | |
<body> | |
</body> | |
<script src="index.js"></script> | |
</html> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.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 | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment