深入解析d3弦图

记得上次看d3应该是1年前的事情了,当时还一边看一边写了d3(v5.7)的一个学习笔记:https://www.cnblogs.com/eco-just/tag/d3/

后来转战three.js就没继续研究了(其实也是感觉api层面的东西也没有深入研究的必要,何况后续项目也不会用到这些东西)。

期间也有同行通过博客问过弦图的问题,出于种种原因吧,当时并没有深入研究。

但是今天!我们就结合d3的3.5.16版本来深入解析一下d3的弦图吧。(demo是找的简书上这为同学的笔记:https://www.jianshu.com/p/4b44c708c2da

先上效果:

step1:根据数据初始化布局

code:

// 初始数据
    var city_name = [ "北京" , "上海" , "广州" , "深圳" , "香港"  ];
    var population = [
              [ 1000,  3045 , 4567 , 1234 , 3714 ],
              [ 3214,  2000 , 2060 , 124  , 3234 ],
              [ 8761,  6545 , 3000 , 8045 , 647  ],
              [ 3211,  1067 , 3214 , 4000 , 1006 ],
              [ 2146,  1034 , 6745 , 4764 , 5000 ]
            ];

    // 弦布局初始化
    var chord_layout = d3.layout.chord()
                            .padding(0.03)
                            .sortSubgroups(d3.descending)
                            .matrix(population);

    // 获取弦布局初始化后的数据
    var groups = chord_layout.groups();
    var chords = chord_layout.chords();

解析:

population数据表格化

  北京 上海 广州 深圳 香港
北京 1000 3045 4567 1234 3714
上海 3214 2000 2060 124 3234
广州 8761 6545 3000 8045 647
深圳 3211 1067 3214 4000 1006
香港 2146 1034 6745 4764 5000

先用d3.layout.chord()这个api传入数据,初始化布局所需要的数据groups、chords;

groups数据5条,5个城市,根据population所占权重分配圆弧的大小,在上述数据上的反应就是startAngle和endAngle;

chords数据15条,5个城市选两个(source,target),根据排列组合应该是5+4+3+2+1=15种(source,target可以相同);

step2:绘制画布和计算内外圆半径

// svg画布
    var width = 600;
    var height = 600;
    var svg = d3.select(".d3content")
                .append("svg")
                .attr("width",width)
                .attr('height', height)
                .append("g")
                .attr('transform', 'translate(' + width/2 + "," + height/2 + ")");

    var color20 = d3.scale.category20();

    var innerRadius = width/2 * 0.7;
    var outerRadius = innerRadius * 1.1;

解析:

.d3content是画布依赖的根元素dom,上述代码将会在600X600的画布上绘制接下来的弦图;

step3:绘制外圆和文字

    var outer_arc = d3.svg.arc()
                        .innerRadius(innerRadius)
                        .outerRadius(outerRadius);
    //绘制外圆(5个城市)
    var g_outer = svg.append("g");
    g_outer.selectAll("path")
            .data(groups)
            .enter()
            .append("path")
            .style("fill",function(d) {
                return color20(d.index);
            })
            .style("stroke",function(d) {
                color20(d.index);
            })
            .attr("d",outer_arc)   // 此处调用了弧生成器
            ;
  //绘制文字 g_outer.selectAll(
"text") .data(groups) .enter() .append("text") .each(function(d,i) { // 对每个绑定的数据添加两个变量 d.angle = (d.startAngle + d.endAngle) / 2; d.name = city_name[i]; }) .attr("dy",".35em") .attr('transform', function(d) { // 平移属性 var result = "rotate(" + (d.angle*180/Math.PI) + ")"; result += "translate(0," + -1 * (outerRadius + 10) + ")"; if (d.angle > Math.PI * 3 / 4 && d.angle < Math.PI * 5 / 4 ) result += "rotate(180)"; return result; }) .text(function(d) { return d.name; });

效果图:

注意上述有一句代码:

.attr("d",outer_arc)   // 此处调用了弧生成器

对于每个path都会根据这个函数来绘制,而这个函数对于对应源码里的d3.svg.arc,并且这里有个隐藏的东西:

.attr("d",out_arc),第二个参数执行的时候(他是一个函数),会将数据作为实参传给他,于是到了源码里:

d3.svg.arc = function() {
    var innerRadius = d3_svg_arcInnerRadius,
     outerRadius = d3_svg_arcOuterRadius, 
     cornerRadius = d3_zero, 
     padRadius = d3_svg_arcAuto, 
     startAngle = d3_svg_arcStartAngle, 
     endAngle = d3_svg_arcEndAngle, 
     padAngle = d3_svg_arcPadAngle;
    function arc() {
      var r0 = Math.max(0, +innerRadius.apply(this, arguments)), 
      r1 = Math.max(0, +outerRadius.apply(this, arguments)), 
      a0 = startAngle.apply(this, arguments) - halfπ, 
      a1 = endAngle.apply(this, arguments) - halfπ, 
      da = Math.abs(a1 - a0), 
      cw = a0 > a1 ? 0 : 1; if (r1 < r0) rc = r1, r1 = r0, r0 = rc; if (da >= τε) return circleSegment(r1, cw) + (r0 ? circleSegment(r0, 1 - cw) : "") + "Z"; var rc, cr, rp, ap, p0 = 0, p1 = 0, x0, y0, x1, y1, x2, y2, x3, y3, path = [];   ..... }

这个arguments就是如下这样的单条数据:

最终根据传入的数据,加上一系列的逻辑处理返回了一个path节点的d属性值,具体的判断逻辑,我截图一张供各位欣赏,如果你有兴趣可以逐个去找他的函数:

最后,由这个属性值绘制了一个path节点:

后面的绘制文字就不多说了,注意一点,回调函数形参里面的d是data(数据)的意思,i是index(索引)的意思。

step4:绘制内弦chord

    // 弦生成器
    var inner_chord = d3.svg.chord()
                            .radius(innerRadius);
                            console.log(inner_chord)

    // 添加g元素,接下来在这个元素里面绘制chord
    var g_inner = svg.append("g")
                    .attr("class","chord");

    g_inner.selectAll("path")
            .data(chords)
            .enter()
            .append("path")
            .attr("d",inner_chord)  // 调用弦的路径值
            .style("fill",function(d,i) {
                return color20(d.source.index);
            })
            .style("opacity",1)
            ;

为了大家看得清晰,我把之前绘制的外圆注释了:

解析:

同理,这里通过d3.svg.chord来绘制弦,依据的数据是:

同样贴上源码的主要部分:

d3.svg.chord = function() {
    var source = d3_source, 
     target = d3_target, 
     radius = d3_svg_chordRadius, 
     startAngle = d3_svg_arcStartAngle, 
     endAngle = d3_svg_arcEndAngle;
    function chord(d, i) {
      var s = subgroup(this, source, d, i), 
      t = subgroup(this, target, d, i);
      return "M" + s.p0 + arc(s.r, s.p1, s.a1 - s.a0) + (equals(s, t) ? curve(s.r, s.p1, s.r, s.p0) : curve(s.r, s.p1, t.r, t.p0) + arc(t.r, t.p1, t.a1 - t.a0) + curve(t.r, t.p1, s.r, s.p0)) + "Z";
    }

equals(s,t)判断两个端点是否相同来决定绘制的方式;

我们看到这里绘制路径,主要用到了两个函数arc()和curve();

   function arc(r, p, a) {
      return "A" + r + "," + r + " 0 " + +(a > π) + ",1 " + p;
    }
    function curve(r0, p0, r1, p1) {
      return "Q 0,0 " + p1;
    }

关于svg的path绘制中各参数的含义,下面给一张图,这里就不多说了:

所以弦主要就是由svg内置的弧绘制api来绘制的(普通的弧线/贝塞尔曲线)!

  

猜你喜欢

转载自www.cnblogs.com/eco-just/p/11967506.html
D3