I'm building a social media network and collaboration tool based on mind maps, documenting my work in this series of blog posts. Follow me if you're interested in what I've learned along the way about building web apps with React, Tailwind CSS, Firebase, Apollo/GraphQL, three.js and TypeScript.
In the previous part I and part II, I've found out how to render React components on sprites in three.js and connect them with lines to make up a mind map root node and the first level of child nodes, displayed around the root in a circle.
Taking It to the Next Level
Today's goal is to draw the child nodes of the level 1 mind map nodes, which I'll call level 2 nodes.
The challenge will be to calculate the X/Y coordinates of those nodes so that they fan out from their parent node while not overlapping each other.
This sketch shows what I'm trying to achieve. Root node is pink, level 1 nodes are purple and level 2 nodes are blue.
Some considerations:
- while level 1 nodes are arranged in a circle, the level 2 nodes need to be arranged in semicircles, facing away from the direction of their parent node
- it will probably be non-trivial to make it so that there can be any number of level 1 nodes for a parent level 2 node and prevent them from overlapping; I'll need to adjust the radius of the semicircle of the level 2 nodes depending on the number of nodes
- even trickier: making sure that the semicircles of level 2 nodes don't overlap the adjourning level 2 nodes from other parent nodes
A Bit of Refactoring
Before I proceed with level 2, I do some refactoring:
function renderMindMap(div) {
const { scene, renderer, camera } = initializeScene(div);
const root = data.find((node) => node.parent === undefined);
const level1 = data.filter((node) => node.parent === root.id);
root.x = 0;
root.y = 0;
root.level = 0;
await addMindMapNode(scene, root);
const radius = 2;
for (let level1index = 0; level1index < level1.length; level1index++) {
const { x, y } = calculateLevel1Coordinates({
numberOfNodes: level1.length,
parent: root,
radius,
index: level1index
});
const level1node = {
...level1[level1index],
x,
y,
level: 1
};
await addMindMapNode(scene, level1node);
addConnection(scene, {
color: colors.magenta,
parentNode: root,
childNode: level1node
});
}
renderer.render(scene, camera);
}
I've moved the calculation of the coordinates for the level 1 nodes to a new module.
function calculateLevel1Coordinates({
numberOfNodes,
parent,
radius,
index
}) {
const slice = (2 * Math.PI) / numberOfNodes;
const angle = slice * index;
const x = parent.x + radius * Math.cos(angle);
const y = parent.y + radius * Math.sin(angle);
return { x, y };
}
Improving the Level 1 Node Layout
In my last post, I complained that the layout of the level 1 nodes doesn't look pleasing to the eye because the distance between the node rectangles varies. Someone gave me the tip to rotate the level 1 nodes by 90 degrees. Currently, the circle of nodes starts to the right of the root node. I subtract 90° from the angle, so that the circle of level 1 nodes starts above the root node, and lo and behold – looks much better already!
Thanks PrudiiArca!
Adding Level 2 Nodes
Now I'm ready to add the next level of nodes. For now, I'm just copying the code from calculateLevel1Coordinates.js to calculateLevel2Coordinates.js, knowing full well that this will have to be adjusted, but let's just see how it turns out without any changes.
In renderMindMap.js, I add another for-loop to add the level 2 nodes to the scene:
for (let level1index = 0; level1index < level1.length; level1index++) {
const { x, y } = calculateLevel1Coordinates({
numberOfNodes: level1.length,
parent: root,
radius,
index: level1index
});
const level1node = { ...level1[level1index], x, y, level: 1 };
await addMindMapNode(scene, level1node);
addConnection(scene, {
color: colors.magenta,
parentNode: root,
childNode: level1node
});
const level2 = data.filter((node) => node.parent === level1node.id);
for (let level2index = 0; level2index < level2.length; level2index++) {
const { x: x2, y: y2 } = calculateLevel2Coordinates({
numberOfNodes: level2.length,
parent: level1node,
radius,
index: level2index
});
const level2node = { ...level2[level2index], x: x2, y: y2, level: 2 };
await addMindMapNode(scene, level2node);
addConnection(scene, {
color: colors.violet,
parentNode: level1node,
childNode: level2node
});
}
}
The result looks weird, as expected:
Arranging the Level 2 Nodes in Half Circles
Being bad at math, I had to do a lot of trial and error to figure out how to arrange the level 2 nodes in a half circle, facing away from the parent node.
Here's what I came up with:
function calculateLevel2Coordinates({
numberOfNodes,
parent,
radius,
index
}) {
const slice = Math.PI / (numberOfNodes - 1);
const angle = slice * index + parent.angle - (90 * Math.PI) / 180;
const x = parent.x + radius * Math.cos(angle);
const y = parent.y + radius * Math.sin(angle);
return { x, y, angle };
}
Now the mind map graph looks good:
To Be Continued…
Stay tuned for the next episode where I will either:
- turn my code for rendering the mind map into a recursive function to facilitate arbitrary nesting depth
- OR throw most my work so far away and try rendering the nodes with a force graph (another useful suggestion someone made)
We'll see…
Top comments (1)
Is there any source for creating a mindmap with react by user not just drawing the list that we have?