loading...
Cover image for Binding Data to Charts using Vue Components and D3

Binding Data to Charts using Vue Components and D3

ignoreintuition profile image Brian Greig Updated on ・5 min read

There is nothing more satisfying than making a change and seeing that change propagated through somewhere immediately. Lately, I have been experimenting a lot with integrating D3 charts into Vue. I tried doing this with helpers, directives and, most recently, with components. Since Vue is declaring components to be their recommended implementation method for reusable components I am going to be focusing here on that version.

If you are unfamiliar with Vue it is a popular JavaScript framework for two-way data binding. D3 is a library for modeling data in the DOM. What we are going to be focusing on is building a reusable component in Vue that leverages the D3 library to create a chart stays in sync with the data in Vue.

We are going to start with the vue-cli which will allow us to create a template for our new app. Once we have our starter app we are going to create a JavaScript file within our src directory called v-chart-plugin.js. This will need to be imported and used in our main.js

import Chart from './v-chart-plugin.js'

Vue.use(Chart);

The idea here is that all of the functionality for rendering the chart and binding the data will be handled within the component completely seperate from the Vue instance. This way when the component is added all the developer needs to do is add in the element with the proper data bindings.

<v-chart v-bind:chartData="chartData"></v-chart>

As long as the object (in this case chartData) being passed follows the appropriate structure with the required values there is nothing further required of the dev. The component itself will be structured as so:

  • Import dependent libraries (D3)
  • Define the component
  • Export the component
  • Use the component (if vue is defined at the global scope)

The framework would look something like this

// Limited to just those dependencies we are going to need for the example
var d3 = Object.assign({}, 
    require("d3-selection"));

const Chart = {
...
}

export default Chart;

if (typeof window !== 'undefined' && window.Vue) {
    window.Vue.use(MyPlugin)
}

The bulk of the plugin is going to be in the definition of the Chart object itself. If you are familiar with Vue then you should recognize a lot of these methods and properties. If you are familiar with other frameworks like React and Angular some of the terminology should be familiar as well. Let's examine the structure of the component before we get into the details

const Chart = {
    install(Vue, options) {
        Vue.component('v-chart', {
            props: ['chartData'], 
            data: function () {
                ...
            },
            methods: {
                ...
            },
            // lifecycle events
            mounted: function () { 
                ...
            },
            // watch functions
            watch: {
                ...

            },
            template:
                ...
        })
    }
}

You should recognize the value v-chart. This is the value that was used in our Vue template to add the component to the application. Below that we are referencing the props. This correlates to the object that we bound via the v-bind directive. You can pass multiple parameters instead of an object but I'll be using an object as it is easier for configuration. Following the props are the familiar data and methods which are values and functions that are scoped to the component. We won't be using data in this example as all the data is coming from the props but we will be using methods extensively. Note that for components data needs to be a function.

Vue.component('v-chart', {
    props: ['chartData'], 
    data: function () {
        return {
        }
    },
    methods: {
        initalizeChart: function () {
            this.drawChart();
        },
        refreshChart: function () {
            this.clearCanvas();
            this.drawChart();
        },
        drawChart: function () {
            ...
        },
        clearCanvas: function () {
            d3.select(this.chartData.selector).selectAll("*").remove();
        },
        getHeight: function () {
            return this.chartData.height || 200;
        },
        getWidth: function () {
            return this.chartData.width || 200;
        }
    },

Outside of our methods we have a few additional functions that serve some specific purposes. Firstly are the lifecyle hooks. These are functions that get called at specific intervals within the application. This allows us to associate functionality with these events such as when an instance is created, updated, destroyed, etc. We'll be using the mounted hook to ensure the page is loaded when we render our chart.

Lifecycle Hooks

mounted: function () { // <-- lifecycle events
    this.initalizeChart();
},

The other special methods we will be adding are watch functions. Watch functions serve the unique purpose of executing when the data is updated. This will allow us to re-render the chart when the data changes. Doing so will ensure that the chart is always in sync with the data.

watch: { // <-- watch functions
    'chartData': {
        handler: function (val) {
            this.refreshChart();
        },
        deep: true
    }
},

Lastly, we have the template. This is a standard Vue template for the content we intend on rendering on the page. In this example, it is simply an SVG as that is what we will be using to draw our graph. We use interpolation to get the values of the width and height based on what was configured in the object.

template:
    `<svg class="chart" :height="this.getHeight()" :width="this.getWidth()"> </svg>`

If you have been following along you will notice I purposefully left out the details of the drawChart function. This is the part of the code that uses D3 to draw the chart on the SVG canvas we created in our template. This is going to rely heavily on the data we are passing in from the props: specifically the chartData.selector for identifying the unique id for the chart and chartData.data which is an array of data configured in the Vue instance. The rest is a boilerplate D3 that binds the data and appends rectangles with a length equal to each value in the array. This can be extended to create any data visualization.

drawChart: function () {
    d3.select(this.chartData.selector)
        .append("text")
        .attr("x", 20)
        .attr("y", 20)
        .style("text-anchor", "left")
        .text(this.chartData.title)

    d3.select(this.chartData.selector)
        .selectAll("g")
        .data(this.chartData.data)
        .enter().append("g")
        .append("rect")
        .attr("width", function (d) {
            return d;
        }).attr("height", 20)
        .attr("y", function (d, i) {
            return (i + 1.5) * 20 + i
        }).attr("x", 0);
},

screenshot

If you want to see the full code go ahead and clone the Github Repo that this tutorial was based on. Hopefully, this gives you a foundation on how to bind your data from Vue with your components and model it in visualizations.

Posted on by:

ignoreintuition profile

Brian Greig

@ignoreintuition

Front-end developer specializing in integration, data collection, data visualization, and data analytics. Family man and board game geek.

Discussion

markdown guide
 

I make charts with d3 in Vue almost every day and here is some of my best practices:

  • Avoid to import whole d3 - it has a very heavy geo module and many unnecessary modules for most cases. Use the following syntax:
const d3 = {
  ...require('d3-scale'),
  ...require('d3-shape'),
};
  • Use jsx, not d3 selections - it's much simpler when you learn it. You also don't have to watch the data updates and write renderLine and updateLine methods, you just write the render function and deal with reactive data and shadow DOM

I would write an article about d3, Vue and jsx, but I have a little experience in writing and bad english

 

Definitely agree on the recommendation to pull in only those dependent modules.

Regarding JSX I assume that is easier / harder based on your familiarity with the syntax.

 

You haven't to use JSX, you can use vue templates, but JSX is a little more flexible. The idea is to use

<path
  :d="lineShape(data)"
  stroke="red"
/>

<!-- or d={this.lineShape(this.data)} in jsx -->

Instead of

d3.select(this.$el).append('path')
  .attr('d', this.lineShape(this.data)
  .attr('stroke', 'red')
;

I think html/svg is more readable

 

Oh and of course some features are simpler with d3 selections - animations and transition for example

 

Per your suggestion I updated the example to just pull the dependent d3 files.

 

This is really cool! Glad to see more data visualization options coming into vue!

 

Ideally I think this is something that needs to be build up by the community. If folks want to use this component as a framework for building that out I am happy to coordinate it.