Custom Directive for D3.js chart

Recently I came accross a project where a requirement was to create a custom D3.js chart directive for Angular so it can be reused and It was really a easy task to convert it into a directive and exposing variables and methods required for functionalities

Hence decided to write this demo so as to demostrate How to create a custom directive reusable plugin in itself

Here we will create a chart demostrating the credit card Vs Debit card usage of my friends according to salary on the pivotal point of assumption that salary is fixed for a year here consideration as 2008,2009,2010

Note : You can get the whole code used for this Demo at Github

Structure of the App

				|
				|_app
				|	|
				|	|_scripts__
                                |       |          |_custom (All the custom js code controller/directive/module etc)
                                |       |          |
                                |       |          |_vendor (All the js dependency files : angular,d3,etc)  
				|	|
				|	|_styles (css file)
				|
				|_index.html (main html page)
				


The JSON Structure

                    {
                          "name":"Niraj",
                          "2008":{      //2008,2009,2010 as the year
                            "x" : 33,   // x:credit card usage in %
                            "y" : 64,   // y:debit card usage in %
                            "z" : 70000 // z:being the salary in that year
                          },
                          "2009":{
                           "x" : 40,
                           "y" : 55,
                           "z" : 90000 
                          },
                          "2010":{
                           "x" : 45,
                           "y" : 40,
                           "z" : 100000 
                          }
                       },{
                          "name":"Sohil",
                          "2008":{
                            "x" : 56,
                            "y" : 33,
                            "z" : 100000 
                          },
                          "2009":{
                           "x" : 65,
                           "y" : 40,
                           "z" : 150000 
                          },
                          "2010":{
                           "x" : 80,
                           "y" : 50,
                           "z" : 200000 
                          }
                       }....
                    }   
					
A Simple JSON self explained in itself, However this is my own assumption of js for the respective code written for d3 directive Hence in reality either this has to be the fixed JSON for this kind of chart if else the directive code might need some refactoring to accomodate the new JSON structure
Goal is to achieve a chart like below where usage can be distinguised into four categories each axis representatin credit/debit usage and size of circle being the salary representation year on the filter side in select box.


Writting an angular custom directive for D3 chart

angular.module('chartApp')
    .directive('chart', [function() {
        return {
            restrict: 'EA',
            scope: {
                data: '=data',  //data being the JSON from external
                prop: '=prop',  //is the filter prop in this case year 
                dim: '=dim',    //the four custom labels in each quadrant
                axis: '=axis',  // x axis and y axis label
                fnCallBackFn1: '&callbackSelect',   //callback for click on each cirle (need to show more info below when circle is clicked)
                fnCallBackFn2: '&callbackDeselect'  //mouse leave event for each circle
            },
            link: function($scope, elements, attrs){

                var paddingAll = 10;
                var margin = {top: 20, right: 15, bottom: 60, left: 60}
                    , width = 960 - margin.left - margin.right
                    , height = 500 - margin.top - margin.bottom;

                var val = d3.scale.linear()
                    .domain([0, d3.max($scope.data, function(d) { return d[$scope.prop].z; })])
                    .range([ 0, 10 ]);

                var x = d3.scale.linear()
                    .domain([0, 100]) //(d3.max($scope.data, function(d) { return d[$scope.prop].x; }) + 25)
                    .range([ 0, width ]);

                var y = d3.scale.linear()
                    .domain([0, 100]) //(d3.max($scope.data, function(d) { return d[$scope.prop].y; }) + 25)
                    .range([ height, 0 ]);
                console.log(elements);
                var chart = d3.select(elements[0])
                    .append('svg:svg')
                    .attr('width', width + margin.right + margin.left)
                    .attr('height', height + margin.top + margin.bottom)
                    .attr('class', 'chart')

                //rectangle
                chart.append("g")
                    .append("rect")
                    .attr("class", "rect")
                    .attr("x", margin.left+paddingAll)
                    .attr("y", margin.top+paddingAll)
                    .attr("rx", 10)
                    .attr("ry", 10)
                    .attr("width", width-(2*paddingAll))
                    .attr("height", height-(2*paddingAll))

                //linehor
                chart.append("g")
                    .append("line")
                    .attr("class", "h-line")
                    .attr("x1", margin.left+paddingAll)
                    .attr("y1", (height+(margin.bottom-paddingAll))/2)
                    .attr("x2", width+margin.left-10)
                    .attr("y2", (height+(margin.bottom-paddingAll))/2)

                //linever
                chart.append("g")
                    .append("line")
                    .attr("class", "h-line")
                    .attr("x1", (width+(margin.left+margin.right+(2*paddingAll)))/2)
                    .attr("y1", (margin.top+10))
                    .attr("x2", (width+(margin.left+margin.right+(2*paddingAll)))/2)
                    .attr("y2", (height+margin.top-paddingAll))

                // four quadrant text    
                chart.append("g")
                    .append("text")
                    .attr("class", "dim-text")
                    .attr("x", margin.left+(2*paddingAll))
                    .attr("y", margin.top+(3*paddingAll))
                    .text($scope.dim.q1)


                chart.append("g")
                    .append("text")
                    .attr("class", "dim-text")
                    .attr("x", (width+(margin.left+margin.right+(3*paddingAll)))/2)
                    .attr("y", margin.top+(3*paddingAll))
                    .text($scope.dim.q2)

                chart.append("g")
                    .append("text")
                    .attr("class", "dim-text")
                    .attr("x", margin.left+(2*paddingAll))
                    .attr("y", (height+(margin.bottom+(3*paddingAll)))/2)
                    .text($scope.dim.q3)


                chart.append("g")
                    .append("text")
                    .attr("class", "dim-text")
                    .attr("x", (width+(margin.left+margin.right+30))/2)
                    .attr("y", (height+(margin.bottom+(3*paddingAll)))/2)
                    .text($scope.dim.q4)


                // x-axis
                chart.append("g")
                    .attr("class", "x axis")
                    .attr("transform", "translate(0," + height + ")")
                    .append("text")
                    .attr("class", "label")
                    .attr("x", width+margin.left)
                    .attr("y", 60)
                    .style("text-anchor", "end")
                    .text($scope.axis.x);

                // y-axis
                chart.append("g")
                    .attr("class", "y axis")
                    .append("text")
                    .attr("class", "label")
                    .attr("transform", "rotate(-90)")
                    .attr("y", 12)
                    .attr("dy", ".71em")
                    .style("text-anchor", "end")
                    .text($scope.axis.y);

                var main = chart.append('g')
                    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
                    .attr('width', width)
                    .attr('height', height)
                    .attr('class', 'main')

                // draw the x axis
                var xAxis = d3.svg.axis()
                    .scale(x)
                    .orient('bottom');

                main.append('g')
                    .attr('transform', 'translate(0,' + height + ')')
                    .attr('class', 'main axis date')
                    .call(xAxis);

                // draw the y axis
                var yAxis = d3.svg.axis()
                    .scale(y)
                    .orient('left');

                main.append('g')
                    .attr('transform', 'translate(0,0)')
                    .attr('class', 'main axis date')
                    .call(yAxis);

                var g = main.append("svg:g");

                //listen for prop change (Year chage)
                $scope.$watch('prop',function(curr,old){
                    $scope.drawChart();
                });

                //draw dots (x,y,r) ==> (credit card usage,debit card usage,salary) scaled in the chart size
                $scope.drawChart = function(){
                    d3.selectAll(".points").remove();
                    g.selectAll("scatter-dots")
                        .data($scope.data)
                        .enter().append("svg:circle")
                        .attr("class","points")
                        .attr("cx", function (d,i) { return x(d[$scope.prop].x); } )
                        .attr("cy", function (d) { return y(d[$scope.prop].y); } )
                        .attr("r", function(d,i){return val(d[$scope.prop].z)})
                        .attr("title", function(d,i){return d["name"]})
                        .on("click",function(event,d){
                            $scope.$apply(function() {
                                $scope.fnCallBackFn1({data: event});
                            });
                        })
                        .on("mouseleave",function(){
                            $scope.$apply(function(){
                                $scope.fnCallBackFn2();
                            });

                        })

                    $('svg circle').tipsy({
                        gravity: 'w',
                        html: true,
                        title: function() {
                            var d = this.__data__;
                            return d.name;
                        }
                    });

                };

                $scope.drawChart();

            }
        };
    }]);
					
The directive inputs all the necessary data and function callback Notable features include :
  1. Calculate scale for x axis and y axis here 100 being maximum but in case of non fixed can use the commented code to get the maximum for the field
  2. Draw the X-axis and Y-axis and rectangle with quadrant separation
  3. Assign all the text as passed into directive
  4. On the basic of selected property(Year) filter JSON and draw graph
  5. A listener is bind on Year property so when initiated redraws the chart accordingly


Calling in HTML



					
Here By is the HTML binding of the directive & controller all the data passed here are angular controller scope variables/methods. Names are in co-ordination w.r.t scope variables inside directive


Controller File

        $scope.data = {...}; //The JSON as discussed previously             
        $scope.prop = "2008";
        $scope.dim = {q1:"credit-up, debit-down",q2:"credit-up, debit-up",q3:"credit-down, debit-down",q4:"credit-up, debit-down"};
        $scope.axis = {x:"Credit card usage",y:"Debit card usage"};

        
        //fn called on mouse enter
        $scope.callBack = function(obj){
            $scope.currObj = obj;
            $scope.conShow = true;
        };

        //fn called on mouse leave
        $scope.nonselect = function(){
            $scope.currObj = {};
            $scope.conShow = false;
        }
					
The controller defination used in HTML to pass values to directive defined are the variables and function callbacks.


Click Here for a Live Demo.

Click Here to get the code used for this Demo.



I dedicate this to My Teachers and friends who constantly encouraged me in my every learning curve.

Feel free to write me at : vinodlouis@hotmail.com