Search code examples
javascriptd3.jslinechartmousehover

Highlight closest point d3js line and dot chart


I currently have a d3js line and dot chart. As of now, when I hover on the dot, it highlights the dot and shows the value on display. What I am trying to see is when I am hovering on the line itself it would hover the closest point to the mouse cursor on the light and do the same as when hovering on a point. I did check bisector but not sure if it is only useful for data with dates. Below is the link for the fiddle with the chart.

https://jsfiddle.net/snt1/56qvqaz9/2/

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

var svg = d3.select("#charts").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 + ")")
  .attr("preserveAspectRatio", "xMinYMin meet")
  .attr("viewBox", "0 0 600 400")
  //class to make it responsive
  .classed("svg-content-responsive", true); ;


// Set the ranges

var x =  d3.scaleBand().rangeRound([0, width]).padding(0.1);
var y = d3.scaleLinear().range([height, 0]);
var xAxis ;

function tickLabels(dataLength, d) {
    if (dataLength > 9) return "";
    return d.replace(/^.+_/, '')
}

/*  var xAxis = d3.axisBottom().scale(x)
           .ticks()
           .tickFormat(function(d,i) {  return tickLabels(toCSV.length, d) })*/
/*   
   var ticks = d3.selectAll(".tick text");
   ticks.attr("class", function(d,i){
       if(i%3 != 0) d3.select(this).remove();
   });
   
   
   */


if (all.length < 16 ){
    xAxis = d3.axisBottom().scale(x).ticks(10);
}
else
{
    xAxis = d3.axisBottom().scale(x)
      .tickFormat(function(d, i) {
          return i % 3 === 0 ? d : null;
      });
}
//     var xAxis = d3.axisBottom().scale(x).ticks(10);

var yAxis = d3.axisLeft().scale(y).ticks(5).tickSizeInner(-width).tickSizeOuter(0).tickPadding(10);


// Define the line

var valueline = d3.line().curve(d3.curveCatmullRom)
  .x(function (d) {
      return x(d.label) + x.bandwidth() / 2;
  })
  .y(function (d) {
      return y(d.value);
  });
var data = all;


// Get the data
data.forEach(function (d) {
    d.value = +d.value;
});

// Scale the range of the data
x.domain(data.map(function (d) {return d.label;}));
y.domain([0, d3.max(data, function (d) {return d.value;})]);

// Add the valueline path.
svg.append("path") // Add the valueline path.
  .attr("d", valueline(data))
  .attr("class", "line");

// Add the scatterplot
svg.selectAll("dot")
  .data(data)
  .enter().append("circle")
  .attr("r", 4)
  .attr("cx", function (d) {return x(d.label) + x.bandwidth() / 2;})
  .attr("cy", function(d) { return y(d.value); }).on("mouseover", function() {
    tooltip.style("display", null);
})
  /* .on("mouseout", function() {
     tooltip.transition()       
         .duration(500)     
         .style("opacity", 0);  
     d3.select(this)
       .attr("fill", "black")
        .attr("r", 4);
   })
*/

  .on("mouseover", function() { tooltip.style("display", null); })
  .on("mouseout", function() {  tooltip.style("display", "none");
      d3.select(this)
        .attr("fill", "black")
        .attr("r", 4);})


  .on("mousemove", function(d) {
      d3.select(this)
        .attr("fill", "red")
        .attr("r", 8);

      tooltip.transition().duration(200)
        .style("opacity", 0.9);
      tooltip.select("div").html( d.name +":" +" <br><strong>"  + d.value + "</strong>")
        .style("position", "fixed")
        .style("text-align", "center")
        .style("width", "120px")
        .style("height", "45px")
        .style("padding", "2px")
        .style("font", "12px sans-serif")
        .style("background", "lightsteelblue")
        .style("border", "0px")
        .style("border-radius", "8px")
        .style("left", (d3.event.pageX + 15) + "px")
        .style("top", (d3.event.pageY - 28) + "px");

  });
var tooltip = d3.select("body").append("div")
  .attr("class", "tooltip")
  .style("opacity", 0.5);

tooltip.append("rect")
  .attr("width", 30)
  .attr("height", 20)
  .attr("fill", "#ffffff")
  .style("opacity", 0.5);

tooltip.append("div")
  .attr("x", 15)
  .attr("dy", "1.2em")
  .style("text-anchor", "middle")
  .attr("font-size", "1.5em")
  .attr("font-weight", "bold");


// Add the X Axis
svg.append("g")
  .attr("class", "x axis")
  .attr("transform", "translate(0," + height + ")")
  .call(xAxis)
  .selectAll("text")
  .style("text-anchor", "end")
  .attr("dx", "0.1em")
  .attr("dy", "-1em")
  .attr("y", 30)
  .attr("transform", "rotate(-30)");


// Add the Y Axis
svg.append("g")
  .attr("class", "y axis")
  .call(yAxis);

svg.append("text")
  .attr("transform", "rotate(-90)")
  .attr("y",-30)
  .attr("x", 0 - (height / 2))
  .attr("dy", "-2em")
  .attr("text-anchor", "middle")
  .style("font-size", "13px")
  .text("Count");

Solution

  • Lots of ways to do what you want. This solution brute force searches for the closest point to the mouseover location on the line:

    .attr("class", "line")
      .on("mouseover", function(d) {
          var mx = d3.mouse(svg.node())[0], // x position of cursor
            c = 1e10,
            idx = -1;
          xs.forEach(function(x, i) { //xs is an array of points x location
              var dis = Math.abs(x - mx); //distance
              if (dis < c) {
                  c = dis;
                  idx = i; // find closest
              }
          });
          var dot = dots.nodes()[idx], //get dot
            d3Dot = d3.select(dot);
          d3Dot.on("mouseover").call(dot, d3Dot.datum()); //call it's mouseover event
      })
      .on("mouseout", function(d){
          tooltip.style("display", "none");
          dots
            .attr("fill", "black")
            .attr("r", 4);
      });
    

    Running code:

    <!DOCTYPE html>
    <html>
    
    <head>
      <script data-require="d3@4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
      <style>
          .chart {
          background: #eee;
          padding: 3px;
        }
        
        .chart div {
          width: 0;
          transition: all 1s ease-out;
          -moz-transition: all 1s ease-out;
          -webkit-transition: all 1s ease-out;
        }
        
        .chart div {
          font: 10px sans-serif;
          background-color: steelblue;
          text-align: right;
          padding: 3px;
          margin: 5px;
          color: white;
          box-shadow: 2px 2px 2px #666;
        }
        
        .bar {
          fill: orange;
        }
        
        .bar:hover {
          fill: red;
        }
        
        rect.background {
          fill: white;
        }
        
        .axis {
          shape-rendering: crispEdges;
        }
        
        .axis path,
        .axis line {
          fill: none;
          stroke: #000;
        }
        
        tooltip {
          position: absolute;
          text-align: center;
          width: 120px;
          height: 45px;
          padding: 2px;
          font: 12px sans-serif;
          background: lightsteelblue;
          border: 0px;
          border-radius: 8px;
          pointer-events: none;
        }
        
        bar {
          fill: #8CD3DD;
        }
        
        bar:hover {
          fill: #F56C4E;
        }
        /* .tick:nth-child(3n) text {
        visibility: hidden;
    } */
        
        .d3-tip {
          line-height: 1;
          font-weight: bold;
          padding: 12px;
          background: rgba(0, 0, 0, 0.8);
          color: #fff;
          border-radius: 2px;
        }
        /* Creates a small triangle extender for the tooltip */
        
        .d3-tip:after {
          box-sizing: border-box;
          display: inline;
          font-size: 10px;
          width: 100%;
          line-height: 1;
          color: rgba(0, 0, 0, 0.8);
          content: "\25BC";
          position: absolute;
          text-align: center;
        }
        /* Style northward tooltips differently */
        
        .d3-tip.n:after {
          margin: -1px 0 0 0;
          top: 100%;
          left: 0;
        }
        
        Creates a small triangle extender for the tooltip .d3-tip:after {
          box-sizing: border-box;
          display: inline;
          font-size: 10px;
          width: 100%;
          line-height: 1;
          color: rgba(0, 0, 0, 0.8);
          content: "\25BC";
          position: absolute;
          text-align: center;
        }
        
        Style northward tooltips differently .d3-tip.n:after {
          margin: -1px 0 0 0;
          top: 100%;
          left: 0;
        }
        
        svg text.label {
          fill: #ff0000;
          font: 25px;
          text-anchor: middle;
          font-weight: 400;
          text-anchor: middle;
          font-size: 125%;
          text-anchor: start;
        }
        
        path {
          stroke: steelblue;
          stroke-width: 2;
          /*   fill: none; */
          /* commenting out to show multiple ring donut chart. */
        }
        
        pathline {
          fill: none;
          stroke-width: 2;
          stroke: #000;
        }
        
        .axis path,
        .axis line {
          fill: none;
          stroke: grey;
          stroke-width: 1;
          shape-rendering: crispEdges;
        }
        
        .graph {
          width: auto;
        }
        
        .tooltip {
          color: black;
        }
        
        .axis {
          font: 12px sans-serif, Georgia, Arial;
        }
        
        .axis path,
        .axis line {
          fill: none;
          stroke: #dadada;
          shape-rendering: crispEdges;
        }
        /* ANGULAR MODAL */
        
        .dialogdemoBasicUsage #popupContainer {
          position: relative;
        }
        
        .dialogdemoBasicUsage .footer {
          width: 100%;
          text-align: center;
          margin-left: 20px;
        }
        
        .dialogdemoBasicUsage .footer,
        .dialogdemoBasicUsage .footer > code {
          font-size: 0.8em;
          margin-top: 50px;
        }
        
        .dialogdemoBasicUsage button {
          width: 200px;
        }
        
        .dialogdemoBasicUsage div#status {
          color: #c60008;
        }
        
        .dialogdemoBasicUsage .dialog-demo-prerendered md-checkbox {
          margin-bottom: 0;
        }
        /* Angular grids */
        
        .gridListdemoBasicUsage md-grid-list {
          margin: 8px;
        }
        
        gridListdemoBasicUsage .gray {
          background: #f5f5f5;
        }
        
        .gridListdemoBasicUsage .green {
          background: #b9f6ca;
        }
        
        .gridListdemoBasicUsage .yellow {
          background: #ffff8d;
        }
        
        .gridListdemoBasicUsage .blue {
          background: #84ffff;
        }
        
        .gridListdemoBasicUsage .purple {
          background: #b388ff;
        }
        
        .gridListdemoBasicUsage .red {
          background: #ff8a80;
        }
        
        .gridListdemoBasicUsage md-grid-tile {
          transition: all 400ms ease-out 50ms;
        }
        
        md-grid-list md-grid-tile md-grid-tile-footer,
        md-grid-list md-grid-tile md-grid-tile-header {
          height: 25px;
        }
        
        .svg-container {
          display: inline-block;
          position: relative;
          width: 100%;
          padding-bottom: 12%;
          vertical-align: top;
          overflow: hidden;
        }
        
        .line {
          fill: none;
          stroke: steelblue;
          stroke-width: 3px;
        }
        
        .svg-content-responsive {
          display: inline-block;
          position: absolute;
          top: 10px;
          left: 0;
        }
        
        .svg-content {
          display: inline-block;
          position: absolute;
          top: 0;
          left: 0;
        }
        
        #linechart {
          width: 100%;
          height: 100%;
          position: absolute;
        }
        /* TESTING GRIDS */
        
        .chart-tile {
          display: block;
          height: 100%;
          width: 100%;
        }
        
        .gridListdemoDynamicTiles .s64 {
          font-size: 64px;
        }
        
        .gridListdemoDynamicTiles .s32 {
          font-size: 48px;
        }
        
        .gridListdemoDynamicTiles md-grid-list {
          margin: 8px;
        }
        
        .gridListdemoDynamicTiles md-grid-tile {
          box-shadow: 2px 2px 3px 3px #888888;
          transition: all 300ms ease-out 50ms;
        }
        
        .gridListdemoDynamicTiles md-grid-tile md-grid-tile-footer {
          background: rgba(0, 0, 0, 0.68);
          height: 36px;
        }
        
        .gridListdemoDynamicTiles md-grid-tile-footer figcaption {
          width: 100%;
        }
        
        .gridListdemoDynamicTiles md-grid-tile-footer figcaption h3 {
          margin: 0;
          font-weight: 700;
          width: 100%;
          text-align: center;
        }
        /*
    Copyright 2016 Google Inc. All Rights Reserved. 
    Use of this source code is governed by an MIT-style license that can be in foundin the LICENSE file at https://material.angularjs.org/license.
    */
        
        @media (min-width: 1200px) {
          .container {
            width: 1400px;
            margin-right: -50px;
          }
        }
        
        @media only screen and (max-width: 260px) and (min-width: 1600px) {
          .container {
            width: 1700px;
            margin-right: -50px;
          }
        }
        
        #wrapper {
          border-style: solid;
          height: 100px;
          width: 200px;
        }
        
        #dropdown {
          vertical-align: middle;
          width: 80px;
          margin-left: 300px;
          margin-top: -30px;
        }
        /* Styles go here */
        
        .gridster-item {
          background-color: darkgrey;
        }
        
        .my-class {
          border: 1px solid red;
          height: 50px;
        }
        
        body {
          background-color: #fcfcfc;
        }
        
        .container {
          text-align: center;
          padding: 15px;
        }
        
        .left-div {
          display: inline-block;
          max-width: 300px;
          text-align: left;
          padding: 30px;
          background-color: #ddd;
          border-radius: 3px;
          margin: 15px;
          vertical-align: top;
        }
        
        .right-div {
          display: inline-block;
          max-width: 150px;
          text-align: left;
          padding: 30px;
          background-color: #ddd;
          border-radius: 3px;
          margin: 15px;
        }
        
        .left-text,
        .right-text {
          font: 300 16px/1.6 'Helvetica Neue' sans-serif;
          color: #333;
        }
        
        @media screen and (max-width: 600px) {
          .left-div, .right-div {
            max-width: 100%;
          }
      </style>
    </head>
    
    <body>
      <div id="charts">
    
      </div>
      <script>
        var all = [{
          "name": "SEASONAL_LYQ1",
          "code": "SEASONAL_LYQ1",
          "parent": "SEASONAL_POP",
          "value": "0",
          "label": "LYQ1",
          "children": []
        }, {
          "name": "SEASONAL_LYQ2",
          "code": "SEASONAL_LYQ2",
          "parent": "SEASONAL_POP",
          "value": "10",
          "label": "LYQ2",
          "children": []
        }, {
          "name": "SEASONAL_LYQ3",
          "code": "SEASONAL_LYQ3",
          "parent": "SEASONAL_POP",
          "value": "16",
          "label": "LYQ3",
          "children": []
        }, {
          "name": "SEASONAL_LYQ4",
          "code": "SEASONAL_LYQ4",
          "parent": "SEASONAL_POP",
          "value": "10",
          "label": "LYQ4",
          "children": []
        }, {
          "name": "SEASONAL_CYQ1",
          "code": "SEASONAL_CYQ1",
          "parent": "SEASONAL_POP",
          "value": "0",
          "label": "CYQ1",
          "children": []
        }, {
          "name": "SEASONAL_CYQ2",
          "code": "SEASONAL_CYQ2",
          "parent": "SEASONAL_POP",
          "value": "10",
          "label": "CYQ2",
          "children": []
        }, {
          "name": "SEASONAL_CYQ3",
          "code": "SEASONAL_CYQ3",
          "parent": "SEASONAL_POP",
          "value": "16",
          "label": "CYQ3",
          "children": []
        }, {
          "name": "SEASONAL_CYQ4",
          "code": "SEASONAL_CYQ4",
          "parent": "SEASONAL_POP",
          "value": "10",
          "label": "CYQ4",
          "children": []
        }];
        var margin = {
            top: 30,
            right: 20,
            bottom: 100,
            left: 80
          },
          width = 400 - margin.left - margin.right,
          height = 300 - margin.top - margin.bottom;
    
    
    
        var svg = d3.select("#charts").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 + ")")
          .attr("preserveAspectRatio", "xMinYMin meet")
          .attr("viewBox", "0 0 600 400")
          //class to make it responsive
          .classed("svg-content-responsive", true);;
    
    
        // Set the ranges
    
        var x = d3.scaleBand().rangeRound([0, width]).padding(0.1);
        var y = d3.scaleLinear().range([height, 0]);
        var xAxis;
    
        function tickLabels(dataLength, d) {
          if (dataLength > 9) return "";
          return d.replace(/^.+_/, '')
        }
    
        /*  var xAxis = d3.axisBottom().scale(x)
                                        .ticks()
                                        .tickFormat(function(d,i) {  return tickLabels(toCSV.length, d) })*/
        /*   
                       var ticks = d3.selectAll(".tick text");
                       ticks.attr("class", function(d,i){
                           if(i%3 != 0) d3.select(this).remove();
                       });
                       
                       
                       */
    
    
        if (all.length < 16) {
          xAxis = d3.axisBottom().scale(x).ticks(10);
        } else {
          xAxis = d3.axisBottom().scale(x)
            .tickFormat(function(d, i) {
              return i % 3 === 0 ? d : null;
            });
        }
        //     var xAxis = d3.axisBottom().scale(x).ticks(10);
    
        var yAxis = d3.axisLeft().scale(y).ticks(5).tickSizeInner(-width).tickSizeOuter(0).tickPadding(10);
    
    
        // Define the line
    
        var valueline = d3.line().curve(d3.curveCatmullRom)
          .x(function(d) {
            return x(d.label) + x.bandwidth() / 2;
          })
          .y(function(d) {
            return y(d.value);
          });
        var data = all;
    
    
        // Get the data
        data.forEach(function(d) {
          d.value = +d.value;
        });
    
        // Scale the range of the data
        x.domain(data.map(function(d) {
          return d.label;
        }));
        y.domain([0, d3.max(data, function(d) {
          return d.value;
        })]);
    
        // Add the valueline path.
        svg.append("path") // Add the valueline path.
          .attr("d", valueline(data))
          .attr("class", "line")
          .on("mouseover", function(d) {
            var mx = d3.mouse(svg.node())[0],
              c = 1e10,
              idx = -1;
            xs.forEach(function(x, i) {
              var dis = Math.abs(x - mx);
              if (dis < c) {
                c = dis;
                idx = i;
              }
            });
            var dot = dots.nodes()[idx],
              d3Dot = d3.select(dot);
            d3Dot.on("mouseover").call(dot, d3Dot.datum());
          })
          .on("mouseout", function(d){
            tooltip.style("display", "none");
            dots
              .attr("fill", "black")
              .attr("r", 4);
          })
    
    
        var xs = [];
        // Add the scatterplot
        var dots = svg.selectAll(".dot")
          .data(data)
          .enter().append("circle")
          .attr("class", "dot")
          .attr("r", 4)
          .attr("cx", function(d) {
            var xp = x(d.label) + x.bandwidth() / 2;
            xs.push(xp);
            return xp;
          })
          .attr("cy", function(d) {
            return y(d.value);
          })
          .on("mouseout", function() {
            tooltip.style("display", "none");
            d3.select(this)
              .attr("fill", "black")
              .attr("r", 4);
          })
          .on("mouseover", function(d) {
            
            tooltip.style("display", "block");
    
            var self = d3.select(this);
    
            self
              .attr("fill", "red")
              .attr("r", 8);
    
            tooltip.transition().duration(200)
              .style("opacity", 0.9);
    
            tooltip.select("div").html(d.name + ":" + " <br><strong>" + d.value + "</strong>")
              .style("position", "fixed")
              .style("text-align", "center")
              .style("width", "120px")
              .style("height", "45px")
              .style("padding", "2px")
              .style("font", "12px sans-serif")
              .style("background", "lightsteelblue")
              .style("border", "0px")
              .style("border-radius", "8px")
              .style("left", (+self.attr('cx') + 100) + "px")
              .style("top", (+self.attr('cy')) + "px");
          });
    
        var tooltip = d3.select("body").append("div")
          .attr("class", "tooltip")
          .style("opacity", 0.5);
    
        tooltip.append("rect")
          .attr("width", 30)
          .attr("height", 20)
          .attr("fill", "#ffffff")
          .style("opacity", 0.5);
    
        tooltip.append("div")
          .attr("x", 15)
          .attr("dy", "1.2em")
          .style("text-anchor", "middle")
          .attr("font-size", "1.5em")
          .attr("font-weight", "bold");
    
    
        // Add the X Axis
        svg.append("g")
          .attr("class", "x axis")
          .attr("transform", "translate(0," + height + ")")
          .call(xAxis)
          .selectAll("text")
          .style("text-anchor", "end")
          .attr("dx", "0.1em")
          .attr("dy", "-1em")
          .attr("y", 30)
          .attr("transform", "rotate(-30)");
    
    
        // Add the Y Axis
        svg.append("g")
          .attr("class", "y axis")
          .call(yAxis);
    
        svg.append("text")
          .attr("transform", "rotate(-90)")
          .attr("y", -30)
          .attr("x", 0 - (height / 2))
          .attr("dy", "-2em")
          .attr("text-anchor", "middle")
          .style("font-size", "13px")
          .text("Count");
      </script>
    </body>
    
    </html>