高亮最近点d3js折线图和点图

Highlight closest point d3js line and dot chart

我目前有一个 d3js 折线图和点图。截至目前,当我将鼠标悬停在点上时,它会突出显示该点并显示显示的值。我想看到的是,当我将鼠标悬停在该线上时,它会将鼠标光标悬停在灯光上最接近的点上,并执行与悬停在某个点上时相同的操作。我确实检查了平分线但不确定它是否仅对带日期的数据有用。下面是 link 对应 fiddle 的图表。

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");

有很多方法可以做你想做的事。本解法暴力搜索直线上距离mouseover位置最近的点:

.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);
  });

运行代码:

<!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: "BC";
      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: "BC";
      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>