Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

D3: Get nearest value from ordinal axis on mouseover

In my D3 line chart, i´m trying to create a mouseover effect like in this example: http://bl.ocks.org/mbostock/3902569

In this example the author uses the bisector function, which is, as far as i understand, only supported for linear scales. The problem is, that in my chart i have an ordinal x axis with different, discrete rangePoint tuples. So if it is like in the situation below (m = mouse position), i want to get the pixel position of the closest x value which would be x2 in this example.

         m
         |
x1----------x2----------x3

Is there any way to do that?

like image 763
flixe Avatar asked Dec 10 '15 21:12

flixe


2 Answers

Using your linked example, here's a quick implementation of the mousemove function for an ordinal scale:

var tickPos = x.range();
function mousemove(d){
  var m = d3.mouse(this),
      lowDiff = 1e99,
      xI = null;
  // if you have a large number of ticks
  // this search could be optimized
  for (var i = 0; i < tickPos.length; i++){
    var diff = Math.abs(m[0] - tickPos[i]);
    if (diff < lowDiff){
      lowDiff = diff;
      xI = i;
    }
  }
  focus
    .select('text')
    .text(ticks[xI]);
  focus
    .attr("transform","translate(" + tickPos[xI] + "," + y(data[xI].y) + ")");
}

Full code:

<!DOCTYPE html>
<meta charset="utf-8">
<style>
  body {
    font: 10px sans-serif;
  }
  
  .axis path,
  .axis line {
    fill: none;
    stroke: #000;
    shape-rendering: crispEdges;
  }
  
  .line {
    fill: none;
    stroke: steelblue;
    stroke-width: 1.5px;
  }
  
  .overlay {
    fill: none;
    pointer-events: all;
  }
  
  .focus circle {
    fill: none;
    stroke: steelblue;
  }
</style>

<body>
  <script src="//d3js.org/d3.v3.min.js"></script>
  <script>
    var margin = {
        top: 20,
        right: 20,
        bottom: 30,
        left: 50
      },
      width = 960 - margin.left - margin.right,
      height = 500 - margin.top - margin.bottom;


    var x = d3.scale.ordinal()
      .rangeRoundPoints([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()
      .x(function(d) {
        return x(d.x);
      })
      .y(function(d) {
        return y(d.y);
      });

    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 = [{
      x: 'A',
      y: Math.random() * 10
    }, {
      x: 'B',
      y: Math.random() * 10
    }, {
      x: 'C',
      y: Math.random() * 10
    }, {
      x: 'D',
      y: Math.random() * 10
    }, {
      x: 'E',
      y: Math.random() * 10
    }, {
      x: 'F',
      y: Math.random() * 10
    }, {
      x: 'G',
      y: Math.random() * 10
    }, {
      x: 'H',
      y: Math.random() * 10
    }, {
      x: 'I',
      y: Math.random() * 10
    }, {
      x: 'J',
      y: Math.random() * 10
    }];

    var ticks = data.map(function(d) {
      return d.x
    });
    x.domain(ticks);
    y.domain(d3.extent(data, function(d) {
      return d.y;
    }));

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

    svg.append("path")
      .datum(data)
      .attr("class", "line")
      .attr("d", line);

    var focus = svg.append("g")
      .attr("class", "focus")
      .style("display", "none");

    focus.append("circle")
      .attr("r", 4.5);

    focus.append("text")
      .attr("x", 9)
      .attr("dy", ".35em")
      .style('')

    svg.append("rect")
      .attr("class", "overlay")
      .attr("width", width)
      .attr("height", height)
      .on("mouseover", function() {
        focus.style("display", null);
      })
      .on("mouseout", function() {
        focus.style("display", "none");
      })
      .on("mousemove", mousemove);
    
    var tickPos = x.range();
    function mousemove(d){
      var m = d3.mouse(this),
          lowDiff = 1e99,
          xI = null;
      for (var i = 0; i < tickPos.length; i++){
        var diff = Math.abs(m[0] - tickPos[i]);
        if (diff < lowDiff){
          lowDiff = diff;
          xI = i;
        }
      }
      focus
        .select('text')
        .text(ticks[xI]);
      focus
        .attr("transform","translate(" + tickPos[xI] + "," + y(data[xI].y) + ")");
    }
  </script>
like image 98
Mark Avatar answered Nov 17 '22 09:11

Mark


Simple Solution:

.on("mousemove", function () {
            const x0 = x.invert(d3.mouse(d3.event.currentTarget)[0]),
                i = bisectDate(data, x0, 1),
                d0 = data[i - 1],
                d1 = data[i];

            const rangeValueOfFirst = x(d0.date),
                rangeValueOfSecond = x(d1.date),
                rangeValueOfMousePos = d3.mouse(d3.event.currentTarget)[0],
                closestD = Math.abs(rangeValueOfMousePos - rangeValueOfFirst) > Math.abs(rangeValueOfMousePos - rangeValueOfSecond) ? d1 : d0;

            const focus = d3.select(".focus");
            focus.attr("transform", () => "translate(" + x(closestD.date) + "," + y(closestD.close) + ")");
            focus.select("text").text(closestD.close);
        });
like image 27
Donevski Avatar answered Nov 17 '22 10:11

Donevski