Tooltips using SVG Path

Michael Rovinsky
Welldone Software

--

After the Map Pins, let’s try a more interesting SVG path shape: a Tooltip. Here is an example (click on the Result tab):

The Tooltip shape’s path is defined by 5 parameters: width, height, pointer offset, corner radius, and placement (left, top, right, or bottom).

To make things simple, let’s first build a path for a top-positioned Tooltip without rounded corners. That is what we have:

  1. Move to A: M aX,aY
  2. Line to B: L bX,bY
  3. Horizontal line to C: H cX
  4. Vertical Line to D: V dY
  5. Horizontal line to E: H eX
  6. Vertical Line to F: V fY
  7. Horizontal line to G: H gX
  8. Line to A: L aX,aY
  9. Close the path: z

The point A coordinates are 0, 0. The other points are calculated from width, height, and offset:

bx = -offset
by = -offset // also cy, fy, gy
cx = -width / 2 // also dx
dy = -offset - height // also ey
ex = width / 2 // also fx
gx = offset

… and the whole path will look like this:

function topTooltipPath(width, height, offset) {
const left = -width / 2
const right = width / 2
const top = -offset - height
const bottom = -offset
return `M 0,0
L ${-offset},${bottom}
H ${left}
V ${top}
H ${right}
V ${bottom}
H ${offset}
L 0,0 z`
}

To get rounded corners, we have two options: Arc and Quadratic Bezier curve. An Arc uses 7 parameters (see the Pin example). A QB curve is much simpler: It takes the current point of path (set by previous Move to / Line to) and two additional points: the Vertex and the Target. In SVG, a QB curve is specified by letter Q: Q vX,vY tX,tY.

For the corner C, the starting point of the curve is on the incoming line (H cX) right to C (sX = cX + r; sY = cY). The target point in on the outgoing line (V dY), above C (tX = cX; tY = cY - r). The QB vertex is the C point itself. We just replace … H cX V dY … with: … H (cX + r) Q cX, cY cX, (cY - r) V dY …, and get the C corner rounded (by radius r). Here is an improved function:

function topTooltipPath(width, height, offset, radius) {
const left = -width / 2
const right = width / 2
const top = -offset - height
const bottom = -offset
return `M 0,0
L ${-offset},${bottom}
H ${left + radius}
Q ${left},${bottom} ${left},${bottom - radius}
V ${top + radius}
Q ${left},${top} ${left + radius},${top}
H ${right - radius}
Q ${right},${top} ${right},${top + radius}
V ${bottom - radius}
Q ${right},${bottom} ${right - radius},${bottom}
H ${offset}
L 0,0 z`
}

To make a bottom-positioned tooltip, we just invert all the Y coordinates in the path:

function bottomTooltipPath(width, height, offset, radius) {
const left = -width / 2
const right = width / 2
const bottom = offset + height
const top = offset
return `M 0,0
L ${-offset},${top}
H ${left + radius}
Q ${left},${top} ${left},${top + radius}
V ${bottom - radius}
Q ${left},${bottom} ${left + radius},${bottom}
H ${right - radius}
Q ${right},${bottom} ${right},${bottom - radius}
V ${top + radius}
Q ${right},${top} ${right - radius},${top}
H ${offset}
L 0,0 z`
}

The same is true for the X coordinates of the left and right-positioned tooltips:

function leftTooltipPath(width, height, offset, radius) {
const left = -offset - width
const right = -offset
const top = -height / 2
const bottom = height / 2
return `M 0,0
L ${right},${-offset}
V ${top + radius}
Q ${right},${top} ${right - radius},${top}
H ${left + radius}
Q ${left},${top} ${left},${top + radius}
V ${bottom - radius}
Q ${left},${bottom} ${left + radius},${bottom}
H ${right - radius}
Q ${right},${bottom} ${right},${bottom - radius}
V ${offset}
L 0,0 z`
}
function rightTooltipPath(width, height, offset, radius) {
const left = offset
const right = offset + width
const top = -height / 2
const bottom = height / 2
return `M 0,0
L ${left},${-offset}
V ${top + radius}
Q ${left},${top} ${left + radius},${top}
H ${right - radius}
Q ${right},${top} ${right},${top + radius}
V ${bottom - radius}
Q ${right},${bottom} ${right - radius},${bottom}
H ${left + radius}
Q ${left},${bottom} ${left},${bottom - radius}
V ${offset}
L 0,0 z`
}

Here is a function that does it all:

function tooltipPath(width, height, offset, radius, position) {
let left, top, right, bottom
switch(position) {
case 'top':
return topTooltipPath(width, height, offset, radius)
case 'left':
return leftTooltipPath(width, height, offset, radius)
case 'right':
return rightTooltipPath(width, height, offset, radius)
case 'bottom':
return bottomTooltipPath(width, height, offset, radius)
}
}

For those valuing the JavaScript elegance:

class Tooltip {
topTooltipPath(width, height, offset, radius) {...}
leftTooltipPath(width, height, offset, radius) {...}
rightTooltipPath(width, height, offset, radius) {...}
bottomTooltipPath(width, height, offset, radius) {...}
tooltipPath(width, height, offset, radius, position) {
const method = `${position}TooltipPath`
return this[method](width, height, offset, radius)
}
}

Now a final demo with movable objects and their tooltips. A tooltip’s appearance depends on the object’s position:

Have a nice day!

--

--