D3.js Tutorial: Drag and Drop

D3.js Tutorial: Drag and Drop

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:

<div class="svg-container">
<svg id="click" xmlns="http://www.w3.org/2000/svg" width="600" height="300">
    <defs>
        <g id="pointer" transform="scale(0.8)">
            <path d="M0-1c-14.5-25.6-14.5-25.7-14.5-33.8c0-8.1,6.5-14.6,14.5-14.6s14.5,6.6,14.5,14.6C14.5-26.7,14.5-26.6,0-1z"></path>
            <path d="M0-49c7.7,0,14,6.3,14,14.1c0,8,0,8.1-14,32.8c-14-24.7-14-24.9-14-32.8C-14-42.7-7.7-49,0-49 M0-50c-8.3,0-15,6.8-15,15.1 S-15-26.5,0,0c15-26.5,15-26.5,15-34.9S8.3-50,0-50L0-50z"></path>
        </g>
    </defs>
</svg>
</div>

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:

var svg = d3.select("svg");
svg.append("use")
    .attr("href", "#pointer")
    .attr("x", 50)
    .attr("y", 50)
    .attr("fill", "#039BE5")
    .attr("stroke", "#039BE5")
    .attr("stroke-width", "1px");

var dragHandler = d3.drag()
    .on("drag", function () {
        d3.select(this)
            .attr("x", d3.event.x)
            .attr("y", d3.event.y);
    });

dragHandler(svg.selectAll("use"));

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:

var svg = d3.select("svg");
svg.append("use")
    .attr("href", "#pointer")
    .attr("x", 50)
    .attr("y", 50)
    .attr("fill", "#039BE5")
    .attr("stroke", "#039BE5")
    .attr("stroke-width", "1px");

var deltaX, deltaY;

var dragHandler = d3.drag()
    .on("start", function () {
        var current = d3.select(this);
        deltaX = current.attr("x") - d3.event.x;
        deltaY = current.attr("y") - d3.event.y;
    })
    .on("drag", function () {
        d3.select(this)
            .attr("x", d3.event.x + deltaX)
            .attr("y", d3.event.y + deltaY);
    });

dragHandler(svg.selectAll("use"));

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:

var svg = d3.select("svg");
svg.selectAll("use")
    .data([{
        x: 50,
        y: 50
    }])
    .enter()
    .append("use")
    .attr("href", "#pointer")
    .attr("x", function (d) {
        return (d.x)
    })
    .attr("y", function (d) {
        return (d.y)
    })
    .attr("fill", "#039BE5")
    .attr("stroke", "#039BE5")
    .attr("stroke-width", "1px");

var dragHandler = d3.drag()
    .on("drag", function () {
        d3.select(this)
            .attr("x", d3.event.x)
            .attr("y", d3.event.y);
    });

dragHandler(svg.selectAll("use"));

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:

var svg = d3.select("#dnd-data-fail");
svg.selectAll("use")
[...]

var dragHandler = d3.drag()
    .on("drag", function (d) {
        d3.select(this)
            .attr("x", d.x = d3.event.x)
            .attr("y", d.y = d3.event.y);
    });

dragHandler(svg.selectAll("use"));

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:

var svg = d3.select("svg");
svg.on("click", function () {
    var mouse = d3.mouse(this);

    var pointer = svg
        .append("use")
        .datum({x: mouse[0], y: mouse[1], selected: false})
        .attr("id", "currentPointer")
        .attr("href", "#pointer")
        .attr("transform", "translate(" + mouse[0] + "," + mouse[1] + ")scale(0)")
        .attr("fill", "#039BE5")
        .attr("stroke", "#039BE5")
        .attr("stroke-width", "1px");

    pointer
        .transition()
        .duration(500)
        .attr("transform", "translate(" + mouse[0] + "," + mouse[1] + ") scale(1)")
        .attr("x", mouse[0])
        .attr("y", mouse[1])
        .attr("transform", "scale(1)");

    pointer.on("click", function () {
        if (d3.event.ctrlKey) {
            pointer.transition()
                .duration(500)
                .attr("transform", "translate(" + pointer.attr("x") + "," + pointer.attr("y") + ") scale(0)")
                .remove();
        } else {
            var datum = pointer.datum();
            if (pointer.datum().selected) {
                datum.selected = false;
                pointer
                    .datum(datum)
                    .transition()
                    .duration(500)
                    .attr("stroke", "#039BE5")
                    .attr("stroke-width", "1px");
            } else {
                datum.selected = true;
                pointer
                    .datum(datum)
                    .transition()
                    .duration(500)
                    .attr("stroke", "#455A64")
                    .attr("stroke-width", "3px");
            }
        }
        d3.event.stopPropagation();
    });

    var dragHandler = d3.drag()
        .on("drag", function (d) {
            d3.select(this)
                .attr("x", d.x = d3.event.x)
                .attr("y", d.y = d3.event.y);
        });

    dragHandler(pointer);
});

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:

 var datum = pointer.datum();
    if (pointer.datum().selected) {
        datum.selected = false;
        pointer
            .datum(datum)

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!

By - CEO.
Tags: D3.js V5 Svg D3 Drag Mouse Event Drag and Drop Drag Drop Data Dnd Datum

Comments

 

Thank you

Your comment has been submitted and will be published once it has been approved.

OK

OOPS!

Your post has failed. Please return to the page and try again. Thank You!

OK