Fix Node Position in D3 Force Directed Layout

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?

Answers:

Answer

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.

Answer

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>

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.