Exploring the squarified tree map algorithm with ReasonML (part 1)

This is part one of a two-part series. Read part two here

In this series of articles, we’ll explore the squarified tree map algorithm, which was invented by Mark Bruls, Kees Huizing, and Jarke J. van Wijk in a 2000 paper as an improvement on the “classic” tree map invented by Ben Shneiderman in a pioneering 1992 paper. This algorithm is used to lay out probably all the pleasing tree maps you see around.

In part one, we’ll explain how the algorithm works. In part two, we’ll implement it using a relatively new functional compiled-to-JavaScript language called ReasonML, or Reason for short.

Motivation

One of my broad areas of interest is exploring how functional, statically-typed languages can help developers write less error-prone and maybe even provably correct web applications. One language that seems promising is ReasonML, an alternative syntax for the programming language OCaml. When I decided to give Reason a try, I wanted to use it on a real problem instead of a toy problem and learn the language alone instead of together with other libraries e.g. ReasonReact. This approach will allow me to evaluate the strengths and weaknesses of the language itself instead of its ecosystem.

The problem I settled on is the squarified tree map algorithm. As a bonus, because I’ve already written a TypeScript implementation of it (published as an npm package), I’ll be able to compare the two languages. The Reason implementation will hew very closely to the pseudo code in Bruls’ paper to show how a mathematical algorithm can map fairly straightforwardly to Reason code.

I really enjoyed reading both papers and this article will adapt a lot of materials from them.

Hierarchical data

Although we encounter hierarchical information every day in the form of directory structures, organizational charts, family trees and so on, it can be hard to visually intuit their content, especially if the hierarchical structures are sufficiently large. The traditional approach to representing these structures is a node-and-link diagram: a rooted, directed graph with the root node at the top of the page and children nodes below the parent node with the lines connecting them.

Consider the following four-level hierarchical structure, which we’ll use as the sample input in this series, represented as one such diagram:

Sample multi-level tree

The nodes are labeled with letters. Longer labels indicate deeper-level nodes. The label of a child is prefixed by the label of its parent e.g. BAA\mathrm{BAA} is a child of BA\mathrm{BA}, which is in turn a child of B\mathrm{B}. Each node carries a numeric value that is either self-assigned if that node is terminal or the sum of the values of its children if not.

Although such diagrams are useful to visualize small trees, they become less effective when the trees get larger because the diagrams use space inefficiently (most of the pixels are used as background) and because it’s hard for users to grasp the entire picture. Furthermore, the diagrams only show structural relationship; additional information, such as the size and importance of each node, is ignored or has to be supplementally annotated in text form.

Classic tree maps

The classic tree map was proposed by Shneiderman as an alternative to node-and-link diagrams. Its two-dimensional space-filling representation is designed to be a better way for humans to visualize complex tree structures. The algorithm proceeds level-by-level through the hierarchy and alternate between horizontal layout for odd-numbered levels and vertical layout for even-numbered levels. Here is how his algorithm works when applied to the problem of visualizing the above sample tree inside a 6×46\times 4 container.

Level 1

Partition the original 6×46\times 4 container into the same number of rectangles as the number of children at level 1 (seven partitions) in the horizontal direction (left to right). Each child rectangle takes up the full height of the parent container and has a width that is in proportion of its node’s value in relation to the parent’s value. For example, rectangle A\mathrm{A}’s width is 32\frac{3}{2}, which is 14\frac{1}{4} of the parent container’s width (66) because node A\mathrm{A} has a value of 66, which is 14\frac{1}{4} of the Root\mathrm{Root} node’s value (2424). After this step, the result looks like this:

Classic tree map level 1 layout

In this series, we’ll call such a collection of rectangles laid out in the same direction a row.

Level 2

Because only node B\mathrm{B} has children, the layout is done for the other level-one nodes. The layout for level two is done vertically (bottom to top1) rather than horizontally. We partition rectangle B\mathrm{B} into two smaller rectangles corresponding to BA\mathrm{BA} and BB\mathrm{BB}, each taking up the full width of B\mathrm{B} and having a height proportional to to their associated numeric values. This is the final result:

Classic tree map level 2 layout

Level 3

Because only BA\mathrm{BA} has children, the layout is done for BBBB. We partition BA\mathrm{BA}, this time horizontally, into BAA\mathrm{BAA} and BAB\mathrm{BAB} in proportion to their values. This is the result: Classic tree map level 3 layout

Squarified tree maps

The main shortcoming of the classic tree map is that it produces thin, elongated rectangles. The squarified tree map algorithm, in addition to partitioning the container into proportional areas, also attempts to produce more square-like rectangles (hence the name “squarified”) by ensuring that the aspect ratios of the rectangles are as close to 1 as possible.

Square-like rectangles offer several advantages:

  • Because the number of pixels used for a rectangle’s border is proportional to its circumference, the display space is used more efficiently.
  • Square rectangles are easier to detect and point at whereas thin rectangles can create aliasing errors.
  • Visual comparison of the sizes of rectangles is easier when their aspect ratios are similar.

Instead of looking for the optimal solution, which would require finding all possible tessellations, whose number is very large, this algorithm returns a good solution that can be computed in a short amount of time.

The squarified algorithm is recursive both breadth-wise and depth-wise:

  • Like the classic algorithm, the squarified version considers one level of the tree at a time. Given the original container, we first try to produce square-like rectangles for the set of siblings at the top level. That is, we layout the nodes A\mathrm{A} through G\mathrm{G} inside of the Root\mathrm{Root} node as if no other nodes exist. Then the starting point for each set of siblings at the next level will be the square-like rectangle of their common parent. For example, the nodes BA\mathrm{BA} and BB\mathrm{BB} will be laid out inside of B\mathrm{B} as if all the other nodes didn’t exist. This process is repeated until all nodes at all levels have been laid out. This is the depth-wise recursion.
  • Unlike the classic algorithm, when a node is processed, a decision is made between two alternatives. Either the node is added to the current row, or the current row is fixed and a new row is started in the remaining subrectangle. This decision depends on whether adding a rectangle to the current row will make the rectangles in that row more square-like i.e. decrease the maximum aspect ratio among those rectangles. This is the breadth-wise recursion.

Example

To illustrate how the squarified algorithm works, we’ll once again try to visualize the earlier sample input tree inside a 6×46\times 4 container. The coordinate system used in this article follows the convention in a web browser: xx increases to the right and yy increases downward.

Coordinate system

Level one

First off, where do we put rectangle A\mathrm{A} (value 66)? If we lay it out against the container’s long side like this:

Rectangle A against the longer side of the container

the aspect ratio of A\mathrm{A} will be 66 but if we position it against the shorter side like this:

Rectangle A against the short side of the container

the aspect ratio of A\mathrm{A} will be only 832.67\frac{8}{3} \approx 2.67. Because we want the laid out rectangles to be as square-like as possible, we’ll pick the shorter side. In general, we always lay out rectangles against the shorter side of a container (can you convince yourself why?).

Let’s move on to rectangle B\mathrm{B} with value 66. As agreed previously, we will only lay out rectangles against the shorter side of their container, meaning B\mathrm{B} will appear above A\mathrm{A}. The choice of putting B\mathrm{B} above instead of below A\mathrm{A} is arbitrary but here we follow the choice made in the paper.

Rectangle B placed above rectangle A

The maximum aspect ratio of the current row (containing A\mathrm{A} and B\mathrm{B}) decreases to 32=1.5\frac{3}{2} = 1.5. Because adding B\mathrm{B} improves aspect ratio, this is a good location for B\mathrm{B}.

Let’s try to add C\mathrm{C} (value 44) to the current row:

Rectangle C added to row containing A and B

However, this causes the row’s maximum aspect ratio to increase to 44 (that of C\mathrm{C}). Because the aspect ratio is worsened by the addition of C\mathrm{C} to the current row, we’ll start a new row for C\mathrm{C} in the remaining space not occupied by A\mathrm{A} and B\mathrm{B}:

Rectangle C added to new row

The maximum aspect ratio of the newly created row is that of C\mathrm{C} i.e. 942.25\frac{9}{4} \approx 2.25.

We then add D\mathrm{D} (value 33) to the current row (along the container’s shorter side) because it improves the row’s maximum aspect ratio to 49271.81\frac{49}{27} \approx 1.81:

Rectangle D added to row containing C

However, adding E\mathrm{E} (value 22) to the current row:

Rectangle E added to row containing C and D

increases the row’s maximum aspect ratio to 92=4.5\frac{9}{2} = 4.5 (that of E\mathrm{E}), which is bad, so we’ll start a new row for E\mathrm{E} in the remaining space not occupied by A\mathrm{A} through D\mathrm{D}:

Create new row for E

This new row has a maximum aspect ratio of 22.

When we try to add F\mathrm{F} (value 22) to E\mathrm{E}’s row, the row’s maximum aspect ratio increases to 72252.88\frac{72}{25} \approx 2.88:

Add F to row containing E

Therefore, we’ll create a new row for F\mathrm{F} in the remaining space. This row has a maximum aspect ratio of 22.

Create new row for F

There’s only one spot for G\mathrm{G} (value 11) to go into so the final layout looks like this:

Final layout

Technically, the process here is that we add G\mathrm{G} to F\mathrm{F}’s row (by placing G\mathrm{G} to the right of F\mathrm{F}) but that increases the maximum aspect ratio of the row to 2592.78\frac{25}{9} \approx 2.78 so we create a new row for G\mathrm{G}, which, because G\mathrm{G} is the last node to be laid out, contains only G\mathrm{G}. Although the visual results of the two choices (adding G\mathrm{G} to current row versus creating a new one) are the same, it’s important to note that we did have to make a decision there.

Levels two and three

By applying the same reasoning (no pun intended), this is how the level-two nodes (BA\mathrm{BA} and BB\mathrm{BB}) are laid out inside their parent (B\mathrm{B}):

Level 2's layout

and this is level three’s layout i.e. the final result:

Level 3's layout

Note that in the final visual output, only terminal nodes of the original input tree remain whereas non-terminal nodes have disappeared.

This is the end of part one. I hope it gives you a sense of how the squarified tree map layout algorithm works. We’ll discuss the implementation in part two.


  1. The bottom to top direction is chosen for consistency with Bruls’ paper.