Saturday, 12 January 2013

Adding tooltips to a d3.js graph


The following post is a portion of the D3 Tips and Tricks document which it free to download. To use this post in context, consider it with the others in the blog or just download the pdf  and / or the examples from the downloads page:-)
--------------------------------------------------------

Tooltips have a marvelous duality. They are on one hand a pretty darned useful thing that aids in giving context and information where required and on the other hand, if done with a bit of care, they can look very stylish :-).

Technically, they represent a slight move from what we have been playing with so far into a mildly more complex arena of 'transitions' and 'events'. You can take this one of two ways. Either accept that it just works and implement it as shown, or you will know what s going on and feel free to deride my efforts as those of a rank amateur :-).

The source for the implementation was taken from Mike Bostock's example here; http://bl.ocks.org/1087001. This was combined with a few other bit's and pieces (the trickiest being working out how to format the displayed date correctly and inserting a line break in the tooltip (which I found here; https://groups.google.com/forum/?fromgroups=#!topic/d3-js/GgFTf24ltjc (well done to all those participating in that discussion)). I make the assumption that any or all errors that occur in the implementation will be mine, whereas, any successes will be down to the original contributors.

So, just in case there is some degree of confusion, a tooltip (one word or two?) is a discrete piece of information that will pop into view when the mouse is hovered over somewhere specific. Most of us have seen and used them, but I suppose we all tend to call them different things such as 'infotip', 'hint' or 'hover box' I don't know if there's a right name for them, but here's an example of what we're trying to achieve;
You can see the mouse has hovered over one of the scatter plot circles and a tip has appeared that provides the user with the exact date and value for that point.

Now, you may also notice that there's a certain degree of 'fancy' here as the information is bound by a rectangular shape whit rounded corners and a slight opacity. The other piece of 'fancy' which you don't see in a pdf is that when these tool tips appear and disappear, they do so in an elegant fade-in, fade-out way. Purty.

Now, before we get started describing how the code goes together, let's take a quick look at the two technique specifics that I mentioned earlier, 'transitions' and 'events'.

Transitions

From the main d3.js web page (d3js.org) transitions are described as gradually interpolating styles and attributes over time. So what I take that to mean is that if you want to change an object, you can do so be simply specifying the attribute / style end point that you want it to end up with and the time you want it to take and go!

Of course, it's not quite that simple, but luckily, smarter people than I have done some fantastic work describing different aspects of transitions so please see the following for a more complete description of the topic;
Hopefully observing the mouseover and mouseout transitions in the tooltips example will whet your appetite for more!

Events

The other technique is related to mouse 'events'. This describes the browser watching for when 'something' happens with the mouse on the screen and when it does, it takes a specified action. A (probably non-comprehensive) list of the types of events are the following;
  • mousedown: Triggered by an element when a mouse button is pressed down over it
  • mouseup: Triggered by an element when a mouse button is released over it
  • mouseover: Triggered by an element when the mouse comes over it
  • mouseout: Triggered by an element when the mouse goes out of it
  • mousemove: Triggered by an element on every mouse move over it.
  • click: Triggered by a mouse click: mousedown and then mouseup over an element
  • contextmenu: Triggered by a right-button mouse click over an element.
  • dblclick: Triggered by two clicks within a short time over an element
How many of these are valid to use within d3 I'm not sure, but I'm willing to bet that there are probably more than those here as well. Please go to http://javascript.info/tutorial/mouse-events for a far better description of the topic if required.

Get tipping

So, bolstered with a couple of new concepts to consider, let's see how they are enacted in practice.
If we start with our simple-scatter plot graph there are 4 areas in it that we will want to modify (it may be easier to check the tooltips.html file in the example files in the downloads section on d3noob.org).

The first area is the CSS. The following code should be added just before the </style> tag;
div.tooltip {   
  position: absolute;           
  text-align: center;           
  width: 60px;                  
  height: 28px;                 
  padding: 2px;             
  font: 12px sans-serif;        
  background: lightsteelblue;   
  border: 0px;      
  border-radius: 8px;           
  pointer-events: none;         
}
These styles are defining how our tooltip will appear . Most of them are fairly straight forward. The position of the tooltip is done in absolute measurements, not relative. The text is centre aligned, the height, width and colour of the rectangle is 28px, 60px and lightsteelblue respectively. The 'padding' is an interesting feature that provides a neat way to grow a shape by a fixed amount from a specified size.

We set the boarder to 0px so that it doesn't show up and a neat style (attribute?) called border-radius provides the nice rounded corners on the rectangle.

Lastly, but by no means least, the 'pointer-events: none' line is in place to instruct the mouse event to go "through" the element and target whatever is "underneath" that element instead (Read more here; https://developer.mozilla.org/en-US/docs/CSS/pointer-events). That means that even if the tooltip partly obscures the circle, the code will stll act as if the mouse is over only the circle.

The second addition is a simple one-liner that should (for forms sake) be placed under the 'parseData' variable declaration;
var formatTime = d3.time.format("%e %B");
This line formats the date when it appears in our tooltip. Without it, the time would default to a disturbingly long combination of temporal details. In the case here we have declared that we want to see the day of the month (%e) and the full month name(%B).

The third block of code is the function declaration for 'div'.
var div = d3.select("body").append("div")   
    .attr("class", "tooltip")               
    .style("opacity", 0);
We can place that just after the 'valueline' definition in the JavaScript. Again there's not too much here that's surprising. We tell it to attach 'div' to the body element, we set the class to the tooltip class (from the CSS) and we set the opacity to zero. It might sound strange to have the opacity set to zero, but remember, that's the natural state of a tooltip. It will live unseen until it's moment of revelation arrives and it pops up!

The final block of code is slightly more complex and could be described as a mutant version of the neat little bit of code that we used to do the drawing of the dots for the scatter plot. That's because the tooltips are all about the scatter plot circles. Without a circle to 'mouseover', the tooltip never appears :-).

So here's the code that includes the scatter plot drawing (it's included since it's pretty much integral);
svg.selectAll("dot")    
        .data(data)         
    .enter().append("circle")                               
        .attr("r", 5)       
        .attr("cx", function(d) { return x(d.date); })       
        .attr("cy", function(d) { return y(d.close); })     
        .on("mouseover", function(d) {      
            div.transition()        
                .duration(200)      
                .style("opacity", .9);      
            div .html(formatTime(d.date) + "<br/>"  + d.close)  
                .style("left", (d3.event.pageX) + "px")     
                .style("top", (d3.event.pageY - 28) + "px");    
            })                  
        .on("mouseout", function(d) {       
            div.transition()        
                .duration(500)      
                .style("opacity", 0);   
        });

Before we start going through the code, the example file for tooltips that is on d3noob.org includes a brief series of comments for the lines that are added or changed from the scatter plot, so if you want to compare what is going on in context, that is an option.

The first six lines of the code are a repeat of the scatter plot drawing script. The only changes are that we've increased the radius of the circle from 3.5 to 5 (just to make it easier to mouse over the object) and we've removed the semicolon from the 'cy' attribute line since the code now has to carry on.

So the additions are broken into to areas that correspond to the two events. 'mouseover' and 'mouseout'. When the mouse moves over any of the circles in the scatter plot, the mouseover code is executed on the 'div' element. When the mouse is moved off the circle a different set of instructions are executed.

It would be a mistake to think of tooltips in the plural because there aren't a whole series of individual tooltips just waiting to appear for their specific circle. There is only one tooltip that will appear when the mouse moves over a circle. And depending on what circle it's over, the properties of the tooltip will alter slightly (in terms of its position and contents).

on.mouseover

The .on("mouseover" line initiates the introduction of the tooltip. Then we declare the element we will be introducing ('div') and that we will be applying a transition to it's introduction (.transition()). The next two lines describe the transition. It will take 200 milliseconds (.duration(200)) and will result in changing the elements opacity to .9 (.style("opacity", .9);). Given that the natural state of our tooltip is an opacity of 0, this make sense for something appearing, but it doesn't go all the way to a solid object and it retains a slight transparency just to make it look less permanent.

The following three lines format our tooltip. The first one adds an html element that contains our x and y information (the date and the d.close value). Now this is done in a slightly strange way. Other tooltips that I have seen have used a '.text' element instead of a '.html' one, but I have used '.html' in this case because I wanted to include the line break tag “<br/>” to separate the date and value. I'm sure there are other ways to do it, but this worked for me. The other interesting part of this line is that this is where we call our time formatting function that we described earlier. The next two lines position the tooltip on the screen and to do this they grab the x and y coordinates of the mouse when the event takes place (with the d3.event.pageX and d3.event.pageY snippets) and apply a correction in the case of the y coordinate to raise the tooltip up by the same amount as its height (28 pixels).

on.mouseout

The .on("mouseout" section is slightly simpler in that it doesn't have to do any fancy text / html / coordinate stuff. All it has to do is to fade out the 'div' element. And that is done by simply reversing the opacity back to 0 and setting the duration for the transition to 500 milliseconds (being slightly longer than the fade-in makes it look slightly cooler IMHO).

Right, there you go. As a description it's ended up being a bit of a wall of text I'm afraid. But hopefully between the explanation and the example code you will get the idea. Please take the time to fiddle with the settings described here to find the ones that work for you and in the process you will reinforce some of the principles that help D3 do it's thing.

I've placed a copy of the file for drawing the tooltips into the downloads section on d3noob.org with the general examples as tooltips.html.

The above description (and heaps of other stuff) is in the D3 Tips and Tricks document that can be accessed from the downloads page of d3noob.org.

32 comments:

  1. D3Noob,

    Followed your examples from start to finish and found them really useful. As a complete novice at D3 I've come a long way over the last two days and started working with my own data. I've currently two lines on the graph displaying properly and am now looking at setting up a transition that will allow for each line to be brought in on clicking a button. Any examples of how this would work?

    appreciate you effort in getting all this on line

    Justin

    ReplyDelete
    Replies
    1. Great stuff Justin.
      Excellent to hear that the information is useful.
      I don't have an example for the basic code that you could follow, but if it's any solace, it's on my (long and getting longer) list of things to add to the tips and tricks manual.
      Mike Dewar has a really good example in his book 'Getting started with D3' that uses multiple lines and you toggle a legend to switch them on and off. But for my money I would start with an excellent post by Jerome Cukier on transitions here; http://blog.visual.ly/creating-animations-and-transitions-with-d3-js/ one of his examples looks like exactly what you describe and I will be taking a long hard look at it when it comes time to add this to the manual.
      When you get it sorted, if you think it would be a useful addition to the tips and tricks manual I would be really keen to add it in (if you're happy to release the code :-))
      Thanks for reading.

      Delete
    2. Or maybe even this post by Ivan Teoh http://www.ivanteoh.com/posts/176-d3-tutorial-scale-graph.html

      Delete
  2. I am lazy. I prefer to simply incorporate jquery ui and append a title. It also allows me to embed HTML in the tooltip. No mouse over hassles either. Of course, it adds new dependencies to your visual, but I am good with the tradeoff.

    - Pat

    ReplyDelete
    Replies
    1. Fair call, using jQuery opens up a whole world of other possibilities, so a good move in my opinion.

      Delete
  3. Hi, great tutorial. Is it possible to embed a hyperlink in the tooltip? When I try it, it shows up formatted as a link, but clicking does nothing.

    -Sam

    ReplyDelete
    Replies
    1. Wow! That's a great question! I think so if it was wrapped in the correct tags. I'll have a play and report back.

      Delete
    2. This comment has been removed by the author.

      Delete
    3. The answer is Yes! http://www.w3schools.com/html/html_links.asp was my friend.
      Sadly trying to put the code into the comments here confuses the blog and it tries to link to the url instead of displaying it :-(. But just add a sample link per the w3schools page above to the line that adds the text for your hyperlink. The only semi-tricky thing I had to do was to put the link (with tags) in single speech marks instead of double speech marks since the URL needed to be in double speech marks.

      The only real problem then being that in order to reach it you need to move the mouse so far that the tooltip vanishes! (Of course this can be solved by several means depending on what's appropriate for the tooltip).

      So yes. Great question and an actual answer!

      Delete
    4. Snap! I got all excited and didn't read your post properly! Now that I have I see that I am experiencing the same problem that you are! I think that there is a solution but it involves blocking mouse events. So I'm wondering if we might be in a chicken / egg situation where we can do one thing, but that prevents us from doing another. Hmm....

      Delete
    5. Thanks for the quick response. I tried that, and I put a delay in the mouseout transition, so that I can actually move the mouse over and click on the tooltip before it vanishes. The text shows up formatted as a hyperlink in the tooltip, but clicking on it doesn't achieve anything (the target page does not open). Am I missing something?

      -Sam

      Delete
    6. Yeah. I think we're both missing something. I get the feeling that by utilising a mouse event as a trigger, we then bind the mouse action to the tooltip, so that while the hyperlink shows up, the mouse is still bound to the originating object and ignores the link. My experience in this area is sadly lacking I'm afraid. That's my best guess. If it's a 'must have' for your visualization, ask a question on stack exchange. There's a bunch of people in that forum with some rare skills. Sorry I wasn't able to help more.

      Delete
    7. No worries. Thanks for your prompt replies. If I figure something out, I'll post back here as well.

      -Sam

      Delete
    8. Oops! Got it: there is "pointer-events: none" in your div.tooltip css. I took that out and it worked :-)

      -Sam

      PS adding a 'target="_blank"' is a good idea so that the link opens in a new tab.

      Delete
  4. Thanks for the helpful post! But I was wondering where exactly did you bind the tooltip class to the mouseover event as you did not specifically refer to div.tooltip within the function. Am I missing something?

    ReplyDelete
    Replies
    1. Good question. div.tooltip is obviously declared in the css. Then in the third block (above) it gets bound (at least that's where I believe it occurs) with the following;
      var div = d3.select("body").append("div")
      .attr("class", "tooltip")

      Delete
  5. Hey man, I've been working through alot of your tutorials and they're great. Better than some of the books I've seen on d3. Thanks man! I appreciate you giving away the fire.

    ReplyDelete
    Replies
    1. Glad that they're useful!
      Just spreading the D3 love :-).

      Delete
  6. Ya, thanks for giving away the fire!

    ReplyDelete
    Replies
    1. No problem! Interestingly I had never heard the phrase 'giving away the fire' before (other than the comment above yours). I shall try to use it in casual conversation!

      Delete
  7. Will it work on the Rickshaw javascript framework That uses D3?

    ReplyDelete
    Replies
    1. Wow! Good question. I hadn't even heard of Rickshaw. That looks really cool. The short answer is, I don't know, however, I do see that they have a interactive hover thingy that might do what you want if you want something simple.

      Delete
  8. I have been trying to adapt the tooltip code to a multi series line chart such as - http://bl.ocks.org/mbostock/3884955. No luck so far. I believe my problem is defining the cx and cy.

    ReplyDelete
    Replies
    1. I think this would be do-able. Put your code and data into a question on Stack Overflow and any problems should be able to be worked out there. Good luck.

      Delete
  9. Nice tutorial. I have employed a slightly modified version to some baseball data here: http://visual-baseball.com/d3/examples/donut/AL_21_40_donuts.html

    ReplyDelete
  10. Hey thanks for the tutorial! How do you ensure your tooltip is generated on top of the svg element its appended too? When I try it the box appears under the svg

    ReplyDelete
    Replies
    1. How curious! Are you able to create a jsfiddle or bl.ocks.org example to demonstrate?

      Delete
  11. Hey, Im trying to create a simple scatter plot with tooltip. My csv file is [country,x,y], I just want the country name (&x,y) to pop up to label each data point upon mouse over (there are ~200 clustered together around an origin so its a little untidy to lable). Simple problem, looking for simple solution?

    ReplyDelete
  12. Hmm... Straight away my first thought is to massage the data before drawing the graph. It could be done dynamically using a php script that loaded the csv, massages and outputs as JSON (or csv again). Not entirely simple however (if you're not familiar with php).
    I have the feeling that there will be a method available within d3's array functions (https://github.com/mbostock/d3/wiki/Arrays or this page may help http://bl.ocks.org/phoebebright/raw/3176159/) but I'm sorry to say that I haven't used them seriously. If you can put together an example on bl.ocks.org for people to comment on a subsequent question on stack overflow might elicit an answer. Good question.

    ReplyDelete
  13. Excellent tutorial! Thank you very much!

    ReplyDelete