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.
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:
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.