今天遇到一個在曲線路徑上標識文本標記的問題,找到一個比較好的解決思路,在這里分享下:
使用d3建立的Force Layout,加上自定義的箭頭形狀,將多條連接線線改成弧線(https://www.cnblogs.com/webhmy/p/10906268.html)。現需沿弧線加上文字
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屬性
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萬",因為它所從屬的有向弧是自右向左從『渠道』連向『資金』,文字也跟着上下左右顛倒了。
要想讓文字的方向正過來,弧的方向需要人為的反過來。這本身並不困難,只要在指定弧的路徑的時候檢查起點和終點的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 arrows
var 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 arrows
var 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 '';
});
...
}
這樣得到的結果是

文字的上下左右方向正確了。但對所有偽裝成功的弧而言,文字都被標記在了弧的內側。這是因為文字默認的定位錨點(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';
});
...
}

總結
這里的主要思路是,文字是跟隨線的方向。如果是反向的弧線,字會顛倒,為了使文字顯示正確,這里使用了一個hack
對箭頭做了個處理,每條線都加了開始和結束的箭頭
根據數據做驅動,當源數據的x坐標小於目標數據的x坐標的時候,顯示end箭頭

當源數據的x坐標大於目標數據的x坐標的時候,顯示start箭頭

這個思路很好的解決了我的問題,因此分享下,本文轉自 https://zhuanlan.zhihu.com/p/20706807
