Efficient tooltip positioning in D3.js chart

Posted on August 8, 2017 in
2 min read

I was just scribbling on paper without a specific goal. Suddenly, a little ah-ha moment brought me in a little exploration.

screen

The proof-of-concept

Apparently, I'm all about Proof-Of-Concept these days.

This time is about positioning a graphic element (let's say, a tooltip) next to the mouse but using the most efficient area in a given canvas in order to avoid the element to be cut by the boundary of the canvas.

Disclaimer

I didn't make any research on the topic, I bet I wouldn't be astonished if there were, at least, a couple of Ph.D. out there on efficient tooltips positioning algorithms.
I don't know whether this technique has been already used somewhere. In this case, I would love to hear more about them.

The rules

They are simple and can be outlined as the following:

  • construct four lines between the canvas edge corners and the mouse position
  • find the longest one
  • find a point along the longest line that is far enough from the mouse
  • use that position to move the centroid of the element we want to position efficiently

A very efficient way to show how it works and what's going on behind the scene might be this progressive visualization:

screen

Here the relevant javascript code that uses a couple of essential native functions to accomplish this feature. It was extrapolated by a D3.js script but it can be adapted to other contexts quite easily. This script requires to be within a mousemove listener:

// finding the longest line
var maxL
var maxV = 0
lines.each(function (d, i) {
  if (d.l >= maxV) {
    maxL = this
    maxV = d.l
  }
})

// get the final point
var l = maxL.getTotalLength()
var p = maxL.getPointAtLength(l - 60)

// position the element
legend.attr('transform', `translate(${p.x}, ${p.y})`)

Here the interactive version (click to toggle the chart visibility and... desktop only):

Conclusion

I don't know whether I'm going to use this technique in the future. Nevertheless, it's always good to figure out what's going on behind the curtain, it can't hurt.

PS: I've also learned something on SVG 2; the getTotalLength() function will be deprecated on some SVG element (i.e. line, text), this is why my implementation uses path instead line elements.

Source code here.