D3 onclick, onmouseover and mouseout behavior overriding - javascript

I have a D3 multi-line graph that uses the legend for onclick mouseover and mouseout events. Clicking on the legend will hide the line. Mousing over the legend will make the line bold and mousing out will put the line back to normal.
The problem is that if I click the legend and then remove the mouse before the D3 transition completes the transition will not finish. If I keep the mouse over the legend long enough for the transition everything works fine.
To test click a legend rectangle and quickly move the mouse out - the line will not disappear.
Fiddle here: https://jsfiddle.net/goodspeedj/5ewLxpre/
The code for the mouse events is below:
.on("click", function(d) {
var selectedPath = svg.select("path." + d.key);
//var totalLength = selectedPath.node().getTotalLength();
if (d.visible === 1) {
d.visible = 0;
} else {
d.visible = 1;
}
rescaleY();
updateLines();
updateCircles();
svg.select("rect." + d.key).transition().duration(500)
.attr("fill", function(d) {
if (d.visible === 1) {
return color(d.key);
} else {
return "white";
}
})
svg.select("path." + d.key).transition().duration(500)
.delay(150)
.style("display", function(d) {
if(d.visible === 1) {
return "inline";
}
else return "none";
})
.attr("d", function(d) {
return line(d.values);
});
svg.selectAll("circle." + d.key).transition().duration(500)
//.delay(function(d, i) { return i * 10; })
.style("display", function(a) {
if(d.visible === 1) {
return "inline";
}
else return "none";
});
})
.on("mouseover", function(d) {
d3.select(this)
.attr("height", 12)
.attr("width", 27)
d3.select("path." + d.key).transition().duration(200)
.style("stroke-width", "4px");
d3.selectAll("circle." + d.key).transition().duration(200)
.attr("r", function(d, i) { return 4 })
// Fade out the other lines
var otherlines = $(".line").not("path." + d.key);
d3.selectAll(otherlines).transition().duration(200)
.style("opacity", 0.3)
.style("stroke-width", 1.5)
.style("stroke", "gray");
var othercircles = $("circle").not("circle." + d.key);
d3.selectAll(othercircles).transition().duration(200)
.style("opacity", 0.3)
.style("stroke", "gray");
})
.on("mouseout", function(d) {
d3.select(this)
.attr("height", 10)
.attr("width", 25)
d3.select("path." + d.key).transition().duration(200)
.style("stroke-width", "1.5px");
d3.selectAll("circle." + d.key).transition().duration(200)
.attr("r", function(d, i) { return 2 })
// Make the other lines normal again
var otherlines = $('.line').not("path." + d.key);
d3.selectAll(otherlines).transition().duration(100)
.style("opacity", 1)
.style("stroke-width", 1.5)
.style("stroke", function(d) { return color(d.key); });
var othercircles = $("circle").not("circle." + d.key);
d3.selectAll(othercircles).transition().duration(200)
.style("opacity", 1)
.style("stroke", function(d) { return color(dimKey(d)); });
});
Thanks in advance.

You could assign a class to your legend when it's clicked (.clicked), then call setTimeout with an appropriate delay to remove that class once the transition is complete.
When you mouseover or mouseout, first check to see if the legend has the .clicked class. If so, set some delay value as suggested in the other answer, otherwise, proceed without a delay. The advantage of this compared to the other answer is that there would only be a delay if it's needed.
EDIT
If your legend has the class ".legend", modify your code as follows:
.on("click", function(d) {
// Add .clicked class to the legend
$('.legend').addClass('clicked');
// remove clicked class after 750ms. Your duration is 500ms,
// so I'm padding it a bit although you can adjust this as needed
setTimeout(function () { $('.legend').removeClass('clicked') }, 750);
... rest of your function
})
.on("mouseover", function(d) {
// check if legend has been clicked recently and change delay if so
var transitionDelay = 0;
if($('.legend').hasClass('clicked')) transitionDelay = 750;
// your function
d3.select(this)
.attr("height", 12)
.attr("width", 27)
d3.select("path." + d.key).transition().delay(transitionDelay).duration(200)
.style("stroke-width", "1.5px");
... rest of your function
});

When you have multiple transitions, one can interrupt another. What is happening with your code is that the onclick transition get interrupted by the mouseout transition. This results in the lines not showing showing up. To fix this, just add delay to your mouseout event, so that it occurs after the onclick event has completed. For example, I made the following changes:
added a delay to line 295:
d3.select("path." + d.key).transition().delay(300).duration(200)
.style("stroke-width", "1.5px");
and on line 244 reduced your onclick delay to 200 from 500, just for this test,
svg.select("path." + d.key).transition().duration(200)
.delay(150)

Related

Change style after a d3 path has been clicked

How can I stop the path style being changed if the path has been clicked? I want path style to not be changed after pathHover has been clicked.
let pathHover = this.svg.append('path')
.data([data])
.attr('class', 'line-padded')
.attr('d', line);
pathHover.on('mouseover', function() {
console.log('path mouse over');
path.style('stroke-width', 6);
});
pathHover.on('mouseleave', function() {
path.style('stroke-width', 4);
});
pathHover.on('click', function() {
console.log('clicked');
path.style('stroke', 'blue');
path.style('stroke-width', 6);
});
There are different ways to achieve this. Since the first D in DDD (also known as D3) means data, the approach I like most is binding a datum to the clicked element, indicating that it was clicked:
d.clicked = true;
Or, if you want to reverse the boolean after a second click:
d.clicked = !d.clicked;
Then, in the mouseover, just check that datum:
if (d.clicked) return;
Here is a demo using green circles: if you mouse over them, they turn red. If you click them, they turn blue, and never turn red (or green) again.
var svg = d3.select("svg");
var circles = svg.selectAll(null)
.data(d3.range(5).map(function(d) {
return {
x: d
}
}))
.enter()
.append("circle")
.attr("cursor", "pointer")
.attr("cy", 75)
.attr("cx", d => 30 + 50 * d.x)
.attr("r", 20)
.style("fill", "lime");
circles.on("mouseover", function(d) {
if (d.clicked) return;
d3.select(this).style("fill", "firebrick")
}).on("mouseout", function(d) {
if (d.clicked) return;
d3.select(this).style("fill", "lime")
}).on("click", function(d) {
d.clicked = !d.clicked;
d3.select(this).style("fill", "blue")
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
Probably a few tactics, either
set path.style("pointer-events", "none") for clicked paths, which will stop all future clicks/mouseover events.
Or if that is too drastic, add a class to clicked paths path.classed("clicked", true), which you could use in a test during your mouseover event before applying any styling changes

d3 version 4, I'm trying to make a mouseover conditional based on class

I am trying to make this mouseover conditional. if the "selected" class exists then return the mouseover function
This is the code:
.on("mouseover", function() { if(d3.select(this).classed("selected")) {
return ""}
else{
return mouseover}
})
Thanks in advance
It seems to me that you want to call the mouseover function depending on the class. If that is correct, you just need to do:
.on("mouseover", function() {
if (d3.select(this).classed("selected")) {
mouseover()
}
})
Or, alternatively:
selection.selectAll(".selected").on("mouseover", mouseover)
Here is a demo. The first circle doesn't have the class, the second one does:
var svg = d3.select("svg");
svg.selectAll(null)
.data([1, 1])
.enter()
.append("circle")
.attr("cy", 50)
.attr("r", 20)
.attr("cx", function(d, i) {
return 100 + 100 * i
})
.classed("selected", function(d, i) {
return i
})
.on("mouseover", function() {
if (d3.select(this).classed("selected")) {
mouseover()
}
})
function mouseover() {
console.log("mouse over")
}
<script src="//d3js.org/d3.v4.min.js"></script>
<svg></svg>

D3 js selectAll each doesn't iterate

I am trying to implement the FishEye lens (Cartesian) in my scatterplot.
I am trying to follow this approach, but apparently my selector already fails.
I have my FishEye defined as
var fisheye = d3.fisheye.circular().radius(120);
svg.on("mousemove", function() {
fisheye.focus(d3.mouse(this));
console.log("here " + points.selectAll("circle").length);
points.selectAll("circle").each(function(d) {
console.log("aaa");
d.fisheye = fisheye(d);
/*points.selectAll("circle")
.each(function(d) {
console.log("???");
this.attr("cx", function(d) { return d.fisheye.x; })
this.attr("cy", function(d) { return d.fisheye.y; })
this.attr("r", function(d) { console.log("hype"); return 10; });
}); */
});
});
and my points is defined as
points = svg.append("g")
.attr("class", "point")
.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) { // Set the x position using the x-scale
return x(d.x);
})
.attr("cy", function(d) { // Set the y position using the y-scale
return y(d.y);
})
.attr("r", 5) // Set the radius of every point to 5
.on("mouseover", function(d) { // On mouse over show and set the tooltip
if(!isBrushing){
tooltip.transition()
.duration(200)
.style("opacity", 0.9);
tooltip.html(d.symbol + "<br/> (" + parseFloat(x(d.x)).toFixed(4)
+ ", " + parseFloat(y(d.y)).toFixed(4) + ")")
.style("left", (d3.event.pageX + 5) + "px")
.style("top", (d3.event.pageY - 28) + "px");
}
})
.on("mouseout", function(d) { // on mouseout, hide the tooltip.
tooltip.transition()
.duration(500)
.style("opacity", 0);
});
The console.log with "here" is spamming when I am moving the mouse, and shows the correct amount. Hwoever, the each loop is never executed as I do not see "aaa". I have also tried to just use selectAll("circle") but that doesn't work either.
What am I doing wrong and how can I get my FishEye to work?

D3 enable mouseover AFTER transition

How can I add a d3-tip / mouseover event AFTER a transition on a histogram / bar chart?
I create a bar chart / plot:
canvas.call(tip);
var sampleBars = canvas.selectAll(".sampleBar")
.data(data)
.enter().insert("rect", ".axis")
.attr("class", "sampleBar")
.attr("x", function(d) { return x(d.x) + 1; })
.attr("y", function(d) { return y(d.y); })
.attr("width", x(data[0].dx + data[0].x) - x(data[0].x) - 1)
.attr("height", 0)
.transition()
.duration(2500)
.delay(500)
.attr("height", function(d) { return height - y(d.y); });
I want to add:
sampleBars
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
And this is the tip:
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<span style='color:white'>" + d3.round((d.y * 100))
+ "%" + "</span>" + " infected"; })
So that the mouseover event occurs after the transition is complete. But I get the error:
Uncaught TypeError: undefined is not a function
The error is for the line:
.on('mouseover', tip.show)
I think there is a simple flaw in my logic. I seem to be confused about how these events or attributes interact with each other.
Again: I want to 'activate' the mouseover tip AFTER the transition is complete so that after the transition does its thing the tip will appear if the user puts their mouse over each bar. I have no problem creating the mouseover event and having it work on user mouseover to display the data I want, but I am having no luck with making this work with a transition of those bars.
Instead of adding/removing events, one approach is to simply set/unset the pointer-events attribute so that the events don't fire when you want them suppressed.
var myselection = d3.select('body')
.append('svg').attr({height: 200, width: 200})
.append('circle')
.attr( {cx: 100, cy: 100, r: 0})
.attr('pointer-events', 'none')
.on('mouseover', function() { console.log('mouseover'); })
.on('mouseout', function() { console.log('mouseout'); })
myselection
.transition().duration(4000).attr('r', 100)
.transition().attr('pointer-events', 'auto')
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
If you have the console window open you'll notice that mouse over/out logs nothing until the circle stops growing.

Interactive Legend onclick or mouseover - D3js

I've been looking for a way to have my legend control my chart animation (similar to NVD3 examples). I've run into a problem though - nested selections.
var legend = svg.append("g")
.attr("class", "legend")
.attr("transform", "translate(70,10)")
;
var legendRect = legend.selectAll('rect').data(colors);
legendRect.enter()
.append("rect")
.attr("x", w - 65)
.attr("width", 10)
.attr("height", 10)
.attr("y", function(d, i) {
return i * 20;
})
.style("stroke", function(d) {
return d[1];
})
.style("fill", function(d) {
return d[1];
});
I'm using a bit of a hack to do my animation. Basically setting style to display: none.
I want to be able to click on the rectangles and call the function. But putting a mouseover or onclick within legendRect doesn't work. The bars to animate are not children of the legend. How can I call the function, or chain my function to my legend?
function updateBars(opts) {
var gbars = svg.selectAll("rect.global");
var lbars = svg.selectAll("rect.local");
if (opts === "global") {
gbars.style("display", "block") ;
lbars.style("display", "none");
gbars.transition()
.duration(500)
.attr("width", xScale.rangeBand());
};
if (opts === "local") {
lbars.style("display", "block")
;
gbars.style("display", "none");
lbars.transition()
.duration(500)
.attr("x", 1 / -xScale.rangeBand())
.attr("width", xScale.rangeBand());
};
}
My other obstacle is changing the fill color on click. I want it to almost imitate a checkbox, so clicking (to deselect) would turn the fill white. I tried something similar as .on("click",(".style" ("fill", "white"))). But that is incorrect.
Here is my fiddle. For some reason, the function isn't updating things on Fiddle. It works on my localhost though. Not sure the problem with that.
I'm not completely sure I understand you correctly, but if your first question is how to change element X when clicking on element Y, you need something along the lines of:
legendRect.on("click", function() {
gbars.transition()
.duration(500)
.style("display", "block")
// etc...
}
As for changing the fill on click, try:
gbars.on("click", function() {
d3.select(this)
.attr("fill", "white");
}

Categories

Resources