Canceling Mouseover on Newly Created DOM elements in D3 - javascript

I've created a DOM element which, when it fires a mouseover event, creates new DOM elements such as labels or a tooltip. Unfortunately, sometimes those elements are created underneath the mouse's current position. This causes the mouseleave event of that DOM element to fire, which is often responsible for removing the newly created DOM element.
In other words, when the new elements are created, the mouse is no longer "hovering" over the DOM element that originally fired the event, but the new DOM element. The browser reads this as a "mouseleave" event, which then fires the "mouseleave" function. This event, as it so happens, then removes the new element – even though the user has not moved the mouse.
This causes a cycle to occur, where the new elements are created and removed in rapid-fire, causing the blinking effect when a certain part of the DOM element is moused over. Here's a simplified version of the problem:
https://jsfiddle.net/KingOfCramers/8wbfjap4/2/
var svg = d3.select("svg").style("background-color","grey")
var data = ["This is a circle"]
var circle = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx",100)
.attr("cy",100)
.attr("r", 20)
.style("fill","red")
circle
.on("mouseover",function(d){
var xPos = d3.select(this).attr("cx")
var yPos = d3.select(this).attr("cy")
svg
.append("text").text(d)
.attr("x",xPos)
.attr("y",yPos)
})
.on("mouseleave",function(){
d3.select("text").remove();
})
Obviously this example is silly, but in instances where the data is much more crowded, simply moving the label by 10 or 15 pixels up or down is not a practical solution. I also cannot just create the label relative to the mouse cursor, because I will often be creating many at once using D3 data, for multiple DOM elements. What do most people do to solve this issue?
Thanks.

If you don't need to interact with that new element, just use pointer-events: none;:
.attr("pointer-events", "none")
Here is your code with that change:
var svg = d3.select("svg").style("background-color", "grey")
var data = ["This is a circle"]
var circle = svg.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", 100)
.attr("cy", 100)
.attr("r", 20)
.style("fill", "red")
circle
.on("mouseover", function(d) {
var xPos = d3.select(this).attr("cx")
var yPos = d3.select(this).attr("cy")
svg
.append("text").text(d)
.attr("x", xPos)
.attr("y", yPos)
.attr("pointer-events", "none")
})
.on("mouseleave", function() {
d3.select("text").remove();
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg width=500 height=500></svg>

Related

Adding a correctly sized rectangle behind text

I am currently appending text nodes to an SVG as follows:
var node = svg.selectAll(...);
node.append("text")
.attr("dy", 10)
.attr("dx", 4)
.text(function (d) { return d["name"]; });
There are currently about 10 nodes, each with a name.
I want to add a rectangle below each of the text nodes, using the correct width, I have tried this:
node.select<SVGTSpanElement>("text")
.each(function(x, i) {
console.log(this.getComputedTextLength());
node.append("rect")
.attr("fill", "#cccccc05")
.attr("width", this.getComputedTextLength())
.attr("height", 20)
});
My problem is that (kind of obviously) I am creating 10 rectangles per node, not one for each node.
How do I include the calculation for text width, and add a single rectangle per text element?
Without refactoring your code too much, simply change where you're appending the rectangles. In this case, the parent node of the texts themselves:
node.select<SVGTSpanElement>("text")
.each(function(x, i) {
console.log(this.getComputedTextLength());
d3.select(this.parentNode).append("rect")
.attr("fill", "#cccccc05")
.attr("width", this.getComputedTextLength())
.attr("height", 20)
});
However, the most idiomatic way is using each() for the node selection, not for the selection of texts inside it. Then, you get the text length for each node element, something like this:
node.each(function(x, i) {
d3.select(this).append("rect")
.attr("fill", "#cccccc05")
.attr("width", d3.select(this).select("text").node().getComputedTextLength())
.attr("height", 20)
});

How to use jQuery to add event to static d3 elements

I want to add click event to a d3 circle element that is static. I dynamically loaded this d3 code:
var svgContainer = d3.select("#vis").append("svg")
.attr("width", 200)
.attr("height", 200);
//Draw the Circle
var circle = svgContainer.append("circle")
.attr("class","item")
.attr("cx", 30)
.attr("cy", 30)
.attr("r", 20);
Somewhere in my code, I want to add the click event to circles that contain a class called "item". I am using following code but it does not work
$('.item').on('click', function(){
console.log("click added");
});
I looked at this post as well but it did not help me.

Assign mouse click handler to line in d3 with snapping behaviour

I have a d3 line that is only one pixel wide. I want to have a on-click
handler assigned to this line. However it is very hard to be exactly on top of the line when trying to select it. Is there some "selection corridor" feature that lets you specify a "distance to line" instead. I.e. the click event is fired if the mouse pointer is, say, in a distance of max 8pt away from a line and the mouse is clicked?
The solution in the other answer is a good solution and an interesting one.
However, I prefer the "traditional" solution, which is painting another line, transparent and thicker than the visible thin line, just to catch the click event. And the reason I prefer this "traditional" solution (the most frequent one among D3 coders) is UX: it's a good idea letting the user know that she/he can click the line.
Thus, the advantage of having a thicker, transparent line over the thin, visible line is that you can set the cursor to a hand...
.attr("cursor", "pointer")
... when the user hover over the transparent line.
Here is a simple demo:
var svg = d3.select("svg");
var data = d3.range(30).map(function(d) {
return {
x: d * 10,
y: Math.random() * 150
}
});
var lineGenerator = d3.line()
.x(function(d) {
return d.x
})
.y(function(d) {
return d.y
});
var realLine = svg.append("path")
.attr("stroke", "teal")
.attr("fill", "none")
.attr("stroke-width", 1)
.attr("d", lineGenerator(data));
var transparentLine = svg.append("path")
.attr("stroke", "teal")
.attr("fill", "none")
.attr("stroke-width", 12)
.attr("opacity", 0)
.attr("cursor", "pointer")
.attr("d", lineGenerator(data));
transparentLine.on("click", function() {
console.log("clicked")
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Of course, this "traditional" solution only makes sense if you have one or few lines on that chart. If you have dozens or hundreds of lines, it will be a complete mess.
You can attack click event on svg/group element and check if the distance from the mouse position and the closest point on the path is less than some fixed value, e.g. 4. The distance can be calculated using the function from Closest Point on Path example.
Click event:
var path = svg.append('path').datum(data).attr("d", line);
svg.on('click', function() {
var m = d3.mouse(this),
p = closestPoint(path.node(), m);
if (p.distance < 4) {
/* make action */
console.log('click', p.distance)
}
})
closestPoint() is taken from the example above.
example: https://jsfiddle.net/xny9bx4v/

D3: Surround mouse point with "Crosshair"

I'm trying to surround the mouse point with a circle, much like a crosshair, and have this circle track the mouses movement. So far the best strategy I have is using D3 enter-update-exit:
Append circle on mouse point underpinned by data.
on mouse move add another circle to data array with data = new mouse point.
if data array exceeds 1, shift() the first value out.
update visualisation.
jsfiddle here - http://jsfiddle.net/hiwilson1/kur2bbv9/1/ - though I think it's largely irrelevent as this strategy is fundamentally flawed. The circle appears as though it's lagging behind the cursor and flickers. A lot. Which I don't want.
Key part of code here:
function onMove() {
var m = d3.mouse(this);
var point = {x: m[0], y: m[1]};
area.push(point)
document.getElementById("svg").onmousedown = function() {
mouseDown++;
addNode(m);
};
document.getElementById("svg").onmouseup = function() {
mouseDown--;
};
if (mouseDown > 0) {
addNode(m);
}
//if theres two circles, remove the first leaving just the second.
if (area.length > 1) {
area.shift();
}
var crosshair = svg.selectAll(".area")
.data([area])
crosshair
.attr("class", "area")
.attr("cx", m[0])
.attr("cy", m[1])
.attr("fill", "none")
.attr("stroke", "grey")
.attr("stroke-width", "3px")
.attr("r", 30)
crosshair.enter()
.append("circle")
.attr("class", "area")
.attr("cx", m[0])
.attr("cy", m[1])
.attr("fill", "none")
.attr("stroke", "grey")
.attr("stroke-width", "3px")
.attr("r", 30)
crosshair.exit().remove()
};
Is there another way of accomplishing this? Happy to accept non D3 strategies.
I couldn't get your JSfiddle to display anything, so I'm not sure if I'm totally missing the point, but could you just use a custom CSS cursor on top of your SVG element? It seems that .cur cursor files have the most wide-spread support. That would be a native alternative for custom hacks (thus giving better performance), and it would also degradate gracefully on un-supported browsers.

d3 mouseover on element conflicts with svg mousemove

I have attached a mouseover event to an element - say, a circle - within the SVG element. I also need a "mousemove" event handler associated with the SVG element/"background" itself. However, they seem to conflict: when mousing over the circle, the handler attached to the circle does not supersede that associated with the SVG element itself.
How do I get the circle's mouseover to supersede the SVG element's event handler? I need them both, but only want the mouseover to be triggered over the circle and the mousemove to be triggered by movement anywhere else in the SVG element.
A simplified example can be seen in this JSFiddle: http://jsfiddle.net/aD8x2/ (JS code below). If you click on a circle (starting a line) and then mouse over another circle, you will see the flickering of color associated with both events being triggered when mousing over the circle.
var svg = d3.select("div#design")
.append("svg")
.attr("width", "500").attr("height", "500");
svg.selectAll("circle").data([100, 300]).enter().append("circle")
.attr("cx", function(d) { return d; })
.attr("cy", function(d) { return d; })
.attr("r", 30)
.on("mouseover", function () {
d3.select(this).attr("fill", "red");
})
.on("mouseout", function() {
d3.select(this).attr("fill", "black");
})
.on("click", function() {
svg.append("line")
.attr(
{
"x1": d3.select(this).attr("cx"),
"y1": d3.select(this).attr("cy"),
"x2": d3.select(this).attr("cx"),
"y2": d3.select(this).attr("cy")
})
.style("stroke-width", "10")
.style("stroke", "rgb(255,0,0)");
});
svg.on("mousemove", function() {
var m = d3.mouse(this);
svg.selectAll("line")
.attr("x2", m[0])
.attr("y2", m[1]);
});
In your case, it is actually the line causing the problem and not the SVG. That is, you're moving the mouse over the line you're drawing and thus a mouseout event is triggered for the circle.
You can prevent this by setting pointer-events to none for the line so it's "transparent" with respect to mouse events. Modified example here.

Categories