I have a project with a draggable circle. In the process of upgrading from D3 v3 > v4 the drag behaviour broke. It only seems to work when the draggable element is created using data([1]).enter().append('circle'). See the simple jsfiddle example for a simplified version:
// Circle dragging: OK
d3
.select("svg")
.selectAll("circle")
.data([1])
.enter()
.append("circle")
.style("fill", 'blue')
.attr("r", 30)
.attr("cx", 2*30)
.attr("cy", 2*30)
// Circle dragging: FAILS
d3
.select("svg")
.append("circle")
.style("fill", 'green')
.attr("r", 30)
.attr("cx", 4*30)
.attr("cy", 4*30)
d3
.select("svg")
.selectAll("circle")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
function dragstarted(d) {
d3.select(this).raise().classed("active", true);
}
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
function dragended(d) {
d3.select(this).classed("active", false);
}
Can someone take a look?
Discussion on StackOverflow here
This is the expected behavior, though perhaps it could be improved. As the documentation for _drag_.subject says:
The default subject is the datum of the element in the originating selection (see _drag_) that received the initiating input event. When dragging circle elements in SVG, the default subject is the datum of the circle being dragged…
And:
If the subject is null or undefined, no drag gesture is started for this pointer; however, other starting touches may yet start drag gestures.
In your failing example, the datum of your circle is undefined, so it can’t initiate a drag gesture. Giving the circle a non-null datum, such as 1, fixes it.
Yet even your non-failing example isn’t ideal because the drag gesture can’t infer the _x_- and _y_-coordinates of the circle, and so when you start a drag gesture, the circle center will jump to the mouse (or touch) position, rather than maintaining the relative position of the circle and pointer. A better way of creating a single, draggable circle would be:
d3.select("svg").append("circle")
.datum({x: 4 * 30, y: 4* 30})
.attr("r", 30)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
It’s possible we should change the default subject to be something else if the element’s datum is undefined… such as the element itself. That would make the drag behavior work as you expect, although the above code is a better solution in my opinion.
Also, the change I mention is actually how it worked previously; see d3/d3-drag@8b25a53c826200fe643cd009fe9fd0ad8e99085b. I reverted it because I thought it was simpler to eliminate this fallback behavior, but maybe it’s okay.
As of d3-drag 0.1.5, dragging is now allowed by default even if the element’s datum is undefined. The fallback subject in this case is simply an object representing the initial coordinates of the pointer (mouse).
Mike, would you mind explaining this piece of code?
function dragged(d) {
d3.select(this).attr("cx", d.x = d3.event.x).attr("cy", d.y = d3.event.y);
}
How is it that a variable assignment inside the attr method reassigns d.x and also sets the attribute "cx" to d3.event.x?
This is what I would have written:
function dragged(d) {
d.x = d3.event.x;
d.y = d3.event.y;
d3.select(this).attr("cx", d3.event.x).attr("cy", d3.event.y);
}
Still, why does the circle snap back to it's original position if d.x and d.y are not set to d3.event?
I don't understand why a new drag event makes the circle revert back to it's old attributes. Thanks!
Your question pertains more to the JavaScript language than D3, so it’d probably be more appropriate for you to take it to Stack Overflow than to tack it on to the end of this issue. That said, here’s my answer.
An assignment expression d.x = value in JavaScript evaluates to value, so if you want to assign a property _and_ pass a value to a function (such as _selection_.attr) you can save a few characters by doing both at the same time. It doesn’t really matter whether you assign d.x as a separate statement or as part of the call to _selection_.attr.
If you don’t set d.x and d.y, then you’re updating the DOM (the circle’s cx and cy attributes), but you’re not updating the data values (the circle’s datum _d_). So if you then reset the circle’s cx and cy attributes to the data (which you didn’t update while dragging), you’d be reverting the circle’s position back to its old position. The drag behavior doesn’t update the data for you—it just reports the new position, and it’s your responsibility to use that however you like, whether it’s to update the DOM or to update your data.
Most helpful comment
Your question pertains more to the JavaScript language than D3, so it’d probably be more appropriate for you to take it to Stack Overflow than to tack it on to the end of this issue. That said, here’s my answer.
An assignment expression
d.x = valuein JavaScript evaluates tovalue, so if you want to assign a property _and_ pass a value to a function (such as _selection_.attr) you can save a few characters by doing both at the same time. It doesn’t really matter whether you assignd.xas a separate statement or as part of the call to _selection_.attr.If you don’t set
d.xandd.y, then you’re updating the DOM (the circle’s cx and cy attributes), but you’re not updating the data values (the circle’s datum _d_). So if you then reset the circle’s cx and cy attributes to the data (which you didn’t update while dragging), you’d be reverting the circle’s position back to its old position. The drag behavior doesn’t update the data for you—it just reports the new position, and it’s your responsibility to use that however you like, whether it’s to update the DOM or to update your data.