Vue.js and D3: A Chart Waiting To Happen

In this article I will show you how combining D3 and Vue can make your quest for the perfect data visualization a whole lot easier.

This article is the summary of a talk I gave at the Vue.js Antwerp meetup.

For a while now, D3.js has been the go-to JavaScript library for creating custom data visualizations. However, it’s sometimes perceived as difficult to get started with or unsuitable for small projects.

Right now, I’m working on a project called uman.ai, together with ML6, a Ghent-based company specialized in Machine Learning. Uman.ai explores new ways of gaining insight in talents and skills within organizations with the help of Artificial Intelligence. I took on the challenge to find a good interactive visualization for this model.

After making some first rough sketches, I started exploring well-known existing libraries like Chart.js and Highcharts. However, none of them turned out to be a good fit for this very specific situation. And this is where D3.js got in and I first got the idea for this talk.

D3.js

D3 had always felt kind of unfeasable for me. Most of the demo projects I saw were impressive, but they also looked pretty hard to recreate. For a long time I was convinced D3 was only suited for large and complex projects. I turned out to be wrong.

Before diving into some code, let me quickly give you an overview of what D3 exactly is. D3 is short for Data Driven Documents and calls itself “a JavaScript library for manipulating documents based on data”. D3 doesn’t include any pre-built visualizations, but provides you with a lot of useful utilities. This list of utilities might look a little intimidating at first, but we will only need a few.

D3 has a jQuery-like syntax when it comes to defining templates:

// Add a <g> element for every data point
const leaf = svg.selectAll('g').data(circles)

// Append a styled <circle> to every <g> element
leaf
  .append('circle')
  .attr('id', d => d.data.id)
  .attr('r', d => d.r)
  .attr('fill-opacity', 0.7)
  .attr('fill', d => d.data.color)

d3-snippet.js hosted with ❤ by GitHub

This might work well most of the time, but it feels a little counter-intuitive when you’re already using Vue.js in your project. With Vue.js, you’re probably used to template code that has a close connection the actual HTML result. In the next part of this article, I will show you how to replace the rendering part in the D3 workflow with Vue’s templating system we’re already using.

Let’s write some code

For the sake of simplicity, I will use the example of a flower shop here. Let’s start with Vue component with nothing more than an empty SVG element and some base data to start from.

<template>
  <svg width="500" height="500">
  </svg>
</template>

<script>
export default {
  data() {
    return {
      flowers: [
        {
          name: 'Roses',
          amount: 25,
          color: '#cc2936'
        },
        {
          name: 'Tulips',
          amount: 40,
          color: '#f2c640'
        },
        {
          name: 'Daisies',
          amount: 15,
          color: '#2a93d4'
        },
        {
          name: 'Narcissuses',
          amount: 9,
          color: '#F7AD0A'
        }
      ]
    }
  }
}
</script>

blank-component.vue hosted with ❤ by GitHub

We now need to find out the best way to:

  1. Render a circle for every type of flower
  2. Size the circles according to the amount of flowers
  3. Give each circle the right color
  4. Find the best position for each circle

This last one is the trickiest one, since we will need some kind of algorithm to calculate the most optimal positions. The algorithm we need is called Circle Packing. One of the layout utilities D3 offers is the pack layout. It takes a data set (which is called a hierarchy here) and outputs a set of packed circles. Exactly what we need.

However, for D3 to correctly parse our flower data, we have to pass it through in a specific format. Let’s use a computed property to transform our original state:

<template>
  <svg width="500" height="500">
  </svg>
</template>

<script>
export default {
  data() {
    return {
      flowers: [
        {
          name: 'Roses',
          amount: 25,
          color: '#cc2936'
        },
        {
          name: 'Tulips',
          amount: 40,
          color: '#f2c640'
        },
        {
          name: 'Daisies',
          amount: 15,
          color: '#2a93d4'
        },
        {
          name: 'Narcissuses',
          amount: 9,
          color: '#F7AD0A'
        }
      ]
    }
  },
  computed: {
    transformedFlowerData() {
      return {
        name: 'Top Level',
        children: this.flowers.map(flower => ({
          ...flower,
          parent: 'Top Level'
        }))
      }
    }
  }
}
</script>

transform-data.vue hosted with ❤ by GitHub

Right now, we have everything in place to start using some of D3’s magic. Let’s import only the parts we need and let D3 do its calculations.

<template>
  <svg width="500" height="500">
  </svg>
</template>

<script>
import { hierarchy, pack } from 'd3-hierarchy'
export default {
  data() {
    return {
      flowers: [
        {
          name: 'Roses',
          amount: 25,
          color: '#cc2936'
        },
        {
          name: 'Tulips',
          amount: 40,
          color: '#f2c640'
        },
        {
          name: 'Daisies',
          amount: 15,
          color: '#2a93d4'
        },
        {
          name: 'Narcissuses',
          amount: 9,
          color: '#F7AD0A'
        }
      ]
    }
  },
  computed: {
    transformedFlowerData() {
      return {
        name: 'Top Level',
        children: this.flowers.map(flower => ({
          ...flower,
          parent: 'Top Level'
        }))
      }
    },
    
    layoutData() {
      // Generate a D3 hierarchy
      const rootHierarchy = 
        hierarchy(this.transformedFlowerData)
        .sum(d => d.size)
        .sort((a, b) => {
          return b.value - a.value
        })
      // Pack the circles inside the viewbox
      return pack()
        .size([500, 500])
        .padding(10)(rootHierarchy)
    }
  }
}
</script>

d3-calculate.vue hosted with ❤ by GitHub

Finally, we can use the layoutData property to compose a template like we would in any other Vue component. Here we use the calculated layout values to add some labels, colors, transforms and sizes.

<template>
  <svg width="500" height="500">
    <g
      class="flower"
      v-for="flower in layoutData.children"
      :key="flower.data.name"
      :style="{
        transform: `translate(${flower.x}px, ${flower.y}px)`
      }"
    >
      <circle
        class=“flower__circle"
        :r=“flower.r"
        :fill=“flower.data.color"
      />
      <text class=“flower__label”>
        {{ flower.data.name }}
      </text>
    </g>
  </svg>
</template>

vue-template.vue hosted with ❤ by GitHub

Adding a simple CSS transition will make value changes animate smoothly:

.flower {
  transition: transform 0.1s ease-in-out;
}

.flower__circle {
  transition: r 0.1s ease-in-out;
}

transitions.css hosted with ❤ by GitHub

<template>
<div>
<svg width="500" height="500">
<g
class="flower"
v-for="flower in layoutData.children"
:key="flower.data.name"
:style="{
transform: `translate(${flower.x}px, ${flower.y}px)`
}"
>
<circle class="flower__circle" :r="flower.r" :fill="flower.data.color"></circle>
<text class="flower__label">{{ flower.data.name }}</text>
</g>
</svg>
<div class="controls">
<div class="control" v-for="flower in flowers" :key="flower.name">
<label>{{ flower.name }}</label>
<input type="number" v-model="flower.amount" step="10" min="10">
</div>
</div>
</div>
</template>

<script>
import { hierarchy, pack } from 'd3-hierarchy'
export default {
data() {
return {
flowers: [
{
name: 'Roses',
amount: 25,
color: '#cc2936'
},
{
name: 'Tulips',
amount: 40,
color: '#00a03e'
},
{
name: 'Daisies',
amount: 15,
color: '#2a93d4'
},
{
name: 'Narcissuses',
amount: 9,
color: '#F7AD0A'
}
]
}
},

computed: {
transformedFlowerData() {
return {
name: 'Top Level',
children: this.flowers.map(flower => ({
...flower,
size: flower.amount,
parent: 'Top Level'
}))
}
},

layoutData() {
// Generate a D3 hierarchy
const rootHierarchy = hierarchy(this.transformedFlowerData)
.sum(d => d.size)
.sort((a, b) => {
return b.value - a.value
})

// Pack the circles inside the viewbox
return pack()
.size([500, 500])
.padding(10)(rootHierarchy)
}
}
}
</script>

<style>
body {
font: 16px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica,
Arial, sans-serif;
}

svg {
display: block;
margin: 0 auto;
}

.flower {
transition: transform 0.2s ease-in-out;
text-anchor: middle;
}

.flower__circle {
transition: r 0.2s ease-in-out;
}

.flower__label {
fill: #fff;
font-weight: bold;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}

.controls {
display: flex;
justify-content: center;
margin-top: 20px;
}

.control {
display: inline-flex;
flex-direction: column;
margin: 0 4px;
}

.control label {
font-size: 14px;
font-weight: bold;
margin-bottom: 4px;
}

.control input {
display: block;
font: inherit;
width: 100px;
}
</style>



Conclusion

Nothing is perfect of course, and there are three caveats to this technique you should know about.

  1. Render a circle for every type of flower
  2. Size the circles according to the amount of flowers
  3. Give each circle the right color
  4. Find the best position for each circle

Luckily, this technique also has a lot of advantages:

  1. Render a circle for every type of flower
  2. Size the circles according to the amount of flowers
  3. Give each circle the right color
  4. Find the best position for each circle

I really hope next time your project needs some kind of custom out-of-the-box chart, you’ll think of this talk and give D3 a chance. The rest will be up to your imagination.

#vue-js #javascript #web-development

Vue.js and D3: A Chart Waiting To Happen
148.10 GEEK