javascript - 当“.enter()”选择应该为空时,D3js 将重复数据附加到 SVG
问题描述
我编写了一个小应用程序,如果用户单击屏幕,将出现一个节点(圆圈)。如果用户随后从一个节点拖动到另一个节点,它们之间的边也会出现。这两个都在内部更新“nodeData”和“edgeData”数据结构。
节点似乎工作正常。如果用户点击屏幕,新节点将被添加到数据结构中,并且将调用函数“restart()”来更新可视化。然而,边缘并没有像我预期的那样工作。“restart()”函数不是更新当前边并将任何新边附加到可视化中,而是将边再次附加到可视化中,因此在“restart()”被调用几次之后,图形的边太多了比它应该的。
这是小提琴的链接: https ://jsfiddle.net/AlexMarshaall/srL3huk9/4/
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
svg {
background-color: #FFF;
cursor: default;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
-o-user-select: none;
user-select: none;
}
svg:not(.active):not(.ctrl) {
cursor: crosshair;
}
path.link {
fill: none;
stroke: #000;
stroke-width: 4px;
cursor: default;
}
svg:not(.active):not(.ctrl) path.link {
cursor: pointer;
}
path.link.selected {
stroke-dasharray: 10, 2;
}
path.link.dragline {
pointer-events: none;
}
path.link.hidden {
stroke-width: 0;
}
circle.node {
stroke-width: 1.5px;
cursor: pointer;
}
text {
font: 12px sans-serif;
pointer-events: none;
}
text.id {
text-anchor: middle;
font-weight: bold;
}
</style>
</head>
<body>
<script src="/static/d3/d3.min.js"></script>
<script>
const svg = d3.select('body')
.append('svg')
.attr('width', 900)
.attr('height', 500);
let hoverOverNode = null; // the node the mouse is currently hovering over
let mousedownNode = null; // the node that the mouse went down over
function resetMouseVars() {
mousedownNode = null;
}
var lastNodeId = 1;
var node1 = {
id: "n" + lastNodeId++,
xVal: 50,
yVal: 50
};
var node2 = {
id: "n" + lastNodeId++,
xVal: 100,
yVal: 100
}
const nodesData = [node1,node2];
const edgeData = [{
source: node1,
target: node2
}];
let paths = svg.append('svg:g').selectAll('path');
let nodes = svg.append('svg:g').selectAll('g');
const dragLine = svg.append('svg:path')
.attr('class', 'link dragline hidden')
.attr('d', 'M0,0L0,0');
function restart() {
nodes = nodes.data(nodesData, (d) => d.id); // Nodes is just the nodes to update
nodes.selectAll('g')
.style('fill', "DarkGreen");
nodes.exit().remove();
var newNodesToBeAdded = nodes.enter().append('svg:g');
newNodesToBeAdded.attr('transform', (d) => `translate(${d.xVal},${d.yVal})`)
.attr('id', (d) => d.id);
newNodesToBeAdded.append('svg:circle')
.attr('class', 'node')
.attr('r', 12)
.attr('stroke-width', 3)
.attr('stroke', 'black')
.style('fill', "DarkGreen")
.on('mouseover', function(d) {
hoverOverNode = d;
d3.select(this).attr('transform', 'scale(1.1)');
})
.on('mouseout', function(d) {
hoverOverNode = null;
d3.select(this).attr('transform', '');
})
.on('mousedown', (d) => {
if (d3.event.ctrlKey) return;
mousedownNode = d;
dragLine
.classed('hidden', false)
.attr('d', `M${mousedownNode.xVal},${mousedownNode.yVal}L${mousedownNode.xVal},${mousedownNode.yVal}`);
restart();
})
.on('mouseup', (d) => {
dragLine.classed('hidden', true);
});
newNodesToBeAdded.append('svg:text')
.attr('x', 0)
.attr('y', 4)
.attr('class', 'id')
.text((d) => d.id.substring(1));
nodes = newNodesToBeAdded.merge(nodes);
paths = paths.data(edgeData);
paths.append('svg:path')
.attr("class", "link")
.attr("d", (d) => {
const sourceX = d.source.xVal;
const sourceY = d.source.yVal;
const targetX = d.target.xVal;
const targetY = d.target.yVal;
return `M${sourceX},${sourceY}L${targetX},${targetY}`;
});
paths.exit().remove();
var newPathsToBeAdded = paths.enter().append('svg:path');
newPathsToBeAdded.attr('class','link')
.attr("d", (d) => {
const sourceX = d.source.xVal;
const sourceY = d.source.yVal;
const targetX = d.target.xVal;
const targetY = d.target.yVal;
return `M${sourceX},${sourceY}L${targetX},${targetY}`;
});
}
function mousedown() {
if (d3.event.ctrlKey || hoverOverNode != null) {
return;
}
var coords = d3.mouse(this);
var newNode = {
id: "n" + lastNodeId++,
xVal: coords[0],
yVal: coords[1]
};
nodesData.push(newNode);
restart();
}
function mousemove() {
if (!mousedownNode) return; // if there's no mousedownNode then there's no need to do anything
// update dragline
dragLine.attr('d', `M${mousedownNode.xVal},${mousedownNode.yVal}L${d3.mouse(this)[0]},${d3.mouse(this)[1]}`);
restart();
}
function mouseup() {
// if there is a mousedown node, hide the dragline that's been drawn
if (mousedownNode){
dragLine
.classed('hidden', true);
if (hoverOverNode != null){
edgeData.push({source:mousedownNode, target:hoverOverNode});
resetMouseVars();
restart();
}
}
}
// App starts here
svg.on('mousedown', mousedown)
.on('mousemove', mousemove)
.on('mouseup', mouseup);
restart()
</script>
</body>
</html>
解决方案
你有这些问题,因为你没有遵循正确的做法。
paths = paths.data(edgeData);
.data 方法返回一个更新选择,它真的不应该在调用之间保存。您应该创建一个局部变量,而不是重用路径。您有一个奇怪的行为,因为您没有将更新选择与输入选择合并 - 结果您创建的路径没有被捕获到更新选择中,并且每次相同的路径数据显示为 d3 的新数据。嗯,至少我是这么认为的。添加paths = newPathsToBeAdded.merge(paths);
应该修复它。
如果您遵循规范循环,则将来遇到此类奇怪问题的可能性较小。我会这样做:
var paths = svg.select('g.pathcnt').selectAll('path.edge'); // add classes for selection reference
paths.exit().remove();
paths.enter()
.append('path')
.classed('edge', true)
//do not duplicate things; put it after the merge to set for both new and existing nodes
.merge(paths)
.attr('d', function(d) { ... });
我不建议在调用之间保留选择;所有示例都根据需要重新选择,因此我不确定重用选择是否有效。
推荐阅读
- jquery - jQuery 如果边框颜色和边框宽度 =
- laravel - 为什么我不能在 Laravel 中创建 Mock
- sql - SQL计算一阶和二阶之间的平均时间?(根据 min 和 min+1 做 sql 计算?)
- python-3.x - 试图将许多 python 文件导入另一个
- php - 如何在 WooCommerce 中调整字段
- html - 如何以角度解析嵌套的json
- java - 客户端无法使用自签名证书连接到 REST API
- rust - 如何通过 curl 使用 actix-from-data 的映射数组发出 POST 请求?
- asp.net - 如何用我在 iis 服务器上运行的 asp.net 中的自定义值替换缓存控制安全标头值中的私有值
- jquery - jQuery - 有多个总计的总计