AngularJS directive for C3.js - tutorial
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:
- we’ll be using AngularJS
- 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.
Encapsulating 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 byconfig
HTML attribute of our directive.'='
means that name of attribute will be the same as scopes attribute name:config
. It’s same asconfig: '=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 inattrs
are attributes likeid
.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 localscope
. 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 leavelink
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 ourlink
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 specificallyconfig.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! :)