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
}
}....
}
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();
}
};
}]);
- 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
- Draw the X-axis and Y-axis and rectangle with quadrant separation
- Assign all the text as passed into directive
- On the basic of selected property(Year) filter JSON and draw graph
- A listener is bind on Year property so when initiated redraws the chart accordingly
Calling in HTML
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;
}
Click Here for a Live Demo.
Click Here to get the code used for this Demo.