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.

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. The chart.spec.js and chart.e2e.js files will be discussed in the last section of this tutorial.

 

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>