将d3 svg附加到当前的ng-repeat元素

Append d3 svg to current ng-repeat element

本文关键字:ng-repeat 元素 d3 svg      更新时间:2023-09-26

我想为每个使用ng-repeat生成的li元素附加一个d3.js饼图。

 <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>

我的美元范围。Hashtag是包含Hashtag参与属性的对象数组:

[{
   "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
}]

坦克到ng-重复,每次调用饼图指令时,我只传递一个h对象:

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

然后我手动将其"映射"为该格式:

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

最后,我希望我的指令将饼添加到当前<div class="pie_chart"></div>(在指令模板中生成),并使用已传递的当前h对象的映射数据。但是正如ocket-san所提到的,d3.select(someElement)只匹配DOM中的第一个元素。

这是我的指令:

.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
            };
        }
    }
}]);
问题是指令
var vis = d3.select(".pie_chart")
    .append("svg:svg")

使用pie_chart类将所有饼图附加到第一个div。

我试着把它改成d3.select(element)(…),但它不起作用。

有什么建议吗?

提前感谢!问:

你可以看到当前的输出:http://i61.tinypic.com/wqqc0z.png

问题是d3.select('.pie_chart')选择了body中匹配此类的第一个元素,而不是在您的指令模板中。要实现这一点,您应该使用link函数中提供的element对象。在你的例子中:

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

我创建了一个简化的小提琴来展示这个。

希望能有所帮助。

当我们一起使用Angularjsd3js时,我们需要更新d3.select('body')选择,使其相对于指令使用d3.select(element[0])而不是整个DOM。我们必须使用element[0]而不是element的原因是因为element"是"一个jQuery而不是普通的DOM对象。执行element[0]只会给我们一个普通的旧DOM元素。(我用引号表示"is",因为从技术上讲,它是一个jqlite封装的DOM元素。jqlite本质上是一个精简版的jQuery。

所以你需要更新你的代码为:
.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
        };
    }
}
}]);    

当你更新你的代码时,directive('pieChart')函数将动态地选择<pie-chart/>标签。如果您有特定的类,请将代码更新为:

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

更新1

您需要将$index添加到ng-repeat,因为:

Angular告诉我们的是,ng-repeat中的每个元素都必须是唯一的。然而,我们可以告诉Angular在数组中使用元素索引来确定唯一性,方法是添加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>

我发现这里的答案对我来说是不正确的。

Jarandaf -是最接近的,但我的解决方案是删除类选择器。

,只使用下面的代码:

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

d3.select("element")总是选择它找到的第一个元素。例如:假设你有以下html结构:

<body>
    <p></p>
    <p></p>
    <p></p> 
</body>
然后写入d3.select("p").append("svg"),结果就是
<body>
        <p>
           <svg></svg>
        </p>
        <p></p>
        <p></p> 
</body>

你需要使用d3. selectall (element),它会给你一个d3选择,包含所有适合选择器的项目。

编辑:

好的,我认为你最终的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">
       {{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>

所以假设没有标签的HTML结构已经存在(因为我不知道任何关于角或指令:-)),你想要添加SVG标签,并添加一个标签到每个div类"pie_chart",你需要这样做:

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

结果将是一个类似上面的html结构。

如果这不是你想要的,那么我很抱歉,我想我完全误解了这个问题:-)

谢谢Gabriel的回答!

与此同时,我找到了一个解决方法(它可能不是最漂亮的,但它有效!)

指令:

.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>

感谢大家的帮助

问: