July 12th, 2026
Working on a graph visualization project in D3, I was having trouble with dragging nodes around. Using d3-drag, I was able to add drag behavior, but nodes wouldn’t actually follow the mouse position perfectly, and the more you dragged them around the more they’d start to jump back and forth between two positions roughly symmetrical around the mouse.
I was using D3’s force-simulation for graph layout, so I spent a lot of time suspecting that and delving into details. I thought for a while that some hidden handler must be updating node positions outside of the code I’d written in the drag event handler. The solution turned out to be simpler.
In addition to force-directed graphs, I was also using the basic d3-zoom to handle zooming & panning. However, I had set it up to transform a ‘g’ element within my SVG rather than the whole SVG itself, so that I could have in-svg labels that didn’t zoom or pan. This turned out to be the source of my jitter, and the fix was thankfully simple: setting .container on the d3 drag manager to be the SVG ‘g’ element that was managing zooming fixed the jitter, and it also fixed the issue with mouse following, which had been due to the drag coordinate corrections not taking the zoom scale into account.
My original code looked like this:
...
.call(d3.drag()
.on("start", dragStarted) // dragStarted assigns per-event 'drag'
.on("end", dragEnded));And the correct code was simply:
...
.call(d3.drag()
.container(OVERVIEW_ZOOM_GROUP.node()) // prevents jitter!
.on("start", dragStarted) // dragStarted assigns per-event 'drag'
.on("end", dragEnded));Note that this code doesn’t include a .on("drag", ... handlers because as the comment indicates, the dragStarted function is using event.on("drag", ... to set a per-drag-event handler that uses a closure to nab some relevant variables from the drag start handler context.
The zoom was set up using this code:
function zoomer(toTransform) {
// Returns zoom handling function that will zoom/pan the specified
// target element.
return function (evt) {
// Updates transform of a specific target element
toTransform.attr("transform", evt.transform);
}
}
OVERVIEW.node().zoom = d3.zoom().on("zoom", zoomer(OVERVIEW_ZOOM_GROUP));Since the node of the OVERVIEW_ZOOM_GROUP is the thing being transformed by the zoomer, that’s the correct container for the things I’m dragging around (which are children of that group so the inherit its transform). I have other SVG elements which are NOT in that group; if I wanted them to be draggable, I’d probably have to set a different container on a separate drag handler for them.