d3.js: 在曲線路徑上添加文本標記的正確方式

最近幾天在學慣用d3.js作圖,有一點小小的心得和大家分享下。

我需要作的圖大體上如題圖所示,是若干個以有向弧連接起來的圓,弧上添加文本作為說明。

從來沒直接用過d3,就先找了一個不錯的例子作為出發點:

基本上,是利用d3內建的Force Layout,加上自定義的箭頭形狀,再把force layout預設使用的直線連接改成弧線。要沿弧線加上文字,我參考了另一個例子,具體做法並不複雜:

var edgelabels = svg.selectAll(".edgelabel") .data(dataset.edges) .enter() .append(text) .attr(...); edgelabels.append(textPath) .attr(xlink:href,function(d,i) {return #edgepath+i}) .text(function(d,i){return label +i});

就是使用text元素定義文字,然後為text元素添加textPath子元素,通過指定textPath的屬性,將文字的定位與其他路徑關聯起來。第一個例子中定義弧線路徑時並未添加id屬性,因此需要加上.attr(id, ...):

var path = svg.append(svg:g).selectAll(path) .data(force.links()) .enter().append(svg:path) .attr(class, function(d) { return link + d.type; }) .attr(...) .attr(id, function(d,i){return edgepath+i;});

這樣得到的顯示效果大約是這樣:

(請不要在意畫風的劇烈轉變——其他與本文主題無關的修改恕不詳述)

問題出在SVG文字路徑的方向性上。注意圖中上下顛倒的"68萬",因為它所從屬的有向弧是自右向左從『渠道』連向『資金』,文字也跟著上下左右顛倒了。

根據"Placing text on arcs with D3.js"一文的提示,要想讓文字的方向正過來,弧的方向需要人為的反過來。這本身並不困難,只要在指定弧的路徑的時候檢查起點和終點的x值,始終把較小的一方作為起點就好。

然而如果直接這樣做,圖中的箭頭也會反過來,圖的含義就變了。

先回顧一下第一個例子中箭頭的畫法,是定義了一個名為"end"的小三角形標記(marker),然後將這個marker指定為弧的結束標記("marker-end"屬性):

// build the arrow.svg.append("svg:defs").selectAll("marker") .data(["end"]) .enter().append("svg:marker") .attr("id", String) .attr(...) .append("svg:path") .attr("d", "M0,-5L10,0L0,5");// add the links and the arrowsvar path = svg.append("svg:g").selectAll("path") .data(force.links()) .enter().append("svg:path") .attr("class", function(d) { return "link " + d.type; }) .attr("marker-end", "url(#end)");

現在因為必須把弧反過來畫,就得在起點畫一個形狀相反的marker,來冒充原來的end marker。所以稍微修改一下代碼,增加一個新的marker:

// build the arrowsvar defs = svg.append(svg:defs);defs.append(svg:marker) .attr(id, end) .attr(...) .append(svg:path) .attr(d, M0,-5L10,0L0,5);defs.append(svg:marker) .attr(id, start) .attr(...) .append(svg:path) .attr(d, M0,0L10,-5L10,5);

因為d3的Force Layout自帶進場動畫,弧的開始和結束點在動畫進行中會一直變化,需要動態決定繪製方向和使用marker-start還是marker-end屬性,就不能再像例子中那樣直接靜態指定,而是要放到負責動畫的tick()函數中:

function tick() { path.attr(d, function(d) { var x1 = ..., y1 = ..., x2 = ..., y2 = ..., r = ...; if (x1 < x2) { return M + x1 + , + y1 + A + r + , + r + 0 0,1 + x2 + , + y2; } else { return M + x2 + , + y2 + A + r + , + r + 0 0,0 + x1 + , + y1; } }) .attr(marker-end, function(d) { if (d.source.x < d.target.x) { return url(#end); } return ; }) .attr(marker-start, function(d) { if (d.source.x >= d.target.x) { return url(#start); } return ; }); ...}

這裡的代碼看起來有點小尷尬。因為每條弧都必須在動畫過程中決定採用正常的還是逆轉的畫法,弧的頂點數據又只能在每個單獨的attr(attrName, callback)回調中才能訪問,我不得不給每條弧都指定marker-start和marker-end屬性,只是選擇性的設成實際marker或空串。另外如上所示,在使用SVG Path的A命令繪製弧時需要指定sweepFlag為0或1來獲得向外凸起的弧。

這樣得到的結果是:

很好,文字的上下左右方向正確了。但對所有偽裝成功的弧而言,文字都被標記在了弧的內側。這是因為文字默認的定位錨點(anchor point)是按基線(baseline)算起,因此始終會在弧的上方。有了上面的經驗,如法炮製,在tick()函數中加上一段:

function tick() { path.attr(d, function(d) { ... }) .attr(marker-end, function(d) { ... }) .attr(marker-start, function(d) { ... }); edgelabels.attr(dominant-baseline, function(d) { if (d.source.x < d.target.x) { return text-after-edge; } return text-before-edge; }); ...}

通過指定文字的dominant-baseline屬性(注意alignment-baseline屬性在這裡是無效的),就可以選擇文字是顯示在弧的上方或是下方。

最後得到了成品,文字始終標註在弧的外側,確保從左至右顯示:

使用到過的參考網址:

  1. Directional Force Layout Diagram with varying link opacity

  2. Graph with labeled edges

  3. SVG Arrow heads

  4. Placing text on arcs with D3.js

  5. Force Layout · mbostock/d3 Wiki · GitHub

  6. Paths - SVG | MDN

  7. Text in SVG (Font, Anchor, Alignment)

  8. How To Adjust The Baseline Alignment Of SVG Text

推薦閱讀:

如何製作 svg 格式的 圖標字體?
為什麼svg格式在前端設計中未能普及?
SVG 與 HTML5 的 canvas 各有什麼優點,哪個更有前途?

TAG:数据可视化 | D3js | SVG |