0

I'm experiencing problems with the d3 drag behavior. Variations of the issue I'm having have been answered before, but I couldn't find an answer to my specific problem.

Here's a fiddle which illustrates the problem I'm going to describe.

What I want to do is to have a click handler on a draggable element where the click handler shouldn't be executed on dragend. I know that inside the click handler I can use d3.event.defaultPrevented which should be set to true if the element was dragged. The problem arises when the mouseup event happens on another element than the mousedown event. This happens when the movement of the dragged element is slower than the mouse cursor. If the mouse is released and the dragged element isn't under the mouse yet d3.event.defaultPrevented is set to false and the click handler doesn't get called. This makes it impossible to find out whether the click event was fired after a drag or not.

In my example the circle flashes green if the click handler executes but d3.event.defaultPrevented was set to true. Also in the click handler propagation is prevented preventing the event to bubble up to the svg click handler. If the click handler of the circle doesn't execute and the event bubbles to the svg click handler the circle flashes blue if d3.event.defaultPrevented was set to true otherwise it flashes red.

What I want to achieve is to get the circle to flash green or blue no matter where the circle is on mouse up in order to be able to know whether the click event happened after a drag or not. Is this even possible or is this a limitation by the nature of javascript/browsers? If so is there a workaround? Or do I just have to disable the 'slowing down' of the circle when it is dragged?

I found a very similar question on SO but there wasn't really a useful answer.

Any help appreciated!

EDIT Looks like the idea to stop the element from slowing down during dragging solves the problem. But I would still be interested if this is possible using the event information available.

Here's the code of the fiddle:

var nodes = [{}];
var svg = d3.select('body')
        .append('svg')
        .attr({
            width: 500,
            height: 500
        })
        .on('click', function(){
            var color = d3.event.defaultPrevented ? 'blue' : 'red';
            flashNode(color);
        });

var force = d3.layout.force()
        .size([500, 500])
        .nodes(nodes)
        .friction(.2)
        .on('tick', forceTickHandler);

var nodeElements = svg
        .selectAll('circle');

nodeElements = nodeElements
        .data(force.nodes())
        .enter()
        .append('circle')
        .attr({
            cx: 10,
            cy: 10,
            r: 10
        })
        .on('click', function(){
            d3.event.stopPropagation();
            var color = d3.event.defaultPrevented ? 'green' : 'orange';
            flashNode(color);
        })
        .call(force.drag);

function forceTickHandler(e){
    nodes.forEach(function(node) {
      var k = e.alpha * 1.4;

      node.x += (250 - node.x) * k;
      node.y += (250 - node.y) * k;
    });

  nodeElements
  .attr('cx', function(d, i){
      return d.x;
  })
  .attr('cy', function(d, i){
      return d.y;
  });
};

function flashNode(color){
    nodeElements
        .attr('fill', color)
        .transition()
        .duration(1000)
        .attr('fill', 'black');
}

force.start();
Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
Flavio
  • 1,507
  • 5
  • 17
  • 30

1 Answers1

0

The issue seems to be coming from the code in your forceTickHandler that updates the node positions:

nodes.forEach(function(node) {
  var k = e.alpha * 1.4;

  node.x += (250 - node.x) * k;
  node.y += (250 - node.y) * k;
});

When this is commented out, the position of the node does not lag the mouse pointer. I don't really understand what you want to happen with the code above. The "typical" way of doing this, is similar to the sticky force layout example at http://bl.ocks.org/mbostock/3750558

Update: Here's a way that might get you close to what you are after: https://jsfiddle.net/ktbe7yh4/3/

I've created a new drag handler from force.drag and then updated what happens on dragend, it seems to achieve the desired effect.

The code changes are the creation of the drag handler:

var drag = force.drag()
                .on("dragend", function(d) {
                    flashNode('green');
                });

And then updating the creation of the nodes to use the new handler:

nodeElements = nodeElements
        .data(force.nodes())
        .enter()
        .append('circle')
        .attr({
            cx: 10,
            cy: 10,
            r: 10
        })
        .on('click', function(){
            d3.event.stopPropagation();
            var color = d3.event.defaultPrevented ? 'green' : 'orange';
            flashNode(color);
        })
        .call(drag);

The dragend in the drag handler gets called no matter what, but it still suffers from a similar problem that you describe, but you can deal with it a little better in the handler. To see what I mean here, try changing:

flashNode('green');

to:

flashNode(d3.event.defaultPrevented ? 'green' : 'orange');

and you'll see that if you release the mouse when the pointer is directly pointing at the circle, it'll flash green. If the circle is lagging the pointer and you release the mouse before the circle has settled under the cursor, then it'll flash orange. Having said that, the data element in the dragend handler always seems to be set to the circle that was dragged to begin with, whether the mouse button was released whilst pointing at the circle or not.

Ben Lyall
  • 1,976
  • 1
  • 12
  • 14
  • Thanks for your answer! I understand that the problem originates in the `tick` handler and the lag the recalculation of position causes. What I want to achieve is to get the click handler to fire even when the mouse cursor isn't on the element anymore when the `dragend` event happens. But now I that I write it like that it sounds kind of impossible and silly. I guess I'll just have to skip the recalculation of position when the element is dragged. My idea was to have something like [this](http://bl.ocks.org/mbostock/1021953) but with additional interactions on the canvas and the element. – Flavio Mar 19 '15 at 08:37
  • I have updated my answer with a possible way of achieving the outcome that you're after. It's a bit of a workaround, but it may let you keep the node moving the way you had it, with the flash behaviour you're after. – Ben Lyall Mar 19 '15 at 21:37