Search code examples
d3.jstooltip

How to append text to D3 Tooltips?


I am having an issue with appending text to D3 tooltip. The code that generates the tooltip results in the following error:

Uncaught TypeError: Cannot read property 'Name' of undefined

The tooltip shows, but does not have any text. The following code is used to generate the tooltip:

1.Code where .toolTip is created

.toolTip {
position: absolute;
display: none;
min-width: 80px;
height: auto;
background: none repeat scroll 0 0 #ffffff;
border: 1px solid #6F257F;
padding: 14px;
text-align: center;

2.Code where variable is created.

 var tooltip = d3.select("body")
                .append("div")
                .attr("class", "toolTip");
  1. Code where attempt to append to node

    // Nodes
    d3.select('svg g.nodes')
    .selectAll('circle.node')
    .data(root.descendants())
    .enter()
    .append('circle')
    .classed('node', true)
    .attr('cx', function(d) {return d.x;})
    .attr('cy', function(d) {return d.y;})
    .attr('r', 4)
    .on("mousemove", function(d){
     tooltip
    .style("left", d3.event.pageX - 50 +"px")
    .style("top", d3.event.pageY - 70 + "px")
    .style("display", "inline-block")
    .html(function (d) {return d.Name});
    })
    .on("mouseout", function(d){ tooltip.style("display", "none");});
    

Solution

  • Let's take a look at the function invoked on mousemove:

    .on("mousemove", function(d){
       tooltip
        .style("left", d3.event.pageX - 50 +"px")
        .style("top", d3.event.pageY - 70 + "px")
        .style("display", "inline-block")
        .html(function (d) {return d.Name});
    })
    

    In the first line, the d in function refers to the datum bound to the svg circle, while here: .html(function(d) {, the d refers to the datum bound to the tooltip. In this case the two ds do not refer to the same thing. You don't have any data bound to the tooltip, so this won't work - d here is undefined, hence your error message: "Cannot read property 'Name' of undefined."

    Instead, just use the datum of the circle:

    .on("mousemove", function(d){
       tooltip
        .style("left", d3.event.pageX - 50 +"px")
        .style("top", d3.event.pageY - 70 + "px")
        .style("display", "inline-block")
        .html(d.Name); 
    })
    

    Whenever you see function(d) in a .html or .attr method of a d3 selection, you are accessing the datum of each element of the current selection. You don't want data associated with the tooltip, so you don't need to use function(d) in the .html() method, you've already accessed the datum you want in the first line.


    It appears as though you are using a d3 hierarchical layout. This will alter the structure of your data, if you start with a form like:

     var data = { "name": "Parent", "children": [ 
        { "name": "Child A", "children": [ { "name": "Grandchild" } ] }, 
        { "name": "Child B", } 
        ] };
    

    d3.hierarchy(data) will return an array with this structure:

    [
     {data: Object, height: number, depth: number, parent: null, children: array[number], x: number, y: number},
     {data: Object, height: number, depth: number, parent: object, children: array[number], x: number, y: number},
      ... and so forth.
    ]
    

    This is so there is one element in the data array for each node that is to be appended (selection.data() expects an array), and that each item in the data being passed to the selection has some properties that allow it to be positioned, connected, etc properly. The key point is that the original element in the data array is now at d.data. This is the same as with d3.pie or other layout generators - and avoids potential overlaps between property names (between original properties and properties created by the layout).

    For you, the main result is your name won't reside at d.Name, but rather d.data.Name as this the data structure created by d3.hierarchy().