Custom AngularJS directive to plot chart using D3.js

In some of my earlier posts I have talked about how to create custom AngularJS directive that i was using to show connection latency values as text indicator. To further enhance user experience, i created a custom directive that renders connection latency values as chart using D3.js. I will not be discussing how to use D3 and how it works. I am going to limit discussion in this post to some key points that I ran into while creating chart that updates every X number of seconds.

What is the objective?

In the application, I measure a user's connection latency every 5 seconds. Based on the result, user is shown indicators to show how their connectivity looks like to the server. This helps in pro-actively warning the user if they need to fix issue with their connection. To achieve this client application makes an asynchronous request to the server. Over time the application collects latency data and then uses some heuristics to project latencies in the future.

As the data is coming in every 5 seconds, application plots the values on column chart as shown below.

AngularJS directive for D3

Following code is taken from the prototype that was built to create proof of concept.


WebPortal.directive('latencyChart', ['$window',
 function ($window) {
     var directiveObj = {
         restrict: 'E',
         scope: {
             latencies: '=',
             config: '='
         },
         transclude: true,
         template: "<svg id='latChart' class='chart'></svg>",
         link: function (scope, element, attr, ctrl) {
             var defaultConfig = { height: 100, showValues: true, maxPoints: 10 };
             scope.config = $.extend(true, defaultConfig, scope.config ? scope.config : {});
             var chartObj = null;
             var trimmedData = [];
             var height, width;
             var barWidth;
             var y;
             scope.$watchCollection('latencies', function (newVal, oldVal) {
                 trimmedData = newVal.slice(0, scope.config.maxPoints);
                 update();
             });
             var update = function () {
                 y.domain([0, d3.max(trimmedData, function (d) { return d.value; })]);
                 var bar = chartObj.selectAll("g").data([]);
                 bar.exit().remove();
                 bar = chartObj.selectAll("g").data(trimmedData)
                     .enter().append("g")
                     .attr("transform", function (d, i) { return "translate(" + i * barWidth + ",0)"; });
                 bar.append("rect")
                     .attr("y", function(d) {
                         return y(d.value);
                     })
                     .attr("height", function(d) {
                         return height - y(d.value);
                     })
                     .attr("width", barWidth - 1).
                     style("fill", function(d, i) {
                         if (d.value > 2000) {return 'red';}
                         else if (d.value > 250) { return 'orange'; }
                         else {return 'green';}
                 });
                 if (scope.config.showValues) {
                     bar.append("text")
                         .attr("x", barWidth / 2)
                         .attr("y", function(d) {
                             return y(d.value) + 3;
                         })
                         .attr("dy", ".75em")
                         .text(function(d) {
                             return d.value;
                         });
                 }
             }
             var initialize = function () {
                 width = $(element).width();
                 //height = $(element).height();
                 height = scope.config.height;
                 barWidth = width / scope.config.maxPoints;
                 y = d3.scale.linear().range([height, 0]);
                 
                 chartObj = d3.select("#latChart").
                     attr("width", width).
                     attr("height", height);
             };
             initialize();
             update();
         }
     };
     return directiveObj;
 }
]);

This custom directive registers a watch over the collection of latency values. As new data comes in, update method is called to render new points and remove old points from the plot. The implementation uses very typical enter, update and exit approach to refresh the plot. Before updating the plot with new value, the chart is attached to an empty array. This makes sure that all the existing points come into the exit pipe line and get removed when remove is called. After that chart object is associated with new set of values.

Most of the implementation for plotting a basic column chart has been taken from the tutorial published on D3 site. I have made modifications to make this implementation that behaves like Cubism's time series plot without having to use extra Cubism.js library.

How to use it?

Following snippet shows how this directive was used on HTML page.


<div ng-controller="ServerClockController">
    <h3>Connection Latency</h3>
    <div class="row">
        <div class="col-xs-6" style="height: 500px;">
            <latency-chart data-latencies="latencies" 
   config="{height:200, showValues:true, maxPoints:10}"></latency-chart>
        </div>
    </div>
</div>

You can extend this implementation to tweak config property of isolated scope to control how the charts is rendered.

Search

Social

Weather

9.3 °C / 48.7 °F

weather conditions Clear

Monthly Posts

Blog Tags