記得上次看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來繪制的(普通的弧線/貝塞爾曲線)!