Creating Charting Directives Using AngularJS and D3.js
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.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| 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){ } }; }); |
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:
1
| < div linear-chart chart-data = "salesData" ></ div > |
1
2
3
4
5
6
7
8
| 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); |
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 usingd3.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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| 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(); |
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:
1
2
3
4
5
6
| $interval( function () { var hour = $scope.salesData.length + 1; var sales = Math.round(Math.random() * 100); $scope.salesData.push({hour: hour, sales: sales}); }, 1000, 10); |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| 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) }); } |
0 comments: