Skip to content

Instantly share code, notes, and snippets.

@rdpoor
Forked from mbostock/.block
Last active June 22, 2023 22:15
Show Gist options
  • Save rdpoor/3a66b3e082ffeaeb5e6e79961192f7d8 to your computer and use it in GitHub Desktop.
Save rdpoor/3a66b3e082ffeaeb5e6e79961192f7d8 to your computer and use it in GitHub Desktop.
Modifying a Force Layout (with comments)
license: gpl-3.0

This is a nearly verbatim copy of Mike Bostock's Modifying a Force Layout example -- the main difference is it has ample documentation explaining what's going on. We've taken the liberty of renaming some variables and methods to make them more descriptive, and in one case (can you spot it?) made a small pessimization to the code in the name of clarity.

From the original version:

This example demonstrates how to add and remove nodes and links from a force layout. The graph initially appears with three nodes A, B and C connected in a loop. Three seconds later, node B is removed, along with the links B-A and B-C. At six seconds, node B is reintroduced, restoring the original links B-A and B-C. This example uses the general update pattern for data joins.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
.link {
stroke: #000;
stroke-width: 1.5px;
}
.node {
fill: #000; /* this is over-ridden by node.a (etc) below */
stroke: #fff;
stroke-width: 1.5px;
}
/* the class of the node determines its color */
.node.a { fill: #1f77b4; }
.node.b { fill: #ff7f0e; }
.node.c { fill: #2ca02c; }
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var width = 960, // width and height of the SVG canvas
height = 500;
// A list of node objects. We represent each node as {id: "name"}, but the D3
// system will decorate the node with addtional fields, notably x: and y: for
// the force layout and index" as part of the binding mechanism.
var nodes = [];
// A list of links. We represent a link as {source: <node>, target: <node>},
// and in fact, the force layout mechanism expects those names.
links = [];
// Create the force layout. After a call to force.start(), the tick method will
// be called repeatedly until the layout "gels" in a stable configuration.
var force = d3.layout.force()
.nodes(nodes)
.links(links)
.linkDistance(100)
.size([width, height])
.on("tick", tick);
// add an SVG element inside the DOM's BODY element
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
function update_graph() {
// First update the links...
// The call to <selection>.data() gets the link objects that need updating
// and associates them with the corresponding DOM elements, in this case,
// SVG line elements. The first argument, force.links() simply returns our
// links[] array. The second argument is a function that produces a unique
// and invariate identifier for each link. This makes it possible for D3 to
// correctly associate each link object with each line element in the DOM,
// even if the link object moves around in memory.
//
// link_update, in addition to processing objects that need updating,
// defines two custom methods, .enter() and .exit(), that refer to link
// objects that are newly added and that have been deleted (respectively).
// See below to see how these methods are used.
var link_update = svg.selectAll(".link").data(
force.links(),
function(d) { return d.source.id + "-" + d.target.id; }
);
// link_update.enter() creates an SVG line element for each new link
// object. Note that we call .insert("line", ".node") to add SVG line
// elements to the DOM before any elements with class="node". This
// guarantees that the lines will be drawn first. (Try changing that to
// .append("line") to see what happens...)
link_update.enter()
.insert("line", ".node")
.attr("class", "link");
// link_update.exit() processes link objects that have been removed
// by removing its corresponding SVG line element.
link_update.exit()
.remove();
// Now update the nodes. This is similar in structure to what we just
// did for the links:
//
// node_selection.data() returns a selection of nodes that need updating
// and defines two custom methods, .enter() and .exit(), that will process
// newly created node objects and deleted node objects.
//
// Note that, similar to what we did for links, we've provided an id method
// that returns a unique and invariate identifier for each node. This is
// what lets D3 associate each node object with its corresponding circle
// element in the DOM, even if the node object moves around in memory.
var node_update = svg.selectAll(".node").data(
force.nodes(),
function(d) { return d.id;}
);
// Create an SVG circle for each new node added to the graph. Note that we
// use a function to set the class of each node, because in this demo, the
// color is determined by the class of the node, e.g.
//
// <style>
// .node.a { fill: #1f77b4; }
// </style>
// ...
// <circle class="node a" r="8"</circle>
//
// ... but other techniques are possible
node_update.enter()
.append("circle")
.attr("class", function(d) { return "node " + d.id; })
.attr("r", 8);
// Remove the SVG circle whenever a node vanishes from the node list.
node_update.exit()
.remove();
// Start calling the tick() method repeatedly to lay out the graph.
force.start();
}
// This tick method is called repeatedly until the layout stabilizes.
//
// NOTE: the order in which we update nodes and links does NOT determine which
// gets drawn first -- the drawing order is determined by the ordering in the
// DOM. See the notes under link_update.enter() above for one technique for
// setting the ordering in the DOM.
function tick() {
// Drawing the nodes: Update the cx, cy attributes of each circle element
// from the x, y fields of the corresponding node object.
svg.selectAll(".node")
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
// Drawing the links: Update the start and end points of each line element
// from the x, y fields of the corresponding source and target node objects.
svg.selectAll(".link")
.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; });
}
// ================================================================
// Toplevel methods start here
// 1. Add three nodes and three links.
setTimeout(function() {
var a = {id: "a"}, b = {id: "b"}, c = {id: "c"};
nodes.push(a, b, c);
links.push({source: a, target: b},
{source: a, target: c},
{source: b, target: c});
update_graph();
}, 0);
// 2. Remove node B and associated links.
setTimeout(function() {
nodes.splice(1, 1); // remove b
links.shift(); // remove a-b
links.pop(); // remove b-c
update_graph();
}, 3000);
// 3. Add node B back.
setTimeout(function() {
var a = nodes[0], b = {id: "b"}, c = nodes[1];
nodes.push(b);
links.push({source: a, target: b}, {source: b, target: c});
update_graph();
}, 6000);
</script>
@LydiaF
Copy link

LydiaF commented May 9, 2018

Thank you so much for sharing this.

@jeandat
Copy link

jeandat commented Apr 10, 2019

When I take your example, it works really well with nice transitions when nodes are added/deleted.
Your example is in v3. If I take Mike current example which is in v4, transitions are gone. I don't see anything in both your code related to transitions.
Does someone know how to reproduce this example in v4 with nice transitions?

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