Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds

Integrating a D3.js visualization into a simple AngularJS application

Save for later
  • 1140 min read
  • 2015-04-27 00:00:00

article-image

In this article by Christoph Körner, author of the book Data Visualization with D3 and AngularJS, we will apply the acquired knowledge to integrate a D3.js visualization into a simple AngularJS application.

First, we will set up an AngularJS template that serves as a boilerplate for the examples and the application. We will see a typical directory structure for an AngularJS project and initialize a controller. Similar to the previous example, the controller will generate random data that we want to display in an autoupdating chart.

Next, we will wrap D3.js in a factory and create a directive for the visualization. You will learn how to isolate the components from each other. We will create a simple AngularJS directive and write a custom compile function to create and update the chart.

(For more resources related to this topic, see here.)

Setting up an AngularJS application

To get started with this article, I assume that you feel comfortable with the main concepts of AngularJS: the application structure, controllers, directives, services, dependency injection, and scopes. I will use these concepts without introducing them in great detail, so if you do not know about one of these topics, first try an intermediate AngularJS tutorial.

Organizing the directory

To begin with, we will create a simple AngularJS boilerplate for the examples and the visualization application. We will use this boilerplate during the development of the sample application. Let's create a project root directory that contains the following files and folders:

  • bower_components/: This directory contains all third-party components
  • src/: This directory contains all source files
  • src/app.js: This file contains source of the application
  • src/app.css: CSS layout of the application
  • test/: This directory contains all test files (test/config/ contains all test configurations, test/spec/ contains all unit tests, and test/e2e/ contains all integration tests)
  • index.html: This is the starting point of the application

Installing AngularJS

In this article, we use the AngularJS version 1.3.14, but different patch versions (~1.3.0) should also work fine with the examples. Let's first install AngularJS with the Bower package manager. Therefore, we execute the following command in the root directory of the project:

bower install angular#1.3.14

Now, AngularJS is downloaded and installed to the bower_components/ directory. If you don't want to use Bower, you can also simply download the source files from the AngularJS website and put them in a libs/ directory.

Note that—if you develop large AngularJS applications—you most likely want to create a separate bower.json file and keep track of all your third-party dependencies.

Bootstrapping the index file

We can move on to the next step and code the index.html file that serves as a starting point for the application and all examples of this section. We need to include the JavaScript application files and the corresponding CSS layouts, the same for the chart component. Then, we need to initialize AngularJS by placing an ng-app attribute to the html tag; this will create the root scope of the application. Here, we will call the AngularJS application myApp, as shown in the following code:

<html ng-app="myApp">
<head>
   <!-- Include 3rd party libraries -->
   <script src="bower_components/d3/d3.js" charset="UTF-   8"></script>
   <script src="bower_components/angular/angular.js"     charset="UTF-8"></script>
 
   <!-- Include the application files -->
   <script src="src/app.js"></script>
   <link href="src/app.css" rel="stylesheet">
 
   <!-- Include the files of the chart component -->
   <script src="src/chart.js"></script>
   <link href="src/chart.css" rel="stylesheet">
 
</head>
<body>
   <!-- AngularJS example go here -->
</body>
</html>

For all the examples in this section, I will use the exact same setup as the preceding code. I will only change the body of the HTML page or the JavaScript or CSS sources of the application. I will indicate to which file the code belongs to with a comment for each code snippet.

If you are not using Bower and previously downloaded D3.js and AngularJS in a libs/ directory, refer to this directory when including the JavaScript files.

Adding a module and a controller

Next, we initialize the AngularJS module in the app.js file and create a main controller for the application. The controller should create random data (that represent some simple logs) in a fixed interval. Let's generate some random number of visitors every second and store all data points on the scope as follows:

/* src/app.js */
// Application Module
angular.module('myApp', [])
 
// Main application controller
.controller('MainCtrl', ['$scope', '$interval',
function ($scope, $interval) {
 
   var time = new Date('2014-01-01 00:00:00 +0100');
 
   // Random data point generator
   var randPoint = function() {
     var rand = Math.random;
     return { time: time.toString(), visitors: rand()*100 };
   }
 
   // We store a list of logs
   $scope.logs = [ randPoint() ];
 
   $interval(function() {
    time.setSeconds(time.getSeconds() + 1);
     $scope.logs.push(randPoint());
   }, 1000);
}]);

In the preceding example, we define an array of logs on the scope that we initialize with a random point. Every second, we will push a new random point to the logs. The points contain a number of visitors and a timestamp—starting with the date 2014-01-01 00:00:00 (timezone GMT+01) and counting up a second on each iteration. I want to keep it simple for now; therefore, we will use just a very basic example of random access log entries.

Consider to use the cleaner controller as syntax for larger AngularJS applications because it makes the scopes in HTML templates explicit! However, for compatibility reasons, I will use the standard controller and $scope notation.

Integrating D3.js into AngularJS

We bootstrapped a simple AngularJS application in the previous section. Now, the goal is to integrate a D3.js component seamlessly into an AngularJS application—in an Angular way. This means that we have to design the AngularJS application and the visualization component such that the modules are fully encapsulated and reusable. In order to do so, we will use a separation on different levels:

  • Code of different components goes into different files
  • Code of the visualization library goes into a separate module
  • Inside a module, we divide logics into controllers, services, and directives

Using this clear separation allows you to keep files and modules organized and clean. If at anytime we want to replace the D3.js backend with a canvas pixel graphic, we can just implement it without interfering with the main application. This means that we want to use a new module of the visualization component and dependency injection.

These modules enable us to have full control of the separate visualization component without touching the main application and they will make the component maintainable, reusable, and testable.

Organizing the directory

First, we add the new files for the visualization component to the project:

  • src/: This is the default directory to store all the file components for the project
  • src/chart.js: This is the JS source of the chart component
  • src/chart.css: This is the CSS layout for the chart component
  • test/test/config/: This directory contains all test configurations
  • test/spec/test/spec/chart.spec.js: This file contains the unit tests of the chart component
  • test/e2e/chart.e2e.js: This file contains the integration tests of the chart component

If you develop large AngularJS applications, this is probably not the folder structure that you are aiming for. Especially in bigger applications, you will most likely want to have components in separate folders and directives and services in separate files.

Then, we will encapsulate the visualization from the main application and create the new myChart module for it. This will make it possible to inject the visualization component or parts of it—for example just the chart directive—to the main application.

Wrapping D3.js

In this module, we will wrap D3.js—which is available via the global d3 variable—in a service; actually, we will use a factory to just return the reference to the d3 variable. This enables us to pass D3.js as a dependency inside the newly created module wherever we need it. The advantage of doing so is that the injectable d3 component—or some parts of it—can be mocked for testing easily.

Let's assume we are loading data from a remote resource and do not want to wait for the time to load the resource every time we test the component. Then, the fact that we can mock and override functions without having to modify anything within the component will become very handy. Another great advantage will be defining custom localization configurations directly in the factory. This will guarantee that we have the proper localization wherever we use D3.js in the component.

Moreover, in every component, we use the injected d3 variable in a private scope of a function and not in the global scope. This is absolutely necessary for clean and encapsulated components; we should never use any variables from global scope within an AngularJS component.

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at $15.99/month. Cancel anytime

Now, let's create a second module that stores all the visualization-specific code dependent on D3.js. Thus, we want to create an injectable factory for D3.js, as shown in the following code:

/* src/chart.js */
// Chart Module
 
angular.module('myChart', [])
 
// D3 Factory
.factory('d3', function() {
 
/* We could declare locals or other D3.js
     specific configurations here. */
 
return d3;
});

In the preceding example, we returned d3 without modifying it from the global scope. We can also define custom D3.js specific configurations here (such as locals and formatters). We can go one step further and load the complete D3.js code inside this factory so that d3 will not be available in the global scope at all. However, we don't use this approach here to keep things as simple and understandable as possible.

We need to make this module or parts of it available to the main application. In AngularJS, we can do this by injecting the myChart module into the myApp application as follows:

/* src/app.js */
 
angular.module('myApp', ['myChart']);

Usually, we will just inject the directives and services of the visualization module that we want to use in the application, not the whole module. However, for the start and to access all parts of the visualization, we will leave it like this. We can use the components of the chart module now on the AngularJS application by injecting them into the controllers, services, and directives.

The boilerplate—with a simple chart.js and chart.css file—is now ready. We can start to design the chart directive.

A chart directive

Next, we want to create a reusable and testable chart directive. The first question that comes into one's mind is where to put which functionality? Should we create a svg element as parent for the directive or a div element? Should we draw a data point as a circle in svg and use ng-repeat to replicate these points in the chart? Or should we better create and modify all data points with D3.js? I will answer all these question in the following sections.

A directive for SVG

As a general rule, we can say that different concepts should be encapsulated so that they can be replaced anytime by a new technology. Hence, we will use AngularJS with an element directive as a parent element for the visualization. We will bind the data and the options of the chart to the private scope of the directive. In the directive itself, we will create the complete chart including the parent svg container, the axis, and all data points using D3.js.

Let's first add a simple directive for the chart component:

/* src/chart.js */
…
 
// Scatter Chart Directive
.directive('myScatterChart', ["d3",
function(d3){
 
   return {
     restrict: 'E',
     scope: {
 
     },
     compile: function( element, attrs, transclude ) {
          
       // Create a SVG root element
       var svg = d3.select(element[0]).append('svg');
 
       // Return the link function
       return function(scope, element, attrs) { };
     }
   };
}]);

In the preceding example, we first inject d3 to the directive by passing it as an argument to the caller function. Then, we return a directive as an element with a private scope. Next, we define a custom compile function that returns the link function of the directive. This is important because we need to create the svg container for the visualization during the compilation of the directive. Then, during the link phase of the directive, we need to draw the visualization.

Let's try to define some of these directives and look at the generated output. We define three directives in the index.html file, as shown in the following code:

<!-- index.html -->
<div ng-controller="MainCtrl">
 
<!-- We can use the visualization directives here -->
<!-- The first chart -->
<my-scatter-chart class="chart"></my-scatter-chart>
 
<!-- A second chart -->
<my-scatter-chart class="chart"></my-scatter-chart>
 
<!-- Another chart -->
<my-scatter-chart class="chart"></my-scatter-chart>
 
</div>

If we look at the output of the html page in the developer tools, we can see that for each base element of the directive, we created a svg parent element for the visualization:

integrating-d3js-visualization-simple-angularjs-application-img-0

Output of the HTML page

In the resulting DOM tree, we can see that three svg elements are appended to the directives. We can now start to draw the chart in these directives. Let's fill these elements with some awesome charts.

Implementing a custom compile function

First, let's add a data attribute to the isolated scope of the directive. This gives us access to the dataset, which we will later pass to the directive in the HTML template. Next, we extend the compile function of the directive to create a g group container for the data points and the axis. We will also add a watcher that checks for changes of the scope data array. Every time the data changes, we call a draw() function that redraws the chart of the directive. Let's get started:

/* src/capp..js */
...
// Scatter Chart Directive
.directive('myScatterChart', ["d3",
function(d3){
 
 
   // we will soon implement this function
   var draw = function(svg, width, height, data){ … };
 
   return {
     restrict: 'E',
     scope: {
       data: '='
     },
     compile: function( element, attrs, transclude ) {
 
       // Create a SVG root element
       var svg = d3.select(element[0]).append('svg');
 
       svg.append('g').attr('class', 'data');
       svg.append('g').attr('class', 'x-axis axis');
       svg.append('g').attr('class', 'y-axis axis');
 
       // Define the dimensions for the chart
       var width = 600, height = 300;
 
       // Return the link function
       return function(scope, element, attrs) {
 
         // Watch the data attribute of the scope
         scope.$watch('data', function(newVal, oldVal, scope) {
 
           // Update the chart
           draw(svg, width, height, scope.data);
         }, true);
       };
     }
   };
}]);

Now, we implement the draw() function in the beginning of the directive.

Drawing charts

So far, the chart directive should look like the following code. We will now implement the draw() function, draw axis, and time series data. We start with setting the height and width for the svg element as follows:

/* src/chart.js */
...
 
// Scatter Chart Directive
.directive('myScatterChart', ["d3",
function(d3){
 
   function draw(svg, width, height, data) {
     svg
       .attr('width', width)
       .attr('height', height);
     // code continues here
}
 
   return {
     restrict: 'E',
     scope: {
       data: '='
     },
     compile: function( element, attrs, transclude ) { ... }
}]);

Axis, scale, range, and domain

We first need to create the scales for the data and then the axis for the chart. The implementation looks very similar to the scatter chart. We want to update the axis with the minimum and maximum values of the dataset; therefore, we also add this code to the draw() function:

/* src/chart.js --> myScatterChart --> draw() */
 
function draw(svg, width, height, data) {
...
// Define a margin
var margin = 30;
 
// Define x-scale
var xScale = d3.time.scale()
   .domain([
     d3.min(data, function(d) { return d.time; }),
     d3.max(data, function(d) { return d.time; })
   ])
   .range([margin, width-margin]);
 
// Define x-axis
var xAxis = d3.svg.axis()
   .scale(xScale)
   .orient('top')
   .tickFormat(d3.time.format('%S'));
 
// Define y-scale
var yScale = d3.time.scale()
   .domain([0, d3.max(data, function(d) { return d.visitors; })])
   .range([margin, height-margin]);
 
// Define y-axis
var yAxis = d3.svg.axis()
   .scale(yScale)
   .orient('left')
   .tickFormat(d3.format('f'));
 
// Draw x-axis
svg.select('.x-axis')
   .attr("transform", "translate(0, " + margin + ")")
   .call(xAxis);
 
// Draw y-axis
svg.select('.y-axis')
   .attr("transform", "translate(" + margin + ")")
   .call(yAxis);
}

In the preceding code, we create a timescale for the x-axis and a linear scale for the y-axis and adapt the domain of both axes to match the maximum value of the dataset (we can also use the d3.extent() function to return min and max at the same time). Then, we define the pixel range for our chart area. Next, we create two axes objects with the previously defined scales and specify the tick format of the axis. We want to display the number of seconds that have passed on the x-axis and an integer value of the number of visitors on the y-axis. In the end, we draw the axes by calling the axis generator on the axis selection.

Joining the data points

Now, we will draw the data points and the axis. We finish the draw() function with this code:

/* src/chart.js --> myScatterChart --> draw() */
function draw(svg, width, height, data) {
...
// Add new the data points
svg.select('.data')
   .selectAll('circle').data(data)
   .enter()
   .append('circle');
 
// Updated all data points
svg.select('.data')
   .selectAll('circle').data(data)
   .attr('r', 2.5)
   .attr('cx', function(d) { return xScale(d.time); })
   .attr('cy', function(d) { return yScale(d.visitors); });
}

In the preceding code, we first create circle elements for the enter join for the data points where no corresponding circle is found in the Selection. Then, we update the attributes of the center point of all circle elements of the chart.

Let's look at the generated output of the application:

integrating-d3js-visualization-simple-angularjs-application-img-1

Output of the chart directive

We notice that the axes and the whole chart scales as soon as new data points are added to the chart. In fact, this result looks very similar to the previous example with the main difference that we used a directive to draw this chart. This means that the data of the visualization that belongs to the application is stored and updated in the application itself, whereas the directive is completely decoupled from the data.

To achieve a nice output like in the previous figure, we need to add some styles to the cart.css file, as shown in the following code:

/* src/chart.css */
.axis path, .axis line {
   fill: none;
   stroke: #999;
   shape-rendering: crispEdges;
}
.tick {
   font: 10px sans-serif;
}
circle {
   fill: steelblue;
}

We need to disable the filling of the axis and enable crisp edges rendering; this will give the whole visualization a much better look.

Summary

In this article, you learned how to properly integrate a D3.js component into an AngularJS application—the Angular way. All files, modules, and components should be maintainable, testable, and reusable.

You learned how to set up an AngularJS application and how to structure the folder structure for the visualization component. We put different responsibilities in different files and modules. Every piece that we can separate from the main application can be reused in another application; the goal is to use as much modularization as possible.

As a next step, we created the visualization directive by implementing a custom compile function. This gives us access to the first compilation of the element—where we can append the svg element as a parent for the visualization—and other container elements.

Resources for Article:


Further resources on this subject: