Creating Charting Directives Using AngularJS and D3.js

Ravi Kiran
Tweet

D3 is a JavaScript library that can be used to create interactive charts with the HTML5 technology Scalable Vector Graphics (SVG). Working directly with SVG to create charts can be painful, as one needs to remember the shapes supported by SVG and make several calls to the API to make the chart dynamic. D3 abstracts most of the pain, and provides a simple interface to build SVG-based charts. Jay Raj published two nice SitePoint articles on working with D3, check them out if you are not already familiar with D3.

Most of you may not need a formal introduction to AngularJS. AngularJS is a client side JavaScript framework for building rich web applications. One of the top selling points of AngularJS is the support for directives. Directives provide an excellent way to define our own HTML properties and elements. It also helps in keeping the markup and code separated from each other.

AngularJS is very strong in data binding as well. This feature saves a lot of time and effort required to update the UI according to data in the model. In the modern web world, customers ask developers to build websites that respond in real-time. This means the customers want to always see the latest data on the screen. Data UI has to be updated as soon as someone modifies a piece of data in the back-end. Performing such real-time updates would be very difficult and inefficient if we don’t have support of data binding.

In this article, we will see how to build real-time AngularJS directives that wrap D3 charts.

Setting Up

First, we need to set up the environment. We need AngularJS and D3 included in the HTML page. As we will build just a chart directive, we need to create an AngularJS controller and a directive. In the controller, we need a collection holding data to be plotted in the chart. The following snippet shows the initial controller and directive. We will add more code to these components later.

var app = angular.module("chartApp", []);

app.controller("SalesController", ["$scope", function($scope) {
  $scope.salesData = [
    {hour: 1,sales: 54},
    {hour: 2,sales: 66},
    {hour: 3,sales: 77},
    {hour: 4,sales: 70},
    {hour: 5,sales: 60},
    {hour: 6,sales: 63},
    {hour: 7,sales: 55},
    {hour: 8,sales: 47},
    {hour: 9,sales: 55},
    {hour: 10,sales: 30}
  ];
}]);

app.directive("linearChart", function($window) {
  return{
    restrict: "EA",
    template: "<svg width='850' height='200'></svg>",
    link: function(scope, elem, attrs){
    }
  };
});

We will fill the link function in the above directive to use the data stored in the controller and plot a line chart using D3. The template of the directive contains an svg element. We will apply D3’s API on this element to get the chart plotted. The following snippet shows an example usage of the directive:

<div linear-chart chart-data="salesData"></div>

Now, let’s gather the basic data needed for plotting the chart. It includes the data to be plotted, JavaScript object of the SVG element, and other static data.

var salesDataToPlot=scope[attrs.chartData];
var padding = 20;
var pathClass = "path";
var xScale, yScale, xAxisGen, yAxisGen, lineFun;
    
var d3 = $window.d3;
var rawSvg = elem.find("svg")[0];
var svg = d3.select(rawSvg);

Once the library for d3 is loaded, the d3 object is available as a global variable. But, if we use it directly inside a code block, it is hard to test that block of code. To make the directive testable, I am using the object through $window.

Drawing a Simple Line Chart

Let’s set up the parameters needed to draw the chart. The chart needs an x-axis, a y-axis, and the domain of data to be represented by these axes. In this example, the x-axis denotes time in hours. We can take the first and last values in the array. On the y-axis, the possible values are from zero to the maximum value of sales. The maximum sales value can be found using d3.max(). The range of the axes vary according to the height and width of the svg element.

Using the above values, we need to ask d3 to draw the axes with the desired orientation and the number of ticks. Finally, we need to use d3.svg.line() to define a function that draws the line according to the scales we defined above. All of the above components have to be appended to the svg element in the directive template. We can apply the styles and transforms to the chart while appending the items. The following code sets up the parameters and appends to the SVG:

function setChartParameters(){
  xScale = d3.scale.linear()
             .domain([salesDataToPlot[0].hour, salesDataToPlot[salesDataToPlot.length - 1].hour])
             .range([padding + 5, rawSvg.clientWidth - padding]);

              yScale = d3.scale.linear()
                .domain([0, d3.max(salesDataToPlot, function (d) {
                  return d.sales;
                })])
             .range([rawSvg.clientHeight - padding, 0]);

  xAxisGen = d3.svg.axis()
               .scale(xScale)
               .orient("bottom")
               .ticks(salesDataToPlot.length - 1);

  yAxisGen = d3.svg.axis()
               .scale(yScale)
               .orient("left")
               .ticks(5);

  lineFun = d3.svg.line()
              .x(function (d) {
                return xScale(d.hour);
              })
              .y(function (d) {
                return yScale(d.sales);
              })
              .interpolate("basis");
}
         
function drawLineChart() {

  setChartParameters();

  svg.append("svg:g")
     .attr("class", "x axis")
     .attr("transform", "translate(0,180)")
     .call(xAxisGen);

   svg.append("svg:g")
      .attr("class", "y axis")
      .attr("transform", "translate(20,0)")
      .call(yAxisGen);

   svg.append("svg:path")
      .attr({
        d: lineFun(salesDataToPlot),
        "stroke": "blue",
        "stroke-width": 2,
        "fill": "none",
        "class": pathClass
   });
}

drawLineChart();

Here is the demo showing the above chart.

Updating the Chart in Real Time

As stated earlier, with the capability of the web today, our users want to see the data charts updating immediately as the underlying data changes. The changed information can be pushed to the client using technologies like WebSockets. The chart directive that we just created should be able to respond to such changes and update the chart.

To push data through WebSockets, we need a component on server built using Socket.IO with Node.js, SignalR with .NET, or a similar technology on other platforms. For the demo, I used the $interval service of AngularJS to push ten random values of sales into the sales array with a delay of one second:

$interval(function() {
  var hour = $scope.salesData.length + 1;
  var sales = Math.round(Math.random() * 100);

  $scope.salesData.push({hour: hour, sales: sales});
}, 1000, 10);

To update the chart as soon as the new data is pushed, we need to redraw the chart with updated data. A collection watcher has to be used in the directive to watch the changes on the collection data. The watcher is invoked when any change is made to the collection. The chart is redrawn in the watcher.

scope.$watchCollection(exp, function(newVal, oldVal) {
  salesDataToPlot = newVal;
  redrawLineChart();
});

function redrawLineChart() {

  setChartParameters();
  svg.selectAll("g.y.axis").call(yAxisGen);
  svg.selectAll("g.x.axis").call(xAxisGen);

  svg.selectAll("." + pathClass)
     .attr({
       d: lineFun(salesDataToPlot)
     });
}

The complete demo can be found here.

Conclusion

AngularJS and D3 are very useful libraries for building rich business apps on the web. We discussed how to use them together to create a simple chart. You can extend this knowledge for creating the charts for your applications.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • M S

    The demos doesn’t seem to work at all in Firefox (30).
    You get a short flash of something huge, then there is some tiny little collection of pixels pressed up in a corner.

    • http://sravi-kiran.blogspot.com Ravi Kiran

      Sorry for that, I am working on fixing the demos on Firefox.

    • http://sravi-kiran.blogspot.com Ravi Kiran

      Demos are fixed on Firefox. Didn’t realize earlier that element.clientHeight and elem.clientWidth don’t work on Firefox

  • http://kasperiunas.com/ Eimantas Kasperiūnas

    Would be nice to build up on that and show animated transition from one hour to another, amy try this on my free time

  • Mohd. Mahabubul ALam

    One of the first things we knew about the Crunchinator is that we wanted to use D3.js. The flexibility and power of D3 lends itself to stunning visualizations and graphs. We wanted to leverage that power for the charts in the Crunchinator.

    We also knew we were going to use AngularJS.
    Because the Crunchinator is so front-end heavy, Angular was the clear
    choice to make sure we kept the code organized and easy to read. The
    declarative nature of Angular means we have a solid separation of code
    and presentation; as a developer, that’s a huge win.

    Knowing that we were going to use D3.js for charts and AngularJS for
    our framework, it seemed obvious that the best way to integrate the two
    was to create directives for each of the chart types we wanted to
    display. We created a total of five or six different directives using
    D3. Eventually we decided against a few of the graphs and we trimmed
    that down to the four you currently see.