title: Add tooltip function to d3.js multi-series line chart
body: <p>I may not have initially explained myself very well regarding what I am trying to accomplish. I would like the tooltip functionality of <a href="https://jsfiddle.net/qvf8hn0u/" rel="nofollow">https://jsfiddle.net/qvf8hn0u/</a> (the code is below) incorporated into my Multi-series line chart. (See code below). When viewing the Tooltip code in a browser while hovering over the chart, 2 tooltips appear with x and y values associated with data on the chart. My goal is to accomplish the same feature in the Multi-series line chart, based on the number of categories selected. In the Multi-series chart there is the ability to select from a list of categories to the right of the chart. With the tooltip functionality applied it would display the values of the x and y data. If I select one category it would only display the data for the selected category, if I select 2 or more it would display the data for all categories selected. I hope this clears up any initial confusion. Again any assistance would be greatly appreciated.</p>
  
  <p>Tooltip:</p>
  
  <pre><code>&lt;!DOCTYPE html&gt;
  &lt;head&gt;
  &lt;meta charset="utf-8"&gt;
  &lt;title&gt;Tooltip&lt;/title&gt;
  &lt;style&gt;
  
  text.inner-circle {
    font-weight: 400;
    font-size: 12px;
    text-transform: uppercase;
  }
  
  text.inner-text {
    font-weight: 400;
    font-size: 36px;
    font-family: 'Metric Regular', 'Metric';
    text-align: center;
    font-style: normal;
    text-transform: uppercase;
  }
  
  path {
    stroke: steelblue;
    stroke-width: 2;
    fill: none;
  }
  
  .axis path,
  .axis line {
    fill: none;
    stroke: grey;
    stroke-width: 2;
    shape-rendering: crispEdges;
  }
  
  .grid .tick {
    stroke: lightgrey;
    stroke-opacity: 0.7;
    shape-rendering: crispEdges;
  }
  
  .grid path {
    stroke-width: 0;
  }
  
  
  &lt;/style&gt;
  &lt;/head&gt;
  &lt;body&gt;
  &lt;script src="http://d3js.org/d3.v3.js"&gt;&lt;/script&gt;
  &lt;script&gt;
  
  var margin = {
      top: 20,
      right: 30,
      bottom: 30,
      left: 40
    },
    width = 960 - margin.left - margin.right,
    height = 500 - margin.top - margin.bottom;
  
  var z = d3.scale.category20c();
  
  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 parseDate = d3.time.format("%Y-%m-%dT%H:%M:%S.%LZ");
  
  var data = [{
    data: [
      ["2016-01-20T05:31:17.000Z", 95.9, {}],
      ["2016-01-20T05:31:47.000Z", 95.9, {}],
      ["2016-01-20T05:32:17.000Z", 95.4, {}],
      ["2016-01-20T05:32:47.000Z", 96.1, {}],
      ["2016-01-20T05:33:17.000Z", 95.7, {}],
      ["2016-01-20T05:33:47.000Z", 95.9, {}],
      ["2016-01-20T05:34:17.000Z", 95.5, {}],
      ["2016-01-20T05:34:47.000Z", 95.9, {}],
      ["2016-01-20T05:35:17.000Z", 95.8, {}],
      ["2016-01-20T05:35:47.000Z", 95.9, {}],
      ["2016-01-20T05:36:17.000Z", 95.7, {}],
      ["2016-01-20T05:36:47.000Z", 95.7, {}],
      ["2016-01-20T05:37:17.000Z", 95.9, {}],
      ["2016-01-20T05:37:47.000Z", 95.5, {}],
      ["2016-01-20T05:38:17.000Z", 95.4, {}],
      ["2016-01-20T05:38:47.000Z", 95.8, {}],
      ["2016-01-20T05:39:17.000Z", 96.0, {}],
      ["2016-01-20T05:39:47.000Z", 96.1, {}],
      ["2016-01-20T05:40:17.000Z", 95.8, {}],
      ["2016-01-20T05:40:47.000Z", 96.0, {}],
      ["2016-01-20T05:41:17.000Z", 95.9, {}],
      ["2016-01-20T05:41:47.000Z", 94.9, {}],
      ["2016-01-20T05:42:17.000Z", 95.8, {}],
      ["2016-01-20T05:42:47.000Z", 95.9, {}],
      ["2016-01-20T05:43:17.000Z", 95.8, {}],
      ["2016-01-20T05:43:47.000Z", 96.0, {}],
      ["2016-01-20T05:44:17.000Z", 95.7, {}],
      ["2016-01-20T05:44:47.000Z", 96.0, {}],
      ["2016-01-20T05:45:17.000Z", 95.9, {}],
      ["2016-01-20T05:45:47.000Z", 96.0, {}],
      ["2016-01-20T05:46:17.000Z", 95.8, {}],
      ["2016-01-20T05:46:47.000Z", 96.0, {}],
      ["2016-01-20T05:47:17.000Z", 95.7, {}],
      ["2016-01-20T05:47:47.000Z", 96.2, {}],
      ["2016-01-20T05:48:17.000Z", 95.8, {}],
      ["2016-01-20T05:48:47.000Z", 95.9, {}],
      ["2016-01-20T05:49:17.000Z", 95.7, {}],
      ["2016-01-20T05:49:47.000Z", 95.9, {}],
      ["2016-01-20T05:50:18.000Z", 95.7, {}],
      ["2016-01-20T05:50:48.000Z", 95.8, {}],
      ["2016-01-20T05:51:18.000Z", 95.7, {}],
      ["2016-01-20T05:51:48.000Z", 95.9, {}],
      ["2016-01-20T05:52:18.000Z", 95.5, {}],
      ["2016-01-20T05:52:48.000Z", 95.9, {}],
      ["2016-01-20T05:53:18.000Z", 95.8, {}],
      ["2016-01-20T05:53:48.000Z", 95.9, {}],
      ["2016-01-20T05:54:18.000Z", 95.7, {}],
      ["2016-01-20T05:54:48.000Z", 95.9, {}],
      ["2016-01-20T05:55:18.000Z", 95.8, {}],
      ["2016-01-20T05:55:48.000Z", 95.8, {}],
      ["2016-01-20T05:56:18.000Z", 95.6, {}],
      ["2016-01-20T05:56:48.000Z", 95.7, {}],
      ["2016-01-20T05:57:18.000Z", 95.7, {}],
      ["2016-01-20T05:57:48.000Z", 95.8, {}],
      ["2016-01-20T05:58:18.000Z", 95.7, {}],
      ["2016-01-20T05:58:48.000Z", 95.7, {}],
      ["2016-01-20T05:59:18.000Z", 95.6, {}],
      ["2016-01-20T05:59:48.000Z", 95.8, {}],
      ["2016-01-20T06:00:18.000Z", 95.7, {}],
      ["2016-01-20T06:00:48.000Z", 95.7, {}],
      ["2016-01-20T06:01:18.000Z", 95.6, {}],
      ["2016-01-20T06:01:48.000Z", 95.7, {}],
      ["2016-01-20T06:02:18.000Z", 95.8, {}],
      ["2016-01-20T06:02:48.000Z", 95.8, {}],
      ["2016-01-20T06:03:18.000Z", 95.8, {}],
      ["2016-01-20T06:03:48.000Z", 95.8, {}],
      ["2016-01-20T06:04:18.000Z", 95.8, {}],
      ["2016-01-20T06:04:48.000Z", 95.8, {}],
      ["2016-01-20T06:05:18.000Z", 95.7, {}],
      ["2016-01-20T06:05:48.000Z", 95.7, {}]
    ],
    label: "a"
  }, {
    data: [
      ["2016-01-20T05:31:17.000Z", 90.9, {}],
      ["2016-01-20T05:31:47.000Z", 91.9, {}],
      ["2016-01-20T05:32:17.000Z", 92.4, {}],
      ["2016-01-20T05:32:47.000Z", 90.1, {}],
      ["2016-01-20T05:33:17.000Z", 89.7, {}],
      ["2016-01-20T05:33:47.000Z", 91.9, {}],
      ["2016-01-20T05:34:17.000Z", 85.5, {}],
      ["2016-01-20T05:34:47.000Z", 93.9, {}],
      ["2016-01-20T05:35:17.000Z", 94.8, {}],
      ["2016-01-20T05:35:47.000Z", 93.9, {}],
      ["2016-01-20T05:36:17.000Z", 92.7, {}],
      ["2016-01-20T05:36:47.000Z", 95.7, {}],
      ["2016-01-20T05:37:17.000Z", 92.9, {}],
      ["2016-01-20T05:37:47.000Z", 93.5, {}],
      ["2016-01-20T05:38:17.000Z", 93.4, {}],
      ["2016-01-20T05:38:47.000Z", 93.8, {}],
      ["2016-01-20T05:39:17.000Z", 93.0, {}],
      ["2016-01-20T05:39:47.000Z", 93.1, {}],
      ["2016-01-20T05:40:17.000Z", 93.8, {}],
      ["2016-01-20T05:40:47.000Z", 93.0, {}],
      ["2016-01-20T05:41:17.000Z", 93.9, {}],
      ["2016-01-20T05:41:47.000Z", 93.9, {}],
      ["2016-01-20T05:42:17.000Z", 92.8, {}],
      ["2016-01-20T05:42:47.000Z", 92.9, {}],
      ["2016-01-20T05:43:17.000Z", 93.8, {}],
      ["2016-01-20T05:43:47.000Z", 93.0, {}],
      ["2016-01-20T05:44:17.000Z", 93.7, {}],
      ["2016-01-20T05:44:47.000Z", 93.0, {}],
      ["2016-01-20T05:45:17.000Z", 93.9, {}],
      ["2016-01-20T05:45:47.000Z", 93.0, {}],
      ["2016-01-20T05:46:17.000Z", 93.8, {}],
      ["2016-01-20T05:46:47.000Z", 96.0, {}],
      ["2016-01-20T05:47:17.000Z", 92.7, {}],
      ["2016-01-20T05:47:47.000Z", 92.2, {}],
      ["2016-01-20T05:48:17.000Z", 92.8, {}],
      ["2016-01-20T05:48:47.000Z", 92.9, {}],
      ["2016-01-20T05:49:17.000Z", 92.7, {}],
      ["2016-01-20T05:49:47.000Z", 92.9, {}],
      ["2016-01-20T05:50:18.000Z", 93.7, {}],
      ["2016-01-20T05:50:48.000Z", 93.8, {}],
      ["2016-01-20T05:51:18.000Z", 92.7, {}],
      ["2016-01-20T05:51:48.000Z", 92.9, {}],
      ["2016-01-20T05:52:18.000Z", 92.5, {}],
      ["2016-01-20T05:52:48.000Z", 94.9, {}],
      ["2016-01-20T05:53:18.000Z", 94.8, {}],
      ["2016-01-20T05:53:48.000Z", 94.9, {}],
      ["2016-01-20T05:54:18.000Z", 94.7, {}],
      ["2016-01-20T05:54:48.000Z", 94.9, {}],
      ["2016-01-20T05:55:18.000Z", 94.8, {}],
      ["2016-01-20T05:55:48.000Z", 93.8, {}],
      ["2016-01-20T05:56:18.000Z", 94.6, {}],
      ["2016-01-20T05:56:48.000Z", 94.7, {}],
      ["2016-01-20T05:57:18.000Z", 93.7, {}],
      ["2016-01-20T05:57:48.000Z", 93.8, {}],
      ["2016-01-20T05:58:18.000Z", 93.7, {}],
      ["2016-01-20T05:58:48.000Z", 93.7, {}],
      ["2016-01-20T05:59:18.000Z", 93.6, {}],
      ["2016-01-20T05:59:48.000Z", 93.8, {}],
      ["2016-01-20T06:00:18.000Z", 93.7, {}],
      ["2016-01-20T06:00:48.000Z", 93.7, {}],
      ["2016-01-20T06:01:18.000Z", 93.6, {}],
      ["2016-01-20T06:01:48.000Z", 94.7, {}],
      ["2016-01-20T06:02:18.000Z", 94.8, {}],
      ["2016-01-20T06:02:48.000Z", 94.8, {}],
      ["2016-01-20T06:03:18.000Z", 94.8, {}],
      ["2016-01-20T06:03:48.000Z", 94.8, {}],
      ["2016-01-20T06:04:18.000Z", 94.8, {}],
      ["2016-01-20T06:04:48.000Z", 94.8, {}],
      ["2016-01-20T06:05:18.000Z", 94.7, {}],
      ["2016-01-20T06:05:48.000Z", 94.7, {}]
    ],
    label: "b"
  }]
  
  
  var x = d3.time.scale()
    .range([0, width]);
  
  var y = d3.scale.linear()
    .range([height, 0]);
  
  var xAxis = d3.svg.axis()
    .scale(x)
    .orient("bottom");
  
  var yAxis = d3.svg.axis()
    .scale(y)
    .orient("left");
  
  var line = d3.svg.line()
    .interpolate("monotone")
    .x(function(d) {
      return x(parseDate.parse(d[0]));
    })
    .y(function(d) {
      return y(d[1]);
    });
  
  var ary = [];
  data.forEach(function(d) {
    ary.push(d.data);
  });
  
  x.domain(d3.extent(d3.merge(ary), function(d) {
    return parseDate.parse(d[0]);
  }));
  
  y.domain([
    d3.min(data, function(c) {
      return d3.min(c.data, function(v) {
        return v[1];
      });
    }),
    d3.max(data, function(c) {
      return d3.max(c.data, function(v) {
        return v[1];
      });
    })
  ]);
  
  svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis);
  
  svg.append("g")
    .attr("class", "y axis")
    .call(yAxis);
  
  var series = svg.selectAll(".series")
    .data(data)
    .enter().append("g")
    .attr("class", "series");
  
  series.append("path")
    .attr("class", "line")
    .attr("d", function(d) {
      return line(d.data);
    })
    .style("stroke", function(d, i) {
      return z(i);
    });
  
  series.append("text")
    .datum(function(d) {
      return {
        label: d.label,
        data: d.data[d.data.length - 1]
      };
    })
    .attr("transform", function(d) {
      return "translate(" + x(parseDate.parse(d.data[0])) + "," + y(d.data[1]) + ")";
    })
    .attr("x", 3)
    .attr("dy", ".35em");
  
  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(data)
    .enter()
    .append("g")
    .attr("class", "mouse-per-line");
  
  mousePerLine.append("circle")
    .attr("r", 7)
    .style("stroke", function(d, i) {
      return z(i);
    })
    .style("fill", "none")
    .style("stroke-width", "1px")
    .style("opacity", "0");
  
  mousePerLine.append("rect")
      .attr("x", 0)
      .attr("y", -10)
    .attr("width", 85)
      .attr("height", 35)
    .style("stroke", function(d, i) {
      return z(i);
    })
      .attr("class", "tooltip-container")
    .style("fill", "red")
      .style("opacity", "0")
    .style("stroke-width", "1px");
  
  
  mousePerLine.append("text").attr("id", "x-text")
    .attr("transform", "translate(10,3)");
  mousePerLine.append("text").attr("id", "y-text")
    .attr("transform", "translate(10,23)");
  
  
  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 rect")
        .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");
          d3.selectAll(".mouse-per-line rect")
        .style("opacity", "0.2");             
    })
    .on('mousemove', function() { // mouse moving over canvas
      var mouse = d3.mouse(this);
      d3.select(".mouse-line")
        .attr("d", function() {
          var d = "M" + mouse[0] + "," + height;
          d += " " + mouse[0] + "," + 0;
          return d;
        });
  
      d3.selectAll(".mouse-per-line")
        .attr("transform", function(d, i) {
          console.log(width / mouse[0])
          var xDate = x.invert(mouse[0]),
            bisect = d3.bisector(function(d) {
              return d[0];
            }).right;
          var idx = bisect(d.data, xDate);
  
          var beginning = 0,
            end = lines[i].getTotalLength(),
            target = null;
  
          while (true) {
            target = Math.floor((beginning + end) / 2);
            var pos = lines[i].getPointAtLength(target);
            if ((target === end || target === beginning) &amp;&amp; pos.x !== mouse[0]) {
              break;
            }
            if (pos.x &gt; mouse[0]) end = target;
            else if (pos.x &lt; mouse[0]) beginning = target;
            else break; //position found
          }
  
          d3.select(this).select('#y-text')
            .text("y: " + y.invert(pos.y).toFixed(2));
  
                  d3.select(this).select('#x-text')
            .text("x: " + d3.time.format("%X")(xDate));
  
          return "translate(" + mouse[0] + "," + pos.y + ")";
        });
    });
  
  &lt;/script&gt;
  &lt;/html&gt;
  </code></pre>
  
  <p>Multi-series line chart:</p>
  
  <pre><code>&lt;!DOCTYPE html&gt;
  &lt;head&gt;
  &lt;meta charset="utf-8"&gt;
  &lt;title&gt;Issues Ratings&lt;/title&gt;
  &lt;style&gt;
  
  body {
    font: 10px sans-serif;
  }
  
  .axis path,
  .axis line, 
  .axis1 path,
  .axis1 line {
    fill: none;
    stroke: #E6E7E8;
    shape-rendering: crispEdges;
  }
  
  .x.axis path, .x.axis1 path {
    display: none;
  }
  
  .line {
    fill: none;
    stroke-width: 1.5px;
  }
  
  .legend-box {
    cursor: pointer;  
  }
  
  #mouse-tracker {
    stroke: #E6E7E8;
    stroke-width: 1px;
  }
  
  .hover-line { 
    stroke: #E6E7E8;
    fill: none;
    stroke-width: 1px;
    left: 10px;
    shape-rendering: crispEdges;
    opacity: 1e-6;
  }
  
  .hover-text {
    stroke: none;
    font-size: 30px;
    font-weight: bold;
    fill: #000000;
  }
  
  .tooltip {
    font-weight: normal;
  }
  
  .brush .extent {
    stroke: #FFF;
    shape-rendering: crispEdges;
  }
  
    text.inner-circle {
    font-weight: 400;
    font-size: 12px;
    text-transform: uppercase;
  }
  
    text.inner-text {
    font-weight: 400;
    font-size: 36px;
    font-family: 'Metric Regular', 'Metric';
    text-align: center;
    font-style: normal;
    text-transform: uppercase;
  }
  &lt;/style&gt;
  &lt;/head&gt;
  &lt;body&gt;
  &lt;script src="http://d3js.org/d3.v3.js"&gt;&lt;/script&gt;
  &lt;script&gt;
  
  var margin = {top: 20, right: 200, bottom: 100, left: 50},
      margin2 = { top: 430, right: 10, bottom: 20, left: 40 },
      width = 960 - margin.left - margin.right,
      height = 500 - margin.top - margin.bottom,
      height2 = 500 - margin2.top - margin2.bottom;
  
  var parseDate = d3.time.format("%Y%m%d").parse;
  var bisectDate = d3.bisector(function(d) { return d.date; }).left;
  
  var xScale = d3.time.scale()
      .range([0, width]),
  
      xScale2 = d3.time.scale()
      .range([0, width]); // Duplicate xScale for brushing ref later
  
  var yScale = d3.scale.linear()
      .range([height, 0]);
  
  // 40 Custom DDV colors 
  var color = d3.scale.ordinal().range(["#48A36D",  "#56AE7C",  "#64B98C", "#72C39B", "#80CEAA", "#80CCB3",
  "#7FC9BD", "#7FC7C6", "#7EC4CF", "#7FBBCF", "#7FB1CF", "#80A8CE", "#809ECE", "#8897CE", "#8F90CD",
  "#9788CD", "#9E81CC", "#AA81C5", "#B681BE", "#C280B7", "#CE80B0", "#D3779F", "#D76D8F", "#DC647E",
  "#E05A6D", "#E16167", "#E26962", "#E2705C", "#E37756", "#E38457", "#E39158", "#E29D58", "#E2AA59",
  "#E0B15B", "#DFB95C", "#DDC05E", "#DBC75F", "#E3CF6D", "#EAD67C", "#F2DE8A"]);  
  
  
  var xAxis = d3.svg.axis()
      .scale(xScale)
      .orient("bottom")
      .ticks(10),
  
      xAxis2 = d3.svg.axis() // xAxis for brush slider
      .scale(xScale2)
      .orient("bottom")
      .ticks(10);    
  
  var yAxis = d3.svg.axis()
      .scale(yScale)
      .orient("left");
  
  var yAxis = d3.svg.axis()
      .scale(xScale)
      .orient("bottom")
      .ticks(15)
      .tickSize(-height, 0, 0)
      .tickFormat("");
  
  var yAxis = d3.svg.axis()
      .scale(yScale)
      .orient("left")
      .ticks(15)
      .tickSize(-width, 0, 0)
      .tickFormat("");
  
  
  var line = d3.svg.line()
      .interpolate("basis")
      .x(function(d) { return xScale(d.date); })
      .y(function(d) { return yScale(d.rating); })
      //.defined(function(d) { return d.rating; });  // Hiding line value defaults of 0 for missing data
  
  var maxY; // Defined later to update yAxis
  
  var svg = d3.select("body").append("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom) //height + margin.top + margin.bottom
    .append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
  
  // Create invisible rect for mouse tracking
  svg.append("rect")
      .attr("width", width)
      .attr("height", height)                                    
      .attr("x", 0) 
      .attr("y", 0)
      //.attr("id", "mouse-tracker")
      .style("fill", "white"); 
  
  //for slider part-----------------------------------------------------------------------------------
  
  //end slider part----------------------------------------------------------------------------------- 
  
  d3.tsv("http://bl.ocks.org/DStruths/raw/9c042e3a6b66048b5bd4/data.tsv", function(error, data) { 
    color.domain(d3.keys(data[0]).filter(function(key) { // Set the domain of the color ordinal scale to be all the csv headers except "date", matching a color to an issue
      return key !== "date"; 
    }));
  
    data.forEach(function(d) { // Make every date in the csv data a javascript date object format
      d.date = parseDate(d.date);
    });
  
    var categories = color.domain().map(function(name) { // Nest the data into an array of objects with new keys
  
      return {
        name: name, // "name": the csv headers except date
        values: data.map(function(d) { // "values": which has an array of the dates and ratings
          return {
            date:d.date, 
            rating:+(d[name]),
            };
        }),
        visible: (name === "Unemployment" ? true : false) // "visible": all false except for economy which is true.
      };
    });
  
    xScale.domain(d3.extent(data, function(d) { return d.date; })); // extent = highest and lowest points, domain is data, range is bouding box
  
    yScale.domain([0, 70
      //d3.max(categories, function(c) { return d3.max(c.values, function(v) { return v.rating; }); })
    ]);
  
  
   //for slider part-----------------------------------------------------------------------------------
  
    //end slider part-----------------------------------------------------------------------------------
  
    // draw line graph
    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("x", -10)
        .attr("dy", ".71em")
        .style("text-anchor", "end")
        .text("Issues Rating");
  
   svg.append("g")
        .attr("class", "grid")
        .attr("transform", "translate(0," + height + ")")
        .call(xGrid);
  
    svg.append("g")
        .attr("class", "grid")
        .call(yGrid)
      .append("text")
        .attr("transform", "rotate(-90)")
        .attr("y", 6)
        .attr("x", -10)
        .attr("dy", ".71em")
        .style("text-anchor", "end")
        .text("Issues Rating");
  
  
    var issue = svg.selectAll(".issue")
        .data(categories) // Select nested data and append to new svg group elements
      .enter().append("g")
        .attr("class", "issue");   
  
    issue.append("path")
        .attr("class", "line")
        .style("pointer-events", "none") // Stop line interferring with cursor
        .attr("id", function(d) {
          return "line-" + d.name.replace(" ", "").replace("/", ""); // Give line id of line-(insert issue name, with any spaces replaced with no spaces)
        })
        .attr("d", function(d) { 
          return d.visible ? line(d.values) : null; // If array key "visible" = true then draw line, if not then don't 
        })
        .attr("clip-path", "url(#clip)")//use clip path to make irrelevant part invisible
        .style("stroke", function(d) { return color(d.name); });
  
    // draw legend
    var legendSpace = 650 / categories.length; // 450/number of issues (ex. 40)    
  
    issue.append("rect")
        .attr("width", 10)
        .attr("height", 10)                                    
        .attr("x", width + (margin.right/3) - 15) 
        .attr("y", function (d, i) { return (legendSpace)+i*(legendSpace) - 8; })  // spacing
        .attr("fill",function(d) {
          return d.visible ? color(d.name) : "#F1F1F2"; // If array key "visible" = true then color rect, if not then make it grey 
        })
        .attr("class", "legend-box")
  
        .on("click", function(d){ // On click make d.visible 
          d.visible = !d.visible; // If array key for this data selection is "visible" = true then make it false, if false then make it true
  
          maxY = findMaxY(categories); // Find max Y rating value categories data with "visible"; true
          yScale.domain([0,maxY]); // Redefine yAxis domain based on highest y value of categories data with "visible"; true
          svg.select(".y.axis")
            .transition()
            .call(yAxis);   
  
          issue.select("path")
            .transition()
            .attr("d", function(d){
              return d.visible ? line(d.values) : null; // If d.visible is true then draw line for this d selection
            })
  
          issue.select("rect")
            .transition()
            .attr("fill", function(d) {
            return d.visible ? color(d.name) : "#F1F1F2";
          });
        })
  
    issue.append("text")
        .attr("x", width + (margin.right/3)) 
        .attr("y", function (d, i) { return (legendSpace)+i*(legendSpace); })  // (return (11.25/2 =) 5.625) + i * (5.625) 
        .text(function(d) { return d.name; });      
  
  }); // End Data callback function
  
    function findMaxY(data){  // Define function "findMaxY"
      var maxYValues = data.map(function(d) { 
        if (d.visible){
          return d3.max(d.values, function(value) { // Return max rating value
            return value.rating; })
        }
      });
      return d3.max(maxYValues);
    }
  
   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(data)
    .enter()
    .append("g")
    .attr("class", "mouse-per-line");
  
  mousePerLine.append("circle")
    .attr("r", 7)
    .style("stroke", function(d, i) {
      return z(i);
    })
    .style("fill", "none")
    .style("stroke-width", "1px")
    .style("opacity", "0");
  
  mousePerLine.append("rect")
      .attr("x", 0)
      .attr("y", -10)
    .attr("width", 85)
      .attr("height", 35)
    .style("stroke", function(d, i) {
      return z(i);
    })
      .attr("class", "tooltip-container")
    .style("fill", "red")
      .style("opacity", "0")
    .style("stroke-width", "1px");
  
  
  mousePerLine.append("text").attr("id", "x-text")
    .attr("transform", "translate(10,3)");
  mousePerLine.append("text").attr("id", "y-text")
    .attr("transform", "translate(10,23)");
  
  
  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 rect")
        .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");
          d3.selectAll(".mouse-per-line rect")
        .style("opacity", "0.2");             
    })
    .on('mousemove', function() { // mouse moving over canvas
      var mouse = d3.mouse(this);
      d3.select(".mouse-line")
        .attr("d", function() {
          var d = "M" + mouse[0] + "," + height;
          d += " " + mouse[0] + "," + 0;
          return d;
        });
  
      d3.selectAll(".mouse-per-line")
        .attr("transform", function(d, i) {
          console.log(width / mouse[0])
          var xDate = x.invert(mouse[0]),
            bisect = d3.bisector(function(d) {
              return d[0];
            }).right;
          var idx = bisect(d.data, xDate);
  
          var beginning = 0,
            end = lines[i].getTotalLength(),
            target = null;
  
          while (true) {
            target = Math.floor((beginning + end) / 2);
            var pos = lines[i].getPointAtLength(target);
            if ((target === end || target === beginning) &amp;&amp; pos.x !== mouse[0]) {
              break;
            }
            if (pos.x &gt; mouse[0]) end = target;
            else if (pos.x &lt; mouse[0]) beginning = target;
            else break; //position found
          }
  
          d3.select(this).select('#y-text')
            .text("y: " + y.invert(pos.y).toFixed(2));
  
                  d3.select(this).select('#x-text')
            .text("x: " + d3.time.format("%X")(xDate));
  
          return "translate(" + mouse[0] + "," + pos.y + ")";
        });
    });
  &lt;/script&gt;
  &lt;/html&gt;
  </code></pre>