d3.js force cancel a drag event - javascript

I have a simple drag event - and if a certain condition is met, I'd like to force cancel the drag currently under way (basically as if you were doing a mouseup).
Something like so:
var drag_behavior = d3.behavior.drag()
.on("drag", function() {
if(mycondition){
// cancel the drag event
}
});
EDIT:
the goal is simply to prevent people from dragging a world map outside certain boundaries in such a way that renders the map in mid-ocean (details below).
Current map code:
var width = window.innerWidth
var height = window.innerHeight
var projection = d3.geo.mercator()
.scale((width + 1) / 2 / Math.PI)
.translate([width / 2, height/1.5])
.precision(.1);
var path = d3.geo.path()
.projection(projection);
var drag_behavior_map = d3.behavior.drag()
.on("drag", function() {
drag_click = true //used later to prevent dragend to fire off a click event
d3.event.sourceEvent.stopPropagation();
// original idea
// if(worldmap_left_boundary > 0 || worldmap_right_boundary < screen.height){
// cancel or limit the drag here
// }
});
var svg = d3.select("#map").append("svg")
.attr("width", width)
.attr("height", height)
.call(drag_behavior_map)
Basically this should not be possible.

Inside the drag event function, you can use mycondition to decide whether or not to update the dragged element's position. Not updating the position essentially means stopping the drag.
You could also unsubscribe from the drag event:
var drag_behavior = d3.behavior.drag()
.on("drag", function() {
if(mycondition) {
// cancel the drag event
drag_behavior.on("drag", null);
}
});
Note, there would still be a dragend event, unless you unsubscribe that one too.
Alternatively –– though I'm not totally familiar with triggering native events and how that affects d3.behavior.drag() –– you can try triggering a native mouseup event and see what happens.
var drag_behavior = d3.behavior.drag()
.on("drag", function() {
if(mycondition) {
// "this" is the dragged dom element
this.dispatchEvent(new Event('mouseup'))
}
});

Related

Stop dragmove event programmatically in D3 v5

In a Vue/D3 project, I need to set some restrictions on where some draggable elements may be moved.
This is an excerpt from the dragmove handler:
dragmove: function(d, i, n) {
// Stop if the node crosses a border
if (parseInt(n[i].getAttribute('x')) > 200) {
this.drag.dragend();
}
}
this.drag.dragend(); is taken from an older answer on Stackoverflow. Unfortunately, it does not work in D3 v5 (this.drag.dragend is not a function).
This is my drag variable:
drag: d3.drag()
.on('drag', this.dragmove)
.on('end', this.dragended),
Is there a way to update my code to work with more recent versions of D3?
You can use d3.event.on to temporarily override event listeners. So, to programatically interrupt a drag during the drag event itself, we can use:
d3.event.on("drag", null)
d3.event.on("end", null)
This temporarily removes the functions assigned to each event listener.
You'll note I remove the end event too - otherwise it will continue to listen for mouse up regardless of if a function is assigned to the "drag" event listener.
This functionality is described in d3-drag under event.on:
event.on(typenames, [listener])
Equivalent to drag.on, but only applies to the current drag gesture.
Before the drag gesture starts, a copy of the current drag event
listeners is made. This copy is bound to the current drag gesture and
modified by event.on. This is useful for temporary listeners that only
receive events for the current drag gesture. (source)
In the example below, the drag events are temporarily removed from the circle when it hits the line. A custom event is dispatched to indicate that the drag was programatically interrupted. All events are logged - indicating that end, drag, and interrupt events work as expected:
var svg = d3.select("svg");
var drag = d3.drag()
.on("drag", function() {
log(); // to log events as they are triggered.
var selection = d3.select(this);
// Update the circle as normal (but don't let cx exceed the line visually):
selection.attr("cx", d3.event.x > 300 ? 300 : d3.event.x)
.attr("cy", d3.event.y);
// If event.x > 300, interrupt drag:
if(d3.event.x > 300) {
// Disable the drag events temporarily
d3.event.on("drag", null)
d3.event.on("end", null)
// Optionally trigger some alternative event
selection.dispatch("interrupted");
}
})
.on("end", function() {
log();
})
var circle = svg.select("circle")
.call(drag)
.on("interrupted", function() {
d3.select(this)
.transition()
.attr("fill","orange")
.attr("cx",250)
.transition()
.attr("fill","steelblue");
log();
})
function log() {
console.log(d3.event.type);
}
.as-console-wrapper { max-height: 40% !important; }
circle { cursor: pointer ; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="300">
<circle cx="100" cy="50" fill="steelblue" r="10"></circle>
<line x1="305" x2="305" y1="0" y2="400" stroke-width="1" stroke="black"></line>
</svg>

D3 zoom event firing on drag in Angular

Tldr; dragging the SVG causes it to rotate as well as translate.
I am trying to implement dragging and zooming events on an SVG group using D3 (v.4) as part of an Angular service.
this.unitGroup = this.svg.append('g')
.attr('id', 'unitGroup')
.call(this.drag)
.call(this.zoom);
Dragging translates the SVG.
drag = d3.drag()
.on('start', () => {
console.log('drag start');
this.setClickOrigin(d3.event);
})
.on('drag', (d, i, n) => {
const target = d3.select(n[i]).node() as any;
const m = target.getCTM();
const x = d3.event.x - this.clickOrigin.x;
const y = d3.event.y - this.clickOrigin.y;
this.setClickOrigin(d3.event);
this.translate(target, x, y);
});
While zooming rotates the SVG.
zoom = d3.zoom()
.on('zoom', (d, i, n) => {
const target = d3.select(n[i]).node() as any;
const m = target.getCTM();
const b = target.getBBox();
const dir = (d3.event.sourceEvent.deltaY > 0) ? 1 : -1;
this.rotate(target, dir);
});
My original code worked fine. However, integrating it into Angular has thrown up some problems.
The current problem is that when you drag the unitGroup it triggers the zoom event along with the drag event.
The expected behaviour is that:
'click-and-drag' translates the small, dark-grey box in the x and y dimensions.
'mouse-wheel-scroll' rotates the small, dark-grey box around its center.
Here is a Plunker: https://embed.plnkr.co/0GrGG7T79ubpjYa2ChYp/
Actually, what you are seeing here is the expected behaviour.
In D3, d3.zoom() handles not only the zoom but the panning as well. So, the mousemove is being handled by d3.drag() and by the zoom function as well.
As Bostock (D3 creator) once said:
combining these two behaviors* means that gesture interpretation is ambiguous and highly sensitive to position. (*zoom and drag)
Off the top of my head the simplest solution is just checking if you had a "real" zoom (mouse wheel) in the zoom function and, if you didn't (no mouse wheel), return:
if(!d3.event.sourceEvent.deltaY) return;
Here is your plunker with that change only: https://plnkr.co/edit/jz5X4Vm9wIzbKmTQLBAT?p=preview

Running functions after a drag occurs?

I'm trying to run a block of code after a drag occurs in my program. I had thought that the following would work:
//behavior for a dragged point
var drag = d3.behavior.drag()
.origin(function (d) {
return d;
})
.on("drag", dragmove);
function dragmove(d) {
d3.select(this).attr("transform", "translate(" + (d.x = d3.event.x) + "," + (d.y = d3.event.y) + ")");
//events to update line to fit dots
updateXs();
updateLineData();
//update line
d3.select(".myLine").transition()
.attr("d", lineFunc(lineData));
}
But after seeing it run I think that the block is running while the drag is occurring and the object is moving, which may be causing it to not work correctly. What I want to find is the correct method for handling code that should run after a drag completely occurs. If there's a way to make the line update while the point is being dragged, that would be really cool and preferred, but I don't mind having it execute after the drag finishes, either.
Here's the full code:
https://jsfiddle.net/cuhwvj8t/4/
To execute some code once the drag has completed you should use the dragend event. So you'd use:
var drag = d3.behavior.drag()
.on("dragend", function(d) {
// Update lines
});
However you should be able to update the lines during the drag using the drag event which you're already wired up to:
var drag = d3.behavior.drag()
.on("drag", function(d) {
// Update lines
});

Binding the same event to SVG g element and it's child

Using D3, I'm trying to bind a drag event to a 'g', group element, and a separate drag event to a child of this group. This seems to be causing issues as only group's drag event fires.
I've read through the specs a bit but don't see anything relating to this. Here's the code:
var group = that.vis.append('g')
.classed('dragger', true)
.attr('transform', 'translate(100, 0)')
.call(drag.on( 'drag', function() {...} )),
box = group.append('rect')
.attr('width', that.width * options.width)
.attr('height', that.height)
.classed('box', true);
var left = group.append('rect')
.attr('width', 4).attr('x', 0)
.attr('height', that.height)
.classed('drag-extend right', true)
.call(drag2.on('drag', function(){...}));
'that.vis' refers to the d3 selection containing the svg element. The d3 drag behaviors were created like so:
var drag = d3.behavior.drag()
.origin(function() {
var string = d3.select(this).attr('transform'),
//string = string.replace(/translate\(/, '');
array = string.match( /translate\((\d+), (\d+)\)/ );
return {
x : parseInt(array[1]),
y : parseInt(array[2])
}
}),
drag2 = d3.behavior.drag()
.origin(function() {
return {
x : d3.select(this).attr('x'),
y : 0
};
});
I need the elements to be grouped in order to move the whole group. My only thought is that when you attach an event handler to a SVG group it attaches that handler to all elements inside it? I'd like to be able to just stop propagation on the second element's drag handler so it doesn't bubble up to the 'g' parent however the second event doesn't seem to be attached at all.
Kind of at loss here, any help would be much appreciated...
UPDATE
So I was able to get this working but not in a way that I would of expected and I'm still interested in what exactly is going on here:
drag2 = d3.behavior.drag()
.origin(function() {
return {
x : d3.select(this).attr('x'),
y : 0
};
}).on( 'drag', shapeBox );
var left = group.append('rect')
.attr('width', 4).attr('x', 0)
.attr('height', that.height)
.classed('drag-extend right', true)
.call(drag2.on('dragstart', function(){...}));
function shapeBox() { ... }
For some reason adding the 'dragstart' handler somehow got the drag handler attached directly to the behavior to fire. This is strange because in looking at the d3 docs and from my knowledge of the DOM (which may be somewhat limited), I should have just been able to pass the 'drag2' variable to call.
Not sure what binding the extra listener did but somehow that got the drag to work.

How to stop d3.drag triggering mouseover/mouseout events in chrome

I have the following d3 visualisation. The darker colour at the top indicates that a node has been selected. When I mouseover a non selected node it changes opacity so a user can see which node would be selected if I click.
This is achieved via a CSS style sheet and the following js/d3:
nodeSelection.select("circle").on('mouseover', function(e) {
d3.select(this).classed("hover", true);
_this.fireDataEvent("mouseOverNode", this);
});
nodeSelection.select("circle").on('mouseout', function(e) {
d3.select(this).classed("hover", false);
_this.fireDataEvent("mouseOutNode", this);
});
So, far, so good. However, when I drag, the drag function seems to randomly trigger mouse over and mouse out events on the nodes that I am not dragging. This causes the node opacity to flicker. If I watch on the development tools in chrome I can see that this is because it is causing nodes to gain the class "hover". The code above to add this CSS class appears nowhere else, and by use of the console logging, I have confirmed that mouseover and mouseout events are being fired. These nodes are often far from the cursor.
This issue does not occur in Firefox.
UPDATE: I actually managed to fix this almost immediately after posting this. I just explicitly de-register the listeners inside drag start, and re register in drag end. It might still be interesting to some people if they are having similar issues.
My drag function now looks like:
var drag = d3.behavior.drag()
.on("dragstart", function(d) {
console.log("dragstart");
d.dragstart = d3.mouse(this); // store this
d.fixedbeforedrag = d.fixed;
d.fixed=true;
// deregister listeners
nodeSelection.select("circle").on("mouseover", null).on("mouseout", null);
})
.on("drag", function(d) {
d.px = d.x; // previous x
d.py = d.y;
var m = d3.mouse(this);
d.x += m[0] - d.dragstart[0];
d.y += m[1] - d.dragstart[1];
nodeSelection.attr("transform", "translate(" + [d.x, d.y] + ")");
_this.getForce().start();
})
.on("dragend", function(d) {
console.log("dragend");
delete d.dragstart;
d.fixed = d.fixedbeforedrag;
//reregisters listeners
_this.updateSVG();
});

Categories