In our previous tutorial we explained how to handle click events with D3.js to add, remove and select shapes in a SVG chart.
Using the same map example this tutorial explains how to handle drag and drop with D3.js. To follow this guide you must know about:
- D3.js installation,
- D3.js selectors,
- and D3.js data binding.
All these concepts are explained in our D3 getting started guide.
It uses the same SVG sample as our previous example:
|
|
It’s a simple SVG with a pointer shape defined. Drag and drop events will be added to use
elements that reference this pre-defined shape (<g id="pointer"/>
). If you need more information about this, please consult our previous tutorial.
Drag and drop shapes
The D3-drag library
To handle drag and drop events easily, D3.js comes with the D3-drag library. In the example bellow you can drag the pointer across the map:
For this first code sample, we first append
a static pointer shape to the SVG:
|
|
Then we create a drag handler function using d3.drag
and use it on our use elements (dragHandler(svg.selectAll("use"))
). Handling drag and drop with D3.js is simple as that!
dragHandler describes the behavior on drag
events. It takes a callback function, that should handle the transformation of the current selection.
In this example we simply update the current pointer coordinates (x,y) using the event position.
There is one minor flaw with this behavior: If the user grabs the pointer shape by somewhere else than the pointy end, it jumps to the mouse position. That’s because at the start of the drag motion, the pointer origin (its lower tip) is moved to the mouse position. Not very user friendly!
Playing with delta
To fix this we must compute the delta between the mouse position and the shape position when the drag motion starts:
The drag behavior can handle both “drag” events and “start” events:
|
|
Here, using the .on("start", callback)
we compute the delta between the event position d3.event.(x|y) and the origin of the shape.
The origin of our pointer shape is returned by .attr("x|y")
method calls.
Then during the drag
event, we can apply this delta to avoid the shape “jump”: d3.event.x + deltaX
.
It works pretty well, but that’s a bit tedious. There might be a better way to do it?
Drag and dropping data-bound elements
A simpler use-case
Of course there is! Don’t forget that D3.js is a library for manipulating SVG and DOM elements based on data. Here we were completely missing the data binding part!
Let’s try again with (x,y) coordinated bound to the element data this time:
|
|
Here when creating the pointer shape, we bind its coordinates .data([{x: 50,y: 50}])
to affect it using attribute callbacks .attr("x", function (d) {return (d.x)}).attr("y", function (d) {return (d.y)})
.
When using data-binding, D3-drag automatically handle the drag events coordinates delta:
Works like a charm, doesn’t it? Well … not that much. If you drag the shape more than once, it jumps back to its original position.
Updating the data
Obviously that’s because we forgot to update the shape’s data in the drag event handler:
|
|
The .on("drag")
callback function takes the parameter d
: the selection data. We simply update it along with the shape (x,y) attributes: d3.select(this).attr("x", d.x = d3.event.x).attr("y", d.y = d3.event.y);
.
Now its perfect:
Good! But what happened to our click
events?
D3.js click and drag events combined
Now it’s time to combine our drag and drop example with our previous “mouse events” tutorial. In the next live demo you can:
- Click on the map to create a new pointer,
- Click while CTRL is pressed on a pointer to remove it,
- Click on a pointer to (un)select it,
- And of course drag and drop pointers across the map.
Here we bind data to the shape using datum()
instead of data()
. But otherwise, it’s the same as the previous code sample put together with a click
handler:
|
|
The data binding done during the shape creation .datum({x: mouse[0], y: mouse[1], selected: false})
must be preserved when it is (un)selected:
|
|
We only update the selected
state of the shape and keep its coordinates.
That’s it for this tutorial. Feel free to check our other guides about D3.js!
Hi, Thanks for tutorial. A point that is not clear for me is the data, If I replace ‘x’ or ‘y’ member data wit another name, like … .data([{ x: 50, ycoord:50 }]) …. .attr(‘y’, function (d) { return (d.yCoord)}) … and in ‘drag’ event: .attr(‘y’, d.yCoord = d3.event.y);
coordinates delta is not managed anymore. Why that? Thanks, Claude
Thanks for the informative D3 posts. I’ve had some issues combining drag-and-drop with zoom-and-pan in a React app and your posts – especially this one – are a useful supplement to other material I’ve come across.