score:5

I took Mark's answer as a reference from the Multiseries line chart with mouseover tooltip you provided.

Basically, what you need to do is set the tooltips to show on each tick of the x-axis data, so instead of grabbing the position of the mouse with mouse[0] and moving the tooltips, you should move it to the position where the x-axis data is.

Here's the detail of the changes I made:

mouseG.append('svg:rect')
    .attr('width', width)
    .attr('height', height)
    .attr('fill', 'none')
    .attr('pointer-events', 'all')
    .on('mouseout', () => {
        d3.select(".mouse-line")
        .style("opacity", "0");
        d3.selectAll(".mouse-per-line circle")
        .style("opacity", "0");
        d3.selectAll(".mouse-per-line text")
        .style("opacity", "0");
    })
    .on('mouseover', () => {
        d3.select(".mouse-line")
        .style("opacity", "1");
        d3.selectAll(".mouse-per-line circle")
        .style("opacity", "1");
        d3.selectAll(".mouse-per-line text")
        .style("opacity", "1");
    })
    .on('mousemove', () => {
        let mouse = d3.mouse(d3.event.currentTarget);
        // MOVE THIS BEFORE THE RETURN
        // d3.select(".mouse-line")
        // .attr("d", () => {
        //     var d = "M" + mouse[0] + "," + height;
        //     d += " " + mouse[0] + "," + 0;
        //     return d;
        // });
        d3.selectAll(".mouse-per-line")
        .attr("transform", (d, i) => {
            var lines = document.getElementsByClassName('line')
            var xDate = this.x.invert(mouse[0])
            var bisect = d3.bisector(function(d) { return d.date; }).right;
            var idx = bisect(this.dataObj, xDate);

            // GET RID OF THIS
            // var beginning = 0,
            // end = lines[i].getTotalLength()
            // var target = null;

            // while (true){
            //     var target = Math.floor((beginning + end) / 2);
            //     var pos = lines[i].getPointAtLength(target);
            //     if ((target === end || target === beginning) && pos.x !== mouse[0]) {
            //         break;
            //     }
            //     if (pos.x > mouse[0])      end = target;
            //     else if (pos.x < mouse[0]) beginning = target;
            //     else break; //position found
            // }

            // REPLACE pos.y WITH y(d.values[idx].temperature)
            // AND mouse[0] WITH x(d.values[idx].date)
            d3.select('text')
            .text(this.y.invert(pos.y));

            return "translate(" + mouse[0] + "," + pos.y +")";
        });
    });

Below is the fully working code with the changes applied. For this snippet I used interpolate('linear') to show the values correctly; if you use interpolate('basis'), the tooltips and lines will not match correctly:

<!DOCTYPE html>
<html>

<head>
  <script data-require="d3@3.5.3" data-semver="3.5.3" src="http://cdnjs.cloudflare.com/ajax/libs/d3/3.5.3/d3.js"></script>
  <style>
    body {
      font: 10px sans-serif;
    }
    
    .axis path,
    .axis line {
      fill: none;
      stroke: #000;
      shape-rendering: crispEdges;
    }
    
    .x.axis path {
      display: none;
    }
    
    .line {
      fill: none;
      stroke: steelblue;
      stroke-width: 1.5px;
    }
  </style>
</head>

<body>
  <script>
    var myData = "date	New York	San Francisco	Austin\n\
20111001	63.4	62.7	72.2\n\
20111002	58.0	59.9	67.7\n\
20111003	53.3	59.1	69.4\n\
20111004	55.7	58.8	68.0\n\
20111005	64.2	58.7	72.4\n\
20111006	58.8	57.0	77.0\n\
20111007	57.9	56.7	82.3\n\
20111008	61.8	56.8	78.9\n\
20111009	69.3	56.7	68.8\n\
20111010	71.2	60.1	68.7\n\
20111011	68.7	61.1	70.3\n\
20111012	61.8	61.5	75.3\n\
20111013	63.0	64.3	76.6\n\
20111014	66.9	67.1	66.6\n\
20111015	61.7	64.6	68.0\n\
20111016	61.8	61.6	70.6\n\
20111017	62.8	61.1	71.1\n\
20111018	60.8	59.2	70.0\n\
20111019	62.1	58.9	61.6\n\
20111020	65.1	57.2	57.4\n\
20111021	55.6	56.4	64.3\n\
20111022	54.4	60.7	72.4\n";

    var margin = {
      top: 20,
      right: 80,
      bottom: 30,
      left: 50
    },
      width = 400 - margin.left - margin.right,
      height = 250 - margin.top - margin.bottom;

    var parseDate = d3.time.format("%Y%m%d").parse;

    var x = d3.time.scale()
      .range([0, width]);

    var y = d3.scale.linear()
      .range([height, 0]);

    var color = d3.scale.category10();

    var xAxis = d3.svg.axis()
      .scale(x)
      .orient("bottom");

    var yAxis = d3.svg.axis()
      .scale(y)
      .orient("left");

    var line = d3.svg.line()
      .interpolate("linear")
      .x(function (d) {
        return x(d.date);
      })
      .y(function (d) {
        return y(d.temperature);
      });

    var svg = d3.select("body").append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
      .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    var data = d3.tsv.parse(myData);

    color.domain(d3.keys(data[0]).filter(function (key) {
      return key !== "date";
    }));

    data.forEach(function (d) {
      d.date = parseDate(d.date);
    });

    var cities = color.domain().map(function (name) {
      return {
        name: name,
        values: data.map(function (d) {
          return {
            date: d.date,
            temperature: +d[name]
          };
        })
      };
    });

    x.domain(d3.extent(data, function (d) {
      return d.date;
    }));

    y.domain([
      d3.min(cities, function (c) {
        return d3.min(c.values, function (v) {
          return v.temperature;
        });
      }),
      d3.max(cities, function (c) {
        return d3.max(c.values, function (v) {
          return v.temperature;
        });
      })
    ]);

    var legend = svg.selectAll('g')
      .data(cities)
      .enter()
      .append('g')
      .attr('class', 'legend');

    legend.append('rect')
      .attr('x', width - 20)
      .attr('y', function (d, i) {
        return i * 20;
      })
      .attr('width', 10)
      .attr('height', 10)
      .style('fill', function (d) {
        return color(d.name);
      });

    legend.append('text')
      .attr('x', width - 8)
      .attr('y', function (d, i) {
        return (i * 20) + 9;
      })
      .text(function (d) {
        return d.name;
      });

    svg.append("g")
      .attr("class", "x axis")
      .attr("transform", "translate(0," + height + ")")
      .call(xAxis);

    svg.append("g")
      .attr("class", "y axis")
      .call(yAxis)
      .append("text")
      .attr("transform", "rotate(-90)")
      .attr("y", 6)
      .attr("dy", ".71em")
      .style("text-anchor", "end")
      .text("Temperature (ºF)");

    var city = svg.selectAll(".city")
      .data(cities)
      .enter().append("g")
      .attr("class", "city");

    city.append("path")
      .attr("class", "line")
      .attr("d", function (d) {
        return line(d.values);
      })
      .style("stroke", function (d) {
        return color(d.name);
      });

    city.append("text")
      .datum(function (d) {
        return {
          name: d.name,
          value: d.values[d.values.length - 1]
        };
      })
      .attr("transform", function (d) {
        return "translate(" + x(d.value.date) + "," + y(d.value.temperature) + ")";
      })
      .attr("x", 3)
      .attr("dy", ".35em")
      .text(function (d) {
        return d.name;
      });

    var mouseG = svg.append("g")
      .attr("class", "mouse-over-effects");

    mouseG.append("path") // this is the black vertical line to follow mouse
      .attr("class", "mouse-line")
      .style("stroke", "black")
      .style("stroke-width", "1px")
      .style("opacity", "0");

    var lines = document.getElementsByClassName('line');

    var mousePerLine = mouseG.selectAll('.mouse-per-line')
      .data(cities)
      .enter()
      .append("g")
      .attr("class", "mouse-per-line");

    mousePerLine.append("circle")
      .attr("r", 7)
      .style("stroke", function (d) {
        return color(d.name);
      })
      .style("fill", "none")
      .style("stroke-width", "1px")
      .style("opacity", "0");

    mousePerLine.append("text")
      .attr("transform", "translate(10,3)");

    mouseG.append('svg:rect') // append a rect to catch mouse movements on canvas
      .attr('width', width) // can't catch mouse events on a g element
      .attr('height', height)
      .attr('fill', 'none')
      .attr('pointer-events', 'all')
      .on('mouseout', function () { // on mouse out hide line, circles and text
        d3.select(".mouse-line")
          .style("opacity", "0");
        d3.selectAll(".mouse-per-line circle")
          .style("opacity", "0");
        d3.selectAll(".mouse-per-line text")
          .style("opacity", "0");
      })
      .on('mouseover', function () { // on mouse in show line, circles and text
        d3.select(".mouse-line")
          .style("opacity", "1");
        d3.selectAll(".mouse-per-line circle")
          .style("opacity", "1");
        d3.selectAll(".mouse-per-line text")
          .style("opacity", "1");
      })
      .on('mousemove', function () { // mouse moving over canvas
        var mouse = d3.mouse(this);

        d3.selectAll(".mouse-per-line")
          .attr("transform", function (d, i) {

            var xDate = x.invert(mouse[0]),
              bisect = d3.bisector(function (d) { return d.date; }).left;
            idx = bisect(d.values, xDate);

            d3.select(this).select('text')
              .text(y.invert(y(d.values[idx].temperature)).toFixed(2));

            d3.select(".mouse-line")
              .attr("d", function () {
                var data = "M" + x(d.values[idx].date) + "," + height;
                data += " " + x(d.values[idx].date) + "," + 0;
                return data;
              });
            return "translate(" + x(d.values[idx].date) + "," + y(d.values[idx].temperature) + ")";
          });
      });

  </script>
</body>

</html>


Related Query

More Query from same tag