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.
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.
mousemove event is registered on the canvas element itself.
<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.
When you hover over the canvas, and if the mouse coordinates are inside any circle, then there is a node present at these coordinates.
<x, y> is
- inside the circle if
d < r
- on the circle if
d = r
- outside the circle if
d > r
Square roots are expensive. Hence
d is compared with
The tooltip has
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
x+tooltipWidth >= canvasRight takes care of the overflow and sets
x-tooltipWidth/2 ensures the tooltip arrow is centre aligned to the node.
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.
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
r is the radius of the circumcircle and
dr is the radius of the incircle.
zoom and drag
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
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.apply come into play.
I hope I have done justice to the math in this post!