Search code examples
d3.jsmouseeventsettimeouttransitionmouseenter

D3 show tooltip on mouse over with a timeout


I want to add tooltips to the barchar's rects every time the mouse enters a rect and the mouse is over the rect after some milliseconds. I also want to remove them when the mouse leaves any rect.

I tried it binding it with d3 events and using setTimeout/clearTimeout.

It seems to work except when you move the mouse quickly through different bars while the line of the tooltip is transitioning,

The problem in this cases is that the tooltip is moved from bar to bar that the mouse enters/leaves with disregard of the timeout. The text of the tooltip does seem to do some weird things.

Let me show you how the tooltip appears before the timeout:

gif showing the problem

DEMO and FULL CODE at CODEPEN

Code for binding the functions that paints the tooltips:

... 
var bar = svg.select(".bars")
            .selectAll(".barchart-group")
            .data(data);
... 
var come = bar.enter()
            .append("rect")
            .style("stroke", undefined)
            .style("fill", "hsla(34, 82%, 48%, 0.79)");
... 
        //bind events to new bars
        come.on("mouseover", delayTooltip)
            .on("mouseout", removeBarTooltip);
... 

Code of the tootlip related functions:

function delayTooltip(d, i) {
  // this here is every rect of the bar chart
    this.hoverTimeout = window.setTimeout(addBarTooltip.bind(this), 1000, d, i);
}

function addBarTooltip(d, i) {
  // **** PROBLEM **** 
  // this executes whithout waiting the set timeout
    var firstAnims = 500;
    var thisbar = d3.select(this);
    var tooltip = d3.select(this.parentNode);
    var gRoot = d3.select(this.parentNode.parentNode);
    //Lets define some points of interest
    var p0 = [+thisbar.attr("x") + (barWidth - 1) / 2, +thisbar.attr("y")];
    var p1 = [p0[0], +p0[1] - 9];
    var p2 = [p0[0], +p0[1] - 13];
    //relevant points to draw tooltips
    var line = gRoot.select(".linePointers");
    var text = gRoot.select(".textHelpers");
    line.append("polyline")
        .style("stroke-dasharray", "2,1")
        .style("stroke", "black")
        .attr("points", [p0, p0])
        .transition("line-Y-axis")
        .duration(firstAnims)
        .attrTween("points", function(d, i, a) {
            return d3.interpolate([p0, p0], [p0, p1]);
        });

    text.append("text")
        .attr("dy", ".2em")
        .attr("text-anchor", "middle")
        .attr("x", p2[0])
        .attr("y", p1[1])
        .text(d.value)
        .style("font-size", ".85em")
        .style("opacity", 0)
        .transition("text-Y-axis")
        .delay(firstAnims)
        .duration(300)
        .style("opacity", 1)
        .attr("y", p2[1]);

}

function removeBarTooltip() {
    window.clearTimeout(this.hoverTimeout);
    var firstAnims = 200;
    var thisbar = d3.select(this);
    var tooltip = d3.select(this.parentNode);
    var gRoot = d3.select(this.parentNode.parentNode);
    //relevant points to draw tooltips
    var p0 = [+thisbar.attr("x") + (barWidth - 1) / 2, +thisbar.attr("y")];
    var p1 = [p0[0], +p0[1] - 9];
    var p2 = [p0[0], +p0[1] - 13];
    //Lets make a group for each part of our tooltip
    var line = gRoot.select(".linePointers")
        .selectAll("polyline");
    var text = gRoot.select(".textHelpers")
        .selectAll("text");

    line.transition("line-Y-axis")
        .duration(firstAnims)
        .attrTween("points", function(d, i, a) {
            return d3.interpolate([p0, p1], [p0, p0]);
        })
        .remove();

    text.style("opacity", 0.7)
        .transition("text-Y-axis")
        .delay(firstAnims)
        .duration(300)
        .style("opacity", 0)
        .attr("dy", p0[1] - p2[1])
        .remove();
}

I prefer avoid adding any other libraries if possible.


Solution

  • The problem was in removeBarTooltip(). Let's look first at this two lines where i defined the points for a tootlip line:

    var p0 = [+thisbar.attr("x") + (barWidth - 1) / 2, +thisbar.attr("y")];
    var p1 = [p0[0], +p0[1] - 9];
    

    later in this function I was doing:

    [...]
    line.transition("line-Y-axis")
        .duration(firstAnims)
        .attrTween("points", function(d, i, a) {
            return d3.interpolate([p0, p1], [p0, p0]);
        })
        .remove();
    

    So what was happening is that the tootlip line was moving from bar to bar due to var p0 value depending on the bar the mouse is over.

    To solve this I changed the line transition with attrTween to:

    line.transition("line-Y-axis")
        .duration(firstAnims)
        .attrTween("points", function(d, i, a) {
            var p0current = a.split(",");
            var p1current = p0current.splice(2);
            return d3.interpolate([p0current, p1current], [p0current, p0current]);
        })
        .remove();
    

    Where it takes the actual value of the points property with argument a and interpolates the actual movemevent based only on it.

    DEMO and FULL code at CODEPEN.