d3 force 网络中的水平 link 标签
Horizontal link labels in d3 force network
我有一个带有弯曲 links 的 d3 (v3) 力网络,如下所示:
我想要完成的是让 links 的 textPath
元素是水平的,因为它们是数字并且“81”需要看起来不同于“18”。我还想要一些白色的 shadow/outer glow/background,因为我将它们直接放在 link 上。我现在那里有一个白色的笔划,但它不能正常工作,因为有时一个数字的笔划会侵入它旁边的数字。
这里有一个可重现的例子,我承认这是从其他 SO 答案中拼凑出来的:https://jsfiddle.net/2gbekL7m/
代码的相关部分是:
var link_label = svg.selectAll(".link_label")
.data(links)
.enter()
.append("text")
.attr("class", "link_label")
.attr("paint-order", "stroke")
.attr("stroke", "white")
.attr("stroke-width", 4)
.attr("stroke-opacity", 1)
.attr("stroke-linecap", "butt")
.attr("stroke-linejoin", "miter")
.style("fill", "black")
.attr("dy", 5)
.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", function(d, i) {
return "#link_" + i;
})
.text(function(d, i) {
return d.n;
});
有谁知道如何通过固定方向和添加背景框来提高 link 标签的可读性?
首先,由于您没有 post 您的 代码,我的回答将解决您分享的 JSFiddle 中的代码。
让我们分别解决您的两个问题:
定位标签
在我看来,您想将标签放在链接的中间,并且始终水平放置,作为常规文本。在这种情况下,解决方案是删除 textPath
,这实际上会使您的选择更简单:
var link_label = svg.selectAll(".link_label")
.data(links)
.enter()
.append("text")
.text(function(d, i) {
return d.n;
});
现在只需要获取这些路径的中间位置,我们可以在 tick
函数中使用 getTotalLength()
和 getPointAtLength()
来实现:
link_label.attr("x", function(d, i) {
var pathLength = d3.select("#link_" + i).node().getTotalLength();
d.point = d3.select("#link_" + i).node().getPointAtLength(pathLength / 2);
return d.point.x
})
.attr("y", function(d) {
return d.point.y
})
这里我将值存储在 属性 中,因为 x 和 y 位置都相同。这样我们就避免了不必要的重复计算。此外,有人可能会争辩说计算应该放在 tick
函数之外,以便仅获取一次路径长度:不幸的是,这里不是这种情况,因为在模拟过程中路径长度不断变化。
文字阴影
这里有几种不同的方法。一个简单的,可能不是最漂亮的,是使用 text-shadow。这是一个简单的上下左右白色阴影:
.shadow {
text-shadow: 2px 2px 0 #fff, 2px -2px 0 #fff, -2px 2px 0 #fff, -2px -2px 0 #fff
}
CSS 适用于 Chrome 和 FireFox,但不适用于 Safari。
综上所述,这是演示:
var nodes = [{
"ix": 0
},
{
"ix": 1
},
{
"ix": 2
},
{
"ix": 3
}
];
var links = [{
"source": 0,
"target": 2,
"n": 12
},
{
"source": 0,
"target": 1,
"n": 34
},
{
"source": 1,
"target": 2,
"n": 56
},
{
"source": 1,
"target": 0,
"n": 78
},
{
"source": 0,
"target": 3,
"n": 90
}
];
var w = 400,
h = 400;
var force = d3.layout.force()
.size([w, h])
.nodes(nodes)
.links(links)
.gravity(1)
.linkDistance(30)
.charge(-20000)
.linkStrength(1);
force.start();
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 " + w + " " + h)
.append("g");
var marker = svg.selectAll("marker")
.append("svg:defs")
.data(["end-arrow"])
.enter()
.append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -3 6 6")
.attr("refX", 11.3)
.attr("refY", -0.2)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-3L6,0L0,3");
var link = svg.selectAll("line.link")
.data(links)
.enter()
.append("svg:path")
.attr("id", function(d, i) {
return "link_" + i;
})
.style("stroke", "black")
.style("stroke-width", 2)
.style("fill", "none")
.attr("marker-end", "url(#end-arrow)");
var link_label = svg.selectAll(".link_label")
.data(links)
.enter()
.append("text")
.style("text-anchor", "middle")
.style("dominant-baseline", "central")
.attr("class", "shadow")
.text(function(d, i) {
return d.n;
});
var node = svg.selectAll(".node")
.data(force.nodes())
.enter()
.append("svg:g")
.attr("class", "node");
node.append("svg:circle")
.attr("r", 10)
.style("fill", "black");
node.call(force.drag);
force.on("tick", function() {
link.attr("d", function(d) {
var dx = d.target.x - d.source.x;
var dy = d.target.y - d.source.y;
var dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x +
"," +
d.source.y +
"A" +
dr +
"," +
dr +
" 0 0,1 " +
d.target.x +
"," +
d.target.y;
});
link_label.attr("x", function(d, i) {
var pathLength = d3.select("#link_" + i).node().getTotalLength();
d.point = d3.select("#link_" + i).node().getPointAtLength(pathLength / 2);
return d.point.x
})
.attr("y", function(d) {
return d.point.y
})
node.attr("transform", function(d) {
return ("translate(" + d.x + "," + d.y + ")");
});
});
.shadow {
text-shadow: 2px 2px 0 #fff, 2px -2px 0 #fff, -2px 2px 0 #fff, -2px -2px 0 #fff
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
编辑:看看答案,白色圆圈看起来比文字阴影更漂亮。
重新使用 和 getPointAtLength()
的用法,这里是问题第二部分的基于白色圆圈的替代方案,涉及标签的 shadow/background:
var nodes = [{
"ix": 0
},
{
"ix": 1
},
{
"ix": 2
},
{
"ix": 3
}
];
var links = [{
"source": 0,
"target": 2,
"n": 12
},
{
"source": 0,
"target": 1,
"n": 34
},
{
"source": 1,
"target": 2,
"n": 56
},
{
"source": 1,
"target": 0,
"n": 78
},
{
"source": 0,
"target": 3,
"n": 90
}
];
var w = 400,
h = 400;
var force = d3.layout.force()
.size([w, h])
.nodes(nodes)
.links(links)
.gravity(1)
.linkDistance(30)
.charge(-20000)
.linkStrength(1);
force.start();
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 " + w + " " + h)
.append("g");
var marker = svg.selectAll("marker")
.append("svg:defs")
.data(["end-arrow"])
.enter()
.append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -3 6 6")
.attr("refX", 11.3)
.attr("refY", -0.2)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-3L6,0L0,3");
var link = svg.selectAll("line.link")
.data(links)
.enter()
.append("svg:path")
.attr("id", function(d, i) {
return "link_" + i;
})
.style("stroke", "black")
.style("stroke-width", 2)
.style("fill", "none")
.attr("marker-end", "url(#end-arrow)");
var link_label_shadow = svg.selectAll(".link_label_shadow")
.data(links)
.enter()
.append("circle")
.attr("r", 10)
.style("fill", "white");
var link_label = svg.selectAll(".link_label")
.data(links)
.enter()
.append("text")
.style("text-anchor", "middle")
.style("dominant-baseline", "central")
.attr("class", "shadow")
.text(function(d, i) {
return d.n;
});
var node = svg.selectAll(".node")
.data(force.nodes())
.enter()
.append("svg:g")
.attr("class", "node");
node.append("svg:circle")
.attr("r", 10)
.style("fill", "black");
node.call(force.drag);
force.on("tick", function() {
link.attr("d", function(d) {
var dx = d.target.x - d.source.x;
var dy = d.target.y - d.source.y;
var dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x +
"," +
d.source.y +
"A" +
dr +
"," +
dr +
" 0 0,1 " +
d.target.x +
"," +
d.target.y;
});
link_label.attr("x", function(d, i) {
var pathLength = d3.select("#link_" + i).node().getTotalLength();
d.point = d3.select("#link_" + i).node().getPointAtLength(pathLength / 2);
return d.point.x
})
.attr("y", function(d) {
return d.point.y
})
node.attr("transform", function(d) {
return ("translate(" + d.x + "," + d.y + ")");
});
link_label_shadow.attr("cx", function(d, i) {
var pathLength = d3.select("#link_" + i).node().getTotalLength();
d.point = d3.select("#link_" + i).node().getPointAtLength(pathLength / 2);
return d.point.x
})
.attr("cy", function(d) {
return d.point.y
})
node.attr("transform", function(d) {
return ("translate(" + d.x + "," + d.y + ")");
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
与使用 getPointAtLength()
设置标签的方式相同,我们可以在 link 和相同位置的文本标签之间添加一个白色圆圈。
由于白色圆圈是在其关联的 link 之后创建的,因此它们将位于 link 之上,因此隐藏了它们的中间部分。然后只插入标签,因此在白色圆圈上方。
我有一个带有弯曲 links 的 d3 (v3) 力网络,如下所示:
我想要完成的是让 links 的 textPath
元素是水平的,因为它们是数字并且“81”需要看起来不同于“18”。我还想要一些白色的 shadow/outer glow/background,因为我将它们直接放在 link 上。我现在那里有一个白色的笔划,但它不能正常工作,因为有时一个数字的笔划会侵入它旁边的数字。
这里有一个可重现的例子,我承认这是从其他 SO 答案中拼凑出来的:https://jsfiddle.net/2gbekL7m/
代码的相关部分是:
var link_label = svg.selectAll(".link_label")
.data(links)
.enter()
.append("text")
.attr("class", "link_label")
.attr("paint-order", "stroke")
.attr("stroke", "white")
.attr("stroke-width", 4)
.attr("stroke-opacity", 1)
.attr("stroke-linecap", "butt")
.attr("stroke-linejoin", "miter")
.style("fill", "black")
.attr("dy", 5)
.append("textPath")
.attr("startOffset", "50%")
.attr("xlink:href", function(d, i) {
return "#link_" + i;
})
.text(function(d, i) {
return d.n;
});
有谁知道如何通过固定方向和添加背景框来提高 link 标签的可读性?
首先,由于您没有 post 您的 代码,我的回答将解决您分享的 JSFiddle 中的代码。
让我们分别解决您的两个问题:
定位标签
在我看来,您想将标签放在链接的中间,并且始终水平放置,作为常规文本。在这种情况下,解决方案是删除 textPath
,这实际上会使您的选择更简单:
var link_label = svg.selectAll(".link_label")
.data(links)
.enter()
.append("text")
.text(function(d, i) {
return d.n;
});
现在只需要获取这些路径的中间位置,我们可以在 tick
函数中使用 getTotalLength()
和 getPointAtLength()
来实现:
link_label.attr("x", function(d, i) {
var pathLength = d3.select("#link_" + i).node().getTotalLength();
d.point = d3.select("#link_" + i).node().getPointAtLength(pathLength / 2);
return d.point.x
})
.attr("y", function(d) {
return d.point.y
})
这里我将值存储在 属性 中,因为 x 和 y 位置都相同。这样我们就避免了不必要的重复计算。此外,有人可能会争辩说计算应该放在 tick
函数之外,以便仅获取一次路径长度:不幸的是,这里不是这种情况,因为在模拟过程中路径长度不断变化。
文字阴影
这里有几种不同的方法。一个简单的,可能不是最漂亮的,是使用 text-shadow。这是一个简单的上下左右白色阴影:
.shadow {
text-shadow: 2px 2px 0 #fff, 2px -2px 0 #fff, -2px 2px 0 #fff, -2px -2px 0 #fff
}
CSS 适用于 Chrome 和 FireFox,但不适用于 Safari。
综上所述,这是演示:
var nodes = [{
"ix": 0
},
{
"ix": 1
},
{
"ix": 2
},
{
"ix": 3
}
];
var links = [{
"source": 0,
"target": 2,
"n": 12
},
{
"source": 0,
"target": 1,
"n": 34
},
{
"source": 1,
"target": 2,
"n": 56
},
{
"source": 1,
"target": 0,
"n": 78
},
{
"source": 0,
"target": 3,
"n": 90
}
];
var w = 400,
h = 400;
var force = d3.layout.force()
.size([w, h])
.nodes(nodes)
.links(links)
.gravity(1)
.linkDistance(30)
.charge(-20000)
.linkStrength(1);
force.start();
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 " + w + " " + h)
.append("g");
var marker = svg.selectAll("marker")
.append("svg:defs")
.data(["end-arrow"])
.enter()
.append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -3 6 6")
.attr("refX", 11.3)
.attr("refY", -0.2)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-3L6,0L0,3");
var link = svg.selectAll("line.link")
.data(links)
.enter()
.append("svg:path")
.attr("id", function(d, i) {
return "link_" + i;
})
.style("stroke", "black")
.style("stroke-width", 2)
.style("fill", "none")
.attr("marker-end", "url(#end-arrow)");
var link_label = svg.selectAll(".link_label")
.data(links)
.enter()
.append("text")
.style("text-anchor", "middle")
.style("dominant-baseline", "central")
.attr("class", "shadow")
.text(function(d, i) {
return d.n;
});
var node = svg.selectAll(".node")
.data(force.nodes())
.enter()
.append("svg:g")
.attr("class", "node");
node.append("svg:circle")
.attr("r", 10)
.style("fill", "black");
node.call(force.drag);
force.on("tick", function() {
link.attr("d", function(d) {
var dx = d.target.x - d.source.x;
var dy = d.target.y - d.source.y;
var dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x +
"," +
d.source.y +
"A" +
dr +
"," +
dr +
" 0 0,1 " +
d.target.x +
"," +
d.target.y;
});
link_label.attr("x", function(d, i) {
var pathLength = d3.select("#link_" + i).node().getTotalLength();
d.point = d3.select("#link_" + i).node().getPointAtLength(pathLength / 2);
return d.point.x
})
.attr("y", function(d) {
return d.point.y
})
node.attr("transform", function(d) {
return ("translate(" + d.x + "," + d.y + ")");
});
});
.shadow {
text-shadow: 2px 2px 0 #fff, 2px -2px 0 #fff, -2px 2px 0 #fff, -2px -2px 0 #fff
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
编辑:看看
重新使用 getPointAtLength()
的用法,这里是问题第二部分的基于白色圆圈的替代方案,涉及标签的 shadow/background:
var nodes = [{
"ix": 0
},
{
"ix": 1
},
{
"ix": 2
},
{
"ix": 3
}
];
var links = [{
"source": 0,
"target": 2,
"n": 12
},
{
"source": 0,
"target": 1,
"n": 34
},
{
"source": 1,
"target": 2,
"n": 56
},
{
"source": 1,
"target": 0,
"n": 78
},
{
"source": 0,
"target": 3,
"n": 90
}
];
var w = 400,
h = 400;
var force = d3.layout.force()
.size([w, h])
.nodes(nodes)
.links(links)
.gravity(1)
.linkDistance(30)
.charge(-20000)
.linkStrength(1);
force.start();
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.attr("preserveAspectRatio", "xMinYMin meet")
.attr("viewBox", "0 0 " + w + " " + h)
.append("g");
var marker = svg.selectAll("marker")
.append("svg:defs")
.data(["end-arrow"])
.enter()
.append("svg:marker")
.attr("id", String)
.attr("viewBox", "0 -3 6 6")
.attr("refX", 11.3)
.attr("refY", -0.2)
.attr("markerWidth", 6)
.attr("markerHeight", 6)
.attr("orient", "auto")
.append("svg:path")
.attr("d", "M0,-3L6,0L0,3");
var link = svg.selectAll("line.link")
.data(links)
.enter()
.append("svg:path")
.attr("id", function(d, i) {
return "link_" + i;
})
.style("stroke", "black")
.style("stroke-width", 2)
.style("fill", "none")
.attr("marker-end", "url(#end-arrow)");
var link_label_shadow = svg.selectAll(".link_label_shadow")
.data(links)
.enter()
.append("circle")
.attr("r", 10)
.style("fill", "white");
var link_label = svg.selectAll(".link_label")
.data(links)
.enter()
.append("text")
.style("text-anchor", "middle")
.style("dominant-baseline", "central")
.attr("class", "shadow")
.text(function(d, i) {
return d.n;
});
var node = svg.selectAll(".node")
.data(force.nodes())
.enter()
.append("svg:g")
.attr("class", "node");
node.append("svg:circle")
.attr("r", 10)
.style("fill", "black");
node.call(force.drag);
force.on("tick", function() {
link.attr("d", function(d) {
var dx = d.target.x - d.source.x;
var dy = d.target.y - d.source.y;
var dr = Math.sqrt(dx * dx + dy * dy);
return "M" +
d.source.x +
"," +
d.source.y +
"A" +
dr +
"," +
dr +
" 0 0,1 " +
d.target.x +
"," +
d.target.y;
});
link_label.attr("x", function(d, i) {
var pathLength = d3.select("#link_" + i).node().getTotalLength();
d.point = d3.select("#link_" + i).node().getPointAtLength(pathLength / 2);
return d.point.x
})
.attr("y", function(d) {
return d.point.y
})
node.attr("transform", function(d) {
return ("translate(" + d.x + "," + d.y + ")");
});
link_label_shadow.attr("cx", function(d, i) {
var pathLength = d3.select("#link_" + i).node().getTotalLength();
d.point = d3.select("#link_" + i).node().getPointAtLength(pathLength / 2);
return d.point.x
})
.attr("cy", function(d) {
return d.point.y
})
node.attr("transform", function(d) {
return ("translate(" + d.x + "," + d.y + ")");
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
与使用 getPointAtLength()
设置标签的方式相同,我们可以在 link 和相同位置的文本标签之间添加一个白色圆圈。
由于白色圆圈是在其关联的 link 之后创建的,因此它们将位于 link 之上,因此隐藏了它们的中间部分。然后只插入标签,因此在白色圆圈上方。