Lecture 4 - iloughman/D3-Lecture-Project GitHub Wiki

Lecture 4 - D3 and Angular

Intro

This tutorial assumes a basic understanding of AngularJS, specifically creating custom directives.

Reusable Directives - Let's Make a Bar Chart

Once upon a time there was a chart of bars.

The Basics

Integrating D3 visualizations with Angular directives is straightforward. For this tutorial, we will create a custom angular directive <bar-chart> which recognizes a scope variable as its source of data.

Let's begin by creating our angular application and including it in our project. In a file we title app.js we instantiate our app:

var app = angular.module('d3Angular', [])

Let's begin with three script tags in index.html, each one giving us access to a different component of this example. First, we need access to the angular framework. Next, we need access to D3's functionality. Finally, referencing our angular application in app.js will perform all of the angular functionality we write.

<html>
    <head>
        <script src="angular.min.js"></script>
        <script src="d3.min.js"></script>
        <script src="app.js"></script>
    </head>
    <body ng-app="d3Angular">
    </body>
</html>

Notice that the ng-app directive (which we've already titled 'd3Angular') is used to auto-bootstrap the Angular application we will write.

A Bar Chart Directive

Let's begin by registering our <bar-chart> directive in app.js.

var app = angular.module('d3Angular', [])

app.directive('barChart', function(){
	
		return {
   	restrict: 'E',
   	template: '<svg></svg>',
   	scope: {data: '=chartData'},
   	link: function(scope,elem,attr){
   	
   	}
})

Here we have restricted our directive to be an element directive, established an SVG element as the template, and defined the isolate scope for this directive. This is important because it allows us to separate the logic of creating a bar chart from the scope data that gets fed into the chart.

Inside the link function we create the chart.

link: function(scope,elem,attr){

			var width = 300;
			var height = 250;

			var svg = d3.select('svg')
			            .attr("width", width)
			            .attr("height", height);

			var xScale = d3.scale.ordinal()
			                .domain(d3.range(scope.data.length))
			                .rangeRoundBands([0, width], 0.3);

			var yScale = d3.scale.linear()
			                .domain([0, d3.max(scope.data)])
			                .range([0, height]);
			
			svg.selectAll("rect")
			    .data(scope.data)
			    .enter()
			    .append("rect")
			    .attr('class','bar')
			    .attr("x", function(d, i) {
			        return xScale(i); // Assigns x coordinate px position
			    })
			    .attr("y", function(d) {
			        return height - yScale(d);
			    })
			    .attr("width", xScale.rangeBand()) // Width = 
			    .attr("height", function(d) {
			        return yScale(d);
			    })
			    .attr("fill", function(d) {
			        return "rgb(" + (d * 10) + ",0,0)";
			    });
			    
	}

A thorough explanation of how we arrive at this bar chart is available here.

Returning to index.html, we can now use our bar chart directive.

<body ng-app="d3Angular">
  <bar-chart chart-data="[10,14,20,25,30,2,9,18]"></bar-chart>
</body>

Try it! You should see the following chart when you start up index.html.

ScreenShot

Hopefully this felt pretty simple and unsatisfying. The beauty of Angular is its two-way data binding. By hard-coding data into the directive, we aren't taking advantage of this functionality. Let's adjust a few things so that we can have a user input data.

User Input Data

In order to do this in a modular way, let's first create a controller in app.js to help manage the data a user inputs.

app.controller('data', function($scope) {
	$scope.chartData = [0];
	$scope.update = function(newData){
		newData = newData.split(',').map(function(e){return +e})
		$scope.chartData = newData
	}
})

The scope variable chartData is initially set to [0] so that our directive will load without an error. Additionally, we've defined an update function, which will take a comma separated string, format it as an array of numbers, and set it as $scope.chartData.

Next we need to give the user the ability to input data. There are many ways to do this, but let's keep things simple. Working in index.html:

    <body ng-app="d3Angular">
      <div ng-controller='data'>
        <bar-chart chart-data="chartData"></bar-chart>
        <input type="text" ng-model="newChartData">
        <button ng-click="update(newChartData)">Update Chart</button>
      </div>
    </body>

Notice that we've wrapped our <bar-chart> directive, <input> and <button> elements in a <div> element that registers the controller. Our controller can now manage the communicationg between a user's input and the scope variable used to generate the bar chart.

Now whenever a user inputs data (again, as a comma separated string) and clicks the Update Chart button, the scope variable chartData will update. But how can we tell our directive to respond?

A convenient way to do this is by watching $scope.data inside the directive, and drawing (or redrawing) the bar chart when this occurs. Here's what this looks like:

		scope.$watch('data', function(newValue,oldValue){
			scope.data = newValue;
			drawChart()
		})

Remember, we are inside the bar chart directive. The first argument of scope.$watch is the scope variable we want to watch. By setting this to 'data', we essentially keep track of when $scope.chartData get's updated by clicking the Update Chart button.

Finally, we need to properly define the drawChart function.

	var drawChart = function(){

		xScale.domain(d3.range(scope.data.length));
		yScale.domain([0,d3.max(scope.data)])

		var bars =svg.selectAll('rect')
		    .data(scope.data)

		bars.exit().remove()

		bars.enter().append('rect')
		        .attr("x", width)
		        .attr("y", function(d) {
		            return height - yScale(d); 
		        })
		        .attr("width", xScale.rangeBand()) 
		        .attr("height", function(d) {
		            return yScale(d); 
		        })
		        .attr("fill", function(d) {
		            return "rgb(" + (d * 10) + ",0,0)";
		        });

		bars.transition()
		    .duration(2000)
		    .delay(function(d,i){
		        return i*100
		    })
		    .attr('x', function(d,i){
		        return xScale(i)
		    })
		    .attr('y', function(d){
	        return height - yScale(d)
		    })
		    .attr('width', xScale.rangeBand())
		    .attr('height', function(d){
		        return yScale(d)
		    })
		    .attr('fill', function(d){
		        return "rgb(" + (d * 10) + ",0,0)"
		    })

	}

This logic of this transition is explained in the previous tutorial.

⚠️ **GitHub.com Fallback** ⚠️