Plotting trees recursively

Plotting trees recursively

May 18, 2022

The idea for this visualization is something I have been thinking about for quite some time now. Although it is not this specific visualization per se, but the idea of visualizing a nested tree in an appealing way. This method is actually quite easy to implement, and HTML and CSS are doing all the heavy lifting of displaying something that actually looks nice. The illustration below is an example of what the final result will look like, and the rest of this post is about developing the visualization.

A data structure for trees

The first thing that we will need is a data structure that can model trees. We can do this by creating a class Node, and giving that class a property children: Node[] in which we collect a bunch of other nodes. If we do this recursively, then we have a tree. We will also add a name: string property, which we will use to give a name to the node. Finally, we add a addNode(name: string) function to the class, which we use to easily add a new node under an existing node. All in all, this results in the following code:

 1class Node {
 2    name = "Untitled node";
 3    children = [];
 4    
 5    constructor(name) {
 6        this.name = name;
 7    }
 8
 9    addNode(name) {
10        const node = new Node(name);
11        this.children.push(node);
12        return node;
13    }
14}

Generating the plot recursively

The next thing that we will do is write a class that will generate the HTML of a tree. The idea is to alternate between have a horizontal and a vertical layout. We will start with a horizontal layout, which is for the first node. Then the second layer of the tree is layed out vertically. The third layer is horizontal again, and so on. This is done with the flex-direction property of CSS.

1Direction = { 
2    Horizontal: "flex-row",
3    Vertical: "flex-col"
4}

The next thing we will do is define a class RecursivePlot that will generate the HTML elements for the plot. This class has a constructor with a elementId: string argument, which is the ID of the element in which we want to add the generated HTML. This class also has a method draw(node: Node) which we will use to generate the HTML.

In this function we will define another function __draw(node: Node, element: HTMLElement, direction: Direction) which is called recursively to generate the HTML element for the entire tree. The HTML that we will generate is simply a div and a class .node which we'll use for styling. We will also set the flex-direction property alternating between row and column.

Once we have the __draw function defined, we will create a div for the root node. We will also add the .node class to this element. We will then call __draw, and pass in the root node, the element of the root node, and the direction for flexbox. If we combine all of this, we get the following class:

 1class RecursivePlot {
 2    constructor(elementId) {
 3        this.element = document.getElementById(elementId);
 4    }
 5
 6    draw(node) {
 7        const __draw = (node, element, direction) => {
 8            // Alternate between the direction.
 9            direction = direction == Direction.Horizontal
10                ? Direction.Vertical
11                : Direction.Horizontal;
12
13            // Create a div for each child node, and recursively call
14            // this function.
15            for (let child of node.children) {
16                const div = document.createElement('div');
17                div.classList.add('node');
18                div.classList.add(direction);
19                element.appendChild(div);
20                __draw(child, div, direction);
21            }
22
23            // Create a label for the name of the node.
24            const label = document.createElement('div');
25            label.classList.add('label');
26            element.appendChild(label);
27            label.innerHTML = node.name;
28
29            // If the node doesn't have any children, indicate
30            // that this node is a leaf.
31            if (node.children.length == 0) {
32                label.classList.add('node-leaf');
33            }
34        }
35
36        // Default direction to start with (sets flex-direction).
37        const defaultDirection = Direction.Vertical;
38
39        // Create the container for the root node.
40        const container = document.createElement('div');
41        container.classList.add('node');
42        container.classList.add(defaultDirection);
43        this.element.appendChild(container);
44
45        __draw(node, container, defaultDirection);
46    }
47}

Styling the recursive tree plot

Now that we have a method to generate the HTML elements, we need to style them. We will start with adding a few utility classes for flex-direction. Note that these class names are also defined in the Direction enum.

1.flex-row {
2    flex-direction: row;
3}
4
5.flex-col {
6    flex-direction: column;
7}

With that out of the way, we are going to style the node itself. We want to set the display mode to display: inline-flex, because we want the plot to be as small as possible and only grow bigger if there is more content in it. You could also have the plot fill the entire container by setting it to display: flex. We also set flex-wrap: wrap in the case we have a lot of elements and flex-grow: 1 so that the elements fill the entire container. The position should be set to position: relative, because we will need this to show the labels in the correct position. So far this gives us the following:

1.node {
2    display: inline-flex;
3    position: relative;
4    overflow: hidden;
5    flex-wrap: wrap;
6    flex-grow: 1;
7    ...
8}

To let the nodes have some space around it we will set padding: 8px and gap: 8px. On the top of the node we reserve some space for the label by setting padding-top: 22px. The background color of the node is a white transparent color with an opacity of $33\%$, giving background-color: rgba(255, 255, 255, 0.33). This gives a nice effect when we are stacking nodes on top of each other. To give the illusion of depth, we will add two different box-shadow's. The idea is to imagine that there is a light on the top of the page, and we are rendering the resulting shadow. A second shadow gives a more realistic look. To ensure the label is visible we set min-width: 40px. We will also add border-radius: 4px to smooth the corners, but this is a personal preference. Finally we set the font size font-size: 90%. Note that the $90\%$ is applied consecutively as we nest more nodes. This ensures that the font size of the label is getting smaller and smaller.

 1.node {
 2    ...
 3    gap: 8px;
 4    padding: 8px;
 5    padding-top: 22px;
 6    background-color: rgba(255, 255, 255, 0.33);
 7    box-shadow: 0 0 4px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.1);
 8    min-width: 40px;
 9    border-radius: 4px;
10    font-size: 90%;
11}

Because the nodes in the plot are set to display: inline (by setting display: inline-flex), we also want to center the content of the plot. This ensures that the plot is always centered in the container that it is rendered in.

1.plot {
2    text-align: center;
3}

We also want to set the box-sizing: border-box, because this fixes the weird sizing when applying padding. Actually, this should have been the default value in browsers anyway, but sadly it's not.

1.plot * {
2    box-sizing: border-box;
3}

Because we can have a white background, which doesn't work well with the transparent background, we also want to set a background for the first node.

1.plot > .node {
2    background-color: #aaa;
3}

Finally, we will put the label in the correct position. We have already added some extra padding to the top of the node, which we will now use to position the label.

1.label {
2    position: absolute;
3    font-family: Arial;
4    text-align: center;
5    white-space: nowrap;
6    width: 100%;
7    top: 4px;
8    left: 0;
9}

In the case we have a reached a leaf node, we want to display the label in vertically in the center also.

1.node-leaf {
2    height: 100%;
3    padding: unset;
4    transform: translateY(50%);
5    top: -6px;
6}

Bringing it all together

Let's test what we have so far. We will first need to define a small tree. We will define a helper method in a moment for this, but lets do it by hand this time.

1const root = new Node('A');
2const A = root.addNode('B');
3A.addNode('a1');
4A.addNode('a2');
5const B = root.addNode('C');
6B.addNode('b1');
7const b2 = B.addNode('b2');
8b2.addNode('c1');
9b2.addNode('c2');

If we now plot this with the RecursivePlot class:

1> (new RecursivePlot('plot-0')).draw(root);

Because it is quite some work to create the tree by hand, here is a hacked together utility method, that will recursively generate a tree. The input of the method is an list of nodes for that level, e.g. [1, 2, 2, 2] for a binary tree.

 1const tree = (size) => {
 2    const __tree = (labels, size, depth) => {
 3        const __generate = (node, labels, size, depth) => {
 4            if (depth < 0) {
 5                return;
 6            }
 7            
 8            for (let i = 0; i < size[depth]; i++) {
 9                const child = node.addNode(labels[depth] + i);
10                __generate(child, labels, size, depth - 1);
11            }
12        }
13
14        const root = new Node(labels[depth]);
15        __generate(root, labels, size, depth - 1);
16        return root;
17    }
18
19    if (size[0] != 1) {
20        console.warn('First node should have size 1. Ignoring size.');
21    }
22    const labels = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'];
23    return __tree(
24        labels.slice(0, size.length).reverse(), 
25        size.reverse(), 
26        size.length - 1
27    );
28}

We can now use tree function to generate a binary tree. Let's see what this looks like.

1> (new RecursivePlot('plot1')).draw(tree([1, 2, 2, 2, 2, 2, 2]));

Pretty neat so far! I'm quite happy with the result. Let's see what else we can do with it.

Isometric projection

To get the same effect as in the first illustration, we will project the plot isometrically. Luckily this is quite easy with the following CSS class, that I've found after some googling around.

1.isometric {
2    transform: rotateX(60deg) rotateY(0deg) rotateZ(-45deg) scale(1.25);
3}

We apply the .isometric class to the HTML element of the root node. Note that we will only have to add this class once! We will add an argument isometric to the draw function. If this is set to true, the .isometric class is added to the element of the root node. I've included an example below to show the isometric projection.

HSL color scheme

To add some colors to the plot, we will define a HSL color scheme. This scheme starts from hsl(0, 50%, 80%), and eight steps, where at each step there is an increment of $30$ for the hue. The scheme can be applied to the plot by adding the .scheme-hsl class to the element in which the plot is rendered.

 1.scheme-hsl > .node {
 2    background-color: hsl(0, 50%, 80%);
 3}
 4.scheme-hsl > * > .node {
 5    background-color: hsl(30, 50%, 80%);
 6}
 7.scheme-hsl > * > * > .node {
 8    background-color: hsl(60, 50%, 80%);
 9}
10.scheme-hsl > * > * > * > .node {
11    background-color: hsl(90, 50%, 80%);
12}
13.scheme-hsl > * > * > * > * > .node {
14    background-color: hsl(120, 50%, 80%);
15}
16.scheme-hsl > * > * > * > * > * > .node {
17    background-color: hsl(150, 50%, 80%);
18}
19.scheme-hsl > * > * > * > * > * > * > .node {
20    background-color: hsl(180, 50%, 80%);
21}
22.scheme-hsl > * > * > * > * > * > * > * > .node {
23    background-color: hsl(210, 50%, 80%);
24}

Magma color scheme

The magma color scheme comes from matplotlib, a library for plotting in Python. The scheme can be applied by adding the .scheme-magma class to the element in which the plot is rendered.

 1.scheme-magma > .node {
 2    background-color: #310496;
 3}
 4.scheme-magma > * > .node {
 5    background-color: #6D01A7;
 6}
 7.scheme-magma > * > * > .node {
 8    background-color: #9B169E;
 9}
10.scheme-magma > * > * > * > .node {
11    background-color: #C03B82;
12}
13.scheme-magma > * > * > * > * > .node {
14    background-color: #DB5C66;
15}
16.scheme-magma > * > * > * > * > * > .node {
17    background-color: #F0814D;
18}
19.scheme-magma > * > * > * > * > * > * > .node {
20    background-color: #FBAE32;
21}
22.scheme-magma > * > * > * > * > * > * > * > .node {
23    background-color: #F7DF23;
24}

Sources

The entire source code of this project can be found on my GitHub.


© 2022 Lars Rotgers
All rights reserved