Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active July 25, 2016 01:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save nitaku/8746032 to your computer and use it in GitHub Desktop.
Save nitaku/8746032 to your computer and use it in GitHub Desktop.
Basic force layout (ID-based, zoomable, fixed random seed)
### 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()
.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;
}
<!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>
/* 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);
.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