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>