Building a Treemap with JavaScript
At Foxintelligence, while building our data visualization platform we wanted to challenge the pie chart. This is our journey of building a Treemap in JavaScript from scratch.
EDIT: an open source JavaScript package
I published an open source JavaScript package that calculates the Treemap. It uses the algorithm described in this article and is available both with npm and in the browser.
https://www.npmjs.com/package/treemap-squarify
The goal: an effective data visualization
The goal was to find the best way to represent market shares among categories in e-commerce. The pie chart representation is the first one that comes to mind for this kind of data.
However, it can be hard to compare different categories in a pie chart. The pain points of this type of visualization are:
- It’s hard to compare pie slices
- It’s hard to display labels on pie slices
- It’s hard to compare two pie charts
What kind of data visualization can we use instead?
🙌 The Treemap
That’s where we discovered the Treemap. A Treemap is a way to visualize hierarchical data in a rectangle (hence tree in Treemap). The value of one data point is proportional to its area in the main rectangle. One popular use is to visualize a computer file system. Here is an example of the initial tree and its counterpart on a Treemap.
In our case however, we don’t use it this way as we have a list of data instead of a tree. Nevertheless, we wanted to keep the idea of having one data point represented by an area on one main rectangle. In the following example, we have one main rectangle which represents 100% and each rectangle is a category market share of x%.
Why is it better?
First it’s visually easier to compare rectangle area than pie slices. It’s also easier for the label of each area to be displayed on the graphic instead of having to refer to the legend each time you need to. Plus we wanted to compare one merchant with the market (or with another merchant), and having two pie charts next to each other is not readable.
🎯 The specifications
So we’ve decided to use a Treemap as our data visualization. Let’s now talk about the specifications from the design/product team.
- Each area should be a rectangle that is as square as possible (which is the Squarified Treemap type of Treemap)
- We want 100% customization possibility over the UI (colors, hover effects, label or icon inside each square)
Leveraging existing knowledge
The first step as a software engineer was to look for existing solutions, and search about this type of visualization. We use Node.js for our application back-end and Vue.js for our front-end. So we turned to npm (the most popular package registry for JavaScript) and looked at existing packages that could help us.
Unfortunately there wasn’t anything we could rely on. What we found was either not working properly, not documented, or not maintained. And some of the existing solutions were not customizable enough and did not meet our requirements.
Another solution was to use a data visualization library. There is a Treemap component on D3.js for instance, but we decided not to use this solution. D3.js is a powerful JavaScript library but it seemed a bit too much to add it on our project only for the Treemap. Plus it has a steep learning curve and not adapted to a one-week sprint.
Instead, we tried to understand how the Treemap works and how to calculate the coordinates of the Treemap rectangles with the initial data. We found a very interesting paper where Mark Bruls, Kees Huizing, and Jarke J. van Wijk explained the squarified algorithm [1].
The squarified Treemap algorithm
This part is essentially a popularization of the article referenced at the end of this post [1].
The goal of the algorithm is to find a 2D representation where each rectangle fits in the main rectangle while optimizing each rectangle square aspect. Let’s call the ratio between the length and width of the rectangle the square ratio. A rectangle is square if the square ratio = 1
. So the algorithm is aiming at having a square ratio for each rectangle as close as possible to 1.
The best way to understand how it works is through an example. Let’s take a main rectangle of 4
by 6
and the data (our rectangle areas): 6, 6, 4, 3, 2, 1
. We can check that 4 x 6 = 24
and 6 + 6 + 4 + 3 + 2 + 1 = 24
, so the data will fit in the rectangle.
The algorithm they suggest is this, step by step:
- Take the least wide side of your main rectangle, so the 4-side
- Take the first data,
6
and put it along the4
side:
- Calculate the square ratio of this representation: the rectangle we placed have two sides: one is
4
and the other is1.5
(because6 / 4 = 1.5
). Which gives a square ratio of4 / 1.5 ~ 2.7
. This is quite far from1
and we can see the rectangle is far from square. The distance from a square is2.7 — 1 = 1.7
. - Now we place the next data we have along the same side:
6
- We get two rectangles that have a new square ratio of
2 / 3 ~ 0.7
. This one is closer to1
(1 — 0.7 = 0.3
and0.3 < 1.7
). Which means this representation is a better optimization of square rectangles. So we discard the previous iteration and keep this one. - We do it again with the next data:
4
- Now the square ratio is
1 / 4 = 0.25
which is further than1
compare to the previous one0.7
so we deformed our rectangles more than necessary. We discard this iteration and the previous one is the one that is the most optimized.
We did the full process for one side of the main rectangle, now we have a empty main rectangle (4 x 3
) and the data list that remains is 4, 3, 2, 2
.
We can now apply the same steps again to get the next iteration and so on. That’s what we call recursion.
At the end the final representation is:
Now that we understand the algorithm, let’s look at the formalized way to express it.
⚙️ The JavaScript implementation
In the paper mentioned, the formalized algorithm is this:
To make it work in JavaScript:
- We have to add a case for the recursion to end if we don’t want an infinite loop. The condition to stop iterating is when there is only one child left
if length children = 1
- For the first iteration,
row
is empty and we want to call the recursion at least one time (we add a caseif length row = 0
) - There is a typo in the condition in the main
if
statement. Indeed it’s whenworst (row++[c], w) ≤ worst(row, w)
that we continue to add an element in the current row.
This gives us:
We then need to implement the layoutRow
function. In a plane space, we can describe a rectangle by 4 numbers:
- The coordinates of the top left corner (
x
andy
) - The
height
- And the
width
We used these 4 numbers to represent one rectangle as it also works with the SVG format.
After that, the layoutRow
function has to differentiate when the wider size of the rectangle is in the vertical direction or the horizontal direction. It also needs to keep track of where we are in the main rectangle. All the while, it must avoid creating a rectangle out of the scope or making two data points overlap.
💅 The front-end component
We use Vue.js as our front-ent framework but it should be easy to transfer this part to React.js instead. Nothing is specific to Vue.js.
Once we had a function that could take our data and return the representation we set up above, we were able to implement our front-end component.
For a simple Treemap this part is pretty straight forward with SVG, we used <rect>
and <text>
(for the label) elements inside <g>
elements to loop over our data.
In our case, we mainly added two features:
- A navigation through the Treemap (you can deep dive inside one category and have another Treemap with all subcategory market shares)
- A hover effect to compare the same category between two merchants (2 Treemaps next to each other). When you hover one rectangle on one Treemap, the same category on the other Treemap has the same hover effect.
Which gives…
🌱 Improving data visualization
This was our journey to create a data visualization that we hope will help our users. So that they can understand more effectively the data we provide them and help them make better decisions. It was challenging because we had to build it from scratch but we achieved exactly what we wanted product-wise.
References
[1] Bruls, M., Huizing, K., & Van Wijk, J. J. (2000). Squarified treemaps. In Data Visualization 2000 (pp. 33–42). Springer, Vienna.