AngularJS directive for C3.js - tutorial

Posted by Peter
tags: angularjs c3 d3 javascript

Today I’d like to write a tutorial showing how to write simple AngularJS directive. We’ll be creating C3.js wrapper, which I need one for my project.

First of all, we need some assumptions:

  1. we’ll be using AngularJS
  2. we want to use all available API calls and options from C3.js

Alright, let’s get to work :)

Basically Directive is kind of a plugin, which is represented by special HTML element or attribute and evaluated by AngularJS. This makes several things possible:

  • HTML code is more human readable (seriously!) and directive is compiled into HTML
  • code is better organized - directive is associated with module
  • we can use AngularJS goodies like two-way binding for updating chart data

First, we’ll start with some basic application definition.

In AngularJS application is a module. Our Directive will be one too. Modules are something like application building blocks. You can mix and match them in order to get all functions you need.

1
var angularDemo = angular.module('angularDemo', []);

Cool. Now Controller.

Controller is a construct which controls what application does, most often you’d want to init values in it and define methods/callbacks - basically handle all what is happening.

1
2
3
4
5
var angularDemo = angular.module('angularDemo', []);

angularDemo.controller('DemoCtrl', ['$scope', function ($scope) {
// here we'll put some code
}

Lastly, some HTML boilerplate to work with, nothing fancy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html ng-app="angularDemo">
<head>
  <title>Angular C3 Simple Demo</title>
</head>
<body>

<!-- AngularJS -->
<script src="path/to/angular.min.js"></script>

<script>
  var angularDemo = angular.module('angularDemo', ['angular-c3-simple'])

  angularDemo.controller('DemoCtrl', ['$scope', function ($scope) {

  }
  ]);

</script>

</body>
</html>

Now we can start working on our directive. We’ll call it “angular-c3-simple” and put it in separate file angular-c3-simple.js. Also, it’ll be a module, which we will plug into our app in order to use it.

We have to use C3.js, which uses D3… so.. let’s make sure we have them included in our HTML file, by adding (in this order) at the bottom of the file, before ending body tag:

1
2
3
4
5
<!-- D3 -->
<script src="path/to/d3.min.js"></script>

<!-- C3.js -->
<script src="path/to/c3.min.js"></script>

Also we need to include our new file for directive, for now it’s empty:

1
<script src="path/to/angular-c3-simple.js"></script>

First, I’d like to make sure, we are using C3 in our anonymous function encapsulating the code.

Encap­su­lat­ing is good practice, read about it :)

1
2
3
4
5
;(function(c3) {
    'use strict';

  // here goes code
}(c3));

Now we have to define a module, which will be providing angular-c3-simple directive:

1
2
3
4
5
6
7
;(function(c3) {
    'use strict';

  angular.module('angular-c3-simple', []);

  // there'll be more code here
}(c3));

Good, module is set. Now we can at last write some code, which will become our awesome directive.

Directive definition has similar syntax to controller and looks like this:

1
2
3
4
5
.directive('c3Simple', function() {
  return {
    // we are returning JS object
  };
  });

Pretty simple. And about this object we’re going to return… This is where all directive magic is happening. It takes a whole bunch of options, both: attributes and methods (functions), but for this tutorial we’ll need only few of them. Keeping it simple ;)

These are options we’ll need:

  • restrict it allows our directive to be used as an (E)lement or (A)ttribute ot other, but we’ll stick with these two.

    1
    
      restrict: 'EA'
    

    I’ll be getting a little bit ahead, but this means that we will be able to use it in this two ways:

    1
    2
    
          <c3-simple id="chart" config="chart"></c3-simple>   <!-- Element -->
          <div c3-simple id="chart" config="chart"></div>   <!-- Attribute -->
    
  • scope: basically, it’s same thing as $scope you might have used in controller or view, but it is limited to our directive and set to have value of object, which is defined by config HTML attribute of our directive. '=' means that name of attribute will be the same as scopes attribute name: config. It’s same as config: '=config'

    1
    2
    3
    
      scope: {
         config: '='
      }
    
  • template: this defines HTML element, which directive code will be wrapped by.

    1
    
      template: '<div></div>'
    
  • replace: our directive will replace HTML element it is defined in.

    1
    
          replace: true
    
  • link: this is place where most, if not almost all, things are happening. You might say that it’s brain of this operation :)link is a function which takes 3 parameters: function(scope, element, attrs) {}. scope is what you think it is. element is our HTML element, we won’t be needing it today. Finally in attrs are attributes like id.

    I want to pass full C3.js config into directive and use it, that’s ease - we’ll just pass it via config attribute and assign to local scope. Now all we have to do is pass it to new chart and… how do we do that? Bare with me to he next section, right now I’ll leave link empty. Like this:

    1
    2
    3
    
      link: function(scope, element, attrs) {
      // code will go here
        }
    

    Alright, It’s time to fill in some gaps, mainly in link, so let’s start with that.

    First I’d like to bind my new chart to div we are defining directive in. Let’s require user to assign it an id="something" and retrieve it in our link function. I’ll assign it to config object, overwriting whatever was set previously: we want to make sure, that chart will be rendered in directive which invoked it, not in any other place:

    1
    
      scope.config.bindto = '#' + attrs.id; // bindto is C3.js parameter
    

    Now I’d like to make sure, that any time values for this chart will change, it will automatically update and refresh. We can achieve this by watching scope with… $scope.$watch() ;) It takes 2 parameters: scope element which we want to keep an eye on and function defined what happens if it changes. Please notice, that I’m watching specifically config.data.columns - that’s because it’s only part of config I’m interested in :)

    1
    2
    3
    
      $scope.$watch('config.data.columns', function(newSeries, oldSeries) {
        // code goes here
      });
    

    Everything left to do is render our new shiny chart.

    1
    2
    3
    
      $scope.$watch('config.data.columns', function(newSeries, oldSeries) {
        var chart = c3.generate(scope.config);
      });
    

But wait, it’s not all :) I’ also want to make sure chart is responsive, so if its dimensions weren’t specified, I’m gonna set its width to 100% of its container. Height will be set proportionally.

1
2
3
4
5
6
$scope.$watch('config.data.columns', function(newSeries, oldSeries) {
  var chart = c3.generate(scope.config);
  if (!scope.config.size) {
      chart.resize();
    }
});

Almost done. Almost. Do you remember, one of my assumptions: “we want to use all available API calls and options from C3.js”? Idea is to define a Service that will hold all chart created by us for the later use. It will return an object holding every chart and indexed by its id, I’ll call it c3SimpleService:

1
2
3
.service('c3SimpleService', function() {
  return {};
})

remember to plug it in directive like this (with funky array notation):

1
2
3
.directive('c3Simple', ['c3SimpleService', function(c3SimpleService) {
 // directive code
}]);

Now we can refactor our code, so it will register every chart with our service. It also overwrites any previous instance, so it’s nice and clean:

1
2
3
4
5
6
7
8
$scope.$watch('config.data.columns', function(newSeries, oldSeries) {

  c3SimpleService[scope.config.bindto] = c3.generate(scope.config); // var chart -> c3SimpleService[scope.config.bindto]

  if (!scope.config.size) {
      c3SimpleService[scope.config.bindto].resize();
    }
});

Now if we include this Service in our previously defined Controller…

1
2
3
4
angularDemo.controller('DemoCtrl', ['$scope', 'c3SimpleService', function ($scope, c3SimpleService) {

  }
]);

… we will be able to access any of our charts in this manner:

1
  c3SimpleService[chartId].transform('serieName', newSerieData); // as in C3.js docs

Cool, huh? :)

I won’t post here full demo application code, but you can see it in action here and full library code is available on GitHub

Have fun developing AngularJS directives and come back for more tutorials! :)