Delaying drag event handling to avoid clicks triggering graph layout

2,123 views
Skip to first unread message

MarkH

unread,
Jun 24, 2011, 10:40:51 AM6/24/11
to d3-js
First off - very nice job on the d3 graph layout.

I've found on some larger graphs that have finished laying out my
attempts to click on a node e.g. to provide a detail popup have the
unfortunate side effect of triggering layout again.
This I suspect is triggered by the .call (force.drag) event handler
added to my graph node. Is there anyway the drag detection behaviour
can be delayed to allow double clicks or clicks to be done safely e.g.
wait for a minimum number of pixels drag to happen before initiating
layout?

I'm more of a Java guy than a Javascript expert so please be gentle
with me...

Mike Bostock

unread,
Jun 24, 2011, 11:16:20 AM6/24/11
to d3...@googlegroups.com
> I've found on some larger graphs that have finished laying out my
> attempts to click on a node e.g. to provide a detail popup have the
> unfortunate side effect of triggering layout again.

Yes, this could be fixed. The force.resume method is only called when
the mouse moves, so clicking itself would not trigger a resume.
However, the mouseup handler currently invokes the mousemove handler,
in dependent of whether or not the node was dragged; thus, if you
click without dragging, you'll receive the "click" event but the
layout will also restart. I think we could change this quite easily by
detecting if the mouse had moved before calling dragmove here:

https://github.com/mbostock/d3/blob/master/src/layout/force.js#L263

The simplest fix would be to move dragmove() into the if block, but
I'm not 100% it's the right fix:

if (d3_layout_forceDragMoved) {
d3_layout_forceStopClick = true;
d3_layout_forceCancel();
dragmove();
}

Another possibility is to remove dragmove() entirely, and just hope
that the browser reports every mousemove.

Mike

MarkH

unread,
Jun 24, 2011, 11:43:35 AM6/24/11
to d3-js
Hi Mike, Thanks for the quick response.

I tried both these approaches (put dragmove call in "if" block and
remove dragmove call) but unfortunately neither seemed to fix it.
I added some console logging and I can see that with the call to
dragmove function removed from that section dragmove is still being
called and it looks like from the "on mousemove.force" event handler
added in the force.drag function

Any more ideas?

Mike Bostock

unread,
Jun 24, 2011, 12:00:40 PM6/24/11
to d3...@googlegroups.com
> I tried both these approaches (put dragmove call in "if" block and
> remove dragmove call) but unfortunately neither seemed to fix it.

I just tested it locally and it worked fine for me, commenting out the
call to dragmove in the dragup handler. Can you post an example of
yours not working? Did you recompile d3.layout.js after making the
change to force.js? What browser are you using? What does the rest of
your code looked like.

Mike

MarkH

unread,
Jun 24, 2011, 12:03:49 PM6/24/11
to d3-js
More context - I found if I click in the *exact* center of the node
then I don't see the dragmove event firing from the on mousemove.force
event handler. Clicking one pixel or more away from the node center
causes the node to move to be centered directly underneath the cursor
and I see the dragmove event trigger.

Could this "auto-centering" logic be falsely triggering drag events
because of a (mis)perceived movement of the mouse in relation to the
node's (auto-adjusted) position?

MarkH

unread,
Jun 24, 2011, 12:25:47 PM6/24/11
to d3-js
I'm confused by the statement about "recompiling" and changing
force.js?

I'm using the latest Chrome and a modified version of the d3 example
force layout demo so a webpage referencing d3.js, d3.layout.js etc and
I added d3.behavior.min.js to get mousewheel zoom working.
Forces.js is also included in the web page and I have modified
slightly to work with my particular data.

The code you linked to I found in d3.layout.js which I tweaked to try
the suggested changes to dragmove calls.
I also added some console logging and get this when I do a single
click on a node in a freshly laid out graph:

227d3.layout.js:398Entered dragmove
d3.layout.js:414dragmove calling force.resume
d3.layout.js:419entered dragup
d3.layout.js:427calling dragmove from dragup if block
d3.layout.js:398Entered dragmove
d3.layout.js:414dragmove calling force.resume

The modified d3.layout.js code that generate the above is below

function dragmove() {
console.log("Entered dragmove");
if (!d3_layout_forceDragNode) return;
var parent = d3_layout_forceDragElement.parentNode;

// O NOES! The drag element was removed from the DOM.
if (!parent) {
d3_layout_forceDragNode.fixed = false;
d3_layout_forceDragNode = d3_layout_forceDragElement = null;
return;
}

var m = d3.svg.mouse(parent);
d3_layout_forceDragMoved = true;
d3_layout_forceDragNode.px = m[0];
d3_layout_forceDragNode.py = m[1];
//MH console.log("drag Move called");
console.log("dragmove calling force.resume");
force.resume(); // restart annealing
}

function dragup() {
console.log("entered dragup");
if (!d3_layout_forceDragNode) return;

// If the node was moved, prevent the mouseup from propagating.
// Also prevent the subsequent click from propagating (e.g., for
anchors).
if (d3_layout_forceDragMoved) {
d3_layout_forceStopClick = true;
d3_layout_forceCancel();
console.log("calling dragmove from dragup if block");
dragmove(); //MH
//MH console.log("In dragup If");
}

// See http://groups.google.com/group/d3-js/browse_thread/thread/a8942e097d598633
// dragmove();
d3_layout_forceDragNode.fixed = false;
d3_layout_forceDragNode = d3_layout_forceDragElement = null;
}

return force;
};


Cheers
Mark


Mike Bostock

unread,
Jun 24, 2011, 1:05:22 PM6/24/11
to d3...@googlegroups.com
> I'm confused by the statement about "recompiling" and changing
> force.js?

I was referring to src/layout/force.js, not the force.js in the
examples directory. The file d3.layout.js is compiled from source
files in the src directory; by compiled, I simply mean that many small
files are concatenated into a larger file. Then d3.layout.min.js is
generated by minifying the large file using UglifyJS. This is all
specified in the Makefile. The point being, if you edit d3.layout.js,
you are editing an auto-generated file, and it's generally better to
edit the source file and run make.

>      console.log("calling dragmove from dragup if block");
>      dragmove(); //MH

Don't call dragmove here. Remove all calls to dragmove inside the
dragup handler.

Mike

Mike Bostock

unread,
Jun 24, 2011, 1:10:34 PM6/24/11
to d3...@googlegroups.com
> Could this "auto-centering" logic be falsely triggering drag events
> because of a (mis)perceived movement of the mouse in relation to the
> node's (auto-adjusted) position?

No. It's true that whenever a mousemove occurs, the node is centered
on the mouse cursor. The layout does not preserve a fixed offset
between the node and the mouse, because we generally assume that the
nodes are small so this difference will be imperceptible. But, a
mousemove is only generated when the mouse moves, independent of where
you click relative to the node. So, it seems more likely that you are
moving the mouse when you are clicking… is that possible? Try clicking
more gently? :)

Mike

MarkH

unread,
Jun 24, 2011, 2:50:50 PM6/24/11
to d3-js
OK. Thanks for the "compile" pointer.

I tried both moving the dragmove call into the "if" block and also
removing it entirely.
I am trying very hard not to move the mouse at all but still
triggering re-layouts - not sure all users would stand for that level
of required mouse control either. I think the answer probably lies in
either:
a) initiating drags only if mouse still down after roughly 500
milliseconds or
b) only dragging after mouse has moved say more than 3 pixels

This should give the tolerance to allow "sloppy" clicks to not trigger
re-layouts. In most apps also misinterpreting a click as a small drag
would have limited consequences but this stuff matters most when you
are tightly zoomed into a cluster deep in the graph because the re-
laying out is very disorientating, often scrolling the items of
interest out of view (it would help to limit layout operations to
roughly that area of the graph only, but that is another issue....)

Cheers
Mark

Mike Bostock

unread,
Jun 24, 2011, 3:12:03 PM6/24/11
to d3...@googlegroups.com
> This should give the tolerance to allow "sloppy" clicks to not trigger re-layouts.

True, but in many cases it's not damaging to restart the layout, and
I'm wary of making the drag behavior either sluggish or imprecise.
Even small things like 500ms or 3px can be noticeable.

Another option might be for you to call force.stop() to stop the
layout immediately, either on mouseup or on click. And, if you want to
permanently stop the layout, you could also reassign the resume method
after the graph cools:

force.resume = function() { return force; }; // noop

Yet another option might be to be more conservative in the value of
alpha when resuming, so that the change is not as jarring. The graph
could heat up gradually rather than jumping immediately to alpha = .1.

Mike

MarkH

unread,
Jun 24, 2011, 4:11:37 PM6/24/11
to d3-js
OK, thanks for the pointers, Mike.

>>you could also reassign the resume method

Javascript really does give you more than enough rope to hang yourself
doesn't it?
:)
Message has been deleted

Peter Meehan

unread,
Aug 16, 2011, 9:06:16 PM8/16/11
to d3...@googlegroups.com
Out of curiosity Mike, what's the motivation for having the mouseup handler invoking the mousemove handler?

I too wanted to suppress the automatic triggering of the resume when clicking nodes, and though I decided to plump for the "call force.stop() on click" method (I'd rather not mess with the source if I can avoid it), I found that both commenting out the dragmove call and moving it into the "if dragmoved" block, as you suggested, also worked perfectly. So why is it in there to begin with?

Mike Bostock

unread,
Aug 21, 2011, 7:02:59 PM8/21/11
to d3...@googlegroups.com
> Out of curiosity Mike, what's the motivation for having the mouseup handler
> invoking the mousemove handler?

There are times when the mouse can move between the last mousemove
event and the mouseup event, and I wanted the nodes to end up at the
mouse location. The fix (moving dragmove to the if dragmoved block)
has been merged and released:

https://github.com/mbostock/d3/commit/2440287d4558585a6d4673a2317cb376078d401a#src/layout/force.js

Mike

Reply all
Reply to author
Forward
0 new messages