Directives for reusable visualizations - D3 on AngularJS: Create Dynamic Visualizations with AngularJS (2014)

D3 on AngularJS: Create Dynamic Visualizations with AngularJS (2014)

Directives for reusable visualizations

Understanding directives

Directives give us a way to encapsulate and reuse our visualizations by allowing us to essentially create our own HTML tags. In a way, you’ve already been using directives. The HTML5 <input> tags like range and date are essentially the browsers own directives. They’re constructed from a combination of several UI element but expose a simple API. We just don’t get access to their code.

1 <input type="range" min="1" max="1000" value="0" style="width:100%">

1 <input type="date" value="1989-01-23">

Creating a directive

Now that you understand what directives are, let’s walk through creating our first with an example. Most directives do something useful but to keep it simple, we’ll create a directive that just says “hello world!”.

We’ll start off with this HTML:

1 <!DOCTYPE html>

2 <html>

3 <head>

4 <script src="angular.js"></script>

5 </head>

6 <body ng-app>

7 <!-- our app! -->

8 </body>

9 </html>

Next we need to make a new app module to hold all of our directives. All of our directives will live on this module. We only have one directive right now so it seems pointless to put it into a module but when we have many, this will help us group related directives.

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

And in our app we’ll need to change ng-app to ng-app="myApp" to tell Angular we’d like to get fancy and use our new app module instead of the default.

1 <body ng-app="myApp">

2 <!-- our app! -->

3 </body>

Now we can actually create our directive. First, we give it a name. If we want to be able to use our directive in HTML and call it <hello-world></hello-world>, we need to name it “helloWorld”. Angular will take care of converting that to the proper HTML tag name with - dashes instead of capital letters.

1 myApp.directive('helloWorld', function(){

2 // TODO: finish directive

3 });

Directives have a link function which is essentially a “constructor” if you’re familiar with Object Oriented Programming. It contains everything that should happen every time the element appears in the HTML. Angular will call our link method with a few arguments. The first is the scope (or “model”). Next is the element the directive on. The third is an object hash of all the elements properties.

The restrict: 'E' option tells angular our directive should be used using the element name, (ie. <hello-world></hello-world>). We could also indicate something is a directive using its class name by using 'C' instead of 'E'. Then when we would use our directive using the <div class="hello-world"></div> style instead.

1 myApp.directive('helloWorld', function(){

2 function link(scope, element, attr){

3 // TODO: finish directive

4 }

5 return {

6 link: link,

7 restrict: 'E'

8 }

9 });

Our directive doesn’t do anything at the moment so lets change that by adding the text “hello world!” to its contents. Because the link method gets called for every new directive instance, our code for modifying the directive will go inside of the link method.

1 function link(scope, element, attr){

2 element.text("hello world!");

3 }

The directive is finished but we haven’t used it yet in our HTML. Here we’ll add it to the <body> tag.

1 <body ng-app="myApp">

2 <hello-world></hello-world>

3 </body>

We’re all done. Now when Angular loads our app, it will check our HTML for any directives and invoke all the link functions on them. The resulting <body> DOM looks like the following:

1 <body ng-app="myApp" class="ng-scope">

2 <hello-world>hello world!</hello-world>

3 <script>...</script>

4 </body>

error

Live version: http://jsbin.com/ABUmAqiR/1/edit

A donut chart directive

Our <hello-world> directive was quaint but not very useful. Let’s expand on it by creating a donut chart directive that we can reuse.

To get us started, we’ll work from this existing D3 code that creates a donut chart.

1 <!DOCTYPE html>

2 <html>

3 <head>

4 <title></title>

5 </head>

6 <body>

7 <script src="angular.js"></script>

8 <script src="d3.js"></script>

9 <script>

10 var color = d3.scale.category10();

11 var data = [10, 20, 30];

12 var width = 300;

13 var height = 300;

14 var min = Math.min(width, height);

15 var svg = d3.select('body').append('svg');

16 var pie = d3.layout.pie().sort(null);

17 var arc = d3.svg.arc()

18 .outerRadius(min / 2 * 0.9)

19 .innerRadius(min / 2 * 0.5);

20

21 svg.attr({width: width, height: height});

22 var g = svg.append('g')

23 // center the donut chart

24 .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');

25

26 // add the <path>s for each arc slice

27 g.selectAll('path').data(pie(data))

28 .enter().append('path')

29 .style('stroke', 'white')

30 .attr('d', arc)

31 .attr('fill', function(d, i){ return color(i) });

32 </script>

33 </body>

34 </html>

error

Live version: http://jsbin.com/zinu/1/edit

Just like in the “hello world!” example above, we’ll need to setup our new directive on the app module.

1 myApp.directive('donutChart', function(){

2 function link(scope, element, attr){

3 // put D3 code here

4 }

5 return {

6 link: link,

7 restrict: 'E'

8 }

9 });

The only difference is changing the directive name from ‘helloWorld’ to ‘donutChart’ so we’ll be able to use our directive by the tag name <donut-chart>.

In the link function, we can copy and paste in all of the original pie chart code. The only change we’ll need to make is updating the d3.select('body') selection to be relative to the directive using d3.select(element[0]) instead of the entire DOM.

1 // var svg = d3.select('body').append('svg'); // old version

2 var svg = d3.select(element[0]).append('svg'); // new version

The reason we have to use element[0] instead of just element is because element “is” a jQuery wrapped selection and not an ordinary DOM object. Doing element[0] gives us just the plain old DOM element. (I say “is” in quotes because it’s technically a jqlite wrapped DOM element. jqlite is essentially a slimmed down version of jQuery.)

error

Live version: http://jsbin.com/niwe/1/edit

It might not look different but we’re well on our way to making it reusable. As a simple demo, we can easily create several donut charts just by copying and pasting the <donut-chart> tag.

1 <donut-chart></donut-chart>

2 <donut-chart></donut-chart>

3 <donut-chart></donut-chart>

error

Live version: http://jsbin.com/yili/1/edit

Isolate scope

Our donut chart could really stand to be improved in a few ways. Specifically, it would be nice if our donut charts didn’t all show the same content. We’d prefer to be able to pass each its own data to display instead of hard coding the same data right in the link function of the directive. To do this, we can use the what’s called an isolate scope. An isolate scope will allow us to pass our data into the directive using an attribute. (In our case, we’ll just call it data but we could have called it whatever we like.)

1 <donut-chart data="[8, 3, 7]"></donut-chart>

2 <donut-chart data="[2, 5, 9]"></donut-chart>

3 <donut-chart data="[6, 2, 3]"></donut-chart>

To tell Angular our directive should have an isolate scope, we just add a scope property to the object returned from our directive declaration.

1 return {

2 link: link,

3 restrict: 'E',

4 scope: { data: '=' }

5 }

Inside of our link method, we’ll just reference scope.data and it will contain the data we passed to it in the data attribute in our template above.

1 function link(scope, element, attr){

2 // our data for the current directive instance!

3 var data = scope.data;

4 // ...

5 }

error

Live version: http://jsbin.com/yili/6/edit

If, you still wanted to have the same data shared between all the pie charts, we could now simply define a variable on the scope and pass it to all the individual donut charts.

1 <body ng-app="myApp" ng-init="ourSharedData=[8, 2, 9]">

2 <donut-chart data="ourSharedData"></donut-chart>

3 <donut-chart data="ourSharedData"></donut-chart>

4 <donut-chart data="ourSharedData"></donut-chart>

error

Live vesion: http://jsbin.com/yili/7/edit

Giving our donut chart an isolate scope also gives us the ability to use Angular’s built-in ng-repeat directive to create an arbitrary number of donut charts given an array of chart data arrays.

1 <body ng-app="myApp" ng-init="charts=[[8, 3, 7],[2, 5, 9],[6, 2, 3]]">

2 <donut-chart data="chart" ng-repeat"chart in charts"></donut-chart>

error

Live version: http://jsbi