Skip to content

Instantly share code, notes, and snippets.

@sathomas
Last active July 13, 2018 07:43
Show Gist options
  • Save sathomas/1ca23ee9588580d768aa to your computer and use it in GitHub Desktop.
Save sathomas/1ca23ee9588580d768aa to your computer and use it in GitHub Desktop.
Understanding D3.js Force Layout - 5: charge

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 previous example shows how linkDistance tells the force layout the desired distance between connected nodes. It may seem strange that D3 doesn't simply compel all links to be that distance. The force layout, however, takes other factors into account as well, which sometimes prevents it from achieving the exact link distance in all cases.

One of these additional factors is charge, so named because it's a property that acts like electrical charge on the nodes. With force-directed graphs in particular, charge causes nodes in the graph to repel each other. This behavior is generally desirable because it tends to prevent the nodes from overlapping each other in the visualization.

The two graphs in the visualization are the same except for their charge. The nodes in the left-most graph have a weaker charge than those on the right. In general, the effect of charge decays rapidly as nodes move farther apart. As the code for this example indicates, it takes a large difference in charge values (in this case, an order of magnitude) to make the two graphs distinct.

Step through the graph one iteration at a time, watch it in slow motion, or play it at full speed using the buttons in the upper left.

The next example looks at a more important use of charge.

<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Force Layout Example 5</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.
// As you can see, we're also free to add other properties of our
// own to the node objects. In this case we're adding a `graph`
// property to indicate which graph "owns" the node.
var dataNodes = [
{ x: width/3, y: height/3, graph: 0 },
{ x: width/3, y: 2*height/3, graph: 0 },
{ x: 2*width/3, y: height/3, graph: 1 },
{ x: 2*width/3, y: 2*height/3, graph: 1 }
];
// 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.
var dataLinks = [
{ source: 0, target: 1},
{ source: 2, target: 3}
];
// 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);
// Define the `linkDistance` for both graphs. This is the
// distance we desire between connected nodes.
force.linkDistance(height/6);
// To highlight the effect of `charge`, we reduce the rigidity
// of the links. More about this property in another example.
force.linkStrength(0.1);
// Here's the part where we make the two graphs differ. Because
// we're looking at the `charge` 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 node.
// Negative charge values indicate repulsion, which is generally
// desirable for force-directed graphs. (Positive values indicate
// attraction and can be helpful for other visualization types.)
force.charge(function(node) {
return node.graph === 0 ? -30 : -300;
});
// 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