Doing a hover event is simple with a circle - you just say hey, when I mouse over the circle, do something! It’s different with a line, though, because they’re generally very very thin. As a result, you need to use a d3.bisector to say “what’s the closest data point?”.

You can see a simple example of the single-line tooltip method over here, but it gets a little more complicated when you have multiple lines. Not only do you need to figure out which data point in a line you’re closets to, you also need to figure out which line you should be displaying the tooltip on!

You can play around with it by clicking “Open example in new window,” but I’ve also put a downloadable version over here.

Open example in new window

...
    // nest your data to group them by country
    var nested = d3.nest()
      .key(function(d) {
        return d.Country;
      })
      .entries(datapoints);

    // Draw your MULTIPLE lines (but remember it isn't called a line)
    svg.selectAll(".country-line")
      .data(nested)
      .enter().append("path")
      .attr("class", "country-line")
      .attr("fill", "none")
      .attr("stroke", "black")
      .attr("d", function(d) {
        return line(d.values)
      });

    // Create the element that will be our tooltip
    // by default it's hidden with display: none
    // DO NOT GIVE IT THE CLASS TOOLTIP
    // IT WILL CONFLICT WITH SOME BOOTSTRAP THING
    // AND WILL NOT SHOW UP AND YOU'LL BE VERY 
    // VERY CONFUSED AND VERY VERY SAD
    var tooltip = svg.append("g")
      .attr("class", "tip")
      .style("display", "none");

    // give the tooltip a circle to highlight our
    // data point
    tooltip.append("circle")
      .attr("r", 3);

    // give the tooltip a text element, but push
    // it to the right and down little bit
    tooltip.append("text")
      .attr("dx", 5)
      .attr("dy", 15);

    // draw an invisible rectangle over the ENTIRE page
    // but even though it's invisible, make it catch
    // everything your pointer (mouse) does
    svg.append("rect")
      .attr("fill", "none")
      .style("pointer-events", "all")
      .attr("width", width)
      .attr("height", height)
      .on("mousemove", function(d) {
        // When the mouse is moved on top of the rectangle,
        // compute what the data point is and where to draw it

        // If you'd understand better, console.log all of these variables
        // as we step through the process

        // STEP ONE: Get the position of the mouse - how many pixels
        // to the right is it?
        var mouse = d3.mouse(this);
        var mousePositionX = mouse[0];

        // STEP TWO: Use the x position scale BACKWARDS to estimate
        // the number of years for our mouse position
        // if we're 200 pixels out, how many years would that be?
        var mouseYear = xPositionScale.invert(mousePositionX);

        // STEP THREE: We have a year, but it's probably not exactly
        // on one of our data points (e.g. mouse is on 1973 but we
        // only have 1970 and 1975). The bisector will take the 
        // year we're at and round it down to the closest data point

        // BUT!!!! This seemed complicated enough when we only had
        // one line, but this time we need to do it for each line.
        // Once we have the closest point for each line, we then see
        // which line is the 'right' one to display the tooltip on

        // Making a new list: It's just the closest datapoints
        // for each and every country/line
        var closeDatapoints = nested.map(function(d) {
          // we find the datapoint closest to our year
          var index = d3.bisector(function(d) { return d.Year; })
            .left(d.values, mouseYear);
          return d.values[index];
        })

        // STEP FOUR:
        // Now we have a list of datapoints that match on the x axis,
        // but we need to see which one is closest on the y axis, too
        var mousePositionY = mouse[1];
        var mouseLifeExpectancy = yPositionScale.invert(mousePositionY);

        // it will only work on sorted datapoints, though, so let's sort first
        var sorted = closeDatapoints.sort(function(a,b) {
          return a.life_expectancy - b.life_expectancy;
        });

        // Use the computed life expectancy from the mouse position
        // to find which line's data point we should be using
        var index = d3.bisector(function(d) { return d.life_expectancy; })
          .left(sorted, mouseLifeExpectancy);

        // STEP FIVE: Use the index to get the datapoint
        var d = sorted[index];
        if(!d) {
          d = sorted[index - 1];
        }

        // STEP FIVE: What's the x and y of that datapoint? Let's
        // move the tooltip to there
        var xPos = xPositionScale(d.Year);
        var yPos = yPositionScale(d.life_expectancy);
        d3.select(".tip")
          .attr("transform", "translate(" + xPos + "," + yPos + ")");

        // STEP FIVE: Use the datapoint information to fill in the tooltip
        d3.select(".tip").select("text").text(d.life_expectancy + " years")

      })
      .on("mouseout", function(d) {
        // Hide the tooltip when you're moving off of the visulization
        svg.select(".tip").style("display", "none")
      })
      .on("mouseover", function(d) {
        // Display the tooltip when you're over the rectangle
        // why is it null? I dunno, stole it from
        // https://bl.ocks.org/mbostock/3902569
        svg.select(".tip").style("display", null)
      })
...

Open example in new window