Block d3 .transition() until another transition has completed - javascript

I have a hover effect implemented in d3 that selects several rectangles in an svg and changes their color:
var rect = d3.selectAll('.rect')
rect.transition()
.duration(1000)
.style('fill', red')
I only want this effect to be operating on one rectangle at a time. If I quickly hover over several rectangle is close succession, the effect is triggered on all the rectangles I've hovered over. How can I block other mouse hover events if there is currently another event being triggered?

You can have a global variable that acts as a semaphore:
var transitioning = false;
rect.append("...")
.on("mouseover", hover);
function hover() {
if(!transitioning) {
transitioning = true;
rect.transition()
.duration(1000)
.style('fill', 'red')
.each("end", function() { transitioning = false; });
}
}
Note that this assumes that all your transitions have the same duration and delay.

Related

D3.js tooltip reappearing after hovering over the hidden tooltip?

How would one use D3.js to prevent this from happening?
The issue is when a user hovers over the point they are shown the tooltip information. But if they just happen to hover over the area where the tooltip used to be visible, it pops back up.
Its not that big of a deal in this example. But if I were to add more information or if the tooltip area were any larger this could get hairy.
http://jsfiddle.net/hx8pjwdu/9/
.on('mouseover', function(d) {
d3.select(".d3-tip").transition().style("opacity", "1");
tip.show(d);
})
.on('mouseout', function(d) {
d3.select(".d3-tip").transition().duration(1000).style("opacity", "0").each("end", tip.hide);
});
d3.select(".d3-tip").on('mouseover', function(d) {
d3.select(".d3-tip").transition().style("opacity", "1");
}).on('mouseout', function(d) {
d3.select(".d3-tip").transition().duration(1000).style("opacity", "0").each("end", tip.hide);
});
You need to omit the final paragraph.
d3.select(".d3-tip").on('mouseover', function(d) {
d3.select(".d3-tip").transition().style("opacity", "1");
}).on('mouseout', function(d) {
d3.select(".d3-tip").transition().duration(200).style("opacity", "0").each("end", tip.hide).disable();
});
This is attaching a mouseover event to the tool tip area hence the reason it is showing. Simple!
http://jsfiddle.net/1ab435px/

Selecting path, changing opacity based on legend mouseover

This, I think, is basically a selection question.
Here's my semi-working Plunker.
I'm trying to select a path, and change the opacity of all paths within a chart that are not selected, based on a mouseover of the respective graphic element (a circle) in the chart's legend.
I've set the id of the paths such they'll have the same id as the circles that are activated on mouseover. I've also gotten the circles that are not selected on hover to change opacity. (Currently, however, all of the non-selected circles, in all of the legends, across all of the charts, change opacity. I'm trying to limit mouseover opacity changes to only the relevant chart.)
What I'm trying to achieve:
When I mouseover a circle in the legend of a given chart, the same opacity changes should be applied to the paths of that chart, as if I had hovered over the paths themselves. If I understand my problem correctly, I'm having trouble defining the selection/non-selection of the circles and their respective paths, and limiting those selections to only one chart out of several on the page.
Here's how the paths' groups and ids are defined:
var pathGroup = main.append('g').classed('paths', true);
var paths = pathGroup.selectAll("path")
.data(dataset)
.enter()
.append("path")
.attr("id", function(d) {
return d.record
})
.attr("data-legend", function(d) {
return d.record
})
And here's the problematic code, I think:
li.selectAll("circle")
.attr("id",function (d) {return d.key})
.style("fill",function(d) { return d.value.color})
.on("mouseover", function(d) {
// need to define "circleGroup" and "circles" (as is done for "pathGroup" and "paths") so that the legend's non-selected circles are the ones that fade)
// also need to find a way of limiting "circles" to a circle group within only that state's chart
// circles
d3.selectAll('circle:not(this)')
.style('opacity', 0.4)
.style('transition', "opacity 0.1s")
d3.select(this)
.classed('hover', true)
.style('opacity', 0.9)
.style('transition', "opacity 0.1s")
d3.select('path:not(this)')
.style('opacity', 0.4)
.style('transition', "opacity 0.1s")
// d3.select('path data-legend', function(d) { return d.key})
d3.select('path id', function(d) { return d.key})
.classed('hover', true)
.style('opacity', 0.9)
.style('transition', "opacity 0.1s")
})
.on('mouseout', function(d) {
d3.selectAll('circle')
.style('opacity', 0.9)
.style('transition', "opacity 0.1s")
})
Here, again, is my semi-working Plunker: http://plnkr.co/edit/mvdqBPMymCt9VAKAPKD1?p=preview
In advance, thanks for any help you can offer in setting this right.
Issues with your code:
d3.selectAll('circle')
selects all the circles in the body and as far as the paths are concerned:
d3.select('path id') wouldn't work as the selector itself is messed up here. Try console logging the selection here.
Option 1:
Try replacing the legend mouse events with the following code:
.on("mouseover", function(d) {
// look for all the circles within the parent node
d3.select(this.parentNode).selectAll('circle').style('opacity', 0.4);
// change opacity of current circle
d3.select(this).style('opacity', 0.9);
// use parentNode to go until SVG and select all paths
d3.select(this.parentNode.parentNode.parentNode).select('g.paths').selectAll('path').style('opacity', 0.4);
// change opacity of path with data-legend = key
d3.select(this.parentNode.parentNode.parentNode).select('g.paths').selectAll('path[data-legend="'+d.key+'"]').style('opacity', 0.9);
})
.on('mouseout', function(d) {
// change all circles' and paths' opacity back to original values
d3.select(this.parentNode).selectAll('circle').style('opacity', 0.9);
d3.select(this.parentNode.parentNode.parentNode).select('g.paths').selectAll('path').style('opacity', 0.9);
});
I hope the comments are clear enough to understand the code. Just parsing through the parentNodes.
Option 2:
Add a class/id to the legend group representing the "state" i.e. Alabama, California etc.
And search for the SVG with selectedState on every mouseover and change the paths' opacity.
Hope this helps. :)

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/

Hiding visible elements on click D3.js

I am using D3.js to dynamically create svg elements. When I click a circle, I run this function:
.on("click", function(d) {
d3.select(this).select("rect").transition().duration(900).style("visibility", "visible");
d3.select(this).selectAll("tspan").transition().duration(900).style("visibility", "visible");
})
Aside from the fact that my transitions aren't working, clicking on the circle shows that circles children rectangle and tspan, all is well. However if I click another circle, the new rectangle and tspan show but I need the current one to hide. Wondering what the best/most efficient way to do this is with D3
If your circles have a class, say ".circle", you can do something like this:
.on("click", function(d) {
var clickedCircle = this;
d3.selectAll(".circle").each(function() {
var currCircle = this;
d3.select(this).select("rect").transition().duration(900).style("visibility", function() {
return (currCircle === clickedCircle) ? "visible" : "hidden";
});
});
});
And obviously do the same for your tspan element. This will hide all but your currently clicked circle.

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