简体   繁体   English

带有角度/水平标签的d3.js饼图

[英]d3.js pie chart with angled/horizontal labels

I'm working on a pie chart mock. 我正在做一个饼图模拟。 That I need to try and match the designs to have the label extruding out with a horizontal line attached to the slice ticks. 我需要尝试匹配设计,使标签挤出,并在切片刻度线上附加水平线。 Is this possible? 这可能吗? It would be a bonus to have the black dots form on the segments. 在片段上形成黑点将是一个奖励。

饼图与水平标签

http://jsfiddle.net/BxLHd/15/ http://jsfiddle.net/BxLHd/15/

Here is the code for the tick marks. 这是刻度线的代码。 Would it be a case of creating another set of lines that intersect? 是否会创建另一组相交的线?

                        //draw tick marks
                        var label_group = d3.select('#'+pieId+' .label_group');
                        lines = label_group.selectAll("line").data(filteredData);
                        lines.enter().append("svg:line")
                                .attr("x1", 0)
                                .attr("x2", 0)
                                .attr("y1", function(d){
                                    if(d.value > threshold){
                                        return -that.r-3;
                                    }else{
                                        return -that.r;
                                    }
                                })
                                .attr("y2", function(d){
                                    if(d.value > threshold){
                                        return -that.r-8;
                                    }
                                    else{                                   
                                        return -that.r;
                                    }
                                })
                                .attr("stroke", "gray")
                                .attr("transform", function(d) {
                                        return "rotate(" + (d.startAngle+d.endAngle)/2 * (180/Math.PI) + ")";
                                });

                        lines.transition()
                                .duration(this.tweenDuration)
                                .attr("transform", function(d) {
                                        return "rotate(" + (d.startAngle+d.endAngle)/2 * (180/Math.PI) + ")";
                                });

                        lines.exit().remove();

Here's a proof of concept (using a different example than yours as a basis as there's quite a lot of code in yours). 这是一个概念证明(使用一个不同于你的例子作为基础,因为你的代码中有很多代码)。 This is the basic approach: 这是基本方法:

  • For each label, compute the start and end of the line underneath it. 对于每个标签,计算其下方的行的开始和结束。 This is done by drawing the label and getting its bounding box. 这是通过绘制标签并获取其边界框来完成的。
  • This gives two points on the pointer path, the third is the center of the respective segment. 这在指针路径上给出两个点,第三个是相应段的中心。 This is computed while computing the positions of the labels. 这是在计算标签位置时计算的。
  • These three points become part of the data. 这三点成为数据的一部分。 Now draw path s for each of the data elements, using the three points computed before. 现在使用之前计算的三个点为每个数据元素绘制path
  • Add an SVG marker at the end of each path for the dot. 在每个路径的末尾添加一个SVG标记。

Here's the code to do it, step by step. 这是一步一步完成的代码。

.attr("x", function(d) {
    var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
    d.cx = Math.cos(a) * (radius - 75);
    return d.x = Math.cos(a) * (radius - 20);
})
.attr("y", function(d) {
    var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
    d.cy = Math.sin(a) * (radius - 75);
    return d.y = Math.sin(a) * (radius - 20);
})

This is computing the x and y positions of the labels outside the segments. 这是计算段外标签的x和y位置。 We also compute the position of the final point of the pointer path, in the center of the segment. 我们还计算指针路径的最终点的位置,在段的中心。 That is, both in the middle between start and end angle and between inner and outer radii. 也就是说,在开始和结束角度之间以及内半径和外半径之间的中间。 This is added to the data. 这将添加到数据中。

.text(function(d) { return d.value; })
.each(function(d) {
    var bbox = this.getBBox();
    d.sx = d.x - bbox.width/2 - 2;
    d.ox = d.x + bbox.width/2 + 2;
    d.sy = d.oy = d.y + 5;
});

After adding the text label (in this case, simply the value), we get for each the bounding box and compute the remaining two points for the path, just below the text to the left and just below to the right. 添加文本标签后(在这种情况下,只是值),我们得到每个边界框并计算路径的剩余两个点,就在左边的文本下方和右下方的文本下方。

svg.selectAll("path.pointer").data(piedata).enter()
  .append("path")
  .attr("class", "pointer")
  .style("fill", "none")
  .style("stroke", "black")
  .attr("marker-end", "url(#circ)")
  .attr("d", function(d) {
    if(d.cx > d.ox) {
        return "M" + d.sx + "," + d.sy + "L" + d.ox + "," + d.oy + " " + d.cx + "," + d.cy;
    } else {
        return "M" + d.ox + "," + d.oy + "L" + d.sx + "," + d.sy + " " + d.cx + "," + d.cy;
    }
  });

Now we can actually add the paths. 现在我们可以实际添加路径。 They are a straightforward connection of the three points computed before, with a marker added at the end. 它们是之前计算的三个点的直接连接,最后添加了一个标记。 The only thing to watch out for is that, depending on whether the label is on the left or the right of the chart, the path needs to start at the lower left of the label or the lower right. 唯一需要注意的是,根据标签是在图表的左侧还是右侧,路径需要从标签的左下角或右下角开始。 This is the if statement here. 这是if语句。

Complete demo here . 在这里完成演示。

Here is the plugin code that should allow multiple instances of the pie chart - along with being able to update each pie chart with a new set of data. 以下是应该允许饼图的多个实例的插件代码 - 以及能够使用一组新数据更新每个饼图。

I am open to ways to enhance the code. 我愿意采用增强代码的方法。 I feel it still looks a bit bulky - especially the way I am reseting the selector on update. 我觉得它看起来仍然有点笨重 - 尤其是我在更新时重置选择器的方式。 Any suggestions to streamline this? 有什么建议可以简化这个吗?

http://jsfiddle.net/Qh9X5/1318/ http://jsfiddle.net/Qh9X5/1318/

$(document).ready(function() {


            (function( $ ){
                var methods = {
                    el: "",
                    init : function(options) {
                        var clone = jQuery.extend(true, {}, options["data"]);

                        methods.el = this;          
                        methods.setup(clone);
                    },
                    setup: function(dataset){               

                        this.width = 300;
                        this.height = 300;
                        this.radius = Math.min(this.width, this.height) / 2;

                        this.color = d3.scale.category20();

                        this.pie = d3.layout.pie()
                            .sort(null);

                        this.arc = d3.svg.arc()
                            .innerRadius(this.radius - 100)
                            .outerRadius(this.radius - 50);

                        this.svg = d3.select(methods.el["selector"]).append("svg")
                            .attr("width", this.width)
                            .attr("height", this.height)
                            .append("g")
                                .attr("class", "piechart")
                                .attr("transform", "translate(" + this.width / 2 + "," + this.height / 2 + ")");                

                        //this.update(dataset[0].segments); 
                    },
                    oldPieData: "",
                    pieTween: function(d, i){
                        var that = this;

                        var theOldDataInPie = methods.oldPieData;
                        // Interpolate the arcs in data space

                        var s0;
                        var e0;

                        if(theOldDataInPie[i]){
                                s0 = theOldDataInPie[i].startAngle;
                                e0 = theOldDataInPie[i].endAngle;
                        } else if (!(theOldDataInPie[i]) && theOldDataInPie[i-1]) {
                                s0 = theOldDataInPie[i-1].endAngle;
                                e0 = theOldDataInPie[i-1].endAngle;
                        } else if(!(theOldDataInPie[i-1]) && theOldDataInPie.length > 0){
                                s0 = theOldDataInPie[theOldDataInPie.length-1].endAngle;
                                e0 = theOldDataInPie[theOldDataInPie.length-1].endAngle;
                        } else {
                                s0 = 0;
                                e0 = 0;
                        }

                        var i = d3.interpolate({startAngle: s0, endAngle: e0}, {startAngle: d.startAngle, endAngle: d.endAngle});

                        return function(t) {
                                var b = i(t);
                                return methods.arc(b);
                        };
                    },
                    removePieTween: function(d, i) {                
                        var that = this;
                        s0 = 2 * Math.PI;
                        e0 = 2 * Math.PI;
                        var i = d3.interpolate({startAngle: d.startAngle, endAngle: d.endAngle}, {startAngle: s0, endAngle: e0});

                        return function(t) {
                                var b = i(t);
                                return methods.arc(b);
                        };
                    },
                    update: function(dataSet){
                        var that = this;

                        methods.el = this;
                        methods.svg = d3.select(methods.el["selector"] + " .piechart");

                        this.piedata = methods.pie(dataSet);

                        //__slices
                        this.path = methods.svg.selectAll("path.pie")
                            .data(this.piedata);

                        this.path.enter().append("path")
                            .attr("class", "pie")
                            .attr("fill", function(d, i) {
                                return methods.color(i); 
                            })
                            .transition()
                                .duration(300)
                                .attrTween("d", methods.pieTween);

                        this.path
                                .transition()
                                .duration(300)
                                .attrTween("d", methods.pieTween);

                        this.path.exit()
                                .transition()
                                .duration(300)
                                .attrTween("d", methods.removePieTween)
                                .remove();    
                        //__slices


                        //__labels  
                        var labels = methods.svg.selectAll("text")
                            .data(this.piedata);

                        labels.enter()
                            .append("text")
                            .attr("text-anchor", "middle")


                        labels
                            .attr("x", function(d) {
                                var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
                                d.cx = Math.cos(a) * (methods.radius - 75);
                                return d.x = Math.cos(a) * (methods.radius - 20);
                            })
                            .attr("y", function(d) {
                                var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
                                d.cy = Math.sin(a) * (methods.radius - 75);
                                return d.y = Math.sin(a) * (methods.radius - 20);
                            })
                            .text(function(d) { 
                                return d.value; 
                            })
                            .each(function(d) {
                                var bbox = this.getBBox();
                                d.sx = d.x - bbox.width/2 - 2;
                                d.ox = d.x + bbox.width/2 + 2;
                                d.sy = d.oy = d.y + 5;
                            })
                            .transition()
                                .duration(300)

                        labels
                            .transition()
                            .duration(300)      

                        labels.exit()
                            .transition()
                            .duration(300)
                        //__labels


                        //__pointers
                        methods.svg.append("defs").append("marker")
                            .attr("id", "circ")
                            .attr("markerWidth", 6)
                            .attr("markerHeight", 6)
                            .attr("refX", 3)
                            .attr("refY", 3)
                            .append("circle")
                            .attr("cx", 3)
                            .attr("cy", 3)
                            .attr("r", 3);

                        var pointers = methods.svg.selectAll("path.pointer")
                            .data(this.piedata);

                        pointers.enter()
                            .append("path")
                            .attr("class", "pointer")
                            .style("fill", "none")
                            .style("stroke", "black")
                            .attr("marker-end", "url(#circ)");

                        pointers
                            .attr("d", function(d) {
                                if(d.cx > d.ox) {
                                    return "M" + d.sx + "," + d.sy + "L" + d.ox + "," + d.oy + " " + d.cx + "," + d.cy;
                                } else {
                                    return "M" + d.ox + "," + d.oy + "L" + d.sx + "," + d.sy + " " + d.cx + "," + d.cy;
                                }
                            })
                            .transition()
                                .duration(300)

                        pointers
                            .transition()
                            .duration(300)      

                        pointers.exit()
                            .transition()
                            .duration(300)

                        //__pointers

                        this.oldPieData = this.piedata;

                    }
                };

                $.fn.piechart = function(methodOrOptions) {
                    if ( methods[methodOrOptions] ) {
                        return methods[ methodOrOptions ].apply( this, Array.prototype.slice.call( arguments, 1 ));
                    } else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) {
                        // Default to "init"
                        return methods.init.apply( this, arguments );
                    } else {
                        $.error( 'Method ' +  methodOrOptions + ' does not exist' );
                    }    
                };

            })(jQuery);



            var dataCharts = [
                {
                    "data": [
                        {
                            "segments": [
                                53245, 28479, 19697, 24037, 40245                           
                            ]
                        }
                    ]
                },
                {
                    "data": [
                        {
                            "segments": [
                                855, 79, 97, 237, 245                   
                            ]
                        }
                    ]
                },
                {
                    "data": [
                        {
                            "segments": [
                                22, 79, 97, 12, 245                 
                            ]
                        }
                    ]
                },
                {
                    "data": [
                        {
                            "segments": [
                                122, 279, 197, 312, 545                 
                            ]
                        }
                    ]
                }            
            ];

            var clone = jQuery.extend(true, {}, dataCharts);

                //__invoke concentric
                $('[data-role="piechart"]').each(function(index) {
                    var selector = "piechart"+index;

                    $(this).attr("id", selector);

                    var options = {
                        data: clone[0].data
                    }

                    $("#"+selector).piechart(options);
                    $("#"+selector).piechart('update', clone[0].data[0].segments);
                });


            $(".testers a").on( "click", function(e) {
                e.preventDefault();

                var clone = jQuery.extend(true, {}, dataCharts);

                var min = 0;
                var max = 3;

                //__invoke pie chart
                $('[data-role="piechart"]').each(function(index) {
                    pos = Math.floor(Math.random() * (max - min + 1)) + min;
                    $("#"+$(this).attr("id")).piechart('update', clone[pos].data[0].segments);
                });

            }); 

});

To conclude I've wrapped the very latest code for this in a jquery plugin. 总结一下,我已经在jquery插件中包含了最新的代码。 Its now possible to develop multiple pie charts with these labels. 现在可以使用这些标签开发多个饼图。

LATEST CODE - ** http://jsfiddle.net/Qh9X5/1336/ - removes label properly on exit. 最新代码 - ** http://jsfiddle.net/Qh9X5/1336/ - 在退出时正确删除标签。

在此输入图像描述

$(document).ready(function() {


            (function( $ ){
                var methods = {
                    el: "",
                    init : function(options) {
                        var clone = jQuery.extend(true, {}, options["data"]);

                        methods.el = this;          
                        methods.setup(clone, options["width"], options["height"], options["r"], options["ir"]);
                    },
                    getArc: function(radius, innerradius){
                        var arc = d3.svg.arc()
                            .innerRadius(innerradius)
                            .outerRadius(radius);

                        return arc;
                    },
                    setup: function(dataset, w, h, r, ir){

                        var padding = 80;

                        this.width = w;
                        this.height = h;
                        this.radius = r
                        this.innerradius = ir;

                        this.color = d3.scale.category20();

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

                        this.arc = this.getArc(this.radius, this.innerradius);

                        this.svg = d3.select(methods.el["selector"]).append("svg")
                            .attr("width", this.width + padding)
                            .attr("height", this.height + padding)
                            .append("g")
                                .attr("class", "piechart")
                                .attr("transform", "translate(" + ((this.width/2) + (padding/2)) + "," + ((this.height/2) + (padding/2)) + ")");                

                        this.segments = this.svg.append("g")
                                .attr("class", "segments");

                        this.labels = this.svg.append("g")
                                .attr("class", "labels");

                        this.pointers = this.svg.append("g")
                                .attr("class", "pointers");


                    },
                    oldPieData: "",
                    pieTween: function(r, ir, d, i){
                        var that = this;

                        var theOldDataInPie = methods.oldPieData;
                        // Interpolate the arcs in data space

                        var s0;
                        var e0;

                        if(theOldDataInPie[i]){
                                s0 = theOldDataInPie[i].startAngle;
                                e0 = theOldDataInPie[i].endAngle;
                        } else if (!(theOldDataInPie[i]) && theOldDataInPie[i-1]) {
                                s0 = theOldDataInPie[i-1].endAngle;
                                e0 = theOldDataInPie[i-1].endAngle;
                        } else if(!(theOldDataInPie[i-1]) && theOldDataInPie.length > 0){
                                s0 = theOldDataInPie[theOldDataInPie.length-1].endAngle;
                                e0 = theOldDataInPie[theOldDataInPie.length-1].endAngle;
                        } else {
                                s0 = 0;
                                e0 = 0;
                        }

                        var i = d3.interpolate({startAngle: s0, endAngle: e0}, {startAngle: d.startAngle, endAngle: d.endAngle});

                        return function(t) {
                                var b = i(t);
                                return methods.getArc(r, ir)(b);
                        };
                    },
                    removePieTween: function(r, ir, d, i) {             
                        var that = this;
                        s0 = 2 * Math.PI;
                        e0 = 2 * Math.PI;
                        var i = d3.interpolate({startAngle: d.startAngle, endAngle: d.endAngle}, {startAngle: s0, endAngle: e0});

                        return function(t) {
                                var b = i(t);
                                return methods.getArc(r, ir)(b);
                        };
                    },
                    update: function(dataSet){
                        var that = this;

                        methods.el = this;
                        var r = $(methods.el["selector"]).data("r");
                        var ir = $(methods.el["selector"]).data("ir");

                        methods.svg = d3.select(methods.el["selector"] + " .piechart");

                        methods.segments = d3.select(methods.el["selector"] + " .segments");
                        methods.labels = d3.select(methods.el["selector"] + " .labels");
                        methods.pointers = d3.select(methods.el["selector"] + " .pointers");

                        dataSet.forEach(function(d) {
                            d.total = +d.value;
                        });                     

                        this.piedata = methods.pie(dataSet);

                        //__slices
                        this.path = methods.segments.selectAll("path.pie")
                            .data(this.piedata);

                        this.path.enter().append("path")
                            .attr("class", "pie")
                            .attr("fill", function(d, i) {
                                return methods.color(i); 
                            })
                            .transition()
                                .duration(300)
                                .attrTween("d", function(d, i) {
                                    return methods.pieTween(r, ir, d, i); 
                                });

                        this.path
                                .transition()
                                .duration(300)
                                .attrTween("d", function(d, i) {
                                    return methods.pieTween(r, ir, d, i); 
                                });

                        this.path.exit()
                                .transition()
                                .duration(300)
                                .attrTween("d", function(d, i) {
                                    return methods.removePieTween(r, ir, d, i); 
                                })
                                .remove();    
                        //__slices


                        //__labels  
                        var labels = methods.labels.selectAll("text")
                            .data(this.piedata);

                        labels.enter()
                            .append("text")
                            .attr("text-anchor", "middle")


                        labels
                            .attr("x", function(d) {
                                var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
                                d.cx = Math.cos(a) * (ir+((r-ir)/2));
                                return d.x = Math.cos(a) * (r + 20);
                            })
                            .attr("y", function(d) {
                                var a = d.startAngle + (d.endAngle - d.startAngle)/2 - Math.PI/2;
                                d.cy = Math.sin(a) * (ir+((r-ir)/2));
                                return d.y = Math.sin(a) * (r + 20);
                            })
                            .text(function(d) {
                                return d.data.label; 
                            })
                            .each(function(d) {
                                var bbox = this.getBBox();
                                d.sx = d.x - bbox.width/2 - 2;
                                d.ox = d.x + bbox.width/2 + 2;
                                d.sy = d.oy = d.y + 5;
                            })
                            .transition()
                                .duration(300)

                        labels
                            .transition()
                            .duration(300)      

                        labels.exit()
                            .transition()
                            .duration(300)
                        //__labels


                        //__pointers
                        methods.pointers.append("defs").append("marker")
                            .attr("id", "circ")
                            .attr("markerWidth", 6)
                            .attr("markerHeight", 6)
                            .attr("refX", 3)
                            .attr("refY", 3)
                            .append("circle")
                            .attr("cx", 3)
                            .attr("cy", 3)
                            .attr("r", 3);

                        var pointers = methods.pointers.selectAll("path.pointer")
                            .data(this.piedata);

                        pointers.enter()
                            .append("path")
                            .attr("class", "pointer")
                            .style("fill", "none")
                            .style("stroke", "black")
                            .attr("marker-end", "url(#circ)");

                        pointers
                            .attr("d", function(d) {
                                if(d.cx > d.ox) {
                                    return "M" + d.sx + "," + d.sy + "L" + d.ox + "," + d.oy + " " + d.cx + "," + d.cy;
                                } else {
                                    return "M" + d.ox + "," + d.oy + "L" + d.sx + "," + d.sy + " " + d.cx + "," + d.cy;
                                }
                            })
                            .transition()
                                .duration(300)

                        pointers
                            .transition()
                            .duration(300)      

                        pointers.exit()
                            .transition()
                            .duration(300)

                        //__pointers

                        this.oldPieData = this.piedata;

                    }
                };

                $.fn.piechart = function(methodOrOptions) {
                    if ( methods[methodOrOptions] ) {
                        return methods[ methodOrOptions ].apply( this, Array.prototype.slice.call( arguments, 1 ));
                    } else if ( typeof methodOrOptions === 'object' || ! methodOrOptions ) {
                        // Default to "init"
                        return methods.init.apply( this, arguments );
                    } else {
                        $.error( 'Method ' +  methodOrOptions + ' does not exist' );
                    }    
                };

            })(jQuery);



            var dataCharts = [
                {
                    "data": [
                        {
                            "segments": [
                                {
                                    "label": "apple",
                                    "value": 53245
                                },
                                {
                                    "label": "cherry",
                                    "value": 145
                                },
                                {
                                    "label": "pear",
                                    "value": 2245
                                },
                                {
                                    "label": "bananana",
                                    "value": 15325
                                }                           
                            ]
                        }
                    ]
                },
                {
                    "data": [
                        {
                            "segments": [
                                {
                                    "label": "milk",
                                    "value": 532
                                },
                                {
                                    "label": "cheese",
                                    "value": 145
                                },
                                {
                                    "label": "grapes",
                                    "value": 22
                                }
                            ]
                        }
                    ]
                },
                {
                    "data": [
                        {
                            "segments": [
                                {
                                    "label": "pineapple",
                                    "value": 1532
                                },
                                {
                                    "label": "orange",
                                    "value": 1435
                                },
                                {
                                    "label": "grapes",
                                    "value": 22
                                }               
                            ]
                        }
                    ]
                },
                {
                    "data": [
                        {
                            "segments": [
                                {
                                    "label": "lemons",
                                    "value": 133
                                },
                                {
                                    "label": "mango",
                                    "value": 435
                                },
                                {
                                    "label": "melon",
                                    "value": 2122
                                }               
                            ]
                        }
                    ]
                }            
            ];

            var clone = jQuery.extend(true, {}, dataCharts);

                //__invoke concentric
                $('[data-role="piechart"]').each(function(index) {
                    var selector = "piechart"+index;

                    $(this).attr("id", selector);

                    var options = {
                        data: clone[0].data,
                        width: $(this).data("width"),
                        height: $(this).data("height"),
                        r: $(this).data("r"),
                        ir: $(this).data("ir")
                    }

                    $("#"+selector).piechart(options);
                    $("#"+selector).piechart('update', clone[0].data[0].segments);
                });


            $(".testers a").on( "click", function(e) {
                e.preventDefault();

                var clone = jQuery.extend(true, {}, dataCharts);

                var min = 0;
                var max = 3;

                //__invoke pie chart
                $('[data-role="piechart"]').each(function(index) {
                    pos = Math.floor(Math.random() * (max - min + 1)) + min;
                    $("#"+$(this).attr("id")).piechart('update', clone[pos].data[0].segments);
                });

            }); 

});

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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