Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@sathomas
Last active February 11, 2024 20:55
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save sathomas/83515b77c2764837aac2 to your computer and use it in GitHub Desktop.
Save sathomas/83515b77c2764837aac2 to your computer and use it in GitHub Desktop.
Understanding D3.js Force Layout - 4: linkDistance

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.

Earlier examples looked at the operation of force layout from a (very) high level. This example begins examining the important properties of the force layout in more detail.

Perhaps the most important property is the linkDistance. This property specifies the distance that we would like between any two connected nodes. The force layout, through its iterations, will try to arrange the nodes so that all links are approximately this distance, but that won't always be possible. Even when it is technically able to make all links the desired distance, the resulting visualization may have other undesirable properties. Too many links may cross each other, for example. In such cases the layout will do it's best.

The visualization shows two separate network graphs that will be processed by force layout. The only difference between the two is that the graph on the left has a linkDistance twice that of the graph on the right. Use the buttons in the upper left to control the layout and see if it can achieve the desired distances.

The following examples continue an exploration of the force layout properties.

<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Force Layout Example 4</title>
<link href="http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css"
rel="stylesheet">
<style>
.node {
fill: #ccc;
stroke: #fff;
stroke-width: 2px;
}
.link {
stroke: #777;
stroke-width: 2px;
}
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 layout. 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.
var force = null,
nodes = null,
links = 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.")
// This example shows two separate network graphs in a single
// visualization (so it's easy to see the difference between them).
// Each graph has only two nodes to keep things as simple as
// possible. 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
// and so we can make sure that the two graphs don't get mixed up
// with each other.
// Note that our initial positions locate the notes uniformly
// throughout the visualization.
var dataNodes = [
{ x: width/3, y: height/3 },
{ x: width/3, y: 2*height/3 },
{ x: 2*width/3, y: height/3 },
{ x: 2*width/3, y: 2*height/3 }
];
// 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 two nodes into one graph and the next two nodes
// into the second graph.
// As you can see, we're also free to add other properties of our
// own to the link objects. In this case we're adding a `graph`
// property to indicate which graph "owns" the link.
var dataLinks = [
{ source: 0, target: 1, graph: 0 },
{ source: 2, target: 3, graph: 1 }
];
// Now we create a force layout object and define its properties.
// Those include the dimensions of the visualization and the arrays
// of nodes and links.
force = d3.layout.force()
.size([width, height])
.nodes(dataNodes)
.links(dataLinks);
// To keep the two distinct graphs from getting mixed up with
// each other, we'll disable the `gravity` property. We'll explore
// this property in a later example, but note that, in general,
// you probably don't want to do this. We can get away with it
// here because we're carefully controlling the graphs.
force.gravity(0);
// Here's the part where we make the two graphs differ. Because
// we're looking at the `linkDistance` property, that's what we
// want to vary between the graphs. Most often this property is
// set to a constant value for an entire visualization, but D3
// also lets us define it as a function. When we do that, we
// can set a different value for each link.
force.linkDistance(function(link) {
return link.graph === 0 ? height/2 : height/4;
});
// Next 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.
links = svg.selectAll('.link')
.data(dataLinks)
.enter().append('line')
.attr('class', 'link')
.attr('x1', function(d) { return dataNodes[d.source].x; })
.attr('y1', function(d) { return dataNodes[d.source].y; })
.attr('x2', function(d) { return dataNodes[d.target].x; })
.attr('y2', function(d) { return dataNodes[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.
nodes = svg.selectAll('.node')
.data(dataNodes)
.enter().append('circle')
.attr('class', 'node')
.attr('r', width/25)
.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.
force.on('tick', stepForce);
};
// The next function is the event handler that will execute
// at each iteration of the layout.
var stepForce = 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 (force.fullSpeed) {
nodes.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 {
nodes.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 (force.fullSpeed) {
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; });
} else {
links.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 (!force.fullSpeed) {
force.stop();
}
// If we're animating the layout in slow motion, continue
// after a delay to allow the animation to take effect.
if (force.slowMotion) {
setTimeout(
function() { force.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() {
force.start();
});
// When the user clicks on the "Slow Motion" button, we're
// going to run the force layout until it concludes.
d3.select('#slow').on('click', function() {
// Indicate that the animation is in progress.
force.slowMotion = true;
force.fullSpeed = false;
// Get the animation rolling
force.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.
force.slowMotion = false;
force.fullSpeed = true;
// Get the animation rolling
force.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 (force) {
force.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