What's preserving local offset in this d3.drag example?
Asked Answered
E

1

5

When dragging any circle in this d3 example, what prevents the circle's center from snapping to the mouse?

In other words: when you initiate a circle drag by clicking somewhere near the outer edges of the circle, what in the code preserves the offset (relative to the circle's center) that's implied at drag start?

I see these .attr() calls:

.attr("cx", d.x = d3.event.x)
.attr("cy", d.y = d3.event.y)

But I expect d3.event.x (and .y) to be the coordinates of the mouse — without accounting for the offset — and therefore I would think that the circle's center would (incorrectly, from a UX point-of-view) end up right under the mouse.

Euphonious answered 8/3, 2018 at 18:3 Comment(0)
C
10

I believe this happens with the d3 drag subject method:

If subject is specified, sets the subject accessor to the specified object or function and returns the drag behavior. If subject is not specified, returns the current subject accessor, which defaults to:

function subject(d) { return d == null ? {x: d3.event.x, y: d3.event.y} : d; }

The subject of a drag gesture represents the thing being dragged. It is computed when an initiating input event is received, such as a mousedown or touchstart, immediately before the drag gesture starts. The subject is then exposed as event.subject on subsequent drag events for this gesture. (link)

We can see that if we don't provide a subject function and we also don't provide a datum with x and y properties, then the drag events will result in a circle's centering/snapping to the drag start point:

var svg = d3.select("body")
  .append("svg")
  .attr("width",500)
  .attr("height",300);
  
var datum = {x:250,y:150}
  
var g = svg.append("g")
    
  g.append("rect")
  .attr("width",500)
  .attr("height",300)
  .attr("fill","#ddd");
  
  g.append("circle")
  .datum(datum)
  .attr("cx",function(d) { return d.x; })
  .attr("cy",function(d) { return d.y; })
  .attr("r",10);
    
g.call(d3.drag().on("drag", dragged))
  
function dragged(d) {
  d3.select(this)
    .select("circle")
    .attr("cx", d3.event.x)
    .attr("cy", d3.event.y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

Taking the same example, and moving assigning the datum to the parent g element allows the drag to access the subject's x and y properties (which were not present in the above example). Here the drag is relative to the initial datum (which remains unmodified), and the node will be re-centered usingn the initial x and y properties specified in the datum as the starting point for each drag (drag more than once to see):

var svg = d3.select("body")
  .append("svg")
  .attr("width",500)
  .attr("height",300);
  
var datum = {x:250,y:150}
  
var g = svg.append("g")
  .datum(datum);
  
  g.append("rect")
  .attr("width",500)
  .attr("height",300)
  .attr("fill","#ddd");
  
  g.append("circle")
  .attr("cx",function(d) { return d.x; })
  .attr("cy",function(d) { return d.y; })
  .attr("r",10);
    
g.call(d3.drag().on("drag", dragged))
  
function dragged(d) {
  d3.select(this)
    .select("circle")
    .attr("cx", d3.event.x)
    .attr("cy", d3.event.y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

Then we can update the datum of the subject, which makes each drag event relative to the circle's current position rather than the initial position:

var svg = d3.select("body")
  .append("svg")
  .attr("width",500)
  .attr("height",300);
  
var datum = {x:250,y:150}
  
var g = svg.append("g")
  .datum(datum);
  
  g.append("rect")
  .attr("width",500)
  .attr("height",300)
  .attr("fill","#ddd");
  
  g.append("circle")
  .attr("cx",function(d) { return d.x; })
  .attr("cy",function(d) { return d.y; })
  .attr("r",10);
    
g.call(d3.drag().on("drag", dragged))
  
function dragged(d) {
  d3.select(this)
    .select("circle")
    .attr("cx", d.x = d3.event.x)
    .attr("cy", d.y = d3.event.y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>

Diving into the drag code a little bit we can see that when a drag is started and if no function has been provided to the subject method, the difference between the drag x,y start and the subject x,y is calculated:

  dx = s.x - p[0] || 0;
  dy = s.y - p[1] || 0;

Where p is the starting mouse position. And s is the subject.

Which explains why when no x or y attributes are provided, the circle snaps to wherever the drag began. When calculating the output, d3 sets the x and y values as:

p[0] + dx,
p[1] + dy

where p is the current mouse position.

So d3.event.x/.y should not be the absolute position of the mouse, but rather the absolute position of the circle given a relative change in position specified by the drag. It is through the subject that the relative change in mouse position is translated into an absolute position for the item being dragged.

Here's an example with a custom subject, where the drag will be relative to [100,100] and the circle will snap there at the beginning of each drag event:

var svg = d3.select("body")
  .append("svg")
  .attr("width",500)
  .attr("height",300);
  
var datum = {x:250,y:150}
  
var g = svg.append("g")
  .datum(datum);
  
  g.append("rect")
  .attr("width",500)
  .attr("height",300)
  .attr("fill","#ddd");
  
  g.append("circle")
  .attr("cx",function(d) { return d.x; })
  .attr("cy",function(d) { return d.y; })
  .attr("r",10);
    
g.call(d3.drag()
   .on("drag", dragged)
   .subject({x:100,y:100})
   )
  
function dragged(d) {
  d3.select(this)
    .select("circle")
    .attr("cx", d3.event.x)
    .attr("cy", d3.event.y);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Christopherchristopherso answered 8/3, 2018 at 18:51 Comment(2)
Andrew, thank very much for your thorough exploration and response AND runnable code(!). Makes sense now.Euphonious
Thanks, this is my favorite type of question, interesting, novel, and forces a closer look at things that are often just taken for granted.Christopherchristopherso

© 2022 - 2024 — McMap. All rights reserved.