In this post I shall discuss all (most of) the math involved in the visualisations. A quick recall:

Lightbeam is now responsive and fully interactive.

### Tooltips

The math behind tooltips is the sequel to my blog post Lightbeam – Tooltips (SVG).

Read this blog post to understand Lightbeam’s migration from SVG to Canvas. Ignore the transforms and the inversions (`this.transform.invert`) in this post. Those are part of d3-zoom and explaining the math of this and d3-force is beyond the scope of this blog post.

`mousemove` event is registered on the canvas element itself. The mouse `<clientX, clientY>` positions are re-calculated w.r.t the canvas’s bounding rectangles. This ensures the mouse coordinates are confined to the canvas’s area.

`getNodeAtCoordinates(x, y)` returns a `node`, if a node is present at the given `<x, y>` values.

D3’s force layout has `simulation.find(x, y[, radius]` which returns the node closest to the position `<x, y>` with the given search radius. I chose to write `isPointInsideCircle()` to find out if a node exists at the given `<x, y>` values. The intention here is to isolate the logic from D3 specific as much as possible.

`isPointInsideCircle()`:

When you hover over the canvas, and if the mouse coordinates are inside any circle, then there is a node present at these coordinates. The point `<x, y>` is

1. inside the circle if `d < r`
2. on the circle if `d = r`
3. outside the circle if `d > r`

Square roots are expensive. Hence `d` is compared with `r*r`!

CSS:

Tooltip position: The tooltip has `position: absolute`.

Because of this property, there is a need to check the tooltips’ `left` property doesn’t exceed the canvas’s `right` property, else there will be horizontal scrollbar on the parent container because of `overflow-x`.

`x+tooltipWidth >= canvasRight` takes care of the overflow and sets `left` to `x-tooltipWidth`.

Setting `left` to `x-tooltipWidth/2` ensures the tooltip arrow is centre aligned to the node.

### Favicons

If a favicon exists for a given node, then it is drawn. The favicon is drawn at the centre of the circle (firstParty) or triangle (thirdParty). A square that fits exactly in a circle has a side length of `sqrt(2) * radius`.

### `firstParty` & `thirdParty` nodes

Given that we are drawing on a canvas, `firstParty` is a circle on the canvas. `thirdParty` is an equilateral triangle. Given the centre of the circle is at `<x, y>`, `r` is the radius of the circumcircle and `dr` is the radius of the incircle.

### zoom and drag

`d3-zoom` and `d3-drag` are used to achieve the zoom and drag behaviours respectively. It is quite complex when the two are combined. If you click and drag on the background, the view pans; if you click and drag on a circle, it moves.

`d3-drag` requires a `dragSubject`. I am using the same `getNodeAtCoordinates(x, y)` function which is used to show the tooltips and the logic remains same. This is how drag and zoom are combined for Lightbeam. If there is a node, (dragSubject) then it drags, else it pans.

Here is the `d3-zoom` implementation. The tricky part here is the need to distinguish between two coordinate spaces: the world coordinates used to position the nodes and links, and the pointer coordinates representing the mouse or touches. The drag behaviour doesn’t know the view is being transformed by the zoom behaviour, so we must convert between the two coordinate spaces.

This is where `transform.invert` or `transform.apply` come into play.

I hope I have done justice to the math in this post!