Drawing a Mind Map with Three.js and React, for Real This Time
Tags: javascript, react, threejs, mindmap
Published: 2020-12-03 07:23:41

---
title: Drawing a Mind Map with Three.js and React, for Real This Time
published: true
description: I'm building a social media network and collaboration tool based on mind maps, documenting my work in this series of blog posts
tags: javascript,react,threejs,mindmap
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/6zy4x8ze4qir1zx84k2v.png
series: Nuffshell Coding Diary
---

*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](https://dev.to/pahund/drawing-a-mind-map-with-three-js-and-react-nuffshell-coding-diary-part-i-1g6n), 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…

![“Winter is Coming” meme: Math is coming!](https://dev-to-uploads.s3.amazonaws.com/i/f2b9qh89txp5dlhd8ajo.png)

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*

```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;

  await addMindMapNode(scene, root);
  const radius = 2;
  for (const level1node of level1) {
    level1node.level = 1;
    // TODO:
    //level1node.x = ?;
    //level1node.y = ?;
    await addMindMapNode(scene, level1node);
  }
  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:

![Calculating level 1 node positions](https://dev-to-uploads.s3.amazonaws.com/i/kzrzrp2rw8ih9j22v00p.png)

Where else could I find the answer that on trusty old StackOverflow?

{% stackoverflow 5301049 %}

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

```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;

  await addMindMapNode(scene, root);
  const radius = 2;
  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);
    await addMindMapNode(scene, level1node);
  }
  renderer.render(scene, camera);
}
```

This works! 

{% codesandbox qi17x %}

# 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:

![Level 1 nodes](https://dev-to-uploads.s3.amazonaws.com/i/xstxai7ioq6yg575qblw.png)

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).

![7 child nodes](https://dev-to-uploads.s3.amazonaws.com/i/xpylyrtaw7wid86la598.png)

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](https://stackoverflow.com/q/65108460/1253156) and [StackExchange Mathematics](https://math.stackexchange.com/q/3932582/752140), 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](https://mattdesl.svbtle.com/drawing-lines-is-hard).

When I naïvely used `THREE.LineBasicMaterial` and `THREE.Line`, as explained in the [three.js documentation](https://threejs.org/docs/index.html#manual/en/introduction/Drawing-lines), 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](https://github.com/spite/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.

*addConnection.js*

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

const lineWidth = 5;

export default async function addConnection(
  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);
  scene.add(mesh);
}
```

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:

```javascript
import addConnection from './addConnection';
import addMindMapNode from './addMindMapNode';
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;

  await addMindMapNode(scene, root);
  const radius = 2;
  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;
    await addMindMapNode(scene, level1node);
    addConnection(scene, {
      color: colors.magenta,
      parentNode: root,
      childNode: level1node
    });
  }
  renderer.render(scene, camera);
}
```

Here's the whole project so far on CodeSandbox:

{% codesandbox ps62w %}

# 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! 😅