Kode_12 Kode_12 - 2 months ago 6
AngularJS Question

How can I set up a d3 chart inside a custom directive in Angular?

I'v been following a few walkthroughs on how to implement d3 charts in an angular application. Basically, Im trying to implement the following d3 chart into my custom angular directive ('workHistory'). For the purpose of this question, I'm following a simple bar chart example where I have it set up like so :

index.html

<!doctype html>
<html lang="en" ng-app="webApp">
<head>
<meta charset="utf-8">

<title>My Portfolio</title>

<!--Stylesheets -->
<link rel="stylesheet" href="styles/main.css"/>
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css"/>
<!--Libraries -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.5.8/angular.min.js"></script>
<script src="bower_components/jquery/dist/jquery.min.js"></script>
<script src="bower_components/angular-route/angular-route.min.js"></script>
<script src="bower_components/angular-loader/angular-loader.min.js"></script>
<script src="bower_components/bootstrap/dist/js/bootstrap.min.js"></script>
<script src="bower_components/d3/d3.min.js"></script>
<!--Module -->
<script src="scripts/modules/module.js"></script>
<script src="scripts/modules/d3.module.js"></script>
<!--Controllers -->
<script src="scripts/controllers/mainHeroController.js"></script>
<script src="scripts/controllers/workHistoryController.js"></script>
<!--Directives-->
<script src="scripts/directives/mainHero.directive.js"></script>
<script src="scripts/directives/mainNavbar.directive.js"></script>
<script src="scripts/directives/workHistory.directive.js"></script>
</head>

<!--Main Landing Page-->
<body ng-app="webApp">
<div id="container1">
<work-history chart-data="myData"></work-history>
</div>
<div id="container2">
Container 2
</div>
</body>
</html>


workHistory.directive.js

(function()
{
'use strict';

angular
.module('webApp')
.directive('workHistory', workHistory);

function workHistory()
{
var directive =
{
restrict: 'EA',
controller: 'WorkHistoryController',
//controllerAs: 'workhistory',
scope: {data: '=chartData'},
template: "<svg width='850' height='200'></svg>",
link: workHistoryLink,
};

return directive;
}

function workHistoryLink(scope, element/*, attrs, ctrl, tfn*/)
{

var chart = d3.select(element[0]);
chart.append("div").attr("class", "chart")
.selectAll('div')
.data(scope.data).enter().append("div")
.transition().ease("elastic")
.style("width", function(d) { return d + "%"; })
.text(function(d) { return d + "%"; });
}
})();


main.css

.axis path,
.axis line{
fill: none;
stroke:black;
shape-rendering:crispEdge;
}

.axis text{
font-family: sans-serif;
font-size: 10px;
}

h1{
font-family: sans-serif;
font-weight: bold;
font-size: 16px;
}
.tick
{
stroke-dasharray: 1, 2;
}


The Problem:
With this code, nothing displays. I get the following error:


angular.js:13920TypeError:
chart.append(...).attr(...).selectAll(...).data(...).enter is not a
function


Can someone help me understand how to properly set this up? (Bonus, if someone can explain how I can get the collapsible tree d3 chart configured into a custom directive.

Thanks.

Answer

There are couple of errors in your code:

  1. you template is not good: you make an svg, but you insert standard html tags inside. It can't possibly work. In this case you should make a template with a div tag as a root node.
  2. if you want to provide an initial template like you do, you should then make it as the root node by setting the replace property of your directive to true.
  3. then, if you want added divs by d3 visible, you should make them visible by setting a background or a border with a ".style('background', 'blue')" for example
  4. I don't understand what you can possibly do in the controller 'WorkHistoryController'. According to me, in such a directive, you should avoid provide both link and controller.
  5. finally, if you want the whole think to always work, you should put the D3 code into a $watch so that you are sure that the generation is triggered once the data is set on the scope.
  6. as a complement, now you graph is always observing the array, it should handle the removal of elements that are not necessary if the array is smaller than before: ".exit().remove();"

The directive should look something like this:

angular
    .module('app', [])
    .directive('workHistory', workHistory);

function workHistory()
{
    var directive = 
        {
          restrict: 'EA',
          scope: {data: '=chartData'},
          replace:true,
          template: "<div style='width:100%'></div>",
          link: workHistoryLink,
        };

    return directive;
}

function workHistoryLink(scope, element)
{
  scope.$watch('data', function(){
    var chart = d3.select(element[0]);
    chart.append("div").attr("class", "chart")
      .selectAll('div')
      .data(scope.data).enter().append("div")
      .style('background', 'blue')
      .transition().ease("elastic")
      .style("width", function(d) { return d + "%"; })
      .text(function(d) { return d + "%"; })
      .exit().remove();
  }, true);
} 

here is a jsbin to make my point: http://jsbin.com/vegesigevi/edit?html,js,output

Hope this helps

Comments