Skip to content

Instantly share code, notes, and snippets.

@sathomas
Last active July 23, 2017 15:19
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 sathomas/774d02a21dc1c714def8 to your computer and use it in GitHub Desktop.
Save sathomas/774d02a21dc1c714def8 to your computer and use it in GitHub Desktop.
Understanding D3.js Force Layout - 7: linkStrength

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.

An earlier example introduced linkDistance as a key parameter for force layouts. It sets the desired distance between any connected nodes. The force layout includes an additional parameter that serves to modify the linkDistance. That parameter is linkStrength. The default linkStrength value is 1.0, which maintains the full effect of linkDistance. By setting the value of linkStrength less than 1, though, the distance constraint can be relaxed.

In this example the red link has a linkStrength of 0.1, while all the other links retain the default value of 1.0. Use the controls in the upper left to run the layout and see the effect of this reduced link strength.

Note: the force that's drawing the nodes together in this case is gravity, which the next example considers in more detail. The links with a default strength are able to resist gravity more than the red link.

<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Force Layout Example 7</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;
}
.link.red {
stroke: #FF0000;
}
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.")
// 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.
var dataNodes = [
{ x: width/3, y: height/3 },
{ x: 2*width/3, y: height/3 },
{ x: width/2, 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 three nodes into one graph and leave the last
// two nodes isolated.
var dataLinks = [
{ source: 0, target: 1, className: 'red'},
{ source: 1, target: 2},
{ source: 2, target: 0}
];
// 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);
// Define the `linkDistance` for the graph. This is the
// distance we desire between connected nodes.
force.linkDistance(height/2);
// Here's the part where things get interesting. Because
// we're looking at the `linkStrength` property, that's what
// we want to vary between the read and blue nodes. 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 node.
force.linkStrength(function(link) {
if (link.className === 'red') return 0.1;
return 1;
});
// 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; });
// If we've defined a class name for a link, add it to the
// element. We'll use the D3 `each` function to iterate
// through the selection. The parameter passed to that
// function is the data objected associated with the
// selection which, by convention, is parameterized as `d`.
// In our case that will be the link object.
// Also in the `each` function, the context (`this`) is
// set to the associated node in the DOM.
links.each(function(d){
if (d.className) {
d3.select(this).classed(d.className, true)
}
});
// 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>
@Nilegfx
Copy link

Nilegfx commented Jul 23, 2017

I came across this one while learning more about d3 force layout, thanks for the detailed comments.

There is a slightly confusing part in lines 125 and 126

Our links bind the first three nodes into one graph and leave the last two nodes isolated.

I think the original tutorial was with 5 nodes and then it got reduced to 3 elements without updating the comment.

This worth mentioning as for the newbies they wonder where do the 5 nodes come from and they'll start thinking maybe the force will add them later .. etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment