1

For d3 force layouts that include drag functionality with d3-drag, it seems that the functions called on each drag event modify d.fx/d.fy, eg:

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

The drag start event often bases d.fx/d.fy on d.x/d.y while the end event sets d.fx/d.fy to null.

Where does d.fx/d.fy come from and why does it get used on elements that are being dragged? Is this built into d3 or d3-force in some way? Where is it assigned to the element being dragged?

Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
Joe
  • 55
  • 1
  • 5
  • 2
    Read the documentation, but I'm not sure `d.fx` is part of the drag output - just part of the examples people have written: https://github.com/d3/d3-drag#drag-events – Ryan Morton Jul 26 '18 at 17:21
  • As I understand drag(0 in other languages, there are parameters (dx, dy...) built into the drag functions BUT the re-assignment of positions is doneby the programmer who assigns them back to the x and y values which repositions the object. It is that step that seems to be internalized in the d3 code. I was curious as to whether it is built-in to to d3 or force() or forceSimulation() routines. – Joe Jul 26 '18 at 18:23

1 Answers1

6

d3 force layout and node.fx/fy

Within a d3 force simulation, a node's fx/fy properties can be used to set a fixed position for that node. If the fx/fy values are undefined or null, the nodes is free to move around. If they are set, the x/y properties of the node will always be set to match the fx/fy properties:

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. (docs)

These fx/fy properties are used to fix nodes in general, not just during drag events.

Application to drag events in a d3 force layout:

In a d3 force simulation the position of each node is updated on every tick. The tick fires repeatedly throughout the simulation to keep the nodes position updated, it does so fast enough to appear to animate the nodes movement.

While dragging you want to keep the node's position where the mouse is. During a drag, each time the mouse is moved, the drag event fires. It doesn't fire continuously unless the mouse moves.

When dragging we don't want to apply a force to the node being dragged: we want the node to follow the mouse (we generally also don't want to freeze the rest of the nodes by stopping the simulation during drags).

In order to remove the effects of the force layout on the dragged node, we can set the node.fx/fy properties so that the force doesn't pull the nodes away from the mouse position. When the drag is complete, we want to unset (using null) those values so the force will position the node again.

In the snippet below two force layouts are presented. Each will behave differently:

  • In the red layout nodes have there fx/fy properties set to the mouse position during the drag.
  • In the blue layout nodes simply have their x/y properties set to the mouse position during the drag.

In the red layout the force won't re-position a node during a drag. In the blue layout the force will continue to act upon a node during a drag. In the blue example both drag and force continuously place the node based on their individual rules, though normally tick events will generally place the node frequently enough that a drag may not be very visible. Try dragging the blue node a bit then don't move the mouse - it'll drift according to the force layout only:

In both examples the drag functions still update the force layout regarding the position of the dragged node

var data1 ={ "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 data2 ={ "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);
  
// FIRST SIMULATION
var simulation1 = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) { return d.id; }).distance(50))
    .force("charge", d3.forceManyBody())
    .force("center", d3.forceCenter(width / 3, height / 2));
    
var link1 = svg.append("g")
  .selectAll("line")
  .data(data1.links)
  .enter().append("line")
  .attr("stroke","black");

var node1 = svg.append("g")
 .selectAll("circle")
 .data(data1.nodes)
 .enter().append("circle")
 .attr("r", 10)
 .call(d3.drag()
   .on("drag", dragged1)
   .on("end", dragended1))
 .attr("fill","crimson");
 
simulation1.nodes(data1.nodes)
 .on("tick", ticked1)
 .alphaDecay(0)
 .force("link")
 .links(data1.links);
      
function ticked1() {
 link1
   .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; });
 node1
   .attr("cx", function(d) { return d.x; })
   .attr("cy", function(d) { return d.y; });
}    
    
function dragged1(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function dragended1(d) {
  d.fx = null;
  d.fy = null;
}

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

var node2 = svg.append("g")
 .selectAll("circle")
 .data(data2.nodes)
 .enter().append("circle")
 .attr("r", 10)
 .call(d3.drag()
   .on("drag", dragged2))
 .attr("fill","steelblue");
 
simulation2.nodes(data2.nodes)
 .on("tick", ticked2)
 .alphaDecay(0)
 .force("link")
 .links(data2.links);
      
function ticked2() {
 link2
   .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; });
 node2
   .attr("cx", function(d) { return d.x; })
   .attr("cy", function(d) { return d.y; });
}    
    
function dragged2(d) {
  d.x = d3.event.x;
  d.y = d3.event.y;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

The d in the drag functions being an individual node in the nodes data array (the node being dragged), from which the force layout bases its calculations and where it updates positions

Also, some drag started events may use d.fx = d.x, this will simply set the node's position to its current position (as I do above), you could also use the mouse's current position without any noticeable difference.

Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
  • THANKS! It may take awhile to digest but when done I'll be far ahead. – Joe Jul 27 '18 at 14:14
  • 1
    in the meantime I found part of the answer in Scott Murray's book where he shyly explains that d3 magically creates some attributes. That was what was bugging me. I didn't know these functions could do that – Joe Jul 27 '18 at 14:18
  • 1
    @Joe, The force layout manipulates the objects in the original data array so that position (`d.x`,`d.y`) and velocity (`d.vx`,`d.vy`) can be recorded - this is why `d.x` and `d.y` are used to update the circles each time a force ticks, `d.index` is also used. If one of these doesn't exist in the data, the layout creates it. But for `d.fy` and `d.fx`, you must manually create these properties if you want to freeze nodes as the layout won't do this automatically behind the scendes. – Andrew Reid Jul 27 '18 at 14:36
  • @AndrewReid Could you move your last comment to be part of your excellent answer? – Tarik Aug 24 '20 at 05:25