D3在弧形中心弯曲标签

时间:2014-02-11 11:32:00

标签: javascript graph svg charts d3.js

我已经能够像下面的小提琴一样构建带标签的圆环图:

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

但是现在我正在尝试将标签放在每个圆弧的中间并沿着圆弧跨越它们(将标签曲线跟随每个圆弧)。要做到这一点,我一直在考虑使用svg:text功能将svg:textPath放在d3.svg.line.radial上。

然后我偶然发现了以下小提琴:

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

然而,由于后一个小提琴使用var arcs,我很难将var line(具有实际数据的那个)与前一个小提琴的d3.range联系起来作为数据。

我已经做了几个小时的反复试验但没有任何作用。有谁知道d3.svg.line.radial如何与d3.svg.arc一起工作?

2 个答案:

答案 0 :(得分:8)

d3.svg.line.radial函数根据每个点的输入极坐标(半径和角度)在数组中的多个点之间构造一系列三次贝塞尔曲线(非弧)。

(链接到的示例显示绘制一个圆圈,但只是因为它将圆圈分解为许多紧密间隔的点 - 尝试使用5个点而不是50个,你会看到曲线的形状不是真正的圆形。)

d3.svg.arc函数根据innerRadius,outerRadius,startAngle和endAngle的值,构造一个由两个同心圆弧和连接它们的直线组成的形状。

两种方法都指定从“12点钟”开始的弧度角度(垂直向上)。但是,使径向线函数与弧数据对象一起使用会有一些困难。

第一个问题是线生成器期望传递多个点的数组,而不是单个对象。为了解决这个问题,你必须将path元素的数据设置为重复两次的弧组对象的数组,一次用于开始,一次用于弧的结束,然后使用函数i确定startAngle或endAngle是否应该用于每个点的角度值。

这是你的小提琴创造这些路径的变体。我没有费心让文字沿着路径跑,我只是用黑色绘制路径:
http://jsfiddle.net/MX7JC/688/

现在你看到了第二个问题:如果只给出两个点,则线生成器只会在它们之间创建一条直线。

参见简单的曲线示例:http://jsfiddle.net/4VnHn/5/

为了使用默认线生成器获得任何类型的曲线,您需要添加其他点作为控制点,并更改the line interpolate method to an "open" option以便不绘制结束控制点。我发现将开始和结束控制点设置在曲线的起点和终点之外45度(围绕圆圈)创建了一条曲线,该曲线在我的简单示例中与弧形相似。

请参阅更简单的曲线示例:http://jsfiddle.net/4VnHn/6/

对于可视化,曲线生成器现在必须在数组中重复四次次传递数据对象,角度访问器现在需要一个switch语句来计算不同的点:http://jsfiddle.net/MX7JC/689/

对于小甜甜圈片段而言,结果是可以接受的,但对于那些宽度超过45度的片段而言,结果是不可接受的 - 在这些情况下,控制点最终到达圆圈周围,完全脱离曲线。曲线生成器对圆圈一无所知,它只是试图平滑地连接点以显示从一个到下一个的趋势。

更好的解决方案是使用arc notation for SVG paths 实际绘制弧。电弧发生器使用弧形符号,但它创建了完整的二维形状。要使用线生成器创建弧,您将需要一个自定义线插补器函数,然后您可以将其传递给线生成器的interpolate方法。

线生成器将执行自定义线插补器功能,传入已经从极坐标转换为x,y坐标的点阵列。从那里你需要定义弧方程。因为弧函数也需要知道弧的半径,我使用嵌套函数 - 外部函数接受radius作为参数并返回将接受points数组作为参数的函数:

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

实例:http://jsfiddle.net/4VnHn/8/

为了让它与你的甜甜圈图一起使用,我开始使用上面生成直线的版本,并更改​​了线生成器的interpolate参数以使用我的自定义函数。我必须进行的唯一额外更改是添加一个额外的检查以确保图表上的任何角度都没有超过360度(我确信这只是最后一个弧段上的舍入问题,但是导致了我的功能是围绕圆圈向后绘制最终圆弧,向后):

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

实例:http://jsfiddle.net/MX7JC/690/

最后,将这些曲线用作文本路径:

  • 将曲线设置为无笔划且无填充;
  • 根据您的数据类别为每条曲线指定唯一 id值 (例如,您可以使用甜甜圈标签和数据标签来提供类似“textcurve-Agg-Intl”的内容);
  • 为每个标签添加<textPath> element;
  • 将文本路径'xlink:href属性设置为#加上该数据的唯一ID值

答案 1 :(得分:3)

我想到了一种不同的方法。这是一种略微迂回的方式,但需要很多更少的自定义代码。

使用d3弧生成器为整个饼图段创建曲线定义,然后使用正则表达式仅为饼图的外部曲线提取曲线定义,而不是创建自定义线插补器来绘制弧。

此处简化示例:http://jsfiddle.net/4VnHn/10/

此处有圆环图示例:http://jsfiddle.net/MX7JC/691/

密码:

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

r-45正好在甜甜圈的内半径和外半径之间。正则表达式的[\d\.\-e,\s]+部分匹配数字,句点,负号,指数指示符('e'),逗号或空格,但不表示任何其他表示不同类型路径命令的字母。我认为其余的都是不言自明的。