Dynamic Graphs with Angular.js & D3.js

D3 is a Javascript library that helps you visualize data by selecting elements and chaining methods together to define behavior.

Angular.js is a Javascript framework made to help you organize your code into modular pieces and encourage testing. Using D3 and Angular together we can make a re-usable graph component that displays dynamic data from any data source. If you need to learn about either one of these before reading this, here are a few resources ~

Let’s get started

In order to get things rolling, I’m going to use the Angular generator from Yeoman to scaffold our application. You can do this yourself and follow along or clone this repository and follow the directions in the README.md file.

Our goal for this directive is to encapsulate the graph and it’s moving parts neatly while allowing the graph to receive data from an outside source. In order to illustrate this, we’ll pump data into the graph both from random user input and from the OpenWeatherMap Weather API.

Ok, let’s start by creating our chart directive. If you haven’t already done so, make a directives folder and in that folder create a file called my-d3-graph.js or whatever you decide is a good name for your file (don’t forget to include all new files in your index.html file). In this file, we’ll need the basic structure for a directive which should look like this ~

angular.module('angularD3App')  
.directive('myD3Graph', function() {
  return {
    restrict: 'E',
    replace: true,
    scope: { val: '=' },
    template: '<div id="bar-graph"></div>',
    link: function(scope, element, attrs) {

    }  
  }
});

How you setup your directive is not very important, what is important is the scope property. Setting val’s scope using the ‘=’ like we did above will allow us to use bi-directional binding for controller to directive communication. When we use the directive on our view, val will be an attribute that we can use on the element to pass in our array of data.

Setting up our playing field

If you’re familiar with D3, you know that it provides you with some nice methods to use in order to define our graphs behavior and structure dynamically. We’ll be appending an SVG to our bar graph div and adding style properties to it to set up our graphs environment.

angular.module('angularD3App')  
.directive('myD3Graph', function() {
// private vars
var margin = { top: 0, right: 0, bottom: 0, left: 0 };  
var w = 400 - margin.left - margin.right;  
var h = 300 - margin.top - margin.bottom;  
return {  
restrict: 'E',  
    replace: true,
    scope: { val: '=' },
    template: '<div id="bar-graph"></div>',
    link: function(scope, element, attrs) {

        var svg = d3.select('#bar-graph').append('svg')
        .attr('width', w + margin.left + margin.right)
        .attr('height', h + margin.top + margin.bottom)
        .append('g')
        .attr('transform', 'translate(' + margin.left + ',' +
        margin.top + ')');
    }  
}
});

Here we have created an SVG with D3, appended it to the bar graph div that we will work in and then gave it a width / height with added margins. This is all we need in order to start setting up our scales in order to use real data.
Since this post is about using angular to create a re-usable graph directive, I won’t dive into the different methods D3 provides to help you generate your graph. If you want to learn more about D3, egghead.io provides some really good free starter videos. I will point out that when we pass data into our data methods, we have to dig into the parent scope in order to talk to the controller.

angular.module('angularD3App')  
.directive('myD3Graph', function() {
// private vars
var margin = { top: 0, right: 0, bottom: 0, left: 0 };  
var w = 400 - margin.left - margin.right;  
var h = 300 - margin.top - margin.bottom;  
return {  
restrict: 'E',  
    replace: true,
    scope: { val: '=' },
    template: '<div id="bar-graph"></div>',
    link: function(scope, element, attrs) {

        var svg = d3.select('#bar-graph').append('svg')
        .attr('width', w + margin.left + margin.right)
        .attr('height', h + margin.top + margin.bottom)
        .append('g')
        .attr('transform', 'translate(' + margin.left + ',' +
        margin.top + ')');
        var yScale = d3.scale.linear()
        .domain([0, d3.max(scope.$parent.graphData)])
        .range([0, h]);
        var colorScale = d3.scale.quantize()
        .domain([0, scope.$parent.graphData.length])
        .range(['orange, 'purple', 'red', 'green']);
        var xScale = d3.scale.ordinal()
        .domain(scope.$parent.graphData)
        .rangeBands([0, w], 0.1, 0);
        svg.selectAll('rect')
        .data(scope.$parent.graphData)
        .enter()
        .append('rect')
        .attr('class', 'bar')
        .attr('x', function(d) {
            return xScale(d);
        })
        .attr('y', function(d) {
            return h - yScale(d);
        })
        .attr('width', xScale.rangeBand())
        .attr('height', function(d) {
            return yScale(d);
        });
        scope.$watch('val', function(newVal, oldVal) {
            yScale = d3.scale.linear()
            .domain([0, d3.max(newVal)])
            .range(0, h]);
            svg.selectAll('rect')
            .data(newVal)
            .transition()
            .duration(250)
            .delay(function (d, i) {
                return i * 25;
            })
            .transition()
            .duration(500)
            .attr('y', function (d) {
                return h - yScale(d);
            })
            .attr('width', xScale.rangeBand())
            .attr('height', function (d) {
                return yScale(d);
            });
        });
   }  
}
});

With our directive fully formed, we only have a few steps to go before we can give this thing a whirl in the browser. We need to setup a view that houses our directive, and gives the user the ability to enter random data or query our API. In order to do this we need to pull the API into a service (if you need help setting up the service, take a look at mine and use it our controller, the user inputted data fields can be defined in the controller as well. When all is said and done, our controller looks like this ~

angular.module(‘angularD3App’)  
 .controller(‘MainCtrl’, function ($scope, WeatherDataService, $log) {
$scope.searchTerm = ‘’;
 // graph vars
 $scope.name = ‘’;
 $scope.humidity = 10;
 $scope.pressure = 15;
 $scope.temp = 5;
 $scope.description = ‘’;
 $scope.wind = 20;
$scope.graphData = [$scope.temp, $scope.humidity, $scope.pressure, $scope.wind];

 $scope.fakeWeather = function () {
 $scope.graphData = [$scope.temp, $scope.humidity, $scope.pressure,    $scope.wind];
 };
$scope.searchWeather = function () {
 WeatherDataService.getWeather($scope.searchTerm)
 .then(function(data) {
 $log.debug(data.data);
 $scope.name = data.data.name;
 $scope.humidity = data.data.main.humidity;
 $scope.pressure = data.data.main.pressure;
 $scope.temp = data.data.main.temp;
 $scope.description = data.data.weather[0].description;
 $scope.wind = data.data.wind.speed;
$scope.graphData = [$scope.temp, $scope.humidity, $scope.pressure, $scope.wind];
 });
 };
});

Our view uses the searchWeather and fakeWeather functions to provide data to our directive via the val attribute.

<nav class=”navbar navbar-default”>  
   <div class=”container-fluid”>
     <div class=”navbar-header”>
       <button type=”button” class=”navbar-toggle collapsed” data-toggle=”collapse” data-target=”#bs-example-navbar-collapse-1">
         <span class=”sr-only”>Toggle navigation</span>
         <span class=”icon-bar”></span>
         <span class=”icon-bar”></span>
         <span class=”icon-bar”></span>
      </button>
      <a class=”navbar-brand” href=”#”>Brand</a>
    </div>
 <div class=”collapse navbar-collapse” id=”bs-example-navbar-collapse-1">
 <form class=”navbar-form navbar-left” role=”search”>
   <div class=”form-group”>
     <input type=”text” class=”form-control” placeholder=”Search” ng-model=”searchTerm”>
   </div>
   <button type=”submit” class=”btn btn-default” ng-click=”searchWeather()”>Submit</button>
 </form>
 <ul class=”nav navbar-nav navbar-right”>
 <li><a href=”#”>Link</a></li>
 </ul>
 </li>
 </ul>
</div>  
</div>  
</nav>  
<div class=”container-fluid”>  
  <div class=”row”>
   <div class=”col-sm-6">
    <my-d3-graph val=”graphData”></my-d3-graph>
    <h3>{{ name }}</h3>
    <p>{{ description }}</p>
   </div>
 <div class=”col-sm-6">
  <form>
    <div class=”form-group”>
      <label for=”temperature”>Temperature</label>
      <input type=”text” class=”form-control” id=”temperature” ng-model=”temp”>
    </div>
    <div class=”form-group”>
      <label for=”humidity”>Humidity</label>
      <input type=”text” class=”form-control” id=”humidity” ng-model=”humidity”>
    </div>
    <div class=”form-group”>
      <label for=”pressure”>Pressure</label>
      <input type=”text” class=”form-control” id=”pressure” ng-model=”pressure”>
    </div>
    <div class=”form-group”>
      <label for=”wind”>Wind</label>
      <input type=”text” class=”form-control” id=”wind” ng-model=”wind”>
    </div>
   <button type=”submit” class=”btn btn-default pull-right” ng-click=”fakeWeather()”>Submit</button>
 </form>
 </div>
 </div>
</div>  

With that, we have a fully functional demo for our directive. If you cloned my repo or used the Angular generator, you can start the app by running grunt serve from the project’s root folder. You can now enter in data for all of the fields provided or search for a location’s weather data and watch the graph dynamically update. While this might not be the most useful way to represent the weather data, it serves as an example of what you can do with D3 and Angular combined.
If you want to look at the entire demo, it lives here.

If you found this useful or want to reach out, you can find me on Twitter: @stides303