Dynamically rendering a Word Cloud

Alex Motor
8 min readAug 26, 2021

--

I recently had the opportunity to work for a start up as part of my Labs training for Lambda School. Labs is the final course before graduation at Lambda. This experience has been truly amazing and I got the chance to work collaboratively with some pretty incredible and talented people. Our team was comprised of 6 people (3 Data Science students, and 3 Full Stack Web developers). I myself was one of the latter group. The start up that we got to work with is Called Story Squad. They are developing a product that engages and encourages children to think critically while also helping to improve their reading and writing comprehension.

One of the more personal parts of their application involves allowing parents to see their children’s writing progression over time by displaying words that they used in their stories in the form of a word cloud. My team was tasked with completing this feature. At first the task seemed a bit daunting. It is one thing to query an API and render the words that are sent back onto the screen, but as it turns out rendering the text in an appealing way in the form of a word cloud without having words overlap while maintaining full responsiveness was a pretty big challenge.

Rendering the word cloud

The first challenge that we had to overcome was rendering the size of the words conditionally based on a weight value that was returned from our data science team mates. They were able to create an API that took in a string of words and returned each word individually as well as a weighted value that was assigned based on a predetermined complexity metric. If you are curious about the process involved with creating this specific algorithm you can read more about it here

The data that was returned from the API was in the form of an array of objects. This meant that in order to render the text to the string we could simply map over the array and render the word found at each index. During my initial research I learned that it is possible to set an index value that can be used by CSS to achieve different size variations for each word. This was easy enough to implement with the following line of code

newWord.setAttribute("style", `--i: ${word.weight}`)

This allowed the font size to be changed in the CSS file by making use of the index of the word as well as the calc function.

font-size: calc(6vw / (var(--i) + 1) + 2vh);

This also had the added benefit of being pretty responsive right away because we are making use of the viewport’s width and height to get the final size of the words. Every time the window is resized the calc function runs and generates a new size for each element. This allowed us to achieve the following result.

Word Cloud version 1.0

Now that the word cloud is rendering size dynamically the next hurdle was to improve the way the words looked once they were rendered onto the screen. That was accomplished with the following lines of JavaScript

injectedWord.style.marginTop = "-" + (line.weight * (Math.random() * Math.random() * Math.random())) + "rem"

This code made it possible to change the positioning of the words so the end result was not so linear. I also wanted to render some words vertically to further break up the grid like layout. That was accomplished with the following if condition

if(Math.random() > 0.8 && line.weight > 3) {
injectedWord.style.writingMode = "vertical-rl";
}

This code changes the writing mode to vertical if the value of a random number between 0 and 1 was greater than 0.8. This meant that roughly only about 20% of the words would be rendered vertically. The last part of version 2.0 of the word cloud was to change the color of words so it was not so monochromatic. That was accomplished with this code

var color = "#" + Math.round(0xffffff * Math.random()).toString(16)
injectedWord.style.color = color

The first part of this code generates a unique hexadecimal color value for every word. The value 0xffffff in JavaScript evaluates to the integer 16777215 due to a JavaScript feature called Type Conversion. We then multiply that integer by a random number and then cast the resulting value to a string with base 16 encoding thanks to the radix parameter. That returns us a valid hexadecimal color code with a different value for each word. We then are able to just change the color directly in JavaScript. Putting all of that together we were able to generate the following word cloud.

Word Cloud 2.0

Already things are looking a lot better between the two versions, however there were still a few problems with this version of the word cloud that needed to be addressed.

  1. Because we are setting the color values based on a completely random calculation some of the colors of the individual words are not that appealing.
  2. The word cloud still looks a bit too linear and the words occasionally (though rarely) end up overlapping.

Version three of the word cloud was created to address these problems. I started out by rethinking my approach for generating random color values all together. Instead of generating completely random color values it made more sense to create a library of colors that worked well together. That was done by creating the following function

function generateColor() {
let number = Math.random();
if(number <= 0.10) {
return "#9BE1DF";
} else if(number <= 0.20) {
return "#F94144";
} else if(number <= 0.30) {
return "#F3722C";
} else if(number <= 0.40) {
return "#F8961E";
} else if(number <= 0.50) {
return "#F9844A";
} else if(number <= 0.60) {
return "#F9C74F";
} else if(number <= 0.70) {
return "#90BE6D";
} else if(number <= 0.80) {
return "#43AA8B";
} else if(number <= 0.90) {
return "#4D908E";
} else {
return"#277DA1";
}
}

I then could just call this function for every word directly in the map function. Every color has a roughly equal probability of being selected so there were no colors that were repeated too much throughout the word cloud. With the color problem solved I moved onto addressing the blocky look of the letters.

For this I actually ended up using CSS to alter the rotation of the letters for every n child. The code for this was the following.

.word-cloud span:nth-child(n) {
transform: rotate(45deg);
}
.word-cloud span:nth-child(2n) {
transform: rotate(-40deg);
}
.word-cloud span:nth-child(3n) {
transform: rotate(-90deg);
}
.word-cloud span:nth-child(4n) {
transform: rotate(120deg);
}
.word-cloud span:nth-child(5n) {
transform: rotate(345deg);
}
.word-cloud span:nth-child(6n) {
transform: rotate(20deg);
}
.word-cloud span:nth-child(7n) {
transform: rotate(240deg);
}

Putting both of these improvements together got us the the final word cloud.

Word Cloud 3.0

Animating the word cloud

With the look of the word cloud completed the next big task was to add animation to it. We really wanted the end user to have their attention drawn to the word cloud as soon as they logged in. For this we needed to add some sort of motion and movement.

I started off by researching animation libraries and I did find a few that would animate the letters; like anime.js for example. Ultimately since I was only concerned with creating an entrance animation I decided it would actually be easier just to use CSS and a bit of JavaScript to accomplish this task.

I learned in version 1 of the word cloud that it was possible to modify the CSS of each element directly in JavaScript. Since I wanted the letters to animate in I was able to just randomly assign a left and right value to each element and position them absolutely on the screen. The code for this is the following

let left = innerWidth * Math.random()
let top = innerHeight * Math.random()
if(Math.random() < 0.5) {
injectedWord.style.left = "-" + left + "px";
} else {
injectedWord.style.left = left + "px";
}
if(Math.random() < 0.5) {
injectedWord.style.top = "-" + top + "px";
} else {
injectedWord.style.top = top + "px";
}

This code renders the words in completely random locations on the screen. All the words should still fit in the canvas because we are multiplying by the height and width of the parent element. Doing this gave us the following result

Random location of elements

The last part of the animation turned out to be pretty straight foreword. I started out by setting the initial opacity for each element to 0. This hid them on the screen. I then created a really simple custom keyframe animation that reset the top and left properties of each element to 0 and changed the opacity to 1. The code for this is the following

@keyframes enter {
100% {
top: 0;
left: 0;
opacity: 1;
}
}

I could then just call the keyframe animation from the element’s CSS with the following

animation: enter 5s ease-in-out ease-in;
animation-fill-mode: forwards;

The important part here is to remember to set the animation fill mode to foreword. Or else the changes made by the keyframe animation will be reverted after the animation loops. This would cause the word cloud to animate in and then jus disappear. Putting all this together gave us this result

animated word cloud

With this the job was almost done. The keen — eyed reader will notice that the animation is a bit choppy and does not animate smoothly throughout the entire duration. To address this I had to use a not so well known CSS function called a cubic bezier. This allows us to set a custom animation curve for the entrance animation based on 4 values that we pass to it. The only thing that you have to remember is that the curve should end when your animation does. Since we are animating based on a 5 second window we needed to make sure that the bezer curve totaled 5. The code for the curve is as follows

transition-timing-function: cubic-bezier(0.5, 1.75, 1.75, 1);

With the bezier curve set up our final animation is smooth and polished.

--

--

Alex Motor

I am a freelance web developer who loves to create beautiful websites that drive customer interaction.