1

I am trying to integrate the d3js graph with my angular directive. but I am not getting the expected result.

  1. the graph is not properly appending to div; instead I am getting undefined% label
  2. Each time I click on the list ('li), instead of updating the graph it is appending a new graph.

Please click on the list on the top.

here is my code and demo :

var myApp = angular.module("myApp", ['ngResource']);

myApp.factory("server", function($resource) {

  return $resource('https://tcp.firebaseio.com/graph.json')

})

myApp.controller("main", function($scope, server) {

  $scope.test = "Hellow";

  $scope.data = server.query();

  $scope.data.$promise.then(function(result) {

    $scope.values = result;
    $scope.defaultValue = $scope.values[0].value;

    console.log($scope.defaultValue);

  })

  $scope.updateGraph = function(item) {

    $scope.defaultValue = item.value;

  }

});


var planVsActual = function($timeout) {

  return {

    replace: true,

    template: "<div id='pieGraph'></div>",

    link: function(scope, element, attr) {

      $timeout(function() {

        scope.$watch("defaultValue", function(newVal, oldVal) {

          var phraseValue = [newVal, 100 - newVal];
          drawPie(phraseValue);

          function drawPie(array) {

            console.log(element.width)

            var width = element.width(), height = element.height(),
                    radius = Math.min(width, height) / 1.2, data = array;

              if (!array.length) return;

              var color = d3.scale.ordinal()
                    .domain(array) 
                    .range(["#ffff00", "#1ebfc5"]);

                    var pie = d3.layout.pie()
                    .sort(null)
                    .value(function(d) { return d });

                    var arc = d3.svg.arc()
                      .outerRadius(radius - 90)
                      .innerRadius(radius - 85);

                      var svg = d3.select("#pieGraph")
                      .append("svg")
                    .attr("width", width)
                    .attr("height", height)
                    .append("g")
                    .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

                    var g = svg.selectAll(".arc")
                      .data(pie(array))
                        .enter().append("g")
                        .attr("class", "arc");

                    g.append("path")
                      .attr("d", arc)
                      .style("fill", function(d,i) {  return color(d.data); });

                      g.append("text")
                     .text(array[0]+'%')
                     .attr("class", "designVal")
                     .style("text-anchor", "middle")


          }

        })

      }, 100)


    }

  }

}

angular.module("myApp")

.directive("planVsActual", planVsActual);

Live Demo

2 Answers 2

1

When making a dynamic chart using d3 on Angular a best practice is to bind the chart's data to the directive.

<plan-vs-actual data="defaultValue"></plan-vs-actual>

in JavaScript you must add scope: {data: '='} to your directive definition object.

return {
  replace: true,
  template: <div id="pieGraph></div>,
  scope: {data: '='},
  link: function(){
    //link
  }
};

Additionally, you want to define as much of your d3 visualization in your link function before any data is loaded, for optimal performance. Load the parts of the graph not dependent on data as soon as possible. In your case that means the variables width...height...radius...color.range()...pie...arc...svg... can all be defined outside the scope.$watch function, before any data is ever loaded. Your link will look like this:

link: function(scope,element,attr){

  $timeout(function(){

    var width = element.width(), 
          height = element.height(),
          radius = Math.min(width, height) / 1.2;

      var color = d3.scale.ordinal()
                  .range(["#ffff00", "#1ebfc5"]);

      var pie = d3.layout.pie()
                .sort(null)
                .value(function(d) { return d });

      var arc = d3.svg.arc()
                .outerRadius(radius - 90)
                .innerRadius(radius - 85);

      var svg = d3.select("#pieGraph")
          .append("svg")
          .attr("width", width)
          .attr("height", height)
          .append("g")
          .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

      //scope.$watch goes here

  },100)

}

Since you're now using best practices, your scope.$watch function will now watch the data attribute of your directive, not defaultValue, which will look like this.

scope.$watch('data',function(newVal, oldVal){
  //code here
})

Now we've got to work out what's got to happen inside scope.$watch ...the most important thing that changes is that we break out the .data() and .enter() methods. Below I show you what the new scope.$watch looks like with inline comments to explain it.

scope.$watch("data", function(newVal, oldVal) {

      var phraseValue = [newVal, 100 - newVal];
      drawPie(phraseValue);

      function drawPie(array) {

        console.log(element.width)

                var data = array;

          if (!array.length) return;

                color.domain(array);

        //NOTICE BELOW how we select the data and then chain .enter() separately,
        //doing that allows us to use D3's enter(), exit(), update pattern

        //select all .arc from SVG and bind to data
        var g = svg.selectAll(".arc")
          .data(pie(array));

        //select all <text> from SVG and bind to data as well
        var t = svg.selectAll('text').data(array);

        //enter
        g.enter()
        .append("path")
          .attr("class", "arc")
          .attr("d", arc)
          .style("fill", function(d,i) {  return color(d.data); });

        t.enter()
        .append('text')
          .text(array[0]+"%")
          .attr("class", "designVal")
          .style("text-anchor", "middle");

        //exit
        //in this example exit() is wholly unnecessary since the # of datum
        //will never decrease. However, if it would, this would account for it.
        g.exit().remove();
        t.exit().remove();

        //update
        //update the 'd' attribute of arc and update the text of the text
        g.attr('d',arc);
        t.text(array[0]+'%');
      }

    })

References:
Enter, Update, Exit
D3 on Angular by Ari Lerner

Link to Forked Plunker

Sign up to request clarification or add additional context in comments.

1 Comment

I'm aware that you can copy/paste this code and you'll see it work. However, I know it's also a bit complicated and missing explanations in places. Even as I look at it, there are further improvements you can make. I recommend checking out Ari Lerner's book, which I link to in the references. I also challenge you to find a way to append the <text> without using two enter() methods.
1

You are appending a new svg each time.

Simple fix is to empty the container first:

scope.$watch("defaultValue", function(newVal, oldVal) {

          // empty the chart element
          element.html(''); 

          // chart code    
});

I'm sure you could store reference to the intial object created instead of creating a new one each time which would allow you to animate the differences but for purpose of this answer I don't intend to rewrite your d3 code and am simply providing solution that fits with what you are doing currently

working plunker

4 Comments

every time it will redraw the html..but it make sense to have it.more simpler version would be element.empty()
@PankajParkar I don't know d3 ..is that a problem? OP is already doing that. empty() doesn't do anything different than html('') ...simpler is pretty subjective in this case
just a different way to accomplish the same thing
but more importantly, its in d3 way :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.