I have defined some drag behaviour that works as expected as follows (code in CoffeeScript):
nodeDrag = d3.behavior.drag()
.on("dragstart", (d, i) ->
force.stop())
.on("drag", (d, i) ->
d.px += d3.event.dx
d.py += d3.event.dy
d.x += d3.event.dx
d.y += d3.event.dy
tick())
.on("dragend", (d, i) ->
force.resume()
d.fixed = true
tick())
// ...
nodes = vis.selectAll(".node")
.data(graph.nodes)
.enter()
.append("g")
// ...
.call(nodeDrag)
I now try to create custom behaviour for right clicks on nodes. However, this triggers "dragstart" and "drag", i.e. after I call e.preventDefault() on the "contextmenu" event, the node in question is stuck to my mouse pointer and follows it around until I do another (left) click to force a release (I assume e.preventDefault() also causes "dragend" to never fire).
I found a brief discussion of this issue in a thread on Google Groups and a discussion in d3's issues on Github. However, I cannot figure out from those comments how to prevent this behaviour.
How can I not trigger dragging on right click?
I found a possibility to limit the drag gestures to left mouse button only.
It involves an additional field that records when a gesture has been initiated:
dragInitiated = false
The rest of the code is then modified to register initiation and termination of a desired drag gestures on "dragstart" and "dragend", respectively. Actions for "drag" are then only performed if a drag gestures was properly initiated.
nodeDrag = d3.behavior.drag()
.on "dragstart", (d, i) ->
if (d3.event.sourceEvent.which == 1) # initiate on left mouse button only
dragInitiated = true # -> set dragInitiated to true
force.stop()
.on "drag", (d, i) ->
if (dragInitiated) # perform only if a drag was initiated
d.px += d3.event.dx
d.py += d3.event.dy
d.x += d3.event.dx
d.y += d3.event.dy
tick()
.on "dragend", (d, i) ->
if (d3.event.sourceEvent.which == 1) # only take gestures into account that
force.resume() # were valid in "dragstart"
d.fixed = true
tick()
dragInitiated = false # terminate drag gesture
I am not sure whether this is the most elegant solution, but it does work and is not exceptionally clumsy or a big hack.
for D3 v4 see drag.filter([filter])
If filter is specified, sets the filter to the specified function and returns the drag behavior. If filter is not specified, returns the current filter, which defaults.
drag.filter([filter])
for the right click:
.filter(['touchstart'])
Filters available
mousedown, mousemove, mouseup, dragstart, selectstart, click, touchstart, touchmove, touchend, touchcancel
Little late to the party, I had the same problem and I used the following method to make sure my drag only works for left click.
var drag = d3.behavior.drag()
.on('drag', function () {
console.log(d3.event.sourceEvent.button);
if(d3.event.sourceEvent.button == 0){
var mouse = d3.mouse(this);
d3.select(this)
.attr('x', mouse[0])
.attr('y', mouse[1]);
}
});
To handle all listeners of drag event, you can use the code below:
function dragChartStart() {
if(d3.event.sourceEvent.button !== 0) {
console.log("not left click");
return;
}
console.log("dragStart");
}
function dragChartEnd() {
if(d3.event.sourceEvent.button !== 0) {
console.log("not left click");
return;
}
console.log("dragEnd");
}
function dragChartMove() {
if(d3.event.sourceEvent.button !== 0) {
console.log("not left click");
return;
}
console.log("dragMove");
}
var dragBehavior = d3.behavior.drag()
.on("drag", dragChartMove)
.on("dragstart", dragChartStart)
.on("dragend", dragChartEnd);
Actually with different version of d3 you might want to use d3.event.which instead of d3.event.sourceEvent.which to detect which mouse button you clicked.
Related
I've recently passed from d3.v2 to d3.v3, and am trying to understand the differences in the transition mechanisms.
In the code underneath, I'm trying to make a bar graph that, when drawn, has bars that increase in height via a transition. This code works without issue in d3.v2, but in v3, the transition seems to happen "instantly" (the height is immediately set to the end value).
graph.enter()//for each bucket
.append('g')
.attr('transform',function(d,i){ return 'translate('+(xBand(i))+')';})
.attr('width',xBand.rangeBand())
.each(function(data,index){//here we are working on the selection for a single bucket
var $this=d3.select(this); //this refers to the group selection
var currentY=0;
var rects=$this.selectAll('rect')
.data(data.values);
rects.enter()
.insert('rect')
.attr('group-id',me.groupId)
.attr('y',Hats.accessor('y'))
.attr('width',xBand.rangeBand())
.attr('fill',(function(elt){ return me.colors(me.groupId(elt));}));
rects.transition()
.duration(750)
.attr('height',(function(elt){
var h=_.compose(heightScale,me.values)(elt);
d3.select(this).attr('y',currentY);
currentY+=h;
return h;
}));
});
Try setting a starting height in your enter selection:
rects.enter()
.insert('rect')
.attr('group-id',me.groupId)
.attr('y',Hats.accessor('y'))
.attr('width',xBand.rangeBand())
.attr('fill',(function(elt){ return me.colors(me.groupId(elt));}))
.attr('height', 0);
rects.transition()
.duration(750)
.attr('height',(function(elt){
var h=_.compose(heightScale,me.values)(elt);
d3.select(this).attr('y',currentY);
currentY+=h;
return h;
}));
I would like to respond to keystroke events directed at nodes in my force layout. I've tried adding all the variants of "keystroke", "keypress", "keyup", "keydown" that I could think of, but none of them is firing. My mouse events fire just fine. I couldn't find any keystroke events in the d3 source.... is there a way to capture key strokes?
nodes.enter().append("circle")
.on("click", function(d) { return d.clickHandler(self); })
.on("mouseover", function(d) { return d.mouseOverHandler(self); })
.on("mouseout", function(d) { return d.mouseOutHandler(self); })
.on("keyup", function(d) {
console.log("keypress", d3.event); // also tried "keyup", "keydown", "key"
})
.classed("qNode", true)
.call(force.drag);
I think the problem here is you are trying to add keyboard events to elements that are not focusable, try adding a keydown event to a focusable element (body in this case):
d3.select("body")
.on("keydown", function() { ...
here you can use properties of d3.event, for instance d3.event.keyCode, or for more specialized cases, d3.event.altKey, d3.event.ctrlKey, d3.event.shiftKey, etc..
Looking at the KeyboardEvent Documentation might be helpful as well.
I've made a simple fiddle with keyboard interaction here: http://jsfiddle.net/qAHC2/292/
You can extend this to apply these keyboard interactions to svg elements by creating a variable to 'select' the current object:
var currentObject = null;
Then update this current object reference during appropriate mouse event methods:
.on("mouseover", function() {currentObject = this;})
.on("mouseout", function() {currentObject = null;});
Now you can use this current object in your keyboard interactions set up earlier.
here's a jsfiddle of this in action: http://jsfiddle.net/qAHC2/295/
In my d3js app, when the user hovers over a particular circle, I have it enlarge. That is no problem. At the same time, I want to select "all the others" and make them smaller. What is a good query to grab "all the other circles"?
You can use selection.filter or the lesser known functional form of the commonly used selection.select depending on your needs.
If you bind your DOM elements to data using key functions, which is the recommended way, then you can filter on a selection's key: http://jsfiddle.net/9TmXs/
.on('click', function (d) {
// The clicked element returns to its original size
d3.select(this).transition() // ...
var circles = d3.selectAll('svg circle');
// All other elements resize randomly.
circles.filter(function (x) { return d.id != x.id; })
.transition() // ...
});
Another general approach is comparing the DOM elements themselves: http://jsfiddle.net/FDt8S/
.on('click', function (d) {
// The clicked element returns to its original size
d3.select(this).transition() // ..
var self = this;
var circles = d3.selectAll('svg circle');
// All other elements resize randomly.
circles.filter(function (x) { return self != this; })
.transition()
// ...
});
Observed Behavior
I'm using d3.js, and I'm in a situation where I'd like to update some data based on a drag event, and redraw everything after the dragend event. The draggable items also have some click behavior.
Draggable items can only move along the x-axis. When an item is dragged, and the cursor is directly above the draggable item on dragend/mouseup, the item must be clicked twice after it is re-drawn for the click event to fire. When an item is dragged, but dragend/mouseup does not occur directly above the item, the click event fires as expected (on the first try) after the redraw.
Desired Behavior
I'd like the click event to always fire on the first click after dragging, regardless of where the cursor is.
If I replace the click event on the draggable items with a mouseup event, everything works as expected, but click is the event I'd really like to handle.
A Demonstration
Here is a self-contained example: http://jsfiddle.net/RRCyq/2/
And here is the relevant javascript code:
var data, click_count,did_drag;
// this is the data I'd like to render
data = [
{x : 100, y : 150},
{x : 200, y : 250}
];
// these are some elements I'm using for debugging
click_count = d3.select('#click-count');
did_drag = d3.select('#did-drag');
function draw() {
var drag_behavior,dragged = false;
// clear all circles from the svg element
d3.select('#test').selectAll('circle')
.remove();
drag_behavior = d3.behavior.drag()
.origin(Object)
.on("drag", function(d) {
// indicate that dragging has occurred
dragged = true;
// update the data
d.x = d3.event.x;
// update the display
d3.select(this).attr('cx',d.x);
}).on('dragend',function() {
// data has been updated. redraw.
if(dragged) { draw(); }
});
d3.select('#test').selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx',function(d) { return d.x; })
.attr('cy',function(d) { return d.y; })
.attr('r',20)
.on('click',function() {
did_drag.text(dragged.toString());
if(!dragged) {
// increment the click counter
click_count.text(parseInt(click_count.text()) + 1);
}
}).call(drag_behavior);
}
draw();
A little late to the party, buuuut...
The documentations suggests that you use d3.event.defaultPrevented in your click event to know whether or not the element was just dragged. If you combine that with your drag and dragend events, a much cleaner approach is to call the exact function you want when necessary (see when and how flashRect is called):
http://jsfiddle.net/langdonx/fE5gN/
var container,
rect,
dragBehavior,
wasDragged = false;
container = d3.select('svg')
.append('g');
rect = container.append('rect')
.attr('width', 100)
.attr('height', 100);
dragBehavior = d3.behavior.drag()
.on('dragstart', onDragStart)
.on('drag', onDrag)
.on('dragend', onDragEnd);
container
.call(dragBehavior)
.on('click', onClick);
function flashRect() {
rect.attr('fill', 'red').transition().attr('fill', 'black');
}
function onDragStart() {
console.log('onDragStart');
}
function onDrag() {
console.log('onDrag');
var x = (d3.event.sourceEvent.pageX - 50);
container.attr('transform', 'translate(' + x + ')');
wasDragged = true;
}
function onDragEnd() {
if (wasDragged === true) {
console.log('onDragEnd');
// always do this on drag end
flashRect();
}
wasDragged = false;
}
function onClick(d) {
if (d3.event.defaultPrevented === false) {
console.log('onClick');
// only do this on click if we didn't just finish dragging
flashRect();
}
}
I didn't like the global variable, so I made a revision to use data: http://jsfiddle.net/langdonx/fE5gN/1/
After observing that the click required before my svg circles would start responding to click events again could happen anywhere in the document, I settled on a hack whereby I simulate a click event on the document (thanks to https://stackoverflow.com/a/2706236/1015178) after the drag ends. It's ugly, but it works.
Here's the function to simulate an event (again, thanks to https://stackoverflow.com/a/2706236/1015178)
function eventFire(el, etype){
if (el.fireEvent) {
(el.fireEvent('on' + etype));
} else {
var evObj = document.createEvent('Events');
evObj.initEvent(etype, true, false);
el.dispatchEvent(evObj);
}
}
And here's the updated drag behavior:
drag_behavior = d3.behavior.drag()
.origin(Object)
.on("drag", function(d) {
// indicate that dragging has occurred
dragged = true;
// update the data
d.x = d3.event.x;
// update the display
d3.select(this).attr('cx',d.x);
}).on('dragend',function() {
// data has been updated. redraw.
if(dragged) { draw(); }
// simulate a click anywhere, so the svg circles
// will start responding to click events again
eventFire(document,'click');
});
Here's the full working example of my hackish "fix":
http://jsfiddle.net/RRCyq/3/
I want to enable dragging in a d3.js force layout. When dragging a circle and release the mouse button, I want to call a specific function via callback, like this:
this.force = d3.layout.force()
.nodes(this.nodes)
.size([this.width, this.height]);
// enable dragging
this.circle
.call(this.force.drag)
.on("dragend", function() {
console.log("You should see this, when releasing a circle.");
})
.on("mouseup.drag",function(d,i) {
console.log("Or see this.");
});
Unfortunately the event is never fired/consumed completely by the force.drag handler.
So how can I execute a given callback function in a d3 force layout at the end of a drag?
You are not calling the "dragend" event on this.force.drag here.
This also depends on how you have defined this.force.drag.
This should work for you
myCustomDrag = d3.behavior.drag()
.on("dragstart", function(d,i){
//do something when drag has just started
})
.on("drag", function(d,i){
//do something while dragging
})
.on("dragend", function(d,i){
//do something just after drag has ended
});
In the above code, just use call(myCustomDrag) on an element (circle here) on which you want this drag behaviour to be present.