I am trying to create a word cloud. In order to render text to the screen I am generating a random position for each word. This works perfectly, however there are a lot of overlapping words. In order to solve this I am storing the position and size of the elements in an array and then I created a helper function that checks for collisions, generates a new position for the element if it finds one, and then calls it's self again to check again from the start of the array. When I run my code the first 2-3 words render just fine but then I get an error saying Maximum call stack size exceeded. I saw there was already a post on this same issue on stack overflow.
I saw that the other person was using a forEach function and so was I so I converted it into a for loop like the answer suggested but it did not do anything. I think the issue boils down to the fact that there are so many collisions but I am not sure how to best approach the issue. Is there another way that I can generate unique positions for elements while still avoiding collisions?
Code:
function calculatePosition(parent, child) {
return Math.random() * parent - (child / 2)
}
// needed for rendering position of span elements
var ranges = []
var totalWidthOfWords = 0
var totalHeightOfWords = 0
// reposition element if there is a collision
function checkForCollisions(element, height, width, wordCloud, injectedSpan) {
for(var i = 0; i < ranges.length; i++) {
let current = ranges[i]
if(element.left >= current.width[0] && element.left <= current.width[1]) {
injectedSpan.style.left = calculatePosition(wordCloud.clientWidth, width) + "px";
checkForCollisions(element, height, width, wordCloud, injectedSpan)
}
if(element.top >= current.height[0] && element.top <= current.height[1]) {
injectedSpan.style.top = calculatePosition(wordCloud.clientHeight, height) + "px";
checkForCollisions(element, height, width, wordCloud, injectedSpan)
}
}
}
// Create content in DOM
const injectedContent = data.map(line => {
const injectedSpan = document.createElement("span")
const injectedWord = document.createElement("p")
const wordCloud = document.querySelector(".word-cloud")
// mod weight value to get more managable inputs
let weightValue = (line.weight * 100).toFixed(2)
// sets values of words and renders them to the screen
injectedWord.innerText = line.word
injectedSpan.appendChild(injectedWord)
wordCloud.appendChild(injectedSpan)
// sets style attribute based on weight value
injectedWord.setAttribute("style", `--i: ${weightValue}`)
// flips words
if(Math.random() > 0.75) {
injectedWord.style.writingMode = "vertical-rl";
}
// Entrance animation
let left = innerWidth * Math.random()
let top = innerHeight * Math.random()
if(Math.random() < 0.5) {
injectedWord.style.left = "-" + left + "px";
injectedSpan.style.left = calculatePosition(wordCloud.clientWidth, injectedSpan.clientWidth) + "px";
} else {
injectedWord.style.left = left + "px";
injectedSpan.style.left = calculatePosition(wordCloud.clientWidth, injectedSpan.clientWidth) + "px";
}
if(Math.random() < 0.5) {
injectedWord.style.top = "-" + top + "px";
injectedSpan.style.top = calculatePosition(wordCloud.clientHeight, injectedSpan.clientHeight) + "px";
} else {
injectedWord.style.top = top + "px";
injectedSpan.style.top = calculatePosition(wordCloud.clientWidth, injectedSpan.clientWidth) + "px";
}
// Get position of span and change coordinites if there is a collision
let spanPosition = injectedSpan.getBoundingClientRect()
console.log(spanPosition)
if(spanPosition) {
checkForCollisions(spanPosition, spanPosition.height, spanPosition.width, wordCloud, injectedSpan)
}
totalWidthOfWords += spanPosition.width
totalHeightOfWords += spanPosition.height
ranges.push({width: [spanPosition.left, spanPosition.right], height: [spanPosition.top, spanPosition.bottom]})
})