AngularJS Directive with Chart.js

PUBLISHED ON NOV 29, 2015 — ANGULARJS, JAVASCRIPT

As part of a project I’m working on with my good friend Ryan we found a need to be able to easily create some charts to display some metrics to our users. As we’re using AngularJS on our site, the logical step was to create a reusable directive that would allow us to easily hook up charts wherever we needed them, and this is what I’m going to walk through in this post.

I’ll assume you know the basics of setting up an Angular app so I won’t cover that here, and I’ve gone with the very unique name app for mine. Download Chart.js and add a reference on your site, or reference the hosted script like I’ve done for this demo (though I’d never recommend this approach for anything that actually goes live!).

So the first step is to create your controller as this is where we’ll be setting the values and options your chart uses, as well as the specified chart type. For the purpose of this demo I’ve borrowed the line chart data and options from the documentation section of the Chart.js website.

I’ve added variables for ChartType, ChartData and ChartOptions on to the controller and initialised with the borrowed values, so my controller looks like this:

angular.module('app').controller('chartController', function($scope) {
    $scope.ChartType = 'line';
    $scope.ChartData = {
        labels: ["January", "February", "March", "April", "May", "June", "July"],
                    datasets: [
                        {
                            label: "My First dataset",
                            fillColor: "rgba(220,220,220,0.2)",
                            strokeColor: "rgba(220,220,220,1)",
                            pointColor: "rgba(220,220,220,1)",
                            pointStrokeColor: "#fff",
                            pointHighlightFill: "#fff",
                            pointHighlightStroke: "rgba(220,220,220,1)",
                            data: [65, 59, 80, 81, 56, 55, 40]
                        },
                        {
                            label: "My Second dataset",
                            fillColor: "rgba(151,187,205,0.2)",
                            strokeColor: "rgba(151,187,205,1)",
                            pointColor: "rgba(151,187,205,1)",
                            pointStrokeColor: "#fff",
                            pointHighlightFill: "#fff",
                            pointHighlightStroke: "rgba(151,187,205,1)",
                            data: [28, 48, 40, 19, 86, 27, 90]
                        }
                    ]
    };
    $scope.ChartOptions = {
        
                    ///Boolean - Whether grid lines are shown across the chart
                    scaleShowGridLines: true,

                    //String - Colour of the grid lines
                    scaleGridLineColor: "rgba(0,0,0,.05)",

                    //Number - Width of the grid lines
                    scaleGridLineWidth: 1,

                    //Boolean - Whether to show horizontal lines (except X axis)
                    scaleShowHorizontalLines: true,

                    //Boolean - Whether to show vertical lines (except Y axis)
                    scaleShowVerticalLines: true,

                    //Boolean - Whether the line is curved between points
                    bezierCurve: true,

                    //Number - Tension of the bezier curve between points
                    bezierCurveTension: 0.4,

                    //Boolean - Whether to show a dot for each point
                    pointDot: true,

                    //Number - Radius of each point dot in pixels
                    pointDotRadius: 4,

                    //Number - Pixel width of point dot stroke
                    pointDotStrokeWidth: 1,

                    //Number - amount extra to add to the radius to cater for hit detection outside the drawn point
                    pointHitDetectionRadius: 20,

                    //Boolean - Whether to show a stroke for datasets
                    datasetStroke: true,

                    //Number - Pixel width of dataset stroke
                    datasetStrokeWidth: 2,

                    //Boolean - Whether to fill the dataset with a colour
                    datasetFill: true,

                    //String - A legend template
                    legendTemplate: "<ul class=\"<%=name.toLowerCase()%>-legend\"><% for (var i=0; i<datasets.length; i++){%><li><span style=\"background-color:<%=datasets[i].strokeColor%>\"></span><%if(datasets[i].label{%><%=datasets[i].label%><%}%></li><%}%></ul>"

    };
});

Now we have a controller where we can dictate the type of chart we want to load, along with the data and options to be used. Obviously in this sample I’ve hard coded the values which isn’t very useful, ideally (and this is what I’ve done for our project) these values would be retrieved from an Angular service.

The next thing we need to do is to create a directive that makes use of these values. The main things we need here are the template that will be used, the link function for the code to generate the chart, and the scope so we can isolate the values required for the charts.

Given how simple it is to create a chart using Chart.js - it’s just a canvas element - I didn’t see the need to separate this out into a separate HTML file to be used, which would be my usual approach for anything more complex.

The link function needs to find the chart on the page, and determine what chart to create based on the ChartType variable I set on the controller. Using this it then creates the specified chart on the canvas.

Following this, I know have the following directive:

angular.module('app').directive('chartDirective', function() {
  return {
    template: '<canvas id="currentChart" width="600" height="400"></canvas>',
    restrict: 'E',
    scope: {
            Options: '=options',
            Data: '=chartData',
            ChartType: '=chartType'
        },
    link: function(scope, element, attributes) {      
      var createChart = function() { 
        var createdChart;
        var chartContext = document.getElementById("currentChart").getContext("2d");
        
        switch (scope.ChartType){
          case 'line':
            createdChart = new Chart(chartContext).Line(scope.Data, scope.Options);
            break;
          case 'pie':
            createdChart = new Chart(chartContext).Pie(scope.Data, scope.Options);
            break;
          case 'bar':
            createdChart = new Chart(chartContext).Bar(scope.Data, scope.Options);
            break;
          case 'radar':
            createdChart = new Chart(chartContext).Radar(scope.Data, scope.Options);
            break;
          case 'polarArea':
            createdChart = new Chart(chartContext).PolarArea(scope.Data, scope.Options);
            break;
          default:
            break;
        }
        return createdChart;
      }
      scope.currentChart = createChart();
    }
  }
});

As you can see I’ve used a switch statement on the ChartType in order to call the appropriate method for creating a chart. This is the part of the sample I’m not overly happy with, but I can’t find a better way. Ideally, it would be a single method call to Chart.js that takes in the chart type as a parameter, but instead there are separate methods for each different type, hence the switch statement.

That’s all that’s required! The directive can now be used to create a chart on your page easily, just modify the controller to set the scope variables and your chart will be created as you specify.

My index.html page to use the directive looks like this:

<html ng-app="app">
	<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.7/angular.js"></script>
	<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js"></script>
	<script src="app.js"></script>
	<script src="chart-directive.js"></script>
	<head>
	</head>
	<body ng-controller="chartController">
		<chart-directive options="ChartOptions" chart-data="ChartData" chart-type="ChartType"></chart-directive>
	</body>
</html>

As always, happy to take input and learn if there’s a way I can improve any of this!

And if you’re interested in taking a look at my full code, it can be found on GitHub here.

comments powered by Disqus