Vue.js and D3: A Chart Waiting To Happen

  • 2019-04-02 01:22 AM
  • 55

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

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

Conclusion

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

  1. For big sets of data, it gets more and more difficult to keep a good performance. Especially when using a lot of animations. However, this also applies when you’re using D3 without Vue.js.
  2. For simple and well-known chart formats, it might still be easier to just use something like Chart.js or Highcharts.
  3. Some more complex features of D3, like gravity-based animations, spring values or drag and drop-interactions might be a little harder to integrate this way. However, most of them will just work fine.

Luckily, this technique also has a lot of advantages:

  1. Your code stays very close to the actual result.
  2. This makes experimenting easy and fast.
  3. Your code will blend very nicely with your existing Vue.js code.
  4. Your code will be very approachable. Even someone who only knows HTML and CSS will be able to make adjustments.
  5. Since you can also use Vue for mobile apps (with for example NativeScript or Weex) you can use D3 on virtually any platform that supports Vue.

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.

Learn More

Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)
Nuxt.js - Vue.js on Steroids
Build Web Apps with Vue JS 2 & Firebase
Vuejs 2 Authentication Tutorial
Build a Progressive Web App In VueJs
Build a CMS with Laravel and Vue
Vue CLI 3 Full-Stack App Structure
Learn Vue.js - Full Course for Beginners
Visualizations Using SVG, Canvas, and WebGL in Vue

Originally published by Simon Wuyts at https://medium.com

Suggest