My Favorite Tooltip: The Final Block

Understand the final block of code that projects a circle to the tooltip location.

Adding the circle to the graph

Adding the circle to the graph is actually fairly simple:

    focus.append("circle")
        .attr("class", "y")
        .style("fill", "none")
        .style("stroke", "blue")
        .attr("r", 4);

If you’ve followed any of the other examples in “D3 Tips and Tricks,” there shouldn’t be any surprises here (well, perhaps assigning a class to the circle (y) could count as mildly unusual).

But there is one small thing…

We don’t place it anywhere on the graph! There are no x, y coordinates and no translation of position. Nothing! Never fear; All we want to do at this stage is create the element. In a few blocks of code time, we will move the circle.

Set the area to capture the mouse movements

As we briefly covered earlier, the thing that makes this particular tooltip technique different is we don’t hover over an element to highlight the tooltip. Instead, we move the mouse into an area that is relevant to the tooltip and it appears.

And it’s all thanks to the following code:

  svg.append("rect")
      .attr("width", width)
      .attr("height", height)
      .style("fill", "none")
      .style("pointer-events", "all")
      .on("mouseover", function() { focus.style("display", null); })
      .on("mouseout", function() { focus.style("display", "none"); })
      .on("mousemove", mousemove);

Here, we’re adding a rectangle to the graph (svg.append("rect")) with the same height and width as our graph area (.attr("width", width) and .attr("height", height)), and we’re making sure that there’s no colour (fill) in it (.style("fill", "none")). There’s nothing too weird about all that.

Then we make sure that if any mouse events occur within the area, we capture them (.style("pointer-events", "all")). This is when things start to get interesting.

The first pointer event that we want to work with is mouseover:

    .on("mouseover", function() { focus.style("display", null); })

This line of code tells the script that when the mouse moves over the area of the rectangle of the area of the graph, the display properties of the focus elements (remember that we appended our circle to focus earlier) are set to null. This might sound like a bit of a strange thing to do since what we want to do is make sure that when the mouse moves over the graph, we want the focus elements to be displayed. But by setting the display style to null, the default value for display is enacted, and this is inline which allows the elements to be rendered as normal. So why not use inline instead of null? Good question. I’ve tried it, and it works without a problem. But the original example that Mike Bostock used had the setting at null, and I’ll make the assumption that Mike knows something that I don’t know about, like when to use null and when to use inline for a display style.

The reverse of making our focus element display everything is by setting it to stop displaying everything. This is what happens in the next line:

    .on("mouseout", function() { focus.style("display", "none"); })

Here, where the mouse moves off the area, the display properties for the focus element are turned off.

Lastly, for this block, we need to capture the actions of the mouse as it moves on the graph area and moves our tooltips as required. This is accomplished with the final line in the block:

    .on("mousemove", mousemove);

If the mouse moves, we call the mousemove function.

Determining which date will be highlighted

Once the mousemove function is called, it carries out the last two steps in our code. The first of which is the clever maths that determines which point in our graph has the tooltip applied to it.

	  var x0 = x.invert(d3.pointer(event,this)[0]),
		  i = bisectDate(data, x0, 1),
		  d0 = data[i - 1],
		  d1 = data[i],
		  d = x0 - d0.date > d1.date - x0 ? d1 : d0;

The first line of this block is a doozy:

	  var x0 = x.invert(d3.pointer(event,this)[0]),

If we break it down, the d3.pointer(event,this)[0] portion returns the x position on the screen of the mouse (d3.pointer(event,this)[1] would return the y position). Then the x.invert function is reversing the process that we use to map the domain (date) to the range (position on screen). So it takes the position on the screen and converts it into an equivalent date!

For the adventurous amongst you, throw a “console.log(x0);” line into the mousemove function and check out the changing date/time as the cursor moves pixel by pixel. This will work for Google Chrome. Very cool.

Then we use our bisectDate function that we declared earlier to find the index of our data array that is close to the mouse cursor.

		  i = bisectDate(data, x0, 1),

It takes our data array and the date corresponding to the position of our mouse cursor and returns the index number of the data array, which has a date that is higher than the cursor position.

Then we declare arrays that are subsets of our data array:

		  d0 = data[i - 1],
		  d1 = data[i],

d0 is the combination of date and close that is in the data array at the index to the left of the cursor, and d1 is the combination of date and close that is in the data array at the index to the right of the cursor. In other words, we now have two variables that know the value and date above and below the date that corresponds to the position of the cursor.

The final line in this segment declares a new array d that represents the date and close combination that is closest to the cursor.

		  d = x0 - d0.date > d1.date - x0 ? d1 : d0;

It is using the magic JavaScript shorthand for an if statement that is essentially saying: if the distance between the mouse cursor and the date and close combination on the left is greater than the distance between the mouse cursor and the date and close combination on the right, then d is an array of the date and close on the right of the cursor (d1). Otherwise, d is an array of the date and close on the left of the cursor (d0).

This could be regarded as a fairly complicated little piece of code, but if you take the time to understand it, you will be surprised how elegant it appears. As we’ve seen before though, if you just want to believe that the D3.js magic is happening, that’s fine.

Move the circle to the appropriate position

The final block of code that we’ll check out takes the closest date/close combination that we’ve just worked out and moves the circle to that position:

	focus.select("circle.y")
	   .attr("transform",
	         "translate(" + x(d.date) + "," +
	                        y(d.close) + ")");

This is a pretty easy bit of code to follow. We select the circle (using the class y that we assigned to it earlier) and then move it using translate to the date/close position that we had just worked out, which was the closest.

Of course, this is provisioning the coordinates to the circle that we noticed was missing earlier in the code when we were appending it to the graph.

And there we have it. It is a simple circle positioned at the closest point to the mouse cursor when the cursor hovers over the graph.

Get hands-on with 1200+ tech skills courses.