D3.js Step by Step: Adding Tooltips

Harnessing mouse events to layer on additional information

UPDATE (July 18, 2016): The code and API links in these tutorials have been updated to target D3 v4, which was a complete rewrite. The D3 wiki contains a breakdown of the changes from v3.

TL;DR

This post is part of a series that explores some key concepts in D3.js by building up an example, step by step, from a bare-bones pie chart to an interactive, animated donut chart that loads external data. For the enough-with-the-jibber-jabber-show-me-the-code types out there, here's a breakdown of the steps we'll be covering:

NOTE: Because we're building things up step by step, the source code contains NEW, UPDATED and REMOVED comments to annotate the lines that have been added, altered or deleted relative to the previous step.

One thing you may have noticed about this new dataset is that the segments are all similarly sized . One thing we could do to help clarify the relative sizes—apart from switching to a bar chart—is provide tooltips with additional information. You can see it in action here:

When you hover over one of the sections, a tooltip appears with the weekday, the total number of tickets issued on that day, and the percentage of the total that number represents. In this case the tooltip stays fixed in place, however we'll see how to make it move with the mouse.

First we need to define some more CSS:

#chart {
  height: 360px;
  position: relative;
  width: 360px;
}

This is the first time we've botherd adding any styles to the div that wraps the SVG. We're setting it to the same dimensions as the chart itself and setting position: relative; so that the tooltip can be positioned absolutely relative to the container rather than to the body. Now for the tooltip itself:

.tooltip {
  background: #eee;
  box-shadow: 0 0 5px #999999;
  color: #333;
  display: none;
  font-size: 12px;
  left: 130px;
  padding: 10px;
  position: absolute;
  text-align: center;
  top: 95px;
  width: 80px;
  z-index: 10;
}

This is pretty standard if you're familiar with CSS. (I like to order my CSS properties alphabetically, which may not be to everyone's taste.) The main properties to take note of are left and top, as their values are specific to the size of the chart. They are meant to position the tooltip centered at the top of the donut hole. Also note that we've set display: none; so that it doesn't appear by default.

With the CSS taken care of, we can add the tooltip itself. We're just going to add one tooltip element and reuse it for all the segments of the chart:

// ...
// More code
// ...

var tooltip = d3.select('#chart')            // NEW
  .append('div')                             // NEW
  .attr('class', 'tooltip');                 // NEW

tooltip.append('div')                        // NEW
  .attr('class', 'label');                   // NEW

tooltip.append('div')                        // NEW
  .attr('class', 'count');                   // NEW

tooltip.append('div')                        // NEW
  .attr('class', 'percent');                 // NEW

d3.csv('weekdays.csv', function(error, dataset) {
  // ...
  // More code
  // ...
});

Because this bit of code doesn't depend on the data, we can put it outside the callback. We start by adding a div with class tooltip to the container, and then add three div elements to it, one after the other, with classes label, count and percent, respectively. Note that I used a class for tooltip instead of an id in case I wanted more than one chart on the page and needed to reuse the tooltip styles.

Now it's time for the mouse event handlers. These will be attached to path so they need to come after its definition inside the callback:

var path = svg.selectAll('path')
  .data(pie(dataset))
  .enter()
  .append('path')
  .attr('d', arc)
  .attr('fill', function(d, i) {
    return color(d.data.label);
  });

path.on('mouseover', function(d) {           // NEW
  // Code                                    // NEW
});                                          // NEW

path.on('mouseout', function(d) {            // NEW
  // Code                                    // NEW
});                                          // NEW

Let's take a look at the mouseover event handler:

path.on('mouseover', function(d) {
  var total = d3.sum(dataset.map(function(d) {
    return d.count;
  }));
  var percent = Math.round(1000 * d.data.count / total) / 10;
  tooltip.select('.label').html(d.data.label);
  tooltip.select('.count').html(d.data.count);
  tooltip.select('.percent').html(percent + '%');
  tooltip.style('display', 'block');
});

The first thing we do is calculate the total number of tickets in the dataset. We do this by extracting the count value from each entry and then using D3's sum() function to add them up. (You may be thinking that this is inefficent: why not calculate the total when we first load the data and then just reuse that value? If this were the final step, that's what we would do. However in the next step we'll be adding filtering, so the total will no longer be fixed.)

Once we have the total, we can determine what percent the current segment represents. Then we can update the tooltip's three div elements with the label, count and percent, and finally switch it from display: none; to display: block;.

The tooltip will now appear when we hover over a segment, but we need it to disappear when we stop hovering. That's where the mouseout event handler comes in:

path.on('mouseout', function() {
  tooltip.style('display', 'none');
});

This one is as simple as it gets. I don't think it needs any explanation. Instead let's look at an optional event handler, mousemove:

// OPTIONAL
path.on('mousemove', function(d) {
  tooltip.style('top', (d3.event.layerY + 10) + 'px')
    .style('left', (d3.event.layerX + 10) + 'px');
});

If you didn't want the tooltip to stay in one place, you could add this after the mouseout event handler. (In the source code it's there but commented out.) It uses D3's handy event, which contains information about the event that just fired, to determine where on the chart the mouse currently is. Then it adjusts the top and left properties of the tooltip so that it will be 10px to the right and 10px below the cursor. That way the tooltip follows the cursor around for as long as its hovering over one of the segments .

Mouse events are critical to interactivity, but so far we've only harnessed passive events, like hovering. In Step 6 we'll add a click event, which provides a more active form of interaction. Before that, though, you may wish to look over the full code as it stands at this point:

comments powered by Disqus