Drawing a Mind Map with Force Directed Graphs
Tags: javascript, react, threejs, mindmap
Published: 2020-12-05 16:04:15

---
title: Drawing a Mind Map with Force Directed Graphs
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/7sedkfin6v750dkzano0.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 I](https://dev.to/pahund/drawing-a-mind-map-with-three-js-and-react-nuffshell-coding-diary-part-i-1g6n), [part II](https://dev.to/pahund/drawing-a-mind-map-with-three-js-and-react-for-real-this-time-nuffshell-coding-diary-part-ii-1n9m) and [part III](https://dev.to/pahund/drawing-a-mind-map-with-three-js-and-react-for-real-this-time-nuffshell-coding-diary-part-iii-1cj1), 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 that's nested two levels deep.

# There's Got to Be a Better Way

I've been thinking about the solution I have this far. It is renders mind maps with a depth of two levels, and already it becomes clear that drawing a graph like this is not easy (it is mathematically speaking a graph). 

I ultimately want my mind map to be potentially hundreds of levels deep and have thousands of nodes. To calculate the position of the nodes so that the don't overlap is a nontrivial problem.

A Facebook friend pointed me to the [wikipedia article about graph drawing](https://en.m.wikipedia.org/wiki/Graph_drawing) (thanks, Stefan!). It discusses different layout methods. This one seems to be the most appropriate for drawing a mind map:

> In **force-based layout systems**, the graph drawing software modifies an initial vertex placement by continuously moving the vertices according to a system of forces based on physical metaphors related to systems of springs or molecular mechanics. Typically, these systems combine attractive forces between adjacent vertices with repulsive forces between all pairs of vertices, in order to seek a layout in which edge lengths are small while vertices are well-separated.

This is what it looks like (with tons and tons of nodes):

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/l7fldtriyd8dh1wjsblh.png)

<small>*Martin Grandjean • [CC BY-SA 3.0](https://commons.wikimedia.org/wiki/File:Social_Network_Analysis_Visualization.png#mw-jump-to-license)*</small>

Note quite what I'm aiming at, but I think I can make this work for me.

# Using `three-forcegraph`

Dev.to user [crimsonmed](https://dev.to/crimsonmed) pointed me to an implementation of force-directed graphs for three.js: [three-forcegraph](https://github.com/vasturiano/three-forcegraph?fbclid=IwAR3muatFcKBruzP7G0pL1-5rZ9qBiWp9DJbyeiCjkaK-IsIZpbHYTy92wew) – thanks Médéric!

To use *three-forcegraph*, I have to reformat the JSON file with my mind map data:

*[data.json](https://codesandbox.io/s/drawing-a-mind-map-with-threejs-and-react-step-10-tslfz?file=/src/data.json)*

```json
{
  "nodes": [
    { "id": "1", "name": "Interests", "val": 64 },
    { "id": "2", "name": "Music", "val": 32 },
    { "id": "3", "name": "Graphic Design", "val": 32 },
    { "id": "4", "name": "Coding", "val": 32 },
    { "id": "5", "name": "Piano", "val": 16 },
    { "id": "6", "name": "Guitar", "val": 16 },
    { "id": "7", "name": "Electronic", "val": 16 },
    { "id": "8", "name": "Procreate", "val": 16 },
    { "id": "9", "name": "Photoshop", "val": 16 },
    { "id": "10", "name": "Illustrator", "val": 16 },
    { "id": "11", "name": "Sketch", "val": 16 },
    { "id": "12", "name": "React", "val": 16 },
    { "id": "13", "name": "TypeScript", "val": 16 },
    { "id": "14", "name": "GraphQL", "val": 16 },
    { "id": "15", "name": "Firebase", "val": 16 },
    { "id": "16", "name": "Tailwind CSS", "val": 16 },
    { "id": "17", "name": "Computer Graphics", "val": 16 },
    { "id": "18", "name": "Ableton Live", "val": 8 },
    { "id": "19", "name": "Reason", "val": 8 },
    { "id": "20", "name": "Phaser", "val": 8 },
    { "id": "21", "name": "Three.js", "val": 8 }
  ],
  "links": [
    { "source": "1", "target": "2" },
    { "source": "1", "target": "3" },
    { "source": "1", "target": "4" },
    { "source": "2", "target": "5" },
    { "source": "2", "target": "6" },
    { "source": "2", "target": "7" },
    { "source": "3", "target": "8" },
    { "source": "3", "target": "9" },
    { "source": "3", "target": "10" },
    { "source": "3", "target": "11" },
    { "source": "4", "target": "12" },
    { "source": "4", "target": "13" },
    { "source": "4", "target": "14" },
    { "source": "4", "target": "15" },
    { "source": "4", "target": "16" },
    { "source": "4", "target": "17" },
    { "source": "7", "target": "18" },
    { "source": "7", "target": "19" },
    { "source": "17", "target": "20" },
    { "source": "17", "target": "21" }
  ]
}
```

While previously, the nodes were linked through a `parent` property, now the nodes and the links between them are defined as separate arrays.

My *[renderMindMap](https://codesandbox.io/s/drawing-a-mind-map-with-threejs-and-react-step-10-tslfz?file=/src/renderMindMap.js)* for now simply throws the data in a `ThreeForceGraph` object and renders it:

```javascript
function renderMindMap(div) {
  const { scene, renderer, camera } = initializeScene(div);
  const graph = new ThreeForceGraph().graphData(data);
  graph.numDimensions(2);
  scene.add(graph);
  camera.lookAt(graph.position);

  (function animate() {
    graph.tickFrame();
    renderer.render(scene, camera);
    requestAnimationFrame(animate);
  })();
}
```

Note the line `graph.numDimensions(2)` – it's important, because by default, the graph will be three-dimensional, which is not suitable for a mind map and would lead to nodes that appear to overlap.

I need to render the graph in an animation loop, because that's how the library works, it starts out drawing all the nodes in the same spot, then the force of each node drives them apart from each other.

The result looks promising already – none of the nodes are overlapping:

{% codesandbox tslfz %}

# Rendering the Mind Map Using the Force Directed Graph

Now, instead of those bubbles, I want the force directed graph to display the *[MindMapNode](https://codesandbox.io/s/drawing-a-mind-map-with-threejs-and-react-step-11-e7l4k?file=/src/MindMapNode.js)* React components I had created in my earlier attempts (see [part I of this series]([part I](https://dev.to/pahund/drawing-a-mind-map-with-three-js-and-react-nuffshell-coding-diary-part-i-1g6n))).

It took quite a bit of fiddling and trial and error, this is what I came up with:

*[renderMindMap.js](https://codesandbox.io/s/drawing-a-mind-map-with-threejs-and-react-step-11-e7l4k?file=/src/renderMindMap.js)*

```javascript
async function renderMindMap(div) {
  const { scene, renderer, camera } = initializeScene(div, data);
  data.nodes = await Promise.all(
    data.nodes.map((node) =>
      renderToSprite(<MindMapNode label={node.name} level={node.level} />, {
        width: 120,
        height: 60
      }).then((sprite) => ({ ...node, sprite }))
    )
  );
  const graph = new ThreeForceGraph().graphData(data);
  graph.nodeThreeObject(({ sprite }) => sprite);
  graph.linkMaterial(
    ({ level }) => new THREE.MeshBasicMaterial({ color: colorsByLevel[level] })
  );
  graph.linkPositionUpdate(updateLinkPosition);
  graph.numDimensions(2);
  graph.linkWidth(1);
  graph.scale.set(0.005, 0.005, 0.005);
  scene.add(graph);
  camera.lookAt(graph.position);

  let counter = 0;

  (function animate() {
    graph.tickFrame();
    renderer.render(scene, camera);
    requestAnimationFrame(animate);
  })();
}
```

Comments:

* I have to render the mind map nodes, that are created through my `MindMapNode` React component asynchronously, hence the `Promise.all` call that renders all of them in one go and stores them in the data, to be pulled out again in `graph.nodeThreeObject`
* I had to set the scale of the graph to tiny (`0.005`), otherwise my mind map nodes would have been very small because the whole graph is so big
* To fix these scale issues, I also had to adjust my *[renderToSprite](https://codesandbox.io/s/drawing-a-mind-map-with-threejs-and-react-step-11-e7l4k?file=/src/renderToSprite.js)* function so that it scales down the sprites to ⅙ size
* I had to overwrite the function that calculates the position of the links with `graph.linkPositionUpdate`, just to set the z position to make the links appear behind the nodes; by default, they were rendered in front

So here you have it, my mind map rendered as a force directed graph:

{% codesandbox e7l4k %}

# To Be Continued…

I think next time, I'll see if I can add interactivity: it would be cool if I could add new mind map nodes and remove existing ones.