Skip to content

Instantly share code, notes, and snippets.

@jimkang
Forked from mbostock/.block
Last active August 29, 2015 14:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jimkang/620e066481967945c65f to your computer and use it in GitHub Desktop.
Save jimkang/620e066481967945c65f to your computer and use it in GitHub Desktop.
A bounded force-directed graph layout with "gravity" at the bottom of the bounds and collision detection/reaction among the nodes.

This block/gist mashes together a bounded force layout with collision detection to create a layout in which the gravity on each node appears to pull straight down. Basically, this is a ball drop simulation.

At large scale, this is probably not practical, as this gist creates a "magnet" node for each "ball" node. The magnet node is fixed and sits at the below the bottom of the bounds and provides personal gravity for the ball. At this scale, though, it's OK.

{
"nodes": [
{
"name": "Myriel",
"group": 1
},
{
"name": "Napoleon",
"group": 1
},
{
"name": "Mlle.Baptistine",
"group": 1
},
{
"name": "Mme.Magloire",
"group": 1
},
{
"name": "CountessdeLo",
"group": 1
},
{
"name": "Geborand",
"group": 1
},
{
"name": "Champtercier",
"group": 1
},
{
"name": "Cravatte",
"group": 1
},
{
"name": "Count",
"group": 1
},
{
"name": "OldMan",
"group": 1
},
{
"name": "Labarre",
"group": 2
},
{
"name": "Valjean",
"group": 2
},
{
"name": "Marguerite",
"group": 3
},
{
"name": "Mme.deR",
"group": 2
},
{
"name": "Isabeau",
"group": 2
},
{
"name": "Gervais",
"group": 2
},
{
"name": "Tholomyes",
"group": 3
},
{
"name": "Listolier",
"group": 3
},
{
"name": "Fameuil",
"group": 3
},
{
"name": "Blacheville",
"group": 3
},
{
"name": "Favourite",
"group": 3
},
{
"name": "Dahlia",
"group": 3
},
{
"name": "Zephine",
"group": 3
},
{
"name": "Fantine",
"group": 3
},
{
"name": "Mme.Thenardier",
"group": 4
},
{
"name": "Thenardier",
"group": 4
},
{
"name": "Cosette",
"group": 5
},
{
"name": "Javert",
"group": 4
},
{
"name": "Fauchelevent",
"group": 0
},
{
"name": "Bamatabois",
"group": 2
},
{
"name": "Perpetue",
"group": 3
},
{
"name": "Simplice",
"group": 2
},
{
"name": "Scaufflaire",
"group": 2
},
{
"name": "Woman1",
"group": 2
},
{
"name": "Judge",
"group": 2
},
{
"name": "Champmathieu",
"group": 2
},
{
"name": "Brevet",
"group": 2
},
{
"name": "Chenildieu",
"group": 2
},
{
"name": "Cochepaille",
"group": 2
},
{
"name": "Pontmercy",
"group": 4
},
{
"name": "Boulatruelle",
"group": 6
},
{
"name": "Eponine",
"group": 4
},
{
"name": "Anzelma",
"group": 4
},
{
"name": "Woman2",
"group": 5
},
{
"name": "MotherInnocent",
"group": 0
},
{
"name": "Gribier",
"group": 0
},
{
"name": "Jondrette",
"group": 7
},
{
"name": "Mme.Burgon",
"group": 7
},
{
"name": "Gavroche",
"group": 8
},
{
"name": "Gillenormand",
"group": 5
},
{
"name": "Magnon",
"group": 5
},
{
"name": "Mlle.Gillenormand",
"group": 5
},
{
"name": "Mme.Pontmercy",
"group": 5
},
{
"name": "Mlle.Vaubois",
"group": 5
},
{
"name": "Lt.Gillenormand",
"group": 5
},
{
"name": "Marius",
"group": 8
},
{
"name": "BaronessT",
"group": 5
},
{
"name": "Mabeuf",
"group": 8
},
{
"name": "Enjolras",
"group": 8
},
{
"name": "Combeferre",
"group": 8
},
{
"name": "Prouvaire",
"group": 8
},
{
"name": "Feuilly",
"group": 8
},
{
"name": "Courfeyrac",
"group": 8
},
{
"name": "Bahorel",
"group": 8
},
{
"name": "Bossuet",
"group": 8
},
{
"name": "Joly",
"group": 8
},
{
"name": "Grantaire",
"group": 8
},
{
"name": "MotherPlutarch",
"group": 9
},
{
"name": "Gueulemer",
"group": 4
},
{
"name": "Babet",
"group": 4
},
{
"name": "Claquesous",
"group": 4
},
{
"name": "Montparnasse",
"group": 4
},
{
"name": "Toussaint",
"group": 5
},
{
"name": "Child1",
"group": 10
},
{
"name": "Child2",
"group": 10
},
{
"name": "Brujon",
"group": 4
},
{
"name": "Mme.Hucheloup",
"group": 8
}
]
}
<!DOCTYPE html>
<html>
<head>
<title>Ball drop with force-directed layout and collision detection</title>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<style type="text/css">
circle {
stroke-width: 1.5px;
}
line {
stroke: #999;
}
.border-box {
fill: hsla(0, 100%, 100%, 0);
stroke: black;
}
</style>
</head>
<body>
<script type="text/javascript">
var w = 440,
h = 500,
r = 16,
fill = d3.scale.category20();
var force = d3.layout.force()
.friction(0.4)
.size([w, h]);
var svg = d3.select('body').append('svg:svg')
.attr('width', w)
.attr('height', h);
var borderbox = svg.append('rect').classed('border-box', true)
.attr({
width: w,
height: h
});
d3.json('readme_formatted.json', function(json) {
var nodes = json.nodes;
nodes.forEach(setRandomPositionAtTopOfBounds);
var magnetNodes = nodes.map(magnetNodeForNode);
var linkData = magnetNodes.map(function makeLink(magnetNode, i) {
return {
source: magnetNode,
target: nodes[i],
value: 1
};
});
nodes = nodes.concat(magnetNodes);
nodes.forEach(function setRadius(node) { node.radius = r; });
var node = svg.selectAll('circle')
.data(nodes)
.enter().append('svg:circle')
.attr('r', r - .75)
.style('fill', function(d) { return fill(d.group); })
.style('stroke', function(d) { return d3.rgb(fill(d.group)).darker(); })
force
.nodes(nodes)
.links(linkData)
.on('tick', tick)
.start();
function tick() {
var q = d3.geom.quadtree(nodes),
i = 0,
n = nodes.length;
while (++i < n) q.visit(collide(nodes[i]));
node.attr('cx', updateCx)
.attr('cy', updateCy);
}
function collide(node) {
var r = node.radius,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (!node.fixed && l < r) {
// The node and the node described by the quad are too close.
// Push them away from each other.
l = (l - r) / l * 0.5;
if (!isFinite(l)) {
l = 0;
}
x *= l;
y *= l;
node.x -= x;
node.y -= y;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
}
}
});
// The magnet nodes do not move.
// It stays under the bottom of the bounding box.
function magnetNodeForNode(node) {
return {
name: 'magnet',
group: 100,
fixed: true,
// isAMagnet: true,
x: node.x,
y: h + r + 20
};
}
function updateCx(d) {
if (!d.fixed) {
d.x = Math.max(r, Math.min(w - r, d.x));
}
return d.x;
}
function updateCy(d) {
if (!d.fixed) {
d.y = Math.max(r, Math.min(h - r, d.y));
}
return d.y;
}
function linkNodesToMagnetNode(nodes, magnetNode) {
return nodes.map(function linkToNode(node) {
return {
source: magnetNode,
target: node,
value: 1
};
});
}
function setRandomPositionAtTopOfBounds(node) {
node.x = ~~(Math.random() * w);
node.y = 0;
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment