Skip to content

Instantly share code, notes, and snippets.

@sathomas
Last active December 2, 2018 18:47
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save sathomas/191a8a302a363ac6a4b0 to your computer and use it in GitHub Desktop.
Save sathomas/191a8a302a363ac6a4b0 to your computer and use it in GitHub Desktop.
Understanding D3.js Force Layout - 8: gravity

This is part of a series of examples that describe the basic operation of the D3.js force layout. Eventually they may end up in a blog post that wraps everything together. If you missed the beginning of the series, here's a link to first example.

The code in the previous examples has mentioned a property that D3 calls gravity. The force layout uses this property to keep nodes from wandering off the edges of the visualization, something they might otherwise do to avoid overlap.

Unlike properties from the other examples, gravity applies to the entire force layout and not individual links or nodes. To illustrate its effect, therefore, this example creates two separate visualizations that happen to reside (unbeknownst to each other) in the same SVG container. To make this as clear as possible, the code is not at all optimized; instead, it consists of one force layout copied and pasted as a second one. The only difference between the two is the color (blue vs. red) and the fact that the red layout has disabled the gravity property by setting it to 0. (Another insignificant difference is that the initial positions are inverted. That's just to help see what's happening, and the positions are otherwise symmetrical.)

Use the controls in the upper left to execute the layout and see the different behaviors. Notice that the blue layout, with gravity enabled, tends to draw all the nodes to the center of the container. The red nodes feel no such compulsion and are free to drift off the visualization entirely.

Further examples will consider some of the other properties of the force layout.

<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Force Layout Example 8</title>
<link href="http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css"
rel="stylesheet">
<style>
.node1 {
fill: #0000cc;
stroke: #fff;
stroke-width: 2px;
}
.node2 {
fill: #cc0000;
stroke: #fff;
stroke-width: 2px;
}
.link1 {
stroke: #0000aa;
stroke-width: 1px;
}
.link2 {
stroke: #aa0000;
stroke-width: 1px;
}
button {
position: absolute;
width: 30px;
}
button#slow {
margin-top: 28px;
}
button#play {
margin-top: 54px;
}
button#reset {
margin-top: 80px;
}
</style>
</head>
<body>
<button id='advance' title='Advance Layout One Increment'>
<i class='fa fa-step-forward'></i>
</button>
<button id='slow' title='Run Layout in Slow Motion'>
<i class='fa fa-play' ></i>
</button>
<button id='play' title='Run Layout at Full Speed'>
<i class='fa fa-fast-forward'></i>
</button>
<button id='reset' title='Reset Layout to Beginning'>
<i class='fa fa-undo'></i>
</button>
<script src='http://d3js.org/d3.v3.min.js'></script>
<script>
// Define the dimensions of the visualization. We're using
// a size that's convenient for displaying the graphic on
// http://jsDataV.is
var width = 640,
height = 480;
// One other parameter for our visualization determines how
// fast (or slow) the animation executes. It's a time value
// measured in milliseconds.
var animationStep = 400;
// Next define the main object for the layouts. We'll also
// define a couple of objects to keep track of the D3 selections
// for the nodes and the links. All of these objects are
// initialized later on. This example is a bit unusual because
// we're running two separate and distinct layouts within
// the same SVG container.
var force1 = null, force2 = null,
nodes1 = null, nodes2 = null,
links1 = null, links2 = null;
// We can also create the SVG container that will hold the
// visualization. D3 makes it easy to set this container's
// dimensions and add it to the DOM.
var svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height);
// Now we'll define a few helper functions. You might not
// need to make these named function in a typical visualization,
// but they'll make it easy to control the visualization in
// this case.
// First up is a function to initialize our visualization.
var initForce = function() {
// Before we do anything else, we clear out the contents
// of the SVG container. This step makes it possible to
// restart the layout without refreshing the page.
svg.selectAll('*').remove();
// Define the data for the example. In general, a force layout
// requires two data arrays. The first array, here named `nodes`,
// contains the object that are the focal point of the visualization.
// The second array, called `links` below, identifies all the links
// between the nodes. (The more mathematical term is "edges.")
// As far as D3 is concerned, nodes are arbitrary objects.
// Normally the objects wouldn't be initialized with `x` and `y`
// properties like we're doing below. When those properties are
// present, they tell D3 where to place the nodes before the force
// layout starts its magic. More typically, they're left out of the
// nodes and D3 picks random locations for each node. We're defining
// them here so we can get a consistent application of the layout.
// Because we're running two layouts in parallel, we have two
// separate sets of nodes and links. The only difference between
// the two, however, is that one is an inverted version of the other
var dataNodes1 = [
{ x: 4*width/10, y: 6*height/9 },
{ x: 6*width/10, y: 6*height/9 },
{ x: width/2, y: 7*height/9 },
{ x: 4*width/10, y: 7*height/9 },
{ x: 6*width/10, y: 7*height/9 },
{ x: width/2, y: 5*height/9 }
];
var dataNodes2 = [
{ x: 4*width/10, y: 3*height/9 },
{ x: 6*width/10, y: 3*height/9 },
{ x: width/2, y: 2*height/9 },
{ x: 4*width/10, y: 2*height/9 },
{ x: 6*width/10, y: 2*height/9 },
{ x: width/2, y: 4*height/9 }
];
// The `links` array contains objects with a `source` and a `target`
// property. The values of those properties are the indices in
// the `nodes` array of the two endpoints of the link. Our links
// bind the first three nodes into one graph and leave the last
// two nodes isolated.
var dataLinks1 = [
{ source: 0, target: 1},
{ source: 1, target: 2},
{ source: 2, target: 0}
];
var dataLinks2 = [
{ source: 0, target: 1},
{ source: 1, target: 2},
{ source: 2, target: 0}
];
// Now we create a force layout objects and define their properties.
// Those include the dimensions of the visualization and the arrays
// of nodes and links.
force1 = d3.layout.force()
.size([width, height])
.nodes(dataNodes1)
.links(dataLinks1);
force2 = d3.layout.force()
.size([width, height])
.nodes(dataNodes2)
.links(dataLinks2);
// The only difference between the two layouts is that we remove
// the gravity from the second layout. The first retains its
// default value.
force2.gravity(0);
// Define the `linkDistance` for the graph. This is the
// distance we desire between connected nodes.
force1.linkDistance(height/2);
force2.linkDistance(height/2);
// Now we'll add the nodes and links to the visualization.
// Note that we're just sticking them into the SVG container
// at this point. We start with the links. The order here is
// important because we want the nodes to appear "on top of"
// the links. SVG doesn't really have a convenient equivalent
// to HTML's `z-index`; instead it relies on the order of the
// elements in the markup. By adding the nodes _after_ the
// links we ensure that nodes appear on top of links.
// Links are pretty simple. They're just SVG lines. We're going
// to position the lines according to the centers of their
// source and target nodes. You'll note that the `source`
// and `target` properties are indices into the `nodes`
// array. That's how our data is structured and that's how
// D3's force layout expects its inputs. As soon as the layout
// begins executing, however, it's going to replace those
// properties with references to the actual node objects
// instead of indices.
links1 = svg.selectAll('.link1')
.data(dataLinks1)
.enter().append('line')
.attr('class', 'link1')
.attr('x1', function(d) { return dataNodes1[d.source].x; })
.attr('y1', function(d) { return dataNodes1[d.source].y; })
.attr('x2', function(d) { return dataNodes1[d.target].x; })
.attr('y2', function(d) { return dataNodes1[d.target].y; });
// Now it's the nodes turn. Each node is drawn as a circle and
// given a radius and initial position within the SVG container.
// As is normal with SVG circles, the position is specified by
// the `cx` and `cy` attributes, which define the center of the
// circle. We actually don't have to position the nodes to start
// off, as the force layout is going to immediately move them.
// But this makes it a little easier to see what's going on
// before we start the layout executing.
nodes1 = svg.selectAll('.node1')
.data(dataNodes1)
.enter().append('circle')
.attr('class', 'node1')
.attr('r', width/40)
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
// Same code but for the second layout.
links2 = svg.selectAll('.link2')
.data(dataLinks2)
.enter().append('line')
.attr('class', 'link2')
.attr('x1', function(d) { return dataNodes2[d.source].x; })
.attr('y1', function(d) { return dataNodes2[d.source].y; })
.attr('x2', function(d) { return dataNodes2[d.target].x; })
.attr('y2', function(d) { return dataNodes2[d.target].y; });
nodes2 = svg.selectAll('.node2')
.data(dataNodes2)
.enter().append('circle')
.attr('class', 'node2')
.attr('r', width/40)
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
// Finally we tell D3 that we want it to call the step
// function at each iteration. Although it's less efficient,
// we'll let the same function handle both layouts.
force1.on('tick', stepForce1);
force2.on('tick', stepForce2);
};
// The next function is the event handler that will execute
// at each iteration of the layout.
var stepForce1 = function() {
// When this function executes, the force layout
// calculations have been updated. The layout will
// have set various properties in our nodes and
// links objects that we can use to position them
// within the SVG container.
// First let's reposition the nodes. As the force
// layout runs it updates the `x` and `y` properties
// that define where the node should be centered.
// To move the node, we set the appropriate SVG
// attributes to their new values.
// The code here differs depending on whether or
// not we're running the layout at full speed.
// In full speed we simply set the new positions.
if (force1.fullSpeed) {
nodes1.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
// Otherwise, we use a transition to move them to
// their positions instead of simply setting the
// values abruptly.
} else {
nodes1.transition().ease('linear').duration(animationStep)
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
}
// We also need to update positions of the links.
// For those elements, the force layout sets the
// `source` and `target` properties, specifying
// `x` and `y` values in each case.
// Here's where you can see how the force layout has
// changed the `source` and `target` properties of
// the links. Now that the layout has executed at least
// one iteration, the indices have been replaced by
// references to the node objects.
// As with the nodes, at full speed we don't use any
// transitions.
if (force1.fullSpeed) {
links1.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; });
} else {
links1.transition().ease('linear').duration(animationStep)
.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; });
}
// Unless the layout is operating at normal speed, we
// only want to show one step at a time.
if (!force1.fullSpeed) {
force1.stop();
}
// If we're animating the layout in slow motion, continue
// after a delay to allow the animation to take effect.
if (force1.slowMotion) {
setTimeout(
function() { force1.start(); },
animationStep
);
}
}
// Same function but for the second layout. This could
// obviously be simplified to reuse code for both layouts,
// but we'll keep them separate so that the code itself
// is a little easier to read.
var stepForce2 = function() {
// When this function executes, the force layout
// calculations have been updated. The layout will
// have set various properties in our nodes and
// links objects that we can use to position them
// within the SVG container.
// First let's reposition the nodes. As the force
// layout runs it updates the `x` and `y` properties
// that define where the node should be centered.
// To move the node, we set the appropriate SVG
// attributes to their new values.
// The code here differs depending on whether or
// not we're running the layout at full speed.
// In full speed we simply set the new positions.
if (force2.fullSpeed) {
nodes2.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
// Otherwise, we use a transition to move them to
// their positions instead of simply setting the
// values abruptly.
} else {
nodes2.transition().ease('linear').duration(animationStep)
.attr('cx', function(d) { return d.x; })
.attr('cy', function(d) { return d.y; });
}
// We also need to update positions of the links.
// For those elements, the force layout sets the
// `source` and `target` properties, specifying
// `x` and `y` values in each case.
// Here's where you can see how the force layout has
// changed the `source` and `target` properties of
// the links. Now that the layout has executed at least
// one iteration, the indices have been replaced by
// references to the node objects.
// As with the nodes, at full speed we don't use any
// transitions.
if (force2.fullSpeed) {
links2.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; });
} else {
links2.transition().ease('linear').duration(animationStep)
.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; });
}
// Unless the layout is operating at normal speed, we
// only want to show one step at a time.
if (!force2.fullSpeed) {
force2.stop();
}
// If we're animating the layout in slow motion, continue
// after a delay to allow the animation to take effect.
if (force2.slowMotion) {
setTimeout(
function() { force2.start(); },
animationStep
);
}
}
// Now let's take care of the user interaction controls.
// We'll add functions to respond to clicks on the individual
// buttons.
// When the user clicks on the "Advance" button, we
// start the force layout (The tick handler will stop
// the layout after one iteration.)
d3.select('#advance').on('click', function() {
force1.start();
force2.start();
});
// When the user clicks on the "Slow Motion" button, we're
// going to run the force layouts until they concludes.
d3.select('#slow').on('click', function() {
// Indicate that the animation is in progress.
force1.slowMotion = force2.slowMotion = true;
force1.fullSpeed = force2.fullSpeed = false;
// Get the animations rolling
force1.start();
force2.start();
});
// When the user clicks on the "Slow Motion" button, we're
// going to run the force layout until it concludes.
d3.select('#play').on('click', function() {
// Indicate that the full speed operation is in progress.
force1.slowMotion = force2.slowMotion = false;
force1.fullSpeed = force2.fullSpeed = true;
// Get the animations rolling
force1.start();
force2.start();
});
// When the user clicks on the "Reset" button, we'll
// start the whole process over again.
d3.select('#reset').on('click', function() {
// If we've already started the layout, stop it.
if (force1) {
force1.stop();
}
if (force2) {
force2.stop();
}
// Re-initialize to start over again.
initForce();
});
// Now we can initialize the force layout so that it's ready
// to run.
initForce();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment