Appending title to D3 Tree layout causes error (D3)

Viewed 84

I'm trying to add tooltips to some SVG labels in the D3 Tree layout. Here is the function where the labels are rendered with a transition:

buildLabels() {
  const labelSelection = d3.select('svg g.labels')
    .selectAll('text.label')
    .data(this.nodes);
  labelSelection.exit()
    .remove();

  labelSelection.enter()
    .append('text')
    .style('fill', 'none')
    .style('stroke', 'none')
    .style('stroke-width', '0')
    .attr('transform', (d: any) => // ...)
    .style('fill-opacity', 0)
    .transition()
    .duration(450)
    .ease(d3.easeCircleIn)
    .attr('transform', (d: any) => {
      // ...
    })
    .attr('class', 'label')
    .style('stroke', '#393D3E')
    .style('fill', '#393D3E')
    .style('fill-opacity', 1)
    .style('stroke-width', '.4')
    .style('text-anchor', (d: any) => d.parent ? 'start' : 'end')
    .text(d => d.name);
}

I've tried adding

.append('title')
.text(d => d.name)

after the .text, but I get a long console error of

core.js:4061 ERROR TypeError: labelSelection.enter(...).append(...).style(...).style(...).style(...).attr(...).style(...).transition(...).duration(...).ease(...).attr(...).attr(...).style(...).style(...).style(...).style(...).style(...).text(...).append is not a function

If I change the function to:

labelSelection.enter()
  .append('text')
  .text(d => d.name)
  .append('title')
  .text(d => d.name);

I get the DOM that I'm expecting, which is

<text>
  Node name
  <title>Node name</title>
</text>

however, none of the nodes look correct and do not have the positions they're supposed to. Of course the transitions are all removed as well.

My question is, is there another way I can add a title that isn't clunky, or how I can get around the error above. Thanks!

1 Answers

You're attempting to append to a transition:

labelSelection.enter() 
  .append('text') // returns a selection of newly entered text elements
  .style(...)     // returns that same selection
  .attr(... )     // returns that same selection
 //  ...
 .transition()    // returns a transition
 .duration(450)   // returns that same transition
 .ease(...)       // returns that same transition
  // ...
 .text(d => d.name) // returns that same transition
 .append(...)       // error

Transitions and selections share a lot of methods (such as .style(), .attr(), and even .text()), so they can look very similar, but they don't share all methods.

You can do selection.append(), but not transition.append(). This is why you get your error message, append isn't a method of a transition, which explains your error message:

labelSelection.enter(...).append(...).style(...).style(...).style(...).attr(...).style(...).transition(...).duration(...).ease(...).attr(...).attr(...).style(...).style(...).style(...).style(...).style(...).text(...).append is not a function

.text returns a transition in this case (as it was chained to a transition, as shown above in the first code block), so we can reduce this to "transition.append is not a function".

Instead, you could break up your method chaining by keeping a reference to the relevant selection:

var labelEnter = labelSelection.enter() 
  .append('text') 
  .style(...)     
  .attr(... )     
 //  ...


 labelEnter.transition()    
 .duration(450)   
 .ease(...)      
 //  ...

 labelEnter.append("title")
   .text(...)

The alternative, which I would think makes your method chain unnecessarily long, is to use transition.selection(), which returns the selection that the transition corresponds to:

 labelSelection.enter() 
  .append('text') 
  .style(...)     
  .attr(... )     
 //  ...
 .transition()    
 .duration(450)   
 .ease(...)      
  // ...
 .text(d => d.name); 
 .selection()   // return a selection instead of a transition
 .append("title")
   .text(...)
Related