Create a Treemap Using d3 and React With Wrapping SVG Text

Alex
The Startup
Published in
12 min readOct 30, 2020
Photo by Todd Quackenbush on Unsplash

Treemaps are a great way to view hierarchical data sets using nested rectangles. In this article we’re going to create a Treemap utilizing the d3 JavaScript library in React to visualize a set of static data.

Setting up our environment

First we’ll need to create a React template which we’ll do using the create-react-app npx command found below. This will set up a React template for us and help us hit the ground running.

npx create-react-app treemap

If you are unfamiliar with npx or npm please head over to nodejs.dev to catch up and learn more. Additionally the official React website is a great resource to touch up on any React skills/methods that are unfamiliar to you in this article.

Also, let’s make sure we install d3:

npm install d3

Getting started

Now that we’ve got our template set up and dependencies installed lets clear some of the boilerplate React code in App.js that we won’t be needing.

***Note: For those who are familiar with React we’re using React 17 which does not require importing React in each file any longer.

You’re right! We haven’t created the Treemap component yet. Let’s do that now.

***Note: Because this is such a small project and for simplicity’s sake I will be keeping all components and other files within the “src” directory. Generally for larger projects you’ll want to break up your app into categorized, maintainable directories for more clarity and easier usage.

Now that we’ve got our components set up let’s create some data for our Treemap to use. Feel free to copy the data set I have provided below or if you’d like to create your own feel free. Just make sure that you stick to the same format I have below to follow along without any discrepancies.

Our data-set will be the average points per game for the 2019–2020 season for each player on the NBA’s Boston Celtics roster, categorized by position. Now that we’ve got our data let’s start working on our Treemap!

The Treemap

Before we can render any cells in our treemap we need to update our component a little bit. First we’ll be passing in the data we want the treemap to use as a parameter along with the height and width (line 4).

Next we’re going to create a ref to the SVG so that d3 has access to it (line 5).

Now we create our renderTreemap function which will be called on the initial render of the component or if the data changes. In that function we have d3 construct a root node with our hierarchical data and then pass that root node into our treemapRoot in order for it to create a treemap layout.

As you can see on line 13 we use the d3.hierarchy function to create that root node, next sum all of our data in each child node, and finally sort the children in each node so that when the treemap renders our rectangles are sorted by the largest value.

On line 17 we create that treemap layout using d3.treemap, denote the size it has to work with within the SVG, add a small padding border in between the cells, and finally invoke the function with our root node we just created on line 13.

Make sure we update the App.js file to pass in the data, height and width values to the Treemap component.

Next up we’ll be rendering our treemap nodes and get to see d3 in action!

Rendering the Treemap Nodes

If you haven’t already, go ahead and run

npm start

in your terminal in the root directory of the project. This will start the app in development mode and open up a browser where you can see our app update live as we add changes to it.

Ok, onto the nodes!

On line 7 we are first grabbing our SVG and then selecting all current ‘g’ elements on it and joining them with our data. If there aren’t any ‘g’ elements, the following ‘join’ method discussed below will take care of that.

The data we’re using is from our treemapRoot we created earlier. The ‘leaves’ method will give us back an array of every single node in our hierarchy. Feel free to punch in a console.log to understand better.

console.log(treemapRoot.leaves())

The join method I previously mentioned is very powerful and is what makes d3 such a great tool for visualizations. Join will add, remove, or reorder elements to match incoming data with previous data. Because we did not have any previous data on our first render it will simply add ‘g’ elements to our SVG, mapping them to each data point. We then use the transform attribute on line 11 to set the origin points for each ‘g’ element.

Next we define our colorScale along with a fader function that will increase the opacity on our colors so that they aren’t as dark and make text easier to read.

Finally on line 16 we grab our nodes and append an SVG ‘rect’ to each one with height and width attributes. These x0/y0, x1/y1 values were created by the root and treemapRoot functions we defined earlier. Definitely toss in a console.log on the treemap leaves as I mentioned a few paragraphs ago to see exactly what we’re given with each node or head over to the d3 documentation to dive deeper.

On line 20 we add a fill color to each rect based on the category the currently selected node is in. We pass that into the colorScale function and it will return a color based on the parameter passed. If you want to mess around feel free to change the parameter to ‘d.data.name’ or ‘d.data.value’ to see the colors change. For this project though, we’ll be sticking with ‘d.data.category’.

Awesome! You should have a colorful treemap rendering on screen. If not be sure to check over your code or look at what our current Treemap component should look like below.

Adding Text

Alright let’s truly bring our Treemap to life with some text.

Pretty straightforward but we’ll walk through it quickly. We start with our nodes and append a ‘text’ element to each node. We’ll then set the text, font-size, x-attribue, and y-attribute and voila! We have text for each cell!

Wait a sec, some of the text is overflowing and we can’t see some of the data…

Unfortunately SVG’s do not have an attribute that allows for text-wrapping, so we’ll have to come up with one on our own. Let’s take care of that next.

Wrapping Text in an SVG

Credit for this function goes to the original developer of d3, Mike Bostock. He was kind enough to create a ‘Wrapping Long Labels’ example and shared it on his website. I used it as a base and refactored it to fit our needs.

We have to make a few additions to our text-appending code from above to make it work right. See below.

The first addition is line 8 where we’re adding a data attribute, naming it ‘width’ and setting it equal to the width of the node. This will be important later in determining how much width our text has before it needs to be wrapped.

The next addition is line 12 where we’re calling our yet-to-be-defined function ‘wrapText’. The call method will be invoked once and will be passed the current selection as a parameter.

Alright let’s get started on this text wrapping function.

There’s a lot going on here so lets line by line. On line 2 we’re calling an ‘each’ method on our selection which is straight from the d3 library and one we can only call on d3 selections. Remember, when we invoked this wrapText function with the ‘call’ method, the current selection is automatically passed as the first parameter to wrapText.

The ‘each’ method will allow us access to each node within that selection. In our case that selection will contain every ‘g’ element in our treemap which also contains each ‘text’ element.

On line 3 we define a variable ‘node’ as a d3 selection of ‘this’. ‘This’ refers to the current node that ‘each’ is mapping over in our selection. From there we’re able to grab the width of the current element from the attribute ‘data-width’ we added earlier and coerce it to a number if it isn’t already with the ‘+’ shorthand.

After that, on line 5 we grab all of our text and split it into an array and then reverse it (it’ll look like this [20.4, “Walker”, “Kemba”]). This will allow us to pop words off later and reconstruct our text.

We then create a temporarily empty array and name it ‘line’ on line 6. We’ll use this reconstruct our new text as we calculate text length later.

On lines 8 and 9 we’re grabbing our x and y-attributes to use for our new text.

And finally on line 10 we’re creating a tspan and initializing it with no text.

Line 11 is where we’ll keep track of what line number we’re on when we start creating new tspans so that we don’t have any text that overlaps.

Now the fun begins.

Line 12 begins our loop which will continue until our original text is down to its last element (our numerical data). We’re going to save this for last because we want our numerical data to have it’s own line to make it more pronounced.

On line 13 we’re popping off the last word in the array of our original text which — because we reversed it — is what we want our first word to be. We’re then pushing that word into our initially empty array (line 14) and then setting it as the text for the tspan we created just before the loop.

By creating the tspan beforehand and then adding text we will be able to check and see how long that text is and then compare it to the width of our current ‘rect’ element (which we got from the ‘g’ element however those values will be identical because we calculated them the same way).

In addition to checking our tspan’s text length on line 17 we’re also checking to see if our line array has a length of 1. If it does that means it is the first word in the original text and it doesn’t matter if it’s too long, we don’t need to create a new line, that would leave an odd blank space at the top. See below.

If the text isn’t longer than the width, the loop will continue and add another word to our line array and then join that line array together with a space as a string and set it as the tspan’s text. We’ll retake the length of the tspan’s text and check again if the newly added word will make this line longer than the width of our rect.

Now if the text length of our tspan is in fact longer than the width AND it isn’t the first word we pop the last word we added off of our ‘line’ array (line 18), join that newly shortened ‘line’ array into a string and set the text of the current tspan to that string (line 19)

Next we redefine ‘line’ as an array of ‘word’ (line 20) which was the word that previously made the text in our tspan overflow our rect and then redefine tspan as a new tspan on a new line with its text as the word that made the previous tspan’s text overflow our rect.

Phew! If you got through that unscathed, kudos! It took me a few tries to write out and understand myself.

Now the loop will continue checking lengths and widths and creating new tspans until it reaches that last element in our ‘words’ array and break out.

We’ll then append that last element— which is our numerical data value — to the end of our node as its own tspan to make sure that it is clearly visible and has its own line.

The addTspan function on line 28 is fairly straightforward. The one note I did want to mention is the line number. Every time add a tspan we need to increase the line number by 1 so that we can calculate the proper amount of space to shift the tspan down by and give that to the ‘dy’ attribute.

We should now have a Treemap component with colors mapped to their respective cells’ categories and text that wraps within its own cell.

Unfortunately, text for one of the cells still doesn’t quite fit but there are options out there to alleviate this issue. You could potentially hide the text so it isn’t visible at all or create tooltips to pop up when you hover over the treemap. Those options however won’t be covered here in this article (but maybe in the future!).

Let’s wrap up and complete our Treemap with a key.

Creating a Key

First we’re going to create a new SVG just for the legend. We’ll add another one to our return statement in the Treemap component and hand it a ref variable as well.

Alright let’s take a look at creating that legend! First on line 11 we need to get the categories for our data. We grab root.leaves which gives us every node in our data. We then go into each node, grab its category and map each one into a new array.

Because we’ve gone into each individual node we now have an array with repeating category names. Line 13 takes care of this by filtering out all of the repeats and hands us back a clean, new array without any repeating categories.

Before we use those categories we need to give our newly added SVG we called ‘legendContainer’ some height and width attributes. We’ll then define a ‘legend’ variable which will hold the entire selection of all the ‘g’ elements we’ll create (or ‘join’) based on the categories array (line 19).

Starting on line 21 we grab the ‘legend’ selection and append ‘rects’ for each category. We’ll keep things simple and set the height and width of each rect to the fontSize variable we defined earlier.

We give the rect’s x-position a value of fontSize because we want to give it a little bit of left padding so that it isn’t right up against the edge of the SVG.

On line 26 we double the y-attribute of the rect to create a block of space equal to its dimenions below it and then multiply that by the data’s index in order to shift the rect down so that each legend rect doesn’t land on top of one another.

Line 27 fills the rect’s color using the same colorScale function and parameter that we used when initially filling the rects for the treemap. This ensures that we’re getting the correct category color that matches the rects in the treemap.

Line 29 initiates adding the text next to each rect. We append a text element and then transform it so that it matches the height of the rect (line 31). We then shift it to the right by setting its x-attribute a multiple of fontSize (line 32) and shift it down by setting its y-attribute the same way we set the rects’ (line 33).

Lastly we set the font size (line 34) and pass in the value of the text we want it to display (line 35).

The one final thing we want to add to the top of the renderTreemap function is the removal of all ‘g’ elements from each SVG to ensure that if we do pass in another data set the text and legends are removed from the previously rendered SVG to avoid any left-behind elements.

Boom! Just like that we’ve created a Treemap component with wrapping SVG text utilizing the d3 JavaScript library with React. Feel free to see if your treemap looks like the one below, if not take a look and compare yours to my final Treemap component code following the image or check the repository out on Github.

Keep an eye out for another post where I take this same treemap but animate it and make it zoomable!

--

--

Alex
The Startup

Avid computerer, giant Celtics and Patriots fan