Laran Evans
Laran Evans

Reputation: 1313

How can I keep a d3 mouseover open while my mouse is over the tooltip?

I have a d3 scatterplot. I have a tooltip which shows when I mouseover a point. I want to do two things.

1) I want the mouseover to stay open as long as my mouse is either over the point or the tooltip. 2) I want to put a clickable link in the tooltip. I believe that #1 is required in order to make this work.

How can I do this?

Here is my code: https://github.com/laran/eisenhower/blob/master/components/plot/scatterplot.js

Upvotes: 3

Views: 2525

Answers (2)

Mark
Mark

Reputation: 108537

One idea may be to create a delayed transition on the mouseout of the circle to allow the user time to mouse to the tooltip. If they mouseover the circle in time, you cancel the transition and hide the tooltip on mouseout of tooltip div:

// create tooltip
var tip = d3.select('body')
  .append('div')
  .attr('class', 'tip')
  .html('I am a tooltip...')
  .style('border', '1px solid steelblue')
  .style('padding', '5px')
  .style('position', 'absolute')
  .style('display', 'none')
  .on('mouseover', function(d, i) {
    tip.transition().duration(0);  // on mouse over cancel circle mouse out transistion
  })
  .on('mouseout', function(d, i) {
    tip.style('display', 'none');  // on mouseout hide tip
  });

 ...

 // mouseover and out of circle
.on('mouseover', function(d, i) {
    tip.transition().duration(0); // cancel any pending transition
    tip.style('top', y(d.y) - 20 + 'px');
    tip.style('left', x(d.x) + 'px');
    tip.style('display', 'block');
  })
  .on('mouseout', function(d, i) {
    tip.transition()
    .delay(500)
    .style('display', 'none'); // give user 500ms to move to tooltip
  });

Here's a quick example.

<!DOCTYPE html>
<html>

<head>
  <script src="https://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>

<body>
  <script>
    // data that you want to plot, I've used separate arrays for x and y values
    var data = [{
        x: Math.random() * 10,
        y: Math.random() * 10
      },
      {
        x: Math.random() * 10,
        y: Math.random() * 10
      },
      {
        x: Math.random() * 10,
        y: Math.random() * 10
      },
      {
        x: Math.random() * 10,
        y: Math.random() * 10
      }
    ];


    xdata = [5, 10, 15, 20],
      ydata = [3, 17, 4, 6];

    // size and margins for the chart
    var margin = {
        top: 20,
        right: 15,
        bottom: 60,
        left: 60
      },
      width = 500 - margin.left - margin.right,
      height = 400 - margin.top - margin.bottom;

    var x = d3.scale.linear()
      .domain([0, 10])
      .range([0, width]);

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

    var tip = d3.select('body')
      .append('div')
      .attr('class', 'tip')
      .html('I am a tooltip...')
      .style('border', '1px solid steelblue')
      .style('padding', '5px')
      .style('position', 'absolute')
      .style('display', 'none')
      .on('mouseover', function(d, i) {
        tip.transition().duration(0);
      })
      .on('mouseout', function(d, i) {
        tip.style('display', 'none');
      });

    var chart = d3.select('body')
      .append('svg')
      .attr('width', width + margin.right + margin.left)
      .attr('height', height + margin.top + margin.bottom)
      .attr('class', 'chart')

    var main = chart.append('g')
      .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
      .attr('width', width)
      .attr('height', height)
      .attr('class', 'main')

    // draw the x axis
    var xAxis = d3.svg.axis()
      .scale(x)
      .orient('bottom');

    main.append('g')
      .attr('transform', 'translate(0,' + height + ')')
      .attr('class', 'main axis date')
      .call(xAxis);

    // draw the y axis
    var yAxis = d3.svg.axis()
      .scale(y)
      .orient('left');

    main.append('g')
      .attr('transform', 'translate(0,0)')
      .attr('class', 'main axis date')
      .call(yAxis);

    // draw the graph object
    var g = main.append("svg:g");

    g.selectAll("scatter-dots")
      .data(data)
      .enter().append("svg:circle")
      .attr("cy", function(d) {
        return y(d.y);
      })
      .attr("cx", function(d, i) {
        return x(d.x);
      })
      .attr("r", 10)
      .style("opacity", 0.6)
      .on('mouseover', function(d, i) {
        tip.transition().duration(0);
        setTimeout(() => {
          tip.style('top', y(d.y) - 20 + 'px');
          tip.style('left', x(d.x) + 'px');
          tip.style('display', 'block');
        }, 500);
      })
      .on('mouseout', function(d, i) {
        tip.transition()
          .delay(500)
          .style('display', 'none');
      })
  </script>
</body>

</html>
LICENSE

Upvotes: 11

meetamit
meetamit

Reputation: 25157

There is probably no easy way to do it.

One option is to nest the tooltip inside the same container as the circle (i.e. an svg <g>) and attach the mouse events to that parent, so that when the mouse goes between the tooltip and circle it won't trigger mouseout. That'll make it hard to transition the tooltip between circles, because it would involve detaching it from one parent and attaching to the other.

Probably the simpler option is to attach a mouseover and mouseout events to the tooltip, and set a flag (like isOverTooltip = true or false) to keep track of where the mouse is. Then check this flag in the mouseout of the circle to determine whether or not to hide the tooltip. In this case, inside the mouseout of the circle, you would want to hide the tooltip from within a setTimeout (and of course only when !isOverTooltip), in order to allow time for the mouse to travel between circle and tooltip without the tooltip disappearing.

Upvotes: 2

Related Questions