Fix Node Position in D3 Force Directed Layout

55,030

Solution 1

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

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.

Share:
55,030
Elijah
Author by

Elijah

Senior Data Visualization Engineer at Netflix. Author of D3.js in Action.

Updated on July 05, 2022

Comments

  • Elijah
    Elijah almost 2 years

    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?

  • Aral Roca
    Aral Roca almost 6 years
    But how d3 know if some nodes are already fixed or not? If is null is not fixed, but then d3 overwrites fx and fy values...
  • Andrew Reid
    Andrew Reid almost 6 years
    I don't know if I understand the comment quite - if the fx or fy is undefined or null the node won't be fixed, since I don't define it for any of my nodes to start, none are fixed. D3 won't overwrite the fx/fy values unless you tell it to, like I do here. (Not really related, but I updated the snippet to run a bit smoother by setting fx/fy values in dragged rather than setting x/y (so the point stays with the mouse and is immune to other forces).