Fix Node Position in D3 Force Directed Layout

Javascriptd3.jsData VisualizationForce LayoutD3 Force-Directed

Javascript Problem Overview


I want some of the nodes in my force-directed layout to ignore all forces and stay in fixed positions based on an attribute of the node, while still being able to be dragged and exert repulsion on other nodes and maintain their link lines.

I thought it would be as simple as this:

force.on("tick", function() {
    vis.selectAll("g.node")
        .attr("transform", function(d) {
            return (d.someAttribute == true) ?
               "translate(" + d.xcoordFromAttribute + "," + d.ycoordFromAttribute +")" :
               "translate(" + d.x + "," + d.y + ")"
        });
  });

I have also tried to manually set the node's x and y attributes each tick, but then the links continue to float out to where the node would be if it was affected by the force.

Obviously I have a basic misunderstanding of how this is supposed to work. How can I fix nodes in a position, while keeping links and still allowing for them to be draggable?

Javascript Solutions


Solution 1 - Javascript

Set d.fixed on the desired nodes to true, and initialize d.x and d.y to the desired position. These nodes will then still be part of the simulation, and you can use the normal display code (e.g., setting a transform attribute); however, because they are marked as fixed, they can only be moved by dragging and not by the simulation.

See the force layout documentation for more details (v3 docs, current docs), and also see how the root node is positioned in this example.

Solution 2 - Javascript

Fixed nodes in force layout for d3v4 and d4v5

In d3v3 d.fixed will fix nodes at d.x and d.y; however, in d3v4/5 this method no longer is supported. The d3 documentation states:

> To fix a node in a given position, you may specify two additional > properties: > > fx - the node’s fixed x-position

> fy - the node’s fixed y-position > > At the end of each tick, after the application of any forces, a node > with a defined node.fx has node.x reset to this value and node.vx set > to zero; likewise, a node with a defined node.fy has node.y reset to > this value and node.vy set to zero. To unfix a node that was > previously fixed, set node.fx and node.fy to null, or delete these > properties.

You can set fx and fy attributes for the force nodes in your data source, or you can add and remove fx and fy values dynamically. The snippet below sets these properties at the end of drag events, just drag a node to fix its position:

var data ={ 
 "nodes": 
  [{"id": "A"},{"id": "B"},{"id": "C"},{"id":"D"}], 
 "links": 
  [{"source": "A", "target": "B"}, 
   {"source": "B", "target": "C"},
   {"source": "C", "target": "A"},
   {"source": "D", "target": "A"}]
}
var height = 250;
var width = 400;

var svg = d3.select("body").append("svg")
  .attr("width",width)
  .attr("height",height);
  
var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) { return d.id; }).distance(50))
    .force("charge", d3.forceManyBody())
    .force("center", d3.forceCenter(width / 2, height / 2));
    
var link = svg.append("g")
  .selectAll("line")
  .data(data.links)
  .enter().append("line")
  .attr("stroke","black");

var node = svg.append("g")
 .selectAll("circle")
 .data(data.nodes)
 .enter().append("circle")
 .attr("r", 5)
 .call(d3.drag()
   .on("drag", dragged)
   .on("end", dragended));
 
simulation
 .nodes(data.nodes)
 .on("tick", ticked)
 .alphaDecay(0);

simulation.force("link")
 .links(data.links);
      
function ticked() {
 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; });
 node
   .attr("cx", function(d) { return d.x; })
   .attr("cy", function(d) { return d.y; });
}    
    
function dragged(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function dragended(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.6.0/d3.min.js"></script>

d3v6 changes to event listners

In the above snippet, the drag events use the form

function dragged(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

Where d is the datum of the node being dragged. In d3v6, the form is now:

function dragged(event) {
  event.subject.fx = event.x;
  event.subject.fy = event.y;
}

or:

function dragged(event,d) {
  d.fx = event.x;
  d.fy = event.y;
}

The event is now passed directly to the listener, the second parameter passed to the event listener is the datum. Here's the canonical example on Observable.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionElijahView Question on Stackoverflow
Solution 1 - JavascriptmbostockView Answer on Stackoverflow
Solution 2 - JavascriptAndrew ReidView Answer on Stackoverflow