## DEV Community is a community of 848,284 amazing developers

We're a place where coders share, stay up-to-date and grow their careers. Patrick Hund

Posted on • Updated on

# Drawing a Mind Map with Three.js and React, for Real This Time

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've found out how to render React components on sprites in three.js. My plan is to create a mind map. So far I've got the root node of my mind map displayed, yay!

# Rendering the Nodes in Radial Arrangement

OK, so now to the part I have been dreading: figuring out how to arrange all the mind map nodes defined in my data.json file so that the are fanning out from the root node in a radial layout. Calculating the positions on the nodes will involve some trigonometry. I'm terrible at math… I'll take baby steps. Let's render only the root node and the level 1 nodes for now. The level 1 nodes will be arranged in a circle around the root node.

Here's my code to find the root node and the level 1 nodes, then render them:

renderMindMap.js

``````import addMindMapNode from './addMindMapNode';
import initializeScene from './initializeScene';
import data from './data';

export default async 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;

for (const level1node of level1) {
level1node.level = 1;
// TODO:
//level1node.x = ?;
//level1node.y = ?;
}
renderer.render(scene, camera);
}
``````

The big fat TODO here is to calculate the `x` and `y` properties for each level 1 node.

I drew me a little picture to illustrate the problem: Where else could I find the answer that on trusty old StackOverflow?

Here's a solution using C#:

``````void DrawCirclePoints(int points, double radius, Point center)
{
double slice = 2 * Math.PI / points;
for (int i = 0; i < points; i++)
{
double angle = slice * i;
int newX = (int)(center.X + radius * Math.Cos(angle));
int newY = (int)(center.Y +``````

I translate the C# code from the StackOverflow post to JavaScript:

``````import addMindMapNode from './addMindMapNode';
import initializeScene from './initializeScene';
import data from './data';

export default async 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;

const slice = (2 * Math.PI) / level1.length;
for (let i = 0; i < level1.length; i++) {
const level1node = level1[i];
level1node.level = 1;
const angle = slice * i;
level1node.x = root.x + radius * Math.cos(angle);
level1node.y = root.y + radius * Math.sin(angle);
}
renderer.render(scene, camera);
}
``````

This works!

# Making the Child Nodes Look Evenly Spaced

Here are screenshots of mind map nodes drawn with this code, with a varying number of level 1 nodes: Although the child nodes are distributed evenly around the root node, in some cases it looks wonky, for example with 3, 7 or 9 child nodes. The problem is that the mind map nodes are rectangles. If they were squares or circles, it would look better (more even). The red segments of the circle I have drawn here have different lengths. For my mind map nodes to look evenly distributed along the circle, these would have to have equal lengths, i.e. I have to take the width and height of the mind map nodes into account when calculating the angle for each node.

I have to admit, I'm at a loss how to calculate this, so I've posted questions on StackOverflow and StackExchange Mathematics, let's see how it goes.

If someone is reading this who can help, please let me know!

# Connecting the Dots

Meanwhile, I continued my work with the connections between the root node and the level 1 node. Drawing lines with three.js is surprisingly hard.

When I naïvely used `THREE.LineBasicMaterial` and `THREE.Line`, as explained in the three.js documentation, I discovered that the lines were always 1 pixel thin, no matter what line width I set.

The problem is that WebGL doesn't support drawing lines really well. Quoting the docs:

Due to limitations of the OpenGL Core Profile with the WebGL renderer on most platforms linewidth will always be 1 regardless of the set value.

I resorted to using the library THREE.MeshLine, which seems like using a sledgehammer to crack a nut, since it is a powerful tool in its own right that can do much more amazing things than just drawing a straight, thick line.

``````import * as THREE from 'three';
import { MeshLine, MeshLineMaterial } from 'three.meshline';

const lineWidth = 5;

scene,
{ color, parentNode, childNode }
) {
const points = new Float32Array([
parentNode.x,
parentNode.y,
0,
childNode.x,
childNode.y,
0
]);
const line = new MeshLine();
line.setGeometry(points);
const material = new MeshLineMaterial({
useMap: false,
color,
opacity: 1,
resolution: new THREE.Vector2(window.innerWidth, window.innerHeight),
sizeAttenuation: false,
lineWidth
});
const mesh = new THREE.Mesh(line.geometry, material);
}
``````

My `addConnection` function is similar to `addNode`, it accepts as arguments a scene to add the connection (line) to and an object with additional arguments, in this case the two mind map nodes to connect.

Like the width and height of the mind map nodes in `addNode`, I've decided to declare the line width as constant for now.

My updated `renderMindMap` function that uses this now looks like this:

``````import addConnection from './addConnection';
import colors from './colors';
import data from './data';
import initializeScene from './initializeScene';

export default async 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;

const slice = (2 * Math.PI) / level1.length;
for (let i = 0; i < level1.length; i++) {
const level1node = level1[i];
level1node.level = 1;
const angle = slice * i;
const x = root.x + radius * Math.cos(angle);
const y = root.y + radius * Math.sin(angle);
level1node.x = x;
level1node.y = y;
color: colors.magenta,
parentNode: root,
childNode: level1node
});
}
renderer.render(scene, camera);
}
``````

Here's the whole project so far on CodeSandbox:

# To Be Continued…

Stay tuned for my ongoing quest to render my perfect mind map!

Will he figure out a way to make the level 1 nodes spaced evenly?

Will he manage to add the level 2 and level 3 nodes without having them overlap?

All these questions and more may or may not be answered in the next episode! 😅