Vue3 and Force Graph

Peter
8 min readFeb 1, 2023

--

I have recently been working on a Graph View feature in acreom, which consists of visualising hierarchies of user Pages through links and folder / tag hierarchies. I have used the Force Graph library which appealed to me, through its simplicity and great capabilities, enough to write a tutorial on how to set it up for yourself.

A force directed graph.

This tutorial covers the integration of the Force Graph library with the Vue3 framework for data visualization. You will learn how to create an interactive graph visualization, fit for many use cases (1,2,3).

Installing Vue3 and setting up the project

First of all, we have to set up our project. We will be using Vue3 for its simplicity — we can get it up and running in about 2 minutes.

Prerequisites

- Node: I have used Node 16

- IDE: vscode / WebStorm / Vim (hardcore coders only)

- terminal (optional, though highly recommended): I will not cover the steps needed to set up Vue3 project without a terminal

Let’s dive right in. Open up your terminal, navigate to a folder you will be working in and execute the following command to create a Vue3 project.

npm init vue@latest

If this is your first Vue3 app, you may be prompted to install create-vue package. Just input y and press enter.

Need to install the following packages:
create-vue@3.5.0
Ok to proceed? (y) y

The setup will start automatically, and you will be prompted to select some options for your project. To read more about the options, you can see the create-vue. I chose the following settings:

Vue.js - The Progressive JavaScript Framework

✔ Project name: … *vue-force-graph*
✔ Add TypeScript? … *No* / Yes
✔ Add JSX Support? … *No* / Yes
✔ Add Vue Router for Single Page Application development? … *No* / Yes
✔ Add Pinia for state management? … *No* / Yes
✔ Add Vitest for Unit Testing? … *No* / Yes
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … *No* / Yes

Scaffolding project in /Users/peterbokor/Documents/graph_blog/vue-force-graph...

Done. Now run:

cd vue-force-graph
npm install
npm run dev

Your project will set in the vue-force-graph directory. We now need to install all the initial dependencies, as well as the Force Graph package to use it later, so while you are inside a terminal, run the following commands to navigate to the folder your app lives in and install the package:

cd vue-force-graph
npm install
npm install force-graph

We are almost set to leave the terminal for now, just run one last command to start the project up.

npm run dev

You have just started a local development server, which serves your app on the localhost:5173. If you open the site in your browser you will see the default Vue project. We are now ready to make changes to the app so open your project in an IDE.

Navigate to the components folder and create a file called Graph.vue inside the folder. The structure should look like this:

vue-force-graph
--| src
--| components
--| Graph.vue
--| pages
--| index.vue

With the Graph.vue file open, type or paste in the following piece of code:

We have just created a Graph component that uses the ForceGraph library to render a graph representation of some data we provide. In this example we provide no nodes and no links so we render an empty graph.

Finally, you should then open the src/App.vue file and replace the default components with your newly created Graph component. The file should look like this:

Now if you navigate to the index page (localhost:5173), you should see an empty screen. This is your graph, but because we did not provide any nodes, the graph is empty. Let’s fix that.

Adding data

Adding nodes to the graph is simple — all a node needs is a unique id. You can add the nodes to the graphData variable of our Graph component.

graphData: {nodes: [{id: 0}, {id: 1}], links: []},

2 nodes just landed in your graph. Nice.

Now let’s connect them using a link. To create a link, we need to specify the source node and the target node.

graphData: {nodes: [{id: 0}, {id: 1}], links: [{source: 0, target: 1}]},

Your nodes are now linked.

Since everybody needs a friend (or two), lets add 100 more nodes:

We added computed properties getNodes and getLinks. Computed property is a function that is used to calculate a value we can use — sort of like a getter. It is useful when reusing the same calculation or filtering the results as it uses caching and only recomputes when the data changes.

We have used it to produce 100 nodes. I have left the getLinks function empty, so you can try to implement your own (random) links getter.

Hint: to generate a random number from 0 to 100 in js, you can use Math.floor(Math.random()*100) .

Now that we have the basics down, lets advance to a real use case.

Making a more complex graph

I have prepared some data ready for you to use. You can find the file here: https://github.com/petttr1/vue3-force-graph/blob/main/src/constants/index.js, and copy and paste the contents to src/constants/index.js file (which you will need to create). The data is a collection of documents "borrowed" from a public obsidian vault with document links and tags extracted and ready to be used.

You can import the file adding the following line to the imports of Graph component.

import {DATA} from "@/constants";

We then alter the getNodes and getLinks methods in the following way:

getNodes() {
return DATA.map(d => ({
id: d.title,
name: d.title,
val: Math.sqrt(d.text.length),
}))
},
getLinks() {
const links = [];
DATA.forEach(d => {
if (d.links.length) {
d.links.forEach(link => {
links.push({
source: d.title,
target: link,
})
})
}
})
return links;
}

If you have a look at your graph, you can see there are now more nodes and the nodes are connected by links.

In the getNodes method we create nodes from information about our documents — we use title as the node id, and use the length of the document as the node size by setting the val parameter of the node.

In the getLinks method we iterate over the documents and if a document references other document, we create node links for every reference.

Making the graph interactive

Let’s say the linked structure is not clear enough for you. We can fix that by adding some logic to highlight the node you click on and the connected nodes, so you can follow the thought path.

To achieve that we need to do the following:

  1. Detect a node click and apply highlight — ForceGraph provides an onNodeClick method
  2. Find neighbour nodes = nodes linked to the clicked node
  3. Highlight neighbour nodes

Detect a node click and apply highlight

First, let's add the onNodeClick handler to our drawGraph method which will highlight the clicked node and all its connected nodes.

drawGraph() {
this.graph(this.$refs.graph)
.graphData({nodes: this.getNodes, links: this.getLinks})
.onNodeClick((node) => {
// highlight the clicked node
node.color = '#999999fa';
})
}

Clicking a node changes its color to grey. However, old highlights don't get reverted. To fix this, on every click we need to iterate over all nodes and reset the color to the default one. To make things easier for us, let's define our colors as constants in our data function.

data() {
return {
defaultNodeColor: '#444444fa',
highlightColor: '#999999fa',
}
},

We can not edit the onNodeClick method:

drawGraph() {
this.graph(this.$refs.graph)
.graphData({nodes: this.getNodes, links: this.getLinks})
.onNodeClick((node) => {
this.graph
.graphData()
.nodes
.forEach(n => {
// clear previous highlights
n.color = this.defaultNodeColor;
});
// highlight the clicked node
node.color = this.highlightColor;
})
}

Find neighbour nodes

We add a new method to getLinksForNode which returns all links for a selected node.

getLinksForNode(node) {
const links = [];
node.links.forEach(link => {
links.push({
source: node.title,
target: link,
})
});
return links;
}

We use this method in our getNode getter. We also simplify the getLinks getter to use the links provided by nodes.

getNodes() {
return DATA.map(d => ({
id: d.title,
name: d.title,
val: Math.cbrt(d.text.length),
links: this.getLinksForNode(d),
color: this.defaultNodeColor
}))
},
getLinks() {
const links = [];
this.getNodes.forEach(node => {
links.push(...node.links);
})
return links;
}

Nodes now hold a reference to all their neighbours, providing us an easier access on demand — when the node is clicked.

Highlight neighbour nodes

We are now ready to highlight nodes neighbouring the clicked node. We should add a little helper function to our data function, which when provided with 2 nodes decides whether the nodes are linked:

data() {
return {
defaultNodeColor: '#444444fa',
highlightColor: '#999999fa',
isLinked: (node1, node2) => {
// checks for a link between 2 nodes
return node1.links.map(l => (l.source)).includes(node2) ||
node1.links.map(l => (l.target)).includes(node2) ||
node2.links.map(l => (l.source)).includes(node1) ||
node2.links.map(l => (l.target)).includes(node1);
}
}
},

We can now alter the drawGraph method to highlight also nodes linked to the clicked node:

drawGraph() {
this.graph(this.$refs.graph)
.graphData({nodes: this.getNodes, links: this.getLinks})
.onNodeClick((node) => {
this.graph
.graphData()
.nodes
.forEach(n => {
// clear previous highlights
n.color = this.defaultNodeColor;
// if node is linked to clicked node, highlight
if (this.isLinked(n, node) {
n.color = this.highlightColor;
}
});
// highlight the clicked node
node.color = this.highlightColor;
})
}

Now each time a node is clicked, the graph goes over all the nodes, clears all previous highlights and if a node is connected to the clicked node, it is highlighted.

The final Graph component should look like this:

One problem we now have is that we can not fully cancel the highlight. I will leave the solution for this problem to you.

Hint: you can use onBackgroundClick method as described here.

Displaying text in graph

The last thing we can try is displaying node name the whole time instead of only on node hover. To do this, we will have to override the default node rendering function with our own.

drawGraph() {
this.graph(this.$refs.graph)
.graphData({nodes: this.getNodes, links: this.getLinks})
.nodeCanvasObject((node, ctx) => {
ctx.font = `12px Sans-Serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = node.color;
ctx.beginPath();
ctx.arc(node.x, node.y, node.val, 0, 2 * Math.PI, false);
ctx.fill();
ctx.fillText(
node.name,
node.x,
node.y + node.val + 3,
);
})
}

What this does, is in every render step, it iterates over all the nodes, draws them as an arc with a diameter of node.val, and coordinates of node.x, node.y. After the node is drawn, the text is also drawn using the fillText method.

Closing thoughts

That is all from me for today. Thank you for reading. You can browse the full repo with code and data example here: https://github.com/petttr1/vue3-force-graph.

I plan on publishing a follow-up blog about more advanced techniques and challenges we faced when using a similar solution in acreom, so stay tuned for part 2.

--

--