Making a Skill-Tree With D3 and Vue
By: Ze Xuan
Tags: Vuejs, Programming, SVG, Skills, D3js

Making a Skill-Tree With D3 and Vue

What I got out of it at the end

Motivation

I have a portfolio website that I use basically as a dynamic resume, to showcase my technical abilities and accomplishments. I built it using Gridsome which is written in Vue, and using Buefy as the main component library simply because I just wanted to try out Vue and whatever frameworks are out there. One of the sections in the page is a ‘Key Skills’ section where I list the technical skills that I feel I’m most comfortable with and where most companies would look out for.

My current portfolio website

The current design works alright, but I feel that it’s a bit lackluster as listing the skills in rows of text doesn’t have much ‘oomph’. Thus, I began to sought out a way to display these skills in a more fancier manner. I thought of the skill-trees that are present in role-playing games (RPG), where the skills and abilities of a character are displayed in a hierarchical manner. I then imagine if it were possible to replicate it and use it to showcase my skills instead. Kind of something like this:

This ‘borderlands-style’ skill tree example uses a library called beautiful-skill-tree and is made for React. Since this won’t be compatible with my Vue website, I needed an alternative, but unfortunately there aren’t any good alternatives out there.

Which is actually awesome, since it’s becoming increasingly rare to not be able to find something you need on the web that’s already built and ready to be used. Thus, I set off to build my own skill tree 💪

Technology

Firstly, I thought of what would be the best way to visualize a dataset in a modern website. The first thing that came to mind would be D3, a JavaScript library that is used for organizing, processing and formatting data to be easily visualized later. I decided to use D3 since this is a perfect opportunity to learn how to use it. To start off my learning of D3, I went through these material:

Observable has a pretty in-depth and well written tutorial/guide on how to use D3, but I found it hard to get it setup and working with Vue, mostly due to the reactivity system of Vue.

I then started to look for Vue + D3 related guides, and luckily found this super useful tutorial. This website explains how to make D3 play well with the reactivity system and it’s actually pretty simple, using Vue’s concept of watchers .

After playing around with D3, I’ve learnt that D3 doesn’t really provide the visualization part for you i.e. the bar charts, tree diagrams, etc. Most of the visualization part is built manually with svg components… which I’m also pretty weak at. 😅 So, it’s time to learn svg as well! After much fiddling, I finally got the ‘scaffolding’ done:

Basic frame of the skill-tree

Code

I’ll explain a bit of how the code works:

// skills data

const skills = [

{ id: "tech", label: 'Tech Tree' },

{ parentId: "tech", id: "languages", label: 'Languages',},

{

parentId: "languages",

id: "python",

label: 'Python',

iconHref:

require('../assets/icons/python.png'),

skillLevel: 90,

descriptions: ['Mainly worked with webserver backend frameworks with it.', 'Used in some simple machine learning applications and projects such as Gestice League and Project 21.']

},

...]

The data to display has to have a certain structure, specifically a parent-child based one. Each data point (skill) is either the root, or the child of a another data point. To construct this, I have a list of objects that have their own id and optionally a parentId to indicate it’s parent if it has one.

Organising the data

...

let hierarchy = d3.stratify()(this.skills);

const treeLayout = d3.tree().size([

1280,

500,

]);

hierarchy = treeLayout(hierarchy);

Next, I utilize D3’s powerful data processing tools. Firstly, using d3.stratify() helps to process the data points and formalize the link structure of the hierarchy. Specifically, it helps to identify the root and it’s children, while also calculating metadata and structures like the depth and height of the resulting graph. It will also throw an error if it detects multiple roots or other structural issues. The resulting output is what D3 terms as a ‘hierarchy’ object. At this stage, the resulting hierarchy does not have a specific layout, meaning that the data points are not ‘positioned’ relative to each other spatially yet.

I then use d3.tree() to process the hierarchy into a top-down tree layout. This helps to assign co-ordinates to the data points, which we can then use to draw out the whole tree visually.

Drawing the tree

Firstly, in the template I have a simple structure setup like this:

<template>

<div>

<svg />

</div>

</template>

and in the mounted method of the Vue component:

...

mounted() {

this.svg = d3

.select("svg")

.attr("width", "100%")

.attr("height", 500)

.attr("cursor", "grab")

.attr("position", "relative"); this.skillTree = this.svg.append("g"); this.drawTree();

},

Over here, we see the first instance of using the D3 library to handle DOM manipulation. By using d3.select("svg") , we’re basically trying to find the first <svg /> object in the document and then manipulating it afterwards.

The first thing we do to the <svg /> is to append a <g /> element to it, which is just a native svg element to act as a container for other components. Doing this.svg.append("g") will also return the resulting <g /> element, which I then assign it as the skillTree object to be built upon later.

Next, we draw out the tree in the drawTree() method, and this is how it works:

// hierarchy object previously made const links = hierarchy.links();

const nodes = hierarchy.descendants(); ...

this.skillTree

.selectAll("rect")

.data(nodes)

.enter()

.append("rect")

.attr("width", (d) => rectWidth)

.attr("x", (d) => d.x)

.attr("y", (d) => d.y)

... //etc.

...

Firstly, we can access the link and node objects via the hierarchy.links() and hierarchy.descendants() respectively.

Next, we see the one of the most powerful features of the D3 library. D3 has a special data enter-update-exit concept that’s explained really well here. I’m not fully familiar with it yet, but basically by doing .data(nodes) you’re informing the dataset that you’re referencing next and by doing .enter() after, the list of elements that needs to be updated will be returned. So, for the initial call, .enter() will basically return the entire list of nodes since previously, there is no data present in the hierarchy.

After getting the list of elements that need to be updated, we then append the svg elements that we want to build for each of the nodes. So in this case, for each of the nodes, I created a square svg element and then assigned it’s position according to the x, y coordinates conveniently derived by the D3 library.

That’s in essence how D3 is used to help structure and support the visualization of whatever data you want to present. The next steps I did are to construct lines for the links, and beautify the nodes, by adding text, images and borders for each of the nodes.

Making the skill-tree more interactive

I realised that my skill-tree has quite a bit of data to show, and there isn’t quite enough space to fit all of it in. Instead of shrinking the data to fit accordingly, I thought why not try to make the user able to pan it instead. I found this very good guide on how to do that exactly:

The whole idea is to utilize the viewBox attribute of the <svg /> component, which basically controls the region of the svg to display. By varying the viewBox params, we can simulate a moving ‘camera’ or like the ‘viewport’ in a game, shifting the region of the whole tree to display.

The next steps is to calculate how much to vary the viewBox by. In order to do this, we need to do a sequence of steps:

1. Record the origin/starting cursor coordinates when user clicks on the svg

2. Track and vary the viewbox by the offset of the new cursor coordinates and the original cursor coordinates

3. Save the last viewbox offset, and factor it in subsequent origin calculations

When done right, we’ll basically have a pan-able skill tree!

Oh look, it moves!

Let me explain the code of it:

...

this.svg.on("pointerdown", this.onPointerDown);

this.svg.on("pointerup", this.onPointerUp);

this.svg.on("pointerleave", this.onPointerUp);

this.svg.on("pointermove", this.onPointerMove);

...

We want to track the different cursor/touch inputs on the whole <svg /> component. To do this, we just need to assign the different callbacks to the different events. Notice how we treat the pointer leaving the <svg /> similarly as the cursor being ‘un-depressed’.

onPointerDown: function () {

this.isClicked = true;

const x = d3.event.pageX;

const y = d3.event.pageY;

this.viewBoxPointer= {

x: x + this.viewBoxOffset.x,

y: y + this.viewBoxOffset.y,

};

},

onPointerUp: function () {

this.isClicked = false;

const x = d3.event.pageX;

const y = d3.event.pageY;

},

onPointerMove: function () {

if (this.isClicked) {

const x = d3.event.pageX;

const y = d3.event.pageY;

this.viewBoxOffset = {

x: this.viewBoxPointer.x - x,

y: this.viewBoxPointer.y - y,

};

this.svg.attr("viewBox", `${x} ${y} ${width} ${height}`);

}

},

...

Next, I’ll briefly explain how the panning works. Firstly, in the onPointerDown method, we want to track when the user is still ‘clicking’ on the <svg /> , and we do this via the this.isClicked flag. We also then track the current position where the cursor is in this.viewBoxPointer . Ignore the viewBoxOffset for now.

After that is done, we observe for when the user starts to move the cursor around the <svg /> . This is done in the onPointerMove method. We first check if the user is actually still clicking on the <svg /> , which implies that the user is dragging around the component. We then track the new cursor coordinates, and then calculate the offset with the original position, which is stored in this.viewBoxPointer . We also store the current offset in viewBoxOffset . Doing so will simulate the skill tree being ‘moved’ around.

Finally, when the user stops dragging and stop clicking, we want to simply unset the this.isClicked flag.

When the user wants to pan the skill tree again, we then factor in the offset in the calculation of the viewBoxPointer in the onPointerDown method, so that we start from the correct last known position.

Final details

The last part of the whole skill tree is to display a ‘description’ drawer on the side of it when the user clicks on one of the nodes. This is done with putting a Buefy card component (via <div class="card" ... ) and wrapping it with a Buefy <b-collapse> to show/hide it when the nodes are clicked. The selected node data is passed in, and the details are then displayed nicely, with a nice little ‘experience bar’ built using Buefy’s <b-progress> .

So after all that, here’s what we end up with:

So ‘skill’-ful!

Conclusion

I’m pretty satisfied with the end result, and I accomplished what I set out to achieve to, having learnt how to use D3 and svg stuff. There’s more I want to explore, namely how to better support mobile touch support for the svg, and maybe adding a zoom function for it. Maybe after I clean up the code a bit, I can publish it as a package for others to use🤔. But overall, this was a fun little project! You can see it on my main website if you’d like: 😃https://zexuan.netlify.app/