D3 curved labels in the center of arc

2019-04-12 01:59发布

I've been able to construct labeled donut chart just like in the following fiddle:

http://jsfiddle.net/MX7JC/9/

But now I'm trying to place the label in the middle of each arc and to span them along the arc (curve the label to follow each arc). To do that I've been thinking of putting the svg:text along svg:textPath using the d3.svg.line.radial function.

Then I stumbled upon the following fiddle:

http://jsfiddle.net/Wexcode/CrDUy/

However I'm having difficulty to tie the var arcs (the one having actual data) from the former fiddle with the var line from the latter fiddle as the latter fiddle uses the d3.range function as the data.

I've been doing trial-and-error for hours but nothing works. Does anyone know how the d3.svg.line.radial works together with the d3.svg.arc?

2条回答
我命由我不由天
2楼-- · 2019-04-12 02:26

The d3.svg.line.radial function constructs a series of cubic Bezier curves (not arcs) between multiple points in an array based on input polar coordinates (radius and angle) for each point.

(The example you linked to appears to draw a circle, but only because it breaks the circle down to many tightly spaced points -- try using 5 points instead of 50, and you'll see that the shape of the curve isn't a real circle.)

The d3.svg.arc function contstructs a shape consisting of two con-centric arcs and the straight lines connecting them, based on values for innerRadius, outerRadius, startAngle and endAngle.

Both methods specify angles in radians starting from "12 o'clock" (vertical pointing up). However, there are a couple difficulties in getting the radial line function to work with the arc data objects.

The first problem is that the line generator expects to be passed an array of multiple points, not a single object. In order to ger around that, you'll have to set the datum of the path element to be an array of the arc group's object repeated twice, once for the start and once for the end of the arc, and then use a function in i to determine whether the startAngle or the endAngle should be used for the angle value of each point.

Here's a variation of your fiddle creating those paths. I haven't bothered getting the text to run along the path, I'm just drawing the paths in black:
http://jsfiddle.net/MX7JC/688/

Now you see the second problem: if only given two points, the line generator will just create a straight line between them.

See simple curve example: http://jsfiddle.net/4VnHn/5/

In order to get any kind of curve with the default line generators, you need to add additional points to act as control points, and change the line interpolate method to an "open" option so that the end control points aren't drawn. I found that making the start and end control points 45 degrees beyond the start and end points of the curve (around the circle) created a curve that was acceptably similar to an arc in my simple example.

See better simple curve example: http://jsfiddle.net/4VnHn/6/

For your visualization, the curves generator now has to be passed the data object repeated four times in an array, and the angle accessor is now going to need a switch statement to figure out the different points: http://jsfiddle.net/MX7JC/689/

The results are acceptable for the small donut segments, but not for the ones that are wider than 45 degrees themselves -- in these cases, the control points end up so far around the around the circle that they throw off the curve completely. The curve generator doesn't know anything about the circle, it's just trying to smoothly connect the points to show a trend from one to the next.

A better solution is to actually draw an arc, using the arc notation for SVG paths. The arc generator uses arc notation, but it creates the full two-dimensional shape. To create arcs with a line generator, you're going to need a custom line interpolator function which you can then pass to the line generator's interpolate method.

The line generator will execute the custom line interpolator function, passing in an array of points that have already been converted from polar coordinates to x,y coordinates. From there you need to define the arc equation. Because an arc function also need to know the radius for the arc, I use a nested function -- the outside function accepts the radius as a parameter and returns the function that will accept the points array as a parameter:

function arcInterpolator(r) {
    //creates a line interpolator function
    //which will draw an arc of radius `r`
    //between successive polar coordinate points on the line

    return function(points) { 
    //the function must return a path definition string
    //that can be appended after a "M" command

        var allCommands = [];

        var startAngle; //save the angle of the previous point
                        //in order to allow comparisons to determine
                        //if this is large arc or not, clockwise or not

        points.forEach(function(point, i) { 

            //the points passed in by the line generator
            //will be two-element arrays of the form [x,y]
            //we also need to know the angle:        
            var angle = Math.atan2(point[0], point[1]);
            //console.log("from", startAngle, "to", angle);

            var command;

            if (i) command = ["A", //draw an arc from the previous point to this point
                        r, //x-radius
                        r, //y-radius (same as x-radius for a circular arc)
                        0, //angle of ellipse (not relevant for circular arc)
                        +(Math.abs(angle - startAngle) > Math.PI), 
                           //large arc flag,
                           //1 if the angle change is greater than 180degrees
                           // (pi radians),
                           //0 otherwise
                       +(angle < startAngle), //sweep flag, draws the arc clockwise
                       point[0], //x-coordinate of new point
                       point[1] //y-coordinate of new point
                       ];

            else command = point; //i = 0, first point of curve

            startAngle = angle;

            allCommands.push( command.join(" ") ); 
                //convert to a string and add to the command list
        });

        return allCommands.join(" ");
    };
}

Live example: http://jsfiddle.net/4VnHn/8/

To get it to work with your donut graph, I started with the version above that was producing straight lines, and changed the interpolate parameter of the line generator to use my custom function. The only additional change I had to make was to add an extra check to make sure none of the angles on the graph ended up more than 360 degrees (which I'm sure was just a rounding issue on the last arc segment, but was causing my function to draw the final arc the entire way around the circle, backwards):

var curveFunction = d3.svg.line.radial()
        .interpolate( arcInterpolator(r-45) )
        .tension(0)
        .radius(r-45)
        .angle(function(d, i) {
            return Math.min(
                i? d.endAngle : d.startAngle,
                Math.PI*2
                );
        //if i is 1 (true), this is the end of the curve,
        //if i is 0 (false), this is the start of the curve
        });

Live example: http://jsfiddle.net/MX7JC/690/

Finally, to use these curves as text paths:

  • set the curve to have no stroke and no fill;
  • give each curve a unique id value based on your data categories
    (for your example, you could use the donut label plus the data label to come up with something like "textcurve-Agg-Intl");
  • add a <textPath> element for each label;
  • set the text paths' xlink:href attribute to be # plus the same unique id value for that data
查看更多
在下西门庆
3楼-- · 2019-04-12 02:43

I thought of a different approach. It's slightly roundabout way to do it, but requires a lot less custom code.

Instead of creating a custom line interpolator to draw the arc, use a d3 arc generator to create the curve definition for an entire pie segment, and then use regular expressions to extract the curve definition for just the outside curve of the pie.

Simplified example here: http://jsfiddle.net/4VnHn/10/

Example with the donut chart here: http://jsfiddle.net/MX7JC/691/

Key code:

var textArc = d3.svg.arc().outerRadius(r-45); //to generate the arcs for the text

textCurves.attr("d",  function(d) {
    var pie = textArc(d); //get the path code for the entire pie piece

    var justArc = /[Mm][\d\.\-e,\s]+[Aa][\d\.\-e,\s]+/; 
        //regex that matches a move statement followed by an arc statement

    return justArc.exec(pie)[0]; 
        //execute regular expression and extract matched part of string
});

The r-45 is just halfway between the inner and outer radii of the donuts. The [\d\.\-e,\s]+ part of the regular expression matches digits, periods, negative signs, exponent indicators ('e'), commas or whitespace, but not any of the other letters which signify a different type of path command. I think the rest is pretty self-explanatory.

查看更多
登录 后发表回答