Getting data into and out of directives - D3 on AngularJS: Create Dynamic Visualizations with AngularJS (2014)

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

Getting data into and out of directives

The combination of Angular and D3 makes it easy to create visualizations that update automatically whenever their data changes, but how do we get our data into Angular in the first place?

The D3 way

As we’ve seen, D3 comes with a set of useful utilities for loading data from remote sources. The most common being the d3.json() or d3.csv()` functions. If used properly, we can continue to use these same functions but Angular also offers its own alternatives for loading in data.

Let’s start off which a simple example of a donut chart directive that loads data in from a remote file source.

1 d3.json('donut-data.json', function(err, data){

2 if(err){ throw err; }

3 arcs = arcs.data(pie(data));

4 arcs.exit().remove();

5 arcs.enter().append('path')

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

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

8 // update all the arcs (not just the ones that might have been added)

9 arcs.attr('d', arc);

10 });

error

Live version: http://bl.ocks.org/vicapow/9535255

This gets the job done for a single instance of our donut chart, but if we wanted to use our donut chart with different data? We would either need to edit our existing donut chart directive by changing the url it was fetching data from or create a new directive, almost exactly like the original that loaded data in from the scope instead of putting d3.json() in our directive. A better alternative would be for our directive to care nothing about where it gets its data. That is, we would prefer our directive “know” nothing about where it got its data and only care about if or when it gets data and when that data changes.

To allow the above directive to confirm to this new heuristic, we should only have it watch for changes to the data on the scope, and then we’ll load our data onto the scope from somewhere else. For now, the “someplace else” will be a controller. The controller is where the logic should go that ties together our application.

The first step to do doing this is defining our controller. In our case, we’ll call it MainCtrl because we only have one.

1 var myApp = myApp.controller('MainCtrl', function($scope){

2 // TODO: controller logic here!

3 });

This creates a controller class that we can then use to create a specific controller in our application using ng-controller.

1 <body ng-app="myApp" ng-controller="MainCtrl">

2 <!-- MainCtrl's scope -->

3 </body>

Next, we’ll just put our d3.json() code within the controller definition.

1 var myApp = myApp.controller('MainCtrl', function($scope){

2 d3.json('donut-data.json', function(err, data){

3 if(err){ throw err; }

4 $scope.data = data;

5 $scope.$apply();

6 });

7 });

So that when the data is pulled in, we’ll put it on the scope. Angular will then notice our data scope property changed and let our <donut-chart> directive know about the change. The scope is checked for changes when call $scope.$apply().

Lastly, we need to wire up the directive to our MainCtrl’s scope and change back our directive to have a data scope property that watches for changes and updates the visualization accordingly.

1 <donut-chart data="data"></donut-chart>

1 scope.$watch('data', function(data){

2 // (update/add/remove the `<path>` elements for each of the pie arcs)

3 }, true);

And just like before, because we want data to be a scope property configured as an attribute on our directive in HTML, we’ll need to set scope: to { data: '=' } in the returned configuration object in our directive.

error

Live version: http://bl.ocks.org/vicapow/9536234

And now we can still use our <donut-chart> directive any time of data. In this example, we’re both loading the donut chart data using d3.json() for the first chart and hard coding our data on the MainCtrl’s scope for the second.

error

Live version: http://bl.ocks.org/vicapow/raw/9554848

The Angular way

In addition to using d3.json(), we can also use angular’s $http service to pull in data.

1 myApp.controller('MainCtrl', function($scope, $http){

2 // using success/error callback style

3 $http.get('donut-data.json').success(function(data){

4 $scope.donutData1 = data;

5 }).error(function(err){

6 throw err;

7 });

We can also use the alternative then() callback style:

1 myApp.controller('MainCtrl', function($scope, $http){

2 // using `then()` callback style

3 $http.get('donut-data-2.json').then(function(response){

4 $scope.donutData2 = response.data;

5 }, function(err){

6 throw err;

7 });

8 });

error

Live version: http://bl.ocks.org/vicapow/9555539

Using $http service instead of d3.json() allows us to avoid having to call $scope.$apply() directly. Angular will do this for us automatically. In fact, Angular almost always knows when to check the scope. The only times it doesn’t are when performing asynchronous events outside of Angular. This includes using any of D3’s behaviors (ie., drag, click, zoom, etc.) or using setTimeout (instead of Angular’s $timeout service.)

information

You may be curios as to how we’re able to use the $http service simply by adding it as another argument to the controller definition function. This is made possible by a feature in Angular called “dependency injection”. A detailed overview of DI and how it works is beyond the scope of this book. Have a look at the Angular documentation here for more details. In the mean, just know that Angular is smart enough to check what arguments it should call your controllers initialization method with by looking at the variable names of the arguments. If we switched the arguments to our controllers initialize function from ($scope, $http) to ($http, $scope)` our code would keep working!

Using all we’ve learned so far, we can now create a simple “real time visualization dashboard” just by using the $interval service to periodically pull data from an API, taking its result and putting it on the scope. Our directive will take care of the rest.

1 myApp.controller('MainCtrl', function($scope, $http, $interval){

2 $interval(function(){

3 $http.get('donut-data-api.json').then(function(response){

4 // your API would presumably send new data each time!

5 var data = response.data.map(function(d){ return d * Math.random() });

6 $scope.donutData = data;

7 }, function(err){

8 throw err;

9 });

10 }, 1000);

11 });

error

Live version: http://bl.ocks.org/vicapow/9556184

Updating the scope from within a directive

So far, the communication of our directives with the outside world has been one sided. Our directives have always been responding to data changes, never the other way around. There are times, however, when we’d like to effect our data from within our directive.

It can often be a very modular technique to update data on our directives scope and then optionally listen for changes to that variable in some other place in our application or even in other directives. This makes our directives more reusable because they no longer need to know anything about each other. We can drop a directive into another part of our application or another application entirely without much work in wiring it up since we’ll only have to use a $watch statement on the scope variable we passed to the directive in its HTML definition to be notified of change to the variable.

As a concrete example, imagine we created a scatter plot directive and wanted to add the ability to see the details of an individual point whenever the mouse is placed over the top of this point. There are a few ways to achieve this, some more modular than others. One way is to create a specific directive, something such as a “scatter-plot-detail” directive. Which is fine until we need another type of scatter plot that does something slightly different. Possibly displaying the details in a slightly different way than the first. If we were to stick with our first strategy, we would now need a completely new directive, almost identical to the first expect in the way it displayed its detail content.

A better approach would be to have a single directive that displays a scatter plot that is not responsible for displaying any details about the selected point. All it has is a new scope variable named selectedPoint. Then, when a point is moused over, it will set the selectedPoint scope property to the data of the newly moused over point.

1 // our `<scatter-plot>` directives new isolate scope

2 scope: { selectedPoint: '=', data: '=' }

1 point.append('circle').attr('r', 5)

2 .on('mouseover', function(d){

3 scope.$apply(function(){

4 scope.selectedPoint = d;

5 });

6 });

To finish up, all we need to do is configure our directive in the HTML and add our description template using Angular’s {{ }} syntax.

1 <body ng-app="myApp" ng-controller="MainCtrl">

2 <div class="scatter-container">

3 <scatter data="employers" selected-point="selectedEmployer"></scatter>

4 </div>

5 <div class="detail">

6 <h2>{{selectedEmployer.name }}</h2>

7 <!-- and possibly some other details about the employer -->

8 </div>

9 </body>

error

Live version