d3.js pie chart with angled/horizontal labels

2019-01-17 02:03发布

问题:

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/

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();

回答1:

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 paths for each of the data elements, using the three points computed before.
  • Add an SVG marker at the end of each path for the dot.

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

Complete demo here.



回答2:

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/

$(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);
                });

            }); 

});


回答3:

To conclude I've wrapped the very latest code for this in a jquery plugin. Its now possible to develop multiple pie charts with these labels.

LATEST CODE - ** http://jsfiddle.net/Qh9X5/1336/ - removes label properly on exit.

$(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);
                });

            }); 

});