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

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.

Related

Parameters needed for d3.mouse()

I am trying to get mouse coordinates relative to a group element. Here's the code:
var backRect = chart.append("g").attr("class", "rect");
var g = backRect.append('rect')
.style('stroke', 'none')
.style('fill', '#FFF')
.style('fill-opacity', 0)
.attr({
width: width,
height: height,
'pointer-events': 'none',
'class': 'backRect'
});
// the code below is inside another container's event; but I want mouse coordinates relative to the above rect, hence can't use d3.mouse(this)
// get mouse pointer location
var coordinates = [0, 0];
coordinates = d3.mouse(backRect); // d3.select(".rect") does not work either
but get the following error:
d3.min.js:1 Uncaught TypeError: n.getBoundingClientRect is not a function
According to the d3 mouse docs d3.mouse() takes a container which can be svg or g element.
What parameter should I pass to d3.mouse()? I tried d3.select(".rect") which is not working either.
Using d3.mouse(backRect.node()) did the trick.
You should be using d3.mouse() inside an event to get the values relative to the passed container.
Check this block
http://bl.ocks.org/hlucasfranca/f133da4493553963e710
svg.on("click", function() {
var coords = d3.mouse(this);
........
........
})

AngularJS DOM modification deletes events -> need viable workaround implementation schema

Could someone explain to me the right way to create custom event listeners in AngularJS that resist DOM modifications?
My problem is that I need to plot 2 different datasets according to a dropdown menu. Each element of the datasets has to respond to an event (mouseover/mousemove etc...). But when switching from one dataset to another, the event listener is lost.
This behavior is well explained here:
Injecting HTML into the DOM destroys the AngularJS events
but I do not understand the answer given to bypass it. The post mentions that manually avoiding ng-app can help but it seems like a bad workaround.
I would rather redefine my own event listeners such that they would not be lost during DOM modification. Can someone tell me how to correctly use addEventListener in the directive/controller model?
Here is an SSCCE html/javascript file that illustrates how my program is organized
angular.module("testUpdate", [])
.directive("shape", function($parse) {
return {
restrict: "E",
replace: false,
scope: false,
link: function(scope, element, attrs) {
// draw dataset according to #shapeSelection choice
d3.select("#shapeSelection")
.on("change", scope.redraw);
// change color on mouseover
d3.selectAll("circle,rect,polygon")
.on("mouseover", function() {
d3.select(this).style("fill", "#0f0");
});
}
};
})
.controller("testController", function($scope) {
$scope.shape = "none";
//create datasets
$scope.dataset1 = [{
cx: 10,
cy: 20
}, {
cx: 30,
cy: 20
}];
$scope.dataset2 = [{
w: 20,
h: 20
}];
// the triangle actually responds to the event
d3.selectAll("svg")
.append("polygon")
.attr("points", "15,20 0,40 30,40")
.style("fill", "#f00");
// draw dataset depending on the user choice
$scope.draw = function() {
if ($scope.shape === "circle") {
d3.selectAll("svg").selectAll("circle")
.data($scope.dataset1).enter()
.append("circle")
.attr("cx", function(d) { return d.cx; })
.attr("cy", function(d) { return d.cy; })
.attr("r", 10)
.style("fill", "#f00");
}
else if ($scope.shape === "rect") {
d3.selectAll("svg").selectAll("rect")
.data($scope.dataset2).enter()
.append("rect")
.attr("width", function(d) { return d.w; })
.attr("height", function(d) { return d.h; })
.attr("x", 0)
.attr("y", 10)
.style("fill", "#f00");
}
else {}
}
// once removed is called, the event listener is destroyed
$scope.redraw = function() {
d3.selectAll("circle,rect,polygon").remove();
$scope.draw()
}
});
<script src="https://code.angularjs.org/1.2.15/angular.js"></script>
<script src="http://d3js.org/d3.v3.min.js"></script>
<div ng-app="testUpdate" ng-controller="testController">
<select name="shapeSelector" ng-model="shape" id="shapeSelection">
<option value="none">--none--</option>
<option value="circle">dataset1</option>
<option value="rect">dataset2</option>
</select>
<shape id="shapeTag">
<svg id="shapeSVG" display="block"></svg>
</shape>
</div>
The d3.selection.remove() function modifies the DOM and nullifies the event listeners so that the triangle responds correctly but not the circles and rectangle.
A dirty workaround would be to create the DOM for both datasets and set the visibility of the non-selected one to hidden, but both datasets are pretty big so it is not a viable option.
Thank you in advance
From the code, it appears that you only want to have the mouseover event captured on the elements inside the shape directive.
I think the best solution for you would be to move the code which binds the event handler to the elements into $scope.redraw or the $scope.draw function:
$scope.draw = function() {
if ($scope.shape === "circle") {
/* ... */
} else {}
// change color on mouseover
d3.selectAll("circle,rect,polygon")
.on("mouseover", function() {
d3.select(this).style("fill", "#0f0");
});
}
While you are manipulating the DOM in D3-land, you should not rely on event-handlers defined in Angular's link function. In the directives I have written, I merely used $watch in the directives to trigger the redraw function when the data updated, just as you are doing with the change function.
Alternate solution
On the other hand, if you want to handle the events in the directive (e.g. if you do not have to pass d3 the $scope), and if you are using jQuery, then you can use the .on method on a parent with proper selectors. Note that jQlite (Angular's internal implementation of jQuery), does not supposed .on with selectors.
In this case, the event handler will be called on the parent even if you change the DOM underneath. If you want access to the data associated with the node inside the event handler, you can use var data = d3.select(this).data().

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();
});

d3.js force cancel a drag event

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'))
}
});

D3 SVG I want to highlight an array of elements by mouse dragging

I have an SVG container with a number of elements each containing a rect and some text elements in a horizontal array.
I want to click on one element and drag to one of the others. The first element and element dragged over should highlight - as it would in a normal text editor selection.
at the end I want to know the first and last elements selected so that I can access their data.
I tried this using d3 drag behaviour but:
1. the intermediate elements can't be highlighted
2. the dragend does not tell me which element was the final one.
Also tried using mouse events but:
1. I can highlight each intermediate item but not easily remove the highlight if the mouse is moved back towards the beginning.
2. if the mouse is moved out of the container I can miss the mouse up events leaving highlighted elements.
3. I still don't really know upon which element I am finishing unless I collect all the mouse over events.
I don't actually want to move the selected elements - just know the first and last one selected.
I created this fiddle to illustrate the problem: http://jsfiddle.net/avowkind/up54b/5/
HTML
<svg class='demosvg' version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" >
<rect width="400" height="220"></rect>
<g id="container" transform="translate(10,10)"></g>
</svg>
Javascript
var mydata = [1, 2, 3, 4, 5];
/* each box is a group with a rect and text
positioned according to the data value
on drag/drop we want to know the dragged element, and the one it is dropped on.
we would like to highlight all the intervening elements too.
Ultimately we just want a result e.g. 2 was dragged to 5
*/
var boxg = d3.select('#container').selectAll('g').data(mydata).enter()
.append('svg:g')
.attr('id', function (d, i) {
return "b" + d;
})
.attr('transform', function (d, i) { return "translate(" + d * 50 + ",80)";})
.call(d3.behavior.drag()
.on("dragstart", function (d, i) {d3.select(this).classed("selected", true); })
.on("drag", function (d, i) {
// which element are we over here - to highlight it
var el = document.elementFromPoint(d3.event.x, d3.event.y);
console.log(el);
})
.on("dragend", function (d, i) {
console.log(d);
console.log(this);
console.log(d3.event);
// turn off all highlights
d3.selectAll('#container g').classed("selected", false);
// Which box got dropped on here ?
})
);
boxg.append('svg:rect')
.classed('box', true)
.attr('width', 40).attr('height', 40);
boxg.append('svg:text')
.text(function(d,i) { return d; })
.attr('dx', 15).attr('dy', 20);
CSS
.demosvg { fill:silver;}
.box { fill:lightblue;}
text { fill:black;}
.selected .box{ fill:gold;}
Thanks Andrew

Categories