What it is to be royal - Family Tree Visualization

Drawing the tree:


The entire family tree is drawn within a single recursive function, "drawFamily". It accepts the parameter, pPerson, which will be the top or root node of the tree which you want to draw. To know how far apart to space pPerson from their partner, the drawFamily function initially makes a call to the function, "getWidth", and copies it into a new variable to be dealt with later.


A person has a width of 100 + the width of any partners they may have + the width of their children.

This function calculates the width of the person, pPerson, in four steps:

  • Initially set own width as 100
  • Increment width by 100 * the number of partners that pPerson has
  • If pPerson has children, recursively increment width by the width of each child
  • Return width before exiting function
  • drawFamily(pPerson) continued...

    At the beginning of the function, the context will be in the correct place to draw the person, however it first of all needs to check if they have two partners, because in this case, the second partner will get drawn before pPerson, followed by the relationship symbol, and the context then gets translated across. pPerson then gets drawn, along with a line up and across to the point above where their sibling would get drawn (providing that they are not the last child or the root node).

    If necessary, a partnership line is drawn from pPerson (using the saved width as the length of this line), and their partner is drawn. The context moves in place for a sibling to get drawn, and so context.save() is called before translating down by 150 pixels and back by the width, ready to draw the first child of this partnership.

    pPerson's children are looped through and the function recurses on each of them. The context gets restored once all the children have been drawn, so it is ready to draw pPerson's siblings once the function has backtracked.

    Auto-fit to screen:


    This function first of all resets the transformation matrix and then uses the "getWidth" function on the person passed in to calculate how wide in pixels the tree is. It then enters a while loop and increments or decrements the scale, 1% at a time until the tree fits nicely within the boundaries of the canvas. It also then uses the "getHeight" function to check in case the tree is too tall for the canvas, and sets the scales accordingly as before. It finally adjusts the panOffset vector to centre the tree on the canvas.

    getHeight(pPerson, pCurrentHeight)

    This is a recursive function that returns the height of a tree, starting with pPerson as the root node. It does so by incrementing pCurrentHeight by 150 for every child that it goes down. When the bottom of the tree is reached, the height at that point is saved as the maxHeight if it is greater than the previous maxHeight. Once the function finally exits, it returns the maxHeight that it found.

    Drawing the minimap:


    The minimap has a fixed size and scale of 20% the width and height of the big tree. It initially calculates its own size and then aligns the context with the top-right edge of the screen with 20px above and to the side. The boundary rectangle is drawn and a clip is performed so that the viewport does not go outside of it. The scale of the context is then set to the minimap scale (0.2) and the drawFamily function is called in exactly the same way as it is called to draw the original sized version.

    The position and size of the viewport rectangle is calculated as follows:

  • Position: -panOffset/scale. In other words, if the pan is such that the big tree would be drawn -100 pixels on the x-axis, then the origin of the viewport is essentially going to be +100 pixels across. It gets divided by the scale so it stays relative no matter what the zoom level is.
  • Size: canvas size/scale. So if the scale is, for example, 2, the viewport rectangle will be half the size of the canvas, to depict zooming in.
  • Note: the context scale is currently 0.2 so the position and size co-ordinates automatically scale down to fit the boundary rectangle of the minimap.

    Zooming and panning:


    The onMouseWheel event listener calls this function with a delta of either -1 if the wheel was scrolled down, or 1 if it scrolled up. Initially, the current scale is set as lastScale and then the scale is modified by a percentage depending on the delta (10% increase in scale if scrolled up, 10% decrease if scrolled down).

    The panOffset vector is then adjusted as follows so the context can be translated appropriately to be zoomed towards the mouse:

  • newPan = mousePos + ((oldPan/lastScale)-(mousePos/lastScale))*newScale
  • This works by first of all dividing the old panOffset and mouse position by the last scale to cancel out the scale factors on them, finding the difference between the two, and then adding back on the current mouse position. This gives the right new panOffset but it is currently unscaled, so it is simply multiplied by whatever the new scale has been set to.

    onMouseDown(e), onMouseMove(e), onMouseUp(e)

    When the mouse is clicked down, the boolean 'drag' is set to true, and the dragStart vector is set to contain the event co-ordinates (current mouse position). As the mouse is moved, the vector 'mouse' updates with the appropriate values. If drag is true, the panOffset increments with the difference between the mouse position at the start of the drag, and now. Finally, dragStart is updated to contain the current co-ordinates. When the mouse is released, drag is set to false.


    Animation when zooming/panning/auto-fitting is done using a lerp function. Whenever the scale or panOffset is altered, a target version of it is actually being modified and the current scale/panOffset remains unchanged. Then, in the draw function (which gets called every frame), the lerp function is used to adjust the current scale/panOffset to gradually move it closer towards the target.

    lerp(pFrom, pTo, pAmount)

    This function accepts three parameters - the value being lerped from, the value being lerped to, and the percentage by which it is going to get modified each time. The formula is as follows:

  • return pFrom + (pTo-pFrom) * pAmount
  • See the following diagram, depicting an example in which pFrom is 0, pTo is 10, and pAmount is 0.5:

    Back to Family Tree Visualization