Skip to content

Instantly share code, notes, and snippets.

@stormpython
Last active May 4, 2016 11:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stormpython/8877799 to your computer and use it in GitHub Desktop.
Save stormpython/8877799 to your computer and use it in GitHub Desktop.
D3 Directives in Angular

Setting up a D3 Directive using Angular and Elasticsearch

Here, we create a donut-chart directive using:

  1. Yeoman: for scaffolding our app (including Bower and Grunt)
  2. Angular: as our MV* framework
  3. D3: as our charting library
  4. Elasticsearch: as our backend data store
  5. Elasticsearch Javascript Client: to communicate with our Elasticsearch instance
  6. NFL dataset: our test dataset

Loading data into Elasticsearch

For instructions on getting started with Elasticsearch, please visit the overview page.

Download and unzip the NFL dataset. To load the data into Elasticsearch, open the terminal, change into the NFL dataset directory, and run:

curl -XPOST localhost:9200/nfl?pretty
curl -XPUT localhost:9200/nfl/2013/_mapping?pretty -d @mappings/nfl_mapping.json
curl -XPOST localhost:9200/nfl/2013/_bulk?pretty --data-binary @datasets/nfl_2013.json

Scaffolding our app with Yeoman

To install Yeoman, you will need to have node and npm installed. If you haven't installed node or npm, this link is a great place to get started.

npm install -g yo

If you are using npm 1.2.10 or above, this will also automatically install grunt and bower for you. If you're on an older version of npm, you will need to install them manually:

npm install -g grunt-cli bower

If you have installed Grunt globally in the past, you will need to remove it first:

npm uninstall -g grunt

Finally, install the angular generator:

npm install -g generator-angular

After that, create a new directory for your application, then run:

yo angular

Here, we are first generating the file structure for a basic web application and then writing a number of boilerplate files for a new AngularJS application on top of it. This includes boilerplate directives and controllers as well as scaffolded Karma unit tests.

Installing dependencies

Let's install the dependencies for our app using bower:

bower install d3
bower install elasticsearch

You should now have d3.js and elasticsearch.js installed. You can check within the bower_components directory to verify or look for the script tag in the index.html file.

Setting up our Angular Service

Now, we are ready to create a connection to our Elasticsearch server, which will be on our localhost on port 9200.

**Note: For the sake of simplicity, we have included both d3.js and elasticsearch.js in our global namespace.

Lets create our Angular service using our angular generator:

yo angular:service ejs

Now, simply modify the file according to the ejs.js file below.

Adding an Angular Controller

Next, let's add a controller to our app:

yo angular:controller pie

Now, simply modify the file according to the pie.js file below.

We could add a controller to our directive explicitly, but I like to keep my controllers flexible and not bound to one specific directive.

Creating our D3 Directive

The last major hurdle is to create our Angular directive with our D3 code. Let's create our directive:

yo angular:directive piechart

Now, modify the file according to the piechart.js file below.

The magic here is in $watch. The scope watches for changes to the data, and then re-renders the chart.

Finishing touches

Let's wrap it up with some final changes. Open your app.js file and modify it according to the app.js file below. Finally, add a pie.hmtl file exactly like the one below.

Now, serve your app by running:

grunt server

You should now see a donut chart in your browser window, and you should be able to change the values of the chart by modifying the values in the text boxes. Give it a try!

Congrats! You've just created an Angular app with a simple D3 directive and Elasticsearch as your backend data store.

'use strict';
angular.module('d3AngularApp', [
'ngCookies',
'ngResource',
'ngSanitize',
'ngRoute'
])
.config(function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/pie.html'
})
.otherwise({
redirectTo: '/'
});
});
'use strict';
angular.module('d3AngularApp')
.service('Ejs', ['$q', function Ejs($q) {
// AngularJS will instantiate a singleton by calling "new" on this function
function getData(query) {
// $q is the magic Angular service which provides
// a few methods in its deferred API that we will need.
// elasticsearch client call uses the elasticsearch
// javascript client API which in this case is in the
// global namespace (see index.html)
var d = $q.defer(),
client = new elasticsearch.Client();
// Runs the query and resolves the promise object
client.search(query).then(function (data) {
d.resolve(data);
});
return d.promise;
}
return {
getData: getData
};
}]);
<!doctype html>
<!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]-->
<!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]-->
<!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]-->
<!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]-->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title></title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<!-- Place favicon.ico and apple-touch-icon.png in the root directory -->
<!-- build:css styles/vendor.css -->
<!-- bower:css -->
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
<!-- endbower -->
<!-- endbuild -->
<!-- build:css({.tmp,app}) styles/main.css -->
<link rel="stylesheet" href="styles/main.css">
<!-- endbuild -->
</head>
<body ng-app="d3AngularApp">
<!--[if lt IE 7]>
<p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p>
<![endif]-->
<!-- Add your site or application content here -->
<div class="container" ng-view=""></div>
<!-- Google Analytics: change UA-XXXXX-X to be your site's ID -->
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-XXXXX-X');
ga('send', 'pageview');
</script>
<!--[if lt IE 9]>
<script src="bower_components/es5-shim/es5-shim.js"></script>
<script src="bower_components/json3/lib/json3.min.js"></script>
<![endif]-->
<!-- build:js scripts/vendor.js -->
<!-- bower:js -->
<script src="bower_components/jquery/jquery.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/bootstrap/dist/js/bootstrap.js"></script>
<script src="bower_components/angular-resource/angular-resource.js"></script>
<script src="bower_components/angular-cookies/angular-cookies.js"></script>
<script src="bower_components/angular-sanitize/angular-sanitize.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<!-- Don't forget to include these -->
<script src="bower_components/d3/d3.js"></script>
<script src="bower_components/elasticsearch/elasticsearch.js"></script>
<!-- endbower -->
<!-- endbuild -->
<!-- build:js({.tmp,app}) scripts/scripts.js -->
<script src="scripts/app.js"></script>
<script src="scripts/controllers/main.js"></script>
<script src="scripts/controllers/pie.js"></script>
<script src="scripts/directives/piechart.js"></script>
<script src="scripts/services/ejs.js"></script>
<!-- endbuild -->
</body>
</html>
<div ng-controller="PieCtrl">
<pie-chart bind="results.aggregations.touchdowns.buckets"></pie-chart>
<h3>Try changing the values below.</h3>
<div ng-repeat="data in results.aggregations.touchdowns.buckets">
Quarter: <input type="text" ng-model="data.key" />
Touchdowns: <input type="text" ng-model="data.doc_count" />
</div>
</div>
'use strict';
angular.module('d3AngularApp')
.controller('PieCtrl', ['$scope', 'Ejs', function ($scope, ejs) {
// creating our elasticsearch query
var query = {
index: 'nfl',
size: 5,
body: {
query: {
bool: {
must: { match: { "description": "touchdown" }},
must_not: { match: { "qtr": 5 }}
}
},
aggs: {
touchdowns: {
terms: {
field: "qtr",
order: { "_term" : "asc" }
}
}
}
}
};
// Using the ejs service to return a promise object
ejs.getData(query)
.then(function (data) {
// saving the returned data object to the scope
$scope.results = data;
});
}]);
'use strict';
angular.module('d3AngularApp')
.directive('pieChart', function () {
// our D3 directive
return {
restrict: 'E',
scope: {
bind: '='
},
link: function postLink(scope, element, attrs) {
// d3 donut chart
var width = 600,
height = 300,
radius = Math.min(width, height) / 2;
var color = d3.scale.category10();
var arc = d3.svg.arc()
.outerRadius(radius - 60)
.innerRadius(120);
var pie = d3.layout.pie()
.sort(null)
.value(function (d) { return d.doc_count; });
var svg = d3.select(element[0]).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width/1.4 + "," + height/2 + ")");
// watches for changes in the data
scope.$watch('bind', function (data) {
scope.render(data);
}, true);
// renders the donut chart when provided data
scope.render = function (data) {
// Clears the svg
svg.selectAll('*').remove();
if (data) {
var g = svg.selectAll(".arc")
.data(pie(data))
.enter()
.append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("stroke", "#ffffff")
.style("stroke-width", 3)
.style("fill", function (d) {
return color(d.data.key);
});
g.append("text")
.attr("transform", function (d) { return "translate(" + arc.centroid(d) + ")"; })
.attr("dy", ".35em")
.style("text-anchor", "middle")
.style("fill", "white")
.text(function (d) { return d.data.key; });
}
};
}
};
});
@jovar85
Copy link

jovar85 commented May 4, 2016

Excellent tutorial!!!! Thanks... It introduces a lot of technologies, and you make it really simple.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment