简体   繁体   中英

Append d3 svg to current ng-repeat element

I would like to append a d3.js pie chart to each li elements generated with ng-repeat.

 <ol>
 <li ng-repeat="h in hashtags | orderBy:predicate:reverse | limitTo: limit">
    <div class="hashtag">
       <a ng-click="showTweetsForHashtag(h)">#{{h.Hashtag}}</a>
    </div>
    <div class="frequency">
       {{h.Frequency}} times
    </div>
    <div class="engagement">
       {{h.Engagement}}
       <pie-chart data="h" on-click="showTweetsForHashtag(item)"></pie-chart>
    </div>
 </li>
</ol>

My $scope.hashtag is an array of objects containing hashtags engagement properties :

[{
   "Favorites": 0,
   "Frequency": 1,
   "Hashtag": "19h30",
   "Replies": 0,
   "Retweets": 1,
   "Engagement":2,
   "tweetId": 615850479952785400
}, {
   "Favorites": 0,
   "Frequency": 1,
   "Hashtag": "80s",
   "Replies": 0,
   "Retweets": 2,
   "Engagement":2,
   "tweetId": [
         616521677275533300,
         617319253738393600
      ] 
}{
   "Favorites": 1,
   "Frequency": 1,
   "Hashtag": "AloeBlacc",
   "Replies": 0,
   "Retweets": 1,
   "Engagement":2,
   "tweetId": 617309488572420100
}, {
   "Favorites": 2,
   "Frequency": 1,
   "Hashtag": "Alpes",
   "Replies": 0,
   "Retweets": 1,
   "Engagement":3,
   "tweetId": 615481266348146700
}]

Tanks to the ng-repeat, each time I call the pie-chart directive, I only pass one h object :

{
   "Favorites": 2,
   "Frequency": 1,
   "Hashtag": "Alpes",
   "Replies": 0,
   "Retweets": 1,
   "Engagement":3,
   "tweetId": 615481266348146700
}

Which I then manually "map" into that format :

var mapped = [{
    "label": "Retweets",
    "value": data.Retweets
}, {
    "label": "Favorites",
    "value": data.Favorites
}, {
    "label": "Replies",
    "value": data.Replies
}];

In the end, I would like my directive to append the pie to the current <div class="pie_chart"></div> (which is generated in the directive template) with the mapped data of the current h object that has been passed. But as ocket-san mentionned d3.select(someElement) only matches the first element in the DOM.

Here is my directive :

.directive('pieChart', ['d3', function(d3) {
    return {
        restrict: 'E',
        scope: {
            data: '=',
            onClick: '&'
        },
        template: '<div class="pie_chart"></div>',
        link: function(scope, iElement, iAttrs) {


            // watch for data changes and re-render
            scope.$watch('data', function(newVals, oldVals) {
                if (newVals) {
                    scope.render(newVals);
                }
            }, true);

            scope.render = function(data) {
                var w = 50, //width
                    h = 50, //height
                    r = data.Engagement / 3, // adapt radius to engagement value
                    color = d3.scale.ordinal().range(["#77b255", "#ffac33", "#07c"]); //custom range of colors

                // map data to to be used by pie chart directive
                var mapped = [{
                    "label": "Retweets",
                    "value": data.Retweets
                }, {
                    "label": "Favorites",
                    "value": data.Favorites
                }, {
                    "label": "Replies",
                    "value": data.Replies
                }];
                data = mapped;


                // Courtesy of https://gist.github.com/enjalot/1203641

                var vis = d3.select(".pie_chart")
                    .append("svg:svg") //create the SVG element inside the <body>
                    .data([data]) //associate our data with the document
                    .attr("width", w) //set the width and height of our visualization (these will be attributes of the <svg> tag
                    .attr("height", h)
                    .append("svg:g") //make a group to hold our pie chart
                    .attr("transform", "translate(" + r + "," + r + ")") //move the center of the pie chart from 0, 0 to radius, radius

                var arc = d3.svg.arc() //this will create <path> elements for us using arc data
                    .outerRadius(r);

                var pie = d3.layout.pie() //this will create arc data for us given a list of values
                    .value(function(d) {
                        return d.value;
                    }); //we must tell it out to access the value of each element in our data array

                var arcs = vis.selectAll("g.slice") //this selects all <g> elements with class slice (there aren't any yet)
                    .data(pie) //associate the generated pie data (an array of arcs, each having startAngle, endAngle and value properties)
                    .enter() //this will create <g> elements for every "extra" data element that should be associated with a selection. The result is creating a <g> for every object in the data array
                    .append("svg:g") //create a group to hold each slice (we will have a <path> and a <text> element associated with each slice)
                    .attr("class", "slice"); //allow us to style things in the slices (like text)

                arcs.append("svg:path")
                    .attr("fill", function(d, i) {
                        return color(i);
                    }) //set the color for each slice to be chosen from the color function defined above
                    .attr("d", arc); //this creates the actual SVG path using the associated data (pie) with the arc drawing function
            };
        }
    }
}]);

The problem is that the instruction

var vis = d3.select(".pie_chart")
    .append("svg:svg")

Appends all the pie charts to the first div with the pie_chart class.

I tried changing it to d3.select(iElement) (…) but it didn't work.

Any suggestions ?

Thanks in advance ! Q.

You can see the current output there : http://i61.tinypic.com/wqqc0z.png

The problem is that d3.select('.pie_chart') selects the first element matching such class in the body, not within your directive template. To achieve this, you should use the element object provided within the link function. In your case:

var vis = d3.select(element[0]).select(".pie_chart").append("svg")...

I have created a simplified fiddle trying to show this.

Hope it helps.

When we using Angularjs and d3js together we'll need to make updating the d3.select('body') selection to be relative to the directive using d3.select(element[0]) instead of the entire DOM. The reason we have to use element[0] instead of just element is because element “is” a jQuery wrapped selection and not an ordinary DOM object. Doing element[0] gives us just the plain old DOM element. (I say “is” in quotes because it's technically a jqlite wrapped DOM element. jqlite is essentially a slimmed down version of jQuery.)

So you need to update your Code to:

.directive('pieChart', ['d3', function(d3) {
return {
    restrict: 'E',
    scope: {
        data: '=',
        onClick: '&'
    },
    template: '<div class="pie_chart"></div>',
    link: function(scope, iElement, iAttrs) {


        // watch for data changes and re-render
        scope.$watch('data', function(newVals, oldVals) {
            if (newVals) {
                scope.render(newVals);
            }
        }, true);

        scope.render = function(data) {
            var w = 50, //width
                h = 50, //height
                r = data.Engagement / 3, // adapt radius to engagement value
                color = d3.scale.ordinal().range(["#77b255", "#ffac33", "#07c"]); //custom range of colors

            // map data to to be used by pie chart directive
            var mapped = [{
                "label": "Retweets",
                "value": data.Retweets
            }, {
                "label": "Favorites",
                "value": data.Favorites
            }, {
                "label": "Replies",
                "value": data.Replies
            }];
            data = mapped;


            // Courtesy of https://gist.github.com/enjalot/1203641
            //Part need Update
            var vis = d3.select(iElement[0])
                .append("svg:svg") //create the SVG element inside the <body>
                .data([data]) //associate our data with the document
                .attr("width", w) //set the width and height of our visualization (these will be attributes of the <svg> tag
                .attr("height", h)
                .append("svg:g") //make a group to hold our pie chart
                .attr("transform", "translate(" + r + "," + r + ")") //move the center of the pie chart from 0, 0 to radius, radius

            var arc = d3.svg.arc() //this will create <path> elements for us using arc data
                .outerRadius(r);

            var pie = d3.layout.pie() //this will create arc data for us given a list of values
                .value(function(d) {
                    return d.value;
                }); //we must tell it out to access the value of each element in our data array

            var arcs = vis.selectAll("g.slice") //this selects all <g> elements with class slice (there aren't any yet)
                .data(pie) //associate the generated pie data (an array of arcs, each having startAngle, endAngle and value properties)
                .enter() //this will create <g> elements for every "extra" data element that should be associated with a selection. The result is creating a <g> for every object in the data array
                .append("svg:g") //create a group to hold each slice (we will have a <path> and a <text> element associated with each slice)
                .attr("class", "slice"); //allow us to style things in the slices (like text)

            arcs.append("svg:path")
                .attr("fill", function(d, i) {
                    return color(i);
                }) //set the color for each slice to be chosen from the color function defined above
                .attr("d", arc); //this creates the actual SVG path using the associated data (pie) with the arc drawing function
        };
    }
}
}]);    

When you update your code the directive('pieChart') function dynamically will select <pie-chart/> tag. if you have specific class, update your code to:

   var vis = d3.select(iElement[0]).select(".pie_chart") 

Update 1

You need to add $index to ng-repeat because:

What Angular is telling us is that every element in an ng-repeat needs to be unique. However, we can tell Angular to use the elements index within the array instead to determine uniqueness by adding track by $index .

 <ol>
   <li ng-repeat="h in hashtags track by $index" | orderBy:predicate:reverse | limitTo: limit">
<div class="hashtag">
   <a ng-click="showTweetsForHashtag(h)">#{{h.Hashtag}}</a>
</div>
  <div class="frequency">
   {{h.Frequency}} times
  </div>
  <div class="engagement">
       {{h.Engagement}}
   <pie-chart data="h" on-click="showTweetsForHashtag(item)"></pie-chart>
  </div>
 </li>
</ol>

I found the answers here to be incorrect in my case.

Jarandaf - was the closest however my solution was to remove the class selector.

and just use the below code:

d3.select(element[0]).append('svg')

d3.select("element") always selects the first element it finds. For example: suppose you have the following html structure:

<body>
    <p></p>
    <p></p>
    <p></p> 
</body>

and you would code: d3.select("p").append("svg"), the result is going to be

<body>
        <p>
           <svg></svg>
        </p>
        <p></p>
        <p></p> 
</body>

You need to use d3.selectAll(element) , which will give you a d3 selection with all the items that fit the selector.

edit:

Ok, so i think your final html structure could look something like this:

<ol>
 <li ng-repeat="h in hashtags | orderBy:predicate:reverse | limitTo: limit">
    <div class="hashtag">
       <a ng-click="showTweetsForHashtag(h)">#{{h.Hashtag}}</a>
    </div>
    <div class="frequency">
       {{h.Frequency}} times
    </div>
    <div class="engagement">
       {{h.Engagement}}
       <div id="pie_chart">
         <svg> your piechart goes here</svg>
       </div>
    </div>
 </li>
<li ng-repeat="h in hashtags | orderBy:predicate:reverse | limitTo: limit">
    <div class="hashtag">
       <a ng-click="showTweetsForHashtag(h)">#{{h.Hashtag}}</a>
    </div>
    <div class="frequency">
       {{h.Frequency}} times
    </div>
    <div class="engagement">
       {{h.Engagement}}
       <div id="pie_chart">
         <svg> another piechart goes here</svg>
       </div>
    </div>
 </li>
</ol>

so suppose that html structure already exists without the tag (its because i dont know anything about angular or directives :-) ) and you want to append the svg tag and append an tag to every div with class "pie_chart", you need to do it as following:

var piecharts = d3.selectAll(".pie_chart").append("svg");

The result will be an html structure like above.

If this is not what you want, then I am sorry, I think i totally misunderstood the question :-)

Thank you Gabriel for you answer !

In the meantime, I found a workaround (it may not be a the prettiest, but it works !)

Directive :

.directive('pieChart', ['d3', function(d3) {
    return {
        restrict: 'E',
        scope: {
            data: '=',
            max: '@',
            item: '@',
            onClick: '&'
        },
        template: '<div class="pie_chart"></div>',
        link: function(scope, iElement, iAttrs) {


            // watch for data changes and re-render
            scope.$watch('data', function(newVals, oldVals) {
                if (newVals) {
                    scope.render(newVals);
                }
            }, true);

            scope.render = function(data) {
                // Courtesy of https://gist.github.com/enjalot/1203641

                var vis = d3.selectAll(".pie_chart")
                    .each(function(d, i) {
                        if (scope.item == i) {
                            var w = 50, //width
                                h = 50, //height
                                normalized = 50 * (data.Engagement) / (scope.max),
                                r = normalized/2, // adapt radius to engagement value
                                color = d3.scale.ordinal().range(["#77b255", "#ffac33", "#07c"]); //custom range of colors

                            // map data to to be used by pie chart directive
                            var mapped = [{
                                "label": "Retweets",
                                "value": data.Retweets
                            }, {
                                "label": "Favorites",
                                "value": data.Favorites
                            }, {
                                "label": "Replies",
                                "value": data.Replies
                            }];
                            var vis = d3.select(this)
                                .append("svg:svg") //create the SVG element inside the template
                                .data([mapped]) //associate our data with the document
                                .attr("width", w) //set the width and height of our visualization (these will be attributes of the <svg> tag
                                .attr("height", h)
                                .append("svg:g") //make a group to hold our pie chart
                                .attr("transform", "translate(" + (w/2) + "," + (h/2) + ")") //move the center of the pie chart from 0, 0 to radius, radius
                                .on("click", function(d, i){
                                   return scope.onClick({item: data});
                                });

                            var arc = d3.svg.arc() //this will create <path> elements for us using arc data
                                .outerRadius(r);

                            var pie = d3.layout.pie() //this will create arc data for us given a list of values
                                .value(function(d) {
                                    return d.value;
                                }); //we must tell it out to access the value of each element in our data array

                            var arcs = vis.selectAll("g.slice") //this selects all <g> elements with class slice (there aren't any yet)
                                .data(pie) //associate the generated pie data (an array of arcs, each having startAngle, endAngle and value properties)
                                .enter() //this will create <g> elements for every "extra" data element that should be associated with a selection. The result is creating a <g> for every object in the data array
                                .append("svg:g") //create a group to hold each slice (we will have a <path> and a <text> element associated with each slice)
                                .attr("class", "slice"); //allow us to style things in the slices (like text)

                            arcs.append("svg:path")
                                .attr("fill", function(d, i) {
                                    return color(i);
                                }) //set the color for each slice to be chosen from the color function defined above
                                .attr("d", arc); //this creates the actual SVG path using the associated data (pie) with the arc drawing function
                        }
                    })
            };
        }
    }
}])

HTML

<ol>
  <li ng-repeat="h in hashtags | orderBy:predicate:reverse | limitTo: limit">
    <div class="hashtag">
      <a ng-click="showTweetsForHashtag(h)">
        #{{h.Hashtag}}
      </a>
    </div>
    <div class="frequency">
      {{h.Frequency}} times
    </div>
    <div class="engagement">
      <pie-chart data="h" max="{{hashtagMaxEngagement}}" item="{{$index}}" on-click="showTweetsForHashtag(item)">
      </pie-chart>
    </div>
  </li>
</ol>

Thanks everyone for your help !

Q.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM